summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Android.mk82
-rw-r--r--AndroidManifest.xml3
-rw-r--r--apache/org/apache/commons/io/IOUtils.java (renamed from java/com/android/voicemailomtp/src/org/apache/commons/io/IOUtils.java)0
-rw-r--r--apache/org/apache/james/mime4j/BodyDescriptor.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/BodyDescriptor.java)0
-rw-r--r--apache/org/apache/james/mime4j/CloseShieldInputStream.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/CloseShieldInputStream.java)0
-rw-r--r--apache/org/apache/james/mime4j/ContentHandler.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/ContentHandler.java)0
-rw-r--r--apache/org/apache/james/mime4j/EOLConvertingInputStream.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/EOLConvertingInputStream.java)0
-rw-r--r--apache/org/apache/james/mime4j/Log.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/Log.java)0
-rw-r--r--apache/org/apache/james/mime4j/LogFactory.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/LogFactory.java)0
-rw-r--r--apache/org/apache/james/mime4j/MimeBoundaryInputStream.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/MimeBoundaryInputStream.java)0
-rw-r--r--apache/org/apache/james/mime4j/MimeStreamParser.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/MimeStreamParser.java)0
-rw-r--r--apache/org/apache/james/mime4j/RootInputStream.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/RootInputStream.java)0
-rw-r--r--apache/org/apache/james/mime4j/codec/EncoderUtil.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/codec/EncoderUtil.java)0
-rw-r--r--apache/org/apache/james/mime4j/decoder/Base64InputStream.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/Base64InputStream.java)0
-rw-r--r--apache/org/apache/james/mime4j/decoder/ByteQueue.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/ByteQueue.java)0
-rw-r--r--apache/org/apache/james/mime4j/decoder/DecoderUtil.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/DecoderUtil.java)0
-rw-r--r--apache/org/apache/james/mime4j/decoder/QuotedPrintableInputStream.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/QuotedPrintableInputStream.java)0
-rw-r--r--apache/org/apache/james/mime4j/decoder/UnboundedFifoByteBuffer.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/UnboundedFifoByteBuffer.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/AddressListField.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/AddressListField.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/ContentTransferEncodingField.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/ContentTransferEncodingField.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/ContentTypeField.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/ContentTypeField.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/DateTimeField.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DateTimeField.java)29
-rw-r--r--apache/org/apache/james/mime4j/field/DefaultFieldParser.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DefaultFieldParser.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/DelegatingFieldParser.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DelegatingFieldParser.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/Field.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/Field.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/FieldParser.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/FieldParser.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/MailboxField.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/MailboxField.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/MailboxListField.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/MailboxListField.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/UnstructuredField.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/UnstructuredField.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/Address.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Address.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/AddressList.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/AddressList.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/Builder.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Builder.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/DomainList.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/DomainList.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/Group.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Group.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/Mailbox.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Mailbox.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/MailboxList.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/MailboxList.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/NamedMailbox.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/NamedMailbox.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/parser/ASTaddr_spec.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddr_spec.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/parser/ASTaddress.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddress.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/parser/ASTaddress_list.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddress_list.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/parser/ASTangle_addr.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTangle_addr.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/parser/ASTdomain.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTdomain.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/parser/ASTgroup_body.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTgroup_body.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/parser/ASTlocal_part.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTlocal_part.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/parser/ASTmailbox.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTmailbox.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/parser/ASTname_addr.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTname_addr.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/parser/ASTphrase.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTphrase.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/parser/ASTroute.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTroute.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/parser/AddressListParser.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParser.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/parser/AddressListParser.jj (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParser.jj)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/parser/AddressListParserConstants.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserConstants.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/parser/AddressListParserTokenManager.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserTokenManager.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/parser/AddressListParserTreeConstants.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserTreeConstants.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/parser/AddressListParserVisitor.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserVisitor.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/parser/BaseNode.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/BaseNode.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/parser/JJTAddressListParserState.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/JJTAddressListParserState.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/parser/Node.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/Node.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/parser/ParseException.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ParseException.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/parser/SimpleCharStream.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/SimpleCharStream.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/parser/SimpleNode.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/SimpleNode.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/parser/Token.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/Token.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/address/parser/TokenMgrError.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/TokenMgrError.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParser.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParser.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserConstants.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserConstants.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserTokenManager.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserTokenManager.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/contenttype/parser/ParseException.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ParseException.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/contenttype/parser/SimpleCharStream.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/SimpleCharStream.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/contenttype/parser/Token.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/Token.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/contenttype/parser/TokenMgrError.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/TokenMgrError.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/datetime/DateTime.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/DateTime.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/datetime/parser/DateTimeParser.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParser.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/datetime/parser/DateTimeParserConstants.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserConstants.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/datetime/parser/DateTimeParserTokenManager.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserTokenManager.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/datetime/parser/ParseException.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/ParseException.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/datetime/parser/SimpleCharStream.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/SimpleCharStream.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/datetime/parser/Token.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/Token.java)0
-rw-r--r--apache/org/apache/james/mime4j/field/datetime/parser/TokenMgrError.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/TokenMgrError.java)0
-rw-r--r--apache/org/apache/james/mime4j/util/CharsetUtil.java (renamed from java/com/android/voicemailomtp/src/org/apache/james/mime4j/util/CharsetUtil.java)0
-rw-r--r--assets/quantum/res/drawable-hdpi/quantum_ic_block_white_24.pngbin0 -> 478 bytes
-rw-r--r--assets/quantum/res/drawable-hdpi/quantum_ic_call_made_white_24.pngbin0 -> 189 bytes
-rw-r--r--assets/quantum/res/drawable-hdpi/quantum_ic_call_missed_white_24.pngbin0 -> 215 bytes
-rw-r--r--assets/quantum/res/drawable-hdpi/quantum_ic_call_received_white_24.pngbin0 -> 189 bytes
-rw-r--r--assets/quantum/res/drawable-hdpi/quantum_ic_content_copy_grey600_24.pngbin0 -> 203 bytes
-rw-r--r--assets/quantum/res/drawable-hdpi/quantum_ic_delete_white_24.pngbin0 -> 154 bytes
-rw-r--r--assets/quantum/res/drawable-hdpi/quantum_ic_edit_grey600_24.pngbin0 -> 216 bytes
-rw-r--r--assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_content_copy_grey600_24.pngbin0 -> 203 bytes
-rw-r--r--assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_content_copy_grey600_24.pngbin0 -> 130 bytes
-rw-r--r--assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_content_copy_grey600_24.pngbin0 -> 194 bytes
-rw-r--r--assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_content_copy_grey600_24.pngbin0 -> 276 bytes
-rw-r--r--assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_content_copy_grey600_24.pngbin0 -> 339 bytes
-rw-r--r--assets/quantum/res/drawable-mdpi/quantum_ic_block_white_24.pngbin0 -> 335 bytes
-rw-r--r--assets/quantum/res/drawable-mdpi/quantum_ic_call_made_white_24.pngbin0 -> 138 bytes
-rw-r--r--assets/quantum/res/drawable-mdpi/quantum_ic_call_missed_white_24.pngbin0 -> 156 bytes
-rw-r--r--assets/quantum/res/drawable-mdpi/quantum_ic_call_received_white_24.pngbin0 -> 138 bytes
-rw-r--r--assets/quantum/res/drawable-mdpi/quantum_ic_content_copy_grey600_24.pngbin0 -> 135 bytes
-rw-r--r--assets/quantum/res/drawable-mdpi/quantum_ic_delete_white_24.pngbin0 -> 111 bytes
-rw-r--r--assets/quantum/res/drawable-mdpi/quantum_ic_edit_grey600_24.pngbin0 -> 166 bytes
-rw-r--r--assets/quantum/res/drawable-xhdpi/quantum_ic_block_white_24.pngbin0 -> 665 bytes
-rw-r--r--assets/quantum/res/drawable-xhdpi/quantum_ic_call_made_white_24.pngbin0 -> 185 bytes
-rw-r--r--assets/quantum/res/drawable-xhdpi/quantum_ic_call_missed_white_24.pngbin0 -> 210 bytes
-rw-r--r--assets/quantum/res/drawable-xhdpi/quantum_ic_call_received_white_24.pngbin0 -> 193 bytes
-rw-r--r--assets/quantum/res/drawable-xhdpi/quantum_ic_content_copy_grey600_24.pngbin0 -> 193 bytes
-rw-r--r--assets/quantum/res/drawable-xhdpi/quantum_ic_delete_white_24.pngbin0 -> 142 bytes
-rw-r--r--assets/quantum/res/drawable-xhdpi/quantum_ic_edit_grey600_24.pngbin0 -> 242 bytes
-rw-r--r--assets/quantum/res/drawable-xxhdpi/quantum_ic_block_white_24.pngbin0 -> 973 bytes
-rw-r--r--assets/quantum/res/drawable-xxhdpi/quantum_ic_call_made_white_24.pngbin0 -> 247 bytes
-rw-r--r--assets/quantum/res/drawable-xxhdpi/quantum_ic_call_missed_white_24.pngbin0 -> 291 bytes
-rw-r--r--assets/quantum/res/drawable-xxhdpi/quantum_ic_call_received_white_24.pngbin0 -> 257 bytes
-rw-r--r--assets/quantum/res/drawable-xxhdpi/quantum_ic_content_copy_grey600_24.pngbin0 -> 274 bytes
-rw-r--r--assets/quantum/res/drawable-xxhdpi/quantum_ic_delete_white_24.pngbin0 -> 177 bytes
-rw-r--r--assets/quantum/res/drawable-xxhdpi/quantum_ic_edit_grey600_24.pngbin0 -> 305 bytes
-rw-r--r--assets/quantum/res/drawable-xxxhdpi/quantum_ic_block_white_24.pngbin0 -> 1295 bytes
-rw-r--r--assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_made_white_24.pngbin0 -> 288 bytes
-rw-r--r--assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_missed_white_24.pngbin0 -> 355 bytes
-rw-r--r--assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_received_white_24.pngbin0 -> 287 bytes
-rw-r--r--assets/quantum/res/drawable-xxxhdpi/quantum_ic_content_copy_grey600_24.pngbin0 -> 340 bytes
-rw-r--r--assets/quantum/res/drawable-xxxhdpi/quantum_ic_delete_white_24.pngbin0 -> 229 bytes
-rw-r--r--assets/quantum/res/drawable-xxxhdpi/quantum_ic_edit_grey600_24.pngbin0 -> 360 bytes
-rw-r--r--java/com/android/contacts/common/ContactPhotoManager.java1
-rw-r--r--java/com/android/contacts/common/lettertiles/LetterTileDrawable.java70
-rw-r--r--java/com/android/contacts/common/list/ContactEntryListFragment.java74
-rw-r--r--java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java18
-rw-r--r--java/com/android/dialer/app/AndroidManifest.xml12
-rw-r--r--java/com/android/dialer/app/CallDetailActivity.java480
-rw-r--r--java/com/android/dialer/app/DialerApplication.java77
-rw-r--r--java/com/android/dialer/app/DialtactsActivity.java87
-rw-r--r--java/com/android/dialer/app/SpecialCharSequenceMgr.java46
-rw-r--r--java/com/android/dialer/app/calllog/CallDetailHistoryAdapter.java214
-rw-r--r--java/com/android/dialer/app/calllog/CallLogActivity.java220
-rw-r--r--java/com/android/dialer/app/calllog/CallLogAdapter.java94
-rw-r--r--java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java243
-rw-r--r--java/com/android/dialer/app/calllog/CallLogFragment.java93
-rw-r--r--java/com/android/dialer/app/calllog/CallLogListItemHelper.java11
-rw-r--r--java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java72
-rw-r--r--java/com/android/dialer/app/calllog/CallLogNotificationsQueryHelper.java (renamed from java/com/android/dialer/app/calllog/CallLogNotificationsHelper.java)83
-rw-r--r--java/com/android/dialer/app/calllog/CallLogNotificationsService.java80
-rw-r--r--java/com/android/dialer/app/calllog/CallLogReceiver.java4
-rw-r--r--java/com/android/dialer/app/calllog/DefaultVoicemailNotifier.java264
-rw-r--r--java/com/android/dialer/app/calllog/IntentProvider.java27
-rw-r--r--java/com/android/dialer/app/calllog/MissedCallNotifier.java405
-rw-r--r--java/com/android/dialer/app/calllog/PhoneAccountHandles.java41
-rw-r--r--java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java5
-rw-r--r--java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java1
-rw-r--r--java/com/android/dialer/app/calllog/VisualVoicemailCallLogFragment.java5
-rw-r--r--java/com/android/dialer/app/calllog/VoicemailQueryHandler.java25
-rw-r--r--java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipopMr1.java10
-rw-r--r--java/com/android/dialer/app/contactinfo/ContactInfoCache.java4
-rw-r--r--java/com/android/dialer/app/contactinfo/ContactPhotoLoader.java2
-rw-r--r--java/com/android/dialer/app/dialpad/DialpadFragment.java4
-rw-r--r--java/com/android/dialer/app/filterednumber/BlockedNumbersSettingsActivity.java5
-rw-r--r--java/com/android/dialer/app/legacybindings/DialerLegacyBindings.java3
-rw-r--r--java/com/android/dialer/app/legacybindings/DialerLegacyBindingsStub.java4
-rw-r--r--java/com/android/dialer/app/list/ListsFragment.java26
-rw-r--r--java/com/android/dialer/app/list/SearchFragment.java11
-rw-r--r--java/com/android/dialer/app/manifests/activities/AndroidManifest.xml12
-rw-r--r--java/com/android/dialer/app/res/drawable-xxxhdpi/search_shadow.9.pngbin0 -> 1148 bytes
-rw-r--r--java/com/android/dialer/app/res/layout/call_detail.xml32
-rw-r--r--java/com/android/dialer/app/res/layout/call_detail_footer.xml52
-rw-r--r--java/com/android/dialer/app/res/layout/call_detail_header.xml89
-rw-r--r--java/com/android/dialer/app/res/layout/call_detail_history_item.xml3
-rw-r--r--java/com/android/dialer/app/res/layout/call_log_activity.xml40
-rw-r--r--java/com/android/dialer/app/res/layout/call_log_list_item.xml3
-rw-r--r--java/com/android/dialer/app/res/menu/call_log_options.xml (renamed from java/com/android/voicemailomtp/res/xml/voicemail_settings.xml)21
-rw-r--r--java/com/android/dialer/app/res/menu/dialtacts_options.xml8
-rw-r--r--java/com/android/dialer/app/res/values/colors.xml8
-rw-r--r--java/com/android/dialer/app/res/values/dimens.xml5
-rw-r--r--java/com/android/dialer/app/res/values/strings.xml84
-rw-r--r--java/com/android/dialer/app/res/values/styles.xml5
-rw-r--r--java/com/android/dialer/app/settings/DialerSettingsActivity.java13
-rw-r--r--java/com/android/dialer/app/voicemail/VoicemailPlaybackLayout.java7
-rw-r--r--java/com/android/dialer/app/voicemail/VoicemailPlaybackPresenter.java84
-rw-r--r--java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java113
-rw-r--r--java/com/android/dialer/app/voicemail/error/VoicemailErrorMessage.java51
-rw-r--r--java/com/android/dialer/app/voicemail/error/VoicemailErrorMessageCreator.java2
-rw-r--r--java/com/android/dialer/app/voicemail/error/VoicemailStatus.java7
-rw-r--r--java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java2
-rw-r--r--java/com/android/dialer/app/voicemail/error/res/layout/voicemai_error_message_fragment.xml1
-rw-r--r--java/com/android/dialer/app/voicemail/error/res/layout/voicemail_tos_fragment.xml1
-rw-r--r--java/com/android/dialer/app/voicemail/error/res/values/strings.xml7
-rw-r--r--java/com/android/dialer/app/widget/ActionBarController.java88
-rw-r--r--java/com/android/dialer/backup/AndroidManifest.xml3
-rw-r--r--java/com/android/dialer/backup/DialerBackupAgent.java18
-rw-r--r--java/com/android/dialer/backup/DialerBackupUtils.java44
-rw-r--r--java/com/android/dialer/backup/nano/VoicemailInfo.java (renamed from java/com/android/dialer/backup/proto/VoicemailInfo.java)280
-rw-r--r--java/com/android/dialer/binary/aosp/AndroidManifest.xml116
-rw-r--r--java/com/android/dialer/binary/aosp/AospDialerApplication.java30
-rw-r--r--java/com/android/dialer/binary/aosp/AospDialerRootComponent.java30
-rw-r--r--java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java34
-rw-r--r--java/com/android/dialer/binary/common/DialerApplication.java43
-rw-r--r--java/com/android/dialer/blocking/FilteredNumbersUtil.java6
-rw-r--r--java/com/android/dialer/buildtype/BuildType.java3
-rw-r--r--java/com/android/dialer/buildtype/release/BuildTypeAccessorImpl.java (renamed from java/com/android/dialer/buildtype/dogfood/BuildTypeAccessorImpl.java)2
-rw-r--r--java/com/android/dialer/callcomposer/AndroidManifest.xml5
-rw-r--r--java/com/android/dialer/callcomposer/CallComposerActivity.java346
-rw-r--r--java/com/android/dialer/callcomposer/CallComposerFragment.java75
-rw-r--r--java/com/android/dialer/callcomposer/CallComposerPagerAdapter.java4
-rw-r--r--java/com/android/dialer/callcomposer/CameraComposerFragment.java43
-rw-r--r--java/com/android/dialer/callcomposer/GalleryComposerFragment.java44
-rw-r--r--java/com/android/dialer/callcomposer/GalleryGridAdapter.java12
-rw-r--r--java/com/android/dialer/callcomposer/GalleryGridItemData.java35
-rw-r--r--java/com/android/dialer/callcomposer/MessageComposerFragment.java8
-rw-r--r--java/com/android/dialer/callcomposer/camera/ImagePersistTask.java4
-rw-r--r--java/com/android/dialer/callcomposer/cameraui/res/layout/camera_view.xml8
-rw-r--r--java/com/android/dialer/callcomposer/res/layout/call_composer_activity.xml7
-rw-r--r--java/com/android/dialer/callcomposer/res/layout/fragment_gallery_composer.xml2
-rw-r--r--java/com/android/dialer/callcomposer/res/layout/fragment_message_composer.xml77
-rw-r--r--java/com/android/dialer/callcomposer/res/values-h260dp/values.xml19
-rw-r--r--java/com/android/dialer/callcomposer/res/values-h480dp/values.xml19
-rw-r--r--java/com/android/dialer/callcomposer/res/values-w360dp/values.xml19
-rw-r--r--java/com/android/dialer/callcomposer/res/values-w500dp/values.xml19
-rw-r--r--java/com/android/dialer/callcomposer/res/values/dimens.xml4
-rw-r--r--java/com/android/dialer/callcomposer/res/values/strings.xml6
-rw-r--r--java/com/android/dialer/callcomposer/res/values/styles.xml11
-rw-r--r--java/com/android/dialer/callcomposer/res/values/values.xml20
-rw-r--r--java/com/android/dialer/calldetails/AndroidManifest.xml31
-rw-r--r--java/com/android/dialer/calldetails/CallDetailsActivity.java130
-rw-r--r--java/com/android/dialer/calldetails/CallDetailsAdapter.java97
-rw-r--r--java/com/android/dialer/calldetails/CallDetailsEntryViewHolder.java195
-rw-r--r--java/com/android/dialer/calldetails/CallDetailsFooterViewHolder.java67
-rw-r--r--java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java125
-rw-r--r--java/com/android/dialer/calldetails/nano/CallDetailsEntries.java440
-rw-r--r--java/com/android/dialer/calldetails/res/drawable/multimedia_image_background.xml20
-rw-r--r--java/com/android/dialer/calldetails/res/layout/call_details_activity.xml37
-rw-r--r--java/com/android/dialer/calldetails/res/layout/call_details_entry.xml73
-rw-r--r--java/com/android/dialer/calldetails/res/layout/call_details_footer.xml43
-rw-r--r--java/com/android/dialer/calldetails/res/layout/contact_container.xml60
-rw-r--r--java/com/android/dialer/calldetails/res/layout/ec_data_container.xml42
-rw-r--r--java/com/android/dialer/calldetails/res/menu/call_details_menu.xml23
-rw-r--r--java/com/android/dialer/calldetails/res/values/dimens.xml40
-rw-r--r--java/com/android/dialer/calldetails/res/values/strings.xml42
-rw-r--r--java/com/android/dialer/calldetails/res/values/styles.xml48
-rw-r--r--java/com/android/dialer/callintent/nano/CallInitiationType.java29
-rw-r--r--java/com/android/dialer/calllogutils/AndroidManifest.xml16
-rw-r--r--java/com/android/dialer/calllogutils/CallEntryFormatter.java113
-rw-r--r--java/com/android/dialer/calllogutils/CallTypeHelper.java (renamed from java/com/android/dialer/app/calllog/CallTypeHelper.java)3
-rw-r--r--java/com/android/dialer/calllogutils/CallTypeIconsView.java (renamed from java/com/android/dialer/app/calllog/CallTypeIconsView.java)29
-rw-r--r--java/com/android/dialer/calllogutils/PhoneAccountUtils.java (renamed from java/com/android/dialer/app/calllog/PhoneAccountUtils.java)2
-rw-r--r--java/com/android/dialer/calllogutils/PhoneCallDetails.java (renamed from java/com/android/dialer/app/PhoneCallDetails.java)3
-rw-r--r--java/com/android/dialer/calllogutils/PhoneNumberDisplayUtil.java (renamed from java/com/android/dialer/app/calllog/PhoneNumberDisplayUtil.java)8
-rw-r--r--java/com/android/dialer/calllogutils/res/values/colors.xml24
-rw-r--r--java/com/android/dialer/calllogutils/res/values/dimens.xml19
-rw-r--r--java/com/android/dialer/calllogutils/res/values/strings.xml90
-rw-r--r--java/com/android/dialer/common/Assert.java30
-rw-r--r--java/com/android/dialer/common/AutoValue_FallibleAsyncTask_FallibleTaskResult.java79
-rw-r--r--java/com/android/dialer/common/FallibleAsyncTask.java6
-rw-r--r--java/com/android/dialer/common/PerAccountSharedPreferences.java146
-rw-r--r--java/com/android/dialer/common/proguard.flags4
-rw-r--r--java/com/android/dialer/common/res/values/config.xml4
-rw-r--r--java/com/android/dialer/constants/Constants.java14
-rw-r--r--java/com/android/dialer/database/CallLogQueryHandler.java22
-rw-r--r--java/com/android/dialer/debug/bindings/stub/DebugBindings.java (renamed from java/com/android/incallui/maps/StaticMapFactory.java)15
-rw-r--r--java/com/android/dialer/enrichedcall/AutoValue_EnrichedCallCapabilities.java76
-rw-r--r--java/com/android/dialer/enrichedcall/AutoValue_OutgoingCallComposerData.java127
-rw-r--r--java/com/android/dialer/enrichedcall/EnrichedCallCapabilities.java13
-rw-r--r--java/com/android/dialer/enrichedcall/EnrichedCallComponent.java48
-rw-r--r--java/com/android/dialer/enrichedcall/EnrichedCallManager.java131
-rw-r--r--java/com/android/dialer/enrichedcall/FuzzyPhoneNumberMatcher.java20
-rw-r--r--java/com/android/dialer/enrichedcall/OutgoingCallComposerData.java16
-rw-r--r--java/com/android/dialer/enrichedcall/Session.java7
-rw-r--r--java/com/android/dialer/enrichedcall/VideoShareSession.java20
-rw-r--r--java/com/android/dialer/enrichedcall/historyquery/HistoryQuery.java31
-rw-r--r--java/com/android/dialer/enrichedcall/historyquery/nano/HistoryResult.java203
-rw-r--r--java/com/android/dialer/enrichedcall/stub/EnrichedCallManagerStub.java (renamed from java/com/android/dialer/enrichedcall/EnrichedCallManagerStub.java)65
-rw-r--r--java/com/android/dialer/enrichedcall/stub/StubEnrichedCallModule.java (renamed from java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java)5
-rw-r--r--java/com/android/dialer/enrichedcall/videoshare/VideoShareListener.java14
-rw-r--r--java/com/android/dialer/inject/ContextModule.java (renamed from java/com/android/dialer/inject/ApplicationModule.java)18
-rw-r--r--java/com/android/dialer/inject/HasRootComponent.java (renamed from java/com/android/dialer/inject/DialerAppComponent.java)16
-rw-r--r--java/com/android/dialer/interactions/PhoneNumberInteraction.java32
-rw-r--r--java/com/android/dialer/logging/nano/ContactLookupResult.java29
-rw-r--r--java/com/android/dialer/logging/nano/ContactSource.java29
-rw-r--r--java/com/android/dialer/logging/nano/DialerImpression.java69
-rw-r--r--java/com/android/dialer/logging/nano/InteractionEvent.java2
-rw-r--r--java/com/android/dialer/logging/nano/ReportingLocation.java29
-rw-r--r--java/com/android/dialer/logging/nano/ScreenEvent.java2
-rw-r--r--java/com/android/dialer/multimedia/AutoValue_MultimediaData.java165
-rw-r--r--java/com/android/dialer/multimedia/MultimediaData.java33
-rw-r--r--java/com/android/dialer/notification/AndroidManifest.xml21
-rw-r--r--java/com/android/dialer/notification/GroupedNotificationUtil.java66
-rw-r--r--java/com/android/dialer/notification/NotificationChannelManager.java232
-rw-r--r--java/com/android/dialer/notification/res/values/ids.xml27
-rw-r--r--java/com/android/dialer/notification/res/values/strings.xml25
-rw-r--r--java/com/android/dialer/oem/AndroidManifest.xml3
-rw-r--r--java/com/android/dialer/oem/MotorolaHiddenMenuKeySequence.java153
-rw-r--r--java/com/android/dialer/oem/MotorolaUtils.java51
-rw-r--r--java/com/android/dialer/oem/res/values/motorola_config.xml64
-rw-r--r--java/com/android/dialer/p13n/inference/P13nRanking.java49
-rw-r--r--java/com/android/dialer/p13n/inference/protocol/P13nRanker.java10
-rw-r--r--java/com/android/dialer/phonenumbercache/CachedNumberLookupService.java4
-rw-r--r--java/com/android/dialer/postcall/AndroidManifest.xml28
-rw-r--r--java/com/android/dialer/postcall/PostCall.java182
-rw-r--r--java/com/android/dialer/postcall/PostCallActivity.java151
-rw-r--r--java/com/android/dialer/postcall/res/layout/post_call_activity.xml38
-rw-r--r--java/com/android/dialer/postcall/res/values/strings.xml31
-rw-r--r--java/com/android/dialer/postcall/res/values/values.xml19
-rw-r--r--java/com/android/dialer/proguard/proguard.flags6
-rw-r--r--java/com/android/dialer/proguard/proguard_base.flags74
-rw-r--r--java/com/android/dialer/proguard/proguard_release.flags24
-rw-r--r--java/com/android/dialer/shortcuts/AutoValue_DialerShortcut.java161
-rw-r--r--java/com/android/dialer/shortcuts/CallContactActivity.java16
-rw-r--r--java/com/android/dialer/shortcuts/DialerShortcut.java6
-rw-r--r--java/com/android/dialer/shortcuts/res/values/strings.xml6
-rw-r--r--java/com/android/dialer/shortcuts/res/xml/shortcuts.xml4
-rw-r--r--java/com/android/dialer/simulator/SimulatorComponent.java46
-rw-r--r--java/com/android/dialer/simulator/impl/AutoValue_SimulatorCallLog_CallEntry.java160
-rw-r--r--java/com/android/dialer/simulator/impl/AutoValue_SimulatorContacts_Contact.java231
-rw-r--r--java/com/android/dialer/simulator/impl/AutoValue_SimulatorVoicemail_Voicemail.java184
-rw-r--r--java/com/android/dialer/simulator/impl/SimulatorCallLog.java6
-rw-r--r--java/com/android/dialer/simulator/impl/SimulatorContacts.java6
-rw-r--r--java/com/android/dialer/simulator/impl/SimulatorImpl.java40
-rw-r--r--java/com/android/dialer/simulator/impl/SimulatorModule.java22
-rw-r--r--java/com/android/dialer/simulator/impl/SimulatorVoicemail.java6
-rw-r--r--java/com/android/dialer/telecom/TelecomUtil.java33
-rw-r--r--java/com/android/dialer/theme/res/drawable-hdpi/ic_block_24dp.pngbin0 -> 478 bytes
-rw-r--r--java/com/android/dialer/theme/res/drawable-hdpi/ic_call_arrow.png (renamed from java/com/android/dialer/app/res/drawable-hdpi/ic_call_arrow.png)bin538 -> 538 bytes
-rw-r--r--java/com/android/dialer/theme/res/drawable-mdpi/ic_block_24dp.pngbin0 -> 335 bytes
-rw-r--r--java/com/android/dialer/theme/res/drawable-mdpi/ic_call_arrow.png (renamed from java/com/android/dialer/app/res/drawable-mdpi/ic_call_arrow.png)bin455 -> 455 bytes
-rw-r--r--java/com/android/dialer/theme/res/drawable-xhdpi/ic_block_24dp.pngbin0 -> 665 bytes
-rw-r--r--java/com/android/dialer/theme/res/drawable-xhdpi/ic_call_arrow.png (renamed from java/com/android/dialer/app/res/drawable-xhdpi/ic_call_arrow.png)bin627 -> 627 bytes
-rw-r--r--java/com/android/dialer/theme/res/drawable-xxhdpi/ic_block_24dp.pngbin0 -> 973 bytes
-rw-r--r--java/com/android/dialer/theme/res/drawable-xxhdpi/ic_call_arrow.png (renamed from java/com/android/dialer/app/res/drawable-xxhdpi/ic_call_arrow.png)bin1203 -> 1203 bytes
-rw-r--r--java/com/android/dialer/theme/res/drawable-xxxhdpi/ic_block_24dp.pngbin0 -> 1295 bytes
-rw-r--r--java/com/android/dialer/theme/res/drawable-xxxhdpi/ic_call_arrow.png (renamed from java/com/android/dialer/app/res/drawable-xxxhdpi/ic_call_arrow.png)bin1344 -> 1344 bytes
-rw-r--r--java/com/android/dialer/theme/res/values/dimens.xml5
-rw-r--r--java/com/android/dialer/theme/res/values/styles.xml11
-rw-r--r--java/com/android/dialer/util/AndroidManifest.xml16
-rw-r--r--java/com/android/dialer/util/PermissionsUtil.java4
-rw-r--r--java/com/android/dialer/util/SettingsUtil.java9
-rw-r--r--java/com/android/dialer/util/ViewUtil.java13
-rw-r--r--java/com/android/dialer/widget/MessageFragment.java172
-rw-r--r--java/com/android/dialer/widget/res/color/dialer_tint_state.xml23
-rw-r--r--java/com/android/dialer/widget/res/layout/fragment_message.xml81
-rw-r--r--java/com/android/dialer/widget/res/layout/selectable_text_view.xml25
-rw-r--r--java/com/android/dialer/widget/res/values/dimens.xml23
-rw-r--r--java/com/android/dialer/widget/res/values/strings.xml5
-rw-r--r--java/com/android/incallui/AnswerScreenPresenter.java17
-rw-r--r--java/com/android/incallui/AnswerScreenPresenterStub.java2
-rw-r--r--java/com/android/incallui/CallButtonPresenter.java85
-rw-r--r--java/com/android/incallui/CallCardPresenter.java138
-rw-r--r--java/com/android/incallui/CallerInfoAsyncQuery.java23
-rw-r--r--java/com/android/incallui/CallerInfoUtils.java18
-rw-r--r--java/com/android/incallui/ContactInfoCache.java332
-rw-r--r--java/com/android/incallui/ExternalCallNotifier.java31
-rw-r--r--java/com/android/incallui/InCallActivity.java106
-rw-r--r--java/com/android/incallui/InCallActivityCommon.java27
-rw-r--r--java/com/android/incallui/InCallPresenter.java89
-rw-r--r--java/com/android/incallui/NotificationBroadcastReceiver.java10
-rw-r--r--java/com/android/incallui/ProximitySensor.java5
-rw-r--r--java/com/android/incallui/StatusBarNotifier.java278
-rw-r--r--java/com/android/incallui/VideoCallPresenter.java247
-rw-r--r--java/com/android/incallui/VideoPauseController.java236
-rw-r--r--java/com/android/incallui/answer/bindings/AnswerBindings.java4
-rw-r--r--java/com/android/incallui/answer/impl/AnswerFragment.java78
-rw-r--r--java/com/android/incallui/answer/impl/AnswerVideoCallScreen.java19
-rw-r--r--java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java2
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/AnswerMethodHolder.java2
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java15
-rw-r--r--java/com/android/incallui/answer/impl/hint/AndroidManifest.xml2
-rw-r--r--java/com/android/incallui/answer/impl/hint/AnswerHintFactory.java13
-rw-r--r--java/com/android/incallui/answer/impl/hint/EventPayloadLoaderImpl.java118
-rw-r--r--java/com/android/incallui/answer/impl/hint/PawAnswerHint.java (renamed from java/com/android/incallui/answer/impl/hint/EventAnswerHint.java)17
-rw-r--r--java/com/android/incallui/answer/impl/hint/PawImageLoader.java (renamed from java/com/android/incallui/answer/impl/hint/EventPayloadLoader.java)8
-rw-r--r--java/com/android/incallui/answer/impl/hint/PawImageLoaderImpl.java48
-rw-r--r--java/com/android/incallui/answer/impl/hint/PawSecretCodeListener.java (renamed from java/com/android/incallui/answer/impl/hint/EventSecretCodeListener.java)38
-rw-r--r--java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/cat_paw.webpbin0 -> 68172 bytes
-rw-r--r--java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/dog_paw.webpbin0 -> 22704 bytes
-rw-r--r--java/com/android/incallui/answer/impl/hint/res/layout/paw_hint.xml (renamed from java/com/android/incallui/answer/impl/hint/res/layout/event_hint.xml)7
-rw-r--r--java/com/android/incallui/answer/impl/proguard.flags5
-rw-r--r--java/com/android/incallui/answer/impl/res/values/dimens.xml1
-rw-r--r--java/com/android/incallui/answer/protocol/AnswerScreen.java2
-rw-r--r--java/com/android/incallui/answer/protocol/AnswerScreenDelegate.java2
-rw-r--r--java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java3
-rw-r--r--java/com/android/incallui/call/CallList.java50
-rw-r--r--java/com/android/incallui/call/DialerCall.java377
-rw-r--r--java/com/android/incallui/call/DialerCallListener.java4
-rw-r--r--java/com/android/incallui/call/InCallVideoCallCallback.java197
-rw-r--r--java/com/android/incallui/call/InCallVideoCallCallbackNotifier.java165
-rw-r--r--java/com/android/incallui/call/VideoUtils.java103
-rw-r--r--java/com/android/incallui/calllocation/CallLocation.java32
-rw-r--r--java/com/android/incallui/calllocation/CallLocationComponent.java46
-rw-r--r--java/com/android/incallui/calllocation/impl/AndroidManifest.xml26
-rw-r--r--java/com/android/incallui/calllocation/impl/AuthException.java25
-rw-r--r--java/com/android/incallui/calllocation/impl/CallLocationImpl.java67
-rw-r--r--java/com/android/incallui/calllocation/impl/CallLocationModule.java29
-rw-r--r--java/com/android/incallui/calllocation/impl/DownloadMapImageTask.java77
-rw-r--r--java/com/android/incallui/calllocation/impl/GoogleLocationSettingHelper.java123
-rw-r--r--java/com/android/incallui/calllocation/impl/HttpFetcher.java289
-rw-r--r--java/com/android/incallui/calllocation/impl/LocationFragment.java197
-rw-r--r--java/com/android/incallui/calllocation/impl/LocationHelper.java219
-rw-r--r--java/com/android/incallui/calllocation/impl/LocationPresenter.java98
-rw-r--r--java/com/android/incallui/calllocation/impl/LocationUrlBuilder.java177
-rw-r--r--java/com/android/incallui/calllocation/impl/ReverseGeocodeTask.java144
-rw-r--r--java/com/android/incallui/calllocation/impl/TrafficStatsTags.java29
-rw-r--r--java/com/android/incallui/calllocation/impl/res/layout/location_fragment.xml134
-rw-r--r--java/com/android/incallui/calllocation/impl/res/values/dimens.xml6
-rw-r--r--java/com/android/incallui/calllocation/impl/res/values/strings.xml15
-rw-r--r--java/com/android/incallui/calllocation/impl/res/values/styles.xml28
-rw-r--r--java/com/android/incallui/calllocation/stub/StubCallLocationModule.java54
-rw-r--r--java/com/android/incallui/commontheme/res/anim/blinking.xml10
-rw-r--r--java/com/android/incallui/contactgrid/BottomRow.java13
-rw-r--r--java/com/android/incallui/contactgrid/ContactGridManager.java53
-rw-r--r--java/com/android/incallui/contactgrid/TopRow.java20
-rw-r--r--java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_bottom_row.xml2
-rw-r--r--java/com/android/incallui/hold/res/layout/incall_on_hold_banner.xml1
-rw-r--r--java/com/android/incallui/incall/impl/AutoValue_MappedButtonConfig_MappingInfo.java135
-rw-r--r--java/com/android/incallui/incall/impl/FakeDragAnimation.java62
-rw-r--r--java/com/android/incallui/incall/impl/InCallFragment.java57
-rw-r--r--java/com/android/incallui/incall/impl/InCallPagerAdapter.java25
-rw-r--r--java/com/android/incallui/incall/impl/MappedButtonConfig.java6
-rw-r--r--java/com/android/incallui/incall/protocol/PrimaryCallState.java26
-rw-r--r--java/com/android/incallui/incall/protocol/PrimaryInfo.java8
-rw-r--r--java/com/android/incallui/maps/Maps.java33
-rw-r--r--java/com/android/incallui/maps/MapsComponent.java49
-rw-r--r--java/com/android/incallui/maps/StaticMapBinding.java51
-rw-r--r--java/com/android/incallui/maps/impl/AndroidManifest.xml26
-rw-r--r--java/com/android/incallui/maps/impl/MapsImpl.java40
-rw-r--r--java/com/android/incallui/maps/impl/MapsModule.java31
-rw-r--r--java/com/android/incallui/maps/impl/StaticMapFragment.java76
-rw-r--r--java/com/android/incallui/maps/impl/res/layout/static_map_fragment.xml29
-rw-r--r--java/com/android/incallui/maps/stub/StubMapsModule.java52
-rw-r--r--java/com/android/incallui/maps/testing/TestMapsModule.java40
-rw-r--r--java/com/android/incallui/res/values/strings.xml31
-rw-r--r--java/com/android/incallui/sessiondata/MultimediaFragment.java18
-rw-r--r--java/com/android/incallui/sessiondata/res/layout/fragment_composer_image.xml3
-rw-r--r--java/com/android/incallui/sessiondata/res/layout/fragment_composer_image_frag.xml8
-rw-r--r--java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image.xml8
-rw-r--r--java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image_frag.xml8
-rw-r--r--java/com/android/incallui/spam/SpamCallListListener.java25
-rw-r--r--java/com/android/incallui/video/bindings/VideoBindings.java4
-rw-r--r--java/com/android/incallui/video/impl/VideoCallFragment.java43
-rw-r--r--java/com/android/incallui/video/impl/res/layout/frag_videocall.xml11
-rw-r--r--java/com/android/incallui/video/impl/res/values/colors.xml20
-rw-r--r--java/com/android/incallui/video/protocol/VideoCallScreen.java6
-rw-r--r--java/com/android/incallui/videotech/VideoTech.java96
-rw-r--r--java/com/android/incallui/videotech/empty/EmptyVideoTech.java76
-rw-r--r--java/com/android/incallui/videotech/ims/ImsVideoCallCallback.java201
-rw-r--r--java/com/android/incallui/videotech/ims/ImsVideoTech.java212
-rw-r--r--java/com/android/incallui/videotech/rcs/RcsVideoShare.java195
-rw-r--r--java/com/android/voicemail/VoicemailClient.java60
-rw-r--r--java/com/android/voicemail/VoicemailComponent.java46
-rw-r--r--java/com/android/voicemail/impl/ActivationTask.java298
-rw-r--r--java/com/android/voicemail/impl/AndroidManifest.xml103
-rw-r--r--java/com/android/voicemail/impl/Assert.java57
-rw-r--r--java/com/android/voicemail/impl/DefaultOmtpEventHandler.java193
-rw-r--r--java/com/android/voicemail/impl/NeededForTesting.java (renamed from java/com/android/voicemailomtp/NeededForTesting.java)6
-rw-r--r--java/com/android/voicemail/impl/OmtpConstants.java239
-rw-r--r--java/com/android/voicemail/impl/OmtpEvents.java152
-rw-r--r--java/com/android/voicemail/impl/OmtpService.java63
-rw-r--r--java/com/android/voicemail/impl/OmtpVvmCarrierConfigHelper.java444
-rw-r--r--java/com/android/voicemail/impl/SubscriptionInfoHelper.java70
-rw-r--r--java/com/android/voicemail/impl/TelephonyManagerStub.java40
-rw-r--r--java/com/android/voicemail/impl/TelephonyMangerCompat.java57
-rw-r--r--java/com/android/voicemail/impl/TelephonyVvmConfigManager.java150
-rw-r--r--java/com/android/voicemail/impl/VisualVoicemailPreferences.java37
-rw-r--r--java/com/android/voicemail/impl/Voicemail.java341
-rw-r--r--java/com/android/voicemail/impl/VoicemailClientImpl.java90
-rw-r--r--java/com/android/voicemail/impl/VoicemailClientReceiver.java51
-rw-r--r--java/com/android/voicemail/impl/VoicemailModule.java41
-rw-r--r--java/com/android/voicemail/impl/VoicemailStatus.java160
-rw-r--r--java/com/android/voicemail/impl/VvmLog.java177
-rw-r--r--java/com/android/voicemail/impl/VvmPackageInstallReceiver.java65
-rw-r--r--java/com/android/voicemail/impl/VvmPhoneStateListener.java104
-rw-r--r--java/com/android/voicemail/impl/fetch/FetchVoicemailReceiver.java218
-rw-r--r--java/com/android/voicemail/impl/fetch/VoicemailFetchedCallback.java102
-rw-r--r--java/com/android/voicemail/impl/imap/ImapHelper.java693
-rw-r--r--java/com/android/voicemail/impl/imap/VoicemailPayload.java (renamed from java/com/android/voicemailomtp/imap/VoicemailPayload.java)32
-rw-r--r--java/com/android/voicemail/impl/mail/Address.java520
-rw-r--r--java/com/android/voicemail/impl/mail/AuthenticationFailedException.java (renamed from java/com/android/voicemailomtp/mail/AuthenticationFailedException.java)24
-rw-r--r--java/com/android/voicemail/impl/mail/Base64Body.java61
-rw-r--r--java/com/android/voicemail/impl/mail/Body.java (renamed from java/com/android/voicemailomtp/mail/Body.java)7
-rw-r--r--java/com/android/voicemail/impl/mail/BodyPart.java (renamed from java/com/android/voicemailomtp/mail/BodyPart.java)10
-rw-r--r--java/com/android/voicemail/impl/mail/CertificateValidationException.java (renamed from java/com/android/voicemailomtp/mail/CertificateValidationException.java)18
-rw-r--r--java/com/android/voicemail/impl/mail/FetchProfile.java79
-rw-r--r--java/com/android/voicemail/impl/mail/Fetchable.java (renamed from java/com/android/voicemailomtp/mail/Fetchable.java)9
-rw-r--r--java/com/android/voicemail/impl/mail/FixedLengthInputStream.java79
-rw-r--r--java/com/android/voicemail/impl/mail/Flag.java (renamed from java/com/android/voicemailomtp/mail/Flag.java)20
-rw-r--r--java/com/android/voicemail/impl/mail/MailTransport.java343
-rw-r--r--java/com/android/voicemail/impl/mail/MeetingInfo.java (renamed from java/com/android/voicemailomtp/mail/MeetingInfo.java)22
-rw-r--r--java/com/android/voicemail/impl/mail/Message.java146
-rw-r--r--java/com/android/voicemail/impl/mail/MessageDateComparator.java (renamed from java/com/android/voicemailomtp/mail/MessageDateComparator.java)27
-rw-r--r--java/com/android/voicemail/impl/mail/MessagingException.java143
-rw-r--r--java/com/android/voicemail/impl/mail/Multipart.java62
-rw-r--r--java/com/android/voicemail/impl/mail/PackedString.java172
-rw-r--r--java/com/android/voicemail/impl/mail/Part.java51
-rw-r--r--java/com/android/voicemail/impl/mail/PeekableInputStream.java81
-rw-r--r--java/com/android/voicemail/impl/mail/TempDirectory.java (renamed from java/com/android/voicemailomtp/mail/TempDirectory.java)29
-rw-r--r--java/com/android/voicemail/impl/mail/internet/BinaryTempFileBody.java87
-rw-r--r--java/com/android/voicemail/impl/mail/internet/MimeBodyPart.java200
-rw-r--r--java/com/android/voicemail/impl/mail/internet/MimeHeader.java158
-rw-r--r--java/com/android/voicemail/impl/mail/internet/MimeMessage.java676
-rw-r--r--java/com/android/voicemail/impl/mail/internet/MimeMultipart.java113
-rw-r--r--java/com/android/voicemail/impl/mail/internet/MimeUtility.java400
-rw-r--r--java/com/android/voicemail/impl/mail/internet/TextBody.java59
-rw-r--r--java/com/android/voicemail/impl/mail/store/ImapConnection.java400
-rw-r--r--java/com/android/voicemail/impl/mail/store/ImapFolder.java797
-rw-r--r--java/com/android/voicemail/impl/mail/store/ImapStore.java181
-rw-r--r--java/com/android/voicemail/impl/mail/store/imap/DigestMd5Utils.java335
-rw-r--r--java/com/android/voicemail/impl/mail/store/imap/ImapConstants.java138
-rw-r--r--java/com/android/voicemail/impl/mail/store/imap/ImapElement.java124
-rw-r--r--java/com/android/voicemail/impl/mail/store/imap/ImapList.java226
-rw-r--r--java/com/android/voicemail/impl/mail/store/imap/ImapMemoryLiteral.java73
-rw-r--r--java/com/android/voicemail/impl/mail/store/imap/ImapResponse.java142
-rw-r--r--java/com/android/voicemail/impl/mail/store/imap/ImapResponseParser.java424
-rw-r--r--java/com/android/voicemail/impl/mail/store/imap/ImapSimpleString.java59
-rw-r--r--java/com/android/voicemail/impl/mail/store/imap/ImapString.java179
-rw-r--r--java/com/android/voicemail/impl/mail/store/imap/ImapTempFileLiteral.java119
-rw-r--r--java/com/android/voicemail/impl/mail/store/imap/ImapUtility.java122
-rw-r--r--java/com/android/voicemail/impl/mail/utility/CountingOutputStream.java (renamed from java/com/android/voicemailomtp/mail/utility/CountingOutputStream.java)44
-rw-r--r--java/com/android/voicemail/impl/mail/utility/EOLConvertingOutputStream.java (renamed from java/com/android/voicemailomtp/mail/utility/EOLConvertingOutputStream.java)44
-rw-r--r--java/com/android/voicemail/impl/mail/utils/LogUtils.java345
-rw-r--r--java/com/android/voicemail/impl/mail/utils/Utility.java76
-rw-r--r--java/com/android/voicemail/impl/protocol/CvvmProtocol.java59
-rw-r--r--java/com/android/voicemail/impl/protocol/OmtpProtocol.java (renamed from java/com/android/voicemailomtp/protocol/OmtpProtocol.java)33
-rw-r--r--java/com/android/voicemail/impl/protocol/ProtocolHelper.java44
-rw-r--r--java/com/android/voicemail/impl/protocol/VisualVoicemailProtocol.java106
-rw-r--r--java/com/android/voicemail/impl/protocol/VisualVoicemailProtocolFactory.java (renamed from java/com/android/voicemailomtp/protocol/VisualVoicemailProtocolFactory.java)40
-rw-r--r--java/com/android/voicemail/impl/protocol/Vvm3EventHandler.java307
-rw-r--r--java/com/android/voicemail/impl/protocol/Vvm3Protocol.java305
-rw-r--r--java/com/android/voicemail/impl/protocol/Vvm3Subscriber.java334
-rw-r--r--java/com/android/voicemail/impl/res/layout/voicemail_change_pin.xml (renamed from java/com/android/voicemailomtp/res/layout/voicemail_change_pin.xml)2
-rw-r--r--java/com/android/voicemail/impl/res/values/arrays.xml (renamed from java/com/android/voicemailomtp/res/values/arrays.xml)0
-rw-r--r--java/com/android/voicemail/impl/res/values/attrs.xml (renamed from java/com/android/voicemailomtp/res/values/attrs.xml)2
-rw-r--r--java/com/android/voicemail/impl/res/values/colors.xml (renamed from java/com/android/voicemailomtp/res/values/colors.xml)0
-rw-r--r--java/com/android/voicemail/impl/res/values/config.xml (renamed from java/com/android/voicemailomtp/res/values/config.xml)0
-rw-r--r--java/com/android/voicemail/impl/res/values/dimens.xml (renamed from java/com/android/voicemailomtp/res/values/dimens.xml)0
-rw-r--r--java/com/android/voicemail/impl/res/values/ids.xml (renamed from java/com/android/voicemailomtp/res/values/ids.xml)0
-rw-r--r--java/com/android/voicemail/impl/res/values/strings.xml114
-rw-r--r--java/com/android/voicemail/impl/res/values/styles.xml (renamed from java/com/android/voicemailomtp/res/values/styles.xml)0
-rw-r--r--java/com/android/voicemail/impl/res/xml/voicemail_settings.xml47
-rw-r--r--java/com/android/voicemail/impl/res/xml/vvm_config.xml (renamed from java/com/android/voicemailomtp/res/xml/vvm_config.xml)28
-rw-r--r--java/com/android/voicemail/impl/scheduling/BaseTask.java202
-rw-r--r--java/com/android/voicemail/impl/scheduling/BlockerTask.java51
-rw-r--r--java/com/android/voicemail/impl/scheduling/MinimalIntervalPolicy.java62
-rw-r--r--java/com/android/voicemail/impl/scheduling/Policy.java (renamed from java/com/android/voicemailomtp/scheduling/Policy.java)12
-rw-r--r--java/com/android/voicemail/impl/scheduling/PostponePolicy.java (renamed from java/com/android/voicemailomtp/scheduling/PostponePolicy.java)65
-rw-r--r--java/com/android/voicemail/impl/scheduling/RetryPolicy.java111
-rw-r--r--java/com/android/voicemail/impl/scheduling/Task.java128
-rw-r--r--java/com/android/voicemail/impl/scheduling/TaskSchedulerService.java396
-rw-r--r--java/com/android/voicemail/impl/settings/VisualVoicemailSettingsUtil.java91
-rw-r--r--java/com/android/voicemail/impl/settings/VoicemailChangePinActivity.java624
-rw-r--r--java/com/android/voicemail/impl/settings/VoicemailRingtonePreference.java110
-rw-r--r--java/com/android/voicemail/impl/settings/VoicemailSettingsFragment.java202
-rw-r--r--java/com/android/voicemail/impl/sms/LegacyModeSmsHandler.java66
-rw-r--r--java/com/android/voicemail/impl/sms/OmtpCvvmMessageSender.java55
-rw-r--r--java/com/android/voicemail/impl/sms/OmtpMessageReceiver.java161
-rw-r--r--java/com/android/voicemail/impl/sms/OmtpMessageSender.java85
-rw-r--r--java/com/android/voicemail/impl/sms/OmtpStandardMessageSender.java120
-rw-r--r--java/com/android/voicemail/impl/sms/StatusMessage.java201
-rw-r--r--java/com/android/voicemail/impl/sms/StatusSmsFetcher.java162
-rw-r--r--java/com/android/voicemail/impl/sms/SyncMessage.java161
-rw-r--r--java/com/android/voicemail/impl/sms/Vvm3MessageSender.java57
-rw-r--r--java/com/android/voicemail/impl/sync/OmtpVvmSyncReceiver.java54
-rw-r--r--java/com/android/voicemail/impl/sync/OmtpVvmSyncService.java340
-rw-r--r--java/com/android/voicemail/impl/sync/SyncOneTask.java78
-rw-r--r--java/com/android/voicemail/impl/sync/SyncTask.java75
-rw-r--r--java/com/android/voicemail/impl/sync/UploadTask.java69
-rw-r--r--java/com/android/voicemail/impl/sync/VoicemailProviderChangeReceiver.java (renamed from java/com/android/voicemailomtp/sync/VoicemailProviderChangeReceiver.java)27
-rw-r--r--java/com/android/voicemail/impl/sync/VoicemailStatusQueryHelper.java113
-rw-r--r--java/com/android/voicemail/impl/sync/VoicemailsQueryHelper.java295
-rw-r--r--java/com/android/voicemail/impl/sync/VvmAccountManager.java79
-rw-r--r--java/com/android/voicemail/impl/sync/VvmNetworkRequest.java120
-rw-r--r--java/com/android/voicemail/impl/sync/VvmNetworkRequestCallback.java183
-rw-r--r--java/com/android/voicemail/impl/utils/IndentingPrintWriter.java155
-rw-r--r--java/com/android/voicemail/impl/utils/VoicemailDatabaseUtil.java85
-rw-r--r--java/com/android/voicemail/impl/utils/VvmDumpHandler.java43
-rw-r--r--java/com/android/voicemail/impl/utils/XmlUtils.java238
-rw-r--r--java/com/android/voicemail/permissions.xml (renamed from java/com/android/voicemailomtp/permissions.xml)6
-rw-r--r--java/com/android/voicemail/stub/StubVoicemailClient.java49
-rw-r--r--java/com/android/voicemail/stub/StubVoicemailModule.java33
-rw-r--r--java/com/android/voicemail/testing/TestVoicemailModule.java38
-rw-r--r--java/com/android/voicemailomtp/ActivationTask.java305
-rw-r--r--java/com/android/voicemailomtp/AndroidManifest.xml105
-rw-r--r--java/com/android/voicemailomtp/Assert.java62
-rw-r--r--java/com/android/voicemailomtp/DefaultOmtpEventHandler.java202
-rw-r--r--java/com/android/voicemailomtp/OmtpConstants.java248
-rw-r--r--java/com/android/voicemailomtp/OmtpEvents.java156
-rw-r--r--java/com/android/voicemailomtp/OmtpService.java65
-rw-r--r--java/com/android/voicemailomtp/OmtpVvmCarrierConfigHelper.java423
-rw-r--r--java/com/android/voicemailomtp/SubscriptionInfoHelper.java75
-rw-r--r--java/com/android/voicemailomtp/TelephonyManagerStub.java80
-rw-r--r--java/com/android/voicemailomtp/TelephonyVvmConfigManager.java154
-rw-r--r--java/com/android/voicemailomtp/VisualVoicemailPreferences.java143
-rw-r--r--java/com/android/voicemailomtp/Voicemail.java330
-rw-r--r--java/com/android/voicemailomtp/VoicemailStatus.java158
-rw-r--r--java/com/android/voicemailomtp/VvmLog.java179
-rw-r--r--java/com/android/voicemailomtp/VvmPackageInstallReceiver.java70
-rw-r--r--java/com/android/voicemailomtp/VvmPhoneStateListener.java103
-rw-r--r--java/com/android/voicemailomtp/fetch/FetchVoicemailReceiver.java219
-rw-r--r--java/com/android/voicemailomtp/fetch/VoicemailFetchedCallback.java101
-rw-r--r--java/com/android/voicemailomtp/imap/ImapHelper.java711
-rw-r--r--java/com/android/voicemailomtp/mail/Address.java541
-rw-r--r--java/com/android/voicemailomtp/mail/Base64Body.java62
-rw-r--r--java/com/android/voicemailomtp/mail/FetchProfile.java84
-rw-r--r--java/com/android/voicemailomtp/mail/FixedLengthInputStream.java79
-rw-r--r--java/com/android/voicemailomtp/mail/MailTransport.java344
-rw-r--r--java/com/android/voicemailomtp/mail/Message.java144
-rw-r--r--java/com/android/voicemailomtp/mail/MessagingException.java139
-rw-r--r--java/com/android/voicemailomtp/mail/Multipart.java62
-rw-r--r--java/com/android/voicemailomtp/mail/PackedString.java175
-rw-r--r--java/com/android/voicemailomtp/mail/Part.java51
-rw-r--r--java/com/android/voicemailomtp/mail/PeekableInputStream.java80
-rw-r--r--java/com/android/voicemailomtp/mail/internet/BinaryTempFileBody.java91
-rw-r--r--java/com/android/voicemailomtp/mail/internet/MimeBodyPart.java207
-rw-r--r--java/com/android/voicemailomtp/mail/internet/MimeHeader.java161
-rw-r--r--java/com/android/voicemailomtp/mail/internet/MimeMessage.java675
-rw-r--r--java/com/android/voicemailomtp/mail/internet/MimeMultipart.java112
-rw-r--r--java/com/android/voicemailomtp/mail/internet/MimeUtility.java416
-rw-r--r--java/com/android/voicemailomtp/mail/internet/TextBody.java63
-rw-r--r--java/com/android/voicemailomtp/mail/store/ImapConnection.java413
-rw-r--r--java/com/android/voicemailomtp/mail/store/ImapFolder.java784
-rw-r--r--java/com/android/voicemailomtp/mail/store/ImapStore.java176
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/DigestMd5Utils.java335
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/ImapConstants.java144
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/ImapElement.java120
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/ImapList.java235
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/ImapMemoryLiteral.java76
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/ImapResponse.java158
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/ImapResponseParser.java432
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/ImapSimpleString.java62
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/ImapString.java192
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/ImapTempFileLiteral.java123
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/ImapUtility.java125
-rw-r--r--java/com/android/voicemailomtp/mail/utils/LogUtils.java413
-rw-r--r--java/com/android/voicemailomtp/mail/utils/Utility.java80
-rw-r--r--java/com/android/voicemailomtp/protocol/CvvmProtocol.java59
-rw-r--r--java/com/android/voicemailomtp/protocol/ProtocolHelper.java43
-rw-r--r--java/com/android/voicemailomtp/protocol/VisualVoicemailProtocol.java100
-rw-r--r--java/com/android/voicemailomtp/protocol/Vvm3EventHandler.java271
-rw-r--r--java/com/android/voicemailomtp/protocol/Vvm3Protocol.java301
-rw-r--r--java/com/android/voicemailomtp/protocol/Vvm3Subscriber.java326
-rw-r--r--java/com/android/voicemailomtp/res/values/strings.xml86
-rw-r--r--java/com/android/voicemailomtp/scheduling/BaseTask.java206
-rw-r--r--java/com/android/voicemailomtp/scheduling/BlockerTask.java55
-rw-r--r--java/com/android/voicemailomtp/scheduling/MinimalIntervalPolicy.java69
-rw-r--r--java/com/android/voicemailomtp/scheduling/RetryPolicy.java117
-rw-r--r--java/com/android/voicemailomtp/scheduling/Task.java133
-rw-r--r--java/com/android/voicemailomtp/scheduling/TaskSchedulerService.java392
-rw-r--r--java/com/android/voicemailomtp/settings/VisualVoicemailSettingsUtil.java77
-rw-r--r--java/com/android/voicemailomtp/settings/VoicemailChangePinActivity.java634
-rw-r--r--java/com/android/voicemailomtp/settings/VoicemailSettingsActivity.java222
-rw-r--r--java/com/android/voicemailomtp/sms/LegacyModeSmsHandler.java67
-rw-r--r--java/com/android/voicemailomtp/sms/OmtpCvvmMessageSender.java55
-rw-r--r--java/com/android/voicemailomtp/sms/OmtpMessageReceiver.java162
-rw-r--r--java/com/android/voicemailomtp/sms/OmtpMessageSender.java89
-rw-r--r--java/com/android/voicemailomtp/sms/OmtpStandardMessageSender.java119
-rw-r--r--java/com/android/voicemailomtp/sms/StatusMessage.java209
-rw-r--r--java/com/android/voicemailomtp/sms/StatusSmsFetcher.java162
-rw-r--r--java/com/android/voicemailomtp/sms/SyncMessage.java166
-rw-r--r--java/com/android/voicemailomtp/sms/Vvm3MessageSender.java56
-rw-r--r--java/com/android/voicemailomtp/sync/OmtpVvmSourceManager.java120
-rw-r--r--java/com/android/voicemailomtp/sync/OmtpVvmSyncReceiver.java61
-rw-r--r--java/com/android/voicemailomtp/sync/OmtpVvmSyncService.java278
-rw-r--r--java/com/android/voicemailomtp/sync/SyncOneTask.java82
-rw-r--r--java/com/android/voicemailomtp/sync/SyncTask.java79
-rw-r--r--java/com/android/voicemailomtp/sync/UploadTask.java68
-rw-r--r--java/com/android/voicemailomtp/sync/VoicemailStatusQueryHelper.java113
-rw-r--r--java/com/android/voicemailomtp/sync/VoicemailsQueryHelper.java244
-rw-r--r--java/com/android/voicemailomtp/sync/VvmNetworkRequest.java118
-rw-r--r--java/com/android/voicemailomtp/sync/VvmNetworkRequestCallback.java171
-rw-r--r--java/com/android/voicemailomtp/utils/IndentingPrintWriter.java160
-rw-r--r--java/com/android/voicemailomtp/utils/VoicemailDatabaseUtil.java90
-rw-r--r--java/com/android/voicemailomtp/utils/VvmDumpHandler.java46
-rw-r--r--java/com/android/voicemailomtp/utils/XmlUtils.java245
662 files changed, 29561 insertions, 23428 deletions
diff --git a/Android.mk b/Android.mk
index 3d2a0baf3..2eddb56d2 100644
--- a/Android.mk
+++ b/Android.mk
@@ -1,8 +1,5 @@
# Local modifications:
-# * All location/maps code has been removed from the incallui.
-# * Precompiled AutoValue classes have been included.
-# * Precompiled Dagger classes have been included.
-# * All autovalue imports and annotations have been stripped.
+# * Dagger classes have been manually crafted.
# * Precompiled proto classes have been included.
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
@@ -18,10 +15,33 @@ BASE_DIR := java/com/android
# Primary dialer module sources.
SRC_DIRS := \
+ apache \
$(BASE_DIR)/contacts/common \
$(BASE_DIR)/dialer \
$(BASE_DIR)/incallui \
- $(BASE_DIR)/voicemailomtp
+ $(BASE_DIR)/voicemail
+
+# Exclude files incompatible with AOSP.
+EXCLUDE_FILES := \
+ $(BASE_DIR)/dialer/debug/bindings/impl/DebugBindings.java \
+ $(BASE_DIR)/dialer/debug/bindings/stub/DebugBindings.java \
+ $(BASE_DIR)/dialer/debug/impl/DebugConnection.java \
+ $(BASE_DIR)/dialer/debug/impl/DebugConnectionService.java \
+ $(BASE_DIR)/incallui/calllocation/impl/AuthException.java \
+ $(BASE_DIR)/incallui/calllocation/impl/CallLocationImpl.java \
+ $(BASE_DIR)/incallui/calllocation/impl/CallLocationModule.java \
+ $(BASE_DIR)/incallui/calllocation/impl/DownloadMapImageTask.java \
+ $(BASE_DIR)/incallui/calllocation/impl/GoogleLocationSettingHelper.java \
+ $(BASE_DIR)/incallui/calllocation/impl/HttpFetcher.java \
+ $(BASE_DIR)/incallui/calllocation/impl/LocationFragment.java \
+ $(BASE_DIR)/incallui/calllocation/impl/LocationHelper.java \
+ $(BASE_DIR)/incallui/calllocation/impl/LocationPresenter.java \
+ $(BASE_DIR)/incallui/calllocation/impl/LocationUrlBuilder.java \
+ $(BASE_DIR)/incallui/calllocation/impl/ReverseGeocodeTask.java \
+ $(BASE_DIR)/incallui/calllocation/impl/TrafficStatsTags.java \
+ $(BASE_DIR)/incallui/maps/impl/MapsImpl.java \
+ $(BASE_DIR)/incallui/maps/impl/MapsModule.java \
+ $(BASE_DIR)/incallui/maps/impl/StaticMapFragment.java
# All Dialers resources.
# find . -type d -name "res" | uniq | sort
@@ -35,10 +55,15 @@ RES_DIRS := \
$(BASE_DIR)/dialer/callcomposer/camera/camerafocus/res \
$(BASE_DIR)/dialer/callcomposer/cameraui/res \
$(BASE_DIR)/dialer/callcomposer/res \
+ $(BASE_DIR)/dialer/calldetails/res \
+ $(BASE_DIR)/dialer/calllogutils/res \
$(BASE_DIR)/dialer/common/res \
$(BASE_DIR)/dialer/dialpadview/res \
$(BASE_DIR)/dialer/interactions/res \
+ $(BASE_DIR)/dialer/notification/res \
+ $(BASE_DIR)/dialer/oem/res \
$(BASE_DIR)/dialer/phonenumberutil/res \
+ $(BASE_DIR)/dialer/postcall/res \
$(BASE_DIR)/dialer/shortcuts/res \
$(BASE_DIR)/dialer/theme/res \
$(BASE_DIR)/dialer/util/res \
@@ -50,6 +75,7 @@ RES_DIRS := \
$(BASE_DIR)/incallui/answer/impl/res \
$(BASE_DIR)/incallui/audioroute/res \
$(BASE_DIR)/incallui/autoresizetext/res \
+ $(BASE_DIR)/incallui/calllocation/impl/res \
$(BASE_DIR)/incallui/commontheme/res \
$(BASE_DIR)/incallui/contactgrid/res \
$(BASE_DIR)/incallui/hold/res \
@@ -58,7 +84,8 @@ RES_DIRS := \
$(BASE_DIR)/incallui/sessiondata/res \
$(BASE_DIR)/incallui/video/impl/res \
$(BASE_DIR)/incallui/wifi/res \
- $(BASE_DIR)/voicemailomtp/res
+ $(BASE_DIR)/voicemail/impl/res
+
# Dialer manifest files to merge.
# find . -type f -name "AndroidManifest.xml" | uniq | sort
@@ -68,17 +95,21 @@ DIALER_MANIFEST_FILES += \
$(BASE_DIR)/dialer/app/manifests/activities/AndroidManifest.xml \
$(BASE_DIR)/dialer/app/voicemail/error/AndroidManifest.xml \
$(BASE_DIR)/dialer/backup/AndroidManifest.xml \
+ $(BASE_DIR)/dialer/binary/aosp/AndroidManifest.xml \
$(BASE_DIR)/dialer/blocking/AndroidManifest.xml \
$(BASE_DIR)/dialer/callcomposer/AndroidManifest.xml \
$(BASE_DIR)/dialer/callcomposer/camera/AndroidManifest.xml \
$(BASE_DIR)/dialer/callcomposer/camera/camerafocus/AndroidManifest.xml \
$(BASE_DIR)/dialer/callcomposer/cameraui/AndroidManifest.xml \
+ $(BASE_DIR)/dialer/calldetails/AndroidManifest.xml \
+ $(BASE_DIR)/dialer/calllogutils/AndroidManifest.xml \
$(BASE_DIR)/dialer/common/AndroidManifest.xml \
- $(BASE_DIR)/dialer/debug/AndroidManifest.xml \
- $(BASE_DIR)/dialer/debug/impl/AndroidManifest.xml \
$(BASE_DIR)/dialer/dialpadview/AndroidManifest.xml \
$(BASE_DIR)/dialer/interactions/AndroidManifest.xml \
+ $(BASE_DIR)/dialer/notification/AndroidManifest.xml \
+ $(BASE_DIR)/dialer/oem/AndroidManifest.xml \
$(BASE_DIR)/dialer/phonenumberutil/AndroidManifest.xml \
+ $(BASE_DIR)/dialer/postcall/AndroidManifest.xml \
$(BASE_DIR)/dialer/shortcuts/AndroidManifest.xml \
$(BASE_DIR)/dialer/simulator/impl/AndroidManifest.xml \
$(BASE_DIR)/dialer/theme/AndroidManifest.xml \
@@ -99,12 +130,14 @@ DIALER_MANIFEST_FILES += \
$(BASE_DIR)/incallui/sessiondata/AndroidManifest.xml \
$(BASE_DIR)/incallui/video/impl/AndroidManifest.xml \
$(BASE_DIR)/incallui/wifi/AndroidManifest.xml \
- $(BASE_DIR)/voicemailomtp/AndroidManifest.xml
+ $(BASE_DIR)/voicemail/impl/AndroidManifest.xml
+
# Merge all manifest files.
LOCAL_FULL_LIBS_MANIFEST_FILES := \
$(addprefix $(LOCAL_PATH)/, $(DIALER_MANIFEST_FILES))
LOCAL_SRC_FILES := $(call all-java-files-under, $(SRC_DIRS))
+LOCAL_SRC_FILES := $(filter-out $(EXCLUDE_FILES),$(LOCAL_SRC_FILES))
LOCAL_RESOURCE_DIR := \
$(addprefix $(LOCAL_PATH)/, $(RES_DIRS)) \
$(support_library_root_dir)/design/res \
@@ -129,10 +162,15 @@ LOCAL_AAPT_FLAGS := \
--extra-packages com.android.dialer.callcomposer.camera \
--extra-packages com.android.dialer.callcomposer.camera.camerafocus \
--extra-packages com.android.dialer.callcomposer.cameraui \
+ --extra-packages com.android.dialer.calldetails \
+ --extra-packages com.android.dialer.calllogutils \
--extra-packages com.android.dialer.common \
--extra-packages com.android.dialer.dialpadview \
--extra-packages com.android.dialer.interactions \
+ --extra-packages com.android.dialer.notification \
+ --extra-packages com.android.dialer.oem \
--extra-packages com.android.dialer.phonenumberutil \
+ --extra-packages com.android.dialer.postcall \
--extra-packages com.android.dialer.shortcuts \
--extra-packages com.android.dialer.util \
--extra-packages com.android.dialer.voicemailstatus \
@@ -145,17 +183,23 @@ LOCAL_AAPT_FLAGS := \
--extra-packages com.android.incallui.answer.impl.hint \
--extra-packages com.android.incallui.audioroute \
--extra-packages com.android.incallui.autoresizetext \
+ --extra-packages com.android.incallui.calllocation \
+ --extra-packages com.android.incallui.calllocation.impl \
--extra-packages com.android.incallui.commontheme \
--extra-packages com.android.incallui.contactgrid \
--extra-packages com.android.incallui.hold \
--extra-packages com.android.incallui.incall.impl \
+ --extra-packages com.android.incallui.maps.impl \
--extra-packages com.android.incallui.sessiondata \
--extra-packages com.android.incallui.video \
--extra-packages com.android.incallui.video.impl \
--extra-packages com.android.incallui.wifi \
--extra-packages com.android.phone.common \
- --extra-packages com.android.voicemailomtp \
- --extra-packages com.android.voicemailomtp.settings \
+ --extra-packages com.android.voicemail \
+ --extra-packages com.android.voicemail.impl \
+ --extra-packages com.android.voicemail.impl.fetch \
+ --extra-packages com.android.voicemail.impl.settings \
+ --extra-packages com.android.voicemail.settings \
--extra-packages me.leolin.shortcutbadger
LOCAL_STATIC_JAVA_LIBRARIES := \
@@ -180,7 +224,8 @@ LOCAL_STATIC_JAVA_LIBRARIES := \
libphonenumber \
libprotobuf-java-nano \
org.apache.http.legacy.boot \
- volley
+ volley \
+ dialer-auto-value
LOCAL_JAVA_LIBRARIES := \
android-support-annotations \
@@ -194,7 +239,8 @@ LOCAL_JAVA_LIBRARIES := \
dialer-javax-inject \
dialer-libshortcutbadger \
jsr305 \
- libprotobuf-java-nano
+ libprotobuf-java-nano \
+ dialer-auto-value
# Libraries needed by the compiler (JACK) to generate code.
PROCESSOR_LIBRARIES_TARGET := \
@@ -203,15 +249,12 @@ PROCESSOR_LIBRARIES_TARGET := \
dialer-dagger2-producers \
dialer-guava \
dialer-javax-annotation-api \
- dialer-javax-inject
-
-# TODO: Include when JACK properly supports AutoValue b/35360557
-# (builders not generated successfully, javac duplicate issues) in
-# LOCAL_STATIC_JAVA_LIBRARIES, LOCAL_JAVA_LIBRARIES, PROCESSOR_LIBRARIES_TARGET
-# dialer-auto-value
+ dialer-javax-inject \
+ dialer-auto-value
# Resolve the jar paths.
PROCESSOR_JARS := $(call java-lib-deps, $(PROCESSOR_LIBRARIES_TARGET))
+# Necessary for annotation processors to work correctly.
LOCAL_ADDITIONAL_DEPENDENCIES += $(PROCESSOR_JARS)
LOCAL_JACK_FLAGS += --processorpath $(call normalize-path-list,$(PROCESSOR_JARS))
@@ -228,6 +271,7 @@ include $(BUILD_PACKAGE)
# Cleanup local state
BASE_DIR :=
SRC_DIRS :=
+EXCLUDE_FILES :=
RES_DIRS :=
DIALER_MANIFEST_FILES :=
PROCESSOR_LIBRARIES_TARGET :=
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 85ed1981c..2e42d5009 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -108,10 +108,9 @@
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher_phone"
android:label="@string/applicationLabel"
- android:name="com.android.dialer.app.DialerApplication"
+ android:name="com.android.dialer.binary.aosp.AospDialerApplication"
android:supportsRtl="true"
android:usesCleartextTraffic="false">
-
</application>
</manifest>
diff --git a/java/com/android/voicemailomtp/src/org/apache/commons/io/IOUtils.java b/apache/org/apache/commons/io/IOUtils.java
index b41450790..b41450790 100644
--- a/java/com/android/voicemailomtp/src/org/apache/commons/io/IOUtils.java
+++ b/apache/org/apache/commons/io/IOUtils.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/BodyDescriptor.java b/apache/org/apache/james/mime4j/BodyDescriptor.java
index 867c43d86..867c43d86 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/BodyDescriptor.java
+++ b/apache/org/apache/james/mime4j/BodyDescriptor.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/CloseShieldInputStream.java b/apache/org/apache/james/mime4j/CloseShieldInputStream.java
index d9f3b078a..d9f3b078a 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/CloseShieldInputStream.java
+++ b/apache/org/apache/james/mime4j/CloseShieldInputStream.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/ContentHandler.java b/apache/org/apache/james/mime4j/ContentHandler.java
index b437e739e..b437e739e 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/ContentHandler.java
+++ b/apache/org/apache/james/mime4j/ContentHandler.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/EOLConvertingInputStream.java b/apache/org/apache/james/mime4j/EOLConvertingInputStream.java
index d6ef706b2..d6ef706b2 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/EOLConvertingInputStream.java
+++ b/apache/org/apache/james/mime4j/EOLConvertingInputStream.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/Log.java b/apache/org/apache/james/mime4j/Log.java
index 5eeead5f3..5eeead5f3 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/Log.java
+++ b/apache/org/apache/james/mime4j/Log.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/LogFactory.java b/apache/org/apache/james/mime4j/LogFactory.java
index ed6e3de3d..ed6e3de3d 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/LogFactory.java
+++ b/apache/org/apache/james/mime4j/LogFactory.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/MimeBoundaryInputStream.java b/apache/org/apache/james/mime4j/MimeBoundaryInputStream.java
index c6d6f248a..c6d6f248a 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/MimeBoundaryInputStream.java
+++ b/apache/org/apache/james/mime4j/MimeBoundaryInputStream.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/MimeStreamParser.java b/apache/org/apache/james/mime4j/MimeStreamParser.java
index a8aad5a38..a8aad5a38 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/MimeStreamParser.java
+++ b/apache/org/apache/james/mime4j/MimeStreamParser.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/RootInputStream.java b/apache/org/apache/james/mime4j/RootInputStream.java
index cc8b2411c..cc8b2411c 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/RootInputStream.java
+++ b/apache/org/apache/james/mime4j/RootInputStream.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/codec/EncoderUtil.java b/apache/org/apache/james/mime4j/codec/EncoderUtil.java
index 6841bc998..6841bc998 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/codec/EncoderUtil.java
+++ b/apache/org/apache/james/mime4j/codec/EncoderUtil.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/Base64InputStream.java b/apache/org/apache/james/mime4j/decoder/Base64InputStream.java
index 77f5d7d4a..77f5d7d4a 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/Base64InputStream.java
+++ b/apache/org/apache/james/mime4j/decoder/Base64InputStream.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/ByteQueue.java b/apache/org/apache/james/mime4j/decoder/ByteQueue.java
index 6d7ccef52..6d7ccef52 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/ByteQueue.java
+++ b/apache/org/apache/james/mime4j/decoder/ByteQueue.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/DecoderUtil.java b/apache/org/apache/james/mime4j/decoder/DecoderUtil.java
index 48fe07dee..48fe07dee 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/DecoderUtil.java
+++ b/apache/org/apache/james/mime4j/decoder/DecoderUtil.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/QuotedPrintableInputStream.java b/apache/org/apache/james/mime4j/decoder/QuotedPrintableInputStream.java
index e43f398f9..e43f398f9 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/QuotedPrintableInputStream.java
+++ b/apache/org/apache/james/mime4j/decoder/QuotedPrintableInputStream.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/UnboundedFifoByteBuffer.java b/apache/org/apache/james/mime4j/decoder/UnboundedFifoByteBuffer.java
index f01194fd1..f01194fd1 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/UnboundedFifoByteBuffer.java
+++ b/apache/org/apache/james/mime4j/decoder/UnboundedFifoByteBuffer.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/AddressListField.java b/apache/org/apache/james/mime4j/field/AddressListField.java
index df9f39835..df9f39835 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/AddressListField.java
+++ b/apache/org/apache/james/mime4j/field/AddressListField.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/ContentTransferEncodingField.java b/apache/org/apache/james/mime4j/field/ContentTransferEncodingField.java
index 73d8d2339..73d8d2339 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/ContentTransferEncodingField.java
+++ b/apache/org/apache/james/mime4j/field/ContentTransferEncodingField.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/ContentTypeField.java b/apache/org/apache/james/mime4j/field/ContentTypeField.java
index ad9f7f9ac..ad9f7f9ac 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/ContentTypeField.java
+++ b/apache/org/apache/james/mime4j/field/ContentTypeField.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DateTimeField.java b/apache/org/apache/james/mime4j/field/DateTimeField.java
index 1e6c8e250..2336d99db 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DateTimeField.java
+++ b/apache/org/apache/james/mime4j/field/DateTimeField.java
@@ -21,10 +21,11 @@ package org.apache.james.mime4j.field;
//BEGIN android-changed: Stubbing out logging
-import com.android.voicemailomtp.mail.utils.LogUtils;
-
+import android.text.TextUtils;
+import java.util.regex.Pattern;
import org.apache.james.mime4j.Log;
import org.apache.james.mime4j.LogFactory;
+
//END
import org.apache.james.mime4j.field.datetime.DateTime;
import org.apache.james.mime4j.field.datetime.parser.ParseException;
@@ -35,6 +36,12 @@ public class DateTimeField extends Field {
private Date date;
private ParseException parseException;
+ //BEGIN android-changed
+ // "GMT" + "+" or "-" + 4 digits
+ private static final Pattern DATE_CLEANUP_PATTERN_WRONG_TIMEZONE =
+ Pattern.compile("GMT([-+]\\d{4})$");
+ //END android-changed
+
protected DateTimeField(String name, String body, String raw, Date date, ParseException parseException) {
super(name, body, raw);
this.date = date;
@@ -56,7 +63,7 @@ public class DateTimeField extends Field {
Date date = null;
ParseException parseException = null;
//BEGIN android-changed
- body = LogUtils.cleanUpMimeDate(body);
+ body = cleanUpMimeDate(body);
//END android-changed
try {
date = DateTime.parse(body).getDate();
@@ -70,4 +77,20 @@ public class DateTimeField extends Field {
return new DateTimeField(name, body, raw, date, parseException);
}
}
+
+ //BEGIN android-changed
+ /**
+ * Try to make a date MIME(RFC 2822/5322)-compliant.
+ *
+ * <p>It fixes: - "Thu, 10 Dec 09 15:08:08 GMT-0700" to "Thu, 10 Dec 09 15:08:08 -0700" (4 digit
+ * zone value can't be preceded by "GMT") We got a report saying eBay sends a date in this format
+ */
+ private static String cleanUpMimeDate(String date) {
+ if (TextUtils.isEmpty(date)) {
+ return date;
+ }
+ date = DATE_CLEANUP_PATTERN_WRONG_TIMEZONE.matcher(date).replaceFirst("$1");
+ return date;
+ }
+ //END android-changed
}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DefaultFieldParser.java b/apache/org/apache/james/mime4j/field/DefaultFieldParser.java
index 3695afe3e..3695afe3e 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DefaultFieldParser.java
+++ b/apache/org/apache/james/mime4j/field/DefaultFieldParser.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DelegatingFieldParser.java b/apache/org/apache/james/mime4j/field/DelegatingFieldParser.java
index 32b69ec13..32b69ec13 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DelegatingFieldParser.java
+++ b/apache/org/apache/james/mime4j/field/DelegatingFieldParser.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/Field.java b/apache/org/apache/james/mime4j/field/Field.java
index 4dea5c5cf..4dea5c5cf 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/Field.java
+++ b/apache/org/apache/james/mime4j/field/Field.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/FieldParser.java b/apache/org/apache/james/mime4j/field/FieldParser.java
index 78aaf1334..78aaf1334 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/FieldParser.java
+++ b/apache/org/apache/james/mime4j/field/FieldParser.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/MailboxField.java b/apache/org/apache/james/mime4j/field/MailboxField.java
index f15980055..f15980055 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/MailboxField.java
+++ b/apache/org/apache/james/mime4j/field/MailboxField.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/MailboxListField.java b/apache/org/apache/james/mime4j/field/MailboxListField.java
index 23378d4fa..23378d4fa 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/MailboxListField.java
+++ b/apache/org/apache/james/mime4j/field/MailboxListField.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/UnstructuredField.java b/apache/org/apache/james/mime4j/field/UnstructuredField.java
index 6084e4435..6084e4435 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/UnstructuredField.java
+++ b/apache/org/apache/james/mime4j/field/UnstructuredField.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Address.java b/apache/org/apache/james/mime4j/field/address/Address.java
index 3e24e91aa..3e24e91aa 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Address.java
+++ b/apache/org/apache/james/mime4j/field/address/Address.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/AddressList.java b/apache/org/apache/james/mime4j/field/address/AddressList.java
index 1829e79aa..1829e79aa 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/AddressList.java
+++ b/apache/org/apache/james/mime4j/field/address/AddressList.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Builder.java b/apache/org/apache/james/mime4j/field/address/Builder.java
index 3bcd15b6f..3bcd15b6f 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Builder.java
+++ b/apache/org/apache/james/mime4j/field/address/Builder.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/DomainList.java b/apache/org/apache/james/mime4j/field/address/DomainList.java
index 49b0f3be5..49b0f3be5 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/DomainList.java
+++ b/apache/org/apache/james/mime4j/field/address/DomainList.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Group.java b/apache/org/apache/james/mime4j/field/address/Group.java
index c0ab7f724..c0ab7f724 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Group.java
+++ b/apache/org/apache/james/mime4j/field/address/Group.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Mailbox.java b/apache/org/apache/james/mime4j/field/address/Mailbox.java
index 25f2548d4..25f2548d4 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Mailbox.java
+++ b/apache/org/apache/james/mime4j/field/address/Mailbox.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/MailboxList.java b/apache/org/apache/james/mime4j/field/address/MailboxList.java
index 2c9efb37f..2c9efb37f 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/MailboxList.java
+++ b/apache/org/apache/james/mime4j/field/address/MailboxList.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/NamedMailbox.java b/apache/org/apache/james/mime4j/field/address/NamedMailbox.java
index 4b8306037..4b8306037 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/NamedMailbox.java
+++ b/apache/org/apache/james/mime4j/field/address/NamedMailbox.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddr_spec.java b/apache/org/apache/james/mime4j/field/address/parser/ASTaddr_spec.java
index 4d56d000b..4d56d000b 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddr_spec.java
+++ b/apache/org/apache/james/mime4j/field/address/parser/ASTaddr_spec.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddress.java b/apache/org/apache/james/mime4j/field/address/parser/ASTaddress.java
index 47bdeda8e..47bdeda8e 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddress.java
+++ b/apache/org/apache/james/mime4j/field/address/parser/ASTaddress.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddress_list.java b/apache/org/apache/james/mime4j/field/address/parser/ASTaddress_list.java
index 737840e38..737840e38 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddress_list.java
+++ b/apache/org/apache/james/mime4j/field/address/parser/ASTaddress_list.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTangle_addr.java b/apache/org/apache/james/mime4j/field/address/parser/ASTangle_addr.java
index 8cb8f421f..8cb8f421f 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTangle_addr.java
+++ b/apache/org/apache/james/mime4j/field/address/parser/ASTangle_addr.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTdomain.java b/apache/org/apache/james/mime4j/field/address/parser/ASTdomain.java
index b52664386..b52664386 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTdomain.java
+++ b/apache/org/apache/james/mime4j/field/address/parser/ASTdomain.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTgroup_body.java b/apache/org/apache/james/mime4j/field/address/parser/ASTgroup_body.java
index f6017b9fc..f6017b9fc 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTgroup_body.java
+++ b/apache/org/apache/james/mime4j/field/address/parser/ASTgroup_body.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTlocal_part.java b/apache/org/apache/james/mime4j/field/address/parser/ASTlocal_part.java
index 5c244fa3e..5c244fa3e 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTlocal_part.java
+++ b/apache/org/apache/james/mime4j/field/address/parser/ASTlocal_part.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTmailbox.java b/apache/org/apache/james/mime4j/field/address/parser/ASTmailbox.java
index aeb469da1..aeb469da1 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTmailbox.java
+++ b/apache/org/apache/james/mime4j/field/address/parser/ASTmailbox.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTname_addr.java b/apache/org/apache/james/mime4j/field/address/parser/ASTname_addr.java
index 846c73167..846c73167 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTname_addr.java
+++ b/apache/org/apache/james/mime4j/field/address/parser/ASTname_addr.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTphrase.java b/apache/org/apache/james/mime4j/field/address/parser/ASTphrase.java
index 7d711c529..7d711c529 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTphrase.java
+++ b/apache/org/apache/james/mime4j/field/address/parser/ASTphrase.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTroute.java b/apache/org/apache/james/mime4j/field/address/parser/ASTroute.java
index 54ea11523..54ea11523 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTroute.java
+++ b/apache/org/apache/james/mime4j/field/address/parser/ASTroute.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParser.java b/apache/org/apache/james/mime4j/field/address/parser/AddressListParser.java
index 8094df0ad..8094df0ad 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParser.java
+++ b/apache/org/apache/james/mime4j/field/address/parser/AddressListParser.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParser.jj b/apache/org/apache/james/mime4j/field/address/parser/AddressListParser.jj
index c14277bc6..c14277bc6 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParser.jj
+++ b/apache/org/apache/james/mime4j/field/address/parser/AddressListParser.jj
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserConstants.java b/apache/org/apache/james/mime4j/field/address/parser/AddressListParserConstants.java
index 006a082c1..006a082c1 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserConstants.java
+++ b/apache/org/apache/james/mime4j/field/address/parser/AddressListParserConstants.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserTokenManager.java b/apache/org/apache/james/mime4j/field/address/parser/AddressListParserTokenManager.java
index d2dd88dd3..d2dd88dd3 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserTokenManager.java
+++ b/apache/org/apache/james/mime4j/field/address/parser/AddressListParserTokenManager.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserTreeConstants.java b/apache/org/apache/james/mime4j/field/address/parser/AddressListParserTreeConstants.java
index 5987f19d8..5987f19d8 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserTreeConstants.java
+++ b/apache/org/apache/james/mime4j/field/address/parser/AddressListParserTreeConstants.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserVisitor.java b/apache/org/apache/james/mime4j/field/address/parser/AddressListParserVisitor.java
index 8ec2fe7d2..8ec2fe7d2 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserVisitor.java
+++ b/apache/org/apache/james/mime4j/field/address/parser/AddressListParserVisitor.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/BaseNode.java b/apache/org/apache/james/mime4j/field/address/parser/BaseNode.java
index 780974616..780974616 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/BaseNode.java
+++ b/apache/org/apache/james/mime4j/field/address/parser/BaseNode.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/JJTAddressListParserState.java b/apache/org/apache/james/mime4j/field/address/parser/JJTAddressListParserState.java
index 08b5c5bef..08b5c5bef 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/JJTAddressListParserState.java
+++ b/apache/org/apache/james/mime4j/field/address/parser/JJTAddressListParserState.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/Node.java b/apache/org/apache/james/mime4j/field/address/parser/Node.java
index 158892016..158892016 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/Node.java
+++ b/apache/org/apache/james/mime4j/field/address/parser/Node.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ParseException.java b/apache/org/apache/james/mime4j/field/address/parser/ParseException.java
index e20146fb6..e20146fb6 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ParseException.java
+++ b/apache/org/apache/james/mime4j/field/address/parser/ParseException.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/SimpleCharStream.java b/apache/org/apache/james/mime4j/field/address/parser/SimpleCharStream.java
index c9ba0b444..c9ba0b444 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/SimpleCharStream.java
+++ b/apache/org/apache/james/mime4j/field/address/parser/SimpleCharStream.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/SimpleNode.java b/apache/org/apache/james/mime4j/field/address/parser/SimpleNode.java
index 9bf537e60..9bf537e60 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/SimpleNode.java
+++ b/apache/org/apache/james/mime4j/field/address/parser/SimpleNode.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/Token.java b/apache/org/apache/james/mime4j/field/address/parser/Token.java
index 2382e8e92..2382e8e92 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/Token.java
+++ b/apache/org/apache/james/mime4j/field/address/parser/Token.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/TokenMgrError.java b/apache/org/apache/james/mime4j/field/address/parser/TokenMgrError.java
index 0299c8523..0299c8523 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/TokenMgrError.java
+++ b/apache/org/apache/james/mime4j/field/address/parser/TokenMgrError.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParser.java b/apache/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParser.java
index cacf3af21..cacf3af21 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParser.java
+++ b/apache/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParser.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserConstants.java b/apache/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserConstants.java
index d933d800d..d933d800d 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserConstants.java
+++ b/apache/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserConstants.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserTokenManager.java b/apache/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserTokenManager.java
index 25b7abafa..25b7abafa 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserTokenManager.java
+++ b/apache/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserTokenManager.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ParseException.java b/apache/org/apache/james/mime4j/field/contenttype/parser/ParseException.java
index d9b69b25c..d9b69b25c 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ParseException.java
+++ b/apache/org/apache/james/mime4j/field/contenttype/parser/ParseException.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/SimpleCharStream.java b/apache/org/apache/james/mime4j/field/contenttype/parser/SimpleCharStream.java
index ae035b717..ae035b717 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/SimpleCharStream.java
+++ b/apache/org/apache/james/mime4j/field/contenttype/parser/SimpleCharStream.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/Token.java b/apache/org/apache/james/mime4j/field/contenttype/parser/Token.java
index 34e65eec0..34e65eec0 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/Token.java
+++ b/apache/org/apache/james/mime4j/field/contenttype/parser/Token.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/TokenMgrError.java b/apache/org/apache/james/mime4j/field/contenttype/parser/TokenMgrError.java
index ea5a7826e..ea5a7826e 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/TokenMgrError.java
+++ b/apache/org/apache/james/mime4j/field/contenttype/parser/TokenMgrError.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/DateTime.java b/apache/org/apache/james/mime4j/field/datetime/DateTime.java
index 506ff54e5..506ff54e5 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/DateTime.java
+++ b/apache/org/apache/james/mime4j/field/datetime/DateTime.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParser.java b/apache/org/apache/james/mime4j/field/datetime/parser/DateTimeParser.java
index 43edebb5c..43edebb5c 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParser.java
+++ b/apache/org/apache/james/mime4j/field/datetime/parser/DateTimeParser.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserConstants.java b/apache/org/apache/james/mime4j/field/datetime/parser/DateTimeParserConstants.java
index 2c203db2e..2c203db2e 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserConstants.java
+++ b/apache/org/apache/james/mime4j/field/datetime/parser/DateTimeParserConstants.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserTokenManager.java b/apache/org/apache/james/mime4j/field/datetime/parser/DateTimeParserTokenManager.java
index 4b2d2fd95..4b2d2fd95 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserTokenManager.java
+++ b/apache/org/apache/james/mime4j/field/datetime/parser/DateTimeParserTokenManager.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/ParseException.java b/apache/org/apache/james/mime4j/field/datetime/parser/ParseException.java
index 13b3ff097..13b3ff097 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/ParseException.java
+++ b/apache/org/apache/james/mime4j/field/datetime/parser/ParseException.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/SimpleCharStream.java b/apache/org/apache/james/mime4j/field/datetime/parser/SimpleCharStream.java
index 2724529f7..2724529f7 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/SimpleCharStream.java
+++ b/apache/org/apache/james/mime4j/field/datetime/parser/SimpleCharStream.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/Token.java b/apache/org/apache/james/mime4j/field/datetime/parser/Token.java
index 0927a0921..0927a0921 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/Token.java
+++ b/apache/org/apache/james/mime4j/field/datetime/parser/Token.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/TokenMgrError.java b/apache/org/apache/james/mime4j/field/datetime/parser/TokenMgrError.java
index e7043c1b7..e7043c1b7 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/TokenMgrError.java
+++ b/apache/org/apache/james/mime4j/field/datetime/parser/TokenMgrError.java
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/util/CharsetUtil.java b/apache/org/apache/james/mime4j/util/CharsetUtil.java
index 4e712fcdd..4e712fcdd 100644
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/util/CharsetUtil.java
+++ b/apache/org/apache/james/mime4j/util/CharsetUtil.java
diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_block_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_block_white_24.png
new file mode 100644
index 000000000..2ccc89d24
--- /dev/null
+++ b/assets/quantum/res/drawable-hdpi/quantum_ic_block_white_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_call_made_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_call_made_white_24.png
new file mode 100644
index 000000000..ea6a8ab5f
--- /dev/null
+++ b/assets/quantum/res/drawable-hdpi/quantum_ic_call_made_white_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_call_missed_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_call_missed_white_24.png
new file mode 100644
index 000000000..f188eb9aa
--- /dev/null
+++ b/assets/quantum/res/drawable-hdpi/quantum_ic_call_missed_white_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_call_received_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_call_received_white_24.png
new file mode 100644
index 000000000..ca2ae411a
--- /dev/null
+++ b/assets/quantum/res/drawable-hdpi/quantum_ic_call_received_white_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_content_copy_grey600_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_content_copy_grey600_24.png
new file mode 100644
index 000000000..6acef1745
--- /dev/null
+++ b/assets/quantum/res/drawable-hdpi/quantum_ic_content_copy_grey600_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_delete_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_delete_white_24.png
new file mode 100644
index 000000000..8444f3138
--- /dev/null
+++ b/assets/quantum/res/drawable-hdpi/quantum_ic_delete_white_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_edit_grey600_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_edit_grey600_24.png
new file mode 100644
index 000000000..4a27b4696
--- /dev/null
+++ b/assets/quantum/res/drawable-hdpi/quantum_ic_edit_grey600_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_content_copy_grey600_24.png b/assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_content_copy_grey600_24.png
new file mode 100644
index 000000000..90bf872ac
--- /dev/null
+++ b/assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_content_copy_grey600_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_content_copy_grey600_24.png b/assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_content_copy_grey600_24.png
new file mode 100644
index 000000000..01b869a60
--- /dev/null
+++ b/assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_content_copy_grey600_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_content_copy_grey600_24.png b/assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_content_copy_grey600_24.png
new file mode 100644
index 000000000..831b5249c
--- /dev/null
+++ b/assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_content_copy_grey600_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_content_copy_grey600_24.png b/assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_content_copy_grey600_24.png
new file mode 100644
index 000000000..71f3bd683
--- /dev/null
+++ b/assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_content_copy_grey600_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_content_copy_grey600_24.png b/assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_content_copy_grey600_24.png
new file mode 100644
index 000000000..3b2aed29b
--- /dev/null
+++ b/assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_content_copy_grey600_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_block_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_block_white_24.png
new file mode 100644
index 000000000..ec1b33f0e
--- /dev/null
+++ b/assets/quantum/res/drawable-mdpi/quantum_ic_block_white_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_call_made_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_call_made_white_24.png
new file mode 100644
index 000000000..9b3cd4380
--- /dev/null
+++ b/assets/quantum/res/drawable-mdpi/quantum_ic_call_made_white_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_call_missed_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_call_missed_white_24.png
new file mode 100644
index 000000000..42c360b8a
--- /dev/null
+++ b/assets/quantum/res/drawable-mdpi/quantum_ic_call_missed_white_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_call_received_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_call_received_white_24.png
new file mode 100644
index 000000000..fbc1e86e2
--- /dev/null
+++ b/assets/quantum/res/drawable-mdpi/quantum_ic_call_received_white_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_content_copy_grey600_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_content_copy_grey600_24.png
new file mode 100644
index 000000000..8ac80b083
--- /dev/null
+++ b/assets/quantum/res/drawable-mdpi/quantum_ic_content_copy_grey600_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_delete_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_delete_white_24.png
new file mode 100644
index 000000000..e2268c9be
--- /dev/null
+++ b/assets/quantum/res/drawable-mdpi/quantum_ic_delete_white_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_edit_grey600_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_edit_grey600_24.png
new file mode 100644
index 000000000..f003bc9d3
--- /dev/null
+++ b/assets/quantum/res/drawable-mdpi/quantum_ic_edit_grey600_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_block_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_block_white_24.png
new file mode 100644
index 000000000..7aba97b65
--- /dev/null
+++ b/assets/quantum/res/drawable-xhdpi/quantum_ic_block_white_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_call_made_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_call_made_white_24.png
new file mode 100644
index 000000000..7fe694105
--- /dev/null
+++ b/assets/quantum/res/drawable-xhdpi/quantum_ic_call_made_white_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_call_missed_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_call_missed_white_24.png
new file mode 100644
index 000000000..dd64489aa
--- /dev/null
+++ b/assets/quantum/res/drawable-xhdpi/quantum_ic_call_missed_white_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_call_received_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_call_received_white_24.png
new file mode 100644
index 000000000..807308d9d
--- /dev/null
+++ b/assets/quantum/res/drawable-xhdpi/quantum_ic_call_received_white_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_content_copy_grey600_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_content_copy_grey600_24.png
new file mode 100644
index 000000000..ca6259859
--- /dev/null
+++ b/assets/quantum/res/drawable-xhdpi/quantum_ic_content_copy_grey600_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_delete_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_delete_white_24.png
new file mode 100644
index 000000000..484260a97
--- /dev/null
+++ b/assets/quantum/res/drawable-xhdpi/quantum_ic_delete_white_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_edit_grey600_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_edit_grey600_24.png
new file mode 100644
index 000000000..b5b3a243c
--- /dev/null
+++ b/assets/quantum/res/drawable-xhdpi/quantum_ic_edit_grey600_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_block_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_block_white_24.png
new file mode 100644
index 000000000..fddfa54b8
--- /dev/null
+++ b/assets/quantum/res/drawable-xxhdpi/quantum_ic_block_white_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_made_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_made_white_24.png
new file mode 100644
index 000000000..ae471c9fc
--- /dev/null
+++ b/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_made_white_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_missed_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_missed_white_24.png
new file mode 100644
index 000000000..2374dc5a1
--- /dev/null
+++ b/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_missed_white_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_received_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_received_white_24.png
new file mode 100644
index 000000000..58421114f
--- /dev/null
+++ b/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_received_white_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_content_copy_grey600_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_content_copy_grey600_24.png
new file mode 100644
index 000000000..c480ba78f
--- /dev/null
+++ b/assets/quantum/res/drawable-xxhdpi/quantum_ic_content_copy_grey600_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_delete_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_delete_white_24.png
new file mode 100644
index 000000000..603f28cbd
--- /dev/null
+++ b/assets/quantum/res/drawable-xxhdpi/quantum_ic_delete_white_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_edit_grey600_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_edit_grey600_24.png
new file mode 100644
index 000000000..f1f9ffce8
--- /dev/null
+++ b/assets/quantum/res/drawable-xxhdpi/quantum_ic_edit_grey600_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_block_white_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_block_white_24.png
new file mode 100644
index 000000000..0378d1bed
--- /dev/null
+++ b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_block_white_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_made_white_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_made_white_24.png
new file mode 100644
index 000000000..844ef86a0
--- /dev/null
+++ b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_made_white_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_missed_white_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_missed_white_24.png
new file mode 100644
index 000000000..b1321a9ae
--- /dev/null
+++ b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_missed_white_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_received_white_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_received_white_24.png
new file mode 100644
index 000000000..417999c85
--- /dev/null
+++ b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_received_white_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_content_copy_grey600_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_content_copy_grey600_24.png
new file mode 100644
index 000000000..f0ea085c9
--- /dev/null
+++ b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_content_copy_grey600_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_delete_white_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_delete_white_24.png
new file mode 100644
index 000000000..c582dc2a4
--- /dev/null
+++ b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_delete_white_24.png
Binary files differ
diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_edit_grey600_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_edit_grey600_24.png
new file mode 100644
index 000000000..a61298dbe
--- /dev/null
+++ b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_edit_grey600_24.png
Binary files differ
diff --git a/java/com/android/contacts/common/ContactPhotoManager.java b/java/com/android/contacts/common/ContactPhotoManager.java
index 834471047..0f65a6c56 100644
--- a/java/com/android/contacts/common/ContactPhotoManager.java
+++ b/java/com/android/contacts/common/ContactPhotoManager.java
@@ -40,6 +40,7 @@ public abstract class ContactPhotoManager implements ComponentCallbacks2 {
public static final int TYPE_BUSINESS = LetterTileDrawable.TYPE_BUSINESS;
public static final int TYPE_VOICEMAIL = LetterTileDrawable.TYPE_VOICEMAIL;
public static final int TYPE_DEFAULT = LetterTileDrawable.TYPE_DEFAULT;
+ public static final int TYPE_GENERIC_AVATAR = LetterTileDrawable.TYPE_GENERIC_AVATAR;
/** Scale and offset default constants used for default letter images */
public static final float SCALE_DEFAULT = 1.0f;
diff --git a/java/com/android/contacts/common/lettertiles/LetterTileDrawable.java b/java/com/android/contacts/common/lettertiles/LetterTileDrawable.java
index 7e1839c1e..ca12f1812 100644
--- a/java/com/android/contacts/common/lettertiles/LetterTileDrawable.java
+++ b/java/com/android/contacts/common/lettertiles/LetterTileDrawable.java
@@ -19,6 +19,7 @@ package com.android.contacts.common.lettertiles;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
@@ -48,13 +49,18 @@ public class LetterTileDrawable extends Drawable {
* #TYPE_BUSINESS}, and voicemail contacts should use {@link #TYPE_VOICEMAIL}.
*/
@Retention(RetentionPolicy.SOURCE)
- @IntDef({TYPE_PERSON, TYPE_BUSINESS, TYPE_VOICEMAIL})
+ @IntDef({TYPE_PERSON, TYPE_BUSINESS, TYPE_VOICEMAIL, TYPE_GENERIC_AVATAR})
public @interface ContactType {}
/** Contact type constants */
public static final int TYPE_PERSON = 1;
public static final int TYPE_BUSINESS = 2;
public static final int TYPE_VOICEMAIL = 3;
+ /**
+ * A generic avatar that features the default icon, default color, and no letter. Useful for
+ * situations where a contact is anonymous.
+ */
+ public static final int TYPE_GENERIC_AVATAR = 4;
@ContactType public static final int TYPE_DEFAULT = TYPE_PERSON;
/**
@@ -87,7 +93,6 @@ public class LetterTileDrawable extends Drawable {
private static Bitmap sDefaultPersonAvatar;
private static Bitmap sDefaultBusinessAvatar;
private static Bitmap sDefaultVoicemailAvatar;
- private static final String TAG = LetterTileDrawable.class.getSimpleName();
private final Paint mPaint;
private int mContactType = TYPE_DEFAULT;
private float mScale = 1.0f;
@@ -97,7 +102,7 @@ public class LetterTileDrawable extends Drawable {
private int mColor;
private Character mLetter = null;
- private boolean mAvatarWasVoicemailOrBusiness = false;
+ @ContactType private int mAvatarType = TYPE_DEFAULT;
private String mDisplayName;
public LetterTileDrawable(final Resources res) {
@@ -130,6 +135,7 @@ public class LetterTileDrawable extends Drawable {
case TYPE_VOICEMAIL:
return sDefaultVoicemailAvatar;
case TYPE_PERSON:
+ case TYPE_GENERIC_AVATAR:
default:
return sDefaultPersonAvatar;
}
@@ -149,6 +155,14 @@ public class LetterTileDrawable extends Drawable {
drawLetterTile(canvas);
}
+ public Bitmap getBitmap(int width, int height) {
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888);
+ this.setBounds(0, 0, width, height);
+ Canvas canvas = new Canvas(bitmap);
+ this.draw(canvas);
+ return bitmap;
+ }
+
/**
* Draw the bitmap onto the canvas at the current bounds taking into account the current scale.
*/
@@ -231,7 +245,9 @@ public class LetterTileDrawable extends Drawable {
/** Returns a deterministic color based on the provided contact identifier string. */
private int pickColor(final String identifier) {
- if (TextUtils.isEmpty(identifier) || mContactType == TYPE_VOICEMAIL) {
+ if (mContactType == TYPE_VOICEMAIL
+ || mContactType == TYPE_BUSINESS
+ || TextUtils.isEmpty(identifier)) {
return sDefaultColor;
}
// String.hashCode() implementation is not supposed to change across java versions, so
@@ -329,6 +345,10 @@ public class LetterTileDrawable extends Drawable {
return this;
}
+ public boolean tileIsCircular() {
+ return this.mIsCircle;
+ }
+
/**
* Creates a canonical letter tile for use across dialer fragments.
*
@@ -344,39 +364,37 @@ public class LetterTileDrawable extends Drawable {
@Nullable final String identifierForTileColor,
@Shape final int shape,
final int contactType) {
- setContactType(contactType);
+
+ this.setIsCircular(shape == SHAPE_CIRCLE);
+
/**
- * During hangup, we lose the call state for special types of contacts, like voicemail. To help
- * callers avoid extraneous LetterTileDrawable allocations, we keep track of the special case
- * until we encounter a new display name.
+ * We return quickly under the following conditions: 1. We are asked to draw a default tile, and
+ * no coloring information is provided, meaning no further initialization is necessary OR 2.
+ * We've already invoked this method before, set mDisplayName, and found that it has not
+ * changed. This is useful during events like hangup, when we lose the call state for special
+ * types of contacts, like voicemail. We keep track of the special case until we encounter a new
+ * display name.
*/
- if (contactType == TYPE_VOICEMAIL || contactType == TYPE_BUSINESS) {
- this.mAvatarWasVoicemailOrBusiness = true;
- } else if (displayName != null && !displayName.equals(mDisplayName)) {
- this.mAvatarWasVoicemailOrBusiness = false;
+ if (contactType == TYPE_DEFAULT
+ && ((displayName == null && identifierForTileColor == null)
+ || (displayName != null && displayName.equals(mDisplayName)))) {
+ return this;
}
+
this.mDisplayName = displayName;
- if (shape == SHAPE_CIRCLE) {
- this.setIsCircular(true);
- } else {
- this.setIsCircular(false);
- }
+ this.mAvatarType = contactType;
+ setContactType(this.mAvatarType);
- /**
- * To preserve style, we don't use contactType to set the tile icon. In the future, when all
- * callers surface this detail, we can use this to better style the tile icon.
- */
- if (mAvatarWasVoicemailOrBusiness) {
- this.setLetterAndColorFromContactDetails(null, displayName);
- return this;
+ // Special contact types receive default color and no letter tile, but special iconography.
+ if (this.mAvatarType != TYPE_PERSON) {
+ this.setLetterAndColorFromContactDetails(null, null);
} else {
if (identifierForTileColor != null) {
this.setLetterAndColorFromContactDetails(displayName, identifierForTileColor);
- return this;
} else {
this.setLetterAndColorFromContactDetails(displayName, displayName);
- return this;
}
}
+ return this;
}
}
diff --git a/java/com/android/contacts/common/list/ContactEntryListFragment.java b/java/com/android/contacts/common/list/ContactEntryListFragment.java
index a8d9b55ba..278175c0b 100644
--- a/java/com/android/contacts/common/list/ContactEntryListFragment.java
+++ b/java/com/android/contacts/common/list/ContactEntryListFragment.java
@@ -16,7 +16,6 @@
package com.android.contacts.common.list;
-import android.app.Activity;
import android.app.Fragment;
import android.app.LoaderManager;
import android.app.LoaderManager.LoaderCallbacks;
@@ -29,8 +28,8 @@ import android.os.Handler;
import android.os.Message;
import android.os.Parcelable;
import android.provider.ContactsContract.Directory;
+import android.support.annotation.Nullable;
import android.text.TextUtils;
-import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
@@ -42,12 +41,13 @@ import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
-import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.ListView;
import com.android.common.widget.CompositeCursorAdapter.Partition;
import com.android.contacts.common.ContactPhotoManager;
import com.android.contacts.common.preference.ContactsPreferences;
import com.android.contacts.common.util.ContactListViewUtils;
+import com.android.dialer.common.LogUtil;
+import java.lang.ref.WeakReference;
import java.util.Locale;
/** Common base class for various contact-related list fragments. */
@@ -56,9 +56,7 @@ public abstract class ContactEntryListFragment<T extends ContactEntryListAdapter
OnScrollListener,
OnFocusChangeListener,
OnTouchListener,
- OnItemLongClickListener,
LoaderCallbacks<Cursor> {
- private static final String TAG = "ContactEntryListFragment";
private static final String KEY_LIST_STATE = "liststate";
private static final String KEY_SECTION_HEADER_DISPLAY_ENABLED = "sectionHeaderDisplayEnabled";
private static final String KEY_PHOTO_LOADER_ENABLED = "photoLoaderEnabled";
@@ -130,15 +128,27 @@ public abstract class ContactEntryListFragment<T extends ContactEntryListAdapter
private LoaderManager mLoaderManager;
- private Handler mDelayedDirectorySearchHandler =
- new Handler() {
- @Override
- public void handleMessage(Message msg) {
- if (msg.what == DIRECTORY_SEARCH_MESSAGE) {
- loadDirectoryPartition(msg.arg1, (DirectoryPartition) msg.obj);
- }
- }
- };
+ private Handler mDelayedDirectorySearchHandler;
+
+ private static class DelayedDirectorySearchHandler extends Handler {
+ private final WeakReference<ContactEntryListFragment<?>> contactEntryListFragmentRef;
+
+ private DelayedDirectorySearchHandler(ContactEntryListFragment<?> contactEntryListFragment) {
+ this.contactEntryListFragmentRef = new WeakReference<>(contactEntryListFragment);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ ContactEntryListFragment<?> contactEntryListFragment = contactEntryListFragmentRef.get();
+ if (contactEntryListFragment == null) {
+ return;
+ }
+ if (msg.what == DIRECTORY_SEARCH_MESSAGE) {
+ contactEntryListFragment.loadDirectoryPartition(msg.arg1, (DirectoryPartition) msg.obj);
+ }
+ }
+ }
+
private ContactsPreferences.ChangeListener mPreferencesChangeListener =
new ContactsPreferences.ChangeListener() {
@Override
@@ -148,6 +158,10 @@ public abstract class ContactEntryListFragment<T extends ContactEntryListAdapter
}
};
+ protected ContactEntryListFragment() {
+ mDelayedDirectorySearchHandler = new DelayedDirectorySearchHandler(this);
+ }
+
protected abstract View inflateView(LayoutInflater inflater, ViewGroup container);
protected abstract T createListAdapter();
@@ -158,18 +172,10 @@ public abstract class ContactEntryListFragment<T extends ContactEntryListAdapter
*/
protected abstract void onItemClick(int position, long id);
- /**
- * @param position Please note that the position is already adjusted for header views, so "0"
- * means the first list item below header views.
- */
- protected boolean onItemLongClick(int position, long id) {
- return false;
- }
-
@Override
- public void onAttach(Activity activity) {
- super.onAttach(activity);
- setContext(activity);
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ setContext(context);
setLoaderManager(super.getLoaderManager());
}
@@ -343,7 +349,9 @@ public abstract class ContactEntryListFragment<T extends ContactEntryListAdapter
} catch (RuntimeException e) {
// We don't even know what the projection should be, so no point trying to
// return an empty MatrixCursor with the correct projection here.
- Log.w(TAG, "RuntimeException while trying to query ContactsProvider.");
+ LogUtil.w(
+ "ContactEntryListFragment.onLoadInBackground",
+ "RuntimeException while trying to query ContactsProvider.");
return null;
}
}
@@ -441,6 +449,7 @@ public abstract class ContactEntryListFragment<T extends ContactEntryListAdapter
}
public boolean isLoading() {
+ //noinspection SimplifiableIfStatement
if (mAdapter != null && mAdapter.isLoading()) {
return true;
}
@@ -511,7 +520,6 @@ public abstract class ContactEntryListFragment<T extends ContactEntryListAdapter
if (mListView != null) {
mListView.setFastScrollEnabled(hasScrollbar);
- mListView.setFastScrollAlwaysVisible(hasScrollbar);
mListView.setVerticalScrollbarPosition(mVerticalScrollbarPosition);
mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
}
@@ -573,6 +581,7 @@ public abstract class ContactEntryListFragment<T extends ContactEntryListAdapter
}
}
+ @Nullable
public final String getQueryString() {
return mQueryString;
}
@@ -694,7 +703,6 @@ public abstract class ContactEntryListFragment<T extends ContactEntryListAdapter
}
mListView.setOnItemClickListener(this);
- mListView.setOnItemLongClickListener(this);
mListView.setOnFocusChangeListener(this);
mListView.setOnTouchListener(this);
mListView.setFastScrollEnabled(!isSearchMode());
@@ -779,16 +787,6 @@ public abstract class ContactEntryListFragment<T extends ContactEntryListAdapter
}
}
- @Override
- public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
- int adjPosition = position - mListView.getHeaderViewsCount();
-
- if (adjPosition >= 0) {
- return onItemLongClick(adjPosition, id);
- }
- return false;
- }
-
private void hideSoftKeyboard() {
// Hide soft keyboard, if visible
InputMethodManager inputMethodManager =
diff --git a/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java b/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java
index 63f8ca580..8156d97cf 100644
--- a/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java
+++ b/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java
@@ -59,8 +59,6 @@ public class SelectPhoneAccountDialogFragment extends DialogFragment {
private static final String ARG_LISTENER = "listener";
private static final String ARG_CALL_ID = "call_id";
- private int mTitleResId;
- private boolean mCanSetDefault;
private List<PhoneAccountHandle> mAccountHandles;
private boolean mIsSelected;
private boolean mIsDefaultChecked;
@@ -126,8 +124,8 @@ public class SelectPhoneAccountDialogFragment extends DialogFragment {
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
- mTitleResId = getArguments().getInt(ARG_TITLE_RES_ID);
- mCanSetDefault = getArguments().getBoolean(ARG_CAN_SET_DEFAULT);
+ int titleResId = getArguments().getInt(ARG_TITLE_RES_ID);
+ boolean canSetDefault = getArguments().getBoolean(ARG_CAN_SET_DEFAULT);
mAccountHandles = getArguments().getParcelableArrayList(ARG_ACCOUNT_HANDLES);
mListener = getArguments().getParcelable(ARG_LISTENER);
if (savedInstanceState != null) {
@@ -167,11 +165,11 @@ public class SelectPhoneAccountDialogFragment extends DialogFragment {
AlertDialog dialog =
builder
- .setTitle(mTitleResId)
+ .setTitle(titleResId)
.setAdapter(selectAccountListAdapter, selectionListener)
.create();
- if (mCanSetDefault) {
+ if (canSetDefault) {
// Generate custom checkbox view, lint suppressed since no appropriate parent (is dialog)
@SuppressLint("InflateParams")
LinearLayout checkboxLayout =
@@ -190,13 +188,13 @@ public class SelectPhoneAccountDialogFragment extends DialogFragment {
}
@Override
- public void onStop() {
+ public void onCancel(DialogInterface dialog) {
if (!mIsSelected && mListener != null) {
Bundle result = new Bundle();
result.putString(SelectPhoneAccountListener.EXTRA_CALL_ID, getCallId());
mListener.onReceiveResult(SelectPhoneAccountListener.RESULT_DISMISSED, result);
}
- super.onStop();
+ super.onCancel(dialog);
}
@Nullable
@@ -213,7 +211,7 @@ public class SelectPhoneAccountDialogFragment extends DialogFragment {
static final String EXTRA_SET_DEFAULT = "extra_set_default";
static final String EXTRA_CALL_ID = "extra_call_id";
- public SelectPhoneAccountListener() {
+ protected SelectPhoneAccountListener() {
super(new Handler());
}
@@ -239,7 +237,7 @@ public class SelectPhoneAccountDialogFragment extends DialogFragment {
private int mResId;
- public SelectAccountListAdapter(
+ SelectAccountListAdapter(
Context context, int resource, List<PhoneAccountHandle> accountHandles) {
super(context, resource, accountHandles);
mResId = resource;
diff --git a/java/com/android/dialer/app/AndroidManifest.xml b/java/com/android/dialer/app/AndroidManifest.xml
index 80f294acc..5ce13dbd7 100644
--- a/java/com/android/dialer/app/AndroidManifest.xml
+++ b/java/com/android/dialer/app/AndroidManifest.xml
@@ -57,11 +57,7 @@
android:minSdkVersion="23"
android:targetSdkVersion="25"/>
- <application
- android:backupAgent='com.android.dialer.backup.DialerBackupAgent'
- android:fullBackupOnly="true"
- android:restoreAnyVersion="true"
- android:name="com.android.dialer.app.DialerApplication">
+ <application android:theme="@style/Theme.AppCompat">
<activity
android:exported="false"
@@ -75,6 +71,12 @@
</intent-filter>
</activity>
+ <activity
+ android:label="@string/call_log_activity_title"
+ android:name="com.android.dialer.app.calllog.CallLogActivity"
+ android:theme="@style/DialtactsThemeWithoutActionBarOverlay">
+ </activity>
+
<receiver android:name="com.android.dialer.app.calllog.CallLogReceiver">
<intent-filter>
<action android:name="android.intent.action.NEW_VOICEMAIL"/>
diff --git a/java/com/android/dialer/app/CallDetailActivity.java b/java/com/android/dialer/app/CallDetailActivity.java
deleted file mode 100644
index cda2b2e2c..000000000
--- a/java/com/android/dialer/app/CallDetailActivity.java
+++ /dev/null
@@ -1,480 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.dialer.app;
-
-import android.content.ContentUris;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.support.v7.app.AppCompatActivity;
-import android.text.BidiFormatter;
-import android.text.TextDirectionHeuristics;
-import android.text.TextUtils;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.MotionEvent;
-import android.view.View;
-import android.widget.ListView;
-import android.widget.QuickContactBadge;
-import android.widget.TextView;
-import android.widget.Toast;
-import com.android.contacts.common.ClipboardUtils;
-import com.android.contacts.common.ContactPhotoManager;
-import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
-import com.android.contacts.common.GeoUtil;
-import com.android.contacts.common.preference.ContactsPreferences;
-import com.android.contacts.common.util.UriUtils;
-import com.android.dialer.app.calllog.CallDetailHistoryAdapter;
-import com.android.dialer.app.calllog.CallLogAsyncTaskUtil;
-import com.android.dialer.app.calllog.CallLogAsyncTaskUtil.CallLogAsyncTaskListener;
-import com.android.dialer.app.calllog.CallTypeHelper;
-import com.android.dialer.app.calllog.PhoneAccountUtils;
-import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
-import com.android.dialer.callintent.CallIntentBuilder;
-import com.android.dialer.callintent.nano.CallInitiationType;
-import com.android.dialer.common.Assert;
-import com.android.dialer.common.AsyncTaskExecutor;
-import com.android.dialer.common.AsyncTaskExecutors;
-import com.android.dialer.compat.CompatUtils;
-import com.android.dialer.logging.Logger;
-import com.android.dialer.logging.nano.DialerImpression;
-import com.android.dialer.logging.nano.ScreenEvent;
-import com.android.dialer.phonenumbercache.ContactInfoHelper;
-import com.android.dialer.phonenumberutil.PhoneNumberHelper;
-import com.android.dialer.proguard.UsedByReflection;
-import com.android.dialer.spam.Spam;
-import com.android.dialer.telecom.TelecomUtil;
-import com.android.dialer.util.CallUtil;
-import com.android.dialer.util.DialerUtils;
-import com.android.dialer.util.TouchPointManager;
-
-/**
- * Displays the details of a specific call log entry.
- *
- * <p>This activity can be either started with the URI of a single call log entry, or with the
- * {@link #EXTRA_CALL_LOG_IDS} extra to specify a group of call log entries.
- */
-@UsedByReflection(value = "AndroidManifest-app.xml")
-public class CallDetailActivity extends AppCompatActivity
- implements MenuItem.OnMenuItemClickListener, View.OnClickListener {
-
- /** A long array extra containing ids of call log entries to display. */
- public static final String EXTRA_CALL_LOG_IDS = "EXTRA_CALL_LOG_IDS";
- /** If we are started with a voicemail, we'll find the uri to play with this extra. */
- public static final String EXTRA_VOICEMAIL_URI = "EXTRA_VOICEMAIL_URI";
- /** If the activity was triggered from a notification. */
- public static final String EXTRA_FROM_NOTIFICATION = "EXTRA_FROM_NOTIFICATION";
-
- public static final String BLOCKED_OR_SPAM_QUERY_IDENTIFIER = "blockedOrSpamIdentifier";
-
- private final AsyncTaskExecutor executor = AsyncTaskExecutors.createAsyncTaskExecutor();
- protected String mNumber;
- private Context mContext;
- private ContactInfoHelper mContactInfoHelper;
- private ContactsPreferences mContactsPreferences;
- private CallTypeHelper mCallTypeHelper;
- private ContactPhotoManager mContactPhotoManager;
- private BidiFormatter mBidiFormatter = BidiFormatter.getInstance();
- private LayoutInflater mInflater;
- private Resources mResources;
- private PhoneCallDetails mDetails;
- private Uri mVoicemailUri;
- private String mPostDialDigits = "";
- private ListView mHistoryList;
- private QuickContactBadge mQuickContactBadge;
- private TextView mCallerName;
- private TextView mCallerNumber;
- private TextView mAccountLabel;
- private View mCallButton;
- private View mEditBeforeCallActionItem;
- private View mReportActionItem;
- private View mCopyNumberActionItem;
- private FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler;
- private CallLogAsyncTaskListener mCallLogAsyncTaskListener =
- new CallLogAsyncTaskListener() {
- @Override
- public void onDeleteCall() {
- finish();
- }
-
- @Override
- public void onDeleteVoicemail() {
- finish();
- }
-
- @Override
- public void onGetCallDetails(final PhoneCallDetails[] details) {
- if (details == null) {
- // Somewhere went wrong: we're going to bail out and show error to users.
- Toast.makeText(mContext, R.string.toast_call_detail_error, Toast.LENGTH_SHORT).show();
- finish();
- return;
- }
-
- // All calls are from the same number and same contact, so pick the first detail.
- mDetails = details[0];
- mNumber = TextUtils.isEmpty(mDetails.number) ? null : mDetails.number.toString();
-
- if (mNumber == null) {
- updateDataAndRender(details);
- return;
- }
-
- executor.submit(
- BLOCKED_OR_SPAM_QUERY_IDENTIFIER,
- new AsyncTask<Void, Void, Void>() {
- @Override
- protected Void doInBackground(Void... params) {
- mDetails.isBlocked =
- mFilteredNumberAsyncQueryHandler.getBlockedIdSynchronousForCalllogOnly(
- mNumber, mDetails.countryIso)
- != null;
- if (Spam.get(mContext).isSpamEnabled()) {
- mDetails.isSpam =
- hasIncomingCalls(details)
- && Spam.get(mContext)
- .checkSpamStatusSynchronous(mNumber, mDetails.countryIso);
- }
- return null;
- }
-
- @Override
- protected void onPostExecute(Void result) {
- updateDataAndRender(details);
- }
- });
- }
-
- private void updateDataAndRender(PhoneCallDetails[] details) {
- mPostDialDigits =
- TextUtils.isEmpty(mDetails.postDialDigits) ? "" : mDetails.postDialDigits;
-
- final CharSequence callLocationOrType = getNumberTypeOrLocation(mDetails);
-
- final CharSequence displayNumber;
- if (!TextUtils.isEmpty(mDetails.postDialDigits)) {
- displayNumber = mDetails.number + mDetails.postDialDigits;
- } else {
- displayNumber = mDetails.displayNumber;
- }
-
- final String displayNumberStr =
- mBidiFormatter.unicodeWrap(displayNumber.toString(), TextDirectionHeuristics.LTR);
-
- mDetails.nameDisplayOrder = mContactsPreferences.getDisplayOrder();
-
- if (!TextUtils.isEmpty(mDetails.getPreferredName())) {
- mCallerName.setText(mDetails.getPreferredName());
- mCallerNumber.setText(callLocationOrType + " " + displayNumberStr);
- } else {
- mCallerName.setText(displayNumberStr);
- if (!TextUtils.isEmpty(callLocationOrType)) {
- mCallerNumber.setText(callLocationOrType);
- mCallerNumber.setVisibility(View.VISIBLE);
- } else {
- mCallerNumber.setVisibility(View.GONE);
- }
- }
-
- CharSequence accountLabel =
- PhoneAccountUtils.getAccountLabel(mContext, mDetails.accountHandle);
- CharSequence accountContentDescription =
- PhoneCallDetails.createAccountLabelDescription(
- mResources, mDetails.viaNumber, accountLabel);
- if (!TextUtils.isEmpty(mDetails.viaNumber)) {
- if (!TextUtils.isEmpty(accountLabel)) {
- accountLabel =
- mResources.getString(
- R.string.call_log_via_number_phone_account, accountLabel, mDetails.viaNumber);
- } else {
- accountLabel = mResources.getString(R.string.call_log_via_number, mDetails.viaNumber);
- }
- }
- if (!TextUtils.isEmpty(accountLabel)) {
- mAccountLabel.setText(accountLabel);
- mAccountLabel.setContentDescription(accountContentDescription);
- mAccountLabel.setVisibility(View.VISIBLE);
- } else {
- mAccountLabel.setVisibility(View.GONE);
- }
-
- final boolean canPlaceCallsTo =
- PhoneNumberHelper.canPlaceCallsTo(mNumber, mDetails.numberPresentation);
- mCallButton.setVisibility(canPlaceCallsTo ? View.VISIBLE : View.GONE);
- mCopyNumberActionItem.setVisibility(canPlaceCallsTo ? View.VISIBLE : View.GONE);
-
- final boolean isSipNumber = PhoneNumberHelper.isSipNumber(mNumber);
- final boolean isVoicemailNumber =
- PhoneNumberHelper.isVoicemailNumber(mContext, mDetails.accountHandle, mNumber);
- final boolean showEditNumberBeforeCallAction =
- canPlaceCallsTo && !isSipNumber && !isVoicemailNumber;
- mEditBeforeCallActionItem.setVisibility(
- showEditNumberBeforeCallAction ? View.VISIBLE : View.GONE);
-
- final boolean showReportAction =
- mContactInfoHelper.canReportAsInvalid(mDetails.sourceType, mDetails.objectId);
- mReportActionItem.setVisibility(showReportAction ? View.VISIBLE : View.GONE);
-
- invalidateOptionsMenu();
-
- mHistoryList.setAdapter(
- new CallDetailHistoryAdapter(mContext, mInflater, mCallTypeHelper, details));
-
- updateContactPhoto(mDetails.isSpam);
-
- findViewById(R.id.call_detail).setVisibility(View.VISIBLE);
- }
-
- /**
- * Determines the location geocode text for a call, or the phone number type (if available).
- *
- * @param details The call details.
- * @return The phone number type or location.
- */
- private CharSequence getNumberTypeOrLocation(PhoneCallDetails details) {
- if (details.isSpam) {
- return mResources.getString(R.string.spam_number_call_log_label);
- } else if (details.isBlocked) {
- return mResources.getString(R.string.blocked_number_call_log_label);
- } else if (!TextUtils.isEmpty(details.namePrimary)) {
- return Phone.getTypeLabel(mResources, details.numberType, details.numberLabel);
- } else {
- return details.geocode;
- }
- }
- };
-
- @Override
- protected void onCreate(Bundle icicle) {
- super.onCreate(icicle);
-
- mContext = this;
- mResources = getResources();
- mContactInfoHelper = new ContactInfoHelper(this, GeoUtil.getCurrentCountryIso(this));
- mContactsPreferences = new ContactsPreferences(mContext);
- mCallTypeHelper = new CallTypeHelper(getResources());
- mFilteredNumberAsyncQueryHandler = new FilteredNumberAsyncQueryHandler(mContext);
-
- mVoicemailUri = getIntent().getParcelableExtra(EXTRA_VOICEMAIL_URI);
-
- getSupportActionBar().setDisplayHomeAsUpEnabled(true);
-
- setContentView(R.layout.call_detail);
- mInflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
-
- mHistoryList = (ListView) findViewById(R.id.history);
- mHistoryList.addHeaderView(mInflater.inflate(R.layout.call_detail_header, null));
- mHistoryList.addFooterView(mInflater.inflate(R.layout.call_detail_footer, null), null, false);
-
- mQuickContactBadge = (QuickContactBadge) findViewById(R.id.quick_contact_photo);
- mQuickContactBadge.setOverlay(null);
- if (CompatUtils.hasPrioritizedMimeType()) {
- mQuickContactBadge.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE);
- }
- mCallerName = (TextView) findViewById(R.id.caller_name);
- mCallerNumber = (TextView) findViewById(R.id.caller_number);
- mAccountLabel = (TextView) findViewById(R.id.phone_account_label);
- mContactPhotoManager = ContactPhotoManager.getInstance(this);
-
- mCallButton = findViewById(R.id.call_back_button);
- mCallButton.setOnClickListener(
- new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- if (TextUtils.isEmpty(mNumber)) {
- return;
- }
- DialerUtils.startActivityWithErrorToast(
- CallDetailActivity.this,
- new CallIntentBuilder(getDialableNumber(), CallInitiationType.Type.CALL_DETAILS)
- .build());
- }
- });
-
- mEditBeforeCallActionItem = findViewById(R.id.call_detail_action_edit_before_call);
- mEditBeforeCallActionItem.setOnClickListener(this);
- mReportActionItem = findViewById(R.id.call_detail_action_report);
- mReportActionItem.setOnClickListener(this);
-
- mCopyNumberActionItem = findViewById(R.id.call_detail_action_copy);
- mCopyNumberActionItem.setOnClickListener(this);
-
- if (getIntent().getBooleanExtra(EXTRA_FROM_NOTIFICATION, false)) {
- closeSystemDialogs();
- }
-
- Logger.get(this).logScreenView(ScreenEvent.Type.CALL_DETAILS, this);
- }
-
- @Override
- public void onResume() {
- super.onResume();
- mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY);
- getCallDetails();
- }
-
- @Override
- public boolean dispatchTouchEvent(MotionEvent ev) {
- if (ev.getAction() == MotionEvent.ACTION_DOWN) {
- TouchPointManager.getInstance().setPoint((int) ev.getRawX(), (int) ev.getRawY());
- }
- return super.dispatchTouchEvent(ev);
- }
-
- public void getCallDetails() {
- CallLogAsyncTaskUtil.getCallDetails(this, mCallLogAsyncTaskListener, getCallLogEntryUris());
- }
-
- /**
- * Returns the list of URIs to show.
- *
- * <p>There are two ways the URIs can be provided to the activity: as the data on the intent, or
- * as a list of ids in the call log added as an extra on the URI.
- *
- * <p>If both are available, the data on the intent takes precedence.
- */
- private Uri[] getCallLogEntryUris() {
- final Uri uri = getIntent().getData();
- if (uri != null) {
- // If there is a data on the intent, it takes precedence over the extra.
- return new Uri[] {uri};
- }
- final long[] ids = getIntent().getLongArrayExtra(EXTRA_CALL_LOG_IDS);
- final int numIds = ids == null ? 0 : ids.length;
- final Uri[] uris = new Uri[numIds];
- for (int index = 0; index < numIds; ++index) {
- uris[index] =
- ContentUris.withAppendedId(
- TelecomUtil.getCallLogUri(CallDetailActivity.this), ids[index]);
- }
- return uris;
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- final MenuItem deleteMenuItem =
- menu.add(
- Menu.NONE, R.id.call_detail_delete_menu_item, Menu.NONE, R.string.call_details_delete);
- deleteMenuItem.setIcon(R.drawable.ic_delete_24dp);
- deleteMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
- deleteMenuItem.setOnMenuItemClickListener(this);
-
- return super.onCreateOptionsMenu(menu);
- }
-
- @Override
- public boolean onMenuItemClick(MenuItem item) {
- if (item.getItemId() == R.id.call_detail_delete_menu_item) {
- Logger.get(mContext).logImpression(DialerImpression.Type.USER_DELETED_CALL_LOG_ITEM);
- if (hasVoicemail()) {
- CallLogAsyncTaskUtil.deleteVoicemail(this, mVoicemailUri, mCallLogAsyncTaskListener);
- } else {
- final StringBuilder callIds = new StringBuilder();
- for (Uri callUri : getCallLogEntryUris()) {
- if (callIds.length() != 0) {
- callIds.append(",");
- }
- callIds.append(ContentUris.parseId(callUri));
- }
- CallLogAsyncTaskUtil.deleteCalls(this, callIds.toString(), mCallLogAsyncTaskListener);
- }
- }
- return true;
- }
-
- @Override
- public void onClick(View view) {
- int resId = view.getId();
- if (resId == R.id.call_detail_action_copy) {
- ClipboardUtils.copyText(mContext, null, mNumber, true);
- } else if (resId == R.id.call_detail_action_edit_before_call) {
- Intent dialIntent = new Intent(Intent.ACTION_DIAL, CallUtil.getCallUri(getDialableNumber()));
- DialerUtils.startActivityWithErrorToast(mContext, dialIntent);
- } else {
- Assert.fail("Unexpected onClick event from " + view);
- }
- }
-
- // Loads and displays the contact photo.
- private void updateContactPhoto(boolean isSpam) {
- if (mDetails == null) {
- return;
- }
-
- mQuickContactBadge.assignContactUri(mDetails.contactUri);
- final String displayName =
- TextUtils.isEmpty(mDetails.namePrimary)
- ? mDetails.displayNumber
- : mDetails.namePrimary.toString();
- mQuickContactBadge.setContentDescription(
- mResources.getString(R.string.description_contact_details, displayName));
-
- final boolean isVoicemailNumber =
- PhoneNumberHelper.isVoicemailNumber(mContext, mDetails.accountHandle, mNumber);
- if (isSpam) {
- mQuickContactBadge.setImageDrawable(mContext.getDrawable(R.drawable.blocked_contact));
- return;
- }
-
- final boolean isBusiness = mContactInfoHelper.isBusiness(mDetails.sourceType);
- int contactType = ContactPhotoManager.TYPE_DEFAULT;
- if (isVoicemailNumber) {
- contactType = ContactPhotoManager.TYPE_VOICEMAIL;
- } else if (isBusiness) {
- contactType = ContactPhotoManager.TYPE_BUSINESS;
- }
-
- final String lookupKey =
- mDetails.contactUri == null ? null : UriUtils.getLookupKeyFromUri(mDetails.contactUri);
-
- final DefaultImageRequest request =
- new DefaultImageRequest(displayName, lookupKey, contactType, true /* isCircular */);
-
- mContactPhotoManager.loadDirectoryPhoto(
- mQuickContactBadge,
- mDetails.photoUri,
- false /* darkTheme */,
- true /* isCircular */,
- request);
- }
-
- private void closeSystemDialogs() {
- sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
- }
-
- private String getDialableNumber() {
- return mNumber + mPostDialDigits;
- }
-
- public boolean hasVoicemail() {
- return mVoicemailUri != null;
- }
-
- private static boolean hasIncomingCalls(PhoneCallDetails[] details) {
- for (int i = 0; i < details.length; i++) {
- if (details[i].hasIncomingCalls()) {
- return true;
- }
- }
- return false;
- }
-}
diff --git a/java/com/android/dialer/app/DialerApplication.java b/java/com/android/dialer/app/DialerApplication.java
deleted file mode 100644
index 3b979212b..000000000
--- a/java/com/android/dialer/app/DialerApplication.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.dialer.app;
-
-import android.app.Application;
-import android.os.Trace;
-import android.preference.PreferenceManager;
-import com.android.dialer.blocking.BlockedNumbersAutoMigrator;
-import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
-import com.android.dialer.enrichedcall.EnrichedCallManager;
-import com.android.dialer.inject.ApplicationModule;
-import com.android.dialer.inject.DaggerDialerAppComponent;
-import com.android.dialer.inject.DialerAppComponent;
-
-public class DialerApplication extends Application implements EnrichedCallManager.Factory {
-
- private static final String TAG = "DialerApplication";
-
- private volatile DialerAppComponent component;
-
- @Override
- public void onCreate() {
- Trace.beginSection(TAG + " onCreate");
- super.onCreate();
- new BlockedNumbersAutoMigrator(
- this,
- PreferenceManager.getDefaultSharedPreferences(this),
- new FilteredNumberAsyncQueryHandler(this))
- .autoMigrate();
- Trace.endSection();
- }
-
- @Override
- public EnrichedCallManager getEnrichedCallManager() {
- return component().enrichedCallManager();
- }
-
- protected DialerAppComponent buildApplicationComponent() {
- return DaggerDialerAppComponent.builder()
- .applicationModule(new ApplicationModule(this))
- .build();
- }
-
- /**
- * Returns the application component.
- *
- * <p>A single Component is created per application instance. Note that it won't be instantiated
- * until it's first requested, but guarantees that only one will ever be created.
- */
- private final DialerAppComponent component() {
- // Double-check idiom for lazy initialization
- DialerAppComponent result = component;
- if (result == null) {
- synchronized (this) {
- result = component;
- if (result == null) {
- component = result = buildApplicationComponent();
- }
- }
- }
- return result;
- }
-}
diff --git a/java/com/android/dialer/app/DialtactsActivity.java b/java/com/android/dialer/app/DialtactsActivity.java
index 4c57cda70..b2837769f 100644
--- a/java/com/android/dialer/app/DialtactsActivity.java
+++ b/java/com/android/dialer/app/DialtactsActivity.java
@@ -63,15 +63,14 @@ import android.widget.Toast;
import com.android.contacts.common.dialog.ClearFrequentsDialog;
import com.android.contacts.common.list.OnPhoneNumberPickerActionListener;
import com.android.contacts.common.list.PhoneNumberListAdapter;
-import com.android.contacts.common.list.PhoneNumberListAdapter.PhoneQuery;
import com.android.contacts.common.list.PhoneNumberPickerFragment.CursorReranker;
import com.android.contacts.common.list.PhoneNumberPickerFragment.OnLoadFinishedListener;
import com.android.contacts.common.widget.FloatingActionButtonController;
import com.android.dialer.animation.AnimUtils;
import com.android.dialer.animation.AnimationListenerAdapter;
+import com.android.dialer.app.calllog.CallLogActivity;
import com.android.dialer.app.calllog.CallLogFragment;
import com.android.dialer.app.calllog.CallLogNotificationsService;
-import com.android.dialer.app.calllog.ClearCallLogDialog;
import com.android.dialer.app.dialpad.DialpadFragment;
import com.android.dialer.app.list.DragDropController;
import com.android.dialer.app.list.ListsFragment;
@@ -85,6 +84,7 @@ import com.android.dialer.app.list.SpeedDialFragment;
import com.android.dialer.app.settings.DialerSettingsActivity;
import com.android.dialer.app.widget.ActionBarController;
import com.android.dialer.app.widget.SearchEditTextLayout;
+import com.android.dialer.callcomposer.CallComposerActivity;
import com.android.dialer.callintent.CallIntentBuilder;
import com.android.dialer.callintent.nano.CallSpecificAppData;
import com.android.dialer.common.Assert;
@@ -101,7 +101,10 @@ import com.android.dialer.p13n.inference.protocol.P13nRanker;
import com.android.dialer.p13n.inference.protocol.P13nRanker.P13nRefreshCompleteListener;
import com.android.dialer.p13n.logging.P13nLogger;
import com.android.dialer.p13n.logging.P13nLogging;
+import com.android.dialer.postcall.PostCall;
import com.android.dialer.proguard.UsedByReflection;
+import com.android.dialer.simulator.Simulator;
+import com.android.dialer.simulator.SimulatorComponent;
import com.android.dialer.smartdial.SmartDialNameMatcher;
import com.android.dialer.smartdial.SmartDialPrefix;
import com.android.dialer.telecom.TelecomUtil;
@@ -124,7 +127,6 @@ public class DialtactsActivity extends TransactionSafeActivity
OnListFragmentScrolledListener,
CallLogFragment.HostInterface,
DialpadFragment.HostInterface,
- ListsFragment.HostInterface,
SpeedDialFragment.HostInterface,
SearchFragment.HostInterface,
OnDragDropListener,
@@ -478,6 +480,7 @@ public class DialtactsActivity extends TransactionSafeActivity
@Override
protected void onResume() {
+ LogUtil.d("DialtactsActivity.onResume", "");
Trace.beginSection(TAG + " onResume");
super.onResume();
@@ -490,6 +493,8 @@ public class DialtactsActivity extends TransactionSafeActivity
} else if (mShowDialpadOnResume) {
showDialpadFragment(false);
mShowDialpadOnResume = false;
+ } else {
+ PostCall.promptUserForMessageIfNecessary(this, mParentLayout);
}
// If there was a voice query result returned in the {@link #onActivityResult} callback, it
@@ -539,7 +544,7 @@ public class DialtactsActivity extends TransactionSafeActivity
}
if (getIntent().getBooleanExtra(EXTRA_CLEAR_NEW_VOICEMAILS, false)) {
- CallLogNotificationsService.markNewVoicemailsAsOld(this);
+ CallLogNotificationsService.markNewVoicemailsAsOld(this, null);
}
setSearchBoxHint();
@@ -588,6 +593,7 @@ public class DialtactsActivity extends TransactionSafeActivity
@Override
public void onAttachFragment(final Fragment fragment) {
+ LogUtil.d("DialtactsActivity.onAttachFragment", "fragment: %s", fragment);
if (fragment instanceof DialpadFragment) {
mDialpadFragment = (DialpadFragment) fragment;
if (!mIsDialpadShown && !mShowDialpadOnResume) {
@@ -616,7 +622,8 @@ public class DialtactsActivity extends TransactionSafeActivity
@MainThread
public Cursor rerankCursor(Cursor data) {
Assert.isMainThread();
- return mP13nRanker.rankCursor(data, PhoneQuery.PHONE_NUMBER);
+ String queryString = searchFragment.getQueryString();
+ return mP13nRanker.rankCursor(data, queryString == null ? 0 : queryString.length());
}
});
searchFragment.addOnLoadFinishedListener(
@@ -674,9 +681,9 @@ public class DialtactsActivity extends TransactionSafeActivity
}
int resId = item.getItemId();
- if (item.getItemId() == R.id.menu_delete_all) {
- ClearCallLogDialog.show(getFragmentManager());
- return true;
+ if (resId == R.id.menu_history) {
+ final Intent intent = new Intent(this, CallLogActivity.class);
+ startActivity(intent);
} else if (resId == R.id.menu_clear_frequents) {
ClearFrequentsDialog.show(getFragmentManager());
Logger.get(this).logScreenView(ScreenEvent.Type.CLEAR_FREQUENTS, this);
@@ -691,6 +698,11 @@ public class DialtactsActivity extends TransactionSafeActivity
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ LogUtil.i(
+ "DialtactsActivity.onActivityResult",
+ "requestCode:%d, resultCode:%d",
+ requestCode,
+ resultCode);
if (requestCode == ACTIVITY_REQUEST_CODE_VOICE_SEARCH) {
if (resultCode == RESULT_OK) {
final ArrayList<String> matches =
@@ -701,15 +713,16 @@ public class DialtactsActivity extends TransactionSafeActivity
LogUtil.i("DialtactsActivity.onActivityResult", "voice search - nothing heard");
}
} else {
- LogUtil.e("DialtactsActivity.onActivityResult", "voice search failed: " + resultCode);
+ LogUtil.e("DialtactsActivity.onActivityResult", "voice search failed");
}
} else if (requestCode == ACTIVITY_REQUEST_CODE_CALL_COMPOSE) {
- if (resultCode != RESULT_OK) {
+ if (resultCode == RESULT_FIRST_USER) {
LogUtil.i(
- "DialtactsActivity.onActivityResult",
- "returned from call composer, error occurred (resultCode=" + resultCode + ")");
+ "DialtactsActivity.onActivityResult", "returned from call composer, error occurred");
String message =
- getString(R.string.call_composer_connection_failed, getString(R.string.share_and_call));
+ getString(
+ R.string.call_composer_connection_failed,
+ data.getStringExtra(CallComposerActivity.KEY_CONTACT_NAME));
Snackbar.make(mParentLayout, message, Snackbar.LENGTH_LONG).show();
} else {
LogUtil.i("DialtactsActivity.onActivityResult", "returned from call composer, no error");
@@ -732,6 +745,7 @@ public class DialtactsActivity extends TransactionSafeActivity
* @see #onDialpadShown
*/
private void showDialpadFragment(boolean animate) {
+ LogUtil.d("DialtactActivity.showDialpadFragment", "animate: %b", animate);
if (mIsDialpadShown || mStateSaved) {
return;
}
@@ -767,6 +781,7 @@ public class DialtactsActivity extends TransactionSafeActivity
/** Callback from child DialpadFragment when the dialpad is shown. */
public void onDialpadShown() {
+ LogUtil.d("DialtactsActivity.onDialpadShown", "");
Assert.isNotNull(mDialpadFragment);
if (mDialpadFragment.getAnimate()) {
Assert.isNotNull(mDialpadFragment.getView()).startAnimation(mSlideIn);
@@ -838,12 +853,21 @@ public class DialtactsActivity extends TransactionSafeActivity
private void updateSearchFragmentPosition() {
SearchFragment fragment = null;
- if (mSmartDialSearchFragment != null && mSmartDialSearchFragment.isVisible()) {
+ if (mSmartDialSearchFragment != null) {
fragment = mSmartDialSearchFragment;
- } else if (mRegularSearchFragment != null && mRegularSearchFragment.isVisible()) {
+ } else if (mRegularSearchFragment != null) {
fragment = mRegularSearchFragment;
}
- if (fragment != null && fragment.isVisible()) {
+ LogUtil.d(
+ "DialtactsActivity.updateSearchFragmentPosition",
+ "fragment: %s, isVisible: %b",
+ fragment,
+ fragment != null && fragment.isVisible());
+ if (fragment != null) {
+ // We need to force animation here even when fragment is not visible since it might not be
+ // visible immediately after screen orientation change and dialpad height would not be
+ // available immediately which is required to update position. By forcing an animation,
+ // position will be updated after a delay by when the dialpad height would be available.
fragment.updatePosition(true /* animate */);
}
}
@@ -858,11 +882,6 @@ public class DialtactsActivity extends TransactionSafeActivity
return !TextUtils.isEmpty(mSearchQuery);
}
- @Override
- public boolean shouldShowActionBar() {
- return mListsFragment.shouldShowActionBar();
- }
-
private void setNotInSearchUi() {
mInDialpadSearch = false;
mInRegularSearch = false;
@@ -1056,7 +1075,8 @@ public class DialtactsActivity extends TransactionSafeActivity
}
// DialtactsActivity will provide the options menu
fragment.setHasOptionsMenu(false);
- fragment.setShowEmptyListForNullQuery(true);
+ // Will show empty list if P13nRanker is not enabled. Else, re-ranked list by the ranker.
+ fragment.setShowEmptyListForNullQuery(mP13nRanker.shouldShowEmptyListForNullQuery());
if (!smartDialSearch) {
fragment.setQueryString(query);
}
@@ -1361,11 +1381,6 @@ public class DialtactsActivity extends TransactionSafeActivity
}
@Override
- public ActionBarController getActionBarController() {
- return mActionBarController;
- }
-
- @Override
public boolean isDialpadShown() {
return mIsDialpadShown;
}
@@ -1379,11 +1394,6 @@ public class DialtactsActivity extends TransactionSafeActivity
}
@Override
- public int getActionBarHideOffset() {
- return getActionBarSafely().getHideOffset();
- }
-
- @Override
public void setActionBarHideOffset(int offset) {
getActionBarSafely().setHideOffset(offset);
}
@@ -1461,8 +1471,19 @@ public class DialtactsActivity extends TransactionSafeActivity
&& mListsFragment.getSpeedDialFragment().hasFrequents()
&& hasContactsPermission);
- menu.findItem(R.id.menu_delete_all)
+ menu.findItem(R.id.menu_history)
.setVisible(PermissionsUtil.hasPhonePermissions(DialtactsActivity.this));
+
+ Context context = DialtactsActivity.this.getApplicationContext();
+ MenuItem simulatorMenuItem = menu.findItem(R.id.menu_simulator_submenu);
+ Simulator simulator = SimulatorComponent.get(context).getSimulator();
+ if (simulator.shouldShow()) {
+ simulatorMenuItem.setVisible(true);
+ simulatorMenuItem.setActionProvider(simulator.getActionProvider(context));
+ } else {
+ simulatorMenuItem.setVisible(false);
+ }
+
super.show();
}
}
diff --git a/java/com/android/dialer/app/SpecialCharSequenceMgr.java b/java/com/android/dialer/app/SpecialCharSequenceMgr.java
index 2ae19704a..712659c12 100644
--- a/java/com/android/dialer/app/SpecialCharSequenceMgr.java
+++ b/java/com/android/dialer/app/SpecialCharSequenceMgr.java
@@ -28,15 +28,14 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
-import android.os.Looper;
import android.provider.Settings;
import android.support.annotation.Nullable;
+import android.support.v4.os.BuildCompat;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telephony.PhoneNumberUtils;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
-import android.util.Log;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.Toast;
@@ -46,8 +45,11 @@ import com.android.contacts.common.database.NoNullCursorAsyncQueryHandler;
import com.android.contacts.common.util.ContactDisplayUtils;
import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment;
import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment.SelectPhoneAccountListener;
-import com.android.dialer.app.calllog.PhoneAccountUtils;
+import com.android.dialer.calllogutils.PhoneAccountUtils;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
import com.android.dialer.compat.CompatUtils;
+import com.android.dialer.oem.MotorolaUtils;
import com.android.dialer.telecom.TelecomUtil;
import java.util.ArrayList;
import java.util.List;
@@ -100,12 +102,19 @@ public class SpecialCharSequenceMgr {
//get rid of the separators so that the string gets parsed correctly
String dialString = PhoneNumberUtils.stripSeparators(input);
- return handleDeviceIdDisplay(context, dialString)
+ if (handleDeviceIdDisplay(context, dialString)
|| handleRegulatoryInfoDisplay(context, dialString)
|| handlePinEntry(context, dialString)
|| handleAdnEntry(context, dialString, textField)
- || handleSecretCode(context, dialString);
+ || handleSecretCode(context, dialString)) {
+ return true;
+ }
+
+ if (MotorolaUtils.handleSpecialCharSequence(context, input)) {
+ return true;
+ }
+ return false;
}
/**
@@ -114,10 +123,7 @@ public class SpecialCharSequenceMgr {
* <p>This should be called when the screen becomes background.
*/
public static void cleanup() {
- if (Looper.myLooper() != Looper.getMainLooper()) {
- Log.wtf(TAG, "cleanup() is called outside the main thread");
- return;
- }
+ Assert.isMainThread();
if (sPreviousAdnQueryHandler != null) {
sPreviousAdnQueryHandler.cancel();
@@ -126,14 +132,21 @@ public class SpecialCharSequenceMgr {
}
/**
- * Handles secret codes to launch arbitrary activities in the form of *#*#<code>#*#*. If a secret
- * code is encountered an Intent is started with the android_secret_code://<code> URI.
+ * Handles secret codes to launch arbitrary activities in the form of *#*#<code>#*#*.
+ * If a secret code is encountered, an Intent is started with the android_secret_code://<code>
+ * URI.
*
* @param context the context to use
* @param input the text to check for a secret code in
- * @return true if a secret code was encountered
+ * @return true if a secret code was encountered and intent is sent out
*/
static boolean handleSecretCode(Context context, String input) {
+ // Must use system service on O+ to avoid using broadcasts, which are not allowed on O+.
+ if (BuildCompat.isAtLeastO()) {
+ return context.getSystemService(TelephonyManager.class).sendDialerCode(input);
+ }
+
+ // System service call is not supported pre-O, so must use a broadcast for N-.
// Secret codes are in the form *#*#<code>#*#*
int len = input.length();
if (len > 8 && input.startsWith("*#*#") && input.endsWith("#*#*")) {
@@ -144,7 +157,6 @@ public class SpecialCharSequenceMgr {
context.sendBroadcast(intent);
return true;
}
-
return false;
}
@@ -237,7 +249,7 @@ public class SpecialCharSequenceMgr {
private static void handleAdnQuery(QueryHandler handler, SimContactQueryCookie cookie, Uri uri) {
if (handler == null || cookie == null || uri == null) {
- Log.w(TAG, "queryAdn parameters incorrect");
+ LogUtil.w("SpecialCharSequenceMgr.handleAdnQuery", "queryAdn parameters incorrect");
return;
}
@@ -325,12 +337,14 @@ public class SpecialCharSequenceMgr {
private static boolean handleRegulatoryInfoDisplay(Context context, String input) {
if (input.equals(MMI_REGULATORY_INFO_DISPLAY)) {
- Log.d(TAG, "handleRegulatoryInfoDisplay() sending intent to settings app");
+ LogUtil.i(
+ "SpecialCharSequenceMgr.handleRegulatoryInfoDisplay", "sending intent to settings app");
Intent showRegInfoIntent = new Intent(Settings.ACTION_SHOW_REGULATORY_INFO);
try {
context.startActivity(showRegInfoIntent);
} catch (ActivityNotFoundException e) {
- Log.e(TAG, "startActivity() failed: " + e);
+ LogUtil.e(
+ "SpecialCharSequenceMgr.handleRegulatoryInfoDisplay", "startActivity() failed: ", e);
}
return true;
}
diff --git a/java/com/android/dialer/app/calllog/CallDetailHistoryAdapter.java b/java/com/android/dialer/app/calllog/CallDetailHistoryAdapter.java
deleted file mode 100644
index ab6ef7362..000000000
--- a/java/com/android/dialer/app/calllog/CallDetailHistoryAdapter.java
+++ /dev/null
@@ -1,214 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.dialer.app.calllog;
-
-import android.content.Context;
-import android.icu.lang.UCharacter;
-import android.icu.text.BreakIterator;
-import android.os.Build.VERSION;
-import android.os.Build.VERSION_CODES;
-import android.provider.CallLog.Calls;
-import android.text.format.DateUtils;
-import android.text.format.Formatter;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.BaseAdapter;
-import android.widget.TextView;
-import com.android.dialer.app.PhoneCallDetails;
-import com.android.dialer.app.R;
-import com.android.dialer.util.CallUtil;
-import com.android.dialer.util.DialerUtils;
-import java.util.ArrayList;
-import java.util.Locale;
-
-/** Adapter for a ListView containing history items from the details of a call. */
-public class CallDetailHistoryAdapter extends BaseAdapter {
-
- /** Each history item shows the detail of a call. */
- private static final int VIEW_TYPE_HISTORY_ITEM = 1;
-
- private final Context mContext;
- private final LayoutInflater mLayoutInflater;
- private final CallTypeHelper mCallTypeHelper;
- private final PhoneCallDetails[] mPhoneCallDetails;
-
- /** List of items to be concatenated together for duration strings. */
- private ArrayList<CharSequence> mDurationItems = new ArrayList<>();
-
- public CallDetailHistoryAdapter(
- Context context,
- LayoutInflater layoutInflater,
- CallTypeHelper callTypeHelper,
- PhoneCallDetails[] phoneCallDetails) {
- mContext = context;
- mLayoutInflater = layoutInflater;
- mCallTypeHelper = callTypeHelper;
- mPhoneCallDetails = phoneCallDetails;
- }
-
- @Override
- public boolean isEnabled(int position) {
- // None of history will be clickable.
- return false;
- }
-
- @Override
- public int getCount() {
- return mPhoneCallDetails.length;
- }
-
- @Override
- public Object getItem(int position) {
- return mPhoneCallDetails[position];
- }
-
- @Override
- public long getItemId(int position) {
- return position;
- }
-
- @Override
- public int getViewTypeCount() {
- return 1;
- }
-
- @Override
- public int getItemViewType(int position) {
- return VIEW_TYPE_HISTORY_ITEM;
- }
-
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- // Make sure we have a valid convertView to start with
- final View result =
- convertView == null
- ? mLayoutInflater.inflate(R.layout.call_detail_history_item, parent, false)
- : convertView;
-
- PhoneCallDetails details = mPhoneCallDetails[position];
- CallTypeIconsView callTypeIconView =
- (CallTypeIconsView) result.findViewById(R.id.call_type_icon);
- TextView callTypeTextView = (TextView) result.findViewById(R.id.call_type_text);
- TextView dateView = (TextView) result.findViewById(R.id.date);
- TextView durationView = (TextView) result.findViewById(R.id.duration);
-
- int callType = details.callTypes[0];
- boolean isVideoCall =
- (details.features & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO
- && CallUtil.isVideoEnabled(mContext);
- boolean isPulledCall =
- (details.features & Calls.FEATURES_PULLED_EXTERNALLY) == Calls.FEATURES_PULLED_EXTERNALLY;
-
- callTypeIconView.clear();
- callTypeIconView.add(callType);
- callTypeIconView.setShowVideo(isVideoCall);
- callTypeTextView.setText(mCallTypeHelper.getCallTypeText(callType, isVideoCall, isPulledCall));
- // Set the date.
- dateView.setText(formatDate(details.date));
- // Set the duration
- if (Calls.VOICEMAIL_TYPE == callType || CallTypeHelper.isMissedCallType(callType)) {
- durationView.setVisibility(View.GONE);
- } else {
- durationView.setVisibility(View.VISIBLE);
- durationView.setText(formatDurationAndDataUsage(details.duration, details.dataUsage));
- }
-
- return result;
- }
-
- /**
- * Formats the provided date into a value suitable for display in the current locale.
- *
- * <p>For example, returns a string like "Wednesday, May 25, 2016, 8:02PM" or "Chorshanba, 2016
- * may 25,20:02".
- *
- * <p>For pre-N devices, the returned value may not start with a capital if the local convention
- * is to not capitalize day names. On N+ devices, the returned value is always capitalized.
- */
- private CharSequence formatDate(long callDateMillis) {
- CharSequence dateValue =
- DateUtils.formatDateRange(
- mContext,
- callDateMillis /* startDate */,
- callDateMillis /* endDate */,
- DateUtils.FORMAT_SHOW_TIME
- | DateUtils.FORMAT_SHOW_DATE
- | DateUtils.FORMAT_SHOW_WEEKDAY
- | DateUtils.FORMAT_SHOW_YEAR);
-
- // We want the beginning of the date string to be capitalized, even if the word at the beginning
- // of the string is not usually capitalized. For example, "Wednesdsay" in Uzbek is "chorshanba”
- // (not capitalized). To handle this issue we apply title casing to the start of the sentence so
- // that "chorshanba, 2016 may 25,20:02" becomes "Chorshanba, 2016 may 25,20:02".
- //
- // The ICU library was not available in Android until N, so we can only do this in N+ devices.
- // Pre-N devices will still see incorrect capitalization in some languages.
- if (VERSION.SDK_INT < VERSION_CODES.N) {
- return dateValue;
- }
-
- // Using the ICU library is safer than just applying toUpperCase() on the first letter of the
- // word because in some languages, there can be multiple starting characters which should be
- // upper-cased together. For example in Dutch "ij" is a digraph in which both letters should be
- // capitalized together.
-
- // TITLECASE_NO_LOWERCASE is necessary so that things that are already capitalized like the
- // month ("May") are not lower-cased as part of the conversion.
- return UCharacter.toTitleCase(
- Locale.getDefault(),
- dateValue.toString(),
- BreakIterator.getSentenceInstance(),
- UCharacter.TITLECASE_NO_LOWERCASE);
- }
-
- private CharSequence formatDuration(long elapsedSeconds) {
- long minutes = 0;
- long seconds = 0;
-
- if (elapsedSeconds >= 60) {
- minutes = elapsedSeconds / 60;
- elapsedSeconds -= minutes * 60;
- seconds = elapsedSeconds;
- return mContext.getString(R.string.callDetailsDurationFormat, minutes, seconds);
- } else {
- seconds = elapsedSeconds;
- return mContext.getString(R.string.callDetailsShortDurationFormat, seconds);
- }
- }
-
- /**
- * Formats a string containing the call duration and the data usage (if specified).
- *
- * @param elapsedSeconds Total elapsed seconds.
- * @param dataUsage Data usage in bytes, or null if not specified.
- * @return String containing call duration and data usage.
- */
- private CharSequence formatDurationAndDataUsage(long elapsedSeconds, Long dataUsage) {
- CharSequence duration = formatDuration(elapsedSeconds);
-
- if (dataUsage != null) {
- mDurationItems.clear();
- mDurationItems.add(duration);
- mDurationItems.add(Formatter.formatShortFileSize(mContext, dataUsage));
-
- return DialerUtils.join(mDurationItems);
- } else {
- return duration;
- }
- }
-}
diff --git a/java/com/android/dialer/app/calllog/CallLogActivity.java b/java/com/android/dialer/app/calllog/CallLogActivity.java
new file mode 100644
index 000000000..719ab4369
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/CallLogActivity.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.dialer.app.calllog;
+
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.provider.CallLog;
+import android.provider.CallLog.Calls;
+import android.support.v13.app.FragmentPagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.support.v7.app.ActionBar;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.ViewGroup;
+import com.android.contacts.common.list.ViewPagerTabs;
+import com.android.dialer.app.DialtactsActivity;
+import com.android.dialer.app.R;
+import com.android.dialer.database.CallLogQueryHandler;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.ScreenEvent;
+import com.android.dialer.util.TransactionSafeActivity;
+import com.android.dialer.util.ViewUtil;
+
+/** Activity for viewing call history. */
+public class CallLogActivity extends TransactionSafeActivity
+ implements ViewPager.OnPageChangeListener {
+
+ private static final int TAB_INDEX_ALL = 0;
+ private static final int TAB_INDEX_MISSED = 1;
+ private static final int TAB_INDEX_COUNT = 2;
+ private ViewPager mViewPager;
+ private ViewPagerTabs mViewPagerTabs;
+ private ViewPagerAdapter mViewPagerAdapter;
+ private CallLogFragment mAllCallsFragment;
+ private CallLogFragment mMissedCallsFragment;
+ private String[] mTabTitles;
+ private boolean mIsResumed;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.call_log_activity);
+ getWindow().setBackgroundDrawable(null);
+
+ final ActionBar actionBar = getSupportActionBar();
+ actionBar.setDisplayShowHomeEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setElevation(0);
+
+ int startingTab = TAB_INDEX_ALL;
+ final Intent intent = getIntent();
+ if (intent != null) {
+ final int callType = intent.getIntExtra(CallLog.Calls.EXTRA_CALL_TYPE_FILTER, -1);
+ if (callType == CallLog.Calls.MISSED_TYPE) {
+ startingTab = TAB_INDEX_MISSED;
+ }
+ }
+
+ mTabTitles = new String[TAB_INDEX_COUNT];
+ mTabTitles[0] = getString(R.string.call_log_all_title);
+ mTabTitles[1] = getString(R.string.call_log_missed_title);
+
+ mViewPager = (ViewPager) findViewById(R.id.call_log_pager);
+
+ mViewPagerAdapter = new ViewPagerAdapter(getFragmentManager());
+ mViewPager.setAdapter(mViewPagerAdapter);
+ mViewPager.setOffscreenPageLimit(1);
+ mViewPager.setOnPageChangeListener(this);
+
+ mViewPagerTabs = (ViewPagerTabs) findViewById(R.id.viewpager_header);
+
+ mViewPagerTabs.setViewPager(mViewPager);
+ mViewPager.setCurrentItem(startingTab);
+ }
+
+ @Override
+ protected void onResume() {
+ mIsResumed = true;
+ super.onResume();
+ sendScreenViewForChildFragment(mViewPager.getCurrentItem());
+ }
+
+ @Override
+ protected void onPause() {
+ mIsResumed = false;
+ super.onPause();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ final MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.call_log_options, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ final MenuItem itemDeleteAll = menu.findItem(R.id.delete_all);
+ if (mAllCallsFragment != null && itemDeleteAll != null) {
+ // If onPrepareOptionsMenu is called before fragments are loaded, don't do anything.
+ final CallLogAdapter adapter = mAllCallsFragment.getAdapter();
+ itemDeleteAll.setVisible(adapter != null && !adapter.isEmpty());
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (!isSafeToCommitTransactions()) {
+ return true;
+ }
+
+ if (item.getItemId() == android.R.id.home) {
+ final Intent intent = new Intent(this, DialtactsActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ return true;
+ } else if (item.getItemId() == R.id.delete_all) {
+ ClearCallLogDialog.show(getFragmentManager());
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+ mViewPagerTabs.onPageScrolled(position, positionOffset, positionOffsetPixels);
+ }
+
+ @Override
+ public void onPageSelected(int position) {
+ if (mIsResumed) {
+ sendScreenViewForChildFragment(position);
+ }
+ mViewPagerTabs.onPageSelected(position);
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {
+ mViewPagerTabs.onPageScrollStateChanged(state);
+ }
+
+ private void sendScreenViewForChildFragment(int position) {
+ Logger.get(this).logScreenView(ScreenEvent.Type.CALL_LOG_FILTER, this);
+ }
+
+ private int getRtlPosition(int position) {
+ if (ViewUtil.isRtl()) {
+ return mViewPagerAdapter.getCount() - 1 - position;
+ }
+ return position;
+ }
+
+ /** Adapter for the view pager. */
+ public class ViewPagerAdapter extends FragmentPagerAdapter {
+
+ public ViewPagerAdapter(FragmentManager fm) {
+ super(fm);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return getRtlPosition(position);
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ switch (getRtlPosition(position)) {
+ case TAB_INDEX_ALL:
+ return new CallLogFragment(
+ CallLogQueryHandler.CALL_TYPE_ALL, true /* isCallLogActivity */);
+ case TAB_INDEX_MISSED:
+ return new CallLogFragment(Calls.MISSED_TYPE, true /* isCallLogActivity */);
+ }
+ throw new IllegalStateException("No fragment at position " + position);
+ }
+
+ @Override
+ public Object instantiateItem(ViewGroup container, int position) {
+ final CallLogFragment fragment = (CallLogFragment) super.instantiateItem(container, position);
+ switch (position) {
+ case TAB_INDEX_ALL:
+ mAllCallsFragment = fragment;
+ break;
+ case TAB_INDEX_MISSED:
+ mMissedCallsFragment = fragment;
+ break;
+ }
+ return fragment;
+ }
+
+ @Override
+ public CharSequence getPageTitle(int position) {
+ return mTabTitles[position];
+ }
+
+ @Override
+ public int getCount() {
+ return TAB_INDEX_COUNT;
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/calllog/CallLogAdapter.java b/java/com/android/dialer/app/calllog/CallLogAdapter.java
index ea09a8c0a..fc5ffbb29 100644
--- a/java/com/android/dialer/app/calllog/CallLogAdapter.java
+++ b/java/com/android/dialer/app/calllog/CallLogAdapter.java
@@ -47,7 +47,6 @@ import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
import com.android.contacts.common.preference.ContactsPreferences;
import com.android.dialer.app.Bindings;
import com.android.dialer.app.DialtactsActivity;
-import com.android.dialer.app.PhoneCallDetails;
import com.android.dialer.app.R;
import com.android.dialer.app.calllog.CallLogGroupBuilder.GroupCreator;
import com.android.dialer.app.calllog.calllogcache.CallLogCache;
@@ -55,13 +54,19 @@ import com.android.dialer.app.contactinfo.ContactInfoCache;
import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter;
import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter.OnVoicemailDeletedListener;
import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
+import com.android.dialer.calldetails.nano.CallDetailsEntries;
+import com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry;
+import com.android.dialer.calllogutils.PhoneAccountUtils;
+import com.android.dialer.calllogutils.PhoneCallDetails;
import com.android.dialer.common.Assert;
import com.android.dialer.common.AsyncTaskExecutor;
import com.android.dialer.common.AsyncTaskExecutors;
import com.android.dialer.common.LogUtil;
import com.android.dialer.enrichedcall.EnrichedCallCapabilities;
+import com.android.dialer.enrichedcall.EnrichedCallComponent;
import com.android.dialer.enrichedcall.EnrichedCallManager;
import com.android.dialer.enrichedcall.EnrichedCallManager.CapabilitiesListener;
+import com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult;
import com.android.dialer.logging.Logger;
import com.android.dialer.logging.nano.DialerImpression;
import com.android.dialer.phonenumbercache.CallLogQuery;
@@ -70,6 +75,7 @@ import com.android.dialer.phonenumbercache.ContactInfoHelper;
import com.android.dialer.phonenumberutil.PhoneNumberHelper;
import com.android.dialer.spam.Spam;
import com.android.dialer.util.PermissionsUtil;
+import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -96,7 +102,7 @@ public class CallLogAdapter extends GroupingListAdapter
protected final CallLogCache mCallLogCache;
private final CallFetcher mCallFetcher;
- private final FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler;
+ @NonNull private final FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler;
private final int mActivityType;
/** Instance of helper class for managing views. */
@@ -182,8 +188,6 @@ public class CallLogAdapter extends GroupingListAdapter
private boolean mIsSpamEnabled;
- @NonNull private final EnrichedCallManager mEnrichedCallManager;
-
public CallLogAdapter(
Activity activity,
ViewGroup alertContainer,
@@ -191,6 +195,7 @@ public class CallLogAdapter extends GroupingListAdapter
CallLogCache callLogCache,
ContactInfoCache contactInfoCache,
VoicemailPlaybackPresenter voicemailPlaybackPresenter,
+ @NonNull FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler,
int activityType) {
super();
@@ -218,7 +223,7 @@ public class CallLogAdapter extends GroupingListAdapter
mCallLogListItemHelper =
new CallLogListItemHelper(phoneCallDetailsHelper, resources, mCallLogCache);
mCallLogGroupBuilder = new CallLogGroupBuilder(this);
- mFilteredNumberAsyncQueryHandler = new FilteredNumberAsyncQueryHandler(mActivity);
+ mFilteredNumberAsyncQueryHandler = Assert.isNotNull(filteredNumberAsyncQueryHandler);
mContactsPreferences = new ContactsPreferences(mActivity);
@@ -232,7 +237,6 @@ public class CallLogAdapter extends GroupingListAdapter
mCallLogAlertManager =
new CallLogAlertManager(this, LayoutInflater.from(mActivity), alertContainer);
- mEnrichedCallManager = EnrichedCallManager.Accessor.getInstance(activity.getApplication());
}
private void expandViewHolderActions(CallLogListItemViewHolder viewHolder) {
@@ -296,7 +300,7 @@ public class CallLogAdapter extends GroupingListAdapter
}
mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY);
mIsSpamEnabled = Spam.get(mActivity).isSpamEnabled();
- mEnrichedCallManager.registerCapabilitiesListener(this);
+ getEnrichedCallManager().registerCapabilitiesListener(this);
notifyDataSetChanged();
}
@@ -305,11 +309,11 @@ public class CallLogAdapter extends GroupingListAdapter
for (Uri uri : mHiddenItemUris) {
CallLogAsyncTaskUtil.deleteVoicemail(mActivity, uri, null);
}
- mEnrichedCallManager.unregisterCapabilitiesListener(this);
+ getEnrichedCallManager().unregisterCapabilitiesListener(this);
}
public void onStop() {
- mEnrichedCallManager.clearCachedData();
+ getEnrichedCallManager().clearCachedData();
}
public CallLogAlertManager getAlertManager() {
@@ -420,7 +424,9 @@ public class CallLogAdapter extends GroupingListAdapter
}
CallLogListItemViewHolder views = (CallLogListItemViewHolder) viewHolder;
views.isLoaded = false;
- PhoneCallDetails details = createPhoneCallDetails(c, getGroupSize(position), views);
+ int groupSize = getGroupSize(position);
+ CallDetailsEntries callDetailsEntries = createCallDetailsEntries(c, groupSize);
+ PhoneCallDetails details = createPhoneCallDetails(c, groupSize, views);
if (mHiddenRowIds.contains(c.getLong(CallLogQuery.ID))) {
views.callLogEntryView.setVisibility(View.GONE);
views.dayGroupHeader.setVisibility(View.GONE);
@@ -432,11 +438,14 @@ public class CallLogAdapter extends GroupingListAdapter
if (mCurrentlyExpandedRowId == views.rowId) {
views.inflateActionViewStub();
}
- loadAndRender(views, views.rowId, details);
+ loadAndRender(views, views.rowId, details, callDetailsEntries);
}
private void loadAndRender(
- final CallLogListItemViewHolder views, final long rowId, final PhoneCallDetails details) {
+ final CallLogListItemViewHolder views,
+ final long rowId,
+ final PhoneCallDetails details,
+ final CallDetailsEntries callDetailsEntries) {
// Reset block and spam information since this view could be reused which may contain
// outdated data.
views.isSpam = false;
@@ -464,12 +473,33 @@ public class CallLogAdapter extends GroupingListAdapter
&& Spam.get(mActivity)
.checkSpamStatusSynchronous(views.number, views.countryIso);
details.isSpam = views.isSpam;
- if (isCancelled()) {
- return false;
+ }
+ if (isCancelled()) {
+ return false;
+ }
+ setCallDetailsEntriesHistoryResults(
+ PhoneNumberUtils.formatNumberToE164(views.number, views.countryIso),
+ callDetailsEntries);
+ views.setDetailedPhoneDetails(callDetailsEntries);
+ return !isCancelled() && loadData(views, rowId, details);
+ }
+
+ private void setCallDetailsEntriesHistoryResults(
+ @Nullable String number, CallDetailsEntries callDetailsEntries) {
+ if (number == null) {
+ return;
+ }
+ Map<CallDetailsEntry, List<HistoryResult>> mappedResults =
+ getEnrichedCallManager().getAllHistoricalData(number, callDetailsEntries);
+ for (CallDetailsEntry entry : callDetailsEntries.entries) {
+ List<HistoryResult> results = mappedResults.get(entry);
+ if (results != null) {
+ entry.historyResults = mappedResults.get(entry).toArray(new HistoryResult[0]);
+ LogUtil.v(
+ "CallLogAdapter.setCallDetailsEntriesHistoryResults",
+ "mapped %d results",
+ entry.historyResults.length);
}
- return loadData(views, rowId, details);
- } else {
- return loadData(views, rowId, details);
}
}
@@ -499,9 +529,9 @@ public class CallLogAdapter extends GroupingListAdapter
return false;
}
- EnrichedCallCapabilities capabilities = mEnrichedCallManager.getCapabilities(e164Number);
+ EnrichedCallCapabilities capabilities = getEnrichedCallManager().getCapabilities(e164Number);
if (capabilities == null) {
- mEnrichedCallManager.requestCapabilities(e164Number);
+ getEnrichedCallManager().requestCapabilities(e164Number);
return false;
}
return capabilities.supportsCallComposer();
@@ -562,6 +592,27 @@ public class CallLogAdapter extends GroupingListAdapter
return details;
}
+ @MainThread
+ private static CallDetailsEntries createCallDetailsEntries(Cursor cursor, int count) {
+ Assert.isMainThread();
+ int position = cursor.getPosition();
+ CallDetailsEntries entries = new CallDetailsEntries();
+ entries.entries = new CallDetailsEntry[count];
+ for (int i = 0; i < count; i++) {
+ CallDetailsEntry entry = new CallDetailsEntry();
+ entry.callId = cursor.getLong(CallLogQuery.ID);
+ entry.callType = cursor.getInt(CallLogQuery.CALL_TYPE);
+ entry.dataUsage = cursor.getLong(CallLogQuery.DATA_USAGE);
+ entry.date = cursor.getLong(CallLogQuery.DATE);
+ entry.duration = cursor.getLong(CallLogQuery.DURATION);
+ entry.features |= cursor.getInt(CallLogQuery.FEATURES);
+ entries.entries[i] = entry;
+ cursor.moveToNext();
+ }
+ cursor.moveToPosition(position);
+ return entries;
+ }
+
/**
* Load data for call log. Any expensive operation should be put here to avoid blocking main
* thread. Do NOT put any cursor operation here since it's not thread safe.
@@ -907,6 +958,11 @@ public class CallLogAdapter extends GroupingListAdapter
notifyDataSetChanged();
}
+ @NonNull
+ private EnrichedCallManager getEnrichedCallManager() {
+ return EnrichedCallComponent.get(mActivity).getEnrichedCallManager();
+ }
+
/** Interface used to initiate a refresh of the content. */
public interface CallFetcher {
diff --git a/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java b/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java
index b4e6fc5ad..2198626d6 100644
--- a/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java
+++ b/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java
@@ -16,37 +16,22 @@
package com.android.dialer.app.calllog;
-import android.Manifest.permission;
import android.annotation.TargetApi;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
-import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.provider.CallLog;
import android.provider.VoicemailContract.Voicemails;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
-import android.support.v4.content.ContextCompat;
-import android.telecom.PhoneAccountHandle;
import android.text.TextUtils;
-import com.android.contacts.common.GeoUtil;
-import com.android.dialer.app.PhoneCallDetails;
import com.android.dialer.common.AsyncTaskExecutor;
import com.android.dialer.common.AsyncTaskExecutors;
-import com.android.dialer.common.LogUtil;
-import com.android.dialer.phonenumbercache.ContactInfo;
-import com.android.dialer.phonenumbercache.ContactInfoHelper;
-import com.android.dialer.phonenumberutil.PhoneNumberHelper;
-import com.android.dialer.telecom.TelecomUtil;
import com.android.dialer.util.PermissionsUtil;
-import java.util.ArrayList;
-import java.util.Arrays;
+import com.android.voicemail.VoicemailClient;
@TargetApi(VERSION_CODES.M)
public class CallLogAsyncTaskUtil {
@@ -58,166 +43,6 @@ public class CallLogAsyncTaskUtil {
sAsyncTaskExecutor = AsyncTaskExecutors.createThreadPoolExecutor();
}
- public static void getCallDetails(
- @NonNull final Context context,
- @Nullable final CallLogAsyncTaskListener callLogAsyncTaskListener,
- @NonNull final Uri... callUris) {
- if (sAsyncTaskExecutor == null) {
- initTaskExecutor();
- }
-
- sAsyncTaskExecutor.submit(
- Tasks.GET_CALL_DETAILS,
- new AsyncTask<Void, Void, PhoneCallDetails[]>() {
- @Override
- public PhoneCallDetails[] doInBackground(Void... params) {
- if (ContextCompat.checkSelfPermission(context, permission.READ_CALL_LOG)
- != PackageManager.PERMISSION_GRANTED) {
- LogUtil.w("CallLogAsyncTaskUtil.getCallDetails", "missing READ_CALL_LOG permission");
- return null;
- }
- // TODO: All calls correspond to the same person, so make a single lookup.
- final int numCalls = callUris.length;
- PhoneCallDetails[] details = new PhoneCallDetails[numCalls];
- try {
- for (int index = 0; index < numCalls; ++index) {
- details[index] = getPhoneCallDetailsForUri(context, callUris[index]);
- }
- return details;
- } catch (IllegalArgumentException e) {
- // Something went wrong reading in our primary data.
- LogUtil.e(
- "CallLogAsyncTaskUtil.getCallDetails", "invalid URI starting call details", e);
- return null;
- }
- }
-
- @Override
- public void onPostExecute(PhoneCallDetails[] phoneCallDetails) {
- if (callLogAsyncTaskListener != null) {
- callLogAsyncTaskListener.onGetCallDetails(phoneCallDetails);
- }
- }
- });
- }
-
- /** Return the phone call details for a given call log URI. */
- private static PhoneCallDetails getPhoneCallDetailsForUri(
- @NonNull Context context, @NonNull Uri callUri) {
- Cursor cursor =
- context
- .getContentResolver()
- .query(callUri, CallDetailQuery.CALL_LOG_PROJECTION, null, null, null);
-
- try {
- if (cursor == null || !cursor.moveToFirst()) {
- throw new IllegalArgumentException("Cannot find content: " + callUri);
- }
-
- // Read call log.
- final String countryIso = cursor.getString(CallDetailQuery.COUNTRY_ISO_COLUMN_INDEX);
- final String number = cursor.getString(CallDetailQuery.NUMBER_COLUMN_INDEX);
- final String postDialDigits =
- (VERSION.SDK_INT >= VERSION_CODES.N)
- ? cursor.getString(CallDetailQuery.POST_DIAL_DIGITS)
- : "";
- final String viaNumber =
- (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallDetailQuery.VIA_NUMBER) : "";
- final int numberPresentation =
- cursor.getInt(CallDetailQuery.NUMBER_PRESENTATION_COLUMN_INDEX);
-
- final PhoneAccountHandle accountHandle =
- PhoneAccountUtils.getAccount(
- cursor.getString(CallDetailQuery.ACCOUNT_COMPONENT_NAME),
- cursor.getString(CallDetailQuery.ACCOUNT_ID));
-
- // If this is not a regular number, there is no point in looking it up in the contacts.
- ContactInfoHelper contactInfoHelper =
- new ContactInfoHelper(context, GeoUtil.getCurrentCountryIso(context));
- boolean isVoicemail = PhoneNumberHelper.isVoicemailNumber(context, accountHandle, number);
- boolean shouldLookupNumber =
- PhoneNumberHelper.canPlaceCallsTo(number, numberPresentation) && !isVoicemail;
- ContactInfo info = ContactInfo.EMPTY;
-
- if (shouldLookupNumber) {
- ContactInfo lookupInfo = contactInfoHelper.lookupNumber(number, countryIso);
- info = lookupInfo != null ? lookupInfo : ContactInfo.EMPTY;
- }
-
- PhoneCallDetails details = new PhoneCallDetails(number, numberPresentation, postDialDigits);
- details.updateDisplayNumber(context, info.formattedNumber, isVoicemail);
-
- details.viaNumber = viaNumber;
- details.accountHandle = accountHandle;
- details.contactUri = info.lookupUri;
- details.namePrimary = info.name;
- details.nameAlternative = info.nameAlternative;
- details.numberType = info.type;
- details.numberLabel = info.label;
- details.photoUri = info.photoUri;
- details.sourceType = info.sourceType;
- details.objectId = info.objectId;
-
- details.callTypes = new int[] {cursor.getInt(CallDetailQuery.CALL_TYPE_COLUMN_INDEX)};
- details.date = cursor.getLong(CallDetailQuery.DATE_COLUMN_INDEX);
- details.duration = cursor.getLong(CallDetailQuery.DURATION_COLUMN_INDEX);
- details.features = cursor.getInt(CallDetailQuery.FEATURES);
- details.geocode = cursor.getString(CallDetailQuery.GEOCODED_LOCATION_COLUMN_INDEX);
- details.transcription = cursor.getString(CallDetailQuery.TRANSCRIPTION_COLUMN_INDEX);
-
- details.countryIso =
- !TextUtils.isEmpty(countryIso) ? countryIso : GeoUtil.getCurrentCountryIso(context);
-
- if (!cursor.isNull(CallDetailQuery.DATA_USAGE)) {
- details.dataUsage = cursor.getLong(CallDetailQuery.DATA_USAGE);
- }
-
- return details;
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
- }
-
- /**
- * Delete specified calls from the call log.
- *
- * @param context The context.
- * @param callIds String of the callIds to delete from the call log, delimited by commas (",").
- * @param callLogAsyncTaskListener The listener to invoke after the entries have been deleted.
- */
- public static void deleteCalls(
- @NonNull final Context context,
- final String callIds,
- @Nullable final CallLogAsyncTaskListener callLogAsyncTaskListener) {
- if (sAsyncTaskExecutor == null) {
- initTaskExecutor();
- }
-
- sAsyncTaskExecutor.submit(
- Tasks.DELETE_CALL,
- new AsyncTask<Void, Void, Void>() {
- @Override
- public Void doInBackground(Void... params) {
- context
- .getContentResolver()
- .delete(
- TelecomUtil.getCallLogUri(context),
- CallLog.Calls._ID + " IN (" + callIds + ")",
- null);
- return null;
- }
-
- @Override
- public void onPostExecute(Void result) {
- if (callLogAsyncTaskListener != null) {
- callLogAsyncTaskListener.onDeleteCall();
- }
- }
- });
- }
-
public static void markVoicemailAsRead(
@NonNull final Context context, @NonNull final Uri voicemailUri) {
if (sAsyncTaskExecutor == null) {
@@ -235,6 +60,8 @@ public class CallLogAsyncTaskUtil {
.getContentResolver()
.update(voicemailUri, values, Voicemails.IS_READ + " = 0", null);
+ uploadVoicemailLocalChangesToServer(context);
+
Intent intent = new Intent(context, CallLogNotificationsService.class);
intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD);
context.startService(intent);
@@ -256,7 +83,12 @@ public class CallLogAsyncTaskUtil {
new AsyncTask<Void, Void, Void>() {
@Override
public Void doInBackground(Void... params) {
- context.getContentResolver().delete(voicemailUri, null, null);
+ ContentValues values = new ContentValues();
+ values.put(Voicemails.DELETED, "1");
+ context.getContentResolver().update(voicemailUri, values, null, null);
+ // TODO(b/35440541): check which source package is changed. Don't need
+ // to upload changes on foreign voicemails, they will get a PROVIDER_CHANGED
+ uploadVoicemailLocalChangesToServer(context);
return null;
}
@@ -305,11 +137,6 @@ public class CallLogAsyncTaskUtil {
});
}
- @VisibleForTesting
- public static void resetForTest() {
- sAsyncTaskExecutor = null;
- }
-
/** The enumeration of {@link AsyncTask} objects used in this class. */
public enum Tasks {
DELETE_VOICEMAIL,
@@ -321,56 +148,12 @@ public class CallLogAsyncTaskUtil {
}
public interface CallLogAsyncTaskListener {
-
- void onDeleteCall();
-
void onDeleteVoicemail();
-
- void onGetCallDetails(PhoneCallDetails[] details);
}
- private static final class CallDetailQuery {
-
- public static final String[] CALL_LOG_PROJECTION;
- static final int DATE_COLUMN_INDEX = 0;
- static final int DURATION_COLUMN_INDEX = 1;
- static final int NUMBER_COLUMN_INDEX = 2;
- static final int CALL_TYPE_COLUMN_INDEX = 3;
- static final int COUNTRY_ISO_COLUMN_INDEX = 4;
- static final int GEOCODED_LOCATION_COLUMN_INDEX = 5;
- static final int NUMBER_PRESENTATION_COLUMN_INDEX = 6;
- static final int ACCOUNT_COMPONENT_NAME = 7;
- static final int ACCOUNT_ID = 8;
- static final int FEATURES = 9;
- static final int DATA_USAGE = 10;
- static final int TRANSCRIPTION_COLUMN_INDEX = 11;
- static final int POST_DIAL_DIGITS = 12;
- static final int VIA_NUMBER = 13;
- private static final String[] CALL_LOG_PROJECTION_INTERNAL =
- new String[] {
- CallLog.Calls.DATE,
- CallLog.Calls.DURATION,
- CallLog.Calls.NUMBER,
- CallLog.Calls.TYPE,
- CallLog.Calls.COUNTRY_ISO,
- CallLog.Calls.GEOCODED_LOCATION,
- CallLog.Calls.NUMBER_PRESENTATION,
- CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME,
- CallLog.Calls.PHONE_ACCOUNT_ID,
- CallLog.Calls.FEATURES,
- CallLog.Calls.DATA_USAGE,
- CallLog.Calls.TRANSCRIPTION
- };
-
- static {
- ArrayList<String> projectionList = new ArrayList<>();
- projectionList.addAll(Arrays.asList(CALL_LOG_PROJECTION_INTERNAL));
- if (VERSION.SDK_INT >= VERSION_CODES.N) {
- projectionList.add(CallLog.Calls.POST_DIAL_DIGITS);
- projectionList.add(CallLog.Calls.VIA_NUMBER);
- }
- projectionList.trimToSize();
- CALL_LOG_PROJECTION = projectionList.toArray(new String[projectionList.size()]);
- }
+ private static void uploadVoicemailLocalChangesToServer(Context context) {
+ Intent intent = new Intent(VoicemailClient.ACTION_UPLOAD);
+ intent.setPackage(context.getPackageName());
+ context.sendBroadcast(intent);
}
}
diff --git a/java/com/android/dialer/app/calllog/CallLogFragment.java b/java/com/android/dialer/app/calllog/CallLogFragment.java
index 1ae68cd65..4abef3430 100644
--- a/java/com/android/dialer/app/calllog/CallLogFragment.java
+++ b/java/com/android/dialer/app/calllog/CallLogFragment.java
@@ -53,6 +53,7 @@ import com.android.dialer.app.list.ListsFragment.ListsPage;
import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter;
import com.android.dialer.app.widget.EmptyContentView;
import com.android.dialer.app.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
import com.android.dialer.common.LogUtil;
import com.android.dialer.database.CallLogQueryHandler;
import com.android.dialer.phonenumbercache.ContactInfoHelper;
@@ -70,9 +71,17 @@ public class CallLogFragment extends Fragment
FragmentCompat.OnRequestPermissionsResultCallback,
CallLogModalAlertManager.Listener {
private static final String KEY_FILTER_TYPE = "filter_type";
+ private static final String KEY_LOG_LIMIT = "log_limit";
+ private static final String KEY_DATE_LIMIT = "date_limit";
+ private static final String KEY_IS_CALL_LOG_ACTIVITY = "is_call_log_activity";
private static final String KEY_HAS_READ_CALL_LOG_PERMISSION = "has_read_call_log_permission";
private static final String KEY_REFRESH_DATA_REQUIRED = "refresh_data_required";
+ // No limit specified for the number of logs to show; use the CallLogQueryHandler's default.
+ private static final int NO_LOG_LIMIT = -1;
+ // No date-based filtering.
+ private static final int NO_DATE_LIMIT = 0;
+
private static final int READ_CALL_LOG_PERMISSION_REQUEST_CODE = 1;
private static final int EVENT_UPDATE_DISPLAY = 1;
@@ -104,8 +113,17 @@ public class CallLogFragment extends Fragment
// Exactly same variable is in Fragment as a package private.
private boolean mMenuVisible = true;
// Default to all calls.
- protected int mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL;
-
+ private int mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL;
+ // Log limit - if no limit is specified, then the default in {@link CallLogQueryHandler}
+ // will be used.
+ private int mLogLimit = NO_LOG_LIMIT;
+ // Date limit (in millis since epoch) - when non-zero, only calls which occurred on or after
+ // the date filter are included. If zero, no date-based filtering occurs.
+ private long mDateLimit = NO_DATE_LIMIT;
+ /*
+ * True if this instance of the CallLogFragment shown in the CallLogActivity.
+ */
+ private boolean mIsCallLogActivity = false;
private final Handler mDisplayUpdateHandler =
new Handler() {
@Override
@@ -121,6 +139,48 @@ public class CallLogFragment extends Fragment
protected CallLogModalAlertManager mModalAlertManager;
private ViewGroup mModalAlertView;
+ public CallLogFragment() {
+ this(CallLogQueryHandler.CALL_TYPE_ALL, NO_LOG_LIMIT);
+ }
+
+ public CallLogFragment(int filterType) {
+ this(filterType, NO_LOG_LIMIT);
+ }
+
+ public CallLogFragment(int filterType, boolean isCallLogActivity) {
+ this(filterType, NO_LOG_LIMIT);
+ mIsCallLogActivity = isCallLogActivity;
+ }
+
+ public CallLogFragment(int filterType, int logLimit) {
+ this(filterType, logLimit, NO_DATE_LIMIT);
+ }
+
+ /**
+ * Creates a call log fragment, filtering to include only calls of the desired type, occurring
+ * after the specified date.
+ *
+ * @param filterType type of calls to include.
+ * @param dateLimit limits results to calls occurring on or after the specified date.
+ */
+ public CallLogFragment(int filterType, long dateLimit) {
+ this(filterType, NO_LOG_LIMIT, dateLimit);
+ }
+
+ /**
+ * Creates a call log fragment, filtering to include only calls of the desired type, occurring
+ * after the specified date. Also provides a means to limit the number of results returned.
+ *
+ * @param filterType type of calls to include.
+ * @param logLimit limits the number of results to return.
+ * @param dateLimit limits results to calls occurring on or after the specified date.
+ */
+ public CallLogFragment(int filterType, int logLimit, long dateLimit) {
+ mCallTypeFilter = filterType;
+ mLogLimit = logLimit;
+ mDateLimit = dateLimit;
+ }
+
@Override
public void onCreate(Bundle state) {
LogUtil.d("CallLogFragment.onCreate", toString());
@@ -128,13 +188,16 @@ public class CallLogFragment extends Fragment
mRefreshDataRequired = true;
if (state != null) {
mCallTypeFilter = state.getInt(KEY_FILTER_TYPE, mCallTypeFilter);
+ mLogLimit = state.getInt(KEY_LOG_LIMIT, mLogLimit);
+ mDateLimit = state.getLong(KEY_DATE_LIMIT, mDateLimit);
+ mIsCallLogActivity = state.getBoolean(KEY_IS_CALL_LOG_ACTIVITY, mIsCallLogActivity);
mHasReadCallLogPermission = state.getBoolean(KEY_HAS_READ_CALL_LOG_PERMISSION, false);
mRefreshDataRequired = state.getBoolean(KEY_REFRESH_DATA_REQUIRED, mRefreshDataRequired);
}
final Activity activity = getActivity();
final ContentResolver resolver = activity.getContentResolver();
- mCallLogQueryHandler = new CallLogQueryHandler(activity, resolver, this);
+ mCallLogQueryHandler = new CallLogQueryHandler(activity, resolver, this, mLogLimit);
mKeyguardManager = (KeyguardManager) activity.getSystemService(Context.KEYGUARD_SERVICE);
resolver.registerContentObserver(CallLog.CONTENT_URI, true, mCallLogObserver);
resolver.registerContentObserver(
@@ -226,7 +289,10 @@ public class CallLogFragment extends Fragment
}
protected void setupData() {
- int activityType = CallLogAdapter.ACTIVITY_TYPE_DIALTACTS;
+ int activityType =
+ mIsCallLogActivity
+ ? CallLogAdapter.ACTIVITY_TYPE_CALL_LOG
+ : CallLogAdapter.ACTIVITY_TYPE_DIALTACTS;
String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
mContactInfoCache =
@@ -244,6 +310,7 @@ public class CallLogFragment extends Fragment
CallLogCache.getCallLogCache(getActivity()),
mContactInfoCache,
getVoicemailPlaybackPresenter(),
+ new FilteredNumberAsyncQueryHandler(getActivity()),
activityType);
mRecyclerView.setAdapter(mAdapter);
fetchCalls();
@@ -324,6 +391,9 @@ public class CallLogFragment extends Fragment
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(KEY_FILTER_TYPE, mCallTypeFilter);
+ outState.putInt(KEY_LOG_LIMIT, mLogLimit);
+ outState.putLong(KEY_DATE_LIMIT, mDateLimit);
+ outState.putBoolean(KEY_IS_CALL_LOG_ACTIVITY, mIsCallLogActivity);
outState.putBoolean(KEY_HAS_READ_CALL_LOG_PERMISSION, mHasReadCallLogPermission);
outState.putBoolean(KEY_REFRESH_DATA_REQUIRED, mRefreshDataRequired);
@@ -334,8 +404,10 @@ public class CallLogFragment extends Fragment
@Override
public void fetchCalls() {
- mCallLogQueryHandler.fetchCalls(mCallTypeFilter);
- ((ListsFragment) getParentFragment()).updateTabUnreadCounts();
+ mCallLogQueryHandler.fetchCalls(mCallTypeFilter, mDateLimit);
+ if (!mIsCallLogActivity) {
+ ((ListsFragment) getParentFragment()).updateTabUnreadCounts();
+ }
}
private void updateEmptyMessage(int filterType) {
@@ -366,7 +438,9 @@ public class CallLogFragment extends Fragment
"Unexpected filter type in CallLogFragment: " + filterType);
}
mEmptyListView.setDescription(messageId);
- if (filterType == CallLogQueryHandler.CALL_TYPE_ALL) {
+ if (mIsCallLogActivity) {
+ mEmptyListView.setActionLabel(EmptyContentView.NO_LABEL);
+ } else if (filterType == CallLogQueryHandler.CALL_TYPE_ALL) {
mEmptyListView.setActionLabel(R.string.call_log_all_empty_action);
}
}
@@ -420,7 +494,7 @@ public class CallLogFragment extends Fragment
if (mKeyguardManager != null
&& !mKeyguardManager.inKeyguardRestrictedInputMode()
&& mCallTypeFilter == Calls.VOICEMAIL_TYPE) {
- CallLogNotificationsHelper.updateVoicemailNotifications(getActivity());
+ CallLogNotificationsQueryHelper.updateVoicemailNotifications(getActivity());
}
}
@@ -434,7 +508,8 @@ public class CallLogFragment extends Fragment
if (!PermissionsUtil.hasPermission(activity, READ_CALL_LOG)) {
FragmentCompat.requestPermissions(
this, new String[] {READ_CALL_LOG}, READ_CALL_LOG_PERMISSION_REQUEST_CODE);
- } else {
+ } else if (!mIsCallLogActivity) {
+ // Show dialpad if we are not in the call log activity.
((HostInterface) activity).showDialpad();
}
}
diff --git a/java/com/android/dialer/app/calllog/CallLogListItemHelper.java b/java/com/android/dialer/app/calllog/CallLogListItemHelper.java
index ea2119c83..a5df8cca1 100644
--- a/java/com/android/dialer/app/calllog/CallLogListItemHelper.java
+++ b/java/com/android/dialer/app/calllog/CallLogListItemHelper.java
@@ -21,18 +21,16 @@ import android.provider.CallLog.Calls;
import android.support.annotation.WorkerThread;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
-import android.util.Log;
-import com.android.dialer.app.PhoneCallDetails;
import com.android.dialer.app.R;
import com.android.dialer.app.calllog.calllogcache.CallLogCache;
+import com.android.dialer.calllogutils.PhoneCallDetails;
import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
import com.android.dialer.compat.AppCompatConstants;
/** Helper class to fill in the views of a call log entry. */
/* package */ class CallLogListItemHelper {
- private static final String TAG = "CallLogListItemHelper";
-
/** Helper for populating the details of a phone call. */
private final PhoneCallDetailsHelper mPhoneCallDetailsHelper;
/** Resources to look up strings. */
@@ -105,7 +103,9 @@ import com.android.dialer.compat.AppCompatConstants;
*/
public void setActionContentDescriptions(CallLogListItemViewHolder views) {
if (views.nameOrNumber == null) {
- Log.e(TAG, "setActionContentDescriptions; name or number is null.");
+ LogUtil.e(
+ "CallLogListItemHelper.setActionContentDescriptions",
+ "setActionContentDescriptions; name or number is null.");
}
// Calling expandTemplate with a null parameter will cause a NullPointerException.
@@ -170,7 +170,6 @@ import com.android.dialer.compat.AppCompatConstants;
*
* <p>2 calls. Answered call from John Doe mobile 1 hour ago.
*
- * @param context The application context.
* @param details Details of call.
* @return Return call action description.
*/
diff --git a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
index 6abd36078..8a2d94499 100644
--- a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
+++ b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
@@ -25,9 +25,11 @@ import android.os.AsyncTask;
import android.provider.CallLog;
import android.provider.CallLog.Calls;
import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.support.annotation.VisibleForTesting;
import android.support.v7.widget.CardView;
import android.support.v7.widget.RecyclerView;
import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
import android.telephony.PhoneNumberUtils;
import android.text.BidiFormatter;
import android.text.TextDirectionHeuristics;
@@ -56,7 +58,7 @@ import com.android.dialer.blocking.FilteredNumberCompat;
import com.android.dialer.blocking.FilteredNumbersUtil;
import com.android.dialer.callcomposer.CallComposerActivity;
import com.android.dialer.callcomposer.nano.CallComposerContact;
-import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.calldetails.nano.CallDetailsEntries;
import com.android.dialer.common.LogUtil;
import com.android.dialer.compat.CompatUtils;
import com.android.dialer.logging.Logger;
@@ -78,8 +80,6 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
implements View.OnClickListener,
MenuItem.OnMenuItemClickListener,
View.OnCreateContextMenuListener {
- private static final String CONFIG_SHARE_VOICEMAIL_ALLOWED = "share_voicemail_allowed";
-
/** The root view of the call log list item */
public final View rootView;
/** The quick contact badge for the contact. */
@@ -201,6 +201,7 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
public boolean isAttachedToWindow;
public AsyncTask<Void, Void, ?> asyncTask;
+ private CallDetailsEntries callDetailsEntries;
private CallLogListItemViewHolder(
Context context,
@@ -549,10 +550,6 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
}
}
- private static boolean isShareVoicemailAllowed(Context context) {
- return ConfigProviderBindings.get(context).getBoolean(CONFIG_SHARE_VOICEMAIL_ALLOWED, true);
- }
-
/**
* Binds text titles, click handlers and intents to the voicemail, details and callback action
* buttons.
@@ -577,13 +574,14 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
unblockView.setVisibility(View.GONE);
reportNotSpamView.setVisibility(View.GONE);
- if (isShareVoicemailAllowed(mContext)) {
- sendVoicemailButtonView.setVisibility(View.VISIBLE);
- }
voicemailPlaybackView.setVisibility(View.VISIBLE);
Uri uri = Uri.parse(voicemailUri);
mVoicemailPlaybackPresenter.setPlaybackView(
- voicemailPlaybackView, rowId, uri, mVoicemailPrimaryActionButtonClicked);
+ voicemailPlaybackView,
+ rowId,
+ uri,
+ mVoicemailPrimaryActionButtonClicked,
+ sendVoicemailButtonView);
mVoicemailPrimaryActionButtonClicked = false;
CallLogAsyncTaskUtil.markVoicemailAsRead(mContext, uri);
return;
@@ -621,14 +619,14 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
&& mVoicemailPlaybackPresenter != null
&& !TextUtils.isEmpty(voicemailUri)) {
voicemailPlaybackView.setVisibility(View.VISIBLE);
- if (isShareVoicemailAllowed(mContext)) {
- Logger.get(mContext).logImpression(DialerImpression.Type.VVM_SHARE_VISIBLE);
- sendVoicemailButtonView.setVisibility(View.VISIBLE);
- }
Uri uri = Uri.parse(voicemailUri);
mVoicemailPlaybackPresenter.setPlaybackView(
- voicemailPlaybackView, rowId, uri, mVoicemailPrimaryActionButtonClicked);
+ voicemailPlaybackView,
+ rowId,
+ uri,
+ mVoicemailPrimaryActionButtonClicked,
+ sendVoicemailButtonView);
mVoicemailPrimaryActionButtonClicked = false;
CallLogAsyncTaskUtil.markVoicemailAsRead(mContext, uri);
} else {
@@ -640,7 +638,8 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
detailsButtonView.setVisibility(View.GONE);
} else {
detailsButtonView.setVisibility(View.VISIBLE);
- detailsButtonView.setTag(IntentProvider.getCallDetailIntentProvider(rowId, callIds, null));
+ detailsButtonView.setTag(
+ IntentProvider.getCallDetailIntentProvider(callDetailsEntries, buildContact()));
}
boolean isBlockedOrSpam = blockId != null || (isSpamFeatureEnabled && isSpam);
@@ -776,6 +775,8 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
contactType = ContactPhotoManager.TYPE_VOICEMAIL;
} else if (isBusiness) {
contactType = ContactPhotoManager.TYPE_BUSINESS;
+ } else if (numberPresentation == TelecomManager.PRESENTATION_RESTRICTED) {
+ contactType = ContactPhotoManager.TYPE_GENERIC_AVATAR;
}
final String lookupKey =
@@ -854,20 +855,9 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
} else if (view.getId() == R.id.call_compose_action) {
LogUtil.i("CallLogListItemViewHolder.onClick", "share and call pressed");
Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_SHARE_AND_CALL);
- CallComposerContact contact = new CallComposerContact();
- contact.photoId = info.photoId;
- contact.photoUri = info.photoUri == null ? null : info.photoUri.toString();
- contact.contactUri = info.lookupUri == null ? null : info.lookupUri.toString();
- contact.nameOrNumber = (String) nameOrNumber;
- contact.isBusiness = isBusiness;
- contact.number = number;
- /* second line of contact view. */
- contact.displayNumber = TextUtils.isEmpty(info.name) ? null : displayNumber;
- /* phone number type (e.g. mobile) in second line of contact view */
- contact.numberLabel = numberType;
Activity activity = (Activity) mContext;
activity.startActivityForResult(
- CallComposerActivity.newIntent(activity, contact),
+ CallComposerActivity.newIntent(activity, buildContact()),
DialtactsActivity.ACTIVITY_REQUEST_CODE_CALL_COMPOSE);
} else if (view.getId() == R.id.share_voicemail) {
Logger.get(mContext).logImpression(DialerImpression.Type.VVM_SHARE_PRESSED);
@@ -885,6 +875,21 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
}
}
+ private CallComposerContact buildContact() {
+ CallComposerContact contact = new CallComposerContact();
+ contact.photoId = info.photoId;
+ contact.photoUri = info.photoUri == null ? null : info.photoUri.toString();
+ contact.contactUri = info.lookupUri == null ? null : info.lookupUri.toString();
+ contact.nameOrNumber = (String) nameOrNumber;
+ contact.isBusiness = isBusiness;
+ contact.number = number;
+ /* second line of contact view. */
+ contact.displayNumber = TextUtils.isEmpty(info.name) ? null : displayNumber;
+ /* phone number type (e.g. mobile) in second line of contact view */
+ contact.numberLabel = numberType;
+ return contact;
+ }
+
private void logCallLogAction(int id) {
if (id == R.id.send_message_action) {
Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_SEND_MESSAGE);
@@ -931,6 +936,15 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
}
}
+ public void setDetailedPhoneDetails(CallDetailsEntries callDetailsEntries) {
+ this.callDetailsEntries = callDetailsEntries;
+ }
+
+ @VisibleForTesting
+ public CallDetailsEntries getDetailedPhoneDetails() {
+ return callDetailsEntries;
+ }
+
public interface OnClickListener {
void onBlockReportSpam(
diff --git a/java/com/android/dialer/app/calllog/CallLogNotificationsHelper.java b/java/com/android/dialer/app/calllog/CallLogNotificationsQueryHelper.java
index 8f664d1a4..f12837e6f 100644
--- a/java/com/android/dialer/app/calllog/CallLogNotificationsHelper.java
+++ b/java/com/android/dialer/app/calllog/CallLogNotificationsQueryHelper.java
@@ -18,37 +18,41 @@ package com.android.dialer.app.calllog;
import android.Manifest;
import android.annotation.TargetApi;
+import android.app.NotificationManager;
import android.content.ContentResolver;
import android.content.ContentUris;
+import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build.VERSION_CODES;
import android.provider.CallLog.Calls;
import android.support.annotation.Nullable;
+import android.support.annotation.WorkerThread;
+import android.support.v4.os.UserManagerCompat;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
-import android.util.Log;
import com.android.contacts.common.GeoUtil;
import com.android.dialer.app.R;
+import com.android.dialer.calllogutils.PhoneNumberDisplayUtil;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.notification.GroupedNotificationUtil;
import com.android.dialer.phonenumbercache.ContactInfo;
import com.android.dialer.phonenumbercache.ContactInfoHelper;
-import com.android.dialer.telecom.TelecomUtil;
import com.android.dialer.util.PermissionsUtil;
import java.util.ArrayList;
import java.util.List;
/** Helper class operating on call log notifications. */
-public class CallLogNotificationsHelper {
+public class CallLogNotificationsQueryHelper {
private static final String TAG = "CallLogNotifHelper";
- private static CallLogNotificationsHelper sInstance;
private final Context mContext;
private final NewCallsQuery mNewCallsQuery;
private final ContactInfoHelper mContactInfoHelper;
private final String mCurrentCountryIso;
- CallLogNotificationsHelper(
+ CallLogNotificationsQueryHelper(
Context context,
NewCallsQuery newCallsQuery,
ContactInfoHelper contactInfoHelper,
@@ -59,29 +63,60 @@ public class CallLogNotificationsHelper {
mCurrentCountryIso = countryIso;
}
- /** Returns the singleton instance of the {@link CallLogNotificationsHelper}. */
- public static CallLogNotificationsHelper getInstance(Context context) {
- if (sInstance == null) {
- ContentResolver contentResolver = context.getContentResolver();
- String countryIso = GeoUtil.getCurrentCountryIso(context);
- sInstance =
- new CallLogNotificationsHelper(
- context,
- createNewCallsQuery(context, contentResolver),
- new ContactInfoHelper(context, countryIso),
- countryIso);
- }
- return sInstance;
+ /** Returns an instance of {@link CallLogNotificationsQueryHelper}. */
+ public static CallLogNotificationsQueryHelper getInstance(Context context) {
+ ContentResolver contentResolver = context.getContentResolver();
+ String countryIso = GeoUtil.getCurrentCountryIso(context);
+ return new CallLogNotificationsQueryHelper(
+ context,
+ createNewCallsQuery(context, contentResolver),
+ new ContactInfoHelper(context, countryIso),
+ countryIso);
}
- /** Removes the missed call notifications. */
- public static void removeMissedCallNotifications(Context context) {
- TelecomUtil.cancelMissedCallsNotification(context);
+ /**
+ * Removes the missed call notifications and marks calls as read. If a callUri is provided, only
+ * that call is marked as read.
+ */
+ @WorkerThread
+ public static void removeMissedCallNotifications(Context context, @Nullable Uri callUri) {
+ // Call log is only accessible when unlocked. If that's the case, clear the list of
+ // new missed calls from the call log.
+ if (UserManagerCompat.isUserUnlocked(context) && PermissionsUtil.hasPhonePermissions(context)) {
+ ContentValues values = new ContentValues();
+ values.put(Calls.NEW, 0);
+ values.put(Calls.IS_READ, 1);
+ StringBuilder where = new StringBuilder();
+ where.append(Calls.NEW);
+ where.append(" = 1 AND ");
+ where.append(Calls.TYPE);
+ where.append(" = ?");
+ try {
+ context
+ .getContentResolver()
+ .update(
+ callUri == null ? Calls.CONTENT_URI : callUri,
+ values,
+ where.toString(),
+ new String[] {Integer.toString(Calls.MISSED_TYPE)});
+ } catch (IllegalArgumentException e) {
+ LogUtil.e(
+ "CallLogNotificationsQueryHelper.removeMissedCallNotifications",
+ "contacts provider update command failed",
+ e);
+ }
+ }
+
+ GroupedNotificationUtil.removeNotification(
+ context.getSystemService(NotificationManager.class),
+ callUri != null ? callUri.toString() : null,
+ R.id.notification_missed_call,
+ MissedCallNotifier.NOTIFICATION_TAG);
}
/** Update the voice mail notifications. */
public static void updateVoicemailNotifications(Context context) {
- CallLogNotificationsService.updateVoicemailNotifications(context, null);
+ CallLogNotificationsService.updateVoicemailNotifications(context);
}
/** Create a new instance of {@link NewCallsQuery}. */
@@ -251,7 +286,7 @@ public class CallLogNotificationsHelper {
@TargetApi(VERSION_CODES.M)
public List<NewCall> query(int type) {
if (!PermissionsUtil.hasPermission(mContext, Manifest.permission.READ_CALL_LOG)) {
- Log.w(TAG, "No READ_CALL_LOG permission, returning null for calls lookup.");
+ LogUtil.w(TAG, "No READ_CALL_LOG permission, returning null for calls lookup.");
return null;
}
final String selection = String.format("%s = 1 AND %s = ?", Calls.NEW, Calls.TYPE);
@@ -272,7 +307,7 @@ public class CallLogNotificationsHelper {
}
return newCalls;
} catch (RuntimeException e) {
- Log.w(TAG, "Exception when querying Contacts Provider for calls lookup");
+ LogUtil.w(TAG, "Exception when querying Contacts Provider for calls lookup");
return null;
}
}
diff --git a/java/com/android/dialer/app/calllog/CallLogNotificationsService.java b/java/com/android/dialer/app/calllog/CallLogNotificationsService.java
index 820528126..b0d48eee5 100644
--- a/java/com/android/dialer/app/calllog/CallLogNotificationsService.java
+++ b/java/com/android/dialer/app/calllog/CallLogNotificationsService.java
@@ -20,6 +20,7 @@ import android.app.IntentService;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
+import android.support.annotation.Nullable;
import com.android.dialer.common.LogUtil;
import com.android.dialer.telecom.TelecomUtil;
import com.android.dialer.util.PermissionsUtil;
@@ -44,21 +45,10 @@ public class CallLogNotificationsService extends IntentService {
/** Action to mark all the new voicemails as old. */
public static final String ACTION_MARK_NEW_VOICEMAILS_AS_OLD =
"com.android.dialer.calllog.ACTION_MARK_NEW_VOICEMAILS_AS_OLD";
- /**
- * Action to update voicemail notifications.
- *
- * <p>May include an optional extra {@link #EXTRA_NEW_VOICEMAIL_URI}.
- */
+ /** Action to update voicemail notifications. */
public static final String ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS =
"com.android.dialer.calllog.UPDATE_VOICEMAIL_NOTIFICATIONS";
/**
- * Extra to included with {@link #ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS} to identify the new
- * voicemail that triggered an update.
- *
- * <p>It must be a {@link Uri}.
- */
- public static final String EXTRA_NEW_VOICEMAIL_URI = "NEW_VOICEMAIL_URI";
- /**
* Action to update the missed call notifications.
*
* <p>Includes optional extras {@link #EXTRA_MISSED_CALL_NUMBER} and {@link
@@ -66,9 +56,15 @@ public class CallLogNotificationsService extends IntentService {
*/
public static final String ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS =
"com.android.dialer.calllog.UPDATE_MISSED_CALL_NOTIFICATIONS";
+
/** Action to mark all the new missed calls as old. */
public static final String ACTION_MARK_NEW_MISSED_CALLS_AS_OLD =
"com.android.dialer.calllog.ACTION_MARK_NEW_MISSED_CALLS_AS_OLD";
+
+ /** Action to update missed call notifications with a post call note. */
+ public static final String ACTION_INCOMING_POST_CALL =
+ "com.android.dialer.calllog.INCOMING_POST_CALL";
+
/** Action to call back a missed call. */
public static final String ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION =
"com.android.dialer.calllog.CALL_BACK_FROM_MISSED_CALL_NOTIFICATION";
@@ -92,6 +88,21 @@ public class CallLogNotificationsService extends IntentService {
*/
public static final String EXTRA_MISSED_CALL_COUNT = "MISSED_CALL_COUNT";
+ /**
+ * Extra to be included with {@link #ACTION_INCOMING_POST_CALL} to represent a post call note.
+ *
+ * <p>It must be a {@link String}
+ */
+ public static final String EXTRA_POST_CALL_NOTE = "POST_CALL_NOTE";
+
+ /**
+ * Extra to be included with {@link #ACTION_INCOMING_POST_CALL} to represent the phone number the
+ * post call note came from.
+ *
+ * <p>It must be a {@link String}
+ */
+ public static final String EXTRA_POST_CALL_NUMBER = "POST_CALL_NUMBER";
+
public static final int UNKNOWN_MISSED_CALL_COUNT = -1;
private VoicemailQueryHandler mVoicemailQueryHandler;
@@ -103,10 +114,8 @@ public class CallLogNotificationsService extends IntentService {
* Updates notifications for any new voicemails.
*
* @param context a valid context.
- * @param voicemailUri The uri pointing to the voicemail to update the notification for. If {@code
- * null}, then notifications for all new voicemails will be updated.
*/
- public static void updateVoicemailNotifications(Context context, Uri voicemailUri) {
+ public static void updateVoicemailNotifications(Context context) {
if (!TelecomUtil.isDefaultDialer(context)) {
LogUtil.i(
"CallLogNotificationsService.updateVoicemailNotifications",
@@ -116,10 +125,6 @@ public class CallLogNotificationsService extends IntentService {
if (TelecomUtil.hasReadWriteVoicemailPermissions(context)) {
Intent serviceIntent = new Intent(context, CallLogNotificationsService.class);
serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS);
- // If voicemailUri is null, then notifications for all voicemails will be updated.
- if (voicemailUri != null) {
- serviceIntent.putExtra(CallLogNotificationsService.EXTRA_NEW_VOICEMAIL_URI, voicemailUri);
- }
context.startService(serviceIntent);
}
}
@@ -139,9 +144,25 @@ public class CallLogNotificationsService extends IntentService {
context.startService(serviceIntent);
}
- public static void markNewVoicemailsAsOld(Context context) {
+ public static void insertPostCallNote(Context context, String number, String postCallNote) {
+ Intent serviceIntent = new Intent(context, CallLogNotificationsService.class);
+ serviceIntent.setAction(ACTION_INCOMING_POST_CALL);
+ serviceIntent.putExtra(EXTRA_POST_CALL_NUMBER, number);
+ serviceIntent.putExtra(EXTRA_POST_CALL_NOTE, postCallNote);
+ context.startService(serviceIntent);
+ }
+
+ public static void markNewVoicemailsAsOld(Context context, @Nullable Uri voicemailUri) {
Intent serviceIntent = new Intent(context, CallLogNotificationsService.class);
serviceIntent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD);
+ serviceIntent.setData(voicemailUri);
+ context.startService(serviceIntent);
+ }
+
+ public static void markNewMissedCallsAsOld(Context context, @Nullable Uri callUri) {
+ Intent serviceIntent = new Intent(context, CallLogNotificationsService.class);
+ serviceIntent.setAction(ACTION_MARK_NEW_MISSED_CALLS_AS_OLD);
+ serviceIntent.setData(callUri);
context.startService(serviceIntent);
}
@@ -172,11 +193,10 @@ public class CallLogNotificationsService extends IntentService {
if (mVoicemailQueryHandler == null) {
mVoicemailQueryHandler = new VoicemailQueryHandler(this, getContentResolver());
}
- mVoicemailQueryHandler.markNewVoicemailsAsOld();
+ mVoicemailQueryHandler.markNewVoicemailsAsOld(intent.getData());
break;
case ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS:
- Uri voicemailUri = intent.getParcelableExtra(EXTRA_NEW_VOICEMAIL_URI);
- DefaultVoicemailNotifier.getInstance(this).updateNotification(voicemailUri);
+ DefaultVoicemailNotifier.getInstance(this).updateNotification();
break;
case ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS:
int count = intent.getIntExtra(EXTRA_MISSED_CALL_COUNT, UNKNOWN_MISSED_CALL_COUNT);
@@ -184,16 +204,24 @@ public class CallLogNotificationsService extends IntentService {
MissedCallNotifier.getInstance(this).updateMissedCallNotification(count, number);
updateBadgeCount(this, count);
break;
+ case ACTION_INCOMING_POST_CALL:
+ String note = intent.getStringExtra(EXTRA_POST_CALL_NOTE);
+ String phoneNumber = intent.getStringExtra(EXTRA_POST_CALL_NUMBER);
+ MissedCallNotifier.getInstance(this).insertPostCallNotification(phoneNumber, note);
+ break;
case ACTION_MARK_NEW_MISSED_CALLS_AS_OLD:
- CallLogNotificationsHelper.removeMissedCallNotifications(this);
+ CallLogNotificationsQueryHelper.removeMissedCallNotifications(this, intent.getData());
+ TelecomUtil.cancelMissedCallsNotification(this);
break;
case ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION:
MissedCallNotifier.getInstance(this)
- .callBackFromMissedCall(intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER));
+ .callBackFromMissedCall(
+ intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER), intent.getData());
break;
case ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION:
MissedCallNotifier.getInstance(this)
- .sendSmsFromMissedCall(intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER));
+ .sendSmsFromMissedCall(
+ intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER), intent.getData());
break;
default:
LogUtil.d("CallLogNotificationsService.onHandleIntent", "could not handle: " + intent);
diff --git a/java/com/android/dialer/app/calllog/CallLogReceiver.java b/java/com/android/dialer/app/calllog/CallLogReceiver.java
index a781b0887..8fd1502bc 100644
--- a/java/com/android/dialer/app/calllog/CallLogReceiver.java
+++ b/java/com/android/dialer/app/calllog/CallLogReceiver.java
@@ -38,9 +38,9 @@ public class CallLogReceiver extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
if (VoicemailContract.ACTION_NEW_VOICEMAIL.equals(intent.getAction())) {
checkVoicemailStatus(context);
- CallLogNotificationsService.updateVoicemailNotifications(context, intent.getData());
+ CallLogNotificationsService.updateVoicemailNotifications(context);
} else if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
- CallLogNotificationsService.updateVoicemailNotifications(context, null);
+ CallLogNotificationsService.updateVoicemailNotifications(context);
} else {
LogUtil.w("CallLogReceiver.onReceive", "could not handle: " + intent);
}
diff --git a/java/com/android/dialer/app/calllog/DefaultVoicemailNotifier.java b/java/com/android/dialer/app/calllog/DefaultVoicemailNotifier.java
index 651a0ccb8..cc1dc4f20 100644
--- a/java/com/android/dialer/app/calllog/DefaultVoicemailNotifier.java
+++ b/java/com/android/dialer/app/calllog/DefaultVoicemailNotifier.java
@@ -19,31 +19,33 @@ package com.android.dialer.app.calllog;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
-import android.content.ComponentName;
-import android.content.ContentResolver;
-import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
+import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
import android.support.v4.util.Pair;
-import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.ArrayMap;
-import android.util.Log;
import com.android.contacts.common.compat.TelephonyManagerCompat;
import com.android.contacts.common.util.ContactDisplayUtils;
import com.android.dialer.app.DialtactsActivity;
import com.android.dialer.app.R;
-import com.android.dialer.app.calllog.CallLogNotificationsHelper.NewCall;
+import com.android.dialer.app.calllog.CallLogNotificationsQueryHelper.NewCall;
+import com.android.dialer.app.contactinfo.ContactPhotoLoader;
import com.android.dialer.app.list.ListsFragment;
import com.android.dialer.blocking.FilteredNumbersUtil;
-import com.android.dialer.telecom.TelecomUtil;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.notification.NotificationChannelManager;
+import com.android.dialer.notification.NotificationChannelManager.Channel;
+import com.android.dialer.phonenumbercache.ContactInfo;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -54,26 +56,23 @@ public class DefaultVoicemailNotifier {
public static final String TAG = "VoicemailNotifier";
/** The tag used to identify notifications from this class. */
- private static final String NOTIFICATION_TAG = "DefaultVoicemailNotifier";
+ static final String NOTIFICATION_TAG = "DefaultVoicemailNotifier";
/** The identifier of the notification of new voicemails. */
- private static final int NOTIFICATION_ID = 1;
+ private static final int NOTIFICATION_ID = R.id.notification_voicemail;
- /** The singleton instance of {@link DefaultVoicemailNotifier}. */
- private static DefaultVoicemailNotifier sInstance;
+ private final Context context;
+ private final CallLogNotificationsQueryHelper queryHelper;
- private final Context mContext;
-
- private DefaultVoicemailNotifier(Context context) {
- mContext = context;
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ DefaultVoicemailNotifier(Context context, CallLogNotificationsQueryHelper queryHelper) {
+ this.context = context;
+ this.queryHelper = queryHelper;
}
- /** Returns the singleton instance of the {@link DefaultVoicemailNotifier}. */
+ /** Returns an instance of {@link DefaultVoicemailNotifier}. */
public static DefaultVoicemailNotifier getInstance(Context context) {
- if (sInstance == null) {
- ContentResolver contentResolver = context.getContentResolver();
- sInstance = new DefaultVoicemailNotifier(context);
- }
- return sInstance;
+ return new DefaultVoicemailNotifier(
+ context, CallLogNotificationsQueryHelper.getInstance(context));
}
/**
@@ -84,34 +83,23 @@ public class DefaultVoicemailNotifier {
*
* <p>It is not safe to call this method from the main thread.
*/
- public void updateNotification(Uri newCallUri) {
+ public void updateNotification() {
// Lookup the list of new voicemails to include in the notification.
- // TODO: Move this into a service, to avoid holding the receiver up.
- final List<NewCall> newCalls =
- CallLogNotificationsHelper.getInstance(mContext).getNewVoicemails();
+ final List<NewCall> newCalls = queryHelper.getNewVoicemails();
if (newCalls == null) {
// Query failed, just return.
return;
}
- if (newCalls.isEmpty()) {
- // No voicemails to notify about: clear the notification.
- getNotificationManager().cancel(NOTIFICATION_TAG, NOTIFICATION_ID);
- return;
- }
-
- Resources resources = mContext.getResources();
+ Resources resources = context.getResources();
// This represents a list of names to include in the notification.
String callers = null;
// Maps each number into a name: if a number is in the map, it has already left a more
// recent voicemail.
- final Map<String, String> names = new ArrayMap<>();
-
- // Determine the call corresponding to the new voicemail we have to notify about.
- NewCall callToNotify = null;
+ final Map<String, ContactInfo> contactInfos = new ArrayMap<>();
// Iterate over the new voicemails to determine all the information above.
Iterator<NewCall> itr = newCalls.iterator();
@@ -120,95 +108,64 @@ public class DefaultVoicemailNotifier {
// Skip notifying for numbers which are blocked.
if (FilteredNumbersUtil.shouldBlockVoicemail(
- mContext, newCall.number, newCall.countryIso, newCall.dateMs)) {
+ context, newCall.number, newCall.countryIso, newCall.dateMs)) {
itr.remove();
// Delete the voicemail.
- mContext.getContentResolver().delete(newCall.voicemailUri, null, null);
+ context.getContentResolver().delete(newCall.voicemailUri, null, null);
continue;
}
// Check if we already know the name associated with this number.
- String name = names.get(newCall.number);
- if (name == null) {
- name =
- CallLogNotificationsHelper.getInstance(mContext)
- .getName(newCall.number, newCall.numberPresentation, newCall.countryIso);
- names.put(newCall.number, name);
+ ContactInfo contactInfo = contactInfos.get(newCall.number);
+ if (contactInfo == null) {
+ contactInfo =
+ queryHelper.getContactInfo(
+ newCall.number, newCall.numberPresentation, newCall.countryIso);
+ contactInfos.put(newCall.number, contactInfo);
// This is a new caller. Add it to the back of the list of callers.
if (TextUtils.isEmpty(callers)) {
- callers = name;
+ callers = contactInfo.name;
} else {
callers =
- resources.getString(R.string.notification_voicemail_callers_list, callers, name);
+ resources.getString(
+ R.string.notification_voicemail_callers_list, callers, contactInfo.name);
}
}
- // Check if this is the new call we need to notify about.
- if (newCallUri != null
- && newCall.voicemailUri != null
- && ContentUris.parseId(newCallUri) == ContentUris.parseId(newCall.voicemailUri)) {
- callToNotify = newCall;
- }
}
- // All the potential new voicemails have been removed, e.g. if they were spam.
if (newCalls.isEmpty()) {
+ // No voicemails to notify about: clear the notification.
+ CallLogNotificationsService.markNewVoicemailsAsOld(context, null);
return;
}
- // If there is only one voicemail, set its transcription as the "long text".
- String transcription = null;
- if (newCalls.size() == 1) {
- transcription = newCalls.get(0).transcription;
- }
-
- if (newCallUri != null && callToNotify == null) {
- Log.e(TAG, "The new call could not be found in the call log: " + newCallUri);
- }
-
- // Determine the title of the notification and the icon for it.
- final String title =
- resources.getQuantityString(
- R.plurals.notification_voicemail_title, newCalls.size(), newCalls.size());
- // TODO: Use the photo of contact if all calls are from the same person.
- final int icon = android.R.drawable.stat_notify_voicemail;
-
- Pair<Uri, Integer> info = getNotificationInfo(callToNotify);
-
- Notification.Builder notificationBuilder =
- new Notification.Builder(mContext)
- .setSmallIcon(icon)
- .setContentTitle(title)
+ Notification.Builder groupSummary =
+ createNotificationBuilder()
+ .setContentTitle(
+ resources.getQuantityString(
+ R.plurals.notification_voicemail_title, newCalls.size(), newCalls.size()))
.setContentText(callers)
- .setColor(resources.getColor(R.color.dialer_theme_color))
- .setSound(info.first)
- .setDefaults(info.second)
- .setDeleteIntent(createMarkNewVoicemailsAsOldIntent())
- .setAutoCancel(true);
-
- if (!TextUtils.isEmpty(transcription)) {
- notificationBuilder.setStyle(new Notification.BigTextStyle().bigText(transcription));
+ .setDeleteIntent(createMarkNewVoicemailsAsOldIntent(null))
+ .setGroupSummary(true)
+ .setContentIntent(newVoicemailIntent(null));
+
+ NotificationChannelManager.applyChannel(
+ groupSummary,
+ context,
+ Channel.VOICEMAIL,
+ PhoneAccountHandles.getAccount(context, newCalls.get(0)));
+
+ LogUtil.i(TAG, "Creating voicemail notification");
+ getNotificationManager().notify(NOTIFICATION_TAG, NOTIFICATION_ID, groupSummary.build());
+
+ for (NewCall voicemail : newCalls) {
+ getNotificationManager()
+ .notify(
+ voicemail.voicemailUri.toString(),
+ NOTIFICATION_ID,
+ createNotificationForVoicemail(voicemail, contactInfos));
}
-
- // Determine the intent to fire when the notification is clicked on.
- final Intent contentIntent;
- // Open the call log.
- contentIntent = DialtactsActivity.getShowTabIntent(mContext, ListsFragment.TAB_INDEX_VOICEMAIL);
- contentIntent.putExtra(DialtactsActivity.EXTRA_CLEAR_NEW_VOICEMAILS, true);
- notificationBuilder.setContentIntent(
- PendingIntent.getActivity(mContext, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT));
-
- // The text to show in the ticker, describing the new event.
- if (callToNotify != null) {
- CharSequence msg =
- ContactDisplayUtils.getTtsSpannedPhoneNumber(
- resources,
- R.string.notification_new_voicemail_ticker,
- names.get(callToNotify.number));
- notificationBuilder.setTicker(msg);
- }
- Log.i(TAG, "Creating voicemail notification");
- getNotificationManager().notify(NOTIFICATION_TAG, NOTIFICATION_ID, notificationBuilder.build());
}
/**
@@ -216,30 +173,15 @@ public class DefaultVoicemailNotifier {
* for the given call.
*/
private Pair<Uri, Integer> getNotificationInfo(@Nullable NewCall callToNotify) {
- Log.v(TAG, "getNotificationInfo");
+ LogUtil.v(TAG, "getNotificationInfo");
if (callToNotify == null) {
- Log.i(TAG, "callToNotify == null");
+ LogUtil.i(TAG, "callToNotify == null");
return new Pair<>(null, 0);
}
- PhoneAccountHandle accountHandle;
- if (callToNotify.accountComponentName == null || callToNotify.accountId == null) {
- Log.v(TAG, "accountComponentName == null || callToNotify.accountId == null");
- accountHandle = TelecomUtil.getDefaultOutgoingPhoneAccount(mContext, PhoneAccount.SCHEME_TEL);
- if (accountHandle == null) {
- Log.i(TAG, "No default phone account found, using default notification ringtone");
- return new Pair<>(null, Notification.DEFAULT_ALL);
- }
-
- } else {
- accountHandle =
- new PhoneAccountHandle(
- ComponentName.unflattenFromString(callToNotify.accountComponentName),
- callToNotify.accountId);
- }
- if (accountHandle.getComponentName() != null) {
- Log.v(TAG, "PhoneAccountHandle.ComponentInfo:" + accountHandle.getComponentName());
- } else {
- Log.i(TAG, "PhoneAccountHandle.ComponentInfo: null");
+ PhoneAccountHandle accountHandle = PhoneAccountHandles.getAccount(context, callToNotify);
+ if (accountHandle == null) {
+ LogUtil.i(TAG, "No default phone account found, using default notification ringtone");
+ return new Pair<>(null, Notification.DEFAULT_ALL);
}
return new Pair<>(
TelephonyManagerCompat.getVoicemailRingtoneUri(getTelephonyManager(), accountHandle),
@@ -257,17 +199,79 @@ public class DefaultVoicemailNotifier {
}
/** Creates a pending intent that marks all new voicemails as old. */
- private PendingIntent createMarkNewVoicemailsAsOldIntent() {
- Intent intent = new Intent(mContext, CallLogNotificationsService.class);
+ private PendingIntent createMarkNewVoicemailsAsOldIntent(@Nullable Uri voicemailUri) {
+ Intent intent = new Intent(context, CallLogNotificationsService.class);
intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD);
- return PendingIntent.getService(mContext, 0, intent, 0);
+ intent.setData(voicemailUri);
+ return PendingIntent.getService(context, 0, intent, 0);
}
private NotificationManager getNotificationManager() {
- return (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
}
private TelephonyManager getTelephonyManager() {
- return (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+ return (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ }
+
+ private Notification createNotificationForVoicemail(
+ @NonNull NewCall voicemail, @NonNull Map<String, ContactInfo> contactInfos) {
+ Pair<Uri, Integer> notificationInfo = getNotificationInfo(voicemail);
+ ContactInfo contactInfo = contactInfos.get(voicemail.number);
+
+ Notification.Builder notificationBuilder =
+ createNotificationBuilder()
+ .setContentTitle(
+ context
+ .getResources()
+ .getQuantityString(R.plurals.notification_voicemail_title, 1, 1))
+ .setContentText(
+ ContactDisplayUtils.getTtsSpannedPhoneNumber(
+ context.getResources(),
+ R.string.notification_new_voicemail_ticker,
+ contactInfo.name))
+ .setWhen(voicemail.dateMs)
+ .setSound(notificationInfo.first)
+ .setDefaults(notificationInfo.second)
+ .setDeleteIntent(createMarkNewVoicemailsAsOldIntent(voicemail.voicemailUri));
+
+ NotificationChannelManager.applyChannel(
+ notificationBuilder,
+ context,
+ Channel.VOICEMAIL,
+ PhoneAccountHandles.getAccount(context, voicemail));
+
+ ContactPhotoLoader loader = new ContactPhotoLoader(context, contactInfo);
+ Bitmap photoIcon = loader.loadPhotoIcon();
+ if (photoIcon != null) {
+ notificationBuilder.setLargeIcon(photoIcon);
+ }
+
+ if (!TextUtils.isEmpty(voicemail.transcription)) {
+ notificationBuilder.setStyle(
+ new Notification.BigTextStyle().bigText(voicemail.transcription));
+ }
+ notificationBuilder.setContentIntent(newVoicemailIntent(voicemail));
+
+ return notificationBuilder.build();
+ }
+
+ private Notification.Builder createNotificationBuilder() {
+ return new Notification.Builder(context)
+ .setSmallIcon(android.R.drawable.stat_notify_voicemail)
+ .setColor(context.getColor(R.color.dialer_theme_color))
+ .setGroup(NOTIFICATION_TAG)
+ .setOnlyAlertOnce(true)
+ .setAutoCancel(true);
+ }
+
+ private PendingIntent newVoicemailIntent(@Nullable NewCall voicemail) {
+ Intent intent = DialtactsActivity.getShowTabIntent(context, ListsFragment.TAB_INDEX_VOICEMAIL);
+ // TODO (b/35486204): scroll to this voicemail
+ if (voicemail != null) {
+ intent.setData(voicemail.voicemailUri);
+ }
+ intent.putExtra(DialtactsActivity.EXTRA_CLEAR_NEW_VOICEMAILS, true);
+ return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
}
diff --git a/java/com/android/dialer/app/calllog/IntentProvider.java b/java/com/android/dialer/app/calllog/IntentProvider.java
index 879ac353d..c53e3ec5e 100644
--- a/java/com/android/dialer/app/calllog/IntentProvider.java
+++ b/java/com/android/dialer/app/calllog/IntentProvider.java
@@ -16,7 +16,6 @@
package com.android.dialer.app.calllog;
-import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
@@ -25,10 +24,11 @@ import android.provider.ContactsContract;
import android.telecom.PhoneAccountHandle;
import com.android.contacts.common.model.Contact;
import com.android.contacts.common.model.ContactLoader;
-import com.android.dialer.app.CallDetailActivity;
+import com.android.dialer.callcomposer.nano.CallComposerContact;
+import com.android.dialer.calldetails.CallDetailsActivity;
+import com.android.dialer.calldetails.nano.CallDetailsEntries;
import com.android.dialer.callintent.CallIntentBuilder;
import com.android.dialer.callintent.nano.CallInitiationType;
-import com.android.dialer.telecom.TelecomUtil;
import com.android.dialer.util.CallUtil;
import com.android.dialer.util.IntentUtil;
import java.util.ArrayList;
@@ -97,29 +97,16 @@ public abstract class IntentProvider {
/**
* Retrieves the call details intent provider for an entry in the call log.
*
- * @param id The call ID of the first call in the call group.
- * @param extraIds The call ID of the other calls grouped together with the call.
- * @param voicemailUri If call log entry is for a voicemail, the voicemail URI.
+ * @param callDetailsEntries The call details of the other calls grouped together with the call.
+ * @param contact The contact with which this call details intent pertains to.
* @return The call details intent provider.
*/
public static IntentProvider getCallDetailIntentProvider(
- final long id, final long[] extraIds, final String voicemailUri) {
+ CallDetailsEntries callDetailsEntries, CallComposerContact contact) {
return new IntentProvider() {
@Override
public Intent getIntent(Context context) {
- Intent intent = new Intent(context, CallDetailActivity.class);
- // Check if the first item is a voicemail.
- if (voicemailUri != null) {
- intent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI, Uri.parse(voicemailUri));
- }
-
- if (extraIds != null && extraIds.length > 0) {
- intent.putExtra(CallDetailActivity.EXTRA_CALL_LOG_IDS, extraIds);
- } else {
- // If there is a single item, use the direct URI for it.
- intent.setData(ContentUris.withAppendedId(TelecomUtil.getCallLogUri(context), id));
- }
- return intent;
+ return CallDetailsActivity.newInstance(context, callDetailsEntries, contact);
}
};
}
diff --git a/java/com/android/dialer/app/calllog/MissedCallNotifier.java b/java/com/android/dialer/app/calllog/MissedCallNotifier.java
index 2fa3dae65..5b5661615 100644
--- a/java/com/android/dialer/app/calllog/MissedCallNotifier.java
+++ b/java/com/android/dialer/app/calllog/MissedCallNotifier.java
@@ -16,16 +16,20 @@
package com.android.dialer.app.calllog;
import android.app.Notification;
+import android.app.Notification.Builder;
import android.app.NotificationManager;
import android.app.PendingIntent;
-import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
-import android.os.AsyncTask;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
import android.provider.CallLog.Calls;
+import android.service.notification.StatusBarNotification;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
+import android.support.annotation.WorkerThread;
import android.support.v4.os.UserManagerCompat;
import android.text.BidiFormatter;
import android.text.TextDirectionHeuristics;
@@ -34,109 +38,117 @@ import com.android.contacts.common.ContactsUtils;
import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
import com.android.dialer.app.DialtactsActivity;
import com.android.dialer.app.R;
-import com.android.dialer.app.calllog.CallLogNotificationsHelper.NewCall;
+import com.android.dialer.app.calllog.CallLogNotificationsQueryHelper.NewCall;
import com.android.dialer.app.contactinfo.ContactPhotoLoader;
import com.android.dialer.app.list.ListsFragment;
import com.android.dialer.callintent.CallIntentBuilder;
import com.android.dialer.callintent.nano.CallInitiationType;
-import com.android.dialer.common.ConfigProviderBindings;
import com.android.dialer.common.LogUtil;
+import com.android.dialer.notification.NotificationChannelManager;
+import com.android.dialer.notification.NotificationChannelManager.Channel;
import com.android.dialer.phonenumbercache.ContactInfo;
import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+import com.android.dialer.telecom.TelecomUtil;
import com.android.dialer.util.DialerUtils;
import com.android.dialer.util.IntentUtil;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
/** Creates a notification for calls that the user missed (neither answered nor rejected). */
public class MissedCallNotifier {
/** The tag used to identify notifications from this class. */
- private static final String NOTIFICATION_TAG = "MissedCallNotifier";
+ static final String NOTIFICATION_TAG = "MissedCallNotifier";
/** The identifier of the notification of new missed calls. */
- private static final int NOTIFICATION_ID = 1;
+ private static final int NOTIFICATION_ID = R.id.notification_missed_call;
- private static MissedCallNotifier sInstance;
- private Context mContext;
- private CallLogNotificationsHelper mCalllogNotificationsHelper;
+ private final Context context;
+ private final CallLogNotificationsQueryHelper callLogNotificationsQueryHelper;
@VisibleForTesting
- MissedCallNotifier(Context context, CallLogNotificationsHelper callLogNotificationsHelper) {
- mContext = context;
- mCalllogNotificationsHelper = callLogNotificationsHelper;
+ MissedCallNotifier(
+ Context context, CallLogNotificationsQueryHelper callLogNotificationsQueryHelper) {
+ this.context = context;
+ this.callLogNotificationsQueryHelper = callLogNotificationsQueryHelper;
}
- /** Returns the singleton instance of the {@link MissedCallNotifier}. */
+ /** Returns an instance of {@link MissedCallNotifier}. */
public static MissedCallNotifier getInstance(Context context) {
- if (sInstance == null) {
- CallLogNotificationsHelper callLogNotificationsHelper =
- CallLogNotificationsHelper.getInstance(context);
- sInstance = new MissedCallNotifier(context, callLogNotificationsHelper);
- }
- return sInstance;
+ CallLogNotificationsQueryHelper callLogNotificationsQueryHelper =
+ CallLogNotificationsQueryHelper.getInstance(context);
+ return new MissedCallNotifier(context, callLogNotificationsQueryHelper);
}
/**
- * Creates a missed call notification with a post call message if there are no existing missed
- * calls.
+ * Update missed call notifications from the call log. Accepts default information in case call
+ * log cannot be accessed.
+ *
+ * @param count the number of missed calls to display if call log cannot be accessed. May be
+ * {@link CallLogNotificationsService#UNKNOWN_MISSED_CALL_COUNT} if unknown.
+ * @param number the phone number of the most recent call to display if the call log cannot be
+ * accessed. May be null if unknown.
*/
- public void createPostCallMessageNotification(String number, String message) {
- int count = CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT;
- if (ConfigProviderBindings.get(mContext).getBoolean("enable_call_compose", false)) {
- updateMissedCallNotification(count, number, message);
- } else {
- updateMissedCallNotification(count, number, null);
- }
- }
-
- /** Creates a missed call notification. */
- public void updateMissedCallNotification(int count, String number) {
- updateMissedCallNotification(count, number, null);
- }
-
- private void updateMissedCallNotification(
- int count, String number, @Nullable String postCallMessage) {
+ @WorkerThread
+ public void updateMissedCallNotification(int count, @Nullable String number) {
final int titleResId;
CharSequence expandedText; // The text in the notification's line 1 and 2.
- final List<NewCall> newCalls = mCalllogNotificationsHelper.getNewMissedCalls();
+ List<NewCall> newCalls = callLogNotificationsQueryHelper.getNewMissedCalls();
- if (count == CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT) {
- if (newCalls == null) {
- // If the intent did not contain a count, and we are unable to get a count from the
- // call log, then no notification can be shown.
- return;
+ if ((newCalls != null && newCalls.isEmpty()) || count == 0) {
+ // No calls to notify about: clear the notification.
+ CallLogNotificationsQueryHelper.removeMissedCallNotifications(context, null);
+ return;
+ }
+
+ if (newCalls != null) {
+ if (count != CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT
+ && count != newCalls.size()) {
+ LogUtil.w(
+ "MissedCallNotifier.updateMissedCallNotification",
+ "Call count does not match call log count."
+ + " count: "
+ + count
+ + " newCalls.size(): "
+ + newCalls.size());
}
count = newCalls.size();
}
- if (count == 0) {
- // No voicemails to notify about: clear the notification.
- clearMissedCalls();
+ if (count == CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT) {
+ // If the intent did not contain a count, and we are unable to get a count from the
+ // call log, then no notification can be shown.
return;
}
- // The call log has been updated, use that information preferentially.
- boolean useCallLog = newCalls != null && newCalls.size() == count;
- NewCall newestCall = useCallLog ? newCalls.get(0) : null;
- long timeMs = useCallLog ? newestCall.dateMs : System.currentTimeMillis();
- String missedNumber = useCallLog ? newestCall.number : number;
+ Notification.Builder groupSummary = createNotificationBuilder();
+ boolean useCallList = newCalls != null;
- Notification.Builder builder = new Notification.Builder(mContext);
- // Display the first line of the notification:
- // 1 missed call: <caller name || handle>
- // More than 1 missed call: <number of calls> + "missed calls"
if (count == 1) {
+ NewCall call =
+ useCallList
+ ? newCalls.get(0)
+ : new NewCall(
+ null,
+ null,
+ number,
+ Calls.PRESENTATION_ALLOWED,
+ null,
+ null,
+ null,
+ null,
+ System.currentTimeMillis());
+
//TODO: look up caller ID that is not in contacts.
ContactInfo contactInfo =
- mCalllogNotificationsHelper.getContactInfo(
- missedNumber,
- useCallLog ? newestCall.numberPresentation : Calls.PRESENTATION_ALLOWED,
- useCallLog ? newestCall.countryIso : null);
-
+ callLogNotificationsQueryHelper.getContactInfo(
+ call.number, call.numberPresentation, call.countryIso);
titleResId =
contactInfo.userType == ContactsUtils.USER_TYPE_WORK
? R.string.notification_missedWorkCallTitle
: R.string.notification_missedCallTitle;
+
if (TextUtils.equals(contactInfo.name, contactInfo.formattedNumber)
|| TextUtils.equals(contactInfo.name, contactInfo.number)) {
expandedText =
@@ -147,134 +159,195 @@ public class MissedCallNotifier {
expandedText = contactInfo.name;
}
- if (!TextUtils.isEmpty(postCallMessage)) {
- // Ex. "John Doe: Hey dude"
- expandedText =
- mContext.getString(
- R.string.post_call_notification_message, expandedText, postCallMessage);
- }
- ContactPhotoLoader loader = new ContactPhotoLoader(mContext, contactInfo);
+ ContactPhotoLoader loader = new ContactPhotoLoader(context, contactInfo);
Bitmap photoIcon = loader.loadPhotoIcon();
if (photoIcon != null) {
- builder.setLargeIcon(photoIcon);
+ groupSummary.setLargeIcon(photoIcon);
}
} else {
titleResId = R.string.notification_missedCallsTitle;
- expandedText = mContext.getString(R.string.notification_missedCallsMsg, count);
+ expandedText = context.getString(R.string.notification_missedCallsMsg, count);
}
// Create a public viewable version of the notification, suitable for display when sensitive
// notification content is hidden.
- Notification.Builder publicBuilder = new Notification.Builder(mContext);
- publicBuilder
- .setSmallIcon(android.R.drawable.stat_notify_missed_call)
- .setColor(mContext.getResources().getColor(R.color.dialer_theme_color))
- // Show "Phone" for notification title.
- .setContentTitle(mContext.getText(R.string.userCallActivityLabel))
- // Notification details shows that there are missed call(s), but does not reveal
- // the missed caller information.
- .setContentText(mContext.getText(titleResId))
+ Notification.Builder publicSummaryBuilder = createNotificationBuilder();
+ publicSummaryBuilder
+ .setContentTitle(context.getText(titleResId))
.setContentIntent(createCallLogPendingIntent())
- .setAutoCancel(true)
- .setWhen(timeMs)
- .setShowWhen(true)
- .setDeleteIntent(createClearMissedCallsPendingIntent());
+ .setDeleteIntent(createClearMissedCallsPendingIntent(null));
+ // Create the notification summary suitable for display when sensitive information is showing.
+ groupSummary
+ .setContentTitle(context.getText(titleResId))
+ .setContentText(expandedText)
+ .setContentIntent(createCallLogPendingIntent())
+ .setDeleteIntent(createClearMissedCallsPendingIntent(null))
+ .setGroupSummary(useCallList)
+ .setOnlyAlertOnce(useCallList)
+ .setPublicVersion(publicSummaryBuilder.build());
+
+ NotificationChannelManager.applyChannel(
+ groupSummary,
+ context,
+ Channel.MISSED_CALL,
+ PhoneAccountHandles.getAccount(context, useCallList ? newCalls.get(0) : null));
+
+ Notification notification = groupSummary.build();
+ configureLedOnNotification(notification);
+
+ LogUtil.i("MissedCallNotifier.updateMissedCallNotification", "adding missed call notification");
+ getNotificationMgr().notify(NOTIFICATION_TAG, NOTIFICATION_ID, notification);
+
+ if (useCallList) {
+ // Do not repost active notifications to prevent erasing post call notes.
+ NotificationManager manager = getNotificationMgr();
+ Set<String> activeTags = new HashSet<>();
+ for (StatusBarNotification activeNotification : manager.getActiveNotifications()) {
+ activeTags.add(activeNotification.getTag());
+ }
+
+ for (NewCall call : newCalls) {
+ String callTag = call.callsUri.toString();
+ if (!activeTags.contains(callTag)) {
+ manager.notify(callTag, NOTIFICATION_ID, getNotificationForCall(call, null));
+ }
+ }
+ }
+ }
+
+ public void insertPostCallNotification(@NonNull String number, @NonNull String note) {
+ List<NewCall> newCalls = callLogNotificationsQueryHelper.getNewMissedCalls();
+ if (newCalls != null && !newCalls.isEmpty()) {
+ for (NewCall call : newCalls) {
+ if (call.number.equals(number.replace("tel:", ""))) {
+ // Update the first notification that matches our post call note sender.
+ getNotificationMgr()
+ .notify(
+ call.callsUri.toString(), NOTIFICATION_ID, getNotificationForCall(call, note));
+ break;
+ }
+ }
+ }
+ }
+
+ private Notification getNotificationForCall(
+ @NonNull NewCall call, @Nullable String postCallMessage) {
+ ContactInfo contactInfo =
+ callLogNotificationsQueryHelper.getContactInfo(
+ call.number, call.numberPresentation, call.countryIso);
+
+ // Create a public viewable version of the notification, suitable for display when sensitive
+ // notification content is hidden.
+ int titleResId =
+ contactInfo.userType == ContactsUtils.USER_TYPE_WORK
+ ? R.string.notification_missedWorkCallTitle
+ : R.string.notification_missedCallTitle;
+ Notification.Builder publicBuilder =
+ createNotificationBuilder(call).setContentTitle(context.getText(titleResId));
+
+ Notification.Builder builder = createNotificationBuilder(call);
+ CharSequence expandedText;
+ if (TextUtils.equals(contactInfo.name, contactInfo.formattedNumber)
+ || TextUtils.equals(contactInfo.name, contactInfo.number)) {
+ expandedText =
+ PhoneNumberUtilsCompat.createTtsSpannable(
+ BidiFormatter.getInstance()
+ .unicodeWrap(contactInfo.name, TextDirectionHeuristics.LTR));
+ } else {
+ expandedText = contactInfo.name;
+ }
+
+ if (postCallMessage != null) {
+ expandedText =
+ context.getString(R.string.post_call_notification_message, expandedText, postCallMessage);
+ }
+
+ ContactPhotoLoader loader = new ContactPhotoLoader(context, contactInfo);
+ Bitmap photoIcon = loader.loadPhotoIcon();
+ if (photoIcon != null) {
+ builder.setLargeIcon(photoIcon);
+ }
// Create the notification suitable for display when sensitive information is showing.
builder
- .setSmallIcon(android.R.drawable.stat_notify_missed_call)
- .setColor(mContext.getResources().getColor(R.color.dialer_theme_color))
- .setContentTitle(mContext.getText(titleResId))
+ .setContentTitle(context.getText(titleResId))
.setContentText(expandedText)
- .setContentIntent(createCallLogPendingIntent())
- .setAutoCancel(true)
- .setWhen(timeMs)
- .setShowWhen(true)
- .setDefaults(Notification.DEFAULT_VIBRATE)
- .setDeleteIntent(createClearMissedCallsPendingIntent())
// Include a public version of the notification to be shown when the missed call
// notification is shown on the user's lock screen and they have chosen to hide
// sensitive notification information.
.setPublicVersion(publicBuilder.build());
- // Add additional actions when there is only 1 missed call and the user isn't locked
- if (UserManagerCompat.isUserUnlocked(mContext) && count == 1) {
- if (!TextUtils.isEmpty(missedNumber)
- && !TextUtils.equals(missedNumber, mContext.getString(R.string.handle_restricted))) {
+ // Add additional actions when the user isn't locked
+ if (UserManagerCompat.isUserUnlocked(context)) {
+ if (!TextUtils.isEmpty(call.number)
+ && !TextUtils.equals(call.number, context.getString(R.string.handle_restricted))) {
builder.addAction(
- R.drawable.ic_phone_24dp,
- mContext.getString(R.string.notification_missedCall_call_back),
- createCallBackPendingIntent(missedNumber));
+ new Notification.Action.Builder(
+ Icon.createWithResource(context, R.drawable.ic_phone_24dp),
+ context.getString(R.string.notification_missedCall_call_back),
+ createCallBackPendingIntent(call.number, call.callsUri))
+ .build());
- if (!PhoneNumberHelper.isUriNumber(missedNumber)) {
+ if (!PhoneNumberHelper.isUriNumber(call.number)) {
builder.addAction(
- R.drawable.ic_message_24dp,
- mContext.getString(R.string.notification_missedCall_message),
- createSendSmsFromNotificationPendingIntent(missedNumber));
+ new Notification.Action.Builder(
+ Icon.createWithResource(context, R.drawable.ic_message_24dp),
+ context.getString(R.string.notification_missedCall_message),
+ createSendSmsFromNotificationPendingIntent(call.number, call.callsUri))
+ .build());
}
}
}
Notification notification = builder.build();
configureLedOnNotification(notification);
+ return notification;
+ }
- LogUtil.i("MissedCallNotifier.updateMissedCallNotification", "adding missed call notification");
- getNotificationMgr().notify(NOTIFICATION_TAG, NOTIFICATION_ID, notification);
+ private Notification.Builder createNotificationBuilder() {
+ return new Notification.Builder(context)
+ .setGroup(NOTIFICATION_TAG)
+ .setSmallIcon(android.R.drawable.stat_notify_missed_call)
+ .setColor(context.getResources().getColor(R.color.dialer_theme_color, null))
+ .setAutoCancel(true)
+ .setOnlyAlertOnce(true)
+ .setShowWhen(true)
+ .setDefaults(Notification.DEFAULT_VIBRATE);
}
- private void clearMissedCalls() {
- AsyncTask.execute(
- new Runnable() {
- @Override
- public void run() {
- // Call log is only accessible when unlocked. If that's the case, clear the list of
- // new missed calls from the call log.
- if (UserManagerCompat.isUserUnlocked(mContext)) {
- ContentValues values = new ContentValues();
- values.put(Calls.NEW, 0);
- values.put(Calls.IS_READ, 1);
- StringBuilder where = new StringBuilder();
- where.append(Calls.NEW);
- where.append(" = 1 AND ");
- where.append(Calls.TYPE);
- where.append(" = ?");
- try {
- mContext
- .getContentResolver()
- .update(
- Calls.CONTENT_URI,
- values,
- where.toString(),
- new String[] {Integer.toString(Calls.MISSED_TYPE)});
- } catch (IllegalArgumentException e) {
- LogUtil.e(
- "MissedCallNotifier.clearMissedCalls",
- "contacts provider update command failed",
- e);
- }
- }
- getNotificationMgr().cancel(NOTIFICATION_TAG, NOTIFICATION_ID);
- }
- });
+ private Notification.Builder createNotificationBuilder(@NonNull NewCall call) {
+ Builder builder =
+ createNotificationBuilder()
+ .setWhen(call.dateMs)
+ .setDeleteIntent(createClearMissedCallsPendingIntent(call.callsUri))
+ .setContentIntent(createCallLogPendingIntent(call.callsUri));
+
+ NotificationChannelManager.applyChannel(
+ builder, context, Channel.MISSED_CALL, PhoneAccountHandles.getAccount(context, call));
+ return builder;
}
/** Trigger an intent to make a call from a missed call number. */
- public void callBackFromMissedCall(String number) {
- closeSystemDialogs(mContext);
- CallLogNotificationsHelper.removeMissedCallNotifications(mContext);
+ @WorkerThread
+ public void callBackFromMissedCall(String number, Uri callUri) {
+ closeSystemDialogs(context);
+ CallLogNotificationsQueryHelper.removeMissedCallNotifications(context, callUri);
+ TelecomUtil.cancelMissedCallsNotification(context);
DialerUtils.startActivityWithErrorToast(
- mContext,
+ context,
new CallIntentBuilder(number, CallInitiationType.Type.MISSED_CALL_NOTIFICATION)
.build()
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
}
/** Trigger an intent to send an sms from a missed call number. */
- public void sendSmsFromMissedCall(String number) {
- closeSystemDialogs(mContext);
- CallLogNotificationsHelper.removeMissedCallNotifications(mContext);
+ @WorkerThread
+ public void sendSmsFromMissedCall(String number, Uri callUri) {
+ closeSystemDialogs(context);
+ CallLogNotificationsQueryHelper.removeMissedCallNotifications(context, callUri);
+ TelecomUtil.cancelMissedCallsNotification(context);
DialerUtils.startActivityWithErrorToast(
- mContext, IntentUtil.getSendSmsIntent(number).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+ context, IntentUtil.getSendSmsIntent(number).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
}
/**
@@ -283,34 +356,50 @@ public class MissedCallNotifier {
* @return The pending intent.
*/
private PendingIntent createCallLogPendingIntent() {
+ return createCallLogPendingIntent(null);
+ }
+
+ /**
+ * Creates a new pending intent that sends the user to the call log.
+ *
+ * @return The pending intent.
+ * @param callUri Uri of the call to jump to. May be null
+ */
+ private PendingIntent createCallLogPendingIntent(@Nullable Uri callUri) {
Intent contentIntent =
- DialtactsActivity.getShowTabIntent(mContext, ListsFragment.TAB_INDEX_HISTORY);
- return PendingIntent.getActivity(mContext, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ DialtactsActivity.getShowTabIntent(context, ListsFragment.TAB_INDEX_HISTORY);
+ // TODO (b/35486204): scroll to call
+ contentIntent.setData(callUri);
+ return PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT);
}
/** Creates a pending intent that marks all new missed calls as old. */
- private PendingIntent createClearMissedCallsPendingIntent() {
- Intent intent = new Intent(mContext, CallLogNotificationsService.class);
+ private PendingIntent createClearMissedCallsPendingIntent(@Nullable Uri callUri) {
+ Intent intent = new Intent(context, CallLogNotificationsService.class);
intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_MISSED_CALLS_AS_OLD);
- return PendingIntent.getService(mContext, 0, intent, 0);
+ intent.setData(callUri);
+ return PendingIntent.getService(context, 0, intent, 0);
}
- private PendingIntent createCallBackPendingIntent(String number) {
- Intent intent = new Intent(mContext, CallLogNotificationsService.class);
+ private PendingIntent createCallBackPendingIntent(String number, @NonNull Uri callUri) {
+ Intent intent = new Intent(context, CallLogNotificationsService.class);
intent.setAction(CallLogNotificationsService.ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION);
intent.putExtra(CallLogNotificationsService.EXTRA_MISSED_CALL_NUMBER, number);
+ intent.setData(callUri);
// Use FLAG_UPDATE_CURRENT to make sure any previous pending intent is updated with the new
// extra.
- return PendingIntent.getService(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
- private PendingIntent createSendSmsFromNotificationPendingIntent(String number) {
- Intent intent = new Intent(mContext, CallLogNotificationsService.class);
+ private PendingIntent createSendSmsFromNotificationPendingIntent(
+ String number, @NonNull Uri callUri) {
+ Intent intent = new Intent(context, CallLogNotificationsService.class);
intent.setAction(CallLogNotificationsService.ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION);
intent.putExtra(CallLogNotificationsService.EXTRA_MISSED_CALL_NUMBER, number);
+ intent.setData(callUri);
// Use FLAG_UPDATE_CURRENT to make sure any previous pending intent is updated with the new
// extra.
- return PendingIntent.getService(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
/** Configures a notification to emit the blinky notification light. */
@@ -325,6 +414,6 @@ public class MissedCallNotifier {
}
private NotificationManager getNotificationMgr() {
- return (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
}
}
diff --git a/java/com/android/dialer/app/calllog/PhoneAccountHandles.java b/java/com/android/dialer/app/calllog/PhoneAccountHandles.java
new file mode 100644
index 000000000..6d51b853c
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/PhoneAccountHandles.java
@@ -0,0 +1,41 @@
+package com.android.dialer.app.calllog;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import com.android.dialer.app.calllog.CallLogNotificationsQueryHelper.NewCall;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.telecom.TelecomUtil;
+
+/** Methods to help extract {@link PhoneAccount} information from database and Telecomm sources. */
+class PhoneAccountHandles {
+
+ @Nullable
+ public static PhoneAccountHandle getAccount(@NonNull Context context, @Nullable NewCall call) {
+ PhoneAccountHandle handle;
+ if (call == null || call.accountComponentName == null || call.accountId == null) {
+ LogUtil.v(
+ "PhoneAccountUtils.getAccount",
+ "accountComponentName == null || callToNotify.accountId == null");
+ handle = TelecomUtil.getDefaultOutgoingPhoneAccount(context, PhoneAccount.SCHEME_TEL);
+ if (handle == null) {
+ return null;
+ }
+ } else {
+ handle =
+ new PhoneAccountHandle(
+ ComponentName.unflattenFromString(call.accountComponentName), call.accountId);
+ }
+ if (handle.getComponentName() != null) {
+ LogUtil.v(
+ "PhoneAccountUtils.getAccount",
+ "PhoneAccountHandle.ComponentInfo:" + handle.getComponentName());
+ } else {
+ LogUtil.i("PhoneAccountUtils.getAccount", "PhoneAccountHandle.ComponentInfo: null");
+ }
+ return handle;
+ }
+}
diff --git a/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java b/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java
index b18270bb3..acbccb39f 100644
--- a/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java
+++ b/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java
@@ -27,9 +27,10 @@ import android.text.TextUtils;
import android.text.format.DateUtils;
import android.view.View;
import android.widget.TextView;
-import com.android.dialer.app.PhoneCallDetails;
import com.android.dialer.app.R;
import com.android.dialer.app.calllog.calllogcache.CallLogCache;
+import com.android.dialer.calllogutils.PhoneCallDetails;
+import com.android.dialer.oem.MotorolaUtils;
import com.android.dialer.phonenumberutil.PhoneNumberHelper;
import com.android.dialer.util.DialerUtils;
import java.util.ArrayList;
@@ -84,6 +85,8 @@ public class PhoneCallDetailsHelper {
// Show the video icon if the call had video enabled.
views.callTypeIcons.setShowVideo(
(details.features & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO);
+ views.callTypeIcons.setShowHd(
+ MotorolaUtils.shouldShowHdIconInCallLog(mContext, details.features));
views.callTypeIcons.requestLayout();
views.callTypeIcons.setVisibility(View.VISIBLE);
diff --git a/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java b/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java
index 476996826..e2e27a179 100644
--- a/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java
+++ b/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java
@@ -20,6 +20,7 @@ import android.content.Context;
import android.view.View;
import android.widget.TextView;
import com.android.dialer.app.R;
+import com.android.dialer.calllogutils.CallTypeIconsView;
/** Encapsulates the views that are used to display the details of a phone call in the call log. */
public final class PhoneCallDetailsViews {
diff --git a/java/com/android/dialer/app/calllog/VisualVoicemailCallLogFragment.java b/java/com/android/dialer/app/calllog/VisualVoicemailCallLogFragment.java
index e539ceef6..6f101f580 100644
--- a/java/com/android/dialer/app/calllog/VisualVoicemailCallLogFragment.java
+++ b/java/com/android/dialer/app/calllog/VisualVoicemailCallLogFragment.java
@@ -40,10 +40,13 @@ public class VisualVoicemailCallLogFragment extends CallLogFragment {
private VoicemailErrorManager mVoicemailAlertManager;
+ public VisualVoicemailCallLogFragment() {
+ super(CallLog.Calls.VOICEMAIL_TYPE);
+ }
+
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
- mCallTypeFilter = CallLog.Calls.VOICEMAIL_TYPE;
mVoicemailPlaybackPresenter = VoicemailPlaybackPresenter.getInstance(getActivity(), state);
getActivity()
.getContentResolver()
diff --git a/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java b/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java
index d6d8354ec..e73684e70 100644
--- a/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java
+++ b/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java
@@ -15,13 +15,18 @@
*/
package com.android.dialer.app.calllog;
+import android.app.NotificationManager;
import android.content.AsyncQueryHandler;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
+import android.net.Uri;
import android.provider.CallLog.Calls;
-import android.util.Log;
+import android.support.annotation.Nullable;
+import com.android.dialer.app.R;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.notification.GroupedNotificationUtil;
/** Handles asynchronous queries to the call log for voicemail. */
public class VoicemailQueryHandler extends AsyncQueryHandler {
@@ -39,7 +44,7 @@ public class VoicemailQueryHandler extends AsyncQueryHandler {
}
/** Updates all new voicemails to mark them as old. */
- public void markNewVoicemailsAsOld() {
+ public void markNewVoicemailsAsOld(@Nullable Uri voicemailUri) {
// Mark all "new" voicemails as not new anymore.
StringBuilder where = new StringBuilder();
where.append(Calls.NEW);
@@ -47,6 +52,10 @@ public class VoicemailQueryHandler extends AsyncQueryHandler {
where.append(Calls.TYPE);
where.append(" = ?");
+ if (voicemailUri != null) {
+ where.append(" AND ").append(Calls.VOICEMAIL_URI).append(" = ?");
+ }
+
ContentValues values = new ContentValues(1);
values.put(Calls.NEW, "0");
@@ -56,7 +65,15 @@ public class VoicemailQueryHandler extends AsyncQueryHandler {
Calls.CONTENT_URI_WITH_VOICEMAIL,
values,
where.toString(),
- new String[] {Integer.toString(Calls.VOICEMAIL_TYPE)});
+ voicemailUri == null
+ ? new String[] {Integer.toString(Calls.VOICEMAIL_TYPE)}
+ : new String[] {Integer.toString(Calls.VOICEMAIL_TYPE), voicemailUri.toString()});
+
+ GroupedNotificationUtil.removeNotification(
+ mContext.getSystemService(NotificationManager.class),
+ voicemailUri != null ? voicemailUri.toString() : null,
+ R.id.notification_voicemail,
+ DefaultVoicemailNotifier.NOTIFICATION_TAG);
}
@Override
@@ -67,7 +84,7 @@ public class VoicemailQueryHandler extends AsyncQueryHandler {
serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS);
mContext.startService(serviceIntent);
} else {
- Log.w(TAG, "Unknown update completed: ignoring: " + token);
+ LogUtil.w(TAG, "Unknown update completed: ignoring: " + token);
}
}
}
diff --git a/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipopMr1.java b/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipopMr1.java
index c342b7e3b..039998780 100644
--- a/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipopMr1.java
+++ b/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipopMr1.java
@@ -20,10 +20,10 @@ import android.content.Context;
import android.support.annotation.VisibleForTesting;
import android.telecom.PhoneAccountHandle;
import android.text.TextUtils;
+import android.util.ArrayMap;
import android.util.Pair;
-import com.android.dialer.app.calllog.PhoneAccountUtils;
+import com.android.dialer.calllogutils.PhoneAccountUtils;
import com.android.dialer.phonenumberutil.PhoneNumberHelper;
-import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -45,9 +45,9 @@ class CallLogCacheLollipopMr1 extends CallLogCache {
final Map<Pair<PhoneAccountHandle, CharSequence>, Boolean> mVoicemailQueryCache =
new ConcurrentHashMap<>();
- private final Map<PhoneAccountHandle, String> mPhoneAccountLabelCache = new HashMap<>();
- private final Map<PhoneAccountHandle, Integer> mPhoneAccountColorCache = new HashMap<>();
- private final Map<PhoneAccountHandle, Boolean> mPhoneAccountCallWithNoteCache = new HashMap<>();
+ private final Map<PhoneAccountHandle, String> mPhoneAccountLabelCache = new ArrayMap<>();
+ private final Map<PhoneAccountHandle, Integer> mPhoneAccountColorCache = new ArrayMap<>();
+ private final Map<PhoneAccountHandle, Boolean> mPhoneAccountCallWithNoteCache = new ArrayMap<>();
/* package */ CallLogCacheLollipopMr1(Context context) {
super(context);
diff --git a/java/com/android/dialer/app/contactinfo/ContactInfoCache.java b/java/com/android/dialer/app/contactinfo/ContactInfoCache.java
index 4135cb7b8..6c35711a8 100644
--- a/java/com/android/dialer/app/contactinfo/ContactInfoCache.java
+++ b/java/com/android/dialer/app/contactinfo/ContactInfoCache.java
@@ -29,8 +29,8 @@ import java.util.concurrent.BlockingQueue;
import java.util.concurrent.PriorityBlockingQueue;
/**
- * This is a cache of contact details for the phone numbers in the c all log. The key is the phone
- * number with the country in which teh call was placed or received. The content of the cache is
+ * This is a cache of contact details for the phone numbers in the call log. The key is the phone
+ * number with the country in which the call was placed or received. The content of the cache is
* expired (but not purged) whenever the application comes to the foreground.
*
* <p>This cache queues request for information and queries for information on a background thread,
diff --git a/java/com/android/dialer/app/contactinfo/ContactPhotoLoader.java b/java/com/android/dialer/app/contactinfo/ContactPhotoLoader.java
index a8c718502..71e4a16ad 100644
--- a/java/com/android/dialer/app/contactinfo/ContactPhotoLoader.java
+++ b/java/com/android/dialer/app/contactinfo/ContactPhotoLoader.java
@@ -104,7 +104,7 @@ public class ContactPhotoLoader {
final RoundedBitmapDrawable drawable =
RoundedBitmapDrawableFactory.create(mContext.getResources(), bitmap);
drawable.setAntiAlias(true);
- drawable.setCornerRadius(bitmap.getHeight() / 2);
+ drawable.setCircular(true);
return drawable;
} catch (IOException e) {
LogUtil.e("ContactPhotoLoader.createPhotoIconDrawable", e.toString());
diff --git a/java/com/android/dialer/app/dialpad/DialpadFragment.java b/java/com/android/dialer/app/dialpad/DialpadFragment.java
index 18bb250ce..4785ab16f 100644
--- a/java/com/android/dialer/app/dialpad/DialpadFragment.java
+++ b/java/com/android/dialer/app/dialpad/DialpadFragment.java
@@ -78,9 +78,9 @@ import com.android.dialer.app.DialtactsActivity;
import com.android.dialer.app.R;
import com.android.dialer.app.SpecialCharSequenceMgr;
import com.android.dialer.app.calllog.CallLogAsync;
-import com.android.dialer.app.calllog.PhoneAccountUtils;
import com.android.dialer.callintent.CallIntentBuilder;
import com.android.dialer.callintent.nano.CallInitiationType;
+import com.android.dialer.calllogutils.PhoneAccountUtils;
import com.android.dialer.common.LogUtil;
import com.android.dialer.dialpadview.DialpadKeyButton;
import com.android.dialer.dialpadview.DialpadView;
@@ -598,6 +598,7 @@ public class DialpadFragment extends Fragment
@Override
public void onStart() {
+ LogUtil.d("DialpadFragment.onStart", "first launch: %b", mFirstLaunch);
Trace.beginSection(TAG + " onStart");
super.onStart();
// if the mToneGenerator creation fails, just continue without it. It is
@@ -624,6 +625,7 @@ public class DialpadFragment extends Fragment
@Override
public void onResume() {
+ LogUtil.d("DialpadFragment.onResume", "");
Trace.beginSection(TAG + " onResume");
super.onResume();
diff --git a/java/com/android/dialer/app/filterednumber/BlockedNumbersSettingsActivity.java b/java/com/android/dialer/app/filterednumber/BlockedNumbersSettingsActivity.java
index eef920710..9ec6042c0 100644
--- a/java/com/android/dialer/app/filterednumber/BlockedNumbersSettingsActivity.java
+++ b/java/com/android/dialer/app/filterednumber/BlockedNumbersSettingsActivity.java
@@ -135,11 +135,6 @@ public class BlockedNumbersSettingsActivity extends AppCompatActivity
}
@Override
- public int getActionBarHideOffset() {
- return 0;
- }
-
- @Override
public int getActionBarHeight() {
return 0;
}
diff --git a/java/com/android/dialer/app/legacybindings/DialerLegacyBindings.java b/java/com/android/dialer/app/legacybindings/DialerLegacyBindings.java
index 2125a1524..1cdeb2175 100644
--- a/java/com/android/dialer/app/legacybindings/DialerLegacyBindings.java
+++ b/java/com/android/dialer/app/legacybindings/DialerLegacyBindings.java
@@ -17,12 +17,14 @@
package com.android.dialer.app.legacybindings;
import android.app.Activity;
+import android.support.annotation.NonNull;
import android.view.ViewGroup;
import com.android.dialer.app.calllog.CallLogAdapter;
import com.android.dialer.app.calllog.calllogcache.CallLogCache;
import com.android.dialer.app.contactinfo.ContactInfoCache;
import com.android.dialer.app.list.RegularSearchFragment;
import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
/**
* These are old bindings between Dialer and the container application. All new bindings should be
@@ -41,6 +43,7 @@ public interface DialerLegacyBindings {
CallLogCache callLogCache,
ContactInfoCache contactInfoCache,
VoicemailPlaybackPresenter voicemailPlaybackPresenter,
+ @NonNull FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler,
int activityType);
RegularSearchFragment newRegularSearchFragment();
diff --git a/java/com/android/dialer/app/legacybindings/DialerLegacyBindingsStub.java b/java/com/android/dialer/app/legacybindings/DialerLegacyBindingsStub.java
index f01df78f8..6e32843ba 100644
--- a/java/com/android/dialer/app/legacybindings/DialerLegacyBindingsStub.java
+++ b/java/com/android/dialer/app/legacybindings/DialerLegacyBindingsStub.java
@@ -17,12 +17,14 @@
package com.android.dialer.app.legacybindings;
import android.app.Activity;
+import android.support.annotation.NonNull;
import android.view.ViewGroup;
import com.android.dialer.app.calllog.CallLogAdapter;
import com.android.dialer.app.calllog.calllogcache.CallLogCache;
import com.android.dialer.app.contactinfo.ContactInfoCache;
import com.android.dialer.app.list.RegularSearchFragment;
import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
/** Default implementation for dialer legacy bindings. */
public class DialerLegacyBindingsStub implements DialerLegacyBindings {
@@ -35,6 +37,7 @@ public class DialerLegacyBindingsStub implements DialerLegacyBindings {
CallLogCache callLogCache,
ContactInfoCache contactInfoCache,
VoicemailPlaybackPresenter voicemailPlaybackPresenter,
+ @NonNull FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler,
int activityType) {
return new CallLogAdapter(
activity,
@@ -43,6 +46,7 @@ public class DialerLegacyBindingsStub implements DialerLegacyBindings {
callLogCache,
contactInfoCache,
voicemailPlaybackPresenter,
+ filteredNumberAsyncQueryHandler,
activityType);
}
diff --git a/java/com/android/dialer/app/list/ListsFragment.java b/java/com/android/dialer/app/list/ListsFragment.java
index 725ad3001..13938f29a 100644
--- a/java/com/android/dialer/app/list/ListsFragment.java
+++ b/java/com/android/dialer/app/list/ListsFragment.java
@@ -30,19 +30,16 @@ import android.support.annotation.Nullable;
import android.support.v13.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v4.view.ViewPager.OnPageChangeListener;
-import android.support.v7.app.ActionBar;
-import android.support.v7.app.AppCompatActivity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.android.contacts.common.list.ViewPagerTabs;
import com.android.dialer.app.R;
import com.android.dialer.app.calllog.CallLogFragment;
-import com.android.dialer.app.calllog.CallLogNotificationsHelper;
+import com.android.dialer.app.calllog.CallLogNotificationsService;
import com.android.dialer.app.calllog.VisualVoicemailCallLogFragment;
import com.android.dialer.app.voicemail.error.VoicemailStatusCorruptionHandler;
import com.android.dialer.app.voicemail.error.VoicemailStatusCorruptionHandler.Source;
-import com.android.dialer.app.widget.ActionBarController;
import com.android.dialer.common.LogUtil;
import com.android.dialer.database.CallLogQueryHandler;
import com.android.dialer.logging.Logger;
@@ -92,7 +89,6 @@ public class ListsFragment extends Fragment
public static final int TAB_COUNT_DEFAULT = 3;
public static final int TAB_COUNT_WITH_VOICEMAIL = 4;
private static final String TAG = "ListsFragment";
- private ActionBar mActionBar;
private ViewPager mViewPager;
private ViewPagerTabs mViewPagerTabs;
private ViewPagerAdapter mViewPagerAdapter;
@@ -108,8 +104,7 @@ public class ListsFragment extends Fragment
private boolean mHasFetchedVoicemailStatus;
private boolean mShowVoicemailTabAfterVoicemailStatusIsFetched;
private VoicemailStatusHelper mVoicemailStatusHelper;
- private ArrayList<OnPageChangeListener> mOnPageChangeListeners =
- new ArrayList<OnPageChangeListener>();
+ private final ArrayList<OnPageChangeListener> mOnPageChangeListeners = new ArrayList<>();
private String[] mTabTitles;
private int[] mTabIcons;
/** The position of the currently selected tab. */
@@ -149,7 +144,6 @@ public class ListsFragment extends Fragment
Trace.beginSection(TAG + " onResume");
super.onResume();
- mActionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
if (getUserVisibleHint()) {
sendScreenViewForCurrentPosition();
}
@@ -329,7 +323,7 @@ public class ListsFragment extends Fragment
.putBoolean(
VisualVoicemailEnabledChecker.PREF_KEY_HAS_ACTIVE_VOICEMAIL_PROVIDER,
hasActiveVoicemailProvider)
- .commit();
+ .apply();
}
if (hasActiveVoicemailProvider) {
@@ -403,7 +397,7 @@ public class ListsFragment extends Fragment
public void markMissedCallsAsReadAndRemoveNotifications() {
if (mCallLogQueryHandler != null) {
mCallLogQueryHandler.markMissedCallsAsRead();
- CallLogNotificationsHelper.removeMissedCallNotifications(getActivity());
+ CallLogNotificationsService.markNewMissedCallsAsOld(getContext(), null);
}
}
@@ -413,11 +407,6 @@ public class ListsFragment extends Fragment
mRemoveView.animate().alpha(show ? 1 : 0).start();
}
- public boolean shouldShowActionBar() {
- // TODO: Update this based on scroll state.
- return mActionBar != null;
- }
-
public SpeedDialFragment getSpeedDialFragment() {
return mSpeedDialFragment;
}
@@ -486,11 +475,6 @@ public class ListsFragment extends Fragment
throw new IllegalStateException("No fragment at position " + position);
}
- public interface HostInterface {
-
- ActionBarController getActionBarController();
- }
-
public class ViewPagerAdapter extends FragmentPagerAdapter {
private final List<Fragment> mFragments = new ArrayList<>();
@@ -518,7 +502,7 @@ public class ListsFragment extends Fragment
return mSpeedDialFragment;
case TAB_INDEX_HISTORY:
if (mHistoryFragment == null) {
- mHistoryFragment = new CallLogFragment();
+ mHistoryFragment = new CallLogFragment(CallLogQueryHandler.CALL_TYPE_ALL);
}
return mHistoryFragment;
case TAB_INDEX_ALL_CONTACTS:
diff --git a/java/com/android/dialer/app/list/SearchFragment.java b/java/com/android/dialer/app/list/SearchFragment.java
index 4a7d48ae4..e6615aa8d 100644
--- a/java/com/android/dialer/app/list/SearchFragment.java
+++ b/java/com/android/dialer/app/list/SearchFragment.java
@@ -98,6 +98,7 @@ public class SearchFragment extends PhoneNumberPickerFragment {
@Override
public void onStart() {
+ LogUtil.d("SearchFragment.onStart", "");
super.onStart();
if (isSearchMode()) {
getAdapter().setHasHeader(0, false);
@@ -301,6 +302,7 @@ public class SearchFragment extends PhoneNumberPickerFragment {
* shown. This can be optionally animated.
*/
public void updatePosition(boolean animate) {
+ LogUtil.d("SearchFragment.updatePosition", "animate: %b", animate);
if (mActivity == null) {
// Activity will be set in onStart, and this method will be called again
return;
@@ -363,6 +365,13 @@ public class SearchFragment extends PhoneNumberPickerFragment {
return;
}
int spacerHeight = mActivity.isDialpadShown() ? mActivity.getDialpadHeight() : 0;
+ LogUtil.d(
+ "SearchFragment.resizeListView",
+ "spacerHeight: %d -> %d, isDialpadShown: %b, dialpad height: %d",
+ mSpacer.getHeight(),
+ spacerHeight,
+ mActivity.isDialpadShown(),
+ mActivity.getDialpadHeight());
if (spacerHeight != mSpacer.getHeight()) {
final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mSpacer.getLayoutParams();
lp.height = spacerHeight;
@@ -418,8 +427,6 @@ public class SearchFragment extends PhoneNumberPickerFragment {
int getDialpadHeight();
- int getActionBarHideOffset();
-
int getActionBarHeight();
}
}
diff --git a/java/com/android/dialer/app/manifests/activities/AndroidManifest.xml b/java/com/android/dialer/app/manifests/activities/AndroidManifest.xml
index 247b34f4c..7e450c4cd 100644
--- a/java/com/android/dialer/app/manifests/activities/AndroidManifest.xml
+++ b/java/com/android/dialer/app/manifests/activities/AndroidManifest.xml
@@ -29,18 +29,6 @@
android:theme="@style/SettingsStyle">
</activity>
- <activity
- android:label="@string/callDetailTitle"
- android:name="com.android.dialer.app.CallDetailActivity"
- android:parentActivityName="com.android.dialer.calllog.CallLogActivity"
- android:theme="@style/CallDetailActivityTheme">
- <intent-filter>
- <action android:name="android.intent.action.VIEW"/>
- <category android:name="android.intent.category.DEFAULT"/>
- <data android:mimeType="vnd.android.cursor.item/calls"/>
- </intent-filter>
- </activity>
-
<!-- The entrance point for Phone UI.
stateAlwaysHidden is set to suppress keyboard show up on
dialpad screen. -->
diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/search_shadow.9.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/search_shadow.9.png
new file mode 100644
index 000000000..ff55620d0
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/search_shadow.9.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/layout/call_detail.xml b/java/com/android/dialer/app/res/layout/call_detail.xml
deleted file mode 100644
index 58a7bf0dc..000000000
--- a/java/com/android/dialer/app/res/layout/call_detail.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2009 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/call_detail"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="@color/background_dialer_call_log">
-
- <!--
- The list view is under everything.
- It contains a first header element which is hidden under the controls UI.
- When scrolling, the controls move up until the name bar hits the top.
- -->
- <ListView
- android:id="@+id/history"
- android:layout_width="match_parent"
- android:layout_height="fill_parent"/>
-
-</FrameLayout>
diff --git a/java/com/android/dialer/app/res/layout/call_detail_footer.xml b/java/com/android/dialer/app/res/layout/call_detail_footer.xml
deleted file mode 100644
index 57713448e..000000000
--- a/java/com/android/dialer/app/res/layout/call_detail_footer.xml
+++ /dev/null
@@ -1,52 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical">
-
- <View
- android:layout_width="match_parent"
- android:layout_height="@dimen/divider_line_thickness"
- android:background="@color/call_log_action_divider"/>
-
- <TextView
- android:id="@+id/call_detail_action_copy"
- style="@style/CallDetailActionItemStyle"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:drawableStart="@drawable/ic_call_detail_content_copy"
- android:text="@string/action_copy_number_text"/>
-
- <TextView
- android:id="@+id/call_detail_action_edit_before_call"
- style="@style/CallDetailActionItemStyle"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:drawableStart="@drawable/ic_call_detail_edit"
- android:text="@string/action_edit_number_before_call"
- android:visibility="gone"/>
-
- <TextView
- android:id="@+id/call_detail_action_report"
- style="@style/CallDetailActionItemStyle"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:drawableStart="@drawable/ic_call_detail_report"
- android:text="@string/action_report_number"
- android:visibility="gone"/>
-
-</LinearLayout>
diff --git a/java/com/android/dialer/app/res/layout/call_detail_header.xml b/java/com/android/dialer/app/res/layout/call_detail_header.xml
deleted file mode 100644
index fd85f0af1..000000000
--- a/java/com/android/dialer/app/res/layout/call_detail_header.xml
+++ /dev/null
@@ -1,89 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/caller_information"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:paddingTop="@dimen/call_detail_top_margin"
- android:paddingBottom="@dimen/call_detail_bottom_margin"
- android:paddingStart="@dimen/call_detail_horizontal_margin"
- android:background="@color/background_dialer_white"
- android:baselineAligned="false"
- android:elevation="@dimen/call_detail_elevation"
- android:focusable="true"
- android:orientation="horizontal">
-
- <QuickContactBadge
- android:id="@+id/quick_contact_photo"
- android:layout_width="@dimen/contact_photo_size"
- android:layout_height="@dimen/contact_photo_size"
- android:layout_marginTop="3dp"
- android:layout_alignParentStart="true"
- android:layout_gravity="top"
- android:focusable="true"/>
-
- <LinearLayout
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:layout_marginStart="@dimen/call_detail_horizontal_margin"
- android:gravity="center_vertical"
- android:orientation="vertical">
-
- <TextView
- android:id="@+id/caller_name"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginTop="2dp"
- android:layout_marginBottom="3dp"
- android:includeFontPadding="false"
- android:singleLine="true"
- android:textColor="?android:textColorPrimary"
- android:textSize="@dimen/call_log_primary_text_size"/>
-
- <TextView
- android:id="@+id/caller_number"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginBottom="1dp"
- android:singleLine="true"
- android:textColor="?android:textColorSecondary"
- android:textSize="@dimen/call_log_detail_text_size"/>
-
- <TextView
- android:id="@+id/phone_account_label"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:singleLine="true"
- android:textColor="?android:textColorSecondary"
- android:textSize="@dimen/call_log_detail_text_size"
- android:visibility="gone"/>
-
- </LinearLayout>
-
- <ImageView
- android:id="@+id/call_back_button"
- android:layout_width="@dimen/call_log_list_item_primary_action_dimen"
- android:layout_height="@dimen/call_log_list_item_primary_action_dimen"
- android:layout_marginEnd="4dp"
- android:background="?android:attr/selectableItemBackgroundBorderless"
- android:contentDescription="@string/description_call_log_call_action"
- android:scaleType="center"
- android:src="@drawable/ic_call_24dp"
- android:tint="@color/call_log_list_item_primary_action_icon_tint"
- android:visibility="gone"/>
-
-</LinearLayout>
diff --git a/java/com/android/dialer/app/res/layout/call_detail_history_item.xml b/java/com/android/dialer/app/res/layout/call_detail_history_item.xml
index 5958ee81c..0184a42f2 100644
--- a/java/com/android/dialer/app/res/layout/call_detail_history_item.xml
+++ b/java/com/android/dialer/app/res/layout/call_detail_history_item.xml
@@ -27,9 +27,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
- <view
+ <com.android.dialer.calllogutils.CallTypeIconsView
android:id="@+id/call_type_icon"
- class="com.android.dialer.app.calllog.CallTypeIconsView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"/>
diff --git a/java/com/android/dialer/app/res/layout/call_log_activity.xml b/java/com/android/dialer/app/res/layout/call_log_activity.xml
new file mode 100644
index 000000000..4e2b1887c
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/call_log_activity.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/calllog_frame"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ <com.android.contacts.common.list.ViewPagerTabs
+ android:id="@+id/viewpager_header"
+ style="@style/DialtactsActionBarTabTextStyle"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/tab_height"
+ android:layout_gravity="top"
+ android:elevation="@dimen/tab_elevation"
+ android:orientation="horizontal"
+ android:textAllCaps="true"/>
+ <android.support.v4.view.ViewPager
+ android:id="@+id/call_log_pager"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"/>
+ <RelativeLayout
+ android:id="@+id/floating_action_button_container"
+ android:layout_width="0dp"
+ android:layout_height="0dp"/>
+</LinearLayout>
diff --git a/java/com/android/dialer/app/res/layout/call_log_list_item.xml b/java/com/android/dialer/app/res/layout/call_log_list_item.xml
index c22ac861d..1592aa928 100644
--- a/java/com/android/dialer/app/res/layout/call_log_list_item.xml
+++ b/java/com/android/dialer/app/res/layout/call_log_list_item.xml
@@ -93,8 +93,7 @@
android:layout_height="wrap_content"
android:orientation="horizontal">
- <view
- class="com.android.dialer.app.calllog.CallTypeIconsView"
+ <com.android.dialer.calllogutils.CallTypeIconsView
android:id="@+id/call_type_icons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
diff --git a/java/com/android/voicemailomtp/res/xml/voicemail_settings.xml b/java/com/android/dialer/app/res/menu/call_log_options.xml
index 03bc34efc..e78b72e3c 100644
--- a/java/com/android/voicemailomtp/res/xml/voicemail_settings.xml
+++ b/java/com/android/dialer/app/res/menu/call_log_options.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2014 The Android Open Source Project
+<!-- Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -13,15 +13,10 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
-<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
- android:title="@string/voicemail_settings_label">
-
- <SwitchPreference
- android:key="@string/voicemail_visual_voicemail_key"
- android:title="@string/voicemail_visual_voicemail_switch_title"/>"
-
- <Preference
- android:key="@string/voicemail_change_pin_key"
- android:title="@string/voicemail_change_pin_dialog_title"/>
-</PreferenceScreen>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/delete_all"
+ android:orderInCategory="1"
+ android:showAsAction="never"
+ android:title="@string/call_log_delete_all"/>
+</menu>
diff --git a/java/com/android/dialer/app/res/menu/dialtacts_options.xml b/java/com/android/dialer/app/res/menu/dialtacts_options.xml
index 434aa81d9..25a3e1811 100644
--- a/java/com/android/dialer/app/res/menu/dialtacts_options.xml
+++ b/java/com/android/dialer/app/res/menu/dialtacts_options.xml
@@ -16,13 +16,17 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
- android:id="@+id/menu_delete_all"
- android:title="@string/call_log_delete_all"/>
+ android:id="@+id/menu_history"
+ android:icon="@drawable/ic_menu_history_lt"
+ android:title="@string/action_menu_call_history_description"/>
<item
android:id="@+id/menu_clear_frequents"
android:title="@string/menu_clear_frequents"/>
<item
android:id="@+id/menu_call_settings"
android:title="@string/dialer_settings_label"/>
+ <item
+ android:id="@+id/menu_simulator_submenu"
+ android:title="@string/simulator_submenu_label"/>
</menu>
diff --git a/java/com/android/dialer/app/res/values/colors.xml b/java/com/android/dialer/app/res/values/colors.xml
index b88e55276..cf6b926be 100644
--- a/java/com/android/dialer/app/res/values/colors.xml
+++ b/java/com/android/dialer/app/res/values/colors.xml
@@ -16,7 +16,6 @@
<resources>
<color name="dialer_red_highlight_color">#ff1744</color>
- <color name="dialer_green_highlight_color">#00c853</color>
<color name="dialer_button_text_color">#fff</color>
<color name="dialer_flat_button_text_color">@color/dialer_theme_color</color>
@@ -84,13 +83,6 @@
as call back, play voicemail, etc. -->
<color name="call_log_action_text">@color/dialer_theme_color</color>
- <!-- Color for missed call icons. -->
- <color name="missed_call">#ff2e58</color>
- <!-- Color for answered or outgoing call icons. -->
- <color name="answered_call">@color/dialer_green_highlight_color</color>
- <!-- Color for blocked call icons. -->
- <color name="blocked_call">@color/dialer_secondary_text_color</color>
-
<color name="dialer_dialpad_touch_tint">@color/dialer_theme_color_20pct</color>
<color name="floating_action_button_touch_tint">#80ffffff</color>
diff --git a/java/com/android/dialer/app/res/values/dimens.xml b/java/com/android/dialer/app/res/values/dimens.xml
index f3fd63350..7da29c7a3 100644
--- a/java/com/android/dialer/app/res/values/dimens.xml
+++ b/java/com/android/dialer/app/res/values/dimens.xml
@@ -28,7 +28,6 @@
<dimen name="call_log_horizontal_margin">8dp</dimen>
<dimen name="call_log_call_action_size">32dp</dimen>
<dimen name="call_log_call_action_width">54dp</dimen>
- <dimen name="call_log_icon_margin">4dp</dimen>
<dimen name="call_log_inner_margin">13dp</dimen>
<dimen name="call_log_outer_margin">8dp</dimen>
<dimen name="call_log_start_margin">8dp</dimen>
@@ -68,7 +67,7 @@
<item name="contact_tile_height_to_width_ratio" type="dimen">76%</item>
<dimen name="contact_tile_text_side_padding">12dp</dimen>
<dimen name="contact_tile_text_bottom_padding">9dp</dimen>
- <dimen name="favorites_row_top_padding">2dp</dimen>
+ <dimen name="favorites_row_top_padding">1dp</dimen>
<dimen name="favorites_row_bottom_padding">0dp</dimen>
<dimen name="favorites_row_start_padding">1dp</dimen>
@@ -143,6 +142,4 @@
<dimen name="blocked_number_search_text_size">14sp</dimen>
<dimen name="blocked_number_settings_description_text_size">14sp</dimen>
<dimen name="blocked_number_header_height">48dp</dimen>
-
- <dimen name="call_type_icon_size">12dp</dimen>
</resources>
diff --git a/java/com/android/dialer/app/res/values/strings.xml b/java/com/android/dialer/app/res/values/strings.xml
index 689ee1ba8..66bf70f1a 100644
--- a/java/com/android/dialer/app/res/values/strings.xml
+++ b/java/com/android/dialer/app/res/values/strings.xml
@@ -55,9 +55,6 @@
<!-- Label for action to unblock a number [CHAR LIMIT=48]-->
<string name="action_unblock_number">Unblock number</string>
- <!-- Menu item in call details used to remove a call or voicemail from the call log. -->
- <string name="call_details_delete">Delete</string>
-
<!-- Label for action to edit a number before calling it. [CHAR LIMIT=48] -->
<string name="action_edit_number_before_call">Edit number before call</string>
@@ -94,7 +91,7 @@
<!-- Missed call notification label, used when there are two or more missed calls -->
<string name="notification_missedCallsTitle">Missed calls</string>
<!-- Missed call notification message used when there are multiple missed calls -->
- <string name="notification_missedCallsMsg"><xliff:g id="num_missed_calls">%s</xliff:g> missed calls</string>
+ <string name="notification_missedCallsMsg"><xliff:g id="num_missed_calls">%d</xliff:g> missed calls</string>
<!-- Message for "call back" Action, which is displayed in the missed call notificaiton.
The user will be able to call back to the person or the phone number.
[CHAR LIMIT=18] -->
@@ -251,15 +248,13 @@
<!-- Label for the dialer app setting page [CHAR LIMIT=30]-->
<string name="dialer_settings_label">Settings</string>
+ <!-- Label for the simulator submenu. This is used to show actions that are useful for development
+ and testing. [CHAR LIMIT=30]-->
+ <string name="simulator_submenu_label">Simulator</string>
+
<!-- Menu item to display all contacts [CHAR LIMIT=30] -->
<string name="menu_allContacts">All contacts</string>
- <!-- Title bar for call detail screen -->
- <string name="callDetailTitle">Call details</string>
-
- <!-- Toast for call detail screen when couldn't read the requested details -->
- <string name="toast_call_detail_error">Details not available</string>
-
<!-- Item label: jump to the in-call DTMF dialpad.
(Part of a list of options shown in the dialer when another call
is already in progress.) -->
@@ -275,52 +270,6 @@
is already in progress.) -->
<string name="dialer_addAnotherCall">Add call</string>
- <!-- Title for incoming call type. [CHAR LIMIT=40] -->
- <string name="type_incoming">Incoming call</string>
-
- <!-- Title for incoming call which was transferred to another device. [CHAR LIMIT=60] -->
- <string name="type_incoming_pulled">Incoming call transferred to another device</string>
-
- <!-- Title for outgoing call type. [CHAR LIMIT=40] -->
- <string name="type_outgoing">Outgoing call</string>
-
- <!-- Title for outgoing call which was transferred to another device. [CHAR LIMIT=60] -->
- <string name="type_outgoing_pulled">Outgoing call transferred to another device</string>
-
- <!-- Title for missed call type. [CHAR LIMIT=40] -->
- <string name="type_missed">Missed call</string>
-
- <!-- Title for incoming video call in call details screen [CHAR LIMIT=60] -->
- <string name="type_incoming_video">Incoming video call</string>
-
- <!-- Title for incoming video call in call details screen which was transferred to another device.
- [CHAR LIMIT=60] -->
- <string name="type_incoming_video_pulled">Incoming video call transferred to another device</string>
-
- <!-- Title for outgoing video call in call details screen [CHAR LIMIT=60] -->
- <string name="type_outgoing_video">Outgoing video call</string>
-
- <!-- Title for outgoing video call in call details screen which was transferred to another device.
- [CHAR LIMIT=60] -->
- <string name="type_outgoing_video_pulled">Outgoing video call transferred to another device</string>
-
- <!-- Title for missed video call in call details screen [CHAR LIMIT=60] -->
- <string name="type_missed_video">Missed video call</string>
-
- <!-- Title for voicemail details screen -->
- <string name="type_voicemail">Voicemail</string>
-
- <!-- Title for rejected call type. [CHAR LIMIT=40] -->
- <string name="type_rejected">Declined call</string>
-
- <!-- Title for blocked call type. [CHAR LIMIT=40] -->
- <string name="type_blocked">Blocked call</string>
-
- <!-- Title for "answered elsewhere" call type. This will happen if a call was ringing
- simultaneously on multiple devices, and the user answered it on a device other than the
- current device. [CHAR LIMIT=60] -->
- <string name="type_answered_elsewhere">Call answered on another device</string>
-
<!-- Description for incoming calls going to voice mail vs. not -->
<string name="actionIncomingCall">Incoming calls</string>
@@ -623,28 +572,10 @@
[CHAR LIMIT=NONE] -->
<string name="description_outgoing_call">Call to <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g>, <xliff:g example="Mobile" id="typeOrLocation">^2</xliff:g>, <xliff:g example="2 min ago" id="timeOfCall">^3</xliff:g>, <xliff:g example="on SIM 1" id="phoneAccount">^4</xliff:g>.</string>
- <!-- String describing the phone account the call was made on or to. This string will be used
- in description_incoming_missed_call, description_incoming_answered_call, and
- description_outgoing_call.
- Note: AccessibilityServices uses this attribute to announce what the view represents.
- [CHAR LIMIT=NONE] -->
- <string name="description_phone_account">on <xliff:g example="SIM 1" id="phoneAccount">^1</xliff:g></string>
-
- <!-- String describing the secondary line number the call was received via.
- Note: AccessibilityServices use this attribute to announce what the view represents.
- [CHAR LIMIT=NONE]-->
- <string name="description_via_number">via <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g></string>
-
<!-- TextView text item showing the secondary line number the call was received via.
[CHAR LIMIT=NONE]-->
<string name="call_log_via_number">via <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g></string>
- <!-- String describing the PhoneAccount and via number that a call was received on, if both are
- visible.
- Note: AccessibilityServices use this attribute to announce what the view represents.
- [CHAR LIMIT=NONE]-->
- <string name="description_via_number_phone_account">on <xliff:g example="SIM 1" id="phoneAccount">%1$s</xliff:g>, via <xliff:g example="(555) 555-5555" id="number">%2$s</xliff:g></string>
-
<!-- The order of the PhoneAccount and via number that a call was received on,
if both are visible.
[CHAR LIMIT=NONE]-->
@@ -827,6 +758,9 @@
<!-- Label for the blocked numbers settings section [CHAR LIMIT=30] -->
<string name="manage_blocked_numbers_label">Call blocking</string>
+ <!-- Label for the voicemail settings section [CHAR LIMIT=30] -->
+ <string name="voicemail_settings_label">Voicemail</string>
+
<!-- Label for a section describing that call blocking is temporarily disabled because an
emergency call was made. [CHAR LIMIT=50] -->
<string name="blocked_numbers_disabled_emergency_header_label">
@@ -955,6 +889,6 @@
<string name="spam_number_call_log_label">Spam</string>
<!-- Shown as a message that notifies the user enriched calling isn't working -->
- <string name="call_composer_connection_failed"><xliff:g id="feature">%1$s</xliff:g> unavailable right now</string>
+ <string name="call_composer_connection_failed"><xliff:g id="name">%1$s</xliff:g> is offline and can\'t be reached</string>
</resources>
diff --git a/java/com/android/dialer/app/res/values/styles.xml b/java/com/android/dialer/app/res/values/styles.xml
index ac4422ba2..24521ddaf 100644
--- a/java/com/android/dialer/app/res/values/styles.xml
+++ b/java/com/android/dialer/app/res/values/styles.xml
@@ -111,11 +111,6 @@
<item name="android:fastScrollTrackDrawable">@null</item>
</style>
- <style name="CallDetailActivityTheme" parent="DialtactsThemeWithoutActionBarOverlay">
- <item name="android:windowBackground">@color/background_dialer_results</item>
- <item name="android:actionOverflowButtonStyle">@style/DialtactsActionBarOverflowWhite</item>
- </style>
-
<style name="CallDetailActionItemStyle">
<item name="android:foreground">?android:attr/selectableItemBackground</item>
<item name="android:clickable">true</item>
diff --git a/java/com/android/dialer/app/settings/DialerSettingsActivity.java b/java/com/android/dialer/app/settings/DialerSettingsActivity.java
index b04674013..bbf1cfae5 100644
--- a/java/com/android/dialer/app/settings/DialerSettingsActivity.java
+++ b/java/com/android/dialer/app/settings/DialerSettingsActivity.java
@@ -33,8 +33,11 @@ import com.android.dialer.app.R;
import com.android.dialer.blocking.FilteredNumberCompat;
import com.android.dialer.compat.CompatUtils;
import com.android.dialer.proguard.UsedByReflection;
+import com.android.voicemail.VoicemailComponent;
import java.util.List;
+/** Activity for dialer settings. */
+@SuppressWarnings("FragmentInjection") // Activity not exported
@UsedByReflection(value = "AndroidManifest-app.xml")
public class DialerSettingsActivity extends AppCompatPreferenceActivity {
@@ -115,6 +118,16 @@ public class DialerSettingsActivity extends AppCompatPreferenceActivity {
target.add(blockedCallsHeader);
migrationStatusOnBuildHeaders = FilteredNumberCompat.hasMigratedToNewBlocking(this);
}
+
+ String voicemailSettingsFragment =
+ VoicemailComponent.get(this).getVoicemailClient().getSettingsFragment();
+ if (isPrimaryUser && voicemailSettingsFragment != null) {
+ Header voicemailSettings = new Header();
+ voicemailSettings.titleRes = R.string.voicemail_settings_label;
+ voicemailSettings.fragment = voicemailSettingsFragment;
+ target.add(voicemailSettings);
+ }
+
if (isPrimaryUser
&& (TelephonyManagerCompat.isTtyModeSupported(telephonyManager)
|| TelephonyManagerCompat.isHearingAidCompatibilitySupported(telephonyManager))) {
diff --git a/java/com/android/dialer/app/voicemail/VoicemailPlaybackLayout.java b/java/com/android/dialer/app/voicemail/VoicemailPlaybackLayout.java
index fc6a37608..f40ed2794 100644
--- a/java/com/android/dialer/app/voicemail/VoicemailPlaybackLayout.java
+++ b/java/com/android/dialer/app/voicemail/VoicemailPlaybackLayout.java
@@ -30,7 +30,6 @@ import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.TextView;
-import com.android.dialer.app.PhoneCallDetails;
import com.android.dialer.app.R;
import com.android.dialer.app.calllog.CallLogAsyncTaskUtil;
import com.android.dialer.app.calllog.CallLogListItemViewHolder;
@@ -347,16 +346,10 @@ public class VoicemailPlaybackLayout extends LinearLayout
}
@Override
- public void onDeleteCall() {}
-
- @Override
public void onDeleteVoicemail() {
mPresenter.onVoicemailDeletedInDatabase();
}
- @Override
- public void onGetCallDetails(PhoneCallDetails[] details) {}
-
private String getString(int resId) {
return mContext.getString(resId);
}
diff --git a/java/com/android/dialer/app/voicemail/VoicemailPlaybackPresenter.java b/java/com/android/dialer/app/voicemail/VoicemailPlaybackPresenter.java
index 657022291..994160ff9 100644
--- a/java/com/android/dialer/app/voicemail/VoicemailPlaybackPresenter.java
+++ b/java/com/android/dialer/app/voicemail/VoicemailPlaybackPresenter.java
@@ -39,6 +39,7 @@ import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.v4.content.FileProvider;
import android.text.TextUtils;
+import android.view.View;
import android.view.WindowManager.LayoutParams;
import android.webkit.MimeTypeMap;
import com.android.common.io.MoreCloseables;
@@ -47,8 +48,11 @@ import com.android.dialer.app.calllog.CallLogListItemViewHolder;
import com.android.dialer.common.Assert;
import com.android.dialer.common.AsyncTaskExecutor;
import com.android.dialer.common.AsyncTaskExecutors;
+import com.android.dialer.common.ConfigProviderBindings;
import com.android.dialer.common.LogUtil;
import com.android.dialer.constants.Constants;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
import com.android.dialer.phonenumbercache.CallLogQuery;
import com.google.common.io.ByteStreams;
import java.io.File;
@@ -71,9 +75,9 @@ import javax.annotation.concurrent.ThreadSafe;
* assumptions about the behaviors and lifecycle of the call log, in particular in the {@link
* CallLogFragment} and {@link CallLogAdapter}.
*
- * <p>This controls a single {@link com.android.dialer.voicemail.VoicemailPlaybackLayout}. A single
- * instance can be reused for different such layouts, using {@link #setPlaybackView}. This is to
- * facilitate reuse across different voicemail call log entries.
+ * <p>This controls a single {@link com.android.dialer.app.voicemail.VoicemailPlaybackLayout}. A
+ * single instance can be reused for different such layouts, using {@link #setPlaybackView}. This is
+ * to facilitate reuse across different voicemail call log entries.
*
* <p>This class is not thread safe. The thread policy for this class is thread-confinement, all
* calls into this class from outside must be done from the main UI thread.
@@ -103,6 +107,8 @@ public class VoicemailPlaybackPresenter
private static final String IS_SPEAKERPHONE_ON_KEY =
VoicemailPlaybackPresenter.class.getName() + ".IS_SPEAKER_PHONE_ON";
private static final String VOICEMAIL_SHARE_FILE_NAME_DATE_FORMAT = "MM-dd-yy_hhmmaa";
+ private static final String CONFIG_SHARE_VOICEMAIL_ALLOWED = "share_voicemail_allowed";
+
private static VoicemailPlaybackPresenter sInstance;
private static ScheduledExecutorService mScheduledExecutorService;
/**
@@ -138,6 +144,7 @@ public class VoicemailPlaybackPresenter
private PowerManager.WakeLock mProximityWakeLock;
private VoicemailAudioManager mVoicemailAudioManager;
private OnVoicemailDeletedListener mOnVoicemailDeletedListener;
+ private View shareVoicemailButtonView;
/** Initialize variables which are activity-independent and state-independent. */
protected VoicemailPlaybackPresenter(Activity activity) {
@@ -222,11 +229,17 @@ public class VoicemailPlaybackPresenter
/** Specify the view which this presenter controls and the voicemail to prepare to play. */
public void setPlaybackView(
- PlaybackView view, long rowId, Uri voicemailUri, final boolean startPlayingImmediately) {
+ PlaybackView view,
+ long rowId,
+ Uri voicemailUri,
+ final boolean startPlayingImmediately,
+ View shareVoicemailButtonView) {
mRowId = rowId;
mView = view;
mView.setPresenter(this, voicemailUri);
mView.onSpeakerphoneOn(mIsSpeakerphoneOn);
+ this.shareVoicemailButtonView = shareVoicemailButtonView;
+ showShareVoicemailButton(false);
// Handles cases where the same entry is binded again when scrolling in list, or where
// the MediaPlayer was retained after an orientation change.
@@ -236,6 +249,7 @@ public class VoicemailPlaybackPresenter
// media player.
mPosition = mMediaPlayer.getCurrentPosition();
onPrepared(mMediaPlayer);
+ showShareVoicemailButton(true);
} else {
if (!voicemailUri.equals(mVoicemailUri)) {
mVoicemailUri = voicemailUri;
@@ -247,19 +261,17 @@ public class VoicemailPlaybackPresenter
* it if the content is not available.
*/
checkForContent(
- new OnContentCheckedListener() {
- @Override
- public void onContentChecked(boolean hasContent) {
- if (hasContent) {
- prepareContent();
- } else {
- if (startPlayingImmediately) {
- requestContent(PLAYBACK_REQUEST);
- }
- if (mView != null) {
- mView.resetSeekBar();
- mView.setClipPosition(0, mDuration.get());
- }
+ hasContent -> {
+ if (hasContent) {
+ showShareVoicemailButton(true);
+ prepareContent();
+ } else {
+ if (startPlayingImmediately) {
+ requestContent(PLAYBACK_REQUEST);
+ }
+ if (mView != null) {
+ mView.resetSeekBar();
+ mView.setClipPosition(0, mDuration.get());
}
}
});
@@ -547,6 +559,7 @@ public class VoicemailPlaybackPresenter
mPosition = 0;
mIsPlaying = false;
+ showShareVoicemailButton(false);
}
/** After done playing the voicemail clip, reset the clip position to the start. */
@@ -600,18 +613,16 @@ public class VoicemailPlaybackPresenter
* timeout, but succeeded.
*/
checkForContent(
- new OnContentCheckedListener() {
- @Override
- public void onContentChecked(boolean hasContent) {
- if (!hasContent) {
- // No local content, download from server. Queue playing if the request was
- // issued,
- mIsPlaying = requestContent(PLAYBACK_REQUEST);
- } else {
- // Queue playing once the media play loaded the content.
- mIsPlaying = true;
- prepareContent();
- }
+ hasContent -> {
+ if (!hasContent) {
+ // No local content, download from server. Queue playing if the request was
+ // issued,
+ mIsPlaying = requestContent(PLAYBACK_REQUEST);
+ } else {
+ showShareVoicemailButton(true);
+ // Queue playing once the media play loaded the content.
+ mIsPlaying = true;
+ prepareContent();
}
});
return;
@@ -813,6 +824,20 @@ public class VoicemailPlaybackPresenter
sInstance = null;
}
+ private void showShareVoicemailButton(boolean show) {
+ if (isShareVoicemailAllowed(mContext) && shareVoicemailButtonView != null) {
+ if (show) {
+ Logger.get(mContext).logImpression(DialerImpression.Type.VVM_SHARE_VISIBLE);
+ }
+ LogUtil.d("VoicemailPlaybackPresenter.showShareVoicemailButton", "show: %b", show);
+ shareVoicemailButtonView.setVisibility(show ? View.VISIBLE : View.GONE);
+ }
+ }
+
+ private static boolean isShareVoicemailAllowed(Context context) {
+ return ConfigProviderBindings.get(context).getBoolean(CONFIG_SHARE_VOICEMAIL_ALLOWED, true);
+ }
+
/**
* Share voicemail to be opened by user selected apps. This method will collect information, copy
* voicemail to a temporary file in background and launch a chooser intent to share it.
@@ -1041,6 +1066,7 @@ public class VoicemailPlaybackPresenter
public void onPostExecute(Boolean hasContent) {
if (hasContent && mContext != null && mIsWaitingForResult.getAndSet(false)) {
mContext.getContentResolver().unregisterContentObserver(FetchResultHandler.this);
+ showShareVoicemailButton(true);
prepareContent();
}
}
diff --git a/java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java b/java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java
index e36406d17..190426e6e 100644
--- a/java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java
+++ b/java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java
@@ -17,10 +17,18 @@
package com.android.dialer.app.voicemail.error;
import android.content.Context;
+import android.preference.PreferenceManager;
import android.provider.VoicemailContract.Status;
import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
import com.android.dialer.app.voicemail.error.VoicemailErrorMessage.Action;
+import com.android.dialer.common.Assert;
import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.PerAccountSharedPreferences;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.voicemail.VoicemailClient;
+import com.android.voicemail.VoicemailComponent;
import java.util.ArrayList;
import java.util.List;
@@ -32,14 +40,18 @@ public class OmtpVoicemailMessageCreator {
private static final float QUOTA_NEAR_FULL_THRESHOLD = 0.9f;
private static final float QUOTA_FULL_THRESHOLD = 0.99f;
+ protected static final String VOICEMAIL_PROMO_DISMISSED_KEY =
+ "voicemail_archive_promo_was_dismissed";
+ protected static final String VOICEMAIL_PROMO_ALMOST_FULL_DISMISSED_KEY =
+ "voicemail_archive_almost_full_promo_was_dismissed";
@Nullable
- public static VoicemailErrorMessage create(Context context, VoicemailStatus status) {
+ public static VoicemailErrorMessage create(
+ Context context, VoicemailStatus status, final VoicemailStatusReader statusReader) {
if (Status.CONFIGURATION_STATE_OK == status.configurationState
&& Status.DATA_CHANNEL_STATE_OK == status.dataChannelState
&& Status.NOTIFICATION_CHANNEL_STATE_OK == status.notificationChannelState) {
-
- return checkQuota(context, status);
+ return checkQuota(context, status, statusReader);
}
// Initial state when the source is activating. Other error might be written into data and
// notification channel during activation.
@@ -120,24 +132,98 @@ public class OmtpVoicemailMessageCreator {
}
@Nullable
- private static VoicemailErrorMessage checkQuota(Context context, VoicemailStatus status) {
+ private static VoicemailErrorMessage checkQuota(
+ Context context, VoicemailStatus status, VoicemailStatusReader statusReader) {
if (status.quotaOccupied != Status.QUOTA_UNAVAILABLE
&& status.quotaTotal != Status.QUOTA_UNAVAILABLE) {
+
+ PhoneAccountHandle phoneAccountHandle = status.getPhoneAccountHandle();
+
+ VoicemailClient voicemailClient = VoicemailComponent.get(context).getVoicemailClient();
+
+ PerAccountSharedPreferences sharedPreferenceForAccount =
+ new PerAccountSharedPreferences(
+ context, phoneAccountHandle, PreferenceManager.getDefaultSharedPreferences(context));
+
+ boolean isVoicemailArchiveEnabled =
+ VoicemailComponent.get(context)
+ .getVoicemailClient()
+ .isVoicemailArchiveEnabled(context, phoneAccountHandle);
+
if ((float) status.quotaOccupied / (float) status.quotaTotal >= QUOTA_FULL_THRESHOLD) {
- return new VoicemailErrorMessage(
+ return createInboxErrorMessage(
+ context,
+ status,
+ status.getPhoneAccountHandle(),
+ statusReader,
+ sharedPreferenceForAccount,
+ voicemailClient,
+ isVoicemailArchiveEnabled,
+ context.getString(R.string.voicemail_error_inbox_full_turn_archive_on_title),
+ context.getString(R.string.voicemail_error_inbox_full_turn_archive_on_message),
context.getString(R.string.voicemail_error_inbox_full_title),
- context.getString(R.string.voicemail_error_inbox_full_message));
+ context.getString(R.string.voicemail_error_inbox_full_message),
+ VOICEMAIL_PROMO_DISMISSED_KEY);
}
if ((float) status.quotaOccupied / (float) status.quotaTotal >= QUOTA_NEAR_FULL_THRESHOLD) {
- return new VoicemailErrorMessage(
+ return createInboxErrorMessage(
+ context,
+ status,
+ status.getPhoneAccountHandle(),
+ statusReader,
+ sharedPreferenceForAccount,
+ voicemailClient,
+ isVoicemailArchiveEnabled,
+ context.getString(R.string.voicemail_error_inbox_almost_full_turn_archive_on_title),
+ context.getString(R.string.voicemail_error_inbox_almost_full_turn_archive_on_message),
context.getString(R.string.voicemail_error_inbox_near_full_title),
- context.getString(R.string.voicemail_error_inbox_near_full_message));
+ context.getString(R.string.voicemail_error_inbox_near_full_message),
+ VOICEMAIL_PROMO_ALMOST_FULL_DISMISSED_KEY);
}
}
return null;
}
+ private static VoicemailErrorMessage createInboxErrorMessage(
+ Context context,
+ VoicemailStatus status,
+ PhoneAccountHandle phoneAccountHandle,
+ VoicemailStatusReader statusReader,
+ PerAccountSharedPreferences sharedPreferenceForAccount,
+ VoicemailClient voicemailClient,
+ boolean isVoicemailArchiveEnabled,
+ String promoTitle,
+ String promoMessage,
+ String nonPromoTitle,
+ String nonPromoMessage,
+ String preferenceKey) {
+
+ boolean wasPromoDismissed = sharedPreferenceForAccount.getBoolean(preferenceKey, false);
+
+ if (!wasPromoDismissed && !isVoicemailArchiveEnabled) {
+ logArchiveImpression(
+ context,
+ preferenceKey,
+ DialerImpression.Type.VVM_USER_SHOWN_VM_ALMOST_FULL_PROMO,
+ DialerImpression.Type.VVM_USER_SHOWN_VM_FULL_PROMO);
+ return new VoicemailErrorMessage(
+ promoTitle,
+ promoMessage,
+ VoicemailErrorMessage.createDismissTurnArchiveOnAction(
+ context, statusReader, sharedPreferenceForAccount, preferenceKey),
+ VoicemailErrorMessage.createTurnArchiveOnAction(
+ context, status, voicemailClient, phoneAccountHandle, preferenceKey));
+ } else {
+ logArchiveImpression(
+ context,
+ preferenceKey,
+ DialerImpression.Type.VVM_USER_SHOWN_VM_ALMOST_FULL_ERROR_MESSAGE,
+ DialerImpression.Type.VVM_USER_SHOWN_VM_FULL_ERROR_MESSAGE);
+ return new VoicemailErrorMessage(nonPromoTitle, nonPromoMessage);
+ }
+ }
+
@Nullable
private static VoicemailErrorMessage createNoSignalMessage(
Context context, VoicemailStatus status) {
@@ -174,4 +260,15 @@ public class OmtpVoicemailMessageCreator {
}
return new VoicemailErrorMessage(title, description, actions);
}
+
+ protected static void logArchiveImpression(
+ Context context, String preference, int vmAlmostFullImpression, int vmFullImpression) {
+ if (preference.equals(VOICEMAIL_PROMO_DISMISSED_KEY)) {
+ Logger.get(context).logImpression(vmAlmostFullImpression);
+ } else if (preference.equals(VOICEMAIL_PROMO_ALMOST_FULL_DISMISSED_KEY)) {
+ Logger.get(context).logImpression(vmFullImpression);
+ } else {
+ throw Assert.createAssertionFailException("Invalid preference key " + preference);
+ }
+ }
}
diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessage.java b/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessage.java
index 61572008b..f85d91186 100644
--- a/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessage.java
+++ b/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessage.java
@@ -22,12 +22,15 @@ import android.provider.Settings;
import android.provider.VoicemailContract;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
import android.telephony.TelephonyManager;
import android.view.View;
import android.view.View.OnClickListener;
+import com.android.dialer.common.PerAccountSharedPreferences;
import com.android.dialer.logging.Logger;
import com.android.dialer.logging.nano.DialerImpression;
import com.android.dialer.util.CallUtil;
+import com.android.voicemail.VoicemailClient;
import java.util.Arrays;
import java.util.List;
@@ -175,4 +178,52 @@ public class VoicemailErrorMessage {
}
});
}
+
+ @NonNull
+ public static Action createTurnArchiveOnAction(
+ final Context context,
+ final VoicemailStatus status,
+ VoicemailClient voicemailClient,
+ PhoneAccountHandle phoneAccountHandle,
+ String preference) {
+ return new Action(
+ context.getString(R.string.voicemail_action_turn_archive_on),
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ OmtpVoicemailMessageCreator.logArchiveImpression(
+ context,
+ preference,
+ DialerImpression.Type.VVM_USER_ENABLED_ARCHIVE_FROM_VM_FULL_PROMO,
+ DialerImpression.Type.VVM_USER_ENABLED_ARCHIVE_FROM_VM_ALMOST_FULL_PROMO);
+
+ voicemailClient.setVoicemailArchiveEnabled(context, phoneAccountHandle, true);
+ Intent intent = new Intent(VoicemailContract.ACTION_SYNC_VOICEMAIL);
+ intent.setPackage(status.sourcePackage);
+ context.sendBroadcast(intent);
+ }
+ });
+ }
+
+ @NonNull
+ public static Action createDismissTurnArchiveOnAction(
+ final Context context,
+ VoicemailStatusReader statusReader,
+ PerAccountSharedPreferences sharedPreferenceForAccount,
+ String preferenceKeyToUpdate) {
+ return new Action(
+ context.getString(R.string.voicemail_action_dimiss),
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ OmtpVoicemailMessageCreator.logArchiveImpression(
+ context,
+ preferenceKeyToUpdate,
+ DialerImpression.Type.VVM_USER_DISMISSED_VM_FULL_PROMO,
+ DialerImpression.Type.VVM_USER_DISMISSED_VM_ALMOST_FULL_PROMO);
+ sharedPreferenceForAccount.edit().putBoolean(preferenceKeyToUpdate, true);
+ statusReader.refresh();
+ }
+ });
+ }
}
diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessageCreator.java b/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessageCreator.java
index 5ebef801d..7dc18f043 100644
--- a/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessageCreator.java
+++ b/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessageCreator.java
@@ -39,7 +39,7 @@ public class VoicemailErrorMessageCreator {
case Vvm3VoicemailMessageCreator.VVM_TYPE_VVM3:
return Vvm3VoicemailMessageCreator.create(context, status, statusReader);
default:
- return OmtpVoicemailMessageCreator.create(context, status);
+ return OmtpVoicemailMessageCreator.create(context, status, statusReader);
}
}
}
diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailStatus.java b/java/com/android/dialer/app/voicemail/error/VoicemailStatus.java
index a09941de2..c429d6dcc 100644
--- a/java/com/android/dialer/app/voicemail/error/VoicemailStatus.java
+++ b/java/com/android/dialer/app/voicemail/error/VoicemailStatus.java
@@ -16,6 +16,7 @@
package com.android.dialer.app.voicemail.error;
+import android.content.ComponentName;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
@@ -25,6 +26,7 @@ import android.provider.Settings;
import android.provider.Settings.Global;
import android.provider.VoicemailContract.Status;
import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
import android.telephony.TelephonyManager;
import com.android.dialer.database.VoicemailStatusQuery;
@@ -257,4 +259,9 @@ public class VoicemailStatus {
}
return cursor.getString(index);
}
+
+ public PhoneAccountHandle getPhoneAccountHandle() {
+ return new PhoneAccountHandle(
+ ComponentName.unflattenFromString(phoneAccountComponentName), phoneAccountId);
+ }
}
diff --git a/java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java b/java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java
index 6e9405cbf..356131bb3 100644
--- a/java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java
+++ b/java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java
@@ -269,7 +269,7 @@ public class Vvm3VoicemailMessageCreator {
VoicemailErrorMessage.createSetPinAction(context));
}
- return OmtpVoicemailMessageCreator.create(context, status);
+ return OmtpVoicemailMessageCreator.create(context, status, statusReader);
}
@NonNull
diff --git a/java/com/android/dialer/app/voicemail/error/res/layout/voicemai_error_message_fragment.xml b/java/com/android/dialer/app/voicemail/error/res/layout/voicemai_error_message_fragment.xml
index 0dfb1c2fd..4a40857a0 100644
--- a/java/com/android/dialer/app/voicemail/error/res/layout/voicemai_error_message_fragment.xml
+++ b/java/com/android/dialer/app/voicemail/error/res/layout/voicemai_error_message_fragment.xml
@@ -48,7 +48,6 @@
<TextView
android:id="@+id/error_card_details"
- android:autoLink="web"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lineSpacingExtra="@dimen/alert_line_spacing"
diff --git a/java/com/android/dialer/app/voicemail/error/res/layout/voicemail_tos_fragment.xml b/java/com/android/dialer/app/voicemail/error/res/layout/voicemail_tos_fragment.xml
index 2b9d17328..c193eaa47 100644
--- a/java/com/android/dialer/app/voicemail/error/res/layout/voicemail_tos_fragment.xml
+++ b/java/com/android/dialer/app/voicemail/error/res/layout/voicemail_tos_fragment.xml
@@ -31,7 +31,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="16dp"
- android:autoLink="web"
android:text="@string/verizon_terms_and_conditions_1.1_english"
android:textColor="@color/secondary_text_color"
android:textSize="@dimen/call_log_detail_text_size"/>
diff --git a/java/com/android/dialer/app/voicemail/error/res/values/strings.xml b/java/com/android/dialer/app/voicemail/error/res/values/strings.xml
index 1d39b9dcb..94d3dba11 100644
--- a/java/com/android/dialer/app/voicemail/error/res/values/strings.xml
+++ b/java/com/android/dialer/app/voicemail/error/res/values/strings.xml
@@ -54,6 +54,11 @@
<string name="voicemail_error_inbox_full_title">Can\'t receive new voicemails</string>
<string name="voicemail_error_inbox_full_message">Your inbox is full. Try deleting some messages to receive new voicemail.</string>
+ <string name="voicemail_error_inbox_full_turn_archive_on_title">Turn on extra storage and backup</string>
+ <string name="voicemail_error_inbox_full_turn_archive_on_message">Your mailbox is full. To free up space, turn on extra storage so Google can manage and backup your voicemail messages.</string>
+
+ <string name="voicemail_error_inbox_almost_full_turn_archive_on_title">Turn on extra storage and backup</string>
+ <string name="voicemail_error_inbox_almost_full_turn_archive_on_message">Your mailbox is almost full. To free up space, turn on extra storage so Google can manage and backup your voicemail messages.</string>
<string name="voicemail_error_pin_not_set_title">Set your voicemail PIN</string>
<string name="voicemail_error_pin_not_set_message">You\'ll need a voicemail PIN anytime you call to access your voicemail.</string>
@@ -63,6 +68,8 @@
<string name="voicemail_action_turn_off_airplane_mode">Airplane Mode Settings</string>
<string name="voicemail_action_set_pin">Set PIN</string>
<string name="voicemail_action_retry">Try Again</string>
+ <string name="voicemail_action_turn_archive_on">Turn on</string>
+ <string name="voicemail_action_dimiss">No Thanks</string>
<string name="voicemail_action_sync">Sync</string>
<string name="voicemail_action_call_voicemail">Call Voicemail</string>
<string name="voicemail_action_call_customer_support">Call Customer Support</string>
diff --git a/java/com/android/dialer/app/widget/ActionBarController.java b/java/com/android/dialer/app/widget/ActionBarController.java
index 7fe056c51..d0eb326ab 100644
--- a/java/com/android/dialer/app/widget/ActionBarController.java
+++ b/java/com/android/dialer/app/widget/ActionBarController.java
@@ -16,12 +16,9 @@
package com.android.dialer.app.widget;
import android.animation.ValueAnimator;
-import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.os.Bundle;
-import android.support.annotation.VisibleForTesting;
-import android.util.Log;
import com.android.dialer.animation.AnimUtils.AnimationCallback;
-import com.android.dialer.app.DialtactsActivity;
+import com.android.dialer.common.LogUtil;
/**
* Controls the various animated properties of the actionBar: showing/hiding, fading/revealing, and
@@ -30,8 +27,6 @@ import com.android.dialer.app.DialtactsActivity;
*/
public class ActionBarController {
- public static final boolean DEBUG = DialtactsActivity.DEBUG;
- public static final String TAG = "ActionBarController";
private static final String KEY_IS_SLID_UP = "key_actionbar_is_slid_up";
private static final String KEY_IS_FADED_OUT = "key_actionbar_is_faded_out";
private static final String KEY_IS_EXPANDED = "key_actionbar_is_expanded";
@@ -66,9 +61,8 @@ public class ActionBarController {
/** Called when the user has tapped on the collapsed search box, to start a new search query. */
public void onSearchBoxTapped() {
- if (DEBUG) {
- Log.d(TAG, "OnSearchBoxTapped: isInSearchUi " + mActivityUi.isInSearchUi());
- }
+ LogUtil.d(
+ "ActionBarController.onSearchBoxTapped", "isInSearchUi " + mActivityUi.isInSearchUi());
if (!mActivityUi.isInSearchUi()) {
mSearchBox.expand(true /* animate */, true /* requestFocus */);
}
@@ -76,16 +70,11 @@ public class ActionBarController {
/** Called when search UI has been exited for some reason. */
public void onSearchUiExited() {
- if (DEBUG) {
- Log.d(
- TAG,
- "OnSearchUIExited: isExpanded "
- + mSearchBox.isExpanded()
- + " isFadedOut: "
- + mSearchBox.isFadedOut()
- + " shouldShowActionBar: "
- + mActivityUi.shouldShowActionBar());
- }
+ LogUtil.d(
+ "ActionBarController.onSearchUIExited",
+ "isExpanded: %b, isFadedOut %b",
+ mSearchBox.isExpanded(),
+ mSearchBox.isFadedOut());
if (mSearchBox.isExpanded()) {
mSearchBox.collapse(true /* animate */);
}
@@ -93,11 +82,7 @@ public class ActionBarController {
mSearchBox.fadeIn();
}
- if (mActivityUi.shouldShowActionBar()) {
- slideActionBar(false /* slideUp */, false /* animate */);
- } else {
- slideActionBar(true /* slideUp */, false /* animate */);
- }
+ slideActionBar(false /* slideUp */, false /* animate */);
}
/**
@@ -105,18 +90,13 @@ public class ActionBarController {
* state changes have actually occurred.
*/
public void onDialpadDown() {
- if (DEBUG) {
- Log.d(
- TAG,
- "OnDialpadDown: isInSearchUi "
- + mActivityUi.isInSearchUi()
- + " hasSearchQuery: "
- + mActivityUi.hasSearchQuery()
- + " isFadedOut: "
- + mSearchBox.isFadedOut()
- + " isExpanded: "
- + mSearchBox.isExpanded());
- }
+ LogUtil.d(
+ "ActionBarController.onDialpadDown",
+ "isInSearchUi: %b, hasSearchQuery: %b, isFadedOut: %b, isExpanded: %b",
+ mActivityUi.isInSearchUi(),
+ mActivityUi.hasSearchQuery(),
+ mSearchBox.isFadedOut(),
+ mSearchBox.isExpanded());
if (mActivityUi.isInSearchUi()) {
if (mActivityUi.hasSearchQuery()) {
if (mSearchBox.isFadedOut()) {
@@ -137,9 +117,7 @@ public class ActionBarController {
* state changes have actually occurred.
*/
public void onDialpadUp() {
- if (DEBUG) {
- Log.d(TAG, "OnDialpadUp: isInSearchUi " + mActivityUi.isInSearchUi());
- }
+ LogUtil.d("ActionBarController.onDialpadUp", "isInSearchUi " + mActivityUi.isInSearchUi());
if (mActivityUi.isInSearchUi()) {
slideActionBar(true /* slideUp */, true /* animate */);
} else {
@@ -149,18 +127,14 @@ public class ActionBarController {
}
public void slideActionBar(boolean slideUp, boolean animate) {
- if (DEBUG) {
- Log.d(TAG, "Sliding actionBar - up: " + slideUp + " animate: " + animate);
- }
+ LogUtil.d("ActionBarController.slidingActionBar", "up: %b, animate: %b", slideUp, animate);
+
if (animate) {
ValueAnimator animator = slideUp ? ValueAnimator.ofFloat(0, 1) : ValueAnimator.ofFloat(1, 0);
animator.addUpdateListener(
- new AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator animation) {
- final float value = (float) animation.getAnimatedValue();
- setHideOffset((int) (mActivityUi.getActionBarHeight() * value));
- }
+ animation -> {
+ final float value = (float) animation.getAnimatedValue();
+ setHideOffset((int) (mActivityUi.getActionBarHeight() * value));
});
animator.start();
} else {
@@ -173,20 +147,11 @@ public class ActionBarController {
mSearchBox.animate().alpha(alphaValue).start();
}
- /** @return The offset the action bar is being translated upwards by */
- public int getHideOffset() {
- return mActivityUi.getActionBarHideOffset();
- }
-
public void setHideOffset(int offset) {
mIsActionBarSlidUp = offset >= mActivityUi.getActionBarHeight();
mActivityUi.setActionBarHideOffset(offset);
}
- public int getActionBarHeight() {
- return mActivityUi.getActionBarHeight();
- }
-
/** Saves the current state of the action bar into a provided {@link Bundle} */
public void saveInstanceState(Bundle outState) {
outState.putBoolean(KEY_IS_SLID_UP, mIsActionBarSlidUp);
@@ -225,23 +190,14 @@ public class ActionBarController {
slideActionBar(mIsActionBarSlidUp /* slideUp */, false /* animate */);
}
- @VisibleForTesting
- public boolean getIsActionBarSlidUp() {
- return mIsActionBarSlidUp;
- }
-
public interface ActivityUi {
boolean isInSearchUi();
boolean hasSearchQuery();
- boolean shouldShowActionBar();
-
int getActionBarHeight();
- int getActionBarHideOffset();
-
void setActionBarHideOffset(int offset);
}
}
diff --git a/java/com/android/dialer/backup/AndroidManifest.xml b/java/com/android/dialer/backup/AndroidManifest.xml
index cfdb3d93d..1cbbe5339 100644
--- a/java/com/android/dialer/backup/AndroidManifest.xml
+++ b/java/com/android/dialer/backup/AndroidManifest.xml
@@ -21,7 +21,6 @@
android:backupAgent="com.android.dialer.backup.DialerBackupAgent"
android:fullBackupOnly="true"
android:restoreAnyVersion="true"
- android:name="com.android.dialer.app.DialerApplication"
/>
-</manifest> \ No newline at end of file
+</manifest>
diff --git a/java/com/android/dialer/backup/DialerBackupAgent.java b/java/com/android/dialer/backup/DialerBackupAgent.java
index 391a93f29..2f8684aa2 100644
--- a/java/com/android/dialer/backup/DialerBackupAgent.java
+++ b/java/com/android/dialer/backup/DialerBackupAgent.java
@@ -31,6 +31,7 @@ import android.provider.CallLog;
import android.provider.CallLog.Calls;
import android.provider.VoicemailContract;
import android.provider.VoicemailContract.Voicemails;
+import android.telecom.PhoneAccountHandle;
import android.util.Pair;
import com.android.dialer.backup.nano.VoicemailInfo;
import com.android.dialer.common.Assert;
@@ -42,6 +43,7 @@ import com.android.dialer.telecom.TelecomUtil;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
+import java.util.List;
import java.util.Locale;
/**
@@ -100,9 +102,11 @@ public class DialerBackupAgent extends BackupAgent {
ConfigProviderBindings.get(this).getBoolean("enable_autobackup", true);
boolean vmBackupEnabled =
ConfigProviderBindings.get(this).getBoolean("enable_vm_backup", false);
+ List<PhoneAccountHandle> phoneAccountsToArchive =
+ DialerBackupUtils.getPhoneAccountsToArchive(this);
if (autoBackupEnabled) {
- if (!maxVoicemailBackupReached && vmBackupEnabled) {
+ if (!maxVoicemailBackupReached && vmBackupEnabled && !phoneAccountsToArchive.isEmpty()) {
voicemailsBackedupSoFar = 0;
sizeOfVoicemailsBackedupSoFar = 0;
@@ -123,9 +127,12 @@ public class DialerBackupAgent extends BackupAgent {
uri,
null,
String.format(
- "(%s = ? AND deleted = 0 AND %s = ?)", Calls.TYPE, Voicemails.SOURCE_PACKAGE),
+ "(%s = ? AND deleted = 0 AND %s = ? AND ?)",
+ Calls.TYPE, Voicemails.SOURCE_PACKAGE),
new String[] {
- Integer.toString(CallLog.Calls.VOICEMAIL_TYPE), VOICEMAIL_SOURCE_PACKAGE
+ Integer.toString(CallLog.Calls.VOICEMAIL_TYPE),
+ VOICEMAIL_SOURCE_PACKAGE,
+ DialerBackupUtils.getPhoneAccountClause(phoneAccountsToArchive)
},
ORDER_BY_DATE,
null)) {
@@ -150,11 +157,12 @@ public class DialerBackupAgent extends BackupAgent {
LogUtil.i(
"DialerBackupAgent.onFullBackup",
"vm files backed up: %d, vm size backed up:%d, "
- + "max vm backup reached:%b, vm backup enabled:%b",
+ + "max vm backup reached:%b, vm backup enabled:%b phone accounts to archive: %d",
voicemailsBackedupSoFar,
sizeOfVoicemailsBackedupSoFar,
maxVoicemailBackupReached,
- vmBackupEnabled);
+ vmBackupEnabled,
+ phoneAccountsToArchive.size());
super.onFullBackup(data);
Logger.get(this).logImpression(DialerImpression.Type.BACKUP_FULL_BACKED_UP);
} else {
diff --git a/java/com/android/dialer/backup/DialerBackupUtils.java b/java/com/android/dialer/backup/DialerBackupUtils.java
index ff0cb4f7c..410772ff0 100644
--- a/java/com/android/dialer/backup/DialerBackupUtils.java
+++ b/java/com/android/dialer/backup/DialerBackupUtils.java
@@ -27,10 +27,14 @@ import android.provider.VoicemailContract;
import android.provider.VoicemailContract.Voicemails;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
import android.util.Pair;
import com.android.dialer.backup.nano.VoicemailInfo;
+import com.android.dialer.common.Assert;
import com.android.dialer.common.ConfigProviderBindings;
import com.android.dialer.common.LogUtil;
+import com.android.voicemail.VoicemailComponent;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import com.google.protobuf.nano.MessageNano;
@@ -40,6 +44,8 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
/** Helper functions for DialerBackupAgent */
public class DialerBackupUtils {
@@ -317,4 +323,42 @@ public class DialerBackupUtils {
}
return false;
}
+
+ public static String getPhoneAccountClause(List<PhoneAccountHandle> phoneAccountsToArchive) {
+ Assert.checkArgument(!phoneAccountsToArchive.isEmpty());
+ StringBuilder whereQuery = new StringBuilder();
+
+ whereQuery.append("(");
+
+ for (int i = 0; i < phoneAccountsToArchive.size(); i++) {
+ whereQuery.append(
+ Voicemails.PHONE_ACCOUNT_ID + " = " + phoneAccountsToArchive.get(i).getId());
+
+ if (phoneAccountsToArchive.size() > 1 && i < phoneAccountsToArchive.size() - 1) {
+ whereQuery.append(" OR ");
+ }
+ }
+ whereQuery.append(")");
+ return whereQuery.toString();
+ }
+
+ public static List<PhoneAccountHandle> getPhoneAccountsToArchive(Context context) {
+ List<PhoneAccountHandle> phoneAccountsToBackUp = new ArrayList<>();
+
+ for (PhoneAccountHandle handle :
+ context.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts()) {
+
+ if (VoicemailComponent.get(context)
+ .getVoicemailClient()
+ .isVoicemailArchiveEnabled(context, handle)) {
+ phoneAccountsToBackUp.add(handle);
+ LogUtil.i(
+ "DialerBackupUtils.getPhoneAccountsToArchive", "enabled for: " + handle.toString());
+ } else {
+ LogUtil.i(
+ "DialerBackupUtils.getPhoneAccountsToArchive", "not enabled for: " + handle.toString());
+ }
+ }
+ return phoneAccountsToBackUp;
+ }
}
diff --git a/java/com/android/dialer/backup/proto/VoicemailInfo.java b/java/com/android/dialer/backup/nano/VoicemailInfo.java
index 9ff8423f3..f11595ec2 100644
--- a/java/com/android/dialer/backup/proto/VoicemailInfo.java
+++ b/java/com/android/dialer/backup/nano/VoicemailInfo.java
@@ -18,16 +18,17 @@
package com.android.dialer.backup.nano;
+/** This file is autogenerated, but javadoc required. */
@SuppressWarnings("hiding")
-public final class VoicemailInfo extends
- com.google.protobuf.nano.ExtendableMessageNano<VoicemailInfo> {
+public final class VoicemailInfo
+ extends com.google.protobuf.nano.ExtendableMessageNano<VoicemailInfo> {
private static volatile VoicemailInfo[] _emptyArray;
+
public static VoicemailInfo[] emptyArray() {
// Lazily initializes the empty array
if (_emptyArray == null) {
- synchronized (
- com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+ synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
if (_emptyArray == null) {
_emptyArray = new VoicemailInfo[0];
}
@@ -178,7 +179,8 @@ public final class VoicemailInfo extends
if (this.voicemailUri != null && !this.voicemailUri.equals("")) {
output.writeString(17, this.voicemailUri);
}
- if (!java.util.Arrays.equals(this.encodedVoicemailKey, com.google.protobuf.nano.WireFormatNano.EMPTY_BYTES)) {
+ if (!java.util.Arrays.equals(
+ this.encodedVoicemailKey, com.google.protobuf.nano.WireFormatNano.EMPTY_BYTES)) {
output.writeBytes(18, this.encodedVoicemailKey);
}
if (this.archived != null && !this.archived.equals("")) {
@@ -191,175 +193,196 @@ public final class VoicemailInfo extends
protected int computeSerializedSize() {
int size = super.computeSerializedSize();
if (this.date != null && !this.date.equals("")) {
- size += com.google.protobuf.nano.CodedOutputByteBufferNano
- .computeStringSize(1, this.date);
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(1, this.date);
}
if (this.deleted != null && !this.deleted.equals("")) {
- size += com.google.protobuf.nano.CodedOutputByteBufferNano
- .computeStringSize(2, this.deleted);
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(2, this.deleted);
}
if (this.dirty != null && !this.dirty.equals("")) {
- size += com.google.protobuf.nano.CodedOutputByteBufferNano
- .computeStringSize(3, this.dirty);
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(3, this.dirty);
}
if (this.dirType != null && !this.dirType.equals("")) {
- size += com.google.protobuf.nano.CodedOutputByteBufferNano
- .computeStringSize(4, this.dirType);
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(4, this.dirType);
}
if (this.duration != null && !this.duration.equals("")) {
- size += com.google.protobuf.nano.CodedOutputByteBufferNano
- .computeStringSize(5, this.duration);
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(5, this.duration);
}
if (this.hasContent != null && !this.hasContent.equals("")) {
- size += com.google.protobuf.nano.CodedOutputByteBufferNano
- .computeStringSize(6, this.hasContent);
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(6, this.hasContent);
}
if (this.isRead != null && !this.isRead.equals("")) {
- size += com.google.protobuf.nano.CodedOutputByteBufferNano
- .computeStringSize(7, this.isRead);
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(7, this.isRead);
}
if (this.itemType != null && !this.itemType.equals("")) {
- size += com.google.protobuf.nano.CodedOutputByteBufferNano
- .computeStringSize(8, this.itemType);
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(8, this.itemType);
}
if (this.lastModified != null && !this.lastModified.equals("")) {
- size += com.google.protobuf.nano.CodedOutputByteBufferNano
- .computeStringSize(9, this.lastModified);
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(
+ 9, this.lastModified);
}
if (this.mimeType != null && !this.mimeType.equals("")) {
- size += com.google.protobuf.nano.CodedOutputByteBufferNano
- .computeStringSize(10, this.mimeType);
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(10, this.mimeType);
}
if (this.number != null && !this.number.equals("")) {
- size += com.google.protobuf.nano.CodedOutputByteBufferNano
- .computeStringSize(11, this.number);
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(11, this.number);
}
if (this.phoneAccountComponentName != null && !this.phoneAccountComponentName.equals("")) {
- size += com.google.protobuf.nano.CodedOutputByteBufferNano
- .computeStringSize(12, this.phoneAccountComponentName);
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(
+ 12, this.phoneAccountComponentName);
}
if (this.phoneAccountId != null && !this.phoneAccountId.equals("")) {
- size += com.google.protobuf.nano.CodedOutputByteBufferNano
- .computeStringSize(13, this.phoneAccountId);
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(
+ 13, this.phoneAccountId);
}
if (this.sourceData != null && !this.sourceData.equals("")) {
- size += com.google.protobuf.nano.CodedOutputByteBufferNano
- .computeStringSize(14, this.sourceData);
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(14, this.sourceData);
}
if (this.sourcePackage != null && !this.sourcePackage.equals("")) {
- size += com.google.protobuf.nano.CodedOutputByteBufferNano
- .computeStringSize(15, this.sourcePackage);
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(
+ 15, this.sourcePackage);
}
if (this.transcription != null && !this.transcription.equals("")) {
- size += com.google.protobuf.nano.CodedOutputByteBufferNano
- .computeStringSize(16, this.transcription);
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(
+ 16, this.transcription);
}
if (this.voicemailUri != null && !this.voicemailUri.equals("")) {
- size += com.google.protobuf.nano.CodedOutputByteBufferNano
- .computeStringSize(17, this.voicemailUri);
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(
+ 17, this.voicemailUri);
}
- if (!java.util.Arrays.equals(this.encodedVoicemailKey, com.google.protobuf.nano.WireFormatNano.EMPTY_BYTES)) {
- size += com.google.protobuf.nano.CodedOutputByteBufferNano
- .computeBytesSize(18, this.encodedVoicemailKey);
+ if (!java.util.Arrays.equals(
+ this.encodedVoicemailKey, com.google.protobuf.nano.WireFormatNano.EMPTY_BYTES)) {
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeBytesSize(
+ 18, this.encodedVoicemailKey);
}
if (this.archived != null && !this.archived.equals("")) {
- size += com.google.protobuf.nano.CodedOutputByteBufferNano
- .computeStringSize(19, this.archived);
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(19, this.archived);
}
return size;
}
@Override
- public VoicemailInfo mergeFrom(
- com.google.protobuf.nano.CodedInputByteBufferNano input)
+ public VoicemailInfo mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
throws java.io.IOException {
while (true) {
int tag = input.readTag();
switch (tag) {
case 0:
return this;
- default: {
- if (!super.storeUnknownField(input, tag)) {
- return this;
+ default:
+ {
+ if (!super.storeUnknownField(input, tag)) {
+ return this;
+ }
+ break;
+ }
+ case 10:
+ {
+ this.date = input.readString();
+ break;
+ }
+ case 18:
+ {
+ this.deleted = input.readString();
+ break;
+ }
+ case 26:
+ {
+ this.dirty = input.readString();
+ break;
+ }
+ case 34:
+ {
+ this.dirType = input.readString();
+ break;
+ }
+ case 42:
+ {
+ this.duration = input.readString();
+ break;
+ }
+ case 50:
+ {
+ this.hasContent = input.readString();
+ break;
+ }
+ case 58:
+ {
+ this.isRead = input.readString();
+ break;
+ }
+ case 66:
+ {
+ this.itemType = input.readString();
+ break;
+ }
+ case 74:
+ {
+ this.lastModified = input.readString();
+ break;
+ }
+ case 82:
+ {
+ this.mimeType = input.readString();
+ break;
+ }
+ case 90:
+ {
+ this.number = input.readString();
+ break;
+ }
+ case 98:
+ {
+ this.phoneAccountComponentName = input.readString();
+ break;
+ }
+ case 106:
+ {
+ this.phoneAccountId = input.readString();
+ break;
+ }
+ case 114:
+ {
+ this.sourceData = input.readString();
+ break;
+ }
+ case 122:
+ {
+ this.sourcePackage = input.readString();
+ break;
+ }
+ case 130:
+ {
+ this.transcription = input.readString();
+ break;
+ }
+ case 138:
+ {
+ this.voicemailUri = input.readString();
+ break;
+ }
+ case 146:
+ {
+ this.encodedVoicemailKey = input.readBytes();
+ break;
+ }
+ case 154:
+ {
+ this.archived = input.readString();
+ break;
}
- break;
- }
- case 10: {
- this.date = input.readString();
- break;
- }
- case 18: {
- this.deleted = input.readString();
- break;
- }
- case 26: {
- this.dirty = input.readString();
- break;
- }
- case 34: {
- this.dirType = input.readString();
- break;
- }
- case 42: {
- this.duration = input.readString();
- break;
- }
- case 50: {
- this.hasContent = input.readString();
- break;
- }
- case 58: {
- this.isRead = input.readString();
- break;
- }
- case 66: {
- this.itemType = input.readString();
- break;
- }
- case 74: {
- this.lastModified = input.readString();
- break;
- }
- case 82: {
- this.mimeType = input.readString();
- break;
- }
- case 90: {
- this.number = input.readString();
- break;
- }
- case 98: {
- this.phoneAccountComponentName = input.readString();
- break;
- }
- case 106: {
- this.phoneAccountId = input.readString();
- break;
- }
- case 114: {
- this.sourceData = input.readString();
- break;
- }
- case 122: {
- this.sourcePackage = input.readString();
- break;
- }
- case 130: {
- this.transcription = input.readString();
- break;
- }
- case 138: {
- this.voicemailUri = input.readString();
- break;
- }
- case 146: {
- this.encodedVoicemailKey = input.readBytes();
- break;
- }
- case 154: {
- this.archived = input.readString();
- break;
- }
}
}
}
@@ -369,8 +392,7 @@ public final class VoicemailInfo extends
return com.google.protobuf.nano.MessageNano.mergeFrom(new VoicemailInfo(), data);
}
- public static VoicemailInfo parseFrom(
- com.google.protobuf.nano.CodedInputByteBufferNano input)
+ public static VoicemailInfo parseFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
throws java.io.IOException {
return new VoicemailInfo().mergeFrom(input);
}
diff --git a/java/com/android/dialer/binary/aosp/AndroidManifest.xml b/java/com/android/dialer/binary/aosp/AndroidManifest.xml
new file mode 100644
index 000000000..63edb8397
--- /dev/null
+++ b/java/com/android/dialer/binary/aosp/AndroidManifest.xml
@@ -0,0 +1,116 @@
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ coreApp="true"
+ package="com.android.dialer"
+ android:versionCode="100000"
+ android:versionName="10.0">
+
+ <uses-sdk
+ android:minSdkVersion="23"
+ android:targetSdkVersion="25"/>
+
+ <uses-permission android:name="android.permission.CALL_PHONE"/>
+ <uses-permission android:name="android.permission.READ_CONTACTS"/>
+ <uses-permission android:name="android.permission.WRITE_CONTACTS"/>
+ <uses-permission android:name="android.permission.READ_CALL_LOG"/>
+ <uses-permission android:name="android.permission.WRITE_CALL_LOG"/>
+ <uses-permission android:name="android.permission.READ_PROFILE"/>
+ <uses-permission android:name="android.permission.MANAGE_ACCOUNTS"/>
+ <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
+ <uses-permission android:name="android.permission.GET_ACCOUNTS_PRIVILEGED"/>
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
+ <uses-permission android:name="android.permission.INTERNET"/>
+ <uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS"/>
+ <uses-permission android:name="android.permission.NFC"/>
+ <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
+ <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
+ <uses-permission android:name="android.permission.MODIFY_PHONE_STATE"/>
+ <uses-permission android:name="android.permission.WAKE_LOCK"/>
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="android.permission.WRITE_SETTINGS"/>
+ <uses-permission android:name="android.permission.USE_CREDENTIALS"/>
+ <uses-permission android:name="android.permission.VIBRATE"/>
+ <uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
+ <uses-permission android:name="com.android.voicemail.permission.ADD_VOICEMAIL"/>
+ <uses-permission android:name="com.android.voicemail.permission.WRITE_VOICEMAIL"/>
+ <uses-permission android:name="com.android.voicemail.permission.READ_VOICEMAIL"/>
+ <uses-permission android:name="android.permission.ALLOW_ANY_CODEC_FOR_PLAYBACK"/>
+ <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
+ <uses-permission android:name="android.permission.BROADCAST_STICKY"/>
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+ <uses-permission android:name="android.permission.SEND_SMS"/>
+
+ <uses-permission android:name="android.permission.CONTROL_INCALL_EXPERIENCE"/>
+ <!-- We use this to disable the status bar buttons of home, back and recent
+ during an incoming call. By doing so this allows us to not show the user
+ is viewing the activity in full screen alert, on a fresh system/factory
+ reset state of the app. -->
+ <uses-permission android:name="android.permission.STATUS_BAR"/>
+ <uses-permission android:name="android.permission.CAMERA"/>
+
+ <!-- This tells the activity manager to not delay any of our activity
+ start requests, even if they happen immediately after the user
+ presses home. -->
+ <uses-permission android:name="android.permission.STOP_APP_SWITCHES"/>
+
+ <!-- Permissions needed for badger count showing on launch icon. -->
+
+ <!--for Samsung-->
+ <uses-permission android:name="com.sec.android.provider.badge.permission.READ"/>
+ <uses-permission android:name="com.sec.android.provider.badge.permission.WRITE"/>
+
+ <!--for htc-->
+ <uses-permission android:name="com.htc.launcher.permission.READ_SETTINGS"/>
+ <uses-permission android:name="com.htc.launcher.permission.UPDATE_SHORTCUT"/>
+
+ <!--for sony-->
+ <uses-permission android:name="com.sonyericsson.home.permission.BROADCAST_BADGE"/>
+ <uses-permission android:name="com.sonymobile.home.permission.PROVIDER_INSERT_BADGE"/>
+
+ <!--for apex-->
+ <uses-permission android:name="com.anddoes.launcher.permission.UPDATE_COUNT"/>
+
+ <!--for solid-->
+ <uses-permission android:name="com.majeur.launcher.permission.UPDATE_BADGE"/>
+
+ <!--for huawei-->
+ <uses-permission android:name="com.huawei.android.launcher.permission.CHANGE_BADGE"/>
+ <uses-permission android:name="com.huawei.android.launcher.permission.READ_SETTINGS"/>
+ <uses-permission android:name="com.huawei.android.launcher.permission.WRITE_SETTINGS"/>
+
+ <!--for ZUK-->
+ <uses-permission android:name="android.permission.READ_APP_BADGE"/>
+
+ <!--for OPPO-->
+ <uses-permission android:name="com.oppo.launcher.permission.READ_SETTINGS"/>
+ <uses-permission android:name="com.oppo.launcher.permission.WRITE_SETTINGS"/>
+
+ <application
+ android:backupAgent='com.android.dialer.backup.DialerBackupAgent'
+ android:fullBackupOnly="true"
+ android:restoreAnyVersion="true"
+ android:hardwareAccelerated="true"
+ android:icon="@mipmap/ic_launcher_phone"
+ android:label="@string/applicationLabel"
+ android:name="com.android.dialer.binary.aosp.AospDialerApplication"
+ android:supportsRtl="true"
+ android:usesCleartextTraffic="false">
+ </application>
+
+</manifest>
diff --git a/java/com/android/dialer/binary/aosp/AospDialerApplication.java b/java/com/android/dialer/binary/aosp/AospDialerApplication.java
new file mode 100644
index 000000000..f657a3987
--- /dev/null
+++ b/java/com/android/dialer/binary/aosp/AospDialerApplication.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.binary.aosp;
+
+import android.support.annotation.NonNull;
+import com.android.dialer.binary.common.DialerApplication;
+import com.android.dialer.inject.ContextModule;
+
+/**
+ * The application class for the AOSP Dialer. This is a version of the Dialer app that has no
+ * dependency on Google Play Services.
+ */
+public class AospDialerApplication extends DialerApplication {
+
+
+}
diff --git a/java/com/android/dialer/binary/aosp/AospDialerRootComponent.java b/java/com/android/dialer/binary/aosp/AospDialerRootComponent.java
new file mode 100644
index 000000000..8628e90c2
--- /dev/null
+++ b/java/com/android/dialer/binary/aosp/AospDialerRootComponent.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.binary.aosp;
+
+import com.android.dialer.binary.basecomponent.BaseDialerRootComponent;
+import com.android.dialer.enrichedcall.stub.StubEnrichedCallModule;
+import com.android.dialer.inject.ContextModule;
+import com.android.dialer.simulator.impl.SimulatorModule;
+import com.android.incallui.calllocation.stub.StubCallLocationModule;
+import com.android.incallui.maps.stub.StubMapsModule;
+import com.android.voicemail.impl.VoicemailModule;
+import dagger.Component;
+import javax.inject.Singleton;
+
+/** Root component for the AOSP Dialer application. */
+public interface AospDialerRootComponent extends BaseDialerRootComponent {}
diff --git a/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java b/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java
new file mode 100644
index 000000000..907671b01
--- /dev/null
+++ b/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.binary.basecomponent;
+
+import com.android.dialer.enrichedcall.EnrichedCallComponent;
+import com.android.dialer.simulator.SimulatorComponent;
+import com.android.incallui.calllocation.CallLocationComponent;
+import com.android.incallui.maps.MapsComponent;
+import com.android.voicemail.VoicemailComponent;
+
+/**
+ * Base class for the core application-wide {@link Component}. All variants of the Dialer app should
+ * extend from this component.
+ */
+public interface BaseDialerRootComponent
+ extends CallLocationComponent.HasComponent,
+ EnrichedCallComponent.HasComponent,
+ MapsComponent.HasComponent,
+ SimulatorComponent.HasComponent,
+ VoicemailComponent.HasComponent {}
diff --git a/java/com/android/dialer/binary/common/DialerApplication.java b/java/com/android/dialer/binary/common/DialerApplication.java
new file mode 100644
index 000000000..c0be4328c
--- /dev/null
+++ b/java/com/android/dialer/binary/common/DialerApplication.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.binary.common;
+
+import android.app.Application;
+import android.os.Trace;
+import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import com.android.dialer.blocking.BlockedNumbersAutoMigrator;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
+
+/** A common application subclass for all Dialer build variants. */
+public abstract class DialerApplication extends Application {
+
+ private volatile Object rootComponent;
+
+ @Override
+ public void onCreate() {
+ Trace.beginSection("DialerApplication.onCreate");
+ super.onCreate();
+ new BlockedNumbersAutoMigrator(
+ this,
+ PreferenceManager.getDefaultSharedPreferences(this),
+ new FilteredNumberAsyncQueryHandler(this))
+ .autoMigrate();
+ Trace.endSection();
+ }
+
+}
diff --git a/java/com/android/dialer/blocking/FilteredNumbersUtil.java b/java/com/android/dialer/blocking/FilteredNumbersUtil.java
index 61ecf1886..f09370e6e 100644
--- a/java/com/android/dialer/blocking/FilteredNumbersUtil.java
+++ b/java/com/android/dialer/blocking/FilteredNumbersUtil.java
@@ -36,6 +36,8 @@ import com.android.dialer.database.FilteredNumberContract.FilteredNumber;
import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns;
import com.android.dialer.logging.Logger;
import com.android.dialer.logging.nano.InteractionEvent;
+import com.android.dialer.notification.NotificationChannelManager;
+import com.android.dialer.notification.NotificationChannelManager.Channel;
import com.android.dialer.util.PermissionsUtil;
import java.util.concurrent.TimeUnit;
@@ -43,7 +45,8 @@ import java.util.concurrent.TimeUnit;
public class FilteredNumbersUtil {
public static final String CALL_BLOCKING_NOTIFICATION_TAG = "call_blocking";
- public static final int CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_NOTIFICATION_ID = 10;
+ public static final int CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_NOTIFICATION_ID =
+ R.id.notification_call_blocking_disabled_by_emergency_call;
// Pref key for storing the time of end of the last emergency call in milliseconds after epoch.
protected static final String LAST_EMERGENCY_CALL_MS_PREF_KEY = "last_emergency_call_ms";
// Pref key for storing whether a notification has been dispatched to notify the user that call
@@ -289,6 +292,7 @@ public class FilteredNumbersUtil {
context.getString(R.string.call_blocking_disabled_notification_text))
.setAutoCancel(true);
+ NotificationChannelManager.applyChannel(builder, context, Channel.MISC, null);
builder.setContentIntent(
PendingIntent.getActivity(
context,
diff --git a/java/com/android/dialer/buildtype/BuildType.java b/java/com/android/dialer/buildtype/BuildType.java
index c0a54a519..6b6bc2906 100644
--- a/java/com/android/dialer/buildtype/BuildType.java
+++ b/java/com/android/dialer/buildtype/BuildType.java
@@ -28,7 +28,7 @@ public class BuildType {
/** The type of build. */
@Retention(RetentionPolicy.SOURCE)
@IntDef({
- BUGFOOD, FISHFOOD, DOGFOOD, RELEASE,
+ BUGFOOD, FISHFOOD, DOGFOOD, RELEASE, TEST,
})
public @interface Type {}
@@ -36,6 +36,7 @@ public class BuildType {
public static final int FISHFOOD = 2;
public static final int DOGFOOD = 3;
public static final int RELEASE = 4;
+ public static final int TEST = 5;
private static int cachedBuildType;
private static boolean didInitializeBuildType;
diff --git a/java/com/android/dialer/buildtype/dogfood/BuildTypeAccessorImpl.java b/java/com/android/dialer/buildtype/release/BuildTypeAccessorImpl.java
index e1f2cdc79..70b9f9e37 100644
--- a/java/com/android/dialer/buildtype/dogfood/BuildTypeAccessorImpl.java
+++ b/java/com/android/dialer/buildtype/release/BuildTypeAccessorImpl.java
@@ -25,6 +25,6 @@ public class BuildTypeAccessorImpl implements BuildTypeAccessor {
@Override
@BuildType.Type
public int getBuildType() {
- return BuildType.DOGFOOD;
+ return BuildType.RELEASE;
}
}
diff --git a/java/com/android/dialer/callcomposer/AndroidManifest.xml b/java/com/android/dialer/callcomposer/AndroidManifest.xml
index c99f22b90..369db6f4a 100644
--- a/java/com/android/dialer/callcomposer/AndroidManifest.xml
+++ b/java/com/android/dialer/callcomposer/AndroidManifest.xml
@@ -20,9 +20,8 @@
<application>
<activity
android:name="com.android.dialer.callcomposer.CallComposerActivity"
- android:exported="false"
+ android:exported="true"
android:theme="@style/Theme.AppCompat.CallComposer"
- android:windowSoftInputMode="adjustResize"
- android:screenOrientation="portrait"/>
+ android:windowSoftInputMode="adjustPan"/>
</application>
</manifest>
diff --git a/java/com/android/dialer/callcomposer/CallComposerActivity.java b/java/com/android/dialer/callcomposer/CallComposerActivity.java
index eef3d210d..f73563ff8 100644
--- a/java/com/android/dialer/callcomposer/CallComposerActivity.java
+++ b/java/com/android/dialer/callcomposer/CallComposerActivity.java
@@ -21,9 +21,9 @@ import android.animation.Animator.AnimatorListener;
import android.animation.AnimatorSet;
import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
-import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.content.Context;
import android.content.Intent;
+import android.content.res.Configuration;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
@@ -35,6 +35,7 @@ import android.support.v4.view.ViewPager.OnPageChangeListener;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
+import android.util.Base64;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLayoutChangeListener;
@@ -60,6 +61,7 @@ import com.android.dialer.common.LogUtil;
import com.android.dialer.common.UiUtil;
import com.android.dialer.compat.CompatUtils;
import com.android.dialer.constants.Constants;
+import com.android.dialer.enrichedcall.EnrichedCallComponent;
import com.android.dialer.enrichedcall.EnrichedCallManager;
import com.android.dialer.enrichedcall.EnrichedCallManager.State;
import com.android.dialer.enrichedcall.Session;
@@ -70,6 +72,7 @@ import com.android.dialer.multimedia.MultimediaData;
import com.android.dialer.protos.ProtoParsers;
import com.android.dialer.telecom.TelecomUtil;
import com.android.dialer.util.ViewUtil;
+import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
import java.io.File;
/**
@@ -88,18 +91,18 @@ public class CallComposerActivity extends AppCompatActivity
OnPageChangeListener,
CallComposerListener,
OnLayoutChangeListener,
- AnimatorListener,
EnrichedCallManager.StateChangedListener {
- private static final int VIEW_PAGER_ANIMATION_DURATION_MILLIS = 300;
+ public static final String KEY_CONTACT_NAME = "contact_name";
+
private static final int ENTRANCE_ANIMATION_DURATION_MILLIS = 500;
+ private static final int EXIT_ANIMATION_DURATION_MILLIS = 500;
private static final String ARG_CALL_COMPOSER_CONTACT = "CALL_COMPOSER_CONTACT";
private static final String ENTRANCE_ANIMATION_KEY = "entrance_animation_key";
private static final String CURRENT_INDEX_KEY = "current_index_key";
private static final String VIEW_PAGER_STATE_KEY = "view_pager_state_key";
- private static final String LOCATIONS_KEY = "locations_key";
private static final String SESSION_ID_KEY = "session_id_key";
private CallComposerContact contact;
@@ -111,6 +114,7 @@ public class CallComposerActivity extends AppCompatActivity
private RelativeLayout contactContainer;
private Toolbar toolbar;
private View sendAndCall;
+ private TextView sendAndCallText;
private ImageView cameraIcon;
private ImageView galleryIcon;
@@ -125,13 +129,8 @@ public class CallComposerActivity extends AppCompatActivity
private boolean shouldAnimateEntrance = true;
private boolean inFullscreenMode;
private boolean isSendAndCallHidingOrHidden = true;
- private boolean isAnimatingContactBar;
private boolean layoutChanged;
private int currentIndex;
- private int[] locations;
- private int currentLocation;
-
- @NonNull private EnrichedCallManager enrichedCallManager;
public static Intent newIntent(Context context, CallComposerContact contact) {
Intent intent = new Intent(context, CallComposerActivity.class);
@@ -156,6 +155,7 @@ public class CallComposerActivity extends AppCompatActivity
windowContainer = (LinearLayout) findViewById(R.id.call_composer_container);
toolbar = (Toolbar) findViewById(R.id.toolbar);
sendAndCall = findViewById(R.id.send_and_call_button);
+ sendAndCallText = (TextView) findViewById(R.id.send_and_call_text);
interpolator = new FastOutSlowInInterpolator();
adapter =
@@ -166,14 +166,8 @@ public class CallComposerActivity extends AppCompatActivity
pager.addOnPageChangeListener(this);
setActionBar(toolbar);
- toolbar.setNavigationIcon(R.drawable.quantum_ic_arrow_back_white_24);
- toolbar.setNavigationOnClickListener(
- new OnClickListener() {
- @Override
- public void onClick(View v) {
- finish();
- }
- });
+ toolbar.setNavigationIcon(R.drawable.quantum_ic_close_white_24);
+ toolbar.setNavigationOnClickListener(v -> finish());
background.addOnLayoutChangeListener(this);
cameraIcon.setOnClickListener(this);
@@ -183,20 +177,12 @@ public class CallComposerActivity extends AppCompatActivity
onHandleIntent(getIntent());
- enrichedCallManager = EnrichedCallManager.Accessor.getInstance(getApplication());
if (savedInstanceState != null) {
shouldAnimateEntrance = savedInstanceState.getBoolean(ENTRANCE_ANIMATION_KEY);
- locations = savedInstanceState.getIntArray(LOCATIONS_KEY);
pager.onRestoreInstanceState(savedInstanceState.getParcelable(VIEW_PAGER_STATE_KEY));
currentIndex = savedInstanceState.getInt(CURRENT_INDEX_KEY);
- sessionId = savedInstanceState.getLong(SESSION_ID_KEY);
+ sessionId = savedInstanceState.getLong(SESSION_ID_KEY, Session.NO_SESSION_ID);
onPageSelected(currentIndex);
- } else {
- locations = new int[adapter.getCount()];
- for (int i = 0; i < locations.length; i++) {
- locations[i] = CallComposerFragment.CONTENT_TOP_UNSET;
- }
- sessionId = enrichedCallManager.startCallComposerSession(contact.number);
}
// Since we can't animate the views until they are ready to be drawn, we use this listener to
@@ -204,37 +190,39 @@ public class CallComposerActivity extends AppCompatActivity
ViewUtil.doOnPreDraw(
windowContainer,
false,
- new Runnable() {
- @Override
- public void run() {
- runEntranceAnimation();
- }
+ () -> {
+ showFullscreen(inFullscreenMode);
+ runEntranceAnimation();
});
setMediaIconSelected(0);
-
- // This activity is started using startActivityForResult. By default, mark this as succeeded
- // and flip this to RESULT_CANCELED if something goes wrong.
- setResult(RESULT_OK);
-
- if (sessionId == Session.NO_SESSION_ID) {
- LogUtil.w("CallComposerActivity.onCreate", "failed to create call composer session");
- setResult(RESULT_CANCELED);
- finish();
- }
}
@Override
protected void onResume() {
super.onResume();
- enrichedCallManager.registerStateChangedListener(this);
+ getEnrichedCallManager().registerStateChangedListener(this);
+ if (sessionId == Session.NO_SESSION_ID) {
+ LogUtil.i("CallComposerActivity.onResume", "creating new session");
+ sessionId = getEnrichedCallManager().startCallComposerSession(contact.number);
+ } else if (getEnrichedCallManager().getSession(sessionId) == null) {
+ LogUtil.i(
+ "CallComposerActivity.onResume", "session closed while activity paused, creating new");
+ sessionId = getEnrichedCallManager().startCallComposerSession(contact.number);
+ } else {
+ LogUtil.i("CallComposerActivity.onResume", "session still open, using old");
+ }
+ if (sessionId == Session.NO_SESSION_ID) {
+ LogUtil.w("CallComposerActivity.onResume", "failed to create call composer session");
+ setFailedResultAndFinish();
+ }
refreshUiForCallComposerState();
}
@Override
protected void onPause() {
super.onPause();
- enrichedCallManager.unregisterStateChangedListener(this);
+ getEnrichedCallManager().unregisterStateChangedListener(this);
}
@Override
@@ -243,7 +231,7 @@ public class CallComposerActivity extends AppCompatActivity
}
private void refreshUiForCallComposerState() {
- Session session = enrichedCallManager.getSession(sessionId);
+ Session session = getEnrichedCallManager().getSession(sessionId);
if (session == null) {
return;
}
@@ -256,8 +244,7 @@ public class CallComposerActivity extends AppCompatActivity
if (state == EnrichedCallManager.STATE_START_FAILED
|| state == EnrichedCallManager.STATE_CLOSED) {
- setResult(RESULT_CANCELED);
- finish();
+ setFailedResultAndFinish();
}
}
@@ -293,7 +280,7 @@ public class CallComposerActivity extends AppCompatActivity
if (fragment instanceof MessageComposerFragment) {
MessageComposerFragment messageComposerFragment = (MessageComposerFragment) fragment;
- builder.setSubject(messageComposerFragment.getMessage());
+ builder.setText(messageComposerFragment.getMessage());
placeRCSCall(builder);
}
if (fragment instanceof GalleryComposerFragment) {
@@ -351,7 +338,7 @@ public class CallComposerActivity extends AppCompatActivity
}
private boolean sessionReady() {
- Session session = enrichedCallManager.getSession(sessionId);
+ Session session = getEnrichedCallManager().getSession(sessionId);
if (session == null) {
return false;
}
@@ -362,9 +349,10 @@ public class CallComposerActivity extends AppCompatActivity
private void placeRCSCall(MultimediaData.Builder builder) {
LogUtil.i("CallComposerActivity.placeRCSCall", "placing enriched call");
Logger.get(this).logImpression(DialerImpression.Type.CALL_COMPOSER_ACTIVITY_PLACE_RCS_CALL);
- enrichedCallManager.sendCallComposerData(sessionId, builder.build());
+ getEnrichedCallManager().sendCallComposerData(sessionId, builder.build());
TelecomUtil.placeCall(
this, new CallIntentBuilder(contact.number, CallInitiationType.Type.CALL_COMPOSER).build());
+ setResult(RESULT_OK);
finish();
}
@@ -379,24 +367,26 @@ public class CallComposerActivity extends AppCompatActivity
/** Animates {@code contactContainer} to align with content inside viewpager. */
@Override
public void onPageSelected(int position) {
+ if (position == CallComposerPagerAdapter.INDEX_MESSAGE) {
+ sendAndCallText.setText(R.string.send_and_call);
+ } else {
+ sendAndCallText.setText(R.string.share_and_call);
+ }
if (currentIndex == CallComposerPagerAdapter.INDEX_MESSAGE) {
UiUtil.hideKeyboardFrom(this, windowContainer);
- } else if (position == CallComposerPagerAdapter.INDEX_MESSAGE && inFullscreenMode) {
+ } else if (position == CallComposerPagerAdapter.INDEX_MESSAGE
+ && inFullscreenMode
+ && !isLandscapeLayout()) {
UiUtil.openKeyboardFrom(this, windowContainer);
}
currentIndex = position;
CallComposerFragment fragment = (CallComposerFragment) adapter.instantiateItem(pager, position);
- locations[currentIndex] = fragment.getContentTopPx();
- animateContactContainer(locations[currentIndex]);
animateSendAndCall(fragment.shouldHide());
setMediaIconSelected(position);
}
@Override
- public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
- CallComposerFragment fragment = (CallComposerFragment) adapter.instantiateItem(pager, position);
- animateContactContainer(fragment.getContentTopPx());
- }
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
@Override
public void onPageScrollStateChanged(int state) {}
@@ -407,13 +397,19 @@ public class CallComposerActivity extends AppCompatActivity
outState.putParcelable(VIEW_PAGER_STATE_KEY, pager.onSaveInstanceState());
outState.putBoolean(ENTRANCE_ANIMATION_KEY, shouldAnimateEntrance);
outState.putInt(CURRENT_INDEX_KEY, currentIndex);
- outState.putIntArray(LOCATIONS_KEY, locations);
outState.putLong(SESSION_ID_KEY, sessionId);
}
@Override
public void onBackPressed() {
- runExitAnimation();
+ if (!isSendAndCallHidingOrHidden) {
+ ((CallComposerFragment) adapter.instantiateItem(pager, currentIndex)).clearComposer();
+ } else {
+ // Unregister first to avoid receiving a callback when the session closes
+ getEnrichedCallManager().unregisterStateChangedListener(this);
+ getEnrichedCallManager().endCallComposerSession(sessionId);
+ runExitAnimation();
+ }
}
@Override
@@ -445,29 +441,9 @@ public class CallComposerActivity extends AppCompatActivity
}
layoutChanged = true;
- if (pager.getTop() < 0 || inFullscreenMode) {
- ViewGroup.LayoutParams layoutParams = pager.getLayoutParams();
- layoutParams.height = background.getHeight() - toolbar.getHeight() - messageIcon.getHeight();
- pager.setLayoutParams(layoutParams);
- }
- }
-
- @Override
- public void onAnimationStart(Animator animation) {
- isAnimatingContactBar = true;
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- isAnimatingContactBar = false;
+ showFullscreen(contactContainer.getTop() < 0 || inFullscreenMode);
}
- @Override
- public void onAnimationCancel(Animator animation) {}
-
- @Override
- public void onAnimationRepeat(Animator animation) {}
-
/**
* Reads arguments from the fragment arguments and populates the necessary instance variables.
* Copied from {@link com.android.contacts.common.dialog.CallSubjectDialog}.
@@ -477,12 +453,26 @@ public class CallComposerActivity extends AppCompatActivity
if (arguments == null) {
throw new RuntimeException("CallComposerActivity.onHandleIntent, Arguments cannot be null.");
}
- contact =
- ProtoParsers.getFromInstanceState(
- arguments, ARG_CALL_COMPOSER_CONTACT, new CallComposerContact());
+ if (arguments.get(ARG_CALL_COMPOSER_CONTACT) instanceof String) {
+ byte[] bytes = Base64.decode(arguments.getString(ARG_CALL_COMPOSER_CONTACT), Base64.DEFAULT);
+ try {
+ contact = CallComposerContact.parseFrom(bytes);
+ } catch (InvalidProtocolBufferNanoException e) {
+ Assert.fail(e.toString());
+ }
+ } else {
+ contact =
+ ProtoParsers.getFromInstanceState(
+ arguments, ARG_CALL_COMPOSER_CONTACT, new CallComposerContact());
+ }
updateContactInfo();
}
+ @Override
+ public boolean isLandscapeLayout() {
+ return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
+ }
+
/**
* Populates the contact info fields based on the current contact information. Copied from {@link
* com.android.contacts.common.dialog.CallSubjectDialog}.
@@ -552,25 +542,6 @@ public class CallComposerActivity extends AppCompatActivity
}
}
- private void animateContactContainer(int toY) {
- if (toY == CallComposerFragment.CONTENT_TOP_UNSET
- || toY == currentLocation
- || (toY != locations[currentIndex]
- && locations[currentIndex] != CallComposerFragment.CONTENT_TOP_UNSET)
- || isAnimatingContactBar
- || inFullscreenMode) {
- return;
- }
- currentLocation = toY;
- contactContainer
- .animate()
- .translationY(toY)
- .setInterpolator(interpolator)
- .setDuration(VIEW_PAGER_ANIMATION_DURATION_MILLIS)
- .setListener(this)
- .start();
- }
-
/** Animates compose UI into view */
private void runEntranceAnimation() {
if (!shouldAnimateEntrance) {
@@ -578,84 +549,90 @@ public class CallComposerActivity extends AppCompatActivity
}
shouldAnimateEntrance = false;
- int colorFrom = ContextCompat.getColor(this, android.R.color.transparent);
- int colorTo = ContextCompat.getColor(this, R.color.call_composer_background_color);
- ValueAnimator backgroundAnimation =
- ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo);
- backgroundAnimation.setInterpolator(interpolator);
- backgroundAnimation.setDuration(ENTRANCE_ANIMATION_DURATION_MILLIS); // milliseconds
- backgroundAnimation.addUpdateListener(
- new AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator animator) {
- background.setBackgroundColor((int) animator.getAnimatedValue());
- }
- });
-
- ValueAnimator contentAnimation = ValueAnimator.ofFloat(windowContainer.getHeight(), 0);
+ int value = isLandscapeLayout() ? windowContainer.getWidth() : windowContainer.getHeight();
+ ValueAnimator contentAnimation = ValueAnimator.ofFloat(value, 0);
contentAnimation.setInterpolator(interpolator);
contentAnimation.setDuration(ENTRANCE_ANIMATION_DURATION_MILLIS);
contentAnimation.addUpdateListener(
- new AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator animation) {
+ animation -> {
+ if (isLandscapeLayout()) {
+ windowContainer.setX((Float) animation.getAnimatedValue());
+ } else {
windowContainer.setY((Float) animation.getAnimatedValue());
}
});
- AnimatorSet set = new AnimatorSet();
- set.play(contentAnimation).with(backgroundAnimation);
- set.start();
+ if (!isLandscapeLayout()) {
+ int colorFrom = ContextCompat.getColor(this, android.R.color.transparent);
+ int colorTo = ContextCompat.getColor(this, R.color.call_composer_background_color);
+ ValueAnimator backgroundAnimation =
+ ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo);
+ backgroundAnimation.setInterpolator(interpolator);
+ backgroundAnimation.setDuration(ENTRANCE_ANIMATION_DURATION_MILLIS); // milliseconds
+ backgroundAnimation.addUpdateListener(
+ animator -> background.setBackgroundColor((int) animator.getAnimatedValue()));
+
+ AnimatorSet set = new AnimatorSet();
+ set.play(contentAnimation).with(backgroundAnimation);
+ set.start();
+ } else {
+ contentAnimation.start();
+ }
}
/** Animates compose UI out of view and ends the activity. */
private void runExitAnimation() {
- int colorTo = ContextCompat.getColor(this, android.R.color.transparent);
- int colorFrom = ContextCompat.getColor(this, R.color.call_composer_background_color);
- ValueAnimator backgroundAnimation =
- ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo);
- backgroundAnimation.setInterpolator(interpolator);
- backgroundAnimation.setDuration(ENTRANCE_ANIMATION_DURATION_MILLIS); // milliseconds
- backgroundAnimation.addUpdateListener(
- new AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator animator) {
- background.setBackgroundColor((int) animator.getAnimatedValue());
- }
- });
-
- ValueAnimator contentAnimation = ValueAnimator.ofFloat(0, windowContainer.getHeight());
+ int value = isLandscapeLayout() ? windowContainer.getWidth() : windowContainer.getHeight();
+ ValueAnimator contentAnimation = ValueAnimator.ofFloat(0, value);
contentAnimation.setInterpolator(interpolator);
- contentAnimation.setDuration(ENTRANCE_ANIMATION_DURATION_MILLIS);
+ contentAnimation.setDuration(EXIT_ANIMATION_DURATION_MILLIS);
contentAnimation.addUpdateListener(
- new AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator animation) {
+ animation -> {
+ if (isLandscapeLayout()) {
+ windowContainer.setX((Float) animation.getAnimatedValue());
+ } else {
windowContainer.setY((Float) animation.getAnimatedValue());
- if (animation.getAnimatedFraction() > .75) {
- finish();
- }
+ }
+ if (animation.getAnimatedFraction() > .95) {
+ finish();
}
});
- AnimatorSet set = new AnimatorSet();
- set.play(contentAnimation).with(backgroundAnimation);
- set.start();
+
+ if (!isLandscapeLayout()) {
+ int colorTo = ContextCompat.getColor(this, android.R.color.transparent);
+ int colorFrom = ContextCompat.getColor(this, R.color.call_composer_background_color);
+ ValueAnimator backgroundAnimation =
+ ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo);
+ backgroundAnimation.setInterpolator(interpolator);
+ backgroundAnimation.setDuration(EXIT_ANIMATION_DURATION_MILLIS);
+ backgroundAnimation.addUpdateListener(
+ animator -> background.setBackgroundColor((int) animator.getAnimatedValue()));
+
+ AnimatorSet set = new AnimatorSet();
+ set.play(contentAnimation).with(backgroundAnimation);
+ set.start();
+ } else {
+ contentAnimation.start();
+ }
}
@Override
- public void showFullscreen(boolean show) {
- if (inFullscreenMode == show) {
- return;
- }
- inFullscreenMode = show;
- toolbar.setVisibility(show ? View.VISIBLE : View.INVISIBLE);
- contactContainer.setVisibility(show ? View.GONE : View.VISIBLE);
+ public void showFullscreen(boolean fullscreen) {
+ inFullscreenMode = fullscreen;
ViewGroup.LayoutParams layoutParams = pager.getLayoutParams();
- if (show) {
+ if (isLandscapeLayout()) {
+ layoutParams.height = background.getHeight() - messageIcon.getHeight();
+ toolbar.setVisibility(View.INVISIBLE);
+ contactContainer.setVisibility(View.GONE);
+ } else if (fullscreen || getResources().getBoolean(R.bool.show_toolbar)) {
layoutParams.height = background.getHeight() - toolbar.getHeight() - messageIcon.getHeight();
+ toolbar.setVisibility(View.VISIBLE);
+ contactContainer.setVisibility(View.GONE);
} else {
layoutParams.height =
getResources().getDimensionPixelSize(R.dimen.call_composer_view_pager_height);
+ toolbar.setVisibility(View.INVISIBLE);
+ contactContainer.setVisibility(View.VISIBLE);
}
pager.setLayoutParams(layoutParams);
}
@@ -686,35 +663,32 @@ public class CallComposerActivity extends AppCompatActivity
ViewUtil.doOnPreDraw(
sendAndCall,
true,
- new Runnable() {
- @Override
- public void run() {
- Animator animator =
- ViewAnimationUtils.createCircularReveal(
- sendAndCall, centerX, centerY, startRadius, endRadius);
- animator.addListener(
- new AnimatorListener() {
- @Override
- public void onAnimationStart(Animator animation) {
- isSendAndCallHidingOrHidden = shouldHide;
- sendAndCall.setVisibility(View.VISIBLE);
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- if (isSendAndCallHidingOrHidden) {
- sendAndCall.setVisibility(View.INVISIBLE);
- }
+ () -> {
+ Animator animator =
+ ViewAnimationUtils.createCircularReveal(
+ sendAndCall, centerX, centerY, startRadius, endRadius);
+ animator.addListener(
+ new AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ isSendAndCallHidingOrHidden = shouldHide;
+ sendAndCall.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (isSendAndCallHidingOrHidden) {
+ sendAndCall.setVisibility(View.INVISIBLE);
}
+ }
- @Override
- public void onAnimationCancel(Animator animation) {}
+ @Override
+ public void onAnimationCancel(Animator animation) {}
- @Override
- public void onAnimationRepeat(Animator animation) {}
- });
- animator.start();
- }
+ @Override
+ public void onAnimationRepeat(Animator animation) {}
+ });
+ animator.start();
});
}
}
@@ -725,4 +699,14 @@ public class CallComposerActivity extends AppCompatActivity
galleryIcon.setAlpha(position == CallComposerPagerAdapter.INDEX_GALLERY ? 1 : alpha);
messageIcon.setAlpha(position == CallComposerPagerAdapter.INDEX_MESSAGE ? 1 : alpha);
}
+
+ private void setFailedResultAndFinish() {
+ setResult(RESULT_FIRST_USER, new Intent().putExtra(KEY_CONTACT_NAME, contact.nameOrNumber));
+ finish();
+ }
+
+ @NonNull
+ private EnrichedCallManager getEnrichedCallManager() {
+ return EnrichedCallComponent.get(this).getEnrichedCallManager();
+ }
}
diff --git a/java/com/android/dialer/callcomposer/CallComposerFragment.java b/java/com/android/dialer/callcomposer/CallComposerFragment.java
index d6f944955..ee1eb462a 100644
--- a/java/com/android/dialer/callcomposer/CallComposerFragment.java
+++ b/java/com/android/dialer/callcomposer/CallComposerFragment.java
@@ -17,13 +17,8 @@
package com.android.dialer.callcomposer;
import android.content.Context;
-import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import com.android.dialer.common.Assert;
import com.android.dialer.common.FragmentUtils;
import com.android.dialer.common.LogUtil;
@@ -34,26 +29,10 @@ public abstract class CallComposerFragment extends Fragment {
protected static final int CAMERA_PERMISSION = 1;
protected static final int STORAGE_PERMISSION = 2;
- private static final String LOCATION_KEY = "location_key";
- public static final int CONTENT_TOP_UNSET = Integer.MAX_VALUE;
-
- private View topView;
- private int contentTopPx = CONTENT_TOP_UNSET;
- private CallComposerListener testListener;
-
- @Nullable
- @Override
- public View onCreateView(
- LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
- View view = super.onCreateView(layoutInflater, viewGroup, bundle);
- Assert.isNotNull(topView);
- return view;
- }
-
@Override
public void onAttach(Context context) {
super.onAttach(context);
- if (!(context instanceof CallComposerListener) && testListener == null) {
+ if (FragmentUtils.getParent(this, CallComposerListener.class) == null) {
LogUtil.e(
"CallComposerFragment.onAttach",
"Container activity must implement CallComposerListener.");
@@ -61,56 +40,15 @@ public abstract class CallComposerFragment extends Fragment {
}
}
- /** Call this method to declare which view is located at the top of the fragment's layout. */
- public void setTopView(View view) {
- topView = view;
- // For each fragment that extends CallComposerFragment, the heights may vary and since
- // ViewPagers cannot have their height set to wrap_content, we have to adjust the top of our
- // container to match the top of the fragment. This listener populates {@code contentTopPx} as
- // it's available.
- topView
- .getViewTreeObserver()
- .addOnGlobalLayoutListener(
- new OnGlobalLayoutListener() {
- @Override
- public void onGlobalLayout() {
- topView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
- contentTopPx = topView.getTop();
- }
- });
- }
-
- public int getContentTopPx() {
- return contentTopPx;
- }
-
- public void setParentForTesting(CallComposerListener listener) {
- testListener = listener;
- }
-
+ @Nullable
public CallComposerListener getListener() {
- if (testListener != null) {
- return testListener;
- }
- return FragmentUtils.getParentUnsafe(this, CallComposerListener.class);
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putInt(LOCATION_KEY, contentTopPx);
- }
-
- @Override
- public void onViewStateRestored(Bundle savedInstanceState) {
- super.onViewStateRestored(savedInstanceState);
- if (savedInstanceState != null) {
- contentTopPx = savedInstanceState.getInt(LOCATION_KEY);
- }
+ return FragmentUtils.getParent(this, CallComposerListener.class);
}
public abstract boolean shouldHide();
+ public abstract void clearComposer();
+
/** Interface used to listen to CallComposeFragments */
public interface CallComposerListener {
/** Let the listener know when a call is ready to be composed. */
@@ -121,5 +59,8 @@ public abstract class CallComposerFragment extends Fragment {
/** True is the listener is in fullscreen. */
boolean isFullscreen();
+
+ /** True if the layout is in landscape mode. */
+ boolean isLandscapeLayout();
}
}
diff --git a/java/com/android/dialer/callcomposer/CallComposerPagerAdapter.java b/java/com/android/dialer/callcomposer/CallComposerPagerAdapter.java
index 4d4058a0a..edf980ee9 100644
--- a/java/com/android/dialer/callcomposer/CallComposerPagerAdapter.java
+++ b/java/com/android/dialer/callcomposer/CallComposerPagerAdapter.java
@@ -18,11 +18,11 @@ package com.android.dialer.callcomposer;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
-import android.support.v4.app.FragmentStatePagerAdapter;
+import android.support.v4.app.FragmentPagerAdapter;
import com.android.dialer.common.Assert;
/** ViewPager adapter for call compose UI. */
-public class CallComposerPagerAdapter extends FragmentStatePagerAdapter {
+public class CallComposerPagerAdapter extends FragmentPagerAdapter {
public static final int INDEX_CAMERA = 0;
public static final int INDEX_GALLERY = 1;
diff --git a/java/com/android/dialer/callcomposer/CameraComposerFragment.java b/java/com/android/dialer/callcomposer/CameraComposerFragment.java
index f2d0a94a7..583fb5446 100644
--- a/java/com/android/dialer/callcomposer/CameraComposerFragment.java
+++ b/java/com/android/dialer/callcomposer/CameraComposerFragment.java
@@ -56,6 +56,9 @@ import com.android.dialer.util.PermissionsUtil;
public class CameraComposerFragment extends CallComposerFragment
implements CameraManagerListener, OnClickListener, CameraManager.MediaCallback {
+ private static final String CAMERA_DIRECTION_KEY = "camera_direction";
+ private static final String CAMERA_URI_KEY = "camera_key";
+
private View permissionView;
private ImageButton exitFullscreen;
private ImageButton fullscreen;
@@ -68,11 +71,13 @@ public class CameraComposerFragment extends CallComposerFragment
private View allowPermission;
private CameraPreviewHost preview;
private ProgressBar loading;
+ private ImageView previewImageView;
private Uri cameraUri;
private boolean processingUri;
private String[] permissions = new String[] {Manifest.permission.CAMERA};
private CameraUriCallback uriCallback;
+ private int cameraDirection = CameraInfo.CAMERA_FACING_BACK;
public static CameraComposerFragment newInstance() {
return new CameraComposerFragment();
@@ -94,6 +99,7 @@ public class CameraComposerFragment extends CallComposerFragment
cancel = (ImageButton) cameraView.findViewById(R.id.camera_cancel_button);
focus = (RenderOverlay) cameraView.findViewById(R.id.focus_visual);
preview = (CameraPreviewHost) cameraView.findViewById(R.id.camera_preview);
+ previewImageView = (ImageView) root.findViewById(R.id.preview_image_view);
exitFullscreen.setOnClickListener(this);
fullscreen.setOnClickListener(this);
@@ -115,10 +121,12 @@ public class CameraComposerFragment extends CallComposerFragment
ContextCompat.getColor(getContext(), R.color.dialer_theme_color));
permissionView.setVisibility(View.VISIBLE);
} else {
+ if (bundle != null) {
+ cameraDirection = bundle.getInt(CAMERA_DIRECTION_KEY);
+ cameraUri = bundle.getParcelable(CAMERA_URI_KEY);
+ }
setupCamera();
}
-
- setTopView(cameraView);
return root;
}
@@ -126,8 +134,8 @@ public class CameraComposerFragment extends CallComposerFragment
CameraManager.get().setListener(this);
preview.setShown();
CameraManager.get().setRenderOverlay(focus);
- CameraManager.get().selectCamera(CameraInfo.CAMERA_FACING_BACK);
- setCameraUri(null);
+ CameraManager.get().selectCamera(cameraDirection);
+ setCameraUri(cameraUri);
}
@Override
@@ -146,10 +154,16 @@ public class CameraComposerFragment extends CallComposerFragment
}
@Override
+ public void clearComposer() {
+ processingUri = false;
+ setCameraUri(null);
+ }
+
+ @Override
public void onClick(View view) {
if (view == capture) {
float heightPercent = 1;
- if (!getListener().isFullscreen()) {
+ if (!getListener().isFullscreen() && !getListener().isLandscapeLayout()) {
heightPercent = Math.min((float) cameraView.getHeight() / preview.getView().getHeight(), 1);
}
@@ -162,8 +176,7 @@ public class CameraComposerFragment extends CallComposerFragment
((Animatable) swapCamera.getDrawable()).start();
CameraManager.get().swapCamera();
} else if (view == cancel) {
- processingUri = false;
- setCameraUri(null);
+ clearComposer();
} else if (view == exitFullscreen) {
getListener().showFullscreen(false);
fullscreen.setVisibility(View.VISIBLE);
@@ -314,6 +327,13 @@ public class CameraComposerFragment extends CallComposerFragment
boolean isCameraAvailable = CameraManager.get().isCameraAvailable();
boolean uriReadyOrProcessing = cameraUri != null || processingUri;
+ if (cameraUri != null) {
+ previewImageView.setImageURI(cameraUri);
+ previewImageView.setVisibility(View.VISIBLE);
+ } else {
+ previewImageView.setVisibility(View.GONE);
+ }
+
if (cameraUri == null && isCameraAvailable) {
CameraManager.get().resetPreview();
cancel.setVisibility(View.GONE);
@@ -328,7 +348,7 @@ public class CameraComposerFragment extends CallComposerFragment
capture.setVisibility(uriReadyOrProcessing ? View.GONE : View.VISIBLE);
cancel.setVisibility(uriReadyOrProcessing ? View.VISIBLE : View.GONE);
- if (uriReadyOrProcessing) {
+ if (uriReadyOrProcessing || getListener().isLandscapeLayout()) {
fullscreen.setVisibility(View.GONE);
exitFullscreen.setVisibility(View.GONE);
} else if (getListener().isFullscreen()) {
@@ -344,6 +364,13 @@ public class CameraComposerFragment extends CallComposerFragment
}
@Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt(CAMERA_DIRECTION_KEY, CameraManager.get().getCameraInfo().facing);
+ outState.putParcelable(CAMERA_URI_KEY, cameraUri);
+ }
+
+ @Override
public void onRequestPermissionsResult(
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (permissions.length > 0 && permissions[0].equals(this.permissions[0])) {
diff --git a/java/com/android/dialer/callcomposer/GalleryComposerFragment.java b/java/com/android/dialer/callcomposer/GalleryComposerFragment.java
index 623127945..b53d6a9d6 100644
--- a/java/com/android/dialer/callcomposer/GalleryComposerFragment.java
+++ b/java/com/android/dialer/callcomposer/GalleryComposerFragment.java
@@ -24,6 +24,7 @@ import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
+import android.os.Parcelable;
import android.provider.Settings;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@@ -45,11 +46,17 @@ import com.android.dialer.logging.Logger;
import com.android.dialer.logging.nano.DialerImpression;
import com.android.dialer.util.PermissionsUtil;
import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
/** Fragment used to compose call with image from the user's gallery. */
public class GalleryComposerFragment extends CallComposerFragment
implements LoaderCallbacks<Cursor>, OnClickListener {
+ private static final String SELECTED_DATA_KEY = "selected_data";
+ private static final String IS_COPY_KEY = "is_copy";
+ private static final String INSERTED_IMAGES_KEY = "inserted_images";
+
private static final int RESULT_LOAD_IMAGE = 1;
private static final int RESULT_OPEN_SETTINGS = 2;
@@ -62,6 +69,7 @@ public class GalleryComposerFragment extends CallComposerFragment
private CursorLoader cursorLoader;
private GalleryGridItemData selectedData = null;
private boolean selectedDataIsCopy;
+ private List<GalleryGridItemData> insertedImages = new ArrayList<>();
public static GalleryComposerFragment newInstance() {
return new GalleryComposerFragment();
@@ -89,10 +97,13 @@ public class GalleryComposerFragment extends CallComposerFragment
ContextCompat.getColor(getContext(), R.color.dialer_theme_color));
permissionView.setVisibility(View.VISIBLE);
} else {
+ if (bundle != null) {
+ selectedData = bundle.getParcelable(SELECTED_DATA_KEY);
+ selectedDataIsCopy = bundle.getBoolean(IS_COPY_KEY);
+ insertedImages = bundle.getParcelableArrayList(INSERTED_IMAGES_KEY);
+ }
setupGallery();
}
-
- setTopView(galleryGridView);
return view;
}
@@ -110,6 +121,10 @@ public class GalleryComposerFragment extends CallComposerFragment
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
adapter.swapCursor(cursor);
+ if (insertedImages != null && !insertedImages.isEmpty()) {
+ adapter.insertEntries(insertedImages);
+ }
+ setSelected(selectedData, selectedDataIsCopy);
}
@Override
@@ -147,7 +162,7 @@ public class GalleryComposerFragment extends CallComposerFragment
intent.addCategory(Intent.CATEGORY_OPENABLE);
startActivityForResult(intent, RESULT_LOAD_IMAGE);
} else if (itemView.getData().equals(selectedData)) {
- setSelected(null, false);
+ clearComposer();
} else {
setSelected(new GalleryGridItemData(itemView.getData()), false);
}
@@ -179,7 +194,10 @@ public class GalleryComposerFragment extends CallComposerFragment
selectedData = data;
selectedDataIsCopy = isCopy;
adapter.setSelected(selectedData);
- getListener().composeCall(this);
+ CallComposerListener listener = getListener();
+ if (listener != null) {
+ getListener().composeCall(this);
+ }
}
@Override
@@ -190,6 +208,20 @@ public class GalleryComposerFragment extends CallComposerFragment
}
@Override
+ public void clearComposer() {
+ setSelected(null, false);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putParcelable(SELECTED_DATA_KEY, selectedData);
+ outState.putBoolean(IS_COPY_KEY, selectedDataIsCopy);
+ outState.putParcelableArrayList(
+ INSERTED_IMAGES_KEY, (ArrayList<? extends Parcelable>) insertedImages);
+ }
+
+ @Override
public void onRequestPermissionsResult(
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (permissions.length > 0 && permissions[0].equals(this.permissions[0])) {
@@ -238,7 +270,9 @@ public class GalleryComposerFragment extends CallComposerFragment
new Callback() {
@Override
public void onCopySuccessful(File file, String mimeType) {
- setSelected(adapter.insertEntry(file.getAbsolutePath(), mimeType), true);
+ GalleryGridItemData data = adapter.insertEntry(file.getAbsolutePath(), mimeType);
+ insertedImages.add(0, data);
+ setSelected(data, true);
}
@Override
diff --git a/java/com/android/dialer/callcomposer/GalleryGridAdapter.java b/java/com/android/dialer/callcomposer/GalleryGridAdapter.java
index 0a7fd769b..84257b2af 100644
--- a/java/com/android/dialer/callcomposer/GalleryGridAdapter.java
+++ b/java/com/android/dialer/callcomposer/GalleryGridAdapter.java
@@ -104,6 +104,18 @@ public class GalleryGridAdapter extends CursorAdapter {
}
}
+ public void insertEntries(@NonNull List<GalleryGridItemData> entries) {
+ Assert.checkArgument(entries.size() != 0);
+ LogUtil.i("GalleryGridAdapter.insertRows", "inserting %d rows", entries.size());
+ MatrixCursor extraRow = new MatrixCursor(GalleryGridItemData.IMAGE_PROJECTION);
+ for (GalleryGridItemData entry : entries) {
+ extraRow.addRow(new Object[] {0L, entry.getFilePath(), entry.getMimeType(), ""});
+ }
+ extraRow.moveToFirst();
+ Cursor extendedCursor = new MergeCursor(new Cursor[] {extraRow, getCursor()});
+ swapCursor(extendedCursor);
+ }
+
public GalleryGridItemData insertEntry(String filePath, String mimeType) {
LogUtil.i("GalleryGridAdapter.insertRow", mimeType + " " + filePath);
diff --git a/java/com/android/dialer/callcomposer/GalleryGridItemData.java b/java/com/android/dialer/callcomposer/GalleryGridItemData.java
index 402c6ce6d..43db96dd5 100644
--- a/java/com/android/dialer/callcomposer/GalleryGridItemData.java
+++ b/java/com/android/dialer/callcomposer/GalleryGridItemData.java
@@ -18,6 +18,8 @@ package com.android.dialer.callcomposer;
import android.database.Cursor;
import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
import android.provider.MediaStore.Images.Media;
import android.support.annotation.Nullable;
import android.text.TextUtils;
@@ -26,7 +28,7 @@ import java.io.File;
import java.util.Objects;
/** Provides data for GalleryGridItemView */
-public final class GalleryGridItemData {
+public final class GalleryGridItemData implements Parcelable {
public static final String[] IMAGE_PROJECTION =
new String[] {Media._ID, Media.DATA, Media.MIME_TYPE, Media.DATE_MODIFIED};
@@ -88,4 +90,35 @@ public final class GalleryGridItemData {
public int hashCode() {
return Objects.hash(filePath, mimeType, dateModifiedSeconds);
}
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(filePath);
+ dest.writeString(mimeType);
+ dest.writeLong(dateModifiedSeconds);
+ }
+
+ public static final Creator<GalleryGridItemData> CREATOR =
+ new Creator<GalleryGridItemData>() {
+ @Override
+ public GalleryGridItemData createFromParcel(Parcel in) {
+ return new GalleryGridItemData(in);
+ }
+
+ @Override
+ public GalleryGridItemData[] newArray(int size) {
+ return new GalleryGridItemData[size];
+ }
+ };
+
+ private GalleryGridItemData(Parcel in) {
+ filePath = in.readString();
+ mimeType = in.readString();
+ dateModifiedSeconds = in.readLong();
+ }
}
diff --git a/java/com/android/dialer/callcomposer/MessageComposerFragment.java b/java/com/android/dialer/callcomposer/MessageComposerFragment.java
index 521b71402..d8100033f 100644
--- a/java/com/android/dialer/callcomposer/MessageComposerFragment.java
+++ b/java/com/android/dialer/callcomposer/MessageComposerFragment.java
@@ -77,6 +77,7 @@ public class MessageComposerFragment extends CallComposerFragment
customMessage.addTextChangedListener(
new TextWatcher() {
@Override
+
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
@Override
@@ -90,8 +91,6 @@ public class MessageComposerFragment extends CallComposerFragment
}
view.findViewById(R.id.message_chat).setOnClickListener(this);
view.findViewById(R.id.message_question).setOnClickListener(this);
-
- setTopView(urgent);
return view;
}
@@ -140,4 +139,9 @@ public class MessageComposerFragment extends CallComposerFragment
public boolean shouldHide() {
return TextUtils.isEmpty(getMessage());
}
+
+ @Override
+ public void clearComposer() {
+ customMessage.getText().clear();
+ }
}
diff --git a/java/com/android/dialer/callcomposer/camera/ImagePersistTask.java b/java/com/android/dialer/callcomposer/camera/ImagePersistTask.java
index 150009495..a23014bf0 100644
--- a/java/com/android/dialer/callcomposer/camera/ImagePersistTask.java
+++ b/java/com/android/dialer/callcomposer/camera/ImagePersistTask.java
@@ -74,7 +74,9 @@ public class ImagePersistTask extends FallibleAsyncTask<Void, Void, Uri> {
if (mHeightPercent != 1.0f) {
writeClippedBitmap(outputStream);
} else {
- outputStream.write(mBytes, 0, mBytes.length);
+ Bitmap bitmap = BitmapFactory.decodeByteArray(mBytes, 0, mBytes.length);
+ bitmap = CopyAndResizeImageTask.resizeForEnrichedCalling(bitmap);
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream);
}
}
diff --git a/java/com/android/dialer/callcomposer/cameraui/res/layout/camera_view.xml b/java/com/android/dialer/callcomposer/cameraui/res/layout/camera_view.xml
index 75401b14b..a4198fcf9 100644
--- a/java/com/android/dialer/callcomposer/cameraui/res/layout/camera_view.xml
+++ b/java/com/android/dialer/callcomposer/cameraui/res/layout/camera_view.xml
@@ -46,6 +46,14 @@
android:background="@android:color/white"
android:visibility="gone" />
+ <ImageView
+ android:id="@+id/preview_image_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scaleType="centerCrop"
+ android:background="#000000"
+ android:visibility="gone"/>
+
<!-- Need a background on this view in order for the ripple effect to have a place to draw -->
<FrameLayout
android:id="@+id/camera_button_container"
diff --git a/java/com/android/dialer/callcomposer/res/layout/call_composer_activity.xml b/java/com/android/dialer/callcomposer/res/layout/call_composer_activity.xml
index 518b53ffd..f687f0b5c 100644
--- a/java/com/android/dialer/callcomposer/res/layout/call_composer_activity.xml
+++ b/java/com/android/dialer/callcomposer/res/layout/call_composer_activity.xml
@@ -120,13 +120,14 @@
android:visibility="invisible"
android:background="@color/compose_and_call_background">
<TextView
+ android:id="@+id/send_and_call_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:drawableStart="@drawable/quantum_ic_call_white_18"
android:drawablePadding="@dimen/send_and_call_drawable_padding"
android:textAllCaps="true"
- android:text="@string/send_and_call"
+ android:text="@string/share_and_call"
android:textSize="@dimen/send_and_call_text_size"
android:fontFamily="sans-serif-medium"
android:textColor="@color/background_dialer_white"/>
@@ -140,8 +141,8 @@
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:visibility="invisible"
- android:titleTextAppearance="@style/call_composer_toolbar_title_text"
- android:subtitleTextAppearance="@style/call_composer_toolbar_subtitle_text"
+ android:titleTextAppearance="@style/toolbar_title_text"
+ android:subtitleTextAppearance="@style/toolbar_subtitle_text"
android:navigationIcon="@drawable/quantum_ic_close_white_24"
android:background="@color/dialer_theme_color"/>
</FrameLayout> \ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/layout/fragment_gallery_composer.xml b/java/com/android/dialer/callcomposer/res/layout/fragment_gallery_composer.xml
index 58893ba50..a4bd4df03 100644
--- a/java/com/android/dialer/callcomposer/res/layout/fragment_gallery_composer.xml
+++ b/java/com/android/dialer/callcomposer/res/layout/fragment_gallery_composer.xml
@@ -27,7 +27,7 @@
android:paddingLeft="@dimen/gallery_item_padding"
android:paddingRight="@dimen/gallery_item_padding"
android:paddingTop="@dimen/gallery_item_padding"
- android:numColumns="3"/>
+ android:numColumns="@integer/gallery_composer_grid_view_rows"/>
<include
android:id="@+id/permission_view"
diff --git a/java/com/android/dialer/callcomposer/res/layout/fragment_message_composer.xml b/java/com/android/dialer/callcomposer/res/layout/fragment_message_composer.xml
index 97f232b3a..577887be9 100644
--- a/java/com/android/dialer/callcomposer/res/layout/fragment_message_composer.xml
+++ b/java/com/android/dialer/callcomposer/res/layout/fragment_message_composer.xml
@@ -15,43 +15,45 @@
~ limitations under the License
-->
<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="@dimen/call_composer_view_pager_height"
- android:orientation="vertical"
- android:gravity="bottom"
- android:background="@color/background_dialer_white">
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/call_composer_view_pager_height"
+ android:orientation="vertical"
+ android:gravity="bottom"
+ android:background="@color/background_dialer_white">
<TextView
- android:id="@+id/message_urgent"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@string/urgent"
- style="@style/message_composer_textview"/>
+ android:id="@+id/message_urgent"
+ android:layout_width="match_parent"
+ android:layout_height="56dp"
+ android:layout_marginTop="8dp"
+ android:text="@string/urgent"
+ style="@style/message_composer_textview"/>
<TextView
- android:id="@+id/message_chat"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@string/want_to_chat"
- style="@style/message_composer_textview"/>
+ android:id="@+id/message_chat"
+ android:layout_width="match_parent"
+ android:layout_height="56dp"
+ android:text="@string/want_to_chat"
+ style="@style/message_composer_textview"/>
<TextView
- android:id="@+id/message_question"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@string/quick_question"
- style="@style/message_composer_textview"/>
+ android:id="@+id/message_question"
+ android:layout_width="match_parent"
+ android:layout_height="56dp"
+ android:layout_marginBottom="8dp"
+ android:text="@string/quick_question"
+ style="@style/message_composer_textview"/>
<View
- android:layout_width="match_parent"
- android:layout_height="@dimen/message_composer_divider_height"
- android:background="@color/call_composer_divider"/>
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/message_composer_divider_height"
+ android:background="@color/call_composer_divider"/>
<RelativeLayout
- android:orientation="horizontal"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content">
+ android:orientation="horizontal"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
<EditText
android:id="@+id/custom_message"
@@ -59,21 +61,22 @@
android:layout_height="wrap_content"
android:padding="@dimen/message_composer_item_padding"
android:textSize="@dimen/message_compose_item_text_size"
- android:hint="@string/custom_message_hint"
+ android:hint="@string/message_composer_custom_message_hint"
android:textColor="@color/dialer_primary_text_color"
android:textColorHint="@color/dialer_edit_text_hint_color"
android:background="@color/background_dialer_white"
android:textCursorDrawable="@drawable/searchedittext_custom_cursor"
- android:layout_toLeftOf="@+id/remaining_characters"/>
+ android:layout_toStartOf="@+id/remaining_characters"
+ android:imeOptions="flagNoExtractUi"/>
<TextView
- android:id="@+id/remaining_characters"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginRight="@dimen/message_composer_item_padding"
- android:layout_alignParentRight="true"
- android:layout_centerVertical="true"
- android:textSize="@dimen/message_compose_remaining_char_text_size"
- android:textColor="@color/dialer_edit_text_hint_color"/>
+ android:id="@+id/remaining_characters"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/message_composer_item_padding"
+ android:layout_alignParentEnd="true"
+ android:layout_centerVertical="true"
+ android:textSize="@dimen/message_compose_remaining_char_text_size"
+ android:textColor="@color/dialer_edit_text_hint_color"/>
</RelativeLayout>
</LinearLayout> \ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/values-h260dp/values.xml b/java/com/android/dialer/callcomposer/res/values-h260dp/values.xml
new file mode 100644
index 000000000..c31f3b015
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/values-h260dp/values.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources>
+ <bool name="show_toolbar">true</bool>
+</resources> \ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/values-h480dp/values.xml b/java/com/android/dialer/callcomposer/res/values-h480dp/values.xml
new file mode 100644
index 000000000..77b77a553
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/values-h480dp/values.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources>
+ <bool name="show_toolbar">false</bool>
+</resources> \ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/values-w360dp/values.xml b/java/com/android/dialer/callcomposer/res/values-w360dp/values.xml
new file mode 100644
index 000000000..adff63518
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/values-w360dp/values.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources>
+ <integer name="gallery_composer_grid_view_rows">3</integer>
+</resources> \ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/values-w500dp/values.xml b/java/com/android/dialer/callcomposer/res/values-w500dp/values.xml
new file mode 100644
index 000000000..3ec2b3513
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/values-w500dp/values.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources>
+ <integer name="gallery_composer_grid_view_rows">4</integer>
+</resources> \ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/values/dimens.xml b/java/com/android/dialer/callcomposer/res/values/dimens.xml
index 3ebda7a0f..5571170b2 100644
--- a/java/com/android/dialer/callcomposer/res/values/dimens.xml
+++ b/java/com/android/dialer/callcomposer/res/values/dimens.xml
@@ -17,10 +17,6 @@
<resources>
<dimen name="call_composer_view_pager_height">258dp</dimen>
- <!-- Toolbar -->
- <dimen name="toolbar_title_text_size">16sp</dimen>
- <dimen name="toolbar_subtitle_text_size">14sp</dimen>
-
<!-- Contact bar -->
<dimen name="call_composer_contact_photo_border_thickness">2dp</dimen>
<dimen name="call_composer_contact_photo_size">116dp</dimen>
diff --git a/java/com/android/dialer/callcomposer/res/values/strings.xml b/java/com/android/dialer/callcomposer/res/values/strings.xml
index 35a8cf9da..cc7762b64 100644
--- a/java/com/android/dialer/callcomposer/res/values/strings.xml
+++ b/java/com/android/dialer/callcomposer/res/values/strings.xml
@@ -22,9 +22,11 @@
<!-- A default message to send with a phone call. [CHAR LIMIT=27] -->
<string name="quick_question">Quick question…</string>
<!-- Hint in a text field to compose a custom message to send with a phone call [CHAR LIMIT=27] -->
- <string name="custom_message_hint">Write a custom message</string>
- <!-- Text for a button to make a phone call combined with a picture or text message [CHAR LIMIT=26] -->
+ <string name="message_composer_custom_message_hint">Write a custom message</string>
+ <!-- Text for a button to make a phone call combined with a text message [CHAR LIMIT=26] -->
<string name="send_and_call">Send and call</string>
+ <!-- Text for a button to make a phone call combined with a picture or other media [CHAR LIMIT=26] -->
+ <string name="share_and_call">Share and call</string>
<!-- Accessibility description for each image in the gallery. For example, "image January 17 2015 1 59 pm". -->
<string name="gallery_item_description">image <xliff:g id="date">%1$tB %1$te %1$tY %1$tl %1$tM %1$tp</xliff:g></string>
<!-- Accessibility description for each image in the gallery when no date is present. -->
diff --git a/java/com/android/dialer/callcomposer/res/values/styles.xml b/java/com/android/dialer/callcomposer/res/values/styles.xml
index 891f6397d..29ac4ddaa 100644
--- a/java/com/android/dialer/callcomposer/res/values/styles.xml
+++ b/java/com/android/dialer/callcomposer/res/values/styles.xml
@@ -36,15 +36,6 @@
<item name="android:textColor">@color/dialer_primary_text_color</item>
<item name="android:padding">@dimen/message_composer_item_padding</item>
<item name="android:background">@drawable/item_background_material_light</item>
- </style>
-
- <style name="call_composer_toolbar_title_text">
- <item name="android:textSize">@dimen/toolbar_title_text_size</item>
- <item name="android:textColor">@color/background_dialer_white</item>
- </style>
-
- <style name="call_composer_toolbar_subtitle_text">
- <item name="android:textSize">@dimen/toolbar_subtitle_text_size</item>
- <item name="android:textColor">@color/background_dialer_white</item>
+ <item name="android:gravity">center_vertical</item>
</style>
</resources> \ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/values/values.xml b/java/com/android/dialer/callcomposer/res/values/values.xml
new file mode 100644
index 000000000..39b8e4071
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/values/values.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources>
+ <integer name="gallery_composer_grid_view_rows">2</integer>
+ <bool name="show_toolbar">false</bool>
+</resources> \ No newline at end of file
diff --git a/java/com/android/dialer/calldetails/AndroidManifest.xml b/java/com/android/dialer/calldetails/AndroidManifest.xml
new file mode 100644
index 000000000..b71207ba2
--- /dev/null
+++ b/java/com/android/dialer/calldetails/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.dialer.calldetails">
+ <application>
+ <activity
+ android:label="@string/call_details"
+ android:name="com.android.dialer.calldetails.CallDetailsActivity"
+ android:theme="@style/Theme.AppCompat.NoActionBar">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ <data android:mimeType="vnd.android.cursor.item/calls"/>
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/java/com/android/dialer/calldetails/CallDetailsActivity.java b/java/com/android/dialer/calldetails/CallDetailsActivity.java
new file mode 100644
index 000000000..6070640a0
--- /dev/null
+++ b/java/com/android/dialer/calldetails/CallDetailsActivity.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.calldetails;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.provider.CallLog;
+import android.provider.CallLog.Calls;
+import android.support.annotation.NonNull;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.MenuItem;
+import android.widget.Toolbar;
+import com.android.dialer.callcomposer.nano.CallComposerContact;
+import com.android.dialer.calldetails.nano.CallDetailsEntries;
+import com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.AsyncTaskExecutors;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.protos.ProtoParsers;
+
+/** Displays the details of a specific call log entry. */
+public class CallDetailsActivity extends AppCompatActivity {
+
+ private static final String EXTRA_CALL_DETAILS_ENTRIES = "call_details_entries";
+ private static final String EXTRA_CONTACT = "contact";
+ private static final String TASK_DELETE = "task_delete";
+
+ private CallDetailsEntry[] entries;
+
+ public static Intent newInstance(
+ Context context, @NonNull CallDetailsEntries details, @NonNull CallComposerContact contact) {
+ Assert.isNotNull(details);
+ Assert.isNotNull(contact);
+
+ Intent intent = new Intent(context, CallDetailsActivity.class);
+ ProtoParsers.put(intent, EXTRA_CONTACT, contact);
+ ProtoParsers.put(intent, EXTRA_CALL_DETAILS_ENTRIES, details);
+ return intent;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.call_details_activity);
+
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ setActionBar(toolbar);
+ toolbar.inflateMenu(R.menu.call_details_menu);
+ toolbar.setNavigationOnClickListener(v -> finish());
+ onHandleIntent(getIntent());
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ onHandleIntent(intent);
+ }
+
+ private void onHandleIntent(Intent intent) {
+ Bundle arguments = intent.getExtras();
+ CallComposerContact contact =
+ ProtoParsers.getFromInstanceState(arguments, EXTRA_CONTACT, new CallComposerContact());
+ entries =
+ ProtoParsers.getFromInstanceState(
+ arguments, EXTRA_CALL_DETAILS_ENTRIES, new CallDetailsEntries())
+ .entries;
+ RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
+ recyclerView.setLayoutManager(new LinearLayoutManager(this));
+ recyclerView.setAdapter(new CallDetailsAdapter(this, contact, entries));
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == R.id.call_detail_delete_menu_item) {
+ Logger.get(this).logImpression(DialerImpression.Type.USER_DELETED_CALL_LOG_ITEM);
+ AsyncTaskExecutors.createAsyncTaskExecutor().submit(TASK_DELETE, new DeleteCallsTask());
+ item.setEnabled(false);
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ /** Delete specified calls from the call log. */
+ private class DeleteCallsTask extends AsyncTask<Void, Void, Void> {
+
+ private final String callIds;
+
+ DeleteCallsTask() {
+ StringBuilder callIds = new StringBuilder();
+ for (CallDetailsEntry entry : entries) {
+ if (callIds.length() != 0) {
+ callIds.append(",");
+ }
+ callIds.append(entry.callId);
+ }
+ this.callIds = callIds.toString();
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ getContentResolver()
+ .delete(Calls.CONTENT_URI, CallLog.Calls._ID + " IN (" + callIds + ")", null);
+ return null;
+ }
+
+ @Override
+ public void onPostExecute(Void result) {
+ finish();
+ }
+ }
+}
diff --git a/java/com/android/dialer/calldetails/CallDetailsAdapter.java b/java/com/android/dialer/calldetails/CallDetailsAdapter.java
new file mode 100644
index 000000000..954583077
--- /dev/null
+++ b/java/com/android/dialer/calldetails/CallDetailsAdapter.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.calldetails;
+
+import android.content.Context;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import com.android.dialer.callcomposer.nano.CallComposerContact;
+import com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry;
+import com.android.dialer.calllogutils.CallTypeHelper;
+import com.android.dialer.common.Assert;
+
+/** Adapter for RecyclerView in {@link CallDetailsActivity}. */
+public class CallDetailsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
+
+ private static final int HEADER_VIEW_TYPE = 1;
+ private static final int CALL_ENTRY_VIEW_TYPE = 2;
+ private static final int FOOTER_VIEW_TYPE = 3;
+
+ private final CallComposerContact contact;
+ private final CallDetailsEntry[] callDetailsEntries;
+ private final CallTypeHelper callTypeHelper;
+
+ public CallDetailsAdapter(
+ Context context, CallComposerContact contact, CallDetailsEntry[] callDetailsEntries) {
+ this.contact = Assert.isNotNull(contact);
+ this.callDetailsEntries = Assert.isNotNull(callDetailsEntries);
+ callTypeHelper = new CallTypeHelper(context.getResources());
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+ switch (viewType) {
+ case HEADER_VIEW_TYPE:
+ return new CallDetailsHeaderViewHolder(
+ inflater.inflate(R.layout.contact_container, parent, false));
+ case CALL_ENTRY_VIEW_TYPE:
+ return new CallDetailsEntryViewHolder(
+ inflater.inflate(R.layout.call_details_entry, parent, false));
+ case FOOTER_VIEW_TYPE:
+ return new CallDetailsFooterViewHolder(
+ inflater.inflate(R.layout.call_details_footer, parent, false));
+ default:
+ Assert.fail("No ViewHolder available for viewType: " + viewType);
+ return null;
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ if (position == 0) { // Header
+ ((CallDetailsHeaderViewHolder) holder).updateContactInfo(contact);
+ } else if (position == getItemCount() - 1) {
+ ((CallDetailsFooterViewHolder) holder).setPhoneNumber(contact.number);
+ } else {
+ CallDetailsEntryViewHolder viewHolder = (CallDetailsEntryViewHolder) holder;
+ viewHolder.setCallDetails(
+ contact.number,
+ callDetailsEntries[position - 1],
+ callTypeHelper,
+ position != getItemCount() - 2);
+ }
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ if (position == 0) { // Header
+ return HEADER_VIEW_TYPE;
+ } else if (position == getItemCount() - 1) {
+ return FOOTER_VIEW_TYPE;
+ } else {
+ return CALL_ENTRY_VIEW_TYPE;
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return callDetailsEntries.length + 2; // Header + footer
+ }
+}
diff --git a/java/com/android/dialer/calldetails/CallDetailsEntryViewHolder.java b/java/com/android/dialer/calldetails/CallDetailsEntryViewHolder.java
new file mode 100644
index 000000000..b1a70af0c
--- /dev/null
+++ b/java/com/android/dialer/calldetails/CallDetailsEntryViewHolder.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.calldetails;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.provider.CallLog.Calls;
+import android.support.annotation.ColorInt;
+import android.support.annotation.NonNull;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+import com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry;
+import com.android.dialer.calllogutils.CallEntryFormatter;
+import com.android.dialer.calllogutils.CallTypeHelper;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.compat.AppCompatConstants;
+import com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult;
+import com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult.Type;
+import com.android.dialer.util.CallUtil;
+import com.android.dialer.util.DialerUtils;
+import com.android.dialer.util.IntentUtil;
+
+/** ViewHolder for call entries in {@link CallDetailsActivity}. */
+public class CallDetailsEntryViewHolder extends ViewHolder {
+
+ private final ImageView callTypeIcon;
+ private final TextView callTypeText;
+ private final TextView callTime;
+ private final TextView callDuration;
+
+ private final View multimediaImageContainer;
+ private final View multimediaDetailsContainer;
+ private final View multimediaDivider;
+
+ private final TextView multimediaDetails;
+
+ private final ImageView multimediaImage;
+
+ // TODO: Display this when location is stored - b/36160042
+ @SuppressWarnings("unused")
+ private final TextView multimediaAttachmentsNumber;
+
+ private final Context context;
+
+ public CallDetailsEntryViewHolder(View container) {
+ super(container);
+ context = container.getContext();
+
+ callTypeIcon = (ImageView) container.findViewById(R.id.call_direction);
+ callTypeText = (TextView) container.findViewById(R.id.call_type);
+ callTime = (TextView) container.findViewById(R.id.call_time);
+ callDuration = (TextView) container.findViewById(R.id.call_duration);
+
+ multimediaImageContainer = container.findViewById(R.id.multimedia_image_container);
+ multimediaDetailsContainer = container.findViewById(R.id.ec_container);
+ multimediaDivider = container.findViewById(R.id.divider);
+ multimediaDetails = (TextView) container.findViewById(R.id.multimedia_details);
+ multimediaImage = (ImageView) container.findViewById(R.id.multimedia_image);
+ multimediaAttachmentsNumber =
+ (TextView) container.findViewById(R.id.multimedia_attachments_number);
+ }
+
+ void setCallDetails(
+ String number,
+ CallDetailsEntry entry,
+ CallTypeHelper callTypeHelper,
+ boolean showMultimediaDivider) {
+ int callType = entry.callType;
+ boolean isVideoCall =
+ (entry.features & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO
+ && CallUtil.isVideoEnabled(context);
+ boolean isPulledCall =
+ (entry.features & Calls.FEATURES_PULLED_EXTERNALLY) == Calls.FEATURES_PULLED_EXTERNALLY;
+
+ Drawable callIcon = getIconForCallType(context.getResources(), callType);
+ int color = getColorForCallType(context, callType);
+ callIcon.setColorFilter(color, PorterDuff.Mode.MULTIPLY);
+ callTime.setTextColor(color);
+ callTypeIcon.setImageDrawable(callIcon);
+
+ callTypeText.setText(callTypeHelper.getCallTypeText(callType, isVideoCall, isPulledCall));
+ callTime.setText(CallEntryFormatter.formatDate(context, entry.date));
+ if (CallTypeHelper.isMissedCallType(callType)) {
+ callDuration.setVisibility(View.GONE);
+ } else {
+ callDuration.setVisibility(View.VISIBLE);
+ callDuration.setText(
+ CallEntryFormatter.formatDurationAndDataUsage(context, entry.duration, entry.dataUsage));
+ }
+ setMultimediaDetails(number, entry, showMultimediaDivider);
+ }
+
+ private void setMultimediaDetails(String number, CallDetailsEntry entry, boolean showDivider) {
+ multimediaDivider.setVisibility(showDivider ? View.VISIBLE : View.GONE);
+ if (entry.historyResults == null || entry.historyResults.length <= 0) {
+ LogUtil.i("CallDetailsEntryViewHolder.setMultimediaDetails", "no data, hiding UI");
+ multimediaDetailsContainer.setVisibility(View.GONE);
+ } else {
+
+ // TODO: b/36158891 Add room for 2 pieces of enriched call data. It's possible
+ // to have both call composer data and post call data for a single call.
+ HistoryResult historyResult = entry.historyResults[0];
+ multimediaDetailsContainer.setVisibility(View.VISIBLE);
+ multimediaDetailsContainer.setOnClickListener(
+ (v) -> {
+ DialerUtils.startActivityWithErrorToast(context, IntentUtil.getSendSmsIntent(number));
+ });
+ multimediaImageContainer.setClipToOutline(true);
+
+ if (!TextUtils.isEmpty(historyResult.imageUri)) {
+ LogUtil.i("CallDetailsEntryViewHolder.setMultimediaDetails", "setting image");
+ multimediaImageContainer.setVisibility(View.VISIBLE);
+ multimediaImage.setImageURI(Uri.parse(historyResult.imageUri));
+ multimediaDetails.setText(
+ isIncoming(historyResult) ? R.string.received_a_photo : R.string.sent_a_photo);
+ } else {
+ LogUtil.i("CallDetailsEntryViewHolder.setMultimediaDetails", "no image");
+ }
+
+ // Set text after image to overwrite the received/sent a photo text
+ if (!TextUtils.isEmpty(historyResult.text)) {
+ LogUtil.i("CallDetailsEntryViewHolder.setMultimediaDetails", "showing text");
+ multimediaDetails.setText(
+ context.getString(R.string.message_in_quotes, historyResult.text));
+ } else {
+ LogUtil.i("CallDetailsEntryViewHolder.setMultimediaDetails", "no text");
+ }
+ }
+ }
+
+ private static boolean isIncoming(@NonNull HistoryResult historyResult) {
+ return historyResult.type == Type.INCOMING_POST_CALL
+ || historyResult.type == Type.INCOMING_CALL_COMPOSER;
+ }
+
+ private static Drawable getIconForCallType(Resources resources, int callType) {
+ switch (callType) {
+ case AppCompatConstants.CALLS_OUTGOING_TYPE:
+ return resources.getDrawable(R.drawable.quantum_ic_call_made_white_24);
+ case AppCompatConstants.CALLS_BLOCKED_TYPE:
+ return resources.getDrawable(R.drawable.quantum_ic_block_white_24);
+ case AppCompatConstants.CALLS_INCOMING_TYPE:
+ case AppCompatConstants.CALLS_ANSWERED_EXTERNALLY_TYPE:
+ case AppCompatConstants.CALLS_REJECTED_TYPE:
+ return resources.getDrawable(R.drawable.quantum_ic_call_received_white_24);
+ case AppCompatConstants.CALLS_MISSED_TYPE:
+ default:
+ // It is possible for users to end up with calls with unknown call types in their
+ // call history, possibly due to 3rd party call log implementations (e.g. to
+ // distinguish between rejected and missed calls). Instead of crashing, just
+ // assume that all unknown call types are missed calls.
+ return resources.getDrawable(R.drawable.quantum_ic_call_missed_white_24);
+ }
+ }
+
+ private static @ColorInt int getColorForCallType(Context context, int callType) {
+ switch (callType) {
+ case AppCompatConstants.CALLS_OUTGOING_TYPE:
+ case AppCompatConstants.CALLS_VOICEMAIL_TYPE:
+ case AppCompatConstants.CALLS_BLOCKED_TYPE:
+ case AppCompatConstants.CALLS_INCOMING_TYPE:
+ case AppCompatConstants.CALLS_ANSWERED_EXTERNALLY_TYPE:
+ case AppCompatConstants.CALLS_REJECTED_TYPE:
+ return ContextCompat.getColor(context, R.color.dialer_secondary_text_color);
+ case AppCompatConstants.CALLS_MISSED_TYPE:
+ default:
+ // It is possible for users to end up with calls with unknown call types in their
+ // call history, possibly due to 3rd party call log implementations (e.g. to
+ // distinguish between rejected and missed calls). Instead of crashing, just
+ // assume that all unknown call types are missed calls.
+ return ContextCompat.getColor(context, R.color.missed_call);
+ }
+ }
+}
diff --git a/java/com/android/dialer/calldetails/CallDetailsFooterViewHolder.java b/java/com/android/dialer/calldetails/CallDetailsFooterViewHolder.java
new file mode 100644
index 000000000..36662bab9
--- /dev/null
+++ b/java/com/android/dialer/calldetails/CallDetailsFooterViewHolder.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.calldetails;
+
+import android.content.Context;
+import android.content.Intent;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.view.View.OnClickListener;
+import com.android.contacts.common.ClipboardUtils;
+import com.android.dialer.common.Assert;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.util.CallUtil;
+import com.android.dialer.util.DialerUtils;
+
+/** ViewHolder container for {@link CallDetailsActivity} footer. */
+public class CallDetailsFooterViewHolder extends RecyclerView.ViewHolder
+ implements OnClickListener {
+
+ private final View copy;
+ private final View edit;
+
+ private String number;
+
+ public CallDetailsFooterViewHolder(View view) {
+ super(view);
+ copy = view.findViewById(R.id.call_detail_action_copy);
+ edit = view.findViewById(R.id.call_detail_action_edit_before_call);
+
+ copy.setOnClickListener(this);
+ edit.setOnClickListener(this);
+ }
+
+ public void setPhoneNumber(String number) {
+ this.number = number;
+ }
+
+ @Override
+ public void onClick(View view) {
+ Context context = view.getContext();
+ if (view == copy) {
+ Logger.get(context).logImpression(DialerImpression.Type.CALL_DETAILS_COPY_NUMBER);
+ ClipboardUtils.copyText(context, null, number, true);
+ } else if (view == edit) {
+ Logger.get(context).logImpression(DialerImpression.Type.CALL_DETAILS_EDIT_BEFORE_CALL);
+ Intent dialIntent = new Intent(Intent.ACTION_DIAL, CallUtil.getCallUri(number));
+ DialerUtils.startActivityWithErrorToast(context, dialIntent);
+ } else {
+ Assert.fail("View on click not implemented: " + view);
+ }
+ }
+}
diff --git a/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java b/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java
new file mode 100644
index 000000000..1679c2baf
--- /dev/null
+++ b/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.calldetails;
+
+import android.content.Context;
+import android.net.Uri;
+import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.QuickContactBadge;
+import android.widget.TextView;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.util.UriUtils;
+import com.android.dialer.callcomposer.nano.CallComposerContact;
+import com.android.dialer.callintent.CallIntentBuilder;
+import com.android.dialer.callintent.nano.CallInitiationType;
+import com.android.dialer.common.Assert;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.util.DialerUtils;
+
+/** ViewHolder for Header/Contact in {@link CallDetailsActivity}. */
+public class CallDetailsHeaderViewHolder extends RecyclerView.ViewHolder
+ implements OnClickListener {
+
+ private final View callBackButton;
+ private final TextView nameView;
+ private final TextView numberView;
+ private final QuickContactBadge contactPhoto;
+ private final Context context;
+
+ private CallComposerContact contact;
+
+ CallDetailsHeaderViewHolder(View container) {
+ super(container);
+ context = container.getContext();
+ callBackButton = container.findViewById(R.id.call_back_button);
+ nameView = (TextView) container.findViewById(R.id.contact_name);
+ numberView = (TextView) container.findViewById(R.id.phone_number);
+ contactPhoto = (QuickContactBadge) container.findViewById(R.id.quick_contact_photo);
+ callBackButton.setOnClickListener(this);
+ }
+
+ /**
+ * Populates the contact info fields based on the current contact information. Copied from {@link
+ * com.android.contacts.common.dialog.CallSubjectDialog}.
+ */
+ public void updateContactInfo(CallComposerContact contact) {
+ this.contact = contact;
+ setPhoto(
+ contact.photoId,
+ Uri.parse(contact.photoUri),
+ Uri.parse(contact.contactUri),
+ contact.nameOrNumber,
+ contact.isBusiness);
+
+ nameView.setText(contact.nameOrNumber);
+ if (!TextUtils.isEmpty(contact.numberLabel) && !TextUtils.isEmpty(contact.displayNumber)) {
+ numberView.setVisibility(View.VISIBLE);
+ String secondaryInfo =
+ context.getString(
+ com.android.contacts.common.R.string.call_subject_type_and_number,
+ contact.numberLabel,
+ contact.displayNumber);
+ numberView.setText(secondaryInfo);
+ } else {
+ numberView.setVisibility(View.GONE);
+ numberView.setText(null);
+ }
+ }
+
+ /**
+ * Sets the photo on the quick contact galleryIcon. Copied from {@link
+ * com.android.contacts.common.dialog.CallSubjectDialog}.
+ */
+ private void setPhoto(
+ long photoId, Uri photoUri, Uri contactUri, String displayName, boolean isBusiness) {
+ contactPhoto.assignContactUri(contactUri);
+ contactPhoto.setOverlay(null);
+
+ int contactType =
+ isBusiness ? ContactPhotoManager.TYPE_BUSINESS : ContactPhotoManager.TYPE_DEFAULT;
+ String lookupKey = contactUri == null ? null : UriUtils.getLookupKeyFromUri(contactUri);
+
+ ContactPhotoManager.DefaultImageRequest request =
+ new ContactPhotoManager.DefaultImageRequest(
+ displayName, lookupKey, contactType, true /* isCircular */);
+
+ if (photoId == 0 && photoUri != null) {
+ contactPhoto.setImageDrawable(
+ context.getDrawable(R.drawable.product_logo_avatar_anonymous_color_120));
+ } else {
+ ContactPhotoManager.getInstance(context)
+ .loadThumbnail(
+ contactPhoto, photoId, false /* darkTheme */, true /* isCircular */, request);
+ }
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (view == callBackButton) {
+ Logger.get(view.getContext()).logImpression(DialerImpression.Type.CALL_DETAILS_CALL_BACK);
+ DialerUtils.startActivityWithErrorToast(
+ view.getContext(),
+ new CallIntentBuilder(contact.number, CallInitiationType.Type.CALL_DETAILS).build());
+ } else {
+ Assert.fail("View OnClickListener not implemented: " + view);
+ }
+ }
+}
diff --git a/java/com/android/dialer/calldetails/nano/CallDetailsEntries.java b/java/com/android/dialer/calldetails/nano/CallDetailsEntries.java
new file mode 100644
index 000000000..aee8f3652
--- /dev/null
+++ b/java/com/android/dialer/calldetails/nano/CallDetailsEntries.java
@@ -0,0 +1,440 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+
+package com.android.dialer.calldetails.nano;
+
+/** This file is autogenerated, but javadoc required. */
+@SuppressWarnings("hiding")
+public final class CallDetailsEntries
+ extends com.google.protobuf.nano.ExtendableMessageNano<CallDetailsEntries> {
+
+ /** This file is autogenerated, but javadoc required. */
+ public static final class CallDetailsEntry
+ extends com.google.protobuf.nano.ExtendableMessageNano<CallDetailsEntry> {
+
+ private static volatile CallDetailsEntry[] _emptyArray;
+ public static CallDetailsEntry[] emptyArray() {
+ // Lazily initializes the empty array
+ if (_emptyArray == null) {
+ synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+ if (_emptyArray == null) {
+ _emptyArray = new CallDetailsEntry[0];
+ }
+ }
+ }
+ return _emptyArray;
+ }
+
+ // optional int64 call_id = 1;
+ public long callId;
+
+ // optional int32 call_type = 2;
+ public int callType;
+
+ // optional int32 features = 3;
+ public int features;
+
+ // optional int64 date = 4;
+ public long date;
+
+ // optional int64 duration = 5;
+ public long duration;
+
+ // optional int64 data_usage = 6;
+ public long dataUsage;
+
+ // repeated .com.android.dialer.enrichedcall.historyquery.proto.
+ // HistoryResult history_results = 7;
+ public com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult[] historyResults;
+
+ // @@protoc_insertion_point(class_scope:com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry)
+
+ public CallDetailsEntry() {
+ clear();
+ }
+
+ public CallDetailsEntry clear() {
+ callId = 0L;
+ callType = 0;
+ features = 0;
+ date = 0L;
+ duration = 0L;
+ dataUsage = 0L;
+ historyResults =
+ com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult.emptyArray();
+ unknownFieldData = null;
+ cachedSize = -1;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (!(o instanceof CallDetailsEntry)) {
+ return false;
+ }
+ CallDetailsEntry other = (CallDetailsEntry) o;
+ if (this.callId != other.callId) {
+ return false;
+ }
+ if (this.callType != other.callType) {
+ return false;
+ }
+ if (this.features != other.features) {
+ return false;
+ }
+ if (this.date != other.date) {
+ return false;
+ }
+ if (this.duration != other.duration) {
+ return false;
+ }
+ if (this.dataUsage != other.dataUsage) {
+ return false;
+ }
+ if (!com.google.protobuf.nano.InternalNano.equals(
+ this.historyResults, other.historyResults)) {
+ return false;
+ }
+ if (unknownFieldData == null || unknownFieldData.isEmpty()) {
+ return other.unknownFieldData == null || other.unknownFieldData.isEmpty();
+ } else {
+ return unknownFieldData.equals(other.unknownFieldData);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + getClass().getName().hashCode();
+ result = 31 * result + (int) (this.callId ^ (this.callId >>> 32));
+ result = 31 * result + this.callType;
+ result = 31 * result + this.features;
+ result = 31 * result + (int) (this.date ^ (this.date >>> 32));
+ result = 31 * result + (int) (this.duration ^ (this.duration >>> 32));
+ result = 31 * result + (int) (this.dataUsage ^ (this.dataUsage >>> 32));
+ result = 31 * result + com.google.protobuf.nano.InternalNano.hashCode(this.historyResults);
+ result =
+ 31 * result
+ + (unknownFieldData == null || unknownFieldData.isEmpty()
+ ? 0
+ : unknownFieldData.hashCode());
+ return result;
+ }
+
+ @Override
+ public void writeTo(com.google.protobuf.nano.CodedOutputByteBufferNano output)
+ throws java.io.IOException {
+ if (this.callId != 0L) {
+ output.writeInt64(1, this.callId);
+ }
+ if (this.callType != 0) {
+ output.writeInt32(2, this.callType);
+ }
+ if (this.features != 0) {
+ output.writeInt32(3, this.features);
+ }
+ if (this.date != 0L) {
+ output.writeInt64(4, this.date);
+ }
+ if (this.duration != 0L) {
+ output.writeInt64(5, this.duration);
+ }
+ if (this.dataUsage != 0L) {
+ output.writeInt64(6, this.dataUsage);
+ }
+ if (this.historyResults != null && this.historyResults.length > 0) {
+ for (int i = 0; i < this.historyResults.length; i++) {
+ com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult element =
+ this.historyResults[i];
+ if (element != null) {
+ output.writeMessage(7, element);
+ }
+ }
+ }
+ super.writeTo(output);
+ }
+
+ @Override
+ protected int computeSerializedSize() {
+ int size = super.computeSerializedSize();
+ if (this.callId != 0L) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeInt64Size(1, this.callId);
+ }
+ if (this.callType != 0) {
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeInt32Size(2, this.callType);
+ }
+ if (this.features != 0) {
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeInt32Size(3, this.features);
+ }
+ if (this.date != 0L) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeInt64Size(4, this.date);
+ }
+ if (this.duration != 0L) {
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeInt64Size(5, this.duration);
+ }
+ if (this.dataUsage != 0L) {
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeInt64Size(6, this.dataUsage);
+ }
+ if (this.historyResults != null && this.historyResults.length > 0) {
+ for (int i = 0; i < this.historyResults.length; i++) {
+ com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult element =
+ this.historyResults[i];
+ if (element != null) {
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeMessageSize(7, element);
+ }
+ }
+ }
+ return size;
+ }
+
+ @Override
+ public CallDetailsEntry mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ while (true) {
+ int tag = input.readTag();
+ switch (tag) {
+ case 0:
+ return this;
+ default:
+ {
+ if (!super.storeUnknownField(input, tag)) {
+ return this;
+ }
+ break;
+ }
+ case 8:
+ {
+ this.callId = input.readInt64();
+ break;
+ }
+ case 16:
+ {
+ this.callType = input.readInt32();
+ break;
+ }
+ case 24:
+ {
+ this.features = input.readInt32();
+ break;
+ }
+ case 32:
+ {
+ this.date = input.readInt64();
+ break;
+ }
+ case 40:
+ {
+ this.duration = input.readInt64();
+ break;
+ }
+ case 48:
+ {
+ this.dataUsage = input.readInt64();
+ break;
+ }
+ case 58:
+ {
+ int arrayLength =
+ com.google.protobuf.nano.WireFormatNano.getRepeatedFieldArrayLength(input, 58);
+ int i = this.historyResults == null ? 0 : this.historyResults.length;
+ com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult[] newArray =
+ new com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult
+ [i + arrayLength];
+ if (i != 0) {
+ java.lang.System.arraycopy(this.historyResults, 0, newArray, 0, i);
+ }
+ for (; i < newArray.length - 1; i++) {
+ newArray[i] =
+ new com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult();
+ input.readMessage(newArray[i]);
+ input.readTag();
+ }
+ // Last one without readTag.
+ newArray[i] =
+ new com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult();
+ input.readMessage(newArray[i]);
+ this.historyResults = newArray;
+ break;
+ }
+ }
+ }
+ }
+
+ public static CallDetailsEntry parseFrom(byte[] data)
+ throws com.google.protobuf.nano.InvalidProtocolBufferNanoException {
+ return com.google.protobuf.nano.MessageNano.mergeFrom(new CallDetailsEntry(), data);
+ }
+
+ public static CallDetailsEntry parseFrom(
+ com.google.protobuf.nano.CodedInputByteBufferNano input) throws java.io.IOException {
+ return new CallDetailsEntry().mergeFrom(input);
+ }
+ }
+
+ private static volatile CallDetailsEntries[] _emptyArray;
+ public static CallDetailsEntries[] emptyArray() {
+ // Lazily initializes the empty array
+ if (_emptyArray == null) {
+ synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+ if (_emptyArray == null) {
+ _emptyArray = new CallDetailsEntries[0];
+ }
+ }
+ }
+ return _emptyArray;
+ }
+
+ // repeated .com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry entries = 1;
+ public com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry[] entries;
+
+ // @@protoc_insertion_point(class_scope:com.android.dialer.calldetails.CallDetailsEntries)
+
+ public CallDetailsEntries() {
+ clear();
+ }
+
+ public CallDetailsEntries clear() {
+ entries = com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry.emptyArray();
+ unknownFieldData = null;
+ cachedSize = -1;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (!(o instanceof CallDetailsEntries)) {
+ return false;
+ }
+ CallDetailsEntries other = (CallDetailsEntries) o;
+ if (!com.google.protobuf.nano.InternalNano.equals(this.entries, other.entries)) {
+ return false;
+ }
+ if (unknownFieldData == null || unknownFieldData.isEmpty()) {
+ return other.unknownFieldData == null || other.unknownFieldData.isEmpty();
+ } else {
+ return unknownFieldData.equals(other.unknownFieldData);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + getClass().getName().hashCode();
+ result = 31 * result + com.google.protobuf.nano.InternalNano.hashCode(this.entries);
+ result =
+ 31 * result
+ + (unknownFieldData == null || unknownFieldData.isEmpty()
+ ? 0
+ : unknownFieldData.hashCode());
+ return result;
+ }
+
+ @Override
+ public void writeTo(com.google.protobuf.nano.CodedOutputByteBufferNano output)
+ throws java.io.IOException {
+ if (this.entries != null && this.entries.length > 0) {
+ for (int i = 0; i < this.entries.length; i++) {
+ com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry element =
+ this.entries[i];
+ if (element != null) {
+ output.writeMessage(1, element);
+ }
+ }
+ }
+ super.writeTo(output);
+ }
+
+ @Override
+ protected int computeSerializedSize() {
+ int size = super.computeSerializedSize();
+ if (this.entries != null && this.entries.length > 0) {
+ for (int i = 0; i < this.entries.length; i++) {
+ com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry element =
+ this.entries[i];
+ if (element != null) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeMessageSize(1, element);
+ }
+ }
+ }
+ return size;
+ }
+
+ @Override
+ public CallDetailsEntries mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ while (true) {
+ int tag = input.readTag();
+ switch (tag) {
+ case 0:
+ return this;
+ default:
+ {
+ if (!super.storeUnknownField(input, tag)) {
+ return this;
+ }
+ break;
+ }
+ case 10:
+ {
+ int arrayLength =
+ com.google.protobuf.nano.WireFormatNano.getRepeatedFieldArrayLength(input, 10);
+ int i = this.entries == null ? 0 : this.entries.length;
+ com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry[] newArray =
+ new com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry
+ [i + arrayLength];
+ if (i != 0) {
+ java.lang.System.arraycopy(this.entries, 0, newArray, 0, i);
+ }
+ for (; i < newArray.length - 1; i++) {
+ newArray[i] =
+ new com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry();
+ input.readMessage(newArray[i]);
+ input.readTag();
+ }
+ // Last one without readTag.
+ newArray[i] =
+ new com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry();
+ input.readMessage(newArray[i]);
+ this.entries = newArray;
+ break;
+ }
+ }
+ }
+ }
+
+ public static CallDetailsEntries parseFrom(byte[] data)
+ throws com.google.protobuf.nano.InvalidProtocolBufferNanoException {
+ return com.google.protobuf.nano.MessageNano.mergeFrom(new CallDetailsEntries(), data);
+ }
+
+ public static CallDetailsEntries parseFrom(
+ com.google.protobuf.nano.CodedInputByteBufferNano input) throws java.io.IOException {
+ return new CallDetailsEntries().mergeFrom(input);
+ }
+}
diff --git a/java/com/android/dialer/calldetails/res/drawable/multimedia_image_background.xml b/java/com/android/dialer/calldetails/res/drawable/multimedia_image_background.xml
new file mode 100644
index 000000000..421bdbfee
--- /dev/null
+++ b/java/com/android/dialer/calldetails/res/drawable/multimedia_image_background.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="2dp"/>
+</shape>
diff --git a/java/com/android/dialer/calldetails/res/layout/call_details_activity.xml b/java/com/android/dialer/calldetails/res/layout/call_details_activity.xml
new file mode 100644
index 000000000..038a8745e
--- /dev/null
+++ b/java/com/android/dialer/calldetails/res/layout/call_details_activity.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ android:background="@color/dialer_theme_color"
+ android:elevation="4dp"
+ android:titleTextAppearance="@style/toolbar_title_text"
+ android:title="@string/call_details"
+ android:navigationIcon="@drawable/quantum_ic_arrow_back_white_24"/>
+
+ <android.support.v7.widget.RecyclerView
+ android:id="@+id/recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/background_dialer_white"/>
+</LinearLayout> \ No newline at end of file
diff --git a/java/com/android/dialer/calldetails/res/layout/call_details_entry.xml b/java/com/android/dialer/calldetails/res/layout/call_details_entry.xml
new file mode 100644
index 000000000..7f8bb8087
--- /dev/null
+++ b/java/com/android/dialer/calldetails/res/layout/call_details_entry.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/call_entry_padding">
+
+ <ImageView
+ android:id="@+id/call_direction"
+ android:layout_width="@dimen/call_entry_icon_size"
+ android:layout_height="@dimen/call_entry_icon_size"
+ android:layout_marginStart="@dimen/call_entry_padding"
+ android:layout_marginEnd="@dimen/call_entry_left_margin"/>
+
+ <TextView
+ android:id="@+id/call_type"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_toEndOf="@+id/call_direction"
+ style="@style/PrimaryText"/>
+
+ <TextView
+ android:id="@+id/call_time"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_toEndOf="@id/call_direction"
+ android:layout_below="@+id/call_type"
+ android:layout_marginBottom="@dimen/call_entry_bottom_padding"
+ style="@style/SecondaryText"/>
+
+ <TextView
+ android:id="@+id/call_duration"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:layout_marginEnd="@dimen/call_entry_padding"
+ style="@style/PrimaryText"/>
+
+ <include
+ layout="@layout/ec_data_container"
+ android:id="@+id/ec_container"
+ android:layout_height="@dimen/ec_container_height"
+ android:layout_width="match_parent"
+ android:layout_marginStart="@dimen/ec_text_left_margin"
+ android:layout_below="@+id/call_time"
+ android:visibility="gone"/>
+
+ <View
+ android:id="@+id/divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_below="@id/ec_container"
+ android:layout_marginTop="@dimen/ec_divider_top_bottom_margin"
+ android:layout_marginBottom="@dimen/ec_divider_top_bottom_margin"
+ android:layout_marginStart="@dimen/ec_text_left_margin"
+ android:background="#12000000"
+ android:visibility="gone"/>
+</RelativeLayout> \ No newline at end of file
diff --git a/java/com/android/dialer/calldetails/res/layout/call_details_footer.xml b/java/com/android/dialer/calldetails/res/layout/call_details_footer.xml
new file mode 100644
index 000000000..885cb0989
--- /dev/null
+++ b/java/com/android/dialer/calldetails/res/layout/call_details_footer.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginTop="@dimen/ec_divider_top_bottom_margin"
+ android:layout_marginBottom="@dimen/ec_divider_top_bottom_margin"
+ android:background="#12000000"/>
+
+ <TextView
+ android:id="@+id/call_detail_action_copy"
+ style="@style/CallDetailsActionItemStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:drawableStart="@drawable/quantum_ic_content_copy_grey600_24"
+ android:text="@string/call_details_copy_number"/>
+
+ <TextView
+ android:id="@+id/call_detail_action_edit_before_call"
+ style="@style/CallDetailsActionItemStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:drawableStart="@drawable/quantum_ic_edit_grey600_24"
+ android:text="@string/call_details_edit_number"/>
+</LinearLayout>
diff --git a/java/com/android/dialer/calldetails/res/layout/contact_container.xml b/java/com/android/dialer/calldetails/res/layout/contact_container.xml
new file mode 100644
index 000000000..95fe189b2
--- /dev/null
+++ b/java/com/android/dialer/calldetails/res/layout/contact_container.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:padding="@dimen/contact_container_padding">
+
+ <QuickContactBadge
+ android:id="@+id/quick_contact_photo"
+ android:layout_width="@dimen/call_details_contact_photo_size"
+ android:layout_height="@dimen/call_details_contact_photo_size"
+ android:focusable="true"/>
+
+ <TextView
+ android:id="@+id/contact_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/text_bottom_margin"
+ android:layout_marginStart="@dimen/photo_text_margin"
+ android:layout_toEndOf="@+id/quick_contact_photo"
+ android:layout_toStartOf="@+id/call_back_button"
+ style="@style/PrimaryText"/>
+
+ <TextView
+ android:id="@+id/phone_number"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/photo_text_margin"
+ android:layout_toEndOf="@+id/quick_contact_photo"
+ android:layout_toStartOf="@+id/call_back_button"
+ android:layout_below="@+id/contact_name"
+ style="@style/SecondaryText"/>
+
+ <ImageView
+ android:id="@+id/call_back_button"
+ android:layout_width="@dimen/call_back_button_size"
+ android:layout_height="@dimen/call_back_button_size"
+ android:layout_alignParentEnd="true"
+ android:layout_centerVertical="true"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ android:contentDescription="@string/description_call_log_call_action"
+ android:src="@drawable/quantum_ic_call_white_24"
+ android:tint="@color/secondary_text_color"/>
+</RelativeLayout> \ No newline at end of file
diff --git a/java/com/android/dialer/calldetails/res/layout/ec_data_container.xml b/java/com/android/dialer/calldetails/res/layout/ec_data_container.xml
new file mode 100644
index 000000000..5ad7912fa
--- /dev/null
+++ b/java/com/android/dialer/calldetails/res/layout/ec_data_container.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/ec_container_height">
+
+ <TextView
+ android:id="@+id/multimedia_details"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:maxLines="2"
+ style="@style/SecondaryText"/>
+
+ <FrameLayout
+ android:id="@+id/multimedia_image_container"
+ android:layout_width="@dimen/ec_photo_size"
+ android:layout_height="@dimen/ec_photo_size"
+ android:layout_alignParentEnd="true"
+ android:layout_marginEnd="@dimen/call_entry_padding"
+ android:layout_centerVertical="true"
+ android:background="@drawable/multimedia_image_background"
+ android:outlineProvider="background"
+ android:visibility="gone">
+
+ <ImageView
+ android:id="@+id/multimedia_image"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scaleType="centerCrop"/>
+
+ <com.android.incallui.autoresizetext.AutoResizeTextView
+ android:id="@+id/multimedia_attachments_number"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:padding="4dp"
+ android:gravity="center"
+ android:textColor="@color/background_dialer_white"
+ android:textSize="100sp"
+ android:background="#80000000"
+ android:visibility="gone"/>
+ </FrameLayout>
+</RelativeLayout> \ No newline at end of file
diff --git a/java/com/android/dialer/calldetails/res/menu/call_details_menu.xml b/java/com/android/dialer/calldetails/res/menu/call_details_menu.xml
new file mode 100644
index 000000000..c2d1032da
--- /dev/null
+++ b/java/com/android/dialer/calldetails/res/menu/call_details_menu.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/call_detail_delete_menu_item"
+ android:icon="@drawable/quantum_ic_delete_white_24"
+ android:title="@string/delete"
+ android:showAsAction="ifRoom"/>
+</menu> \ No newline at end of file
diff --git a/java/com/android/dialer/calldetails/res/values/dimens.xml b/java/com/android/dialer/calldetails/res/values/dimens.xml
new file mode 100644
index 000000000..b1a8f1c8e
--- /dev/null
+++ b/java/com/android/dialer/calldetails/res/values/dimens.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources>
+ <dimen name="text_bottom_margin">2dp</dimen>
+ <dimen name="call_details_primary_text_size">16sp</dimen>
+ <dimen name="call_details_secondary_text_size">14sp</dimen>
+
+ <!-- contact container -->
+ <dimen name="contact_container_padding">16dp</dimen>
+ <dimen name="call_details_contact_photo_size">40dp</dimen>
+ <dimen name="photo_text_margin">16dp</dimen>
+ <dimen name="call_back_button_size">24dp</dimen>
+
+ <!-- call entry container -->
+ <dimen name="call_entry_icon_size">24dp</dimen>
+ <dimen name="call_entry_padding">16dp</dimen>
+ <dimen name="call_entry_bottom_padding">14dp</dimen>
+ <dimen name="call_entry_left_margin">32dp</dimen>
+
+ <!-- EC container -->
+ <dimen name="call_details_ec_text_size">12sp</dimen>
+ <dimen name="ec_container_height">48dp</dimen>
+ <dimen name="ec_text_left_margin">72dp</dimen>
+ <dimen name="ec_photo_size">40dp</dimen>
+ <dimen name="ec_divider_top_bottom_margin">8dp</dimen>
+</resources> \ No newline at end of file
diff --git a/java/com/android/dialer/calldetails/res/values/strings.xml b/java/com/android/dialer/calldetails/res/values/strings.xml
new file mode 100644
index 000000000..8a7cc4cfc
--- /dev/null
+++ b/java/com/android/dialer/calldetails/res/values/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Title bar for call detail screen -->
+ <string name="call_details">Call details</string>
+
+ <!-- Menu item in call details used to remove a call or voicemail from the call log. -->
+ <string name="delete">Delete</string>
+
+ <!-- Option displayed in context menu to copy long pressed phone number. [CHAR LIMIT=48] -->
+ <string name="call_details_copy_number">Copy number</string>
+
+ <!-- Label for action to edit a number before calling it. [CHAR LIMIT=48] -->
+ <string name="call_details_edit_number">Edit number before call</string>
+
+ <!-- String describing the phone icon on a call log list item. When tapped, it will place a
+ call to the number represented by that call log entry. [CHAR LIMIT=NONE]-->
+ <string name="description_call_log_call_action">Call</string>
+
+ <!-- String shown when the call details show a image that was sent -->
+ <string name="sent_a_photo">Sent a photo</string>
+
+ <!-- String shown when the call details show a image that was received -->
+ <string name="received_a_photo">Received a photo</string>
+
+ <!-- Messages shown to the user are wrapped in quotes, e.g. the user would see "Some text" -->
+ <string name="message_in_quotes">\"<xliff:g id="message">%1$s</xliff:g>\"</string>
+</resources>
diff --git a/java/com/android/dialer/calldetails/res/values/styles.xml b/java/com/android/dialer/calldetails/res/values/styles.xml
new file mode 100644
index 000000000..4fffe1afb
--- /dev/null
+++ b/java/com/android/dialer/calldetails/res/values/styles.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources>
+ <style name="PrimaryText">
+ <item name="android:textColor">#DE000000</item>
+ <item name="android:textSize">@dimen/call_details_primary_text_size</item>
+ <item name="android:maxLines">1</item>
+ </style>
+
+ <style name="SecondaryText">
+ <item name="android:textColor">#8A000000</item>
+ <item name="android:textSize">@dimen/call_details_secondary_text_size</item>
+ <item name="android:maxLines">1</item>
+ </style>
+
+ <style name="ECText">
+ <item name="android:textColor">#8A000000</item>
+ <item name="android:textSize">@dimen/call_details_ec_text_size</item>
+ <item name="android:maxLines">1</item>
+ </style>
+
+ <style name="CallDetailsActionItemStyle">
+ <item name="android:foreground">?android:attr/selectableItemBackground</item>
+ <item name="android:clickable">true</item>
+ <item name="android:drawablePadding">28dp</item>
+ <item name="android:gravity">center_vertical</item>
+ <item name="android:paddingStart">28dp</item>
+ <item name="android:paddingEnd">28dp</item>
+ <item name="android:paddingTop">16dp</item>
+ <item name="android:paddingBottom">16dp</item>
+ <item name="android:textColor">#8A000000</item>
+ <item name="android:textSize">14sp</item>
+ </style>
+</resources> \ No newline at end of file
diff --git a/java/com/android/dialer/callintent/nano/CallInitiationType.java b/java/com/android/dialer/callintent/nano/CallInitiationType.java
index 4badd6e57..1dddb6ce8 100644
--- a/java/com/android/dialer/callintent/nano/CallInitiationType.java
+++ b/java/com/android/dialer/callintent/nano/CallInitiationType.java
@@ -11,17 +11,19 @@
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
- * limitations under the License
+ * limitations under the License.
*/
// Generated by the protocol buffer compiler. DO NOT EDIT!
package com.android.dialer.callintent.nano;
+/** This file is autogenerated, but javadoc required. */
@SuppressWarnings("hiding")
-public final class CallInitiationType extends
- com.google.protobuf.nano.ExtendableMessageNano<CallInitiationType> {
+public final class CallInitiationType
+ extends com.google.protobuf.nano.ExtendableMessageNano<CallInitiationType> {
+ /** This file is autogenerated, but javadoc required. */
// enum Type
public interface Type {
public static final int UNKNOWN_INITIATION = 0;
@@ -44,11 +46,11 @@ public final class CallInitiationType extends
}
private static volatile CallInitiationType[] _emptyArray;
+
public static CallInitiationType[] emptyArray() {
// Lazily initializes the empty array
if (_emptyArray == null) {
- synchronized (
- com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+ synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
if (_emptyArray == null) {
_emptyArray = new CallInitiationType[0];
}
@@ -70,20 +72,20 @@ public final class CallInitiationType extends
}
@Override
- public CallInitiationType mergeFrom(
- com.google.protobuf.nano.CodedInputByteBufferNano input)
+ public CallInitiationType mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
throws java.io.IOException {
while (true) {
int tag = input.readTag();
switch (tag) {
case 0:
return this;
- default: {
- if (!super.storeUnknownField(input, tag)) {
- return this;
+ default:
+ {
+ if (!super.storeUnknownField(input, tag)) {
+ return this;
+ }
+ break;
}
- break;
- }
}
}
}
@@ -94,8 +96,7 @@ public final class CallInitiationType extends
}
public static CallInitiationType parseFrom(
- com.google.protobuf.nano.CodedInputByteBufferNano input)
- throws java.io.IOException {
+ com.google.protobuf.nano.CodedInputByteBufferNano input) throws java.io.IOException {
return new CallInitiationType().mergeFrom(input);
}
}
diff --git a/java/com/android/dialer/calllogutils/AndroidManifest.xml b/java/com/android/dialer/calllogutils/AndroidManifest.xml
new file mode 100644
index 000000000..228865a38
--- /dev/null
+++ b/java/com/android/dialer/calllogutils/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<manifest package="com.android.dialer.calllogutils"/> \ No newline at end of file
diff --git a/java/com/android/dialer/calllogutils/CallEntryFormatter.java b/java/com/android/dialer/calllogutils/CallEntryFormatter.java
new file mode 100644
index 000000000..bd6d53f48
--- /dev/null
+++ b/java/com/android/dialer/calllogutils/CallEntryFormatter.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.calllogutils;
+
+import android.content.Context;
+import android.icu.lang.UCharacter;
+import android.icu.text.BreakIterator;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.text.format.DateUtils;
+import android.text.format.Formatter;
+import com.android.dialer.util.DialerUtils;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/** Utility class for formatting data and data usage in call log entries. */
+public class CallEntryFormatter {
+
+ /**
+ * Formats the provided date into a value suitable for display in the current locale.
+ *
+ * <p>For example, returns a string like "Wednesday, May 25, 2016, 8:02PM" or "Chorshanba, 2016
+ * may 25,20:02".
+ *
+ * <p>For pre-N devices, the returned value may not start with a capital if the local convention
+ * is to not capitalize day names. On N+ devices, the returned value is always capitalized.
+ */
+ public static CharSequence formatDate(Context context, long callDateMillis) {
+ CharSequence dateValue =
+ DateUtils.formatDateRange(
+ context,
+ callDateMillis /* startDate */,
+ callDateMillis /* endDate */,
+ DateUtils.FORMAT_SHOW_TIME
+ | DateUtils.FORMAT_SHOW_DATE
+ | DateUtils.FORMAT_SHOW_WEEKDAY
+ | DateUtils.FORMAT_SHOW_YEAR);
+
+ // We want the beginning of the date string to be capitalized, even if the word at the beginning
+ // of the string is not usually capitalized. For example, "Wednesdsay" in Uzbek is "chorshanba”
+ // (not capitalized). To handle this issue we apply title casing to the start of the sentence so
+ // that "chorshanba, 2016 may 25,20:02" becomes "Chorshanba, 2016 may 25,20:02".
+ //
+ // The ICU library was not available in Android until N, so we can only do this in N+ devices.
+ // Pre-N devices will still see incorrect capitalization in some languages.
+ if (VERSION.SDK_INT < VERSION_CODES.N) {
+ return dateValue;
+ }
+
+ // Using the ICU library is safer than just applying toUpperCase() on the first letter of the
+ // word because in some languages, there can be multiple starting characters which should be
+ // upper-cased together. For example in Dutch "ij" is a digraph in which both letters should be
+ // capitalized together.
+
+ // TITLECASE_NO_LOWERCASE is necessary so that things that are already capitalized like the
+ // month ("May") are not lower-cased as part of the conversion.
+ return UCharacter.toTitleCase(
+ Locale.getDefault(),
+ dateValue.toString(),
+ BreakIterator.getSentenceInstance(),
+ UCharacter.TITLECASE_NO_LOWERCASE);
+ }
+
+ private static CharSequence formatDuration(Context context, long elapsedSeconds) {
+ long minutes = 0;
+ long seconds = 0;
+
+ if (elapsedSeconds >= 60) {
+ minutes = elapsedSeconds / 60;
+ elapsedSeconds -= minutes * 60;
+ seconds = elapsedSeconds;
+ return context.getString(R.string.call_details_duration_format, minutes, seconds);
+ } else {
+ seconds = elapsedSeconds;
+ return context.getString(R.string.call_details_short_duration_format, seconds);
+ }
+ }
+
+ /**
+ * Formats a string containing the call duration and the data usage (if specified).
+ *
+ * @param elapsedSeconds Total elapsed seconds.
+ * @param dataUsage Data usage in bytes, or null if not specified.
+ * @return String containing call duration and data usage.
+ */
+ public static CharSequence formatDurationAndDataUsage(
+ Context context, long elapsedSeconds, Long dataUsage) {
+ CharSequence duration = formatDuration(context, elapsedSeconds);
+ List<CharSequence> durationItems = new ArrayList<>();
+ if (dataUsage != null) {
+ durationItems.add(duration);
+ durationItems.add(Formatter.formatShortFileSize(context, dataUsage));
+ return DialerUtils.join(durationItems);
+ } else {
+ return duration;
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/calllog/CallTypeHelper.java b/java/com/android/dialer/calllogutils/CallTypeHelper.java
index f3c27a1ac..d3b5b67d7 100644
--- a/java/com/android/dialer/app/calllog/CallTypeHelper.java
+++ b/java/com/android/dialer/calllogutils/CallTypeHelper.java
@@ -14,10 +14,9 @@
* limitations under the License.
*/
-package com.android.dialer.app.calllog;
+package com.android.dialer.calllogutils;
import android.content.res.Resources;
-import com.android.dialer.app.R;
import com.android.dialer.compat.AppCompatConstants;
/** Helper class to perform operations related to call types. */
diff --git a/java/com/android/dialer/app/calllog/CallTypeIconsView.java b/java/com/android/dialer/calllogutils/CallTypeIconsView.java
index cd5c5460c..61208bc9a 100644
--- a/java/com/android/dialer/app/calllog/CallTypeIconsView.java
+++ b/java/com/android/dialer/calllogutils/CallTypeIconsView.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.dialer.app.calllog;
+package com.android.dialer.calllogutils;
import android.content.Context;
import android.graphics.Bitmap;
@@ -26,7 +26,6 @@ import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import com.android.contacts.common.util.BitmapUtil;
-import com.android.dialer.app.R;
import com.android.dialer.compat.AppCompatConstants;
import java.util.ArrayList;
import java.util.List;
@@ -41,6 +40,7 @@ public class CallTypeIconsView extends View {
private static Resources sResources;
private List<Integer> mCallTypes = new ArrayList<>(3);
private boolean mShowVideo = false;
+ private boolean mShowHd = false;
private int mWidth;
private int mHeight;
@@ -94,6 +94,15 @@ public class CallTypeIconsView extends View {
return mShowVideo;
}
+ public void setShowHd(boolean showHd) {
+ mShowHd = showHd;
+ if (showHd) {
+ mWidth += sResources.hdCall.getIntrinsicWidth();
+ mHeight = Math.max(mHeight, sResources.hdCall.getIntrinsicHeight());
+ invalidate();
+ }
+ }
+
public int getCount() {
return mCallTypes.size();
}
@@ -147,6 +156,13 @@ public class CallTypeIconsView extends View {
drawable.setBounds(left, 0, right, sResources.videoCall.getIntrinsicHeight());
drawable.draw(canvas);
}
+ // If showing HD call icon, draw it scaled appropriately.
+ if (mShowHd) {
+ final Drawable drawable = sResources.hdCall;
+ final int right = left + sResources.hdCall.getIntrinsicWidth();
+ drawable.setBounds(left, 0, right, sResources.hdCall.getIntrinsicHeight());
+ drawable.draw(canvas);
+ }
}
private static class Resources {
@@ -166,9 +182,12 @@ public class CallTypeIconsView extends View {
// Drawable representing a blocked call.
public final Drawable blocked;
- // Drawable repesenting a video call.
+ // Drawable repesenting a video call.
public final Drawable videoCall;
+ // Drawable represeting a hd call.
+ public final Drawable hdCall;
+
/** The margin to use for icons. */
public final int iconMargin;
@@ -204,6 +223,10 @@ public class CallTypeIconsView extends View {
videoCall.setColorFilter(
r.getColor(R.color.dialer_secondary_text_color), PorterDuff.Mode.MULTIPLY);
+ hdCall = getScaledBitmap(context, R.drawable.quantum_ic_hd_white_24);
+ hdCall.setColorFilter(
+ r.getColor(R.color.dialer_secondary_text_color), PorterDuff.Mode.MULTIPLY);
+
iconMargin = r.getDimensionPixelSize(R.dimen.call_log_icon_margin);
}
diff --git a/java/com/android/dialer/app/calllog/PhoneAccountUtils.java b/java/com/android/dialer/calllogutils/PhoneAccountUtils.java
index c6d94d341..c639893ef 100644
--- a/java/com/android/dialer/app/calllog/PhoneAccountUtils.java
+++ b/java/com/android/dialer/calllogutils/PhoneAccountUtils.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.dialer.app.calllog;
+package com.android.dialer.calllogutils;
import android.content.ComponentName;
import android.content.Context;
diff --git a/java/com/android/dialer/app/PhoneCallDetails.java b/java/com/android/dialer/calllogutils/PhoneCallDetails.java
index 436f68eec..ba05a87e2 100644
--- a/java/com/android/dialer/app/PhoneCallDetails.java
+++ b/java/com/android/dialer/calllogutils/PhoneCallDetails.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.dialer.app;
+package com.android.dialer.calllogutils;
import android.content.Context;
import android.content.res.Resources;
@@ -27,7 +27,6 @@ import android.text.TextUtils;
import com.android.contacts.common.ContactsUtils.UserType;
import com.android.contacts.common.preference.ContactsPreferences;
import com.android.contacts.common.util.ContactDisplayUtils;
-import com.android.dialer.app.calllog.PhoneNumberDisplayUtil;
import com.android.dialer.phonenumbercache.ContactInfo;
/** The details of a phone call to be shown in the UI. */
diff --git a/java/com/android/dialer/app/calllog/PhoneNumberDisplayUtil.java b/java/com/android/dialer/calllogutils/PhoneNumberDisplayUtil.java
index 410d4cc37..9bebfacac 100644
--- a/java/com/android/dialer/app/calllog/PhoneNumberDisplayUtil.java
+++ b/java/com/android/dialer/calllogutils/PhoneNumberDisplayUtil.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.dialer.app.calllog;
+package com.android.dialer.calllogutils;
import android.content.Context;
import android.provider.CallLog.Calls;
@@ -22,15 +22,13 @@ import android.text.BidiFormatter;
import android.text.TextDirectionHeuristics;
import android.text.TextUtils;
import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
-import com.android.dialer.app.R;
import com.android.dialer.phonenumberutil.PhoneNumberHelper;
/** Helper for formatting and managing the display of phone numbers. */
public class PhoneNumberDisplayUtil {
/** Returns the string to display for the given phone number if there is no matching contact. */
- /* package */
- static CharSequence getDisplayName(
+ public static CharSequence getDisplayName(
Context context, CharSequence number, int presentation, boolean isVoicemail) {
if (presentation == Calls.PRESENTATION_UNKNOWN) {
return context.getResources().getString(R.string.unknown);
@@ -42,7 +40,7 @@ public class PhoneNumberDisplayUtil {
return context.getResources().getString(R.string.payphone);
}
if (isVoicemail) {
- return context.getResources().getString(R.string.voicemail);
+ return context.getResources().getString(R.string.voicemail_string);
}
if (PhoneNumberHelper.isLegacyUnknownNumbers(number)) {
return context.getResources().getString(R.string.unknown);
diff --git a/java/com/android/dialer/calllogutils/res/values/colors.xml b/java/com/android/dialer/calllogutils/res/values/colors.xml
new file mode 100644
index 000000000..dc4ec2493
--- /dev/null
+++ b/java/com/android/dialer/calllogutils/res/values/colors.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources>
+ <!-- Color for missed call icons. -->
+ <color name="missed_call">#ff2e58</color>
+ <!-- Color for answered or outgoing call icons. -->
+ <color name="answered_call">#00c853</color>
+ <!-- Color for blocked call icons. -->
+ <color name="blocked_call">@color/dialer_secondary_text_color</color>
+</resources> \ No newline at end of file
diff --git a/java/com/android/dialer/calllogutils/res/values/dimens.xml b/java/com/android/dialer/calllogutils/res/values/dimens.xml
new file mode 100644
index 000000000..0935ac188
--- /dev/null
+++ b/java/com/android/dialer/calllogutils/res/values/dimens.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources>
+ <dimen name="call_type_icon_size">12dp</dimen>
+</resources> \ No newline at end of file
diff --git a/java/com/android/dialer/calllogutils/res/values/strings.xml b/java/com/android/dialer/calllogutils/res/values/strings.xml
new file mode 100644
index 000000000..6a6f10113
--- /dev/null
+++ b/java/com/android/dialer/calllogutils/res/values/strings.xml
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Title for incoming call type. [CHAR LIMIT=40] -->
+ <string name="type_incoming">Incoming call</string>
+
+ <!-- Title for incoming call which was transferred to another device. [CHAR LIMIT=60] -->
+ <string name="type_incoming_pulled">Incoming call transferred to another device</string>
+
+ <!-- Title for outgoing call type. [CHAR LIMIT=40] -->
+ <string name="type_outgoing">Outgoing call</string>
+
+ <!-- Title for outgoing call which was transferred to another device. [CHAR LIMIT=60] -->
+ <string name="type_outgoing_pulled">Outgoing call transferred to another device</string>
+
+ <!-- Title for missed call type. [CHAR LIMIT=40] -->
+ <string name="type_missed">Missed call</string>
+
+ <!-- Title for incoming video call in call details screen [CHAR LIMIT=60] -->
+ <string name="type_incoming_video">Incoming video call</string>
+
+ <!-- Title for incoming video call in call details screen which was transferred to another device.
+ [CHAR LIMIT=60] -->
+ <string name="type_incoming_video_pulled">Incoming video call transferred to another device</string>
+
+ <!-- Title for outgoing video call in call details screen [CHAR LIMIT=60] -->
+ <string name="type_outgoing_video">Outgoing video call</string>
+
+ <!-- Title for outgoing video call in call details screen which was transferred to another device.
+ [CHAR LIMIT=60] -->
+ <string name="type_outgoing_video_pulled">Outgoing video call transferred to another device</string>
+
+ <!-- Title for missed video call in call details screen [CHAR LIMIT=60] -->
+ <string name="type_missed_video">Missed video call</string>
+
+ <!-- Title for voicemail details screen -->
+ <string name="type_voicemail">Voicemail</string>
+
+ <!-- Title for rejected call type. [CHAR LIMIT=40] -->
+ <string name="type_rejected">Declined call</string>
+
+ <!-- Title for blocked call type. [CHAR LIMIT=40] -->
+ <string name="type_blocked">Blocked call</string>
+
+ <!-- Title for "answered elsewhere" call type. This will happen if a call was ringing
+ simultaneously on multiple devices, and the user answered it on a device other than the
+ current device. [CHAR LIMIT=60] -->
+ <string name="type_answered_elsewhere">Call answered on another device</string>
+
+ <!-- String describing the phone account the call was made on or to. This string will be used
+ in description_incoming_missed_call, description_incoming_answered_call, and
+ description_outgoing_call.
+ Note: AccessibilityServices uses this attribute to announce what the view represents.
+ [CHAR LIMIT=NONE] -->
+ <string name="description_phone_account">on <xliff:g example="SIM 1" id="phoneAccount">^1</xliff:g></string>
+
+ <!-- String describing the secondary line number the call was received via.
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ [CHAR LIMIT=NONE]-->
+ <string name="description_via_number">via <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g></string>
+
+ <!-- String describing the PhoneAccount and via number that a call was received on, if both are
+ visible.
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ [CHAR LIMIT=NONE]-->
+ <string name="description_via_number_phone_account">on <xliff:g example="SIM 1" id="phoneAccount">%1$s</xliff:g>, via <xliff:g example="(555) 555-5555" id="number">%2$s</xliff:g></string>
+
+ <!-- String used for displaying calls to the voicemail number in the call log -->
+ <string name="voicemail_string">Voicemail</string>
+
+ <!-- A nicely formatted call duration displayed when viewing call details. For example "42 min 28 sec" -->
+ <string name="call_details_duration_format"><xliff:g example="42" id="minutes">%s</xliff:g> min <xliff:g example="28" id="seconds">%s</xliff:g> sec</string>
+
+ <!-- A nicely formatted call duration displayed when viewing call details for duration less than 1 minute. For example "28 sec" -->
+ <string name="call_details_short_duration_format"><xliff:g example="28" id="seconds">%s</xliff:g> sec</string>
+</resources> \ No newline at end of file
diff --git a/java/com/android/dialer/common/Assert.java b/java/com/android/dialer/common/Assert.java
index 00b4f2595..943e1ddcf 100644
--- a/java/com/android/dialer/common/Assert.java
+++ b/java/com/android/dialer/common/Assert.java
@@ -19,6 +19,7 @@ package com.android.dialer.common;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import javax.annotation.CheckReturnValue;
/** Assertions which will result in program termination unless disabled by flags. */
public class Assert {
@@ -33,7 +34,9 @@ public class Assert {
* Called when a truly exceptional case occurs.
*
* @throws AssertionError
+ * @deprecated Use throw Assert.create*FailException() instead.
*/
+ @Deprecated
public static void fail() {
throw new AssertionError("Fail");
}
@@ -43,11 +46,38 @@ public class Assert {
*
* @param reason the optional reason to supply as the exception message
* @throws AssertionError
+ * @deprecated Use throw Assert.create*FailException() instead.
*/
+ @Deprecated
public static void fail(String reason) {
throw new AssertionError(reason);
}
+ @CheckReturnValue
+ public static AssertionError createAssertionFailException(String msg) {
+ return new AssertionError(msg);
+ }
+
+ @CheckReturnValue
+ public static UnsupportedOperationException createUnsupportedOperationFailException() {
+ return new UnsupportedOperationException();
+ }
+
+ @CheckReturnValue
+ public static UnsupportedOperationException createUnsupportedOperationFailException(String msg) {
+ return new UnsupportedOperationException(msg);
+ }
+
+ @CheckReturnValue
+ public static IllegalStateException createIllegalStateFailException() {
+ return new IllegalStateException();
+ }
+
+ @CheckReturnValue
+ public static IllegalStateException createIllegalStateFailException(String msg) {
+ return new IllegalStateException(msg);
+ }
+
/**
* Ensures the truth of an expression involving one or more parameters to the calling method.
*
diff --git a/java/com/android/dialer/common/AutoValue_FallibleAsyncTask_FallibleTaskResult.java b/java/com/android/dialer/common/AutoValue_FallibleAsyncTask_FallibleTaskResult.java
deleted file mode 100644
index f9d7cea90..000000000
--- a/java/com/android/dialer/common/AutoValue_FallibleAsyncTask_FallibleTaskResult.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.dialer.common;
-
-import android.support.annotation.Nullable;
-import javax.annotation.Generated;
-
-
- final class AutoValue_FallibleAsyncTask_FallibleTaskResult<ResultT> extends FallibleAsyncTask.FallibleTaskResult<ResultT> {
-
- private final Throwable throwable;
- private final ResultT result;
-
- AutoValue_FallibleAsyncTask_FallibleTaskResult(
- @Nullable Throwable throwable,
- @Nullable ResultT result) {
- this.throwable = throwable;
- this.result = result;
- }
-
- @Nullable
- @Override
- public Throwable getThrowable() {
- return throwable;
- }
-
- @Nullable
- @Override
- public ResultT getResult() {
- return result;
- }
-
- @Override
- public String toString() {
- return "FallibleTaskResult{"
- + "throwable=" + throwable + ", "
- + "result=" + result
- + "}";
- }
-
- @Override
- public boolean equals(Object o) {
- if (o == this) {
- return true;
- }
- if (o instanceof FallibleAsyncTask.FallibleTaskResult) {
- FallibleAsyncTask.FallibleTaskResult<?> that = (FallibleAsyncTask.FallibleTaskResult<?>) o;
- return ((this.throwable == null) ? (that.getThrowable() == null) : this.throwable.equals(that.getThrowable()))
- && ((this.result == null) ? (that.getResult() == null) : this.result.equals(that.getResult()));
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- int h = 1;
- h *= 1000003;
- h ^= (throwable == null) ? 0 : this.throwable.hashCode();
- h *= 1000003;
- h ^= (result == null) ? 0 : this.result.hashCode();
- return h;
- }
-
-}
-
diff --git a/java/com/android/dialer/common/FallibleAsyncTask.java b/java/com/android/dialer/common/FallibleAsyncTask.java
index fbdbda75f..f3abace1a 100644
--- a/java/com/android/dialer/common/FallibleAsyncTask.java
+++ b/java/com/android/dialer/common/FallibleAsyncTask.java
@@ -20,7 +20,7 @@ import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.android.dialer.common.FallibleAsyncTask.FallibleTaskResult;
-
+import com.google.auto.value.AutoValue;
/**
* A task that runs work in the background, passing Throwables from {@link
@@ -52,8 +52,8 @@ public abstract class FallibleAsyncTask<ParamsT, ProgressT, ResultT>
*
* @param <ResultT> the type of the result of the background computation
*/
-
- protected abstract static class FallibleTaskResult<ResultT> {
+ @AutoValue
+ public abstract static class FallibleTaskResult<ResultT> {
/** Creates an instance of FallibleTaskResult for the given throwable. */
private static <ResultT> FallibleTaskResult<ResultT> createFailureResult(@NonNull Throwable t) {
diff --git a/java/com/android/dialer/common/PerAccountSharedPreferences.java b/java/com/android/dialer/common/PerAccountSharedPreferences.java
new file mode 100644
index 000000000..0ed1b03a5
--- /dev/null
+++ b/java/com/android/dialer/common/PerAccountSharedPreferences.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.dialer.common;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import java.util.Set;
+
+/**
+ * Class that helps us store dialer preferences that are phone account dependent. This is necessary
+ * for cases such as settings that are phone account dependent e.g endless vm. The logic is to
+ * essentially store the shared preference by appending the phone account id to the key.
+ */
+public class PerAccountSharedPreferences {
+ private final String sharedPrefsKeyPrefix;
+ private final SharedPreferences preferences;
+ private final PhoneAccountHandle phoneAccountHandle;
+
+ public PerAccountSharedPreferences(
+ Context context, PhoneAccountHandle handle, SharedPreferences prefs) {
+ preferences = prefs;
+ phoneAccountHandle = handle;
+ sharedPrefsKeyPrefix = "phone_account_dependent_";
+ }
+
+ /**
+ * Not to be used, currently only used by {@VisualVoicemailPreferences} for legacy reasons.
+ */
+ protected PerAccountSharedPreferences(
+ Context context, PhoneAccountHandle handle, SharedPreferences prefs, String prefix) {
+ Assert.checkArgument(prefix.equals("visual_voicemail_"));
+ preferences = prefs;
+ phoneAccountHandle = handle;
+ sharedPrefsKeyPrefix = prefix;
+ }
+
+ public class Editor {
+
+ private final SharedPreferences.Editor editor;
+
+ private Editor() {
+ editor = preferences.edit();
+ }
+
+ public void apply() {
+ editor.apply();
+ }
+
+ public Editor putBoolean(String key, boolean value) {
+ editor.putBoolean(getKey(key), value);
+ return this;
+ }
+
+ public Editor putFloat(String key, float value) {
+ editor.putFloat(getKey(key), value);
+ return this;
+ }
+
+ public Editor putInt(String key, int value) {
+ editor.putInt(getKey(key), value);
+ return this;
+ }
+
+ public Editor putLong(String key, long value) {
+ editor.putLong(getKey(key), value);
+ return this;
+ }
+
+ public Editor putString(String key, String value) {
+ editor.putString(getKey(key), value);
+ return this;
+ }
+
+ public Editor putStringSet(String key, Set<String> value) {
+ editor.putStringSet(getKey(key), value);
+ return this;
+ }
+ }
+
+ public Editor edit() {
+ return new Editor();
+ }
+
+ public boolean getBoolean(String key, boolean defValue) {
+ return getValue(key, defValue);
+ }
+
+ public float getFloat(String key, float defValue) {
+ return getValue(key, defValue);
+ }
+
+ public int getInt(String key, int defValue) {
+ return getValue(key, defValue);
+ }
+
+ public long getLong(String key, long defValue) {
+ return getValue(key, defValue);
+ }
+
+ public String getString(String key, String defValue) {
+ return getValue(key, defValue);
+ }
+
+ @Nullable
+ public String getString(String key) {
+ return getValue(key, null);
+ }
+
+ public Set<String> getStringSet(String key, Set<String> defValue) {
+ return getValue(key, defValue);
+ }
+
+ public boolean contains(String key) {
+ return preferences.contains(getKey(key));
+ }
+
+ private <T> T getValue(String key, T defValue) {
+ if (!contains(key)) {
+ return defValue;
+ }
+ Object object = preferences.getAll().get(getKey(key));
+ if (object == null) {
+ return defValue;
+ }
+ return (T) object;
+ }
+
+ private String getKey(String key) {
+ return sharedPrefsKeyPrefix + key + "_" + phoneAccountHandle.getId();
+ }
+}
diff --git a/java/com/android/dialer/common/proguard.flags b/java/com/android/dialer/common/proguard.flags
new file mode 100644
index 000000000..4b6b84671
--- /dev/null
+++ b/java/com/android/dialer/common/proguard.flags
@@ -0,0 +1,4 @@
+-assumenosideeffects class com.android.dialer.common.LogUtil {
+ public static void v(...);
+ public static void d(...);
+}
diff --git a/java/com/android/dialer/common/res/values/config.xml b/java/com/android/dialer/common/res/values/config.xml
new file mode 100644
index 000000000..c4df279ba
--- /dev/null
+++ b/java/com/android/dialer/common/res/values/config.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <bool name="spring_hd_codec">false</bool>
+</resources> \ No newline at end of file
diff --git a/java/com/android/dialer/constants/Constants.java b/java/com/android/dialer/constants/Constants.java
index 77773018a..d92c0bcfc 100644
--- a/java/com/android/dialer/constants/Constants.java
+++ b/java/com/android/dialer/constants/Constants.java
@@ -19,7 +19,6 @@ package com.android.dialer.constants;
import android.support.annotation.NonNull;
import com.android.dialer.common.Assert;
import com.android.dialer.proguard.UsedByReflection;
-import com.android.dialer.constants.ConstantsImpl;
/**
* Utility to access constants that are different across build variants (Google Dialer, AOSP,
@@ -29,11 +28,22 @@ import com.android.dialer.constants.ConstantsImpl;
*/
@UsedByReflection(value = "Constants.java")
public abstract class Constants {
- private static Constants instance = new ConstantsImpl();
+ private static Constants instance;
private static boolean didInitializeInstance;
@NonNull
public static synchronized Constants get() {
+ if (!didInitializeInstance) {
+ didInitializeInstance = true;
+ try {
+ Class<?> clazz = Class.forName(Constants.class.getName() + "Impl");
+ instance = (Constants) clazz.getConstructor().newInstance();
+ } catch (ReflectiveOperationException e) {
+ Assert.fail(
+ "Unable to create an instance of ConstantsImpl. To fix this error include one of the "
+ + "constants modules (googledialer, aosp etc...) in your target.");
+ }
+ }
return instance;
}
diff --git a/java/com/android/dialer/database/CallLogQueryHandler.java b/java/com/android/dialer/database/CallLogQueryHandler.java
index ffca69f40..1f6bd5fb3 100644
--- a/java/com/android/dialer/database/CallLogQueryHandler.java
+++ b/java/com/android/dialer/database/CallLogQueryHandler.java
@@ -33,6 +33,7 @@ import android.os.Message;
import android.provider.CallLog.Calls;
import android.provider.VoicemailContract.Status;
import android.provider.VoicemailContract.Voicemails;
+import android.support.v4.os.BuildCompat;
import com.android.contacts.common.database.NoNullCursorAsyncQueryHandler;
import com.android.dialer.common.LogUtil;
import com.android.dialer.compat.AppCompatConstants;
@@ -40,6 +41,7 @@ import com.android.dialer.compat.SdkVersionOverride;
import com.android.dialer.phonenumbercache.CallLogQuery;
import com.android.dialer.telecom.TelecomUtil;
import com.android.dialer.util.PermissionsUtil;
+import com.android.voicemail.VoicemailComponent;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
@@ -126,13 +128,23 @@ public class CallLogQueryHandler extends NoNullCursorAsyncQueryHandler {
public void fetchVoicemailUnreadCount() {
if (TelecomUtil.hasReadWriteVoicemailPermissions(mContext)) {
// Only count voicemails that have not been read and have not been deleted.
+ StringBuilder where =
+ new StringBuilder(Voicemails.IS_READ + "=0" + " AND " + Voicemails.DELETED + "=0 ");
+ List<String> selectionArgs = new ArrayList<>();
+
+ if (BuildCompat.isAtLeastO()) {
+ VoicemailComponent.get(mContext)
+ .getVoicemailClient()
+ .appendOmtpVoicemailSelectionClause(mContext, where, selectionArgs);
+ }
+
startQuery(
QUERY_VOICEMAIL_UNREAD_COUNT_TOKEN,
null,
Voicemails.CONTENT_URI,
new String[] {Voicemails._ID},
- Voicemails.IS_READ + "=0" + " AND " + Voicemails.DELETED + "=0",
- null,
+ where.toString(),
+ selectionArgs.toArray(new String[selectionArgs.size()]),
null);
}
}
@@ -168,6 +180,12 @@ public class CallLogQueryHandler extends NoNullCursorAsyncQueryHandler {
selectionArgs.add(Long.toString(newerThan));
}
+ if (callType == Calls.VOICEMAIL_TYPE) {
+ VoicemailComponent.get(mContext)
+ .getVoicemailClient()
+ .appendOmtpVoicemailSelectionClause(mContext, where, selectionArgs);
+ }
+
final int limit = (mLogLimit == -1) ? NUM_LOGS_TO_DISPLAY : mLogLimit;
final String selection = where.length() > 0 ? where.toString() : null;
Uri uri =
diff --git a/java/com/android/incallui/maps/StaticMapFactory.java b/java/com/android/dialer/debug/bindings/stub/DebugBindings.java
index a35013886..7df38341d 100644
--- a/java/com/android/incallui/maps/StaticMapFactory.java
+++ b/java/com/android/dialer/debug/bindings/stub/DebugBindings.java
@@ -14,15 +14,14 @@
* limitations under the License
*/
-package com.android.incallui.maps;
+package com.android.dialer.debug.bindings;
-import android.location.Location;
-import android.support.annotation.NonNull;
-import android.support.v4.app.Fragment;
+import android.content.Context;
-/** A Factory that can create Fragments for showing a static map */
-public interface StaticMapFactory {
+/** Hooks into the debug module. */
+public class DebugBindings {
- @NonNull
- Fragment getStaticMap(@NonNull Location location);
+ public static void registerConnectionService(Context context) {}
+
+ public static void addNewIncomingCall(Context context, String phoneNumber) {}
}
diff --git a/java/com/android/dialer/enrichedcall/AutoValue_EnrichedCallCapabilities.java b/java/com/android/dialer/enrichedcall/AutoValue_EnrichedCallCapabilities.java
deleted file mode 100644
index 14299f92c..000000000
--- a/java/com/android/dialer/enrichedcall/AutoValue_EnrichedCallCapabilities.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.dialer.enrichedcall;
-
-import javax.annotation.Generated;
-
-@Generated("com.google.auto.value.processor.AutoValueProcessor")
- final class AutoValue_EnrichedCallCapabilities extends EnrichedCallCapabilities {
-
- private final boolean supportsCallComposer;
- private final boolean supportsPostCall;
-
- AutoValue_EnrichedCallCapabilities(
- boolean supportsCallComposer,
- boolean supportsPostCall) {
- this.supportsCallComposer = supportsCallComposer;
- this.supportsPostCall = supportsPostCall;
- }
-
- @Override
- public boolean supportsCallComposer() {
- return supportsCallComposer;
- }
-
- @Override
- public boolean supportsPostCall() {
- return supportsPostCall;
- }
-
- @Override
- public String toString() {
- return "EnrichedCallCapabilities{"
- + "supportsCallComposer=" + supportsCallComposer + ", "
- + "supportsPostCall=" + supportsPostCall + ", "
- + "}";
- }
-
- @Override
- public boolean equals(Object o) {
- if (o == this) {
- return true;
- }
- if (o instanceof EnrichedCallCapabilities) {
- EnrichedCallCapabilities that = (EnrichedCallCapabilities) o;
- return (this.supportsCallComposer == that.supportsCallComposer())
- && (this.supportsPostCall == that.supportsPostCall());
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- int h = 1;
- h *= 1000003;
- h ^= this.supportsCallComposer ? 1231 : 1237;
- h *= 1000003;
- h ^= this.supportsPostCall ? 1231 : 1237;
- return h;
- }
-
-}
-
diff --git a/java/com/android/dialer/enrichedcall/AutoValue_OutgoingCallComposerData.java b/java/com/android/dialer/enrichedcall/AutoValue_OutgoingCallComposerData.java
deleted file mode 100644
index edfefc479..000000000
--- a/java/com/android/dialer/enrichedcall/AutoValue_OutgoingCallComposerData.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.dialer.enrichedcall;
-
-import android.net.Uri;
-import android.support.annotation.Nullable;
-import javax.annotation.Generated;
-
-@Generated("com.google.auto.value.processor.AutoValueProcessor")
- final class AutoValue_OutgoingCallComposerData extends OutgoingCallComposerData {
-
- private final String subject;
- private final Uri imageUri;
- private final String imageContentType;
-
- private AutoValue_OutgoingCallComposerData(
- @Nullable String subject,
- @Nullable Uri imageUri,
- @Nullable String imageContentType) {
- this.subject = subject;
- this.imageUri = imageUri;
- this.imageContentType = imageContentType;
- }
-
- @Nullable
- @Override
- public String getSubject() {
- return subject;
- }
-
- @Nullable
- @Override
- public Uri getImageUri() {
- return imageUri;
- }
-
- @Nullable
- @Override
- public String getImageContentType() {
- return imageContentType;
- }
-
- @Override
- public String toString() {
- return "OutgoingCallComposerData{"
- + "subject=" + subject + ", "
- + "imageUri=" + imageUri + ", "
- + "imageContentType=" + imageContentType
- + "}";
- }
-
- @Override
- public boolean equals(Object o) {
- if (o == this) {
- return true;
- }
- if (o instanceof OutgoingCallComposerData) {
- OutgoingCallComposerData that = (OutgoingCallComposerData) o;
- return ((this.subject == null) ? (that.getSubject() == null) : this.subject.equals(that.getSubject()))
- && ((this.imageUri == null) ? (that.getImageUri() == null) : this.imageUri.equals(that.getImageUri()))
- && ((this.imageContentType == null) ? (that.getImageContentType() == null) : this.imageContentType.equals(that.getImageContentType()));
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- int h = 1;
- h *= 1000003;
- h ^= (subject == null) ? 0 : this.subject.hashCode();
- h *= 1000003;
- h ^= (imageUri == null) ? 0 : this.imageUri.hashCode();
- h *= 1000003;
- h ^= (imageContentType == null) ? 0 : this.imageContentType.hashCode();
- return h;
- }
-
- static final class Builder extends OutgoingCallComposerData.Builder {
- private String subject;
- private Uri imageUri;
- private String imageContentType;
- Builder() {
- }
- private Builder(OutgoingCallComposerData source) {
- this.subject = source.getSubject();
- this.imageUri = source.getImageUri();
- this.imageContentType = source.getImageContentType();
- }
- @Override
- public OutgoingCallComposerData.Builder setSubject(@Nullable String subject) {
- this.subject = subject;
- return this;
- }
- @Override
- OutgoingCallComposerData.Builder setImageUri(@Nullable Uri imageUri) {
- this.imageUri = imageUri;
- return this;
- }
- @Override
- OutgoingCallComposerData.Builder setImageContentType(@Nullable String imageContentType) {
- this.imageContentType = imageContentType;
- return this;
- }
- @Override
- OutgoingCallComposerData autoBuild() {
- return new AutoValue_OutgoingCallComposerData(
- this.subject,
- this.imageUri,
- this.imageContentType);
- }
- }
-
-}
diff --git a/java/com/android/dialer/enrichedcall/EnrichedCallCapabilities.java b/java/com/android/dialer/enrichedcall/EnrichedCallCapabilities.java
index b7d780950..c3c78c9c8 100644
--- a/java/com/android/dialer/enrichedcall/EnrichedCallCapabilities.java
+++ b/java/com/android/dialer/enrichedcall/EnrichedCallCapabilities.java
@@ -16,21 +16,24 @@
package com.android.dialer.enrichedcall;
-
+import com.google.auto.value.AutoValue;
/** Value type holding enriched call capabilities. */
-
+@AutoValue
public abstract class EnrichedCallCapabilities {
public static final EnrichedCallCapabilities NO_CAPABILITIES =
- EnrichedCallCapabilities.create(false, false);
+ EnrichedCallCapabilities.create(false, false, false);
public static EnrichedCallCapabilities create(
- boolean supportsCallComposer, boolean supportsPostCall) {
- return new AutoValue_EnrichedCallCapabilities(supportsCallComposer, supportsPostCall);
+ boolean supportsCallComposer, boolean supportsPostCall, boolean supportsVideoCall) {
+ return new AutoValue_EnrichedCallCapabilities(
+ supportsCallComposer, supportsPostCall, supportsVideoCall);
}
public abstract boolean supportsCallComposer();
public abstract boolean supportsPostCall();
+
+ public abstract boolean supportsVideoShare();
}
diff --git a/java/com/android/dialer/enrichedcall/EnrichedCallComponent.java b/java/com/android/dialer/enrichedcall/EnrichedCallComponent.java
new file mode 100644
index 000000000..5291e292f
--- /dev/null
+++ b/java/com/android/dialer/enrichedcall/EnrichedCallComponent.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.enrichedcall;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import dagger.Subcomponent;
+import com.android.dialer.enrichedcall.stub.EnrichedCallManagerStub;
+
+/** Subcomponent that can be used to access the enriched call implementation. */
+public class EnrichedCallComponent {
+ private static EnrichedCallComponent instance;
+ private EnrichedCallManager enrichedCallManager;
+
+ @NonNull
+ public EnrichedCallManager getEnrichedCallManager() {
+ if (enrichedCallManager == null) {
+ enrichedCallManager = new EnrichedCallManagerStub();
+ }
+ return enrichedCallManager;
+ }
+
+ public static EnrichedCallComponent get(Context context) {
+ if (instance == null) {
+ instance = new EnrichedCallComponent();
+ }
+ return instance;
+ }
+
+ /** Used to refer to the root application component. */
+ public interface HasComponent {
+ EnrichedCallComponent enrichedCallComponent();
+ }
+}
diff --git a/java/com/android/dialer/enrichedcall/EnrichedCallManager.java b/java/com/android/dialer/enrichedcall/EnrichedCallManager.java
index 6af8c409a..a36b2cc0d 100644
--- a/java/com/android/dialer/enrichedcall/EnrichedCallManager.java
+++ b/java/com/android/dialer/enrichedcall/EnrichedCallManager.java
@@ -16,38 +16,25 @@
package com.android.dialer.enrichedcall;
-import android.app.Application;
import android.support.annotation.IntDef;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
-import com.android.dialer.common.Assert;
+import android.support.annotation.WorkerThread;
+import com.android.dialer.calldetails.nano.CallDetailsEntries;
+import com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry;
+import com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult;
+import com.android.dialer.enrichedcall.videoshare.VideoShareListener;
import com.android.dialer.multimedia.MultimediaData;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.Map;
/** Performs all enriched calling logic. */
public interface EnrichedCallManager {
- /** Factory for {@link EnrichedCallManager}. */
- interface Factory {
- EnrichedCallManager getEnrichedCallManager();
- }
-
- /** Accessor for {@link EnrichedCallManager}. */
- class Accessor {
-
- /**
- * @throws IllegalArgumentException if application does not implement {@link
- * EnrichedCallManager.Factory}
- */
- @NonNull
- public static EnrichedCallManager getInstance(@NonNull Application application) {
- Assert.isNotNull(application);
-
- return ((EnrichedCallManager.Factory) application).getEnrichedCallManager();
- }
- }
+ int POST_CALL_NOTE_MAX_CHAR = 60;
/** Receives updates when enriched call capabilities are ready. */
interface CapabilitiesListener {
@@ -148,6 +135,15 @@ public interface EnrichedCallManager {
void endCallComposerSession(long sessionId);
/**
+ * Sends a post call note to the given number.
+ *
+ * @throws IllegalArgumentException if message is longer than {@link #POST_CALL_NOTE_MAX_CHAR}
+ * characters
+ */
+ @MainThread
+ void sendPostCallNote(@NonNull String number, @NonNull String message);
+
+ /**
* Called once the capabilities are available for a corresponding call to {@link
* #requestCapabilities(String)}.
*
@@ -162,8 +158,8 @@ public interface EnrichedCallManager {
interface StateChangedListener {
/**
- * Callback fired when state changes. Listeners should call {@link #getSession(String)} to
- * retrieve the new state.
+ * Callback fired when state changes. Listeners should call {@link #getSession(long)} or {@link
+ * #getSession(String, String)} to retrieve the new state.
*/
void onEnrichedCallStateChanged();
}
@@ -177,10 +173,10 @@ public interface EnrichedCallManager {
@MainThread
void registerStateChangedListener(@NonNull StateChangedListener listener);
- /** Returns the {@link Session} for the given number, or {@code null} if no session exists. */
+ /** Returns the {@link Session} for the given unique call id, falling back to the number. */
@MainThread
@Nullable
- Session getSession(@NonNull String number);
+ Session getSession(@NonNull String uniqueCallId, @NonNull String number);
/** Returns the {@link Session} for the given sessionId, or {@code null} if no session exists. */
@MainThread
@@ -188,6 +184,18 @@ public interface EnrichedCallManager {
Session getSession(long sessionId);
/**
+ * Returns a mapping of enriched call data for all of the given {@link CallDetailsEntries}.
+ *
+ * <p>The mapping is created by finding the HistoryResults whose timestamps occurred during or
+ * close after a CallDetailsEntry. A CallDetailsEntry can have multiple HistoryResults in the
+ * event that both a CallComposer message and PostCall message were sent for the same call.
+ */
+ @WorkerThread
+ @NonNull
+ Map<CallDetailsEntry, List<HistoryResult>> getAllHistoricalData(
+ @NonNull String number, @NonNull CallDetailsEntries entries);
+
+ /**
* Unregisters the given {@link StateChangedListener}.
*
* <p>As a result of this method, the listener will not receive updates when the state of enriched
@@ -222,4 +230,77 @@ public interface EnrichedCallManager {
*/
@MainThread
void onIncomingCallComposerData(long sessionId, @NonNull MultimediaData multimediaData);
+
+ /**
+ * Called when post call data arrives for the given session.
+ *
+ * @throws IllegalStateException if there's no session for the given id
+ */
+ @MainThread
+ void onIncomingPostCallData(long sessionId, @NonNull MultimediaData multimediaData);
+
+ /**
+ * Registers the given {@link VideoShareListener}.
+ *
+ * <p>As a result of this method, the listener will receive updates when any video share state
+ * changes.
+ */
+ @MainThread
+ void registerVideoShareListener(@NonNull VideoShareListener listener);
+
+ /**
+ * Unregisters the given {@link VideoShareListener}.
+ *
+ * <p>As a result of this method, the listener will not receive updates when any video share state
+ * changes.
+ */
+ @MainThread
+ void unregisterVideoShareListener(@NonNull VideoShareListener listener);
+
+ /** Called when an incoming video share invite is received. */
+ @MainThread
+ void onIncomingVideoShareInvite(long sessionId, @NonNull String number);
+
+ /**
+ * Starts a video share session with the given remote number.
+ *
+ * @param number the remote number in any format
+ * @return the id for the started session, or {@link Session#NO_SESSION_ID} if the session fails
+ */
+ @MainThread
+ long startVideoShareSession(@NonNull String number);
+
+ /**
+ * Accepts a video share session invite.
+ *
+ * @param sessionId the session to accept
+ * @return whether or not accepting the session succeeded
+ */
+ @MainThread
+ boolean acceptVideoShareSession(long sessionId);
+
+ /**
+ * Retrieve the session id for an incoming video share invite.
+ *
+ * @param number the remote number in any format
+ * @return the id for the session invite, or {@link Session#NO_SESSION_ID} if there is no invite
+ */
+ @MainThread
+ long getVideoShareInviteSessionId(@NonNull String number);
+
+ /**
+ * Ends the given video share session.
+ *
+ * @param sessionId the id of the session to end
+ */
+ @MainThread
+ void endVideoShareSession(long sessionId);
+
+ /**
+ * Returns the {@link VideoShareSession} for the given sessionId, or {@code null} if no session
+ * exists.
+ */
+ @MainThread
+ @Nullable
+ VideoShareSession getVideoShareSession(long sessionId);
}
diff --git a/java/com/android/dialer/enrichedcall/FuzzyPhoneNumberMatcher.java b/java/com/android/dialer/enrichedcall/FuzzyPhoneNumberMatcher.java
new file mode 100644
index 000000000..f589f94a6
--- /dev/null
+++ b/java/com/android/dialer/enrichedcall/FuzzyPhoneNumberMatcher.java
@@ -0,0 +1,20 @@
+package com.android.dialer.enrichedcall;
+
+import android.support.annotation.NonNull;
+import com.android.dialer.common.Assert;
+
+/** Utility for comparing phone numbers. */
+public class FuzzyPhoneNumberMatcher {
+
+ /** Returns {@code true} if the given numbers can be interpreted to be the same. */
+ public static boolean matches(@NonNull String a, @NonNull String b) {
+ String aNormalized = Assert.isNotNull(a).replaceAll("[^0-9]", "");
+ String bNormalized = Assert.isNotNull(b).replaceAll("[^0-9]", "");
+ if (aNormalized.length() < 7 || bNormalized.length() < 7) {
+ return false;
+ }
+ String aMatchable = aNormalized.substring(aNormalized.length() - 7);
+ String bMatchable = bNormalized.substring(bNormalized.length() - 7);
+ return aMatchable.equals(bMatchable);
+ }
+}
diff --git a/java/com/android/dialer/enrichedcall/OutgoingCallComposerData.java b/java/com/android/dialer/enrichedcall/OutgoingCallComposerData.java
index a8ee49d4e..56145ddd4 100644
--- a/java/com/android/dialer/enrichedcall/OutgoingCallComposerData.java
+++ b/java/com/android/dialer/enrichedcall/OutgoingCallComposerData.java
@@ -20,7 +20,7 @@ import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.android.dialer.common.Assert;
-
+import com.google.auto.value.AutoValue;
/**
* Value type holding references to all data that could be provided for the call composer.
@@ -29,19 +29,19 @@ import com.android.dialer.common.Assert;
*
* <pre>
* OutgoingCallComposerData.builder.build(); // throws exception, no data set
- * OutgoingCallComposerData
- * .setSubject(subject)
+ * OutgoingCallComposerData.builder
+ * .setText(subject)
* .build(); // Success
- * OutgoingCallComposerData
+ * OutgoingCallComposerData.builder
* .setImageData(uri, contentType)
* .build(); // Success
- * OutgoingCallComposerData
- * .setSubject(subject)
+ * OutgoingCallComposerData.builder
+ * .setText(subject)
* .setImageData(uri, contentType)
* .build(); // Success
* </pre>
*/
-
+@AutoValue
public abstract class OutgoingCallComposerData {
public static Builder builder() {
@@ -62,7 +62,7 @@ public abstract class OutgoingCallComposerData {
public abstract String getImageContentType();
/** Builds instances of {@link OutgoingCallComposerData}. */
-
+ @AutoValue.Builder
public abstract static class Builder {
public abstract Builder setSubject(String subject);
diff --git a/java/com/android/dialer/enrichedcall/Session.java b/java/com/android/dialer/enrichedcall/Session.java
index b0439fae9..b3f291438 100644
--- a/java/com/android/dialer/enrichedcall/Session.java
+++ b/java/com/android/dialer/enrichedcall/Session.java
@@ -17,6 +17,7 @@
package com.android.dialer.enrichedcall;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import com.android.dialer.enrichedcall.EnrichedCallManager.State;
import com.android.dialer.multimedia.MultimediaData;
@@ -38,6 +39,12 @@ public interface Session {
*/
long getSessionId();
+ /** Returns the id of the dialer call associated with this session, or null if there isn't one. */
+ @Nullable
+ String getUniqueDialerCallId();
+
+ void setUniqueDialerCallId(@NonNull String id);
+
/** Returns the number associated with the remote end of this session. */
@NonNull
String getRemoteNumber();
diff --git a/java/com/android/dialer/enrichedcall/VideoShareSession.java b/java/com/android/dialer/enrichedcall/VideoShareSession.java
new file mode 100644
index 000000000..07bc4ed09
--- /dev/null
+++ b/java/com/android/dialer/enrichedcall/VideoShareSession.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.enrichedcall;
+
+/** Holds state information and data about video share sessions. */
+public interface VideoShareSession {}
diff --git a/java/com/android/dialer/enrichedcall/historyquery/HistoryQuery.java b/java/com/android/dialer/enrichedcall/historyquery/HistoryQuery.java
new file mode 100644
index 000000000..b7593cebb
--- /dev/null
+++ b/java/com/android/dialer/enrichedcall/historyquery/HistoryQuery.java
@@ -0,0 +1,31 @@
+package com.android.dialer.enrichedcall.historyquery;
+
+import android.support.annotation.NonNull;
+import com.android.dialer.common.LogUtil;
+import com.google.auto.value.AutoValue;
+
+/**
+ * Data object representing the pieces of information required to query for historical enriched call
+ * data.
+ */
+@AutoValue
+public abstract class HistoryQuery {
+
+ @NonNull
+ public static HistoryQuery create(@NonNull String number, long callStartTime, long callEndTime) {
+ return new AutoValue_HistoryQuery(number, callStartTime, callEndTime);
+ }
+
+ public abstract String getNumber();
+
+ public abstract long getCallStartTimestamp();
+
+ public abstract long getCallEndTimestamp();
+
+ @Override
+ public String toString() {
+ return String.format(
+ "HistoryQuery{number: %s, callStartTimestamp: %d, callEndTimestamp: %d}",
+ LogUtil.sanitizePhoneNumber(getNumber()), getCallStartTimestamp(), getCallEndTimestamp());
+ }
+}
diff --git a/java/com/android/dialer/enrichedcall/historyquery/nano/HistoryResult.java b/java/com/android/dialer/enrichedcall/historyquery/nano/HistoryResult.java
new file mode 100644
index 000000000..2fdc2da50
--- /dev/null
+++ b/java/com/android/dialer/enrichedcall/historyquery/nano/HistoryResult.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+
+package com.android.dialer.enrichedcall.historyquery.proto.nano;
+
+/** This file is autogenerated, but javadoc required. */
+@SuppressWarnings("hiding")
+public final class HistoryResult
+ extends com.google.protobuf.nano.ExtendableMessageNano<HistoryResult> {
+
+ /** This file is autogenerated, but javadoc required. */
+ // enum Type
+ public interface Type {
+ public static final int INCOMING_CALL_COMPOSER = 1;
+ public static final int OUTGOING_CALL_COMPOSER = 2;
+ public static final int INCOMING_POST_CALL = 3;
+ public static final int OUTGOING_POST_CALL = 4;
+ }
+
+ private static volatile HistoryResult[] _emptyArray;
+
+ public static HistoryResult[] emptyArray() {
+ // Lazily initializes the empty array
+ if (_emptyArray == null) {
+ synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+ if (_emptyArray == null) {
+ _emptyArray = new HistoryResult[0];
+ }
+ }
+ }
+ return _emptyArray;
+ }
+
+ // optional .com.android.dialer.enrichedcall.historyquery.proto.HistoryResult.Type type = 1;
+ public int type;
+
+ // optional string text = 2;
+ public java.lang.String text;
+
+ // optional string image_uri = 4;
+ public java.lang.String imageUri;
+
+ // optional string image_content_type = 5;
+ public java.lang.String imageContentType;
+
+ // optional int64 timestamp = 7;
+ public long timestamp;
+
+ // @@protoc_insertion_point(class_scope:com.android.dialer.enrichedcall.historyquery.proto.HistoryResult)
+
+ public HistoryResult() {
+ clear();
+ }
+
+ public HistoryResult clear() {
+ type =
+ com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult.Type
+ .INCOMING_CALL_COMPOSER;
+ text = "";
+ imageUri = "";
+ imageContentType = "";
+ timestamp = 0L;
+ unknownFieldData = null;
+ cachedSize = -1;
+ return this;
+ }
+
+ @Override
+ public void writeTo(com.google.protobuf.nano.CodedOutputByteBufferNano output)
+ throws java.io.IOException {
+ if (this.type
+ != com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult.Type
+ .INCOMING_CALL_COMPOSER) {
+ output.writeInt32(1, this.type);
+ }
+ if (this.text != null && !this.text.equals("")) {
+ output.writeString(2, this.text);
+ }
+ if (this.imageUri != null && !this.imageUri.equals("")) {
+ output.writeString(4, this.imageUri);
+ }
+ if (this.imageContentType != null && !this.imageContentType.equals("")) {
+ output.writeString(5, this.imageContentType);
+ }
+ if (this.timestamp != 0L) {
+ output.writeInt64(7, this.timestamp);
+ }
+ super.writeTo(output);
+ }
+
+ @Override
+ protected int computeSerializedSize() {
+ int size = super.computeSerializedSize();
+ if (this.type
+ != com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult.Type
+ .INCOMING_CALL_COMPOSER) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeInt32Size(1, this.type);
+ }
+ if (this.text != null && !this.text.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(2, this.text);
+ }
+ if (this.imageUri != null && !this.imageUri.equals("")) {
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(4, this.imageUri);
+ }
+ if (this.imageContentType != null && !this.imageContentType.equals("")) {
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(
+ 5, this.imageContentType);
+ }
+ if (this.timestamp != 0L) {
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeInt64Size(7, this.timestamp);
+ }
+ return size;
+ }
+
+ @Override
+ public HistoryResult mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ while (true) {
+ int tag = input.readTag();
+ switch (tag) {
+ case 0:
+ return this;
+ default:
+ {
+ if (!super.storeUnknownField(input, tag)) {
+ return this;
+ }
+ break;
+ }
+ case 8:
+ {
+ int initialPos = input.getPosition();
+ int value = input.readInt32();
+ switch (value) {
+ case com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult.Type
+ .INCOMING_CALL_COMPOSER:
+ case com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult.Type
+ .OUTGOING_CALL_COMPOSER:
+ case com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult.Type
+ .INCOMING_POST_CALL:
+ case com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult.Type
+ .OUTGOING_POST_CALL:
+ this.type = value;
+ break;
+ default:
+ input.rewindToPosition(initialPos);
+ storeUnknownField(input, tag);
+ break;
+ }
+ break;
+ }
+ case 18:
+ {
+ this.text = input.readString();
+ break;
+ }
+ case 34:
+ {
+ this.imageUri = input.readString();
+ break;
+ }
+ case 42:
+ {
+ this.imageContentType = input.readString();
+ break;
+ }
+ case 56:
+ {
+ this.timestamp = input.readInt64();
+ break;
+ }
+ }
+ }
+ }
+
+ public static HistoryResult parseFrom(byte[] data)
+ throws com.google.protobuf.nano.InvalidProtocolBufferNanoException {
+ return com.google.protobuf.nano.MessageNano.mergeFrom(new HistoryResult(), data);
+ }
+
+ public static HistoryResult parseFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ return new HistoryResult().mergeFrom(input);
+ }
+}
diff --git a/java/com/android/dialer/enrichedcall/EnrichedCallManagerStub.java b/java/com/android/dialer/enrichedcall/stub/EnrichedCallManagerStub.java
index db9a799d3..01d1f2aac 100644
--- a/java/com/android/dialer/enrichedcall/EnrichedCallManagerStub.java
+++ b/java/com/android/dialer/enrichedcall/stub/EnrichedCallManagerStub.java
@@ -14,11 +14,24 @@
* limitations under the License
*/
-package com.android.dialer.enrichedcall;
+package com.android.dialer.enrichedcall.stub;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.annotation.WorkerThread;
+import android.util.ArrayMap;
+import com.android.dialer.calldetails.nano.CallDetailsEntries;
+import com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry;
+import com.android.dialer.common.Assert;
+import com.android.dialer.enrichedcall.EnrichedCallCapabilities;
+import com.android.dialer.enrichedcall.EnrichedCallManager;
+import com.android.dialer.enrichedcall.Session;
+import com.android.dialer.enrichedcall.VideoShareSession;
+import com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult;
+import com.android.dialer.enrichedcall.videoshare.VideoShareListener;
import com.android.dialer.multimedia.MultimediaData;
+import java.util.List;
+import java.util.Map;
/** Stub implementation of {@link EnrichedCallManager}. */
public final class EnrichedCallManagerStub implements EnrichedCallManager {
@@ -52,6 +65,9 @@ public final class EnrichedCallManagerStub implements EnrichedCallManager {
public void endCallComposerSession(long sessionId) {}
@Override
+ public void sendPostCallNote(@NonNull String number, @NonNull String message) {}
+
+ @Override
public void onCapabilitiesReceived(
@NonNull String number, @NonNull EnrichedCallCapabilities capabilities) {}
@@ -60,7 +76,7 @@ public final class EnrichedCallManagerStub implements EnrichedCallManager {
@Nullable
@Override
- public Session getSession(@NonNull String number) {
+ public Session getSession(@NonNull String uniqueCallId, @NonNull String number) {
return null;
}
@@ -70,6 +86,15 @@ public final class EnrichedCallManagerStub implements EnrichedCallManager {
return null;
}
+ @NonNull
+ @Override
+ @WorkerThread
+ public Map<CallDetailsEntry, List<HistoryResult>> getAllHistoricalData(
+ @NonNull String number, @NonNull CallDetailsEntries entries) {
+ Assert.isWorkerThread();
+ return new ArrayMap<>();
+ }
+
@Override
public void unregisterStateChangedListener(@NonNull StateChangedListener listener) {}
@@ -81,4 +106,40 @@ public final class EnrichedCallManagerStub implements EnrichedCallManager {
@Override
public void onIncomingCallComposerData(long sessionId, @NonNull MultimediaData multimediaData) {}
+
+ @Override
+ public void onIncomingPostCallData(long sessionId, @NonNull MultimediaData multimediaData) {}
+
+ @Override
+ public void registerVideoShareListener(@NonNull VideoShareListener listener) {}
+
+ @Override
+ public void unregisterVideoShareListener(@NonNull VideoShareListener listener) {}
+
+ @Override
+ public void onIncomingVideoShareInvite(long sessionId, @NonNull String number) {}
+
+ @Override
+ public long startVideoShareSession(String number) {
+ return Session.NO_SESSION_ID;
+ }
+
+ @Override
+ public boolean acceptVideoShareSession(long sessionId) {
+ return false;
+ }
+
+ @Override
+ public long getVideoShareInviteSessionId(@NonNull String number) {
+ return Session.NO_SESSION_ID;
+ }
+
+ @Override
+ public void endVideoShareSession(long sessionId) {}
+
+ @Nullable
+ @Override
+ public VideoShareSession getVideoShareSession(long sessionId) {
+ return null;
+ }
}
diff --git a/java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java b/java/com/android/dialer/enrichedcall/stub/StubEnrichedCallModule.java
index 39c55d040..0ec72111e 100644
--- a/java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java
+++ b/java/com/android/dialer/enrichedcall/stub/StubEnrichedCallModule.java
@@ -14,8 +14,9 @@
* limitations under the License
*/
-package com.android.dialer.enrichedcall;
+package com.android.dialer.enrichedcall.stub;
+import com.android.dialer.enrichedcall.EnrichedCallManager;
import dagger.Module;
import dagger.Provides;
import javax.inject.Singleton;
@@ -29,4 +30,6 @@ public class StubEnrichedCallModule {
static EnrichedCallManager provideEnrichedCallManager() {
return new EnrichedCallManagerStub();
}
+
+ private StubEnrichedCallModule() {}
}
diff --git a/java/com/android/dialer/enrichedcall/videoshare/VideoShareListener.java b/java/com/android/dialer/enrichedcall/videoshare/VideoShareListener.java
new file mode 100644
index 000000000..bcc387a3f
--- /dev/null
+++ b/java/com/android/dialer/enrichedcall/videoshare/VideoShareListener.java
@@ -0,0 +1,14 @@
+package com.android.dialer.enrichedcall.videoshare;
+
+import android.support.annotation.MainThread;
+
+/** Receives updates when video share status has changed. */
+public interface VideoShareListener {
+
+ /**
+ * Callback fired when video share has changed (service connected / disconnected, video share
+ * invite received or canceled, or when a session changes).
+ */
+ @MainThread
+ void onVideoShareChanged();
+}
diff --git a/java/com/android/dialer/inject/ApplicationModule.java b/java/com/android/dialer/inject/ContextModule.java
index 99e5296ea..aa83f0105 100644
--- a/java/com/android/dialer/inject/ApplicationModule.java
+++ b/java/com/android/dialer/inject/ContextModule.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,24 +16,24 @@
package com.android.dialer.inject;
-import android.app.Application;
+import android.content.Context;
import android.support.annotation.NonNull;
import com.android.dialer.common.Assert;
import dagger.Module;
import dagger.Provides;
-/** Provides the singleton application object. */
+/** Provides the singleton context object. */
@Module
-public final class ApplicationModule {
+public final class ContextModule {
- @NonNull private final Application application;
+ @NonNull private final Context context;
- public ApplicationModule(@NonNull Application application) {
- this.application = Assert.isNotNull(application);
+ public ContextModule(@NonNull Context context) {
+ this.context = Assert.isNotNull(context);
}
@Provides
- Application provideApplication() {
- return application;
+ Context provideContext() {
+ return context;
}
}
diff --git a/java/com/android/dialer/inject/DialerAppComponent.java b/java/com/android/dialer/inject/HasRootComponent.java
index 9832ce804..0802b806a 100644
--- a/java/com/android/dialer/inject/DialerAppComponent.java
+++ b/java/com/android/dialer/inject/HasRootComponent.java
@@ -16,14 +16,10 @@
package com.android.dialer.inject;
-import com.android.dialer.enrichedcall.EnrichedCallManager;
-import com.android.dialer.enrichedcall.StubEnrichedCallModule;
-import dagger.Component;
-import javax.inject.Singleton;
-
-/** Core application-wide {@link Component} for the open source dialer app. */
-@Singleton
-@Component(modules = {ApplicationModule.class, StubEnrichedCallModule.class})
-public interface DialerAppComponent {
- EnrichedCallManager enrichedCallManager();
+/**
+ * Used by packages to access the root component from the Application without creating a dependency
+ * cycle.
+ */
+public interface HasRootComponent {
+ Object component();
}
diff --git a/java/com/android/dialer/interactions/PhoneNumberInteraction.java b/java/com/android/dialer/interactions/PhoneNumberInteraction.java
index f36e5319c..b797629dc 100644
--- a/java/com/android/dialer/interactions/PhoneNumberInteraction.java
+++ b/java/com/android/dialer/interactions/PhoneNumberInteraction.java
@@ -81,8 +81,10 @@ public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> {
private static final String TAG = PhoneNumberInteraction.class.getSimpleName();
/** The identifier for a permissions request if one is generated. */
public static final int REQUEST_READ_CONTACTS = 1;
+ public static final int REQUEST_CALL_PHONE = 2;
- private static final String[] PHONE_NUMBER_PROJECTION =
+ @VisibleForTesting
+ public static final String[] PHONE_NUMBER_PROJECTION =
new String[] {
Phone._ID,
Phone.NUMBER,
@@ -191,13 +193,14 @@ public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> {
* numbers have been queried for. The activity must implement {@link InteractionErrorListener}
* and {@link DisambigDialogDismissedListener}.
* @param isVideoCall {@code true} if the call is a video call, {@code false} otherwise.
+ * @return true if the necessary permissions were found to start the interaction, false otherwise
*/
- public static void startInteractionForPhoneCall(
+ public static boolean startInteractionForPhoneCall(
TransactionSafeActivity activity,
Uri uri,
boolean isVideoCall,
CallSpecificAppData callSpecificAppData) {
- new PhoneNumberInteraction(
+ return new PhoneNumberInteraction(
activity, ContactDisplayUtils.INTERACTION_CALL, isVideoCall, callSpecificAppData)
.startInteraction(uri);
}
@@ -211,11 +214,19 @@ public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> {
* Initiates the interaction to result in either a phone call or sms message for a contact.
*
* @param uri Contact Uri
+ * @return true if the necessary permissions were found to start the interaction, false otherwise
*/
- private void startInteraction(Uri uri) {
- // It's possible for a shortcut to have been created, and then Contacts permissions revoked. To
- // avoid a crash when the user tries to use such a shortcut, check for this condition and ask
- // the user for the permission.
+ private boolean startInteraction(Uri uri) {
+ // It's possible for a shortcut to have been created, and then permissions revoked. To avoid a
+ // crash when the user tries to use such a shortcut, check for this condition and ask the user
+ // for the permission.
+ if (ContextCompat.checkSelfPermission(mContext, Manifest.permission.CALL_PHONE)
+ != PackageManager.PERMISSION_GRANTED) {
+ LogUtil.i("PhoneNumberInteraction.startInteraction", "No phone permissions");
+ ActivityCompat.requestPermissions(
+ (Activity) mContext, new String[] {Manifest.permission.CALL_PHONE}, REQUEST_CALL_PHONE);
+ return false;
+ }
if (ContextCompat.checkSelfPermission(mContext, Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED) {
LogUtil.i("PhoneNumberInteraction.startInteraction", "No contact permissions");
@@ -223,7 +234,7 @@ public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> {
(Activity) mContext,
new String[] {Manifest.permission.READ_CONTACTS},
REQUEST_READ_CONTACTS);
- return;
+ return false;
}
if (mLoader != null) {
@@ -249,6 +260,7 @@ public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> {
mContext, queryUri, PHONE_NUMBER_PROJECTION, PHONE_NUMBER_SELECTION, null, null);
mLoader.registerListener(0, this);
mLoader.startLoading();
+ return true;
}
@Override
@@ -457,8 +469,8 @@ public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> {
* will be chosen to make a call or initiate an sms message.
*
* <p>It is recommended to use {@link #startInteractionForPhoneCall(TransactionSafeActivity, Uri,
- * boolean, int)} instead of directly using this class, as those methods handle one or multiple
- * data cases appropriately.
+ * boolean, CallSpecificAppData)} instead of directly using this class, as those methods handle
+ * one or multiple data cases appropriately.
*
* <p>This fragment may only be attached to activities which implement {@link
* DisambigDialogDismissedListener}.
diff --git a/java/com/android/dialer/logging/nano/ContactLookupResult.java b/java/com/android/dialer/logging/nano/ContactLookupResult.java
index 8960560fb..93f5f0135 100644
--- a/java/com/android/dialer/logging/nano/ContactLookupResult.java
+++ b/java/com/android/dialer/logging/nano/ContactLookupResult.java
@@ -11,17 +11,19 @@
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
- * limitations under the License
+ * limitations under the License.
*/
// Generated by the protocol buffer compiler. DO NOT EDIT!
package com.android.dialer.logging.nano;
+/** This file is autogenerated, but javadoc required. */
@SuppressWarnings("hiding")
-public final class ContactLookupResult extends
- com.google.protobuf.nano.ExtendableMessageNano<ContactLookupResult> {
+public final class ContactLookupResult
+ extends com.google.protobuf.nano.ExtendableMessageNano<ContactLookupResult> {
+ /** This file is autogenerated, but javadoc required. */
// enum Type
public interface Type {
public static final int UNKNOWN_LOOKUP_RESULT_TYPE = 0;
@@ -34,11 +36,11 @@ public final class ContactLookupResult extends
}
private static volatile ContactLookupResult[] _emptyArray;
+
public static ContactLookupResult[] emptyArray() {
// Lazily initializes the empty array
if (_emptyArray == null) {
- synchronized (
- com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+ synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
if (_emptyArray == null) {
_emptyArray = new ContactLookupResult[0];
}
@@ -60,20 +62,20 @@ public final class ContactLookupResult extends
}
@Override
- public ContactLookupResult mergeFrom(
- com.google.protobuf.nano.CodedInputByteBufferNano input)
+ public ContactLookupResult mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
throws java.io.IOException {
while (true) {
int tag = input.readTag();
switch (tag) {
case 0:
return this;
- default: {
- if (!super.storeUnknownField(input, tag)) {
- return this;
+ default:
+ {
+ if (!super.storeUnknownField(input, tag)) {
+ return this;
+ }
+ break;
}
- break;
- }
}
}
}
@@ -84,8 +86,7 @@ public final class ContactLookupResult extends
}
public static ContactLookupResult parseFrom(
- com.google.protobuf.nano.CodedInputByteBufferNano input)
- throws java.io.IOException {
+ com.google.protobuf.nano.CodedInputByteBufferNano input) throws java.io.IOException {
return new ContactLookupResult().mergeFrom(input);
}
}
diff --git a/java/com/android/dialer/logging/nano/ContactSource.java b/java/com/android/dialer/logging/nano/ContactSource.java
index 35d8b8ca1..dbe40cd53 100644
--- a/java/com/android/dialer/logging/nano/ContactSource.java
+++ b/java/com/android/dialer/logging/nano/ContactSource.java
@@ -11,17 +11,19 @@
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
- * limitations under the License
+ * limitations under the License.
*/
// Generated by the protocol buffer compiler. DO NOT EDIT!
package com.android.dialer.logging.nano;
+/** This file is autogenerated, but javadoc required. */
@SuppressWarnings("hiding")
-public final class ContactSource extends
- com.google.protobuf.nano.ExtendableMessageNano<ContactSource> {
+public final class ContactSource
+ extends com.google.protobuf.nano.ExtendableMessageNano<ContactSource> {
+ /** This file is autogenerated, but javadoc required. */
// enum Type
public interface Type {
public static final int UNKNOWN_SOURCE_TYPE = 0;
@@ -33,11 +35,11 @@ public final class ContactSource extends
}
private static volatile ContactSource[] _emptyArray;
+
public static ContactSource[] emptyArray() {
// Lazily initializes the empty array
if (_emptyArray == null) {
- synchronized (
- com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+ synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
if (_emptyArray == null) {
_emptyArray = new ContactSource[0];
}
@@ -59,20 +61,20 @@ public final class ContactSource extends
}
@Override
- public ContactSource mergeFrom(
- com.google.protobuf.nano.CodedInputByteBufferNano input)
+ public ContactSource mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
throws java.io.IOException {
while (true) {
int tag = input.readTag();
switch (tag) {
case 0:
return this;
- default: {
- if (!super.storeUnknownField(input, tag)) {
- return this;
+ default:
+ {
+ if (!super.storeUnknownField(input, tag)) {
+ return this;
+ }
+ break;
}
- break;
- }
}
}
}
@@ -82,8 +84,7 @@ public final class ContactSource extends
return com.google.protobuf.nano.MessageNano.mergeFrom(new ContactSource(), data);
}
- public static ContactSource parseFrom(
- com.google.protobuf.nano.CodedInputByteBufferNano input)
+ public static ContactSource parseFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
throws java.io.IOException {
return new ContactSource().mergeFrom(input);
}
diff --git a/java/com/android/dialer/logging/nano/DialerImpression.java b/java/com/android/dialer/logging/nano/DialerImpression.java
index 6bb56751f..80a006b55 100644
--- a/java/com/android/dialer/logging/nano/DialerImpression.java
+++ b/java/com/android/dialer/logging/nano/DialerImpression.java
@@ -14,12 +14,16 @@
* limitations under the License.
*/
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+
package com.android.dialer.logging.nano;
+/** This file is autogenerated, but javadoc required. */
@SuppressWarnings("hiding")
-public final class DialerImpression extends
- com.google.protobuf.nano.ExtendableMessageNano<DialerImpression> {
+public final class DialerImpression
+ extends com.google.protobuf.nano.ExtendableMessageNano<DialerImpression> {
+ /** This file is autogenerated, but javadoc required. */
// enum Type
public interface Type {
public static final int UNKNOWN_AOSP_EVENT_TYPE = 1000;
@@ -33,7 +37,8 @@ public final class DialerImpression extends
public static final int DIALOG_ACTION_CONFIRM_NUMBER_NOT_SPAM = 1008;
public static final int REPORT_AS_NOT_SPAM_VIA_UNBLOCK_NUMBER = 1009;
public static final int DIALOG_ACTION_CONFIRM_NUMBER_SPAM_INDIRECTLY_VIA_BLOCK_NUMBER = 1010;
- public static final int REPORT_CALL_AS_SPAM_VIA_CALL_LOG_BLOCK_REPORT_SPAM_SENT_VIA_BLOCK_NUMBER_DIALOG = 1011;
+ public static final int
+ REPORT_CALL_AS_SPAM_VIA_CALL_LOG_BLOCK_REPORT_SPAM_SENT_VIA_BLOCK_NUMBER_DIALOG = 1011;
public static final int USER_ACTION_BLOCKED_NUMBER = 1012;
public static final int USER_ACTION_UNBLOCKED_NUMBER = 1013;
public static final int SPAM_AFTER_CALL_NOTIFICATION_BLOCK_NUMBER = 1014;
@@ -41,7 +46,8 @@ public final class DialerImpression extends
public static final int SPAM_AFTER_CALL_NOTIFICATION_SHOW_NON_SPAM_DIALOG = 1016;
public static final int SPAM_AFTER_CALL_NOTIFICATION_ADD_TO_CONTACTS = 1019;
public static final int SPAM_AFTER_CALL_NOTIFICATION_MARKED_NUMBER_AS_SPAM = 1020;
- public static final int SPAM_AFTER_CALL_NOTIFICATION_MARKED_NUMBER_AS_NOT_SPAM_AND_BLOCKED = 1021;
+ public static final int SPAM_AFTER_CALL_NOTIFICATION_MARKED_NUMBER_AS_NOT_SPAM_AND_BLOCKED =
+ 1021;
public static final int SPAM_AFTER_CALL_NOTIFICATION_REPORT_NUMBER_AS_NOT_SPAM = 1022;
public static final int SPAM_AFTER_CALL_NOTIFICATION_ON_DISMISS_SPAM_DIALOG = 1024;
public static final int SPAM_AFTER_CALL_NOTIFICATION_ON_DISMISS_NON_SPAM_DIALOG = 1025;
@@ -89,7 +95,8 @@ public final class DialerImpression extends
public static final int STORAGE_PERMISSION_DENIED = 1073;
public static final int CAMERA_PERMISSION_DENIED = 1078;
public static final int VOICEMAIL_CONFIGURATION_STATE_CORRUPTION_DETECTED_FROM_ACTIVITY = 1079;
- public static final int VOICEMAIL_CONFIGURATION_STATE_CORRUPTION_DETECTED_FROM_NOTIFICATION = 1080;
+ public static final int VOICEMAIL_CONFIGURATION_STATE_CORRUPTION_DETECTED_FROM_NOTIFICATION =
+ 1080;
public static final int BACKUP_ON_BACKUP = 1081;
public static final int BACKUP_ON_FULL_BACKUP = 1082;
public static final int BACKUP_ON_BACKUP_DISABLED = 1083;
@@ -116,15 +123,43 @@ public final class DialerImpression extends
public static final int BACKUP_ON_RESTORE_VM_DUPLICATE_NOT_RESTORING = 1104;
public static final int CALL_LOG_SHARE_AND_CALL = 1105;
public static final int CALL_COMPOSER_ACTIVITY_PLACE_RCS_CALL = 1106;
- public static final int CALL_COMPOSER_ACTIVITY_SEND_AND_CALL_PRESSED_WHEN_SESSION_NOT_READY = 1107;
+ public static final int CALL_COMPOSER_ACTIVITY_SEND_AND_CALL_PRESSED_WHEN_SESSION_NOT_READY =
+ 1107;
+ public static final int POST_CALL_PROMPT_USER_TO_SEND_MESSAGE_CLICKED = 1108;
+ public static final int POST_CALL_PROMPT_USER_TO_SEND_MESSAGE = 1109;
+ public static final int POST_CALL_PROMPT_USER_TO_VIEW_SENT_MESSAGE = 1110;
+ public static final int POST_CALL_PROMPT_USER_TO_VIEW_SENT_MESSAGE_CLICKED = 1111;
+ public static final int IN_CALL_SCREEN_TURN_ON_MUTE = 1112;
+ public static final int IN_CALL_SCREEN_TURN_OFF_MUTE = 1113;
+ public static final int IN_CALL_SCREEN_SWAP_CAMERA = 1114;
+ public static final int IN_CALL_SCREEN_TURN_ON_VIDEO = 1115;
+ public static final int IN_CALL_SCREEN_TURN_OFF_VIDEO = 1116;
+ public static final int VIDEO_CALL_WITH_INCOMING_VOICE_CALL = 1117;
+ public static final int VIDEO_CALL_WITH_INCOMING_VIDEO_CALL = 1118;
+ public static final int VOICE_CALL_WITH_INCOMING_VOICE_CALL = 1119;
+ public static final int VOICE_CALL_WITH_INCOMING_VIDEO_CALL = 1120;
+ public static final int CALL_DETAILS_COPY_NUMBER = 1121;
+ public static final int CALL_DETAILS_EDIT_BEFORE_CALL = 1122;
+ public static final int CALL_DETAILS_CALL_BACK = 1123;
+ public static final int VVM_USER_DISMISSED_VM_ALMOST_FULL_PROMO = 1124;
+ public static final int VVM_USER_DISMISSED_VM_FULL_PROMO = 1125;
+ public static final int VVM_USER_ENABLED_ARCHIVE_FROM_VM_ALMOST_FULL_PROMO = 1126;
+ public static final int VVM_USER_ENABLED_ARCHIVE_FROM_VM_FULL_PROMO = 1127;
+ public static final int VVM_USER_SHOWN_VM_ALMOST_FULL_PROMO = 1128;
+ public static final int VVM_USER_SHOWN_VM_FULL_PROMO = 1129;
+ public static final int VVM_USER_SHOWN_VM_ALMOST_FULL_ERROR_MESSAGE = 1130;
+ public static final int VVM_USER_SHOWN_VM_FULL_ERROR_MESSAGE = 1131;
+ public static final int VVM_USER_TURNED_ARCHIVE_ON_FROM_SETTINGS = 1132;
+ public static final int VVM_USER_TURNED_ARCHIVE_OFF_FROM_SETTINGS = 1133;
+ public static final int VVM_ARCHIVE_AUTO_DELETED_VM_FROM_SERVER = 1134;
+ public static final int VVM_ARCHIVE_AUTO_DELETE_TURNED_OFF = 1135;
}
private static volatile DialerImpression[] _emptyArray;
public static DialerImpression[] emptyArray() {
// Lazily initializes the empty array
if (_emptyArray == null) {
- synchronized (
- com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+ synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
if (_emptyArray == null) {
_emptyArray = new DialerImpression[0];
}
@@ -146,20 +181,20 @@ public final class DialerImpression extends
}
@Override
- public DialerImpression mergeFrom(
- com.google.protobuf.nano.CodedInputByteBufferNano input)
+ public DialerImpression mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
throws java.io.IOException {
while (true) {
int tag = input.readTag();
switch (tag) {
case 0:
return this;
- default: {
- if (!super.storeUnknownField(input, tag)) {
- return this;
+ default:
+ {
+ if (!super.storeUnknownField(input, tag)) {
+ return this;
+ }
+ break;
}
- break;
- }
}
}
}
@@ -169,10 +204,8 @@ public final class DialerImpression extends
return com.google.protobuf.nano.MessageNano.mergeFrom(new DialerImpression(), data);
}
- public static DialerImpression parseFrom(
- com.google.protobuf.nano.CodedInputByteBufferNano input)
+ public static DialerImpression parseFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
throws java.io.IOException {
return new DialerImpression().mergeFrom(input);
}
}
-
diff --git a/java/com/android/dialer/logging/nano/InteractionEvent.java b/java/com/android/dialer/logging/nano/InteractionEvent.java
index 8d9430be9..7ca95fa45 100644
--- a/java/com/android/dialer/logging/nano/InteractionEvent.java
+++ b/java/com/android/dialer/logging/nano/InteractionEvent.java
@@ -23,8 +23,8 @@ package com.android.dialer.logging.nano;
public final class InteractionEvent
extends com.google.protobuf.nano.ExtendableMessageNano<InteractionEvent> {
- // enum Type
/** This file is autogenerated, but javadoc required. */
+ // enum Type
public interface Type {
public static final int UNKNOWN = 0;
public static final int CALL_BLOCKED = 15;
diff --git a/java/com/android/dialer/logging/nano/ReportingLocation.java b/java/com/android/dialer/logging/nano/ReportingLocation.java
index 1f05ce414..08ee04e7e 100644
--- a/java/com/android/dialer/logging/nano/ReportingLocation.java
+++ b/java/com/android/dialer/logging/nano/ReportingLocation.java
@@ -11,17 +11,19 @@
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
- * limitations under the License
+ * limitations under the License.
*/
// Generated by the protocol buffer compiler. DO NOT EDIT!
package com.android.dialer.logging.nano;
+/** This file is autogenerated, but javadoc required. */
@SuppressWarnings("hiding")
-public final class ReportingLocation extends
- com.google.protobuf.nano.ExtendableMessageNano<ReportingLocation> {
+public final class ReportingLocation
+ extends com.google.protobuf.nano.ExtendableMessageNano<ReportingLocation> {
+ /** This file is autogenerated, but javadoc required. */
// enum Type
public interface Type {
public static final int UNKNOWN_REPORTING_LOCATION = 0;
@@ -30,11 +32,11 @@ public final class ReportingLocation extends
}
private static volatile ReportingLocation[] _emptyArray;
+
public static ReportingLocation[] emptyArray() {
// Lazily initializes the empty array
if (_emptyArray == null) {
- synchronized (
- com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+ synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
if (_emptyArray == null) {
_emptyArray = new ReportingLocation[0];
}
@@ -56,20 +58,20 @@ public final class ReportingLocation extends
}
@Override
- public ReportingLocation mergeFrom(
- com.google.protobuf.nano.CodedInputByteBufferNano input)
+ public ReportingLocation mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
throws java.io.IOException {
while (true) {
int tag = input.readTag();
switch (tag) {
case 0:
return this;
- default: {
- if (!super.storeUnknownField(input, tag)) {
- return this;
+ default:
+ {
+ if (!super.storeUnknownField(input, tag)) {
+ return this;
+ }
+ break;
}
- break;
- }
}
}
}
@@ -79,8 +81,7 @@ public final class ReportingLocation extends
return com.google.protobuf.nano.MessageNano.mergeFrom(new ReportingLocation(), data);
}
- public static ReportingLocation parseFrom(
- com.google.protobuf.nano.CodedInputByteBufferNano input)
+ public static ReportingLocation parseFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
throws java.io.IOException {
return new ReportingLocation().mergeFrom(input);
}
diff --git a/java/com/android/dialer/logging/nano/ScreenEvent.java b/java/com/android/dialer/logging/nano/ScreenEvent.java
index be4e5eb9e..bd5b817e1 100644
--- a/java/com/android/dialer/logging/nano/ScreenEvent.java
+++ b/java/com/android/dialer/logging/nano/ScreenEvent.java
@@ -22,8 +22,8 @@ package com.android.dialer.logging.nano;
@SuppressWarnings("hiding")
public final class ScreenEvent extends com.google.protobuf.nano.ExtendableMessageNano<ScreenEvent> {
- // enum Type
/** This file is autogenerated, but javadoc required. */
+ // enum Type
public interface Type {
public static final int UNKNOWN = 0;
public static final int DIALPAD = 1;
diff --git a/java/com/android/dialer/multimedia/AutoValue_MultimediaData.java b/java/com/android/dialer/multimedia/AutoValue_MultimediaData.java
deleted file mode 100644
index cc6815094..000000000
--- a/java/com/android/dialer/multimedia/AutoValue_MultimediaData.java
+++ /dev/null
@@ -1,165 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.dialer.multimedia;
-
-import android.location.Location;
-import android.net.Uri;
-import android.support.annotation.Nullable;
-import javax.annotation.Generated;
-
-@Generated("com.google.auto.value.processor.AutoValueProcessor")
- final class AutoValue_MultimediaData extends MultimediaData {
-
- private final String subject;
- private final Location location;
- private final Uri imageUri;
- private final String imageContentType;
- private final boolean important;
-
- private AutoValue_MultimediaData(
- @Nullable String subject,
- @Nullable Location location,
- @Nullable Uri imageUri,
- @Nullable String imageContentType,
- boolean important) {
- this.subject = subject;
- this.location = location;
- this.imageUri = imageUri;
- this.imageContentType = imageContentType;
- this.important = important;
- }
-
- @Nullable
- @Override
- public String getSubject() {
- return subject;
- }
-
- @Nullable
- @Override
- public Location getLocation() {
- return location;
- }
-
- @Nullable
- @Override
- public Uri getImageUri() {
- return imageUri;
- }
-
- @Nullable
- @Override
- public String getImageContentType() {
- return imageContentType;
- }
-
- @Override
- public boolean isImportant() {
- return important;
- }
-
- @Override
- public boolean equals(Object o) {
- if (o == this) {
- return true;
- }
- if (o instanceof MultimediaData) {
- MultimediaData that = (MultimediaData) o;
- return ((this.subject == null) ? (that.getSubject() == null) : this.subject.equals(that.getSubject()))
- && ((this.location == null) ? (that.getLocation() == null) : this.location.equals(that.getLocation()))
- && ((this.imageUri == null) ? (that.getImageUri() == null) : this.imageUri.equals(that.getImageUri()))
- && ((this.imageContentType == null) ? (that.getImageContentType() == null) : this.imageContentType.equals(that.getImageContentType()))
- && (this.important == that.isImportant());
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- int h = 1;
- h *= 1000003;
- h ^= (subject == null) ? 0 : this.subject.hashCode();
- h *= 1000003;
- h ^= (location == null) ? 0 : this.location.hashCode();
- h *= 1000003;
- h ^= (imageUri == null) ? 0 : this.imageUri.hashCode();
- h *= 1000003;
- h ^= (imageContentType == null) ? 0 : this.imageContentType.hashCode();
- h *= 1000003;
- h ^= this.important ? 1231 : 1237;
- return h;
- }
-
- static final class Builder extends MultimediaData.Builder {
- private String subject;
- private Location location;
- private Uri imageUri;
- private String imageContentType;
- private Boolean important;
- Builder() {
- }
- private Builder(MultimediaData source) {
- this.subject = source.getSubject();
- this.location = source.getLocation();
- this.imageUri = source.getImageUri();
- this.imageContentType = source.getImageContentType();
- this.important = source.isImportant();
- }
- @Override
- public MultimediaData.Builder setSubject(@Nullable String subject) {
- this.subject = subject;
- return this;
- }
- @Override
- public MultimediaData.Builder setLocation(@Nullable Location location) {
- this.location = location;
- return this;
- }
- @Override
- MultimediaData.Builder setImageUri(@Nullable Uri imageUri) {
- this.imageUri = imageUri;
- return this;
- }
- @Override
- MultimediaData.Builder setImageContentType(@Nullable String imageContentType) {
- this.imageContentType = imageContentType;
- return this;
- }
- @Override
- public MultimediaData.Builder setImportant(boolean important) {
- this.important = important;
- return this;
- }
- @Override
- public MultimediaData build() {
- String missing = "";
- if (this.important == null) {
- missing += " important";
- }
- if (!missing.isEmpty()) {
- throw new IllegalStateException("Missing required properties:" + missing);
- }
- return new AutoValue_MultimediaData(
- this.subject,
- this.location,
- this.imageUri,
- this.imageContentType,
- this.important);
- }
- }
-
-}
diff --git a/java/com/android/dialer/multimedia/MultimediaData.java b/java/com/android/dialer/multimedia/MultimediaData.java
index ebd41a918..22bb7641c 100644
--- a/java/com/android/dialer/multimedia/MultimediaData.java
+++ b/java/com/android/dialer/multimedia/MultimediaData.java
@@ -21,10 +21,10 @@ import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.android.dialer.common.LogUtil;
+import com.google.auto.value.AutoValue;
-
-/** Holds the data associated with an enriched call session. */
-
+/** Holds data associated with a call. */
+@AutoValue
public abstract class MultimediaData {
public static final MultimediaData EMPTY = builder().build();
@@ -34,32 +34,33 @@ public abstract class MultimediaData {
return new AutoValue_MultimediaData.Builder().setImportant(false);
}
- /** Returns the call composer subject if set, or null if this isn't a call composer session. */
+ /**
+ * Returns the text part of this data.
+ *
+ * <p>This field is used for both the call composer session and the post call note.
+ */
@Nullable
- public abstract String getSubject();
+ public abstract String getText();
- /** Returns the call composer location if set, or null if this isn't a call composer session. */
+ /** Returns the location part of this data. */
@Nullable
public abstract Location getLocation();
- /** Returns {@code true} if this session contains image data. */
+ /** Returns {@code true} if this object contains image data. */
public boolean hasImageData() {
// imageUri and content are always either both null or nonnull
return getImageUri() != null && getImageContentType() != null;
}
- /** Returns the call composer photo if set, or null if this isn't a call composer session. */
+ /** Returns the image uri part of this object's image. */
@Nullable
public abstract Uri getImageUri();
- /**
- * Returns the content type of the image, either image/png or image/jpeg, if set, or null if this
- * isn't a call composer session.
- */
+ /** Returns the content type part of this object's image, either image/png or image/jpeg. */
@Nullable
public abstract String getImageContentType();
- /** Returns {@code true} if this is a call composer session that's marked as important. */
+ /** Returns {@code true} if this data is marked as important. */
public abstract boolean isImportant();
/** Returns the string form of this MultimediaData with no PII. */
@@ -68,7 +69,7 @@ public abstract class MultimediaData {
return String.format(
"MultimediaData{subject: %s, location: %s, imageUrl: %s, imageContentType: %s, "
+ "important: %b}",
- LogUtil.sanitizePii(getSubject()),
+ LogUtil.sanitizePii(getText()),
LogUtil.sanitizePii(getLocation()),
LogUtil.sanitizePii(getImageUri()),
getImageContentType(),
@@ -76,10 +77,10 @@ public abstract class MultimediaData {
}
/** Creates instances of {@link MultimediaData}. */
-
+ @AutoValue.Builder
public abstract static class Builder {
- public abstract Builder setSubject(@NonNull String subject);
+ public abstract Builder setText(@NonNull String subject);
public abstract Builder setLocation(@NonNull Location location);
diff --git a/java/com/android/dialer/notification/AndroidManifest.xml b/java/com/android/dialer/notification/AndroidManifest.xml
new file mode 100644
index 000000000..c5484f263
--- /dev/null
+++ b/java/com/android/dialer/notification/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<manifest
+ package="com.android.dialer.notification"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <uses-sdk android:minSdkVersion="23" />
+</manifest>
diff --git a/java/com/android/dialer/notification/GroupedNotificationUtil.java b/java/com/android/dialer/notification/GroupedNotificationUtil.java
new file mode 100644
index 000000000..63ea51739
--- /dev/null
+++ b/java/com/android/dialer/notification/GroupedNotificationUtil.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.notification;
+
+import android.app.NotificationManager;
+import android.service.notification.StatusBarNotification;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import java.util.Objects;
+
+/** Utilities for dealing with grouped notifications */
+public final class GroupedNotificationUtil {
+
+ /**
+ * Remove notification(s) that were added as part of a group. Will ensure that if this is the last
+ * notification in the group the summary will be removed.
+ *
+ * @param tag String tag as included in {@link NotificationManager#notify(String, int,
+ * android.app.Notification)}. If null will remove all notifications under id
+ * @param id notification id as included with {@link NotificationManager#notify(String, int,
+ * android.app.Notification)}.
+ * @param summaryTag String tag of the summary notification
+ */
+ public static void removeNotification(
+ @NonNull NotificationManager notificationManager,
+ @Nullable String tag,
+ int id,
+ @NonNull String summaryTag) {
+ if (tag == null) {
+ // Clear all missed call notifications
+ for (StatusBarNotification notification : notificationManager.getActiveNotifications()) {
+ if (notification.getId() == id) {
+ notificationManager.cancel(notification.getTag(), id);
+ }
+ }
+ } else {
+ notificationManager.cancel(tag, id);
+
+ // See if other non-summary missed call notifications exist, and if not then clear the summary
+ boolean clearSummary = true;
+ for (StatusBarNotification notification : notificationManager.getActiveNotifications()) {
+ if (notification.getId() == id && !Objects.equals(summaryTag, notification.getTag())) {
+ clearSummary = false;
+ break;
+ }
+ }
+ if (clearSummary) {
+ notificationManager.cancel(summaryTag, id);
+ }
+ }
+ }
+}
diff --git a/java/com/android/dialer/notification/NotificationChannelManager.java b/java/com/android/dialer/notification/NotificationChannelManager.java
new file mode 100644
index 000000000..9ff57321e
--- /dev/null
+++ b/java/com/android/dialer/notification/NotificationChannelManager.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.notification;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationChannelGroup;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.StringDef;
+import android.support.v4.os.BuildCompat;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telephony.TelephonyManager;
+import com.android.contacts.common.compat.TelephonyManagerCompat;
+import com.android.dialer.buildtype.BuildType;
+import com.android.dialer.common.LogUtil;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Contains info on how to create {@link NotificationChannel NotificationChannels} */
+public class NotificationChannelManager {
+
+ private static NotificationChannelManager instance;
+
+ public static NotificationChannelManager getInstance() {
+ if (instance == null) {
+ instance = new NotificationChannelManager();
+ }
+ return instance;
+ }
+
+ /**
+ * Set the channel of notification appropriately. Will create the channel if it does not already
+ * exist. Safe to call pre-O (will no-op).
+ *
+ * <p>phoneAccount should only be null if channelName is {@link Channel#MISC}.
+ */
+ public static void applyChannel(
+ @NonNull Notification.Builder notification,
+ @NonNull Context context,
+ @Channel String channelName,
+ @Nullable PhoneAccountHandle phoneAccount) {
+ if (phoneAccount == null) {
+ if (!Channel.MISC.equals(channelName)) {
+ IllegalArgumentException exception =
+ new IllegalArgumentException(
+ "Phone account handle must not be null unless on Channel.MISC");
+ if (BuildType.get() >= BuildType.RELEASE) {
+ LogUtil.e("NotificationChannelManager.applyChannel", null, exception);
+ } else {
+ throw exception;
+ }
+ }
+ }
+
+ if (BuildCompat.isAtLeastO()) {
+ NotificationChannel channel =
+ NotificationChannelManager.getInstance().getChannel(context, channelName, phoneAccount);
+ notification.setChannel(channel.getId());
+ }
+ }
+
+ /** The base Channel IDs for {@link NotificationChannel} */
+ @Retention(RetentionPolicy.SOURCE)
+ @StringDef({
+ Channel.INCOMING_CALL,
+ Channel.ONGOING_CALL,
+ Channel.MISSED_CALL,
+ Channel.VOICEMAIL,
+ Channel.EXTERNAL_CALL,
+ Channel.MISC
+ })
+ public @interface Channel {
+ String INCOMING_CALL = "incomingCall";
+ String ONGOING_CALL = "ongoingCall";
+ String MISSED_CALL = "missedCall";
+ String VOICEMAIL = "voicemail";
+ String EXTERNAL_CALL = "externalCall";
+ String MISC = "miscellaneous";
+ }
+
+ private NotificationChannelManager() {}
+
+ private NotificationChannel getChannel(
+ @NonNull Context context,
+ @Channel String channelName,
+ @Nullable PhoneAccountHandle phoneAccount) {
+ String channelId = channelNameToId(channelName, phoneAccount);
+ NotificationChannel channel = getNotificationManager(context).getNotificationChannel(channelId);
+ if (channel == null) {
+ channel = createChannel(context, channelName, phoneAccount);
+ }
+ return channel;
+ }
+
+ private static String channelNameToId(
+ @Channel String name, @Nullable PhoneAccountHandle phoneAccountHandle) {
+ if (phoneAccountHandle == null) {
+ return name;
+ } else {
+ return name + ":" + phoneAccountHandle.getId();
+ }
+ }
+
+ private NotificationChannel createChannel(
+ Context context,
+ @Channel String channelName,
+ @Nullable PhoneAccountHandle phoneAccountHandle) {
+ String channelId = channelNameToId(channelName, phoneAccountHandle);
+
+ if (phoneAccountHandle != null) {
+ PhoneAccount account = getTelecomManager(context).getPhoneAccount(phoneAccountHandle);
+ NotificationChannelGroup group =
+ new NotificationChannelGroup(
+ phoneAccountHandle.getId(),
+ (account == null) ? phoneAccountHandle.getId() : account.getLabel().toString());
+ getNotificationManager(context)
+ .createNotificationChannelGroup(group); // No-op if already exists
+ } else if (!Channel.MISC.equals(channelName)) {
+ LogUtil.w(
+ "NotificationChannelManager.createChannel",
+ "Null PhoneAccountHandle with channel " + channelName);
+ }
+
+ Uri silentRingtone = Uri.parse("");
+
+ CharSequence name;
+ int importance;
+ boolean canShowBadge;
+ boolean lights;
+ boolean vibration;
+ Uri sound;
+ switch (channelName) {
+ case Channel.INCOMING_CALL:
+ name = context.getText(R.string.notification_channel_incoming_call);
+ importance = NotificationManager.IMPORTANCE_MAX;
+ canShowBadge = false;
+ lights = true;
+ vibration = false;
+ sound = silentRingtone;
+ break;
+ case Channel.MISSED_CALL:
+ name = context.getText(R.string.notification_channel_missed_call);
+ importance = NotificationManager.IMPORTANCE_DEFAULT;
+ canShowBadge = true;
+ lights = true;
+ vibration = true;
+ sound = silentRingtone;
+ break;
+ case Channel.ONGOING_CALL:
+ name = context.getText(R.string.notification_channel_ongoing_call);
+ importance = NotificationManager.IMPORTANCE_DEFAULT;
+ canShowBadge = false;
+ lights = false;
+ vibration = false;
+ sound = null;
+ break;
+ case Channel.VOICEMAIL:
+ name = context.getText(R.string.notification_channel_voicemail);
+ importance = NotificationManager.IMPORTANCE_DEFAULT;
+ canShowBadge = true;
+ lights = true;
+ vibration = true;
+ sound =
+ TelephonyManagerCompat.getVoicemailRingtoneUri(
+ getTelephonyManager(context), phoneAccountHandle);
+ break;
+ case Channel.EXTERNAL_CALL:
+ name = context.getText(R.string.notification_channel_external_call);
+ importance = NotificationManager.IMPORTANCE_HIGH;
+ canShowBadge = false;
+ lights = true;
+ vibration = true;
+ sound = null;
+ break;
+ case Channel.MISC:
+ name = context.getText(R.string.notification_channel_misc);
+ importance = NotificationManager.IMPORTANCE_DEFAULT;
+ canShowBadge = false;
+ lights = true;
+ vibration = true;
+ sound = null;
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown channel: " + channelName);
+ }
+
+ NotificationChannel channel = new NotificationChannel(channelId, name, importance);
+ channel.setShowBadge(canShowBadge);
+ if (sound != null) {
+ channel.setSound(
+ sound,
+ new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION).build());
+ }
+ channel.enableLights(lights);
+ channel.enableVibration(vibration);
+ getNotificationManager(context).createNotificationChannel(channel);
+ return channel;
+ }
+
+ private static NotificationManager getNotificationManager(@NonNull Context context) {
+ return context.getSystemService(NotificationManager.class);
+ }
+
+ private static TelephonyManager getTelephonyManager(@NonNull Context context) {
+ return context.getSystemService(TelephonyManager.class);
+ }
+
+ private static TelecomManager getTelecomManager(@NonNull Context context) {
+ return context.getSystemService(TelecomManager.class);
+ }
+}
diff --git a/java/com/android/dialer/notification/res/values/ids.xml b/java/com/android/dialer/notification/res/values/ids.xml
new file mode 100644
index 000000000..6bdb489a7
--- /dev/null
+++ b/java/com/android/dialer/notification/res/values/ids.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+ <item name="notification_incoming_call" type="id"/>
+ <item name="notification_ongoing_call" type="id"/>
+ <item name="notification_missed_call" type="id"/>
+ <item name="notification_voicemail" type="id"/>
+ <item name="notification_external_call" type="id"/>
+ <item name="notification_call_blocking_disabled_by_emergency_call" type="id"/>
+ <item name="notification_spam_call" type="id"/>
+ <item name="notification_feedback" type="id"/>
+</resources>
diff --git a/java/com/android/dialer/notification/res/values/strings.xml b/java/com/android/dialer/notification/res/values/strings.xml
new file mode 100644
index 000000000..2fc4962c6
--- /dev/null
+++ b/java/com/android/dialer/notification/res/values/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+ <string name="notification_channel_incoming_call">Incoming calls</string>
+ <string name="notification_channel_ongoing_call">Ongoing calls</string>
+ <string name="notification_channel_missed_call">Missed calls</string>
+ <string name="notification_channel_voicemail">Voicemails</string>
+ <string name="notification_channel_external_call">External calls</string>
+ <string name="notification_channel_misc">Miscellaneous</string>
+</resources>
diff --git a/java/com/android/dialer/oem/AndroidManifest.xml b/java/com/android/dialer/oem/AndroidManifest.xml
new file mode 100644
index 000000000..e161a6d14
--- /dev/null
+++ b/java/com/android/dialer/oem/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.dialer.oem">
+</manifest> \ No newline at end of file
diff --git a/java/com/android/dialer/oem/MotorolaHiddenMenuKeySequence.java b/java/com/android/dialer/oem/MotorolaHiddenMenuKeySequence.java
new file mode 100644
index 000000000..18f621e01
--- /dev/null
+++ b/java/com/android/dialer/oem/MotorolaHiddenMenuKeySequence.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * This file is derived in part from code issued under the following license.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.android.dialer.oem;
+
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import com.android.dialer.common.LogUtil;
+import java.util.regex.Pattern;
+
+/**
+ * Util class to handle special char sequence and launch corresponding intent based the sequence.
+ */
+public class MotorolaHiddenMenuKeySequence {
+ private static final String EXTRA_HIDDEN_MENU_CODE = "HiddenMenuCode";
+ private static MotorolaHiddenMenuKeySequence instance = null;
+
+ private static String[] hiddenKeySequenceArray = null;
+ private static String[] hiddenKeySequenceIntentArray = null;
+ private static String[] hiddenKeyPatternArray = null;
+ private static String[] hiddenKeyPatternIntentArray = null;
+ private static boolean featureHiddenMenuEnabled = false;
+
+ /**
+ * Handle input char sequence.
+ *
+ * @param context context
+ * @param input input sequence
+ * @return true if the input matches any pattern
+ */
+ static boolean handleCharSequence(Context context, String input) {
+ getInstance(context);
+ if (!featureHiddenMenuEnabled) {
+ return false;
+ }
+ return handleKeySequence(context, input) || handleKeyPattern(context, input);
+ }
+
+ /**
+ * Public interface to return the Singleton instance
+ *
+ * @param context the Context
+ * @return the MotorolaHiddenMenuKeySequence singleton instance
+ */
+ private static synchronized MotorolaHiddenMenuKeySequence getInstance(Context context) {
+ if (null == instance) {
+ instance = new MotorolaHiddenMenuKeySequence(context);
+ }
+ return instance;
+ }
+
+ private MotorolaHiddenMenuKeySequence(Context context) {
+ featureHiddenMenuEnabled =
+ context.getResources().getBoolean(R.bool.motorola_feature_hidden_menu);
+ // In case we do have a SPN from resource we need to match from service; otherwise we are
+ // free to go
+ if (featureHiddenMenuEnabled) {
+
+ hiddenKeySequenceArray =
+ context.getResources().getStringArray(R.array.motorola_hidden_menu_key_sequence);
+ hiddenKeySequenceIntentArray =
+ context.getResources().getStringArray(R.array.motorola_hidden_menu_key_sequence_intents);
+ hiddenKeyPatternArray =
+ context.getResources().getStringArray(R.array.motorola_hidden_menu_key_pattern);
+ hiddenKeyPatternIntentArray =
+ context.getResources().getStringArray(R.array.motorola_hidden_menu_key_pattern_intents);
+
+ if (hiddenKeySequenceArray.length != hiddenKeySequenceIntentArray.length
+ || hiddenKeyPatternArray.length != hiddenKeyPatternIntentArray.length
+ || (hiddenKeySequenceArray.length == 0 && hiddenKeyPatternArray.length == 0)) {
+ LogUtil.e(
+ "MotorolaHiddenMenuKeySequence",
+ "the key sequence array is not matching, turn off feature."
+ + "key sequence: %d != %d, key pattern %d != %d",
+ hiddenKeySequenceArray.length,
+ hiddenKeySequenceIntentArray.length,
+ hiddenKeyPatternArray.length,
+ hiddenKeyPatternIntentArray.length);
+ featureHiddenMenuEnabled = false;
+ }
+ }
+ }
+
+ private static boolean handleKeyPattern(Context context, String input) {
+ int len = input.length();
+ if (len <= 3 || hiddenKeyPatternArray == null || hiddenKeyPatternIntentArray == null) {
+ return false;
+ }
+
+ for (int i = 0; i < hiddenKeyPatternArray.length; i++) {
+ if ((Pattern.compile(hiddenKeyPatternArray[i])).matcher(input).matches()) {
+ return sendIntent(context, input, hiddenKeyPatternIntentArray[i]);
+ }
+ }
+ return false;
+ }
+
+ private static boolean handleKeySequence(Context context, String input) {
+ int len = input.length();
+ if (len <= 3 || hiddenKeySequenceArray == null || hiddenKeySequenceIntentArray == null) {
+ return false;
+ }
+
+ for (int i = 0; i < hiddenKeySequenceArray.length; i++) {
+ if (hiddenKeySequenceArray[i].equals(input)) {
+ return sendIntent(context, input, hiddenKeySequenceIntentArray[i]);
+ }
+ }
+ return false;
+ }
+
+ private static boolean sendIntent(
+ final Context context, final String input, final String action) {
+ LogUtil.d("MotorolaHiddenMenuKeySequence.sendIntent", "input: %s", input);
+ try {
+ Intent intent = new Intent(action);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.putExtra(EXTRA_HIDDEN_MENU_CODE, input);
+
+ ResolveInfo resolveInfo = context.getPackageManager().resolveActivity(intent, 0);
+
+ if (resolveInfo != null
+ && resolveInfo.activityInfo != null
+ && resolveInfo.activityInfo.enabled) {
+ context.startActivity(intent);
+ return true;
+ } else {
+ LogUtil.w("MotorolaHiddenMenuKeySequence.sendIntent", "not able to resolve the intent");
+ }
+ } catch (ActivityNotFoundException e) {
+ LogUtil.e(
+ "MotorolaHiddenMenuKeySequence.sendIntent", "handleHiddenMenu Key Pattern Exception", e);
+ }
+ return false;
+ }
+}
diff --git a/java/com/android/dialer/oem/MotorolaUtils.java b/java/com/android/dialer/oem/MotorolaUtils.java
new file mode 100644
index 000000000..29bf0b23d
--- /dev/null
+++ b/java/com/android/dialer/oem/MotorolaUtils.java
@@ -0,0 +1,51 @@
+package com.android.dialer.oem;
+
+import android.content.Context;
+import com.android.dialer.common.ConfigProviderBindings;
+
+/** Util class for Motorola OEM devices. */
+public class MotorolaUtils {
+
+ private static final String CONFIG_HD_CODEC_BLINKING_ICON_WHEN_CONNECTING_CALL_ENABLED =
+ "hd_codec_blinking_icon_when_connecting_enabled";
+ private static final String CONFIG_HD_CODEC_SHOW_ICON_IN_CALL_LOG_ENABLED =
+ "hd_codec_show_icon_in_call_log_enabled";
+
+ // This is used to check if a Motorola device supports HD voice call feature, which comes from
+ // system feature setting.
+ private static final String HD_CALL_FEATRURE = "com.motorola.software.sprint.hd_call";
+
+ // Feature flag indicates it's a HD call, currently this is only used by Motorola system build.
+ // TODO(b/35359461): Upstream and move it to android.provider.CallLog.
+ private static final int FEATURES_HD_CALL = 0x10000000;
+
+ public static boolean shouldBlinkHdIconWhenConnectingCall(Context context) {
+ return ConfigProviderBindings.get(context)
+ .getBoolean(CONFIG_HD_CODEC_BLINKING_ICON_WHEN_CONNECTING_CALL_ENABLED, true)
+ && isSupportingSprintHdCodec(context);
+ }
+
+ public static boolean shouldShowHdIconInCallLog(Context context, int features) {
+ return ConfigProviderBindings.get(context)
+ .getBoolean(CONFIG_HD_CODEC_SHOW_ICON_IN_CALL_LOG_ENABLED, true)
+ && isSupportingSprintHdCodec(context)
+ && (features & FEATURES_HD_CALL) == FEATURES_HD_CALL;
+ }
+
+ /**
+ * Handle special char sequence entered in dialpad. This may launch special intent based on input.
+ *
+ * @param context context
+ * @param input input string
+ * @return true if the input is consumed and the intent is launched
+ */
+ public static boolean handleSpecialCharSequence(Context context, String input) {
+ // TODO(b/35395377): Add check for Motorola devices.
+ return MotorolaHiddenMenuKeySequence.handleCharSequence(context, input);
+ }
+
+ private static boolean isSupportingSprintHdCodec(Context context) {
+ return context.getPackageManager().hasSystemFeature(HD_CALL_FEATRURE)
+ && context.getResources().getBoolean(R.bool.motorola_sprint_hd_codec);
+ }
+}
diff --git a/java/com/android/dialer/oem/res/values/motorola_config.xml b/java/com/android/dialer/oem/res/values/motorola_config.xml
new file mode 100644
index 000000000..f875d573d
--- /dev/null
+++ b/java/com/android/dialer/oem/res/values/motorola_config.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Flag to control if HD codec is supported by Sprint. -->
+ <bool name="motorola_sprint_hd_codec">false</bool>
+
+ <!-- Hidden menu configuration for Motorola. -->
+ <!-- Flag to control if the Hidden Menu sequence will be supported by Sprint. -->
+ <bool name="motorola_feature_hidden_menu">false</bool>
+
+ <!-- This defines the specific key seuquence that will be catched in the SpecialCharSequenceMgr
+ such as, ##OMADM# -->
+ <string-array name="motorola_hidden_menu_key_sequence">
+ <item>##66236#</item> <!--##OMADM#-->
+ <item>##2539#</item> <!--##AKEY#-->
+ <item>##786#</item> <!--##RTN#-->
+ <item>##72786#</item> <!--##SCRTN#-->
+ <item>##3282#</item> <!--##DATA#-->
+ <item>##33284#</item> <!--##DEBUG#-->
+ <item>##3424#</item> <!--##DIAG#-->
+ <item>##564#</item> <!--##LOG#-->
+ <item>##4567257#</item> <!--##GLMSCLR#-->
+ <item>##873283#</item> <!--##UPDATE#-->
+ <item>##6343#</item> <!--##MEID#-->
+ <item>##27263#</item> <!--##BRAND#-->
+ <item>##258#</item> <!--##BLV#-->
+ <item>##8422#</item> <!--##UICC#-->
+ <item>##4382#</item> <!--CMAS/WEA-->
+ </string-array>
+
+ <string name="motorola_hidden_menu_intent">com.motorola.intent.action.LAUNCH_HIDDEN_MENU</string>
+
+ <!-- This defines the intents that will be send out when the key quence is matched, this must be
+ in the same order with he KeySequence array. -->
+ <string-array name="motorola_hidden_menu_key_sequence_intents">
+ <item>@string/motorola_hidden_menu_intent</item>
+ <item>@string/motorola_hidden_menu_intent</item>
+ <item>@string/motorola_hidden_menu_intent</item>
+ <item>@string/motorola_hidden_menu_intent</item>
+ <item>@string/motorola_hidden_menu_intent</item>
+ <item>@string/motorola_hidden_menu_intent</item>
+ <item>@string/motorola_hidden_menu_intent</item>
+ <item>@string/motorola_hidden_menu_intent</item>
+ <item>@string/motorola_hidden_menu_intent</item>
+ <item>com.motorola.android.intent.action.omadm.sprint.hfa</item>
+ <item>@string/motorola_hidden_menu_intent</item>
+ <item>@string/motorola_hidden_menu_intent</item>
+ <item>@string/motorola_hidden_menu_intent</item>
+ <item>@string/motorola_hidden_menu_intent</item>
+ <item>@string/motorola_hidden_menu_intent</item>
+ </string-array>
+
+ <!-- This defines the specific key patterns that will be catched in the SpecialCharSequenceMgr
+ such as, ##[0-9]{3,7}# -->
+ <string-array name="motorola_hidden_menu_key_pattern">
+ <!--##MSL#, here MSL is 6 digits SPC code, ##OTKSL#, OTKSL is also digits code -->
+ <item>##[0-9]{6}#</item>
+ </string-array>
+
+ <!-- This defines the intents that will be send out when the key quence is matched, this must be
+ in the same order with he KeyPattern array. -->
+ <string-array name="motorola_hidden_menu_key_pattern_intents">
+ <item>@string/motorola_hidden_menu_intent</item>
+ </string-array>
+</resources> \ No newline at end of file
diff --git a/java/com/android/dialer/p13n/inference/P13nRanking.java b/java/com/android/dialer/p13n/inference/P13nRanking.java
index 6bfc0352a..0682e85db 100644
--- a/java/com/android/dialer/p13n/inference/P13nRanking.java
+++ b/java/com/android/dialer/p13n/inference/P13nRanking.java
@@ -22,6 +22,7 @@ import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.android.dialer.common.Assert;
+import com.android.dialer.common.ConfigProviderBindings;
import com.android.dialer.p13n.inference.protocol.P13nRanker;
import com.android.dialer.p13n.inference.protocol.P13nRankerFactory;
import java.util.List;
@@ -38,37 +39,51 @@ public final class P13nRanking {
public static P13nRanker get(@NonNull Context context) {
Assert.isNotNull(context);
Assert.isMainThread();
+
if (ranker != null) {
return ranker;
}
+ if (!ConfigProviderBindings.get(context).getBoolean("p13n_ranker_should_enable", false)) {
+ setToIdentityRanker();
+ return ranker;
+ }
+
Context application = context.getApplicationContext();
if (application instanceof P13nRankerFactory) {
ranker = ((P13nRankerFactory) application).newP13nRanker();
}
if (ranker == null) {
- ranker =
- new P13nRanker() {
- @Override
- public void refresh(@Nullable P13nRefreshCompleteListener listener) {}
-
- @Override
- public List<String> rankList(List<String> phoneNumbers) {
- return phoneNumbers;
- }
-
- @NonNull
- @Override
- public Cursor rankCursor(
- @NonNull Cursor phoneQueryResults, int phoneNumberColumnIndex) {
- return phoneQueryResults;
- }
- };
+ setToIdentityRanker();
}
return ranker;
}
+ private static void setToIdentityRanker() {
+ ranker =
+ new P13nRanker() {
+ @Override
+ public void refresh(@Nullable P13nRefreshCompleteListener listener) {}
+
+ @Override
+ public List<String> rankList(List<String> phoneNumbers) {
+ return phoneNumbers;
+ }
+
+ @NonNull
+ @Override
+ public Cursor rankCursor(@NonNull Cursor phoneQueryResults, int queryLength) {
+ return phoneQueryResults;
+ }
+
+ @Override
+ public boolean shouldShowEmptyListForNullQuery() {
+ return true;
+ }
+ };
+ }
+
public static void setForTesting(@NonNull P13nRanker ranker) {
P13nRanking.ranker = ranker;
}
diff --git a/java/com/android/dialer/p13n/inference/protocol/P13nRanker.java b/java/com/android/dialer/p13n/inference/protocol/P13nRanker.java
index 9a859a6db..41f1de49d 100644
--- a/java/com/android/dialer/p13n/inference/protocol/P13nRanker.java
+++ b/java/com/android/dialer/p13n/inference/protocol/P13nRanker.java
@@ -41,13 +41,15 @@ public interface P13nRanker {
* input cursor is closed or invalid, or if any other error occurs in the ranking process.
*
* @param phoneQueryResults cursor of results of a Dialer search query
- * @param phoneNumberColumnIndex column index of the phone number in the cursor data
+ * @param queryLength length of the search query that resulted in the cursor data, if below 0,
+ * assumes no length is specified, thus applies the default behavior which is same as when
+ * queryLength is greater than zero.
* @return new cursor of data reordered by ranking (or reference to input cursor if order
* unchanged)
*/
@NonNull
@MainThread
- Cursor rankCursor(@NonNull Cursor phoneQueryResults, int phoneNumberColumnIndex);
+ Cursor rankCursor(@NonNull Cursor phoneQueryResults, int queryLength);
/**
* Refreshes ranking cache (pulls fresh contextual features, pre-caches inference results, etc.).
@@ -61,6 +63,10 @@ public interface P13nRanker {
@MainThread
void refresh(@Nullable P13nRefreshCompleteListener listener);
+ /** Decides if results should be displayed for no-query search. */
+ @MainThread
+ boolean shouldShowEmptyListForNullQuery();
+
/**
* Callback class for when ranking refresh has completed.
*
diff --git a/java/com/android/dialer/phonenumbercache/CachedNumberLookupService.java b/java/com/android/dialer/phonenumbercache/CachedNumberLookupService.java
index 03b77b91c..f443d56fb 100644
--- a/java/com/android/dialer/phonenumbercache/CachedNumberLookupService.java
+++ b/java/com/android/dialer/phonenumbercache/CachedNumberLookupService.java
@@ -18,7 +18,9 @@ package com.android.dialer.phonenumbercache;
import android.content.Context;
import android.net.Uri;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.annotation.WorkerThread;
import java.io.InputStream;
public interface CachedNumberLookupService {
@@ -35,6 +37,7 @@ public interface CachedNumberLookupService {
* found in the cache, {@link ContactInfo#EMPTY} if the phone number was not found in the
* cache, and null if there was an error when querying the cache.
*/
+ @WorkerThread
CachedContactInfo lookupCachedContactFromNumber(Context context, String number);
void addContact(Context context, CachedContactInfo info);
@@ -64,6 +67,7 @@ public interface CachedNumberLookupService {
int SOURCE_TYPE_PROFILE = 4;
int SOURCE_TYPE_CNAP = 5;
+ @NonNull
ContactInfo getContactInfo();
void setSource(int sourceType, String name, long directoryId);
diff --git a/java/com/android/dialer/postcall/AndroidManifest.xml b/java/com/android/dialer/postcall/AndroidManifest.xml
new file mode 100644
index 000000000..2bf07bca2
--- /dev/null
+++ b/java/com/android/dialer/postcall/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.dialer.callcomposer">
+
+ <application>
+ <activity
+ android:name="com.android.dialer.postcall.PostCallActivity"
+ android:exported="false"
+ android:theme="@style/Theme.AppCompat.NoActionBar"
+ android:windowSoftInputMode="adjustResize"
+ android:screenOrientation="portrait"/>
+ </application>
+</manifest>
diff --git a/java/com/android/dialer/postcall/PostCall.java b/java/com/android/dialer/postcall/PostCall.java
new file mode 100644
index 000000000..cfe7c867b
--- /dev/null
+++ b/java/com/android/dialer/postcall/PostCall.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.postcall;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.support.design.widget.BaseTransientBottomBar.BaseCallback;
+import android.support.design.widget.Snackbar;
+import android.view.View;
+import android.view.View.OnClickListener;
+import com.android.dialer.buildtype.BuildType;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.ConfigProvider;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.util.DialerUtils;
+import com.android.dialer.util.IntentUtil;
+
+/** Helper class to handle all post call actions. */
+public class PostCall {
+
+ private static final String KEY_POST_CALL_CALL_CONNECT_TIME = "post_call_call_connect_time";
+ private static final String KEY_POST_CALL_CALL_DISCONNECT_TIME = "post_call_call_disconnect_time";
+ private static final String KEY_POST_CALL_CALL_NUMBER = "post_call_call_number";
+ private static final String KEY_POST_CALL_MESSAGE_SENT = "post_call_message_sent";
+
+ public static void promptUserForMessageIfNecessary(Activity activity, View rootView) {
+ if (isEnabled(activity)) {
+ if (shouldPromptUserToViewSentMessage(activity)) {
+ promptUserToViewSentMessage(activity, rootView);
+ } else if (shouldPromptUserToSendMessage(activity)) {
+ promptUserToSendMessage(activity, rootView);
+ }
+ }
+ }
+
+ private static void promptUserToSendMessage(Activity activity, View rootView) {
+ LogUtil.i("PostCall.promptUserToSendMessage", "returned from call, showing post call SnackBar");
+ String message = activity.getString(R.string.post_call_message);
+ String addMessage = activity.getString(R.string.post_call_add_message);
+ OnClickListener onClickListener =
+ v -> {
+ Logger.get(activity)
+ .logImpression(DialerImpression.Type.POST_CALL_PROMPT_USER_TO_SEND_MESSAGE_CLICKED);
+ activity.startActivity(PostCallActivity.newIntent(activity, getPhoneNumber(activity)));
+ };
+
+ Snackbar.make(rootView, message, Snackbar.LENGTH_INDEFINITE)
+ .setAction(addMessage, onClickListener)
+ .setActionTextColor(
+ activity.getResources().getColor(R.color.dialer_snackbar_action_text_color))
+ .show();
+ Logger.get(activity).logImpression(DialerImpression.Type.POST_CALL_PROMPT_USER_TO_SEND_MESSAGE);
+ PreferenceManager.getDefaultSharedPreferences(activity)
+ .edit()
+ .remove(KEY_POST_CALL_CALL_DISCONNECT_TIME)
+ .apply();
+ }
+
+ private static void promptUserToViewSentMessage(Activity activity, View rootView) {
+ LogUtil.i(
+ "PostCall.promptUserToViewSentMessage",
+ "returned from sending a post call message, message sent.");
+ String message = activity.getString(R.string.post_call_message_sent);
+ String addMessage = activity.getString(R.string.view);
+ OnClickListener onClickListener =
+ v -> {
+ Logger.get(activity)
+ .logImpression(
+ DialerImpression.Type.POST_CALL_PROMPT_USER_TO_VIEW_SENT_MESSAGE_CLICKED);
+ Intent intent = IntentUtil.getSendSmsIntent(getPhoneNumber(activity));
+ DialerUtils.startActivityWithErrorToast(activity, intent);
+ };
+
+ Snackbar.make(rootView, message, Snackbar.LENGTH_INDEFINITE)
+ .setAction(addMessage, onClickListener)
+ .setActionTextColor(
+ activity.getResources().getColor(R.color.dialer_snackbar_action_text_color))
+ .addCallback(
+ new BaseCallback<Snackbar>() {
+ @Override
+ public void onDismissed(Snackbar snackbar, int i) {
+ super.onDismissed(snackbar, i);
+ clear(snackbar.getContext());
+ }
+ })
+ .show();
+ Logger.get(activity)
+ .logImpression(DialerImpression.Type.POST_CALL_PROMPT_USER_TO_VIEW_SENT_MESSAGE);
+ PreferenceManager.getDefaultSharedPreferences(activity)
+ .edit()
+ .remove(KEY_POST_CALL_MESSAGE_SENT)
+ .apply();
+ }
+
+ public static void onCallDisconnected(Context context, String number, long callConnectedMillis) {
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit()
+ .putLong(KEY_POST_CALL_CALL_CONNECT_TIME, callConnectedMillis)
+ .putLong(KEY_POST_CALL_CALL_DISCONNECT_TIME, System.currentTimeMillis())
+ .putString(KEY_POST_CALL_CALL_NUMBER, number)
+ .apply();
+ }
+
+ public static void onMessageSent(Context context, String number) {
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit()
+ .putString(KEY_POST_CALL_CALL_NUMBER, number)
+ .putBoolean(KEY_POST_CALL_MESSAGE_SENT, true)
+ .apply();
+ }
+
+ private static void clear(Context context) {
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit()
+ .remove(KEY_POST_CALL_CALL_DISCONNECT_TIME)
+ .remove(KEY_POST_CALL_CALL_NUMBER)
+ .remove(KEY_POST_CALL_MESSAGE_SENT)
+ .remove(KEY_POST_CALL_CALL_CONNECT_TIME)
+ .apply();
+ }
+
+ private static boolean shouldPromptUserToSendMessage(Context context) {
+ SharedPreferences manager = PreferenceManager.getDefaultSharedPreferences(context);
+ long disconnectTimeMillis = manager.getLong(KEY_POST_CALL_CALL_DISCONNECT_TIME, -1);
+ long connectTimeMillis = manager.getLong(KEY_POST_CALL_CALL_CONNECT_TIME, -1);
+
+ long timeSinceDisconnect = System.currentTimeMillis() - disconnectTimeMillis;
+ long callDurationMillis = disconnectTimeMillis - connectTimeMillis;
+
+ ConfigProvider binding = ConfigProviderBindings.get(context);
+ return disconnectTimeMillis != -1
+ && connectTimeMillis != -1
+ && binding.getLong("postcall_last_call_threshold", 30_000) > timeSinceDisconnect
+ && binding.getLong("postcall_call_duration_threshold", 60_000) > callDurationMillis;
+ }
+
+ private static boolean shouldPromptUserToViewSentMessage(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context)
+ .getBoolean(KEY_POST_CALL_MESSAGE_SENT, false);
+ }
+
+ private static String getPhoneNumber(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context)
+ .getString(KEY_POST_CALL_CALL_NUMBER, null);
+ }
+
+ private static boolean isEnabled(Context context) {
+ @BuildType.Type int type = BuildType.get();
+ switch (type) {
+ case BuildType.BUGFOOD:
+ case BuildType.DOGFOOD:
+ case BuildType.FISHFOOD:
+ case BuildType.TEST:
+ return ConfigProviderBindings.get(context).getBoolean("enable_post_call", true);
+ case BuildType.RELEASE:
+ return ConfigProviderBindings.get(context).getBoolean("enable_post_call_prod", true);
+ default:
+ Assert.fail();
+ return false;
+ }
+ }
+}
diff --git a/java/com/android/dialer/postcall/PostCallActivity.java b/java/com/android/dialer/postcall/PostCallActivity.java
new file mode 100644
index 000000000..8da03dcd1
--- /dev/null
+++ b/java/com/android/dialer/postcall/PostCallActivity.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.postcall;
+
+import android.Manifest.permission;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v7.app.AppCompatActivity;
+import android.telephony.SmsManager;
+import android.widget.Toolbar;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.enrichedcall.EnrichedCallCapabilities;
+import com.android.dialer.enrichedcall.EnrichedCallComponent;
+import com.android.dialer.enrichedcall.EnrichedCallManager;
+import com.android.dialer.util.PermissionsUtil;
+import com.android.dialer.widget.MessageFragment;
+
+/** Activity used to send post call messages after a phone call. */
+public class PostCallActivity extends AppCompatActivity implements MessageFragment.Listener {
+
+ public static final String KEY_PHONE_NUMBER = "phone_number";
+ public static final String KEY_MESSAGE = "message";
+ private static final int REQUEST_CODE_SEND_SMS = 1;
+
+ private boolean useRcs;
+
+ public static Intent newIntent(@NonNull Context context, @NonNull String number) {
+ Intent intent = new Intent(Assert.isNotNull(context), PostCallActivity.class);
+ intent.putExtra(KEY_PHONE_NUMBER, Assert.isNotNull(number));
+ return intent;
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle bundle) {
+ super.onCreate(bundle);
+ setContentView(R.layout.post_call_activity);
+
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ toolbar.setTitle(getString(R.string.post_call_message));
+ toolbar.setNavigationOnClickListener(v -> finish());
+
+ useRcs = canUseRcs(getIntent().getStringExtra(KEY_PHONE_NUMBER));
+ LogUtil.i("PostCallActivity.onCreate", "useRcs: %b", useRcs);
+
+ int postCallCharLimit =
+ useRcs
+ ? getResources().getInteger(R.integer.post_call_char_limit)
+ : MessageFragment.NO_CHAR_LIMIT;
+ String[] messages =
+ new String[] {
+ getString(R.string.post_call_message_1),
+ getString(R.string.post_call_message_2),
+ getString(R.string.post_call_message_3)
+ };
+ MessageFragment fragment =
+ MessageFragment.builder()
+ .setCharLimit(postCallCharLimit)
+ .showSendIcon()
+ .setMessages(messages)
+ .build();
+ getSupportFragmentManager()
+ .beginTransaction()
+ .replace(R.id.message_container, fragment)
+ .commit();
+ }
+
+ private boolean canUseRcs(@NonNull String number) {
+ EnrichedCallCapabilities capabilities =
+ getEnrichedCallManager().getCapabilities(Assert.isNotNull(number));
+ LogUtil.i(
+ "PostCallActivity.canUseRcs",
+ "number: %s, capabilities: %s",
+ LogUtil.sanitizePhoneNumber(number),
+ capabilities);
+ return capabilities != null && capabilities.supportsPostCall();
+ }
+
+ @Override
+ public void onMessageFragmentSendMessage(@NonNull String message) {
+ String number = Assert.isNotNull(getIntent().getStringExtra(KEY_PHONE_NUMBER));
+ getIntent().putExtra(KEY_MESSAGE, message);
+
+ if (useRcs) {
+ LogUtil.i("PostCallActivity.onMessageFragmentSendMessage", "sending post call Rcs.");
+ getEnrichedCallManager().sendPostCallNote(number, message);
+ PostCall.onMessageSent(this, number);
+ finish();
+ } else if (PermissionsUtil.hasPermission(this, permission.SEND_SMS)) {
+ LogUtil.i("PostCallActivity.sendMessage", "Sending post call SMS.");
+ SmsManager smsManager = SmsManager.getDefault();
+ smsManager.sendMultipartTextMessage(
+ number, null, smsManager.divideMessage(message), null, null);
+ PostCall.onMessageSent(this, number);
+ finish();
+ } else if (PermissionsUtil.isFirstRequest(this, permission.SEND_SMS)
+ || shouldShowRequestPermissionRationale(permission.SEND_SMS)) {
+ LogUtil.i("PostCallActivity.sendMessage", "Requesting SMS_SEND permission.");
+ requestPermissions(new String[] {permission.SEND_SMS}, REQUEST_CODE_SEND_SMS);
+ } else {
+ LogUtil.i(
+ "PostCallActivity.sendMessage", "Permission permanently denied, sending to settings.");
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.setData(Uri.parse("package:" + this.getPackageName()));
+ startActivity(intent);
+ }
+ }
+
+ @Override
+ public void onMessageFragmentAfterTextChange(String message) {}
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ if (permissions.length > 0 && permissions[0].equals(permission.SEND_SMS)) {
+ PermissionsUtil.permissionRequested(this, permissions[0]);
+ }
+ if (requestCode == REQUEST_CODE_SEND_SMS
+ && grantResults.length > 0
+ && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ onMessageFragmentSendMessage(getIntent().getStringExtra(KEY_MESSAGE));
+ }
+ }
+
+ @NonNull
+ private EnrichedCallManager getEnrichedCallManager() {
+ return EnrichedCallComponent.get(this).getEnrichedCallManager();
+ }
+}
diff --git a/java/com/android/dialer/postcall/res/layout/post_call_activity.xml b/java/com/android/dialer/postcall/res/layout/post_call_activity.xml
new file mode 100644
index 000000000..6ea8126c5
--- /dev/null
+++ b/java/com/android/dialer/postcall/res/layout/post_call_activity.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:background="@color/background_dialer_white"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <FrameLayout
+ android:id="@+id/message_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:background="@color/background_dialer_white"/>
+
+ <Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?attr/actionBarSize"
+ android:titleTextAppearance="@style/toolbar_title_text"
+ android:subtitleTextAppearance="@style/toolbar_subtitle_text"
+ android:navigationIcon="@drawable/quantum_ic_close_white_24"
+ android:background="@color/dialer_theme_color"/>
+</RelativeLayout> \ No newline at end of file
diff --git a/java/com/android/dialer/postcall/res/values/strings.xml b/java/com/android/dialer/postcall/res/values/strings.xml
new file mode 100644
index 000000000..d5e085a05
--- /dev/null
+++ b/java/com/android/dialer/postcall/res/values/strings.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources>
+ <!-- Shown as a message that notifies asks the user if they want to send a post call message -->
+ <string name="post_call_message">Say why you called</string>
+ <!-- Premade message to be sent as a text/RCS message -->
+ <string name="post_call_message_1">This is urgent. Call me back.</string>
+ <!-- Premade message to be sent as a text/RCS message -->
+ <string name="post_call_message_2">Call me back when you have some time.</string>
+ <!-- Premade message to be sent as a text/RCS message -->
+ <string name="post_call_message_3">Not urgent, we can chat later.</string>
+ <!-- Asks the user if they want to send a post call message -->
+ <string name="post_call_add_message">Add message</string>
+ <!-- Shown to let the user know that their message was sent. -->
+ <string name="post_call_message_sent">Message sent</string>
+ <string name="view">View</string>
+</resources> \ No newline at end of file
diff --git a/java/com/android/dialer/postcall/res/values/values.xml b/java/com/android/dialer/postcall/res/values/values.xml
new file mode 100644
index 000000000..64fe9f6c8
--- /dev/null
+++ b/java/com/android/dialer/postcall/res/values/values.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources>
+ <integer name="post_call_char_limit">60</integer>
+</resources> \ No newline at end of file
diff --git a/java/com/android/dialer/proguard/proguard.flags b/java/com/android/dialer/proguard/proguard.flags
new file mode 100644
index 000000000..0f684a0b3
--- /dev/null
+++ b/java/com/android/dialer/proguard/proguard.flags
@@ -0,0 +1,6 @@
+# Keep the annotation, classes, methods, and fields marked as UsedByReflection
+-keep class com.android.dialer.proguard.UsedByReflection
+-keep @com.android.dialer.proguard.UsedByReflection class *
+-keepclassmembers class * {
+ @com.android.dialer.proguard.UsedByReflection *;
+}
diff --git a/java/com/android/dialer/proguard/proguard_base.flags b/java/com/android/dialer/proguard/proguard_base.flags
new file mode 100644
index 000000000..7b5794ec7
--- /dev/null
+++ b/java/com/android/dialer/proguard/proguard_base.flags
@@ -0,0 +1,74 @@
+# Copied from http://google3/java/com/google/android/apps/common/proguard/base.flags
+
+# This file is intended to contain proguard options that *nobody* would ever
+# not want, in *any* configuration - they ensure basic correctness, and have
+# no downsides. You probably do not want to make changes to this file.
+
+# The presence of both of these attributes causes dalvik and other jvms to print
+# stack traces on uncaught exceptions, which is necessary to get useful crash
+# reports.
+-keepattributes SourceFile,LineNumberTable
+
+# Preverification was introduced in Java 6 to enable faster classloading, but
+# dex doesn't use the java .class format, so it has no benefit and can cause
+# problems.
+-dontpreverify
+
+# Skipping analysis of some classes may make proguard strip something that's
+# needed.
+-dontskipnonpubliclibraryclasses
+
+# Case-insensitive filesystems can't handle when a.class and A.class exist in
+# the same directory.
+-dontusemixedcaseclassnames
+
+# This prevents the names of native methods from being obfuscated and prevents
+# UnsatisfiedLinkErrors.
+-keepclasseswithmembernames class * {
+ native <methods>;
+}
+
+# hackbod discourages the use of enums on android, but if you use them, they
+# should work. Allow instantiation via reflection by keeping the values method.
+-keepclassmembers enum * {
+ public static **[] values();
+}
+
+# Parcel reflectively accesses this field.
+-keepclassmembers class * implements android.os.Parcelable {
+ public static *** CREATOR;
+}
+
+# These methods are needed to ensure that serialization behaves as expected when
+# classes are obfuscated, shrunk, and/or optimized.
+-keepclassmembers class * implements java.io.Serializable {
+ static final long serialVersionUID;
+ private static final java.io.ObjectStreamField[] serialPersistentFields;
+ private void writeObject(java.io.ObjectOutputStream);
+ private void readObject(java.io.ObjectInputStream);
+ java.lang.Object writeReplace();
+ java.lang.Object readResolve();
+}
+
+# Don't warn about Guava. Any Guava-using app will fail the proguard stage without this dontwarn,
+# and since Guava is so widely used, we include it here in the base.
+-dontwarn com.google.common.**
+
+# Don't warn about Error Prone annotations (e.g. @CompileTimeConstant)
+-dontwarn com.google.errorprone.annotations.**
+
+# Based on http://ag/718466: android.app.Notification.setLatestEventInfo() was
+# removed in MNC, but is still referenced (safely) by the NotificationCompat
+# code.
+-dontwarn android.app.Notification
+
+# Silence notes about dynamically referenced classes from AOSP support
+# libraries.
+-dontnote android.graphics.Insets
+
+# AOSP support library: ICU references to gender and plurals messages.
+-dontnote libcore.icu.ICU
+-keep class libcore.icu.ICU { *** get(...);}
+
+# AOSP support library: Handle classes that use reflection.
+-dontnote android.support.v4.app.NotificationCompatJellybean
diff --git a/java/com/android/dialer/proguard/proguard_release.flags b/java/com/android/dialer/proguard/proguard_release.flags
new file mode 100644
index 000000000..1c845cfa3
--- /dev/null
+++ b/java/com/android/dialer/proguard/proguard_release.flags
@@ -0,0 +1,24 @@
+# Copied from http://google3/java/com/google/android/apps/common/proguard/release.flags
+
+# Used for building release binaries. Obfuscates, optimizes, and shrinks.
+
+# By default, proguard leaves all classes in their original package, which
+# needlessly repeats com.google.android.apps.etc.
+-repackageclasses ""
+
+# Allows proguard to make private and protected methods and fields public as
+# part of optimization. This lets proguard inline trivial getter/setter methods.
+-allowaccessmodification
+
+# The source file attribute must be present in order to print stack traces, but
+# we rename it in order to avoid leaking the pre-obfuscation class name.
+-renamesourcefileattribute PG
+
+# This allows proguard to strip isLoggable() blocks containing only debug log
+# code from release builds.
+-assumenosideeffects class android.util.Log {
+ static *** i(...);
+ static *** d(...);
+ static *** v(...);
+ static *** isLoggable(...);
+}
diff --git a/java/com/android/dialer/shortcuts/AutoValue_DialerShortcut.java b/java/com/android/dialer/shortcuts/AutoValue_DialerShortcut.java
deleted file mode 100644
index ef995c816..000000000
--- a/java/com/android/dialer/shortcuts/AutoValue_DialerShortcut.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.dialer.shortcuts;
-
-import android.support.annotation.NonNull;
-import javax.annotation.Generated;
-
-@Generated("com.google.auto.value.processor.AutoValueProcessor")
- final class AutoValue_DialerShortcut extends DialerShortcut {
-
- private final long contactId;
- private final String lookupKey;
- private final String displayName;
- private final int rank;
-
- private AutoValue_DialerShortcut(
- long contactId,
- String lookupKey,
- String displayName,
- int rank) {
- this.contactId = contactId;
- this.lookupKey = lookupKey;
- this.displayName = displayName;
- this.rank = rank;
- }
-
- @Override
- long getContactId() {
- return contactId;
- }
-
- @NonNull
- @Override
- String getLookupKey() {
- return lookupKey;
- }
-
- @NonNull
- @Override
- String getDisplayName() {
- return displayName;
- }
-
- @Override
- int getRank() {
- return rank;
- }
-
- @Override
- public String toString() {
- return "DialerShortcut{"
- + "contactId=" + contactId + ", "
- + "lookupKey=" + lookupKey + ", "
- + "displayName=" + displayName + ", "
- + "rank=" + rank
- + "}";
- }
-
- @Override
- public boolean equals(Object o) {
- if (o == this) {
- return true;
- }
- if (o instanceof DialerShortcut) {
- DialerShortcut that = (DialerShortcut) o;
- return (this.contactId == that.getContactId())
- && (this.lookupKey.equals(that.getLookupKey()))
- && (this.displayName.equals(that.getDisplayName()))
- && (this.rank == that.getRank());
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- int h = 1;
- h *= 1000003;
- h ^= (this.contactId >>> 32) ^ this.contactId;
- h *= 1000003;
- h ^= this.lookupKey.hashCode();
- h *= 1000003;
- h ^= this.displayName.hashCode();
- h *= 1000003;
- h ^= this.rank;
- return h;
- }
-
- static final class Builder extends DialerShortcut.Builder {
- private Long contactId;
- private String lookupKey;
- private String displayName;
- private Integer rank;
- Builder() {
- }
- private Builder(DialerShortcut source) {
- this.contactId = source.getContactId();
- this.lookupKey = source.getLookupKey();
- this.displayName = source.getDisplayName();
- this.rank = source.getRank();
- }
- @Override
- DialerShortcut.Builder setContactId(long contactId) {
- this.contactId = contactId;
- return this;
- }
- @Override
- DialerShortcut.Builder setLookupKey(String lookupKey) {
- this.lookupKey = lookupKey;
- return this;
- }
- @Override
- DialerShortcut.Builder setDisplayName(String displayName) {
- this.displayName = displayName;
- return this;
- }
- @Override
- DialerShortcut.Builder setRank(int rank) {
- this.rank = rank;
- return this;
- }
- @Override
- DialerShortcut build() {
- String missing = "";
- if (this.contactId == null) {
- missing += " contactId";
- }
- if (this.lookupKey == null) {
- missing += " lookupKey";
- }
- if (this.displayName == null) {
- missing += " displayName";
- }
- if (this.rank == null) {
- missing += " rank";
- }
- if (!missing.isEmpty()) {
- throw new IllegalStateException("Missing required properties:" + missing);
- }
- return new AutoValue_DialerShortcut(
- this.contactId,
- this.lookupKey,
- this.displayName,
- this.rank);
- }
- }
-
-} \ No newline at end of file
diff --git a/java/com/android/dialer/shortcuts/CallContactActivity.java b/java/com/android/dialer/shortcuts/CallContactActivity.java
index 1e9a01b39..40bf97b87 100644
--- a/java/com/android/dialer/shortcuts/CallContactActivity.java
+++ b/java/com/android/dialer/shortcuts/CallContactActivity.java
@@ -56,11 +56,20 @@ public class CallContactActivity extends TransactionSafeActivity
}
}
+ /**
+ * Attempt to make a call, finishing the activity if the required permissions are already granted.
+ * If the required permissions are not already granted, the activity is not finished so that the
+ * user can choose to grant or deny them.
+ */
private void makeCall() {
CallSpecificAppData callSpecificAppData = new CallSpecificAppData();
callSpecificAppData.callInitiationType = CallInitiationType.Type.LAUNCHER_SHORTCUT;
- PhoneNumberInteraction.startInteractionForPhoneCall(
- this, contactUri, false /* isVideoCall */, callSpecificAppData);
+ boolean interactionStarted =
+ PhoneNumberInteraction.startInteractionForPhoneCall(
+ this, contactUri, false /* isVideoCall */, callSpecificAppData);
+ if (interactionStarted) {
+ finish();
+ }
}
@Override
@@ -115,6 +124,7 @@ public class CallContactActivity extends TransactionSafeActivity
int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case PhoneNumberInteraction.REQUEST_READ_CONTACTS:
+ case PhoneNumberInteraction.REQUEST_CALL_PHONE:
{
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
@@ -122,8 +132,8 @@ public class CallContactActivity extends TransactionSafeActivity
} else {
Toast.makeText(this, R.string.dialer_shortcut_no_permissions, Toast.LENGTH_SHORT)
.show();
+ finish();
}
- finish();
break;
}
default:
diff --git a/java/com/android/dialer/shortcuts/DialerShortcut.java b/java/com/android/dialer/shortcuts/DialerShortcut.java
index f2fb3301a..a8d4204fe 100644
--- a/java/com/android/dialer/shortcuts/DialerShortcut.java
+++ b/java/com/android/dialer/shortcuts/DialerShortcut.java
@@ -22,7 +22,7 @@ import android.net.Uri;
import android.os.Build.VERSION_CODES;
import android.provider.ContactsContract.Contacts;
import android.support.annotation.NonNull;
-
+import com.google.auto.value.AutoValue;
/**
* Convenience data structure.
@@ -31,7 +31,7 @@ import android.support.annotation.NonNull;
* convenience methods for doing things like constructing labels.
*/
@TargetApi(VERSION_CODES.N_MR1) // Shortcuts introduced in N MR1
-
+@AutoValue
abstract class DialerShortcut {
/** Marker value indicates that shortcut has no setRank. Used by pinned shortcuts. */
@@ -160,7 +160,7 @@ abstract class DialerShortcut {
return new AutoValue_DialerShortcut.Builder().setRank(NO_RANK);
}
-
+ @AutoValue.Builder
abstract static class Builder {
/**
diff --git a/java/com/android/dialer/shortcuts/res/values/strings.xml b/java/com/android/dialer/shortcuts/res/values/strings.xml
index 1e2c87f12..5f14a8100 100644
--- a/java/com/android/dialer/shortcuts/res/values/strings.xml
+++ b/java/com/android/dialer/shortcuts/res/values/strings.xml
@@ -30,8 +30,8 @@
be found or doesn't have any phone numbers. [CHAR LIMIT=70] -->
<string name="dialer_shortcut_contact_not_found_or_has_no_number">Contact no longer available.</string>
- <!-- Error message to display when a tapping a shortcut fails because contact permissions are
- missing. [CHAR LIMIT=70] -->
- <string name="dialer_shortcut_no_permissions">Cannot call without contact permissions.</string>
+ <!-- Error message to display when a tapping a shortcut fails because permissions are missing.
+ [CHAR LIMIT=70] -->
+ <string name="dialer_shortcut_no_permissions">Cannot call without permissions.</string>
</resources>
diff --git a/java/com/android/dialer/shortcuts/res/xml/shortcuts.xml b/java/com/android/dialer/shortcuts/res/xml/shortcuts.xml
index 5e8f58d1f..49149e3e1 100644
--- a/java/com/android/dialer/shortcuts/res/xml/shortcuts.xml
+++ b/java/com/android/dialer/shortcuts/res/xml/shortcuts.xml
@@ -24,8 +24,6 @@
<intent
android:action="android.intent.action.INSERT"
- android:data="content://com.android.contacts/contacts"
- android:targetPackage="com.google.android.contacts"
- android:targetClass="com.android.contacts.activities.CompactContactEditorActivity"/>
+ android:data="content://com.android.contacts/contacts"/>
</shortcut>
</shortcuts>
diff --git a/java/com/android/dialer/simulator/SimulatorComponent.java b/java/com/android/dialer/simulator/SimulatorComponent.java
new file mode 100644
index 000000000..a16592e34
--- /dev/null
+++ b/java/com/android/dialer/simulator/SimulatorComponent.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.simulator;
+
+import android.content.Context;
+import dagger.Subcomponent;
+import com.android.dialer.simulator.impl.SimulatorImpl;
+
+/** Subcomponent that can be used to access the simulator implementation. */
+public class SimulatorComponent {
+ private static SimulatorComponent instance;
+ private Simulator simulator;
+
+ public Simulator getSimulator() {
+ if (simulator == null) {
+ simulator = new SimulatorImpl();
+ }
+ return simulator;
+ }
+
+ public static SimulatorComponent get(Context context) {
+ if (instance == null) {
+ instance = new SimulatorComponent();
+ }
+ return instance;
+ }
+
+ /** Used to refer to the root application component. */
+ public interface HasComponent {
+ SimulatorComponent simulatorComponent();
+ }
+}
diff --git a/java/com/android/dialer/simulator/impl/AutoValue_SimulatorCallLog_CallEntry.java b/java/com/android/dialer/simulator/impl/AutoValue_SimulatorCallLog_CallEntry.java
deleted file mode 100644
index 591819819..000000000
--- a/java/com/android/dialer/simulator/impl/AutoValue_SimulatorCallLog_CallEntry.java
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.dialer.simulator.impl;
-
-import android.support.annotation.NonNull;
-import javax.annotation.Generated;
-
-@Generated("com.google.auto.value.processor.AutoValueProcessor")
- final class AutoValue_SimulatorCallLog_CallEntry extends SimulatorCallLog.CallEntry {
-
- private final String number;
- private final int type;
- private final int presentation;
- private final long timeMillis;
-
- private AutoValue_SimulatorCallLog_CallEntry(
- String number,
- int type,
- int presentation,
- long timeMillis) {
- this.number = number;
- this.type = type;
- this.presentation = presentation;
- this.timeMillis = timeMillis;
- }
-
- @NonNull
- @Override
- String getNumber() {
- return number;
- }
-
- @Override
- int getType() {
- return type;
- }
-
- @Override
- int getPresentation() {
- return presentation;
- }
-
- @Override
- long getTimeMillis() {
- return timeMillis;
- }
-
- @Override
- public String toString() {
- return "CallEntry{"
- + "number=" + number + ", "
- + "type=" + type + ", "
- + "presentation=" + presentation + ", "
- + "timeMillis=" + timeMillis
- + "}";
- }
-
- @Override
- public boolean equals(Object o) {
- if (o == this) {
- return true;
- }
- if (o instanceof SimulatorCallLog.CallEntry) {
- SimulatorCallLog.CallEntry that = (SimulatorCallLog.CallEntry) o;
- return (this.number.equals(that.getNumber()))
- && (this.type == that.getType())
- && (this.presentation == that.getPresentation())
- && (this.timeMillis == that.getTimeMillis());
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- int h = 1;
- h *= 1000003;
- h ^= this.number.hashCode();
- h *= 1000003;
- h ^= this.type;
- h *= 1000003;
- h ^= this.presentation;
- h *= 1000003;
- h ^= (this.timeMillis >>> 32) ^ this.timeMillis;
- return h;
- }
-
- static final class Builder extends SimulatorCallLog.CallEntry.Builder {
- private String number;
- private Integer type;
- private Integer presentation;
- private Long timeMillis;
- Builder() {
- }
- private Builder(SimulatorCallLog.CallEntry source) {
- this.number = source.getNumber();
- this.type = source.getType();
- this.presentation = source.getPresentation();
- this.timeMillis = source.getTimeMillis();
- }
- @Override
- SimulatorCallLog.CallEntry.Builder setNumber(String number) {
- this.number = number;
- return this;
- }
- @Override
- SimulatorCallLog.CallEntry.Builder setType(int type) {
- this.type = type;
- return this;
- }
- @Override
- SimulatorCallLog.CallEntry.Builder setPresentation(int presentation) {
- this.presentation = presentation;
- return this;
- }
- @Override
- SimulatorCallLog.CallEntry.Builder setTimeMillis(long timeMillis) {
- this.timeMillis = timeMillis;
- return this;
- }
- @Override
- SimulatorCallLog.CallEntry build() {
- String missing = "";
- if (this.number == null) {
- missing += " number";
- }
- if (this.type == null) {
- missing += " type";
- }
- if (this.presentation == null) {
- missing += " presentation";
- }
- if (this.timeMillis == null) {
- missing += " timeMillis";
- }
- if (!missing.isEmpty()) {
- throw new IllegalStateException("Missing required properties:" + missing);
- }
- return new AutoValue_SimulatorCallLog_CallEntry(
- this.number,
- this.type,
- this.presentation,
- this.timeMillis);
- }
- }
-
-}
diff --git a/java/com/android/dialer/simulator/impl/AutoValue_SimulatorContacts_Contact.java b/java/com/android/dialer/simulator/impl/AutoValue_SimulatorContacts_Contact.java
deleted file mode 100644
index 00295f359..000000000
--- a/java/com/android/dialer/simulator/impl/AutoValue_SimulatorContacts_Contact.java
+++ /dev/null
@@ -1,231 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.dialer.simulator.impl;
-
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import java.io.ByteArrayOutputStream;
-import java.util.List;
-import javax.annotation.Generated;
-
-@Generated("com.google.auto.value.processor.AutoValueProcessor")
- final class AutoValue_SimulatorContacts_Contact extends SimulatorContacts.Contact {
-
- private final String accountType;
- private final String accountName;
- private final String name;
- private final boolean isStarred;
- private final ByteArrayOutputStream photoStream;
- private final List<SimulatorContacts.PhoneNumber> phoneNumbers;
- private final List<SimulatorContacts.Email> emails;
-
- private AutoValue_SimulatorContacts_Contact(
- String accountType,
- String accountName,
- @Nullable String name,
- boolean isStarred,
- @Nullable ByteArrayOutputStream photoStream,
- List<SimulatorContacts.PhoneNumber> phoneNumbers,
- List<SimulatorContacts.Email> emails) {
- this.accountType = accountType;
- this.accountName = accountName;
- this.name = name;
- this.isStarred = isStarred;
- this.photoStream = photoStream;
- this.phoneNumbers = phoneNumbers;
- this.emails = emails;
- }
-
- @NonNull
- @Override
- String getAccountType() {
- return accountType;
- }
-
- @NonNull
- @Override
- String getAccountName() {
- return accountName;
- }
-
- @Nullable
- @Override
- String getName() {
- return name;
- }
-
- @Override
- boolean getIsStarred() {
- return isStarred;
- }
-
- @Nullable
- @Override
- ByteArrayOutputStream getPhotoStream() {
- return photoStream;
- }
-
- @NonNull
- @Override
- List<SimulatorContacts.PhoneNumber> getPhoneNumbers() {
- return phoneNumbers;
- }
-
- @NonNull
- @Override
- List<SimulatorContacts.Email> getEmails() {
- return emails;
- }
-
- @Override
- public String toString() {
- return "Contact{"
- + "accountType=" + accountType + ", "
- + "accountName=" + accountName + ", "
- + "name=" + name + ", "
- + "isStarred=" + isStarred + ", "
- + "photoStream=" + photoStream + ", "
- + "phoneNumbers=" + phoneNumbers + ", "
- + "emails=" + emails
- + "}";
- }
-
- @Override
- public boolean equals(Object o) {
- if (o == this) {
- return true;
- }
- if (o instanceof SimulatorContacts.Contact) {
- SimulatorContacts.Contact that = (SimulatorContacts.Contact) o;
- return (this.accountType.equals(that.getAccountType()))
- && (this.accountName.equals(that.getAccountName()))
- && ((this.name == null) ? (that.getName() == null) : this.name.equals(that.getName()))
- && (this.isStarred == that.getIsStarred())
- && ((this.photoStream == null) ? (that.getPhotoStream() == null) : this.photoStream.equals(that.getPhotoStream()))
- && (this.phoneNumbers.equals(that.getPhoneNumbers()))
- && (this.emails.equals(that.getEmails()));
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- int h = 1;
- h *= 1000003;
- h ^= this.accountType.hashCode();
- h *= 1000003;
- h ^= this.accountName.hashCode();
- h *= 1000003;
- h ^= (name == null) ? 0 : this.name.hashCode();
- h *= 1000003;
- h ^= this.isStarred ? 1231 : 1237;
- h *= 1000003;
- h ^= (photoStream == null) ? 0 : this.photoStream.hashCode();
- h *= 1000003;
- h ^= this.phoneNumbers.hashCode();
- h *= 1000003;
- h ^= this.emails.hashCode();
- return h;
- }
-
- static final class Builder extends SimulatorContacts.Contact.Builder {
- private String accountType;
- private String accountName;
- private String name;
- private Boolean isStarred;
- private ByteArrayOutputStream photoStream;
- private List<SimulatorContacts.PhoneNumber> phoneNumbers;
- private List<SimulatorContacts.Email> emails;
- Builder() {
- }
- private Builder(SimulatorContacts.Contact source) {
- this.accountType = source.getAccountType();
- this.accountName = source.getAccountName();
- this.name = source.getName();
- this.isStarred = source.getIsStarred();
- this.photoStream = source.getPhotoStream();
- this.phoneNumbers = source.getPhoneNumbers();
- this.emails = source.getEmails();
- }
- @Override
- SimulatorContacts.Contact.Builder setAccountType(String accountType) {
- this.accountType = accountType;
- return this;
- }
- @Override
- SimulatorContacts.Contact.Builder setAccountName(String accountName) {
- this.accountName = accountName;
- return this;
- }
- @Override
- SimulatorContacts.Contact.Builder setName(@Nullable String name) {
- this.name = name;
- return this;
- }
- @Override
- SimulatorContacts.Contact.Builder setIsStarred(boolean isStarred) {
- this.isStarred = isStarred;
- return this;
- }
- @Override
- SimulatorContacts.Contact.Builder setPhotoStream(@Nullable ByteArrayOutputStream photoStream) {
- this.photoStream = photoStream;
- return this;
- }
- @Override
- SimulatorContacts.Contact.Builder setPhoneNumbers(List<SimulatorContacts.PhoneNumber> phoneNumbers) {
- this.phoneNumbers = phoneNumbers;
- return this;
- }
- @Override
- SimulatorContacts.Contact.Builder setEmails(List<SimulatorContacts.Email> emails) {
- this.emails = emails;
- return this;
- }
- @Override
- SimulatorContacts.Contact build() {
- String missing = "";
- if (this.accountType == null) {
- missing += " accountType";
- }
- if (this.accountName == null) {
- missing += " accountName";
- }
- if (this.isStarred == null) {
- missing += " isStarred";
- }
- if (this.phoneNumbers == null) {
- missing += " phoneNumbers";
- }
- if (this.emails == null) {
- missing += " emails";
- }
- if (!missing.isEmpty()) {
- throw new IllegalStateException("Missing required properties:" + missing);
- }
- return new AutoValue_SimulatorContacts_Contact(
- this.accountType,
- this.accountName,
- this.name,
- this.isStarred,
- this.photoStream,
- this.phoneNumbers,
- this.emails);
- }
- }
-
-}
diff --git a/java/com/android/dialer/simulator/impl/AutoValue_SimulatorVoicemail_Voicemail.java b/java/com/android/dialer/simulator/impl/AutoValue_SimulatorVoicemail_Voicemail.java
deleted file mode 100644
index 58934801c..000000000
--- a/java/com/android/dialer/simulator/impl/AutoValue_SimulatorVoicemail_Voicemail.java
+++ /dev/null
@@ -1,184 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.dialer.simulator.impl;
-
-import android.support.annotation.NonNull;
-import javax.annotation.Generated;
-
-@Generated("com.google.auto.value.processor.AutoValueProcessor")
- final class AutoValue_SimulatorVoicemail_Voicemail extends SimulatorVoicemail.Voicemail {
-
- private final String phoneNumber;
- private final String transcription;
- private final long durationSeconds;
- private final long timeMillis;
- private final boolean isRead;
-
- private AutoValue_SimulatorVoicemail_Voicemail(
- String phoneNumber,
- String transcription,
- long durationSeconds,
- long timeMillis,
- boolean isRead) {
- this.phoneNumber = phoneNumber;
- this.transcription = transcription;
- this.durationSeconds = durationSeconds;
- this.timeMillis = timeMillis;
- this.isRead = isRead;
- }
-
- @NonNull
- @Override
- String getPhoneNumber() {
- return phoneNumber;
- }
-
- @NonNull
- @Override
- String getTranscription() {
- return transcription;
- }
-
- @Override
- long getDurationSeconds() {
- return durationSeconds;
- }
-
- @Override
- long getTimeMillis() {
- return timeMillis;
- }
-
- @Override
- boolean getIsRead() {
- return isRead;
- }
-
- @Override
- public String toString() {
- return "Voicemail{"
- + "phoneNumber=" + phoneNumber + ", "
- + "transcription=" + transcription + ", "
- + "durationSeconds=" + durationSeconds + ", "
- + "timeMillis=" + timeMillis + ", "
- + "isRead=" + isRead
- + "}";
- }
-
- @Override
- public boolean equals(Object o) {
- if (o == this) {
- return true;
- }
- if (o instanceof SimulatorVoicemail.Voicemail) {
- SimulatorVoicemail.Voicemail that = (SimulatorVoicemail.Voicemail) o;
- return (this.phoneNumber.equals(that.getPhoneNumber()))
- && (this.transcription.equals(that.getTranscription()))
- && (this.durationSeconds == that.getDurationSeconds())
- && (this.timeMillis == that.getTimeMillis())
- && (this.isRead == that.getIsRead());
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- int h = 1;
- h *= 1000003;
- h ^= this.phoneNumber.hashCode();
- h *= 1000003;
- h ^= this.transcription.hashCode();
- h *= 1000003;
- h ^= (this.durationSeconds >>> 32) ^ this.durationSeconds;
- h *= 1000003;
- h ^= (this.timeMillis >>> 32) ^ this.timeMillis;
- h *= 1000003;
- h ^= this.isRead ? 1231 : 1237;
- return h;
- }
-
- static final class Builder extends SimulatorVoicemail.Voicemail.Builder {
- private String phoneNumber;
- private String transcription;
- private Long durationSeconds;
- private Long timeMillis;
- private Boolean isRead;
- Builder() {
- }
- private Builder(SimulatorVoicemail.Voicemail source) {
- this.phoneNumber = source.getPhoneNumber();
- this.transcription = source.getTranscription();
- this.durationSeconds = source.getDurationSeconds();
- this.timeMillis = source.getTimeMillis();
- this.isRead = source.getIsRead();
- }
- @Override
- SimulatorVoicemail.Voicemail.Builder setPhoneNumber(String phoneNumber) {
- this.phoneNumber = phoneNumber;
- return this;
- }
- @Override
- SimulatorVoicemail.Voicemail.Builder setTranscription(String transcription) {
- this.transcription = transcription;
- return this;
- }
- @Override
- SimulatorVoicemail.Voicemail.Builder setDurationSeconds(long durationSeconds) {
- this.durationSeconds = durationSeconds;
- return this;
- }
- @Override
- SimulatorVoicemail.Voicemail.Builder setTimeMillis(long timeMillis) {
- this.timeMillis = timeMillis;
- return this;
- }
- @Override
- SimulatorVoicemail.Voicemail.Builder setIsRead(boolean isRead) {
- this.isRead = isRead;
- return this;
- }
- @Override
- SimulatorVoicemail.Voicemail build() {
- String missing = "";
- if (this.phoneNumber == null) {
- missing += " phoneNumber";
- }
- if (this.transcription == null) {
- missing += " transcription";
- }
- if (this.durationSeconds == null) {
- missing += " durationSeconds";
- }
- if (this.timeMillis == null) {
- missing += " timeMillis";
- }
- if (this.isRead == null) {
- missing += " isRead";
- }
- if (!missing.isEmpty()) {
- throw new IllegalStateException("Missing required properties:" + missing);
- }
- return new AutoValue_SimulatorVoicemail_Voicemail(
- this.phoneNumber,
- this.transcription,
- this.durationSeconds,
- this.timeMillis,
- this.isRead);
- }
- }
-
-}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorCallLog.java b/java/com/android/dialer/simulator/impl/SimulatorCallLog.java
index 9ace047d0..f127d5603 100644
--- a/java/com/android/dialer/simulator/impl/SimulatorCallLog.java
+++ b/java/com/android/dialer/simulator/impl/SimulatorCallLog.java
@@ -26,7 +26,7 @@ import android.provider.CallLog.Calls;
import android.support.annotation.NonNull;
import android.support.annotation.WorkerThread;
import com.android.dialer.common.Assert;
-
+import com.google.auto.value.AutoValue;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
@@ -96,7 +96,7 @@ final class SimulatorCallLog {
}
}
-
+ @AutoValue
abstract static class CallEntry {
@NonNull
abstract String getNumber();
@@ -121,7 +121,7 @@ final class SimulatorCallLog {
return values;
}
-
+ @AutoValue.Builder
abstract static class Builder {
abstract Builder setNumber(@NonNull String number);
diff --git a/java/com/android/dialer/simulator/impl/SimulatorContacts.java b/java/com/android/dialer/simulator/impl/SimulatorContacts.java
index 89315094a..c5e25b357 100644
--- a/java/com/android/dialer/simulator/impl/SimulatorContacts.java
+++ b/java/com/android/dialer/simulator/impl/SimulatorContacts.java
@@ -31,7 +31,7 @@ import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
import android.text.TextUtils;
import com.android.dialer.common.Assert;
-
+import com.google.auto.value.AutoValue;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.List;
@@ -190,7 +190,7 @@ final class SimulatorContacts {
}
}
-
+ @AutoValue
abstract static class Contact {
@NonNull
abstract String getAccountType();
@@ -221,7 +221,7 @@ final class SimulatorContacts {
.setEmails(new ArrayList<>());
}
-
+ @AutoValue.Builder
abstract static class Builder {
@NonNull private final List<PhoneNumber> phoneNumbers = new ArrayList<>();
@NonNull private final List<Email> emails = new ArrayList<>();
diff --git a/java/com/android/dialer/simulator/impl/SimulatorImpl.java b/java/com/android/dialer/simulator/impl/SimulatorImpl.java
new file mode 100644
index 000000000..9c6826940
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/SimulatorImpl.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.simulator.impl;
+
+import android.content.Context;
+import android.view.ActionProvider;
+import com.android.dialer.buildtype.BuildType;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.simulator.Simulator;
+import javax.inject.Inject;
+
+/** The entry point for the simulator feature. */
+final public class SimulatorImpl implements Simulator {
+ @Inject
+ public SimulatorImpl() {}
+
+ @Override
+ public boolean shouldShow() {
+ return BuildType.get() == BuildType.BUGFOOD || LogUtil.isDebugEnabled();
+ }
+
+ @Override
+ public ActionProvider getActionProvider(Context context) {
+ return new SimulatorActionProvider(context);
+ }
+}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorModule.java b/java/com/android/dialer/simulator/impl/SimulatorModule.java
index 0f8ad3954..c0cca271b 100644
--- a/java/com/android/dialer/simulator/impl/SimulatorModule.java
+++ b/java/com/android/dialer/simulator/impl/SimulatorModule.java
@@ -16,19 +16,15 @@
package com.android.dialer.simulator.impl;
-import android.content.Context;
-import android.view.ActionProvider;
import com.android.dialer.simulator.Simulator;
+import dagger.Binds;
+import dagger.Module;
+import javax.inject.Singleton;
-/** The entry point for the simulator module. */
-public final class SimulatorModule implements Simulator {
- @Override
- public boolean shouldShow() {
- return true;
- }
-
- @Override
- public ActionProvider getActionProvider(Context context) {
- return new SimulatorActionProvider(context);
- }
+/** This module provides an instance of the simulator. */
+@Module
+public abstract class SimulatorModule {
+ @Binds
+ @Singleton
+ public abstract Simulator bindsSimulator(SimulatorImpl simulator);
}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorVoicemail.java b/java/com/android/dialer/simulator/impl/SimulatorVoicemail.java
index ffb9191dc..04de201ae 100644
--- a/java/com/android/dialer/simulator/impl/SimulatorVoicemail.java
+++ b/java/com/android/dialer/simulator/impl/SimulatorVoicemail.java
@@ -26,7 +26,7 @@ import android.support.annotation.WorkerThread;
import android.telecom.PhoneAccountHandle;
import android.telephony.TelephonyManager;
import com.android.dialer.common.Assert;
-
+import com.google.auto.value.AutoValue;
import java.util.concurrent.TimeUnit;
/** Populates the device database with voicemail entries. */
@@ -105,7 +105,7 @@ final class SimulatorVoicemail {
context.getContentResolver().insert(Status.buildSourceUri(context.getPackageName()), values);
}
-
+ @AutoValue
abstract static class Voicemail {
@NonNull
abstract String getPhoneNumber();
@@ -134,7 +134,7 @@ final class SimulatorVoicemail {
return values;
}
-
+ @AutoValue.Builder
abstract static class Builder {
abstract Builder setPhoneNumber(@NonNull String phoneNumber);
diff --git a/java/com/android/dialer/telecom/TelecomUtil.java b/java/com/android/dialer/telecom/TelecomUtil.java
index a11e7f77a..87ddda58b 100644
--- a/java/com/android/dialer/telecom/TelecomUtil.java
+++ b/java/com/android/dialer/telecom/TelecomUtil.java
@@ -23,12 +23,13 @@ import android.content.pm.PackageManager;
import android.net.Uri;
import android.provider.CallLog.Calls;
import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
import android.support.v4.content.ContextCompat;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.text.TextUtils;
-import android.util.Log;
+import com.android.dialer.common.LogUtil;
import java.util.ArrayList;
import java.util.List;
@@ -41,6 +42,8 @@ public class TelecomUtil {
private static final String TAG = "TelecomUtil";
private static boolean sWarningLogged = false;
+ private static Boolean isDefaultDialerForTesting;
+ private static Boolean hasPermissionForTesting;
public static void showInCallScreen(Context context, boolean showDialpad) {
if (hasReadPhoneStatePermission(context)) {
@@ -48,7 +51,7 @@ public class TelecomUtil {
getTelecomManager(context).showInCallScreen(showDialpad);
} catch (SecurityException e) {
// Just in case
- Log.w(TAG, "TelecomManager.showInCallScreen called without permission.");
+ LogUtil.w(TAG, "TelecomManager.showInCallScreen called without permission.");
}
}
}
@@ -59,7 +62,7 @@ public class TelecomUtil {
getTelecomManager(context).silenceRinger();
} catch (SecurityException e) {
// Just in case
- Log.w(TAG, "TelecomManager.silenceRinger called without permission.");
+ LogUtil.w(TAG, "TelecomManager.silenceRinger called without permission.");
}
}
}
@@ -69,7 +72,7 @@ public class TelecomUtil {
try {
getTelecomManager(context).cancelMissedCallsNotification();
} catch (SecurityException e) {
- Log.w(TAG, "TelecomManager.cancelMissedCalls called without permission.");
+ LogUtil.w(TAG, "TelecomManager.cancelMissedCalls called without permission.");
}
}
}
@@ -79,7 +82,7 @@ public class TelecomUtil {
try {
return getTelecomManager(context).getAdnUriForPhoneAccount(handle);
} catch (SecurityException e) {
- Log.w(TAG, "TelecomManager.getAdnUriForPhoneAccount called without permission.");
+ LogUtil.w(TAG, "TelecomManager.getAdnUriForPhoneAccount called without permission.");
}
}
return null;
@@ -95,7 +98,7 @@ public class TelecomUtil {
return getTelecomManager(context).handleMmi(dialString, handle);
}
} catch (SecurityException e) {
- Log.w(TAG, "TelecomManager.handleMmi called without permission.");
+ LogUtil.w(TAG, "TelecomManager.handleMmi called without permission.");
}
}
return false;
@@ -186,11 +189,17 @@ public class TelecomUtil {
}
private static boolean hasPermission(Context context, String permission) {
+ if (hasPermissionForTesting != null) {
+ return hasPermissionForTesting;
+ }
return ContextCompat.checkSelfPermission(context, permission)
== PackageManager.PERMISSION_GRANTED;
}
public static boolean isDefaultDialer(Context context) {
+ if (isDefaultDialerForTesting != null) {
+ return isDefaultDialerForTesting;
+ }
final boolean result =
TextUtils.equals(
context.getPackageName(), getTelecomManager(context).getDefaultDialerPackage());
@@ -199,7 +208,7 @@ public class TelecomUtil {
} else {
if (!sWarningLogged) {
// Log only once to prevent spam.
- Log.w(TAG, "Dialer is not currently set to be default dialer");
+ LogUtil.w(TAG, "Dialer is not currently set to be default dialer");
sWarningLogged = true;
}
}
@@ -209,4 +218,14 @@ public class TelecomUtil {
private static TelecomManager getTelecomManager(Context context) {
return (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
}
+
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ public static void setIsDefaultDialerForTesting(Boolean defaultDialer) {
+ isDefaultDialerForTesting = defaultDialer;
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ public static void setHasPermissionForTesting(Boolean hasPermission) {
+ hasPermissionForTesting = hasPermission;
+ }
}
diff --git a/java/com/android/dialer/theme/res/drawable-hdpi/ic_block_24dp.png b/java/com/android/dialer/theme/res/drawable-hdpi/ic_block_24dp.png
new file mode 100644
index 000000000..2ccc89d24
--- /dev/null
+++ b/java/com/android/dialer/theme/res/drawable-hdpi/ic_block_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_call_arrow.png b/java/com/android/dialer/theme/res/drawable-hdpi/ic_call_arrow.png
index 14a33e39f..14a33e39f 100644
--- a/java/com/android/dialer/app/res/drawable-hdpi/ic_call_arrow.png
+++ b/java/com/android/dialer/theme/res/drawable-hdpi/ic_call_arrow.png
Binary files differ
diff --git a/java/com/android/dialer/theme/res/drawable-mdpi/ic_block_24dp.png b/java/com/android/dialer/theme/res/drawable-mdpi/ic_block_24dp.png
new file mode 100644
index 000000000..ec1b33f0e
--- /dev/null
+++ b/java/com/android/dialer/theme/res/drawable-mdpi/ic_block_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_call_arrow.png b/java/com/android/dialer/theme/res/drawable-mdpi/ic_call_arrow.png
index 169cf2934..169cf2934 100644
--- a/java/com/android/dialer/app/res/drawable-mdpi/ic_call_arrow.png
+++ b/java/com/android/dialer/theme/res/drawable-mdpi/ic_call_arrow.png
Binary files differ
diff --git a/java/com/android/dialer/theme/res/drawable-xhdpi/ic_block_24dp.png b/java/com/android/dialer/theme/res/drawable-xhdpi/ic_block_24dp.png
new file mode 100644
index 000000000..7aba97b65
--- /dev/null
+++ b/java/com/android/dialer/theme/res/drawable-xhdpi/ic_block_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_call_arrow.png b/java/com/android/dialer/theme/res/drawable-xhdpi/ic_call_arrow.png
index 6f1366018..6f1366018 100644
--- a/java/com/android/dialer/app/res/drawable-xhdpi/ic_call_arrow.png
+++ b/java/com/android/dialer/theme/res/drawable-xhdpi/ic_call_arrow.png
Binary files differ
diff --git a/java/com/android/dialer/theme/res/drawable-xxhdpi/ic_block_24dp.png b/java/com/android/dialer/theme/res/drawable-xxhdpi/ic_block_24dp.png
new file mode 100644
index 000000000..fddfa54b8
--- /dev/null
+++ b/java/com/android/dialer/theme/res/drawable-xxhdpi/ic_block_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_call_arrow.png b/java/com/android/dialer/theme/res/drawable-xxhdpi/ic_call_arrow.png
index 0364ee015..0364ee015 100644
--- a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_call_arrow.png
+++ b/java/com/android/dialer/theme/res/drawable-xxhdpi/ic_call_arrow.png
Binary files differ
diff --git a/java/com/android/dialer/theme/res/drawable-xxxhdpi/ic_block_24dp.png b/java/com/android/dialer/theme/res/drawable-xxxhdpi/ic_block_24dp.png
new file mode 100644
index 000000000..0378d1bed
--- /dev/null
+++ b/java/com/android/dialer/theme/res/drawable-xxxhdpi/ic_block_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_call_arrow.png b/java/com/android/dialer/theme/res/drawable-xxxhdpi/ic_call_arrow.png
index 8243c2536..8243c2536 100644
--- a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_call_arrow.png
+++ b/java/com/android/dialer/theme/res/drawable-xxxhdpi/ic_call_arrow.png
Binary files differ
diff --git a/java/com/android/dialer/theme/res/values/dimens.xml b/java/com/android/dialer/theme/res/values/dimens.xml
index 2d11ecc84..fa750c625 100644
--- a/java/com/android/dialer/theme/res/values/dimens.xml
+++ b/java/com/android/dialer/theme/res/values/dimens.xml
@@ -25,4 +25,9 @@
<!-- actionbar height + tab height -->
<dimen name="actionbar_and_tab_height">107dp</dimen>
<dimen name="actionbar_contentInsetStart">72dp</dimen>
+
+ <dimen name="toolbar_title_text_size">20sp</dimen>
+ <dimen name="toolbar_subtitle_text_size">14sp</dimen>
+
+ <dimen name="call_log_icon_margin">4dp</dimen>
</resources>
diff --git a/java/com/android/dialer/theme/res/values/styles.xml b/java/com/android/dialer/theme/res/values/styles.xml
index ac94d0687..b5e89ff48 100644
--- a/java/com/android/dialer/theme/res/values/styles.xml
+++ b/java/com/android/dialer/theme/res/values/styles.xml
@@ -53,4 +53,15 @@
<item name="android:background">@color/actionbar_background_color</item>
<item name="background">@color/actionbar_background_color</item>
</style>
+
+ <style name="toolbar_title_text">
+ <item name="android:textSize">@dimen/toolbar_title_text_size</item>
+ <item name="android:textColor">@color/background_dialer_white</item>
+ <item name="android:fontFamily">sans-serif-medium</item>
+ </style>
+
+ <style name="toolbar_subtitle_text">
+ <item name="android:textSize">@dimen/toolbar_subtitle_text_size</item>
+ <item name="android:textColor">@color/background_dialer_white</item>
+ </style>
</resources>
diff --git a/java/com/android/dialer/util/AndroidManifest.xml b/java/com/android/dialer/util/AndroidManifest.xml
index 499df9b4e..ba22c1781 100644
--- a/java/com/android/dialer/util/AndroidManifest.xml
+++ b/java/com/android/dialer/util/AndroidManifest.xml
@@ -1,3 +1,19 @@
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
<manifest
package="com.android.dialer.util">
</manifest>
diff --git a/java/com/android/dialer/util/PermissionsUtil.java b/java/com/android/dialer/util/PermissionsUtil.java
index 70b96dfe1..5741e734a 100644
--- a/java/com/android/dialer/util/PermissionsUtil.java
+++ b/java/com/android/dialer/util/PermissionsUtil.java
@@ -47,6 +47,10 @@ public class PermissionsUtil {
return hasPermission(context, permission.CAMERA);
}
+ public static boolean hasMicrophonePermissions(Context context) {
+ return hasPermission(context, permission.RECORD_AUDIO);
+ }
+
public static boolean hasPermission(Context context, String permission) {
return ContextCompat.checkSelfPermission(context, permission)
== PackageManager.PERMISSION_GRANTED;
diff --git a/java/com/android/dialer/util/SettingsUtil.java b/java/com/android/dialer/util/SettingsUtil.java
index c61c09b6c..5043c3d56 100644
--- a/java/com/android/dialer/util/SettingsUtil.java
+++ b/java/com/android/dialer/util/SettingsUtil.java
@@ -69,6 +69,15 @@ public class SettingsUtil {
}
}
}
+ getRingtoneName(context, handler, ringtoneUri, msg, defaultRingtone);
+ }
+
+ public static void getRingtoneName(Context context, Handler handler, Uri ringtoneUri, int msg) {
+ getRingtoneName(context, handler, ringtoneUri, msg, false);
+ }
+
+ public static void getRingtoneName(
+ Context context, Handler handler, Uri ringtoneUri, int msg, boolean defaultRingtone) {
CharSequence summary = context.getString(R.string.ringtone_unknown);
// Is it a silent ringtone?
if (ringtoneUri == null) {
diff --git a/java/com/android/dialer/util/ViewUtil.java b/java/com/android/dialer/util/ViewUtil.java
index de08e41a7..81a32f985 100644
--- a/java/com/android/dialer/util/ViewUtil.java
+++ b/java/com/android/dialer/util/ViewUtil.java
@@ -27,6 +27,7 @@ import android.text.TextUtils;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.ViewTreeObserver.OnPreDrawListener;
import android.widget.TextView;
import java.util.Locale;
@@ -113,6 +114,18 @@ public class ViewUtil {
});
}
+ public static void doOnGlobalLayout(@NonNull final View view, final ViewRunnable runnable) {
+ view.getViewTreeObserver()
+ .addOnGlobalLayoutListener(
+ new OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ runnable.run(view);
+ }
+ });
+ }
+
/**
* Returns {@code true} if animations should be disabled.
*
diff --git a/java/com/android/dialer/widget/MessageFragment.java b/java/com/android/dialer/widget/MessageFragment.java
new file mode 100644
index 000000000..ab47f2463
--- /dev/null
+++ b/java/com/android/dialer/widget/MessageFragment.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.widget;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.FragmentUtils;
+
+/** Fragment used to compose call with message fragment. */
+public class MessageFragment extends Fragment implements OnClickListener, TextWatcher {
+ private static final String CHAR_LIMIT_KEY = "char_limit";
+ private static final String SHOW_SEND_ICON_KEY = "show_send_icon";
+ private static final String MESSAGE_LIST_KEY = "message_list";
+
+ public static final int NO_CHAR_LIMIT = -1;
+
+ private EditText customMessage;
+ private ImageView sendMessage;
+ private TextView remainingChar;
+ private int charLimit;
+
+ private static MessageFragment newInstance(Builder builder) {
+ MessageFragment fragment = new MessageFragment();
+ Bundle args = new Bundle();
+ args.putInt(CHAR_LIMIT_KEY, builder.charLimit);
+ args.putBoolean(SHOW_SEND_ICON_KEY, builder.showSendIcon);
+ args.putStringArray(MESSAGE_LIST_KEY, builder.messages);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Nullable
+ public String getMessage() {
+ return customMessage == null ? null : customMessage.getText().toString();
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_message, container, false);
+
+ sendMessage = (ImageView) view.findViewById(R.id.send_message);
+ if (getArguments().getBoolean(SHOW_SEND_ICON_KEY, false)) {
+ sendMessage.setVisibility(View.VISIBLE);
+ sendMessage.setEnabled(false);
+ sendMessage.setOnClickListener(this);
+ }
+
+ customMessage = (EditText) view.findViewById(R.id.custom_message);
+ customMessage.addTextChangedListener(this);
+ charLimit = getArguments().getInt(CHAR_LIMIT_KEY, NO_CHAR_LIMIT);
+ if (charLimit != NO_CHAR_LIMIT) {
+ remainingChar = (TextView) view.findViewById(R.id.remaining_characters);
+ remainingChar.setVisibility(View.VISIBLE);
+ remainingChar = (TextView) view.findViewById(R.id.remaining_characters);
+ remainingChar.setText("" + charLimit);
+ customMessage.setFilters(new InputFilter[] {new InputFilter.LengthFilter(charLimit)});
+ }
+
+ LinearLayout messageContainer = (LinearLayout) view.findViewById(R.id.message_container);
+ for (String message : getArguments().getStringArray(MESSAGE_LIST_KEY)) {
+ TextView textView = (TextView) inflater.inflate(R.layout.selectable_text_view, null);
+ textView.setOnClickListener(this);
+ textView.setText(message);
+ messageContainer.addView(textView);
+ }
+ return view;
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (view == sendMessage) {
+ getListener().onMessageFragmentSendMessage(customMessage.getText().toString());
+ } else if (view.getId() == R.id.selectable_text_view) {
+ customMessage.setText(((TextView) view).getText());
+ customMessage.setSelection(customMessage.getText().length());
+ } else {
+ Assert.fail("Unknown view clicked");
+ }
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ sendMessage.setEnabled(s.length() > 0);
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (charLimit != NO_CHAR_LIMIT) {
+ remainingChar.setText("" + (charLimit - s.length()));
+ }
+ getListener().onMessageFragmentAfterTextChange(s.toString());
+ }
+
+ private Listener getListener() {
+ return FragmentUtils.getParentUnsafe(this, Listener.class);
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /** Builder for {@link MessageFragment}. */
+ public static class Builder {
+ private String[] messages;
+ private boolean showSendIcon;
+ private int charLimit = NO_CHAR_LIMIT;
+
+ /**
+ * @throws NullPointerException if message is null
+ * @throws IllegalArgumentException if messages.length is outside the range [1,3].
+ */
+ public Builder setMessages(String... messages) {
+ // Since we only allow up to 3 messages, crash if more are set.
+ Assert.checkArgument(messages.length > 0 && messages.length <= 3);
+ this.messages = messages;
+ return this;
+ }
+
+ public Builder showSendIcon() {
+ showSendIcon = true;
+ return this;
+ }
+
+ public Builder setCharLimit(int charLimit) {
+ this.charLimit = charLimit;
+ return this;
+ }
+
+ public MessageFragment build() {
+ return MessageFragment.newInstance(this);
+ }
+ }
+
+ /** Interface for parent activity to implement to listen for important events. */
+ public interface Listener {
+ void onMessageFragmentSendMessage(String message);
+
+ void onMessageFragmentAfterTextChange(String message);
+ }
+}
diff --git a/java/com/android/dialer/widget/res/color/dialer_tint_state.xml b/java/com/android/dialer/widget/res/color/dialer_tint_state.xml
new file mode 100644
index 000000000..c29f334ac
--- /dev/null
+++ b/java/com/android/dialer/widget/res/color/dialer_tint_state.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@color/dialer_edit_text_hint_color" android:state_enabled="false"/>
+ <item android:color="@color/dialer_theme_color"/>
+</selector> \ No newline at end of file
diff --git a/java/com/android/dialer/widget/res/layout/fragment_message.xml b/java/com/android/dialer/widget/res/layout/fragment_message.xml
new file mode 100644
index 000000000..f09c54f57
--- /dev/null
+++ b/java/com/android/dialer/widget/res/layout/fragment_message.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:gravity="bottom"
+ android:background="@color/background_dialer_white">
+
+ <LinearLayout
+ android:id="@+id/message_container"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/message_divider_height"
+ android:background="#12000000"/>
+
+ <RelativeLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <EditText
+ android:id="@+id/custom_message"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="@dimen/textview_item_padding"
+ android:textSize="@dimen/message_item_text_size"
+ android:hint="@string/custom_message_hint"
+ android:textColor="@color/dialer_primary_text_color"
+ android:textColorHint="@color/dialer_edit_text_hint_color"
+ android:background="@color/background_dialer_white"
+ android:textCursorDrawable="@drawable/searchedittext_custom_cursor"
+ android:layout_toStartOf="@+id/count_and_send_container"/>
+
+ <LinearLayout
+ android:id="@+id/count_and_send_container"
+ android:orientation="vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:layout_centerVertical="true"
+ android:layout_marginEnd="@dimen/textview_item_padding"
+ android:gravity="center">
+
+ <ImageView
+ android:id="@+id/send_message"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:src="@drawable/quantum_ic_send_white_24"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ android:tint="@color/dialer_tint_state"/>
+
+ <TextView
+ android:id="@+id/remaining_characters"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:textSize="@dimen/message_remaining_char_text_size"
+ android:textColor="@color/dialer_edit_text_hint_color"/>
+ </LinearLayout>
+ </RelativeLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/java/com/android/dialer/widget/res/layout/selectable_text_view.xml b/java/com/android/dialer/widget/res/layout/selectable_text_view.xml
new file mode 100644
index 000000000..3d120d13d
--- /dev/null
+++ b/java/com/android/dialer/widget/res/layout/selectable_text_view.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<TextView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/selectable_text_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="16sp"
+ android:textColor="@color/dialer_primary_text_color"
+ android:padding="16dp"
+ android:background="@drawable/item_background_material_light"/> \ No newline at end of file
diff --git a/java/com/android/dialer/widget/res/values/dimens.xml b/java/com/android/dialer/widget/res/values/dimens.xml
new file mode 100644
index 000000000..6c4ea604f
--- /dev/null
+++ b/java/com/android/dialer/widget/res/values/dimens.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources>
+ <!-- Message Fragment -->
+ <dimen name="message_item_text_size">16sp</dimen>
+ <dimen name="textview_item_padding">16dp</dimen>
+ <dimen name="message_remaining_char_text_size">12sp</dimen>
+ <dimen name="message_divider_height">1dp</dimen>
+</resources> \ No newline at end of file
diff --git a/java/com/android/dialer/widget/res/values/strings.xml b/java/com/android/dialer/widget/res/values/strings.xml
new file mode 100644
index 000000000..6904c2de1
--- /dev/null
+++ b/java/com/android/dialer/widget/res/values/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Hint in a text field to compose a custom message to send with a phone call [CHAR LIMIT=27] -->
+ <string name="custom_message_hint">Write a custom message</string>
+</resources> \ No newline at end of file
diff --git a/java/com/android/incallui/AnswerScreenPresenter.java b/java/com/android/incallui/AnswerScreenPresenter.java
index a21876b2b..442ad260f 100644
--- a/java/com/android/incallui/AnswerScreenPresenter.java
+++ b/java/com/android/incallui/AnswerScreenPresenter.java
@@ -20,6 +20,7 @@ import android.content.Context;
import android.support.annotation.FloatRange;
import android.support.annotation.NonNull;
import android.support.v4.os.UserManagerCompat;
+import android.telecom.VideoProfile;
import com.android.dialer.common.Assert;
import com.android.dialer.common.LogUtil;
import com.android.incallui.answer.protocol.AnswerScreen;
@@ -71,18 +72,26 @@ public class AnswerScreenPresenter
}
@Override
- public void onAnswer(int videoState) {
+ public void onAnswer(boolean answerVideoAsAudio) {
if (answerScreen.isVideoUpgradeRequest()) {
- call.acceptUpgradeRequest(videoState);
+ if (answerVideoAsAudio) {
+ call.getVideoTech().acceptVideoRequestAsAudio();
+ } else {
+ call.getVideoTech().acceptVideoRequest();
+ }
} else {
- call.answer(videoState);
+ if (answerVideoAsAudio) {
+ call.answer(VideoProfile.STATE_AUDIO_ONLY);
+ } else {
+ call.answer();
+ }
}
}
@Override
public void onReject() {
if (answerScreen.isVideoUpgradeRequest()) {
- call.declineUpgradeRequest();
+ call.getVideoTech().declineVideoRequest();
} else {
call.reject(false /* rejectWithMessage */, null);
}
diff --git a/java/com/android/incallui/AnswerScreenPresenterStub.java b/java/com/android/incallui/AnswerScreenPresenterStub.java
index fc47bf5b0..fc4e7df65 100644
--- a/java/com/android/incallui/AnswerScreenPresenterStub.java
+++ b/java/com/android/incallui/AnswerScreenPresenterStub.java
@@ -34,7 +34,7 @@ public class AnswerScreenPresenterStub implements AnswerScreenDelegate {
public void onRejectCallWithMessage(String message) {}
@Override
- public void onAnswer(int videoState) {}
+ public void onAnswer(boolean answerVideoAsAudio) {}
@Override
public void onReject() {}
diff --git a/java/com/android/incallui/CallButtonPresenter.java b/java/com/android/incallui/CallButtonPresenter.java
index d6f4cddc9..c5c43f7aa 100644
--- a/java/com/android/incallui/CallButtonPresenter.java
+++ b/java/com/android/incallui/CallButtonPresenter.java
@@ -17,17 +17,13 @@
package com.android.incallui;
import android.content.Context;
-import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.os.UserManagerCompat;
import android.telecom.CallAudioState;
-import android.telecom.InCallService.VideoCall;
-import android.telecom.VideoProfile;
import com.android.contacts.common.compat.CallCompat;
import com.android.dialer.common.Assert;
import com.android.dialer.common.LogUtil;
-import com.android.dialer.compat.SdkVersionOverride;
import com.android.dialer.logging.Logger;
import com.android.dialer.logging.nano.DialerImpression;
import com.android.incallui.AudioModeProvider.AudioModeListener;
@@ -39,6 +35,7 @@ import com.android.incallui.InCallPresenter.InCallStateListener;
import com.android.incallui.InCallPresenter.IncomingCallListener;
import com.android.incallui.call.CallList;
import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.DialerCall.CameraDirection;
import com.android.incallui.call.TelecomAdapter;
import com.android.incallui.call.VideoUtils;
import com.android.incallui.incall.protocol.InCallButtonIds;
@@ -212,6 +209,13 @@ public class CallButtonPresenter
@Override
public void muteClicked(boolean checked) {
LogUtil.v("CallButtonPresenter", "turning on mute: " + checked);
+ Logger.get(mContext)
+ .logCallImpression(
+ checked
+ ? DialerImpression.Type.IN_CALL_SCREEN_TURN_ON_MUTE
+ : DialerImpression.Type.IN_CALL_SCREEN_TURN_OFF_MUTE,
+ mCall.getUniqueCallId(),
+ mCall.getTimeAddedMs());
TelecomAdapter.getInstance().mute(checked);
}
@@ -262,18 +266,8 @@ public class CallButtonPresenter
@Override
public void changeToVideoClicked() {
- VideoCall videoCall = mCall.getVideoCall();
- if (videoCall == null) {
- return;
- }
- int currVideoState = mCall.getVideoState();
- int currUnpausedVideoState = VideoUtils.getUnPausedVideoState(currVideoState);
- currUnpausedVideoState |= VideoProfile.STATE_BIDIRECTIONAL;
-
- VideoProfile videoProfile = new VideoProfile(currUnpausedVideoState);
- videoCall.sendSessionModifyRequest(videoProfile);
- mCall.setSessionModificationState(
- DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE);
+ LogUtil.enterBlock("CallButtonPresenter.changeToVideoClicked");
+ mCall.getVideoTech().upgradeToVideo();
}
@Override
@@ -300,26 +294,25 @@ public class CallButtonPresenter
InCallCameraManager cameraManager = InCallPresenter.getInstance().getInCallCameraManager();
cameraManager.setUseFrontFacingCamera(useFrontFacingCamera);
- VideoCall videoCall = mCall.getVideoCall();
- if (videoCall == null) {
- return;
- }
-
String cameraId = cameraManager.getActiveCameraId();
if (cameraId != null) {
final int cameraDir =
cameraManager.isUsingFrontFacingCamera()
- ? DialerCall.VideoSettings.CAMERA_DIRECTION_FRONT_FACING
- : DialerCall.VideoSettings.CAMERA_DIRECTION_BACK_FACING;
- mCall.getVideoSettings().setCameraDir(cameraDir);
- videoCall.setCamera(cameraId);
- videoCall.requestCameraCapabilities();
+ ? CameraDirection.CAMERA_DIRECTION_FRONT_FACING
+ : CameraDirection.CAMERA_DIRECTION_BACK_FACING;
+ mCall.setCameraDir(cameraDir);
+ mCall.getVideoTech().setCamera(cameraId);
}
}
@Override
public void toggleCameraClicked() {
LogUtil.i("CallButtonPresenter.toggleCameraClicked", "");
+ Logger.get(mContext)
+ .logCallImpression(
+ DialerImpression.Type.IN_CALL_SCREEN_SWAP_CAMERA,
+ mCall.getUniqueCallId(),
+ mCall.getTimeAddedMs());
switchCameraClicked(
!InCallPresenter.getInstance().getInCallCameraManager().isUsingFrontFacingCamera());
}
@@ -333,24 +326,19 @@ public class CallButtonPresenter
@Override
public void pauseVideoClicked(boolean pause) {
LogUtil.i("CallButtonPresenter.pauseVideoClicked", "%s", pause ? "pause" : "unpause");
- VideoCall videoCall = mCall.getVideoCall();
- if (videoCall == null) {
- return;
- }
- int currUnpausedVideoState = VideoUtils.getUnPausedVideoState(mCall.getVideoState());
+ Logger.get(mContext)
+ .logCallImpression(
+ pause
+ ? DialerImpression.Type.IN_CALL_SCREEN_TURN_OFF_VIDEO
+ : DialerImpression.Type.IN_CALL_SCREEN_TURN_ON_VIDEO,
+ mCall.getUniqueCallId(),
+ mCall.getTimeAddedMs());
+
if (pause) {
- videoCall.setCamera(null);
- VideoProfile videoProfile =
- new VideoProfile(currUnpausedVideoState & ~VideoProfile.STATE_TX_ENABLED);
- videoCall.sendSessionModifyRequest(videoProfile);
+ mCall.getVideoTech().stopTransmission();
} else {
- InCallCameraManager cameraManager = InCallPresenter.getInstance().getInCallCameraManager();
- videoCall.setCamera(cameraManager.getActiveCameraId());
- VideoProfile videoProfile =
- new VideoProfile(currUnpausedVideoState | VideoProfile.STATE_TX_ENABLED);
- videoCall.sendSessionModifyRequest(videoProfile);
- mCall.setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE);
+ mCall.getVideoTech().resumeTransmission();
}
mInCallButtonUi.setVideoPaused(pause);
@@ -386,7 +374,7 @@ public class CallButtonPresenter
*/
private void updateButtonsState(DialerCall call) {
LogUtil.v("CallButtonPresenter.updateButtonsState", "");
- final boolean isVideo = VideoUtils.isVideoCall(call);
+ final boolean isVideo = call.isVideoCall();
// Common functionality (audio, hold, etc).
// Show either HOLD or SWAP, but not both. If neither HOLD or SWAP is available:
@@ -402,7 +390,7 @@ public class CallButtonPresenter
final boolean showAddCall =
TelecomAdapter.getInstance().canAddCall() && UserManagerCompat.isUserUnlocked(mContext);
final boolean showMerge = call.can(android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE);
- final boolean showUpgradeToVideo = !isVideo && hasVideoCallCapabilities(call);
+ final boolean showUpgradeToVideo = !isVideo && (hasVideoCallCapabilities(call));
final boolean showDowngradeToAudio = isVideo && isDowngradeToAudioSupported(call);
final boolean showMute = call.can(android.telecom.Call.Details.CAPABILITY_MUTE);
@@ -427,8 +415,7 @@ public class CallButtonPresenter
InCallButtonIds.BUTTON_SWITCH_CAMERA, isVideo && hasCameraPermission);
mInCallButtonUi.showButton(InCallButtonIds.BUTTON_PAUSE_VIDEO, showPauseVideo);
if (isVideo) {
- mInCallButtonUi.setVideoPaused(
- !VideoUtils.isTransmissionEnabled(call) || !hasCameraPermission);
+ mInCallButtonUi.setVideoPaused(!call.getVideoTech().isTransmitting() || !hasCameraPermission);
}
mInCallButtonUi.showButton(InCallButtonIds.BUTTON_DIALPAD, true);
mInCallButtonUi.showButton(InCallButtonIds.BUTTON_MERGE, showMerge);
@@ -437,12 +424,7 @@ public class CallButtonPresenter
}
private boolean hasVideoCallCapabilities(DialerCall call) {
- if (SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.M) >= Build.VERSION_CODES.M) {
- return call.can(android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_TX)
- && call.can(android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_RX);
- }
- // In L, this single flag represents both video transmitting and receiving capabilities
- return call.can(android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_TX);
+ return call.getVideoTech().isAvailable();
}
/**
@@ -454,6 +436,7 @@ public class CallButtonPresenter
* @return {@code true} if downgrading to an audio-only call from a video call is supported.
*/
private boolean isDowngradeToAudioSupported(DialerCall call) {
+ // TODO(b/33676907): If there is an RCS video share session, return true here
return !call.can(CallCompat.Details.CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO);
}
diff --git a/java/com/android/incallui/CallCardPresenter.java b/java/com/android/incallui/CallCardPresenter.java
index 930775772..668692d71 100644
--- a/java/com/android/incallui/CallCardPresenter.java
+++ b/java/com/android/incallui/CallCardPresenter.java
@@ -19,7 +19,6 @@ package com.android.incallui;
import static com.android.contacts.common.compat.CallCompat.Details.PROPERTY_ENTERPRISE_CALL;
import android.Manifest;
-import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
@@ -29,6 +28,7 @@ import android.graphics.drawable.Drawable;
import android.hardware.display.DisplayManager;
import android.os.BatteryManager;
import android.os.Handler;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;
@@ -46,9 +46,12 @@ import com.android.contacts.common.util.ContactDisplayUtils;
import com.android.dialer.common.Assert;
import com.android.dialer.common.ConfigProviderBindings;
import com.android.dialer.common.LogUtil;
+import com.android.dialer.compat.ActivityCompat;
+import com.android.dialer.enrichedcall.EnrichedCallComponent;
import com.android.dialer.enrichedcall.EnrichedCallManager;
import com.android.dialer.enrichedcall.Session;
import com.android.dialer.multimedia.MultimediaData;
+import com.android.dialer.oem.MotorolaUtils;
import com.android.incallui.ContactInfoCache.ContactCacheEntry;
import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback;
import com.android.incallui.InCallPresenter.InCallDetailsListener;
@@ -58,14 +61,16 @@ import com.android.incallui.InCallPresenter.InCallStateListener;
import com.android.incallui.InCallPresenter.IncomingCallListener;
import com.android.incallui.call.CallList;
import com.android.incallui.call.DialerCall;
-import com.android.incallui.call.DialerCall.SessionModificationState;
import com.android.incallui.call.DialerCallListener;
+import com.android.incallui.calllocation.CallLocation;
+import com.android.incallui.calllocation.CallLocationComponent;
import com.android.incallui.incall.protocol.ContactPhotoType;
import com.android.incallui.incall.protocol.InCallScreen;
import com.android.incallui.incall.protocol.InCallScreenDelegate;
import com.android.incallui.incall.protocol.PrimaryCallState;
import com.android.incallui.incall.protocol.PrimaryInfo;
import com.android.incallui.incall.protocol.SecondaryInfo;
+import com.android.incallui.videotech.VideoTech;
import java.lang.ref.WeakReference;
/**
@@ -116,7 +121,8 @@ public class CallCardPresenter
private InCallScreen mInCallScreen;
private boolean isInCallScreenReady;
private boolean shouldSendAccessibilityEvent;
- private final String locationModule = null;
+
+ @NonNull private final CallLocation callLocation;
private final Runnable sendAccessibilityEventRunnable =
new Runnable() {
@Override
@@ -135,6 +141,7 @@ public class CallCardPresenter
public CallCardPresenter(Context context) {
LogUtil.i("CallCardController.constructor", null);
mContext = Assert.isNotNull(context).getApplicationContext();
+ callLocation = CallLocationComponent.get(mContext).getCallLocation();
}
private static boolean hasCallSubject(DialerCall call) {
@@ -175,8 +182,7 @@ public class CallCardPresenter
mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY);
}
- EnrichedCallManager.Accessor.getInstance(((Application) mContext))
- .registerStateChangedListener(this);
+ EnrichedCallComponent.get(mContext).getEnrichedCallManager().registerStateChangedListener(this);
// Contact search may have completed before ui is ready.
if (mPrimaryContactInfo != null) {
@@ -189,6 +195,11 @@ public class CallCardPresenter
InCallPresenter.getInstance().addDetailsListener(this);
InCallPresenter.getInstance().addInCallEventListener(this);
isInCallScreenReady = true;
+
+ // Showing the location may have been skipped if the UI wasn't ready during previous layout.
+ if (shouldShowLocation()) {
+ updatePrimaryDisplayInfo();
+ }
}
@Override
@@ -196,7 +207,8 @@ public class CallCardPresenter
LogUtil.i("CallCardController.onInCallScreenUnready", null);
Assert.checkState(isInCallScreenReady);
- EnrichedCallManager.Accessor.getInstance(((Application) mContext))
+ EnrichedCallComponent.get(mContext)
+ .getEnrichedCallManager()
.unregisterStateChangedListener(this);
// stop getting call state changes
InCallPresenter.getInstance().removeListener(this);
@@ -207,6 +219,8 @@ public class CallCardPresenter
mPrimary.removeListener(this);
}
+ callLocation.close();
+
mPrimary = null;
mPrimaryContactInfo = null;
mSecondaryContactInfo = null;
@@ -282,7 +296,6 @@ public class CallCardPresenter
mContext, mPrimary, mPrimary.getState() == DialerCall.State.INCOMING);
updatePrimaryDisplayInfo();
maybeStartSearch(mPrimary, true);
- maybeClearSessionModificationState(mPrimary);
}
if (previousPrimary != null && mPrimary == null) {
@@ -300,7 +313,6 @@ public class CallCardPresenter
mContext, mSecondary, mSecondary.getState() == DialerCall.State.INCOMING);
updateSecondaryDisplayInfo();
maybeStartSearch(mSecondary, false);
- maybeClearSessionModificationState(mSecondary);
}
// Set the call state
@@ -373,25 +385,18 @@ public class CallCardPresenter
@Override
public void onDialerCallUpgradeToVideo() {}
- /**
- * Handles a change to the session modification state for a call.
- *
- * @param sessionModificationState The new session modification state.
- */
+ /** Handles a change to the session modification state for a call. */
@Override
- public void onDialerCallSessionModificationStateChange(
- @SessionModificationState int sessionModificationState) {
- LogUtil.v(
- "CallCardPresenter.onDialerCallSessionModificationStateChange",
- "state: " + sessionModificationState);
+ public void onDialerCallSessionModificationStateChange() {
+ LogUtil.enterBlock("CallCardPresenter.onDialerCallSessionModificationStateChange");
if (mPrimary == null) {
return;
}
getUi()
.setEndCallButtonEnabled(
- sessionModificationState
- != DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST,
+ mPrimary.getVideoTech().getSessionModificationState()
+ != VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST,
true /* shouldAnimate */);
updatePrimaryCallState();
}
@@ -418,6 +423,13 @@ public class CallCardPresenter
&& mPrimaryContactInfo.userType == ContactsUtils.USER_TYPE_WORK);
boolean isHdAudioCall =
isPrimaryCallActive() && mPrimary.hasProperty(Details.PROPERTY_HIGH_DEF_AUDIO);
+ boolean isAttemptingHdAudioCall =
+ !isHdAudioCall
+ && !mPrimary.hasProperty(DialerCall.PROPERTY_CODEC_KNOWN)
+ && MotorolaUtils.shouldBlinkHdIconWhenConnectingCall(mContext);
+
+ boolean isBusiness = mPrimaryContactInfo != null && mPrimaryContactInfo.isBusiness;
+
// Check for video state change and update the visibility of the contact photo. The contact
// photo is hidden when the incoming video surface is shown.
// The contact photo visibility can also change in setPrimary().
@@ -427,8 +439,8 @@ public class CallCardPresenter
.setCallState(
new PrimaryCallState(
mPrimary.getState(),
- mPrimary.getVideoState(),
- mPrimary.getSessionModificationState(),
+ mPrimary.isVideoCall(),
+ mPrimary.getVideoTech().getSessionModificationState(),
mPrimary.getDisconnectCause(),
getConnectionLabel(),
getCallStateIcon(),
@@ -438,12 +450,14 @@ public class CallCardPresenter
mPrimary.hasProperty(Details.PROPERTY_WIFI),
mPrimary.isConferenceCall(),
isWorkCall,
+ isAttemptingHdAudioCall,
isHdAudioCall,
!TextUtils.isEmpty(mPrimary.getLastForwardedNumber()),
shouldShowContactPhoto,
mPrimary.getConnectTimeMillis(),
CallerInfoUtils.isVoiceMailNumber(mContext, mPrimary),
- mPrimary.isRemotelyHeld()));
+ mPrimary.isRemotelyHeld(),
+ isBusiness));
InCallActivity activity =
(InCallActivity) (mInCallScreen.getInCallScreenFragment().getActivity());
@@ -508,15 +522,6 @@ public class CallCardPresenter
}
}
- private void maybeClearSessionModificationState(DialerCall call) {
- @SessionModificationState int state = call.getSessionModificationState();
- if (state != DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST
- && state != DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
- LogUtil.i("CallCardPresenter.maybeClearSessionModificationState", "clearing state");
- call.setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
- }
- }
-
/** Starts a query for more contact data for the save primary and secondary calls. */
private void startContactInfoSearch(
final DialerCall call, final boolean isPrimary, boolean isIncoming) {
@@ -642,13 +647,17 @@ public class CallCardPresenter
// DialerCall placed through a work phone account.
boolean hasWorkCallProperty = mPrimary.hasProperty(PROPERTY_ENTERPRISE_CALL);
- Session enrichedCallSession =
- mPrimary.getNumber() == null
- ? null
- : EnrichedCallManager.Accessor.getInstance(((Application) mContext))
- .getSession(mPrimary.getNumber());
- MultimediaData enrichedCallMultimediaData =
- enrichedCallSession == null ? null : enrichedCallSession.getMultimediaData();
+ MultimediaData multimediaData = null;
+ if (mPrimary.getNumber() != null) {
+ Session enrichedCallSession =
+ EnrichedCallComponent.get(mContext)
+ .getEnrichedCallManager()
+ .getSession(mPrimary.getUniqueCallId(), mPrimary.getNumber());
+ if (enrichedCallSession != null) {
+ enrichedCallSession.setUniqueDialerCallId(mPrimary.getUniqueCallId());
+ multimediaData = enrichedCallSession.getMultimediaData();
+ }
+ }
if (mPrimary.isConferenceCall()) {
LogUtil.v(
@@ -671,7 +680,8 @@ public class CallCardPresenter
false /* answeringDisconnectsOngoingCall */,
shouldShowLocation(),
null /* contactInfoLookupKey */,
- null /* enrichedCallMultimediaData */));
+ null /* enrichedCallMultimediaData */,
+ mPrimary.getNumberPresentation()));
} else if (mPrimaryContactInfo != null) {
LogUtil.v(
"CallCardPresenter.updatePrimaryDisplayInfo",
@@ -696,6 +706,7 @@ public class CallCardPresenter
}
boolean nameIsNumber = name != null && name.equals(mPrimaryContactInfo.number);
+
// DialerCall with caller that is a work contact.
boolean isWorkContact = (mPrimaryContactInfo.userType == ContactsUtils.USER_TYPE_WORK);
mInCallScreen.setPrimary(
@@ -714,13 +725,52 @@ public class CallCardPresenter
mPrimary.answeringDisconnectsForegroundVideoCall(),
shouldShowLocation(),
mPrimaryContactInfo.lookupKey,
- enrichedCallMultimediaData));
+ multimediaData,
+ mPrimary.getNumberPresentation()));
} else {
// Clear the primary display info.
mInCallScreen.setPrimary(PrimaryInfo.createEmptyPrimaryInfo());
}
- mInCallScreen.showLocationUi(null);
+ if (isInCallScreenReady) {
+ mInCallScreen.showLocationUi(getLocationFragment());
+ } else {
+ LogUtil.i("CallCardPresenter.updatePrimaryDisplayInfo", "UI not ready, not showing location");
+ }
+ }
+
+ private Fragment getLocationFragment() {
+ if (!ConfigProviderBindings.get(mContext)
+ .getBoolean(CONFIG_ENABLE_EMERGENCY_LOCATION, CONFIG_ENABLE_EMERGENCY_LOCATION_DEFAULT)) {
+ LogUtil.i("CallCardPresenter.getLocationFragment", "disabled by config.");
+ return null;
+ }
+ if (!shouldShowLocation()) {
+ LogUtil.i("CallCardPresenter.getLocationFragment", "shouldn't show location");
+ return null;
+ }
+ if (!hasLocationPermission()) {
+ LogUtil.i("CallCardPresenter.getLocationFragment", "no location permission.");
+ return null;
+ }
+ if (isBatteryTooLowForEmergencyLocation()) {
+ LogUtil.i("CallCardPresenter.getLocationFragment", "low battery.");
+ return null;
+ }
+ if (ActivityCompat.isInMultiWindowMode(mInCallScreen.getInCallScreenFragment().getActivity())) {
+ LogUtil.i("CallCardPresenter.getLocationFragment", "in multi-window mode");
+ return null;
+ }
+ if (mPrimary.isVideoCall()) {
+ LogUtil.i("CallCardPresenter.getLocationFragment", "emergency video calls not supported");
+ return null;
+ }
+ if (!callLocation.canGetLocation(mContext)) {
+ LogUtil.i("CallCardPresenter.getLocationFragment", "can't get current location");
+ return null;
+ }
+ LogUtil.i("CallCardPresenter.getLocationFragment", "returning location fragment");
+ return callLocation.getLocationFragment(mContext);
}
private boolean shouldShowLocation() {
@@ -972,8 +1022,8 @@ public class CallCardPresenter
|| callState == DialerCall.State.INCOMING) {
return false;
}
- if (mPrimary.getSessionModificationState()
- == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+ if (mPrimary.getVideoTech().getSessionModificationState()
+ == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
return false;
}
return true;
diff --git a/java/com/android/incallui/CallerInfoAsyncQuery.java b/java/com/android/incallui/CallerInfoAsyncQuery.java
index f8d7ac65a..d620d4705 100644
--- a/java/com/android/incallui/CallerInfoAsyncQuery.java
+++ b/java/com/android/incallui/CallerInfoAsyncQuery.java
@@ -55,7 +55,7 @@ import java.util.Arrays;
public class CallerInfoAsyncQuery {
/** Interface for a CallerInfoAsyncQueryHandler result return. */
- public interface OnQueryCompleteListener {
+ interface OnQueryCompleteListener {
/** Called when the query is complete. */
@MainThread
@@ -85,7 +85,7 @@ public class CallerInfoAsyncQuery {
private CallerInfoAsyncQuery() {}
@RequiresPermission(Manifest.permission.READ_CONTACTS)
- public static void startQuery(
+ static void startQuery(
final int token,
final Context context,
final CallerInfo info,
@@ -99,7 +99,7 @@ public class CallerInfoAsyncQuery {
new OnQueryCompleteListener() {
@Override
public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
- Log.d(LOG_TAG, "contactsProviderQueryCompleteListener done");
+ Log.d(LOG_TAG, "contactsProviderQueryCompleteListener onQueryComplete");
// If there are no other directory queries, make sure that the listener is
// notified of this result. see b/27621628
if ((ci != null && ci.contactExists)
@@ -112,6 +112,7 @@ public class CallerInfoAsyncQuery {
@Override
public void onDataLoaded(int token, Object cookie, CallerInfo ci) {
+ Log.d(LOG_TAG, "contactsProviderQueryCompleteListener onDataLoaded");
listener.onDataLoaded(token, cookie, ci);
}
};
@@ -270,9 +271,9 @@ public class CallerInfoAsyncQuery {
/* Directory lookup related code - END */
/** Simple exception used to communicate problems with the query pool. */
- public static class QueryPoolException extends SQLException {
+ private static class QueryPoolException extends SQLException {
- public QueryPoolException(String error) {
+ QueryPoolException(String error) {
super(error);
}
}
@@ -337,7 +338,7 @@ public class CallerInfoAsyncQuery {
}
}
- public OnQueryCompleteListener newListener(long directoryId) {
+ OnQueryCompleteListener newListener(long directoryId) {
return new DirectoryQueryCompleteListener(directoryId);
}
@@ -351,11 +352,13 @@ public class CallerInfoAsyncQuery {
@Override
public void onDataLoaded(int token, Object cookie, CallerInfo ci) {
+ Log.d(LOG_TAG, "DirectoryQueryCompleteListener.onDataLoaded");
mListener.onDataLoaded(token, cookie, ci);
}
@Override
public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
+ Log.d(LOG_TAG, "DirectoryQueryCompleteListener.onQueryComplete");
onDirectoryQueryComplete(token, cookie, ci, mDirectoryId);
}
}
@@ -446,7 +449,7 @@ public class CallerInfoAsyncQuery {
mCallerInfo = null;
}
- protected void updateData(int token, Object cookie, Cursor cursor) {
+ void updateData(int token, Object cookie, Cursor cursor) {
try {
Log.d(this, "##### updateData() ##### for token: " + token);
@@ -549,9 +552,9 @@ public class CallerInfoAsyncQuery {
* times before the query is complete. All accesses (listeners) must be queued up and informed
* in order when the query is complete.
*/
- protected class CallerInfoWorkerHandler extends WorkerHandler {
+ class CallerInfoWorkerHandler extends WorkerHandler {
- public CallerInfoWorkerHandler(Looper looper) {
+ CallerInfoWorkerHandler(Looper looper) {
super(looper);
}
@@ -624,7 +627,7 @@ public class CallerInfoAsyncQuery {
case EVENT_ADD_LISTENER:
updateData(msg.arg1, cw, (Cursor) args.result);
break;
- default:
+ default: // fall out
}
Message reply = args.handler.obtainMessage(msg.what);
reply.obj = args;
diff --git a/java/com/android/incallui/CallerInfoUtils.java b/java/com/android/incallui/CallerInfoUtils.java
index 9f57fba65..7c14533bb 100644
--- a/java/com/android/incallui/CallerInfoUtils.java
+++ b/java/com/android/incallui/CallerInfoUtils.java
@@ -22,6 +22,7 @@ import android.content.Loader;
import android.content.Loader.OnLoadCompleteListener;
import android.content.pm.PackageManager;
import android.net.Uri;
+import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.telecom.PhoneAccount;
import android.telecom.TelecomManager;
@@ -53,7 +54,7 @@ public class CallerInfoUtils {
* OnQueryCompleteListener (which contains information about the phone number label, user's name,
* etc).
*/
- public static CallerInfo getCallerInfoForCall(
+ static CallerInfo getCallerInfoForCall(
Context context,
DialerCall call,
Object cookie,
@@ -81,7 +82,7 @@ public class CallerInfoUtils {
return info;
}
- public static CallerInfo buildCallerInfo(Context context, DialerCall call) {
+ static CallerInfo buildCallerInfo(Context context, DialerCall call) {
CallerInfo info = new CallerInfo();
// Store CNAP information retrieved from the Connection (we want to do this
@@ -91,6 +92,7 @@ public class CallerInfoUtils {
info.numberPresentation = call.getNumberPresentation();
info.namePresentation = call.getCnapNamePresentation();
info.callSubject = call.getCallSubject();
+ info.contactExists = false;
String number = call.getNumber();
if (!TextUtils.isEmpty(number)) {
@@ -109,9 +111,7 @@ public class CallerInfoUtils {
// Because the InCallUI is immediately launched before the call is connected, occasionally
// a voicemail call will be passed to InCallUI as a "voicemail:" URI without a number.
// This call should still be handled as a voicemail call.
- if ((call.getHandle() != null
- && PhoneAccount.SCHEME_VOICEMAIL.equals(call.getHandle().getScheme()))
- || isVoiceMailNumber(context, call)) {
+ if (isVoiceMailNumber(context, call)) {
info.markAsVoiceMail(context);
}
@@ -145,11 +145,17 @@ public class CallerInfoUtils {
return cacheInfo;
}
- public static boolean isVoiceMailNumber(Context context, DialerCall call) {
+ public static boolean isVoiceMailNumber(Context context, @NonNull DialerCall call) {
+ if (call.getHandle() != null
+ && PhoneAccount.SCHEME_VOICEMAIL.equals(call.getHandle().getScheme())) {
+ return true;
+ }
+
if (ContextCompat.checkSelfPermission(context, permission.READ_PHONE_STATE)
!= PackageManager.PERMISSION_GRANTED) {
return false;
}
+
return TelecomUtil.isVoicemailNumber(context, call.getAccountHandle(), call.getNumber());
}
diff --git a/java/com/android/incallui/ContactInfoCache.java b/java/com/android/incallui/ContactInfoCache.java
index 4d4d94a17..c4e25e700 100644
--- a/java/com/android/incallui/ContactInfoCache.java
+++ b/java/com/android/incallui/ContactInfoCache.java
@@ -35,6 +35,7 @@ import android.support.annotation.NonNull;
import android.support.annotation.WorkerThread;
import android.support.v4.os.UserManagerCompat;
import android.telecom.TelecomManager;
+import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -74,10 +75,11 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
private final PhoneNumberService mPhoneNumberService;
// Cache info map needs to be thread-safe since it could be modified by both main thread and
// worker thread.
- private final Map<String, ContactCacheEntry> mInfoMap = new ConcurrentHashMap<>();
+ private final ConcurrentHashMap<String, ContactCacheEntry> mInfoMap = new ConcurrentHashMap<>();
private final Map<String, Set<ContactInfoCacheCallback>> mCallBacks = new ArrayMap<>();
private Drawable mDefaultContactPhotoDrawable;
private Drawable mConferencePhotoDrawable;
+ private int mQueryId;
private ContactInfoCache(Context context) {
mContext = context;
@@ -91,7 +93,7 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
return sCache;
}
- public static ContactCacheEntry buildCacheEntryFromCall(
+ static ContactCacheEntry buildCacheEntryFromCall(
Context context, DialerCall call, boolean isIncoming) {
final ContactCacheEntry entry = new ContactCacheEntry();
@@ -103,7 +105,7 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
}
/** Populate a cache entry from a call (which got converted into a caller info). */
- public static void populateCacheEntry(
+ private static void populateCacheEntry(
@NonNull Context context,
@NonNull CallerInfo info,
@NonNull ContactCacheEntry cce,
@@ -153,7 +155,7 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
// (Typically, we promote the phone number up to the "name" slot
// onscreen, and possibly display a descriptive string in the
// "number" slot.)
- if (TextUtils.isEmpty(number)) {
+ if (TextUtils.isEmpty(number) && TextUtils.isEmpty(info.cnapName)) {
// No name *or* number! Display a generic "unknown" string
// (or potentially some other default based on the presentation.)
displayName = getPresentationString(context, presentation, info.callSubject);
@@ -236,6 +238,7 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
cce.label = label;
cce.isSipCall = isSipCall;
cce.userType = info.userType;
+ cce.originalPhoneNumber = info.phoneNumber;
if (info.contactExists) {
cce.contactLookupResult = ContactLookupResult.Type.LOCAL_CONTACT;
@@ -261,11 +264,11 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
return name;
}
- public ContactCacheEntry getInfo(String callId) {
+ ContactCacheEntry getInfo(String callId) {
return mInfoMap.get(callId);
}
- public void maybeInsertCnapInformationIntoCache(
+ void maybeInsertCnapInformationIntoCache(
Context context, final DialerCall call, final CallerInfo info) {
final CachedNumberLookupService cachedNumberLookupService =
PhoneNumberCache.get(context).getCachedNumberLookupService();
@@ -331,8 +334,13 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
final ContactCacheEntry cacheEntry = mInfoMap.get(callId);
Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
- // If we have a previously obtained intermediate result return that now
- if (cacheEntry != null) {
+ // We need to force a new query if phone number has changed.
+ boolean forceQuery = needForceQuery(call, cacheEntry);
+ Log.d(TAG, "findInfo: callId = " + callId + "; forceQuery = " + forceQuery);
+
+ // If we have a previously obtained intermediate result return that now except needs
+ // force query.
+ if (cacheEntry != null && !forceQuery) {
Log.d(
TAG,
"Contact lookup. In memory cache hit; lookup "
@@ -346,14 +354,19 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
// If the entry already exists, add callback
if (callBacks != null) {
+ Log.d(TAG, "Another query is in progress, add callback only.");
callBacks.add(callback);
- return;
+ if (!forceQuery) {
+ Log.d(TAG, "No need to query again, just return and wait for existing query to finish");
+ return;
+ }
+ } else {
+ Log.d(TAG, "Contact lookup. In memory cache miss; searching provider.");
+ // New lookup
+ callBacks = new ArraySet<>();
+ callBacks.add(callback);
+ mCallBacks.put(callId, callBacks);
}
- Log.d(TAG, "Contact lookup. In memory cache miss; searching provider.");
- // New lookup
- callBacks = new ArraySet<>();
- callBacks.add(callback);
- mCallBacks.put(callId, callBacks);
/**
* Performs a query for caller information. Save any immediate data we get from the query. An
@@ -361,25 +374,47 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
* such as those for voicemail and emergency call information, will not perform an additional
* asynchronous query.
*/
+ final CallerInfoQueryToken queryToken = new CallerInfoQueryToken(mQueryId, callId);
+ mQueryId++;
final CallerInfo callerInfo =
CallerInfoUtils.getCallerInfoForCall(
mContext,
call,
new DialerCallCookieWrapper(callId, call.getNumberPresentation()),
- new FindInfoCallback(isIncoming));
+ new FindInfoCallback(isIncoming, queryToken));
- updateCallerInfoInCacheOnAnyThread(
- callId, call.getNumberPresentation(), callerInfo, isIncoming, false);
- sendInfoNotifications(callId, mInfoMap.get(callId));
+ if (cacheEntry != null) {
+ // We should not override the old cache item until the new query is
+ // back. We should only update the queryId. Otherwise, we may see
+ // flicker of the name and image (old cache -> new cache before query
+ // -> new cache after query)
+ cacheEntry.queryId = queryToken.mQueryId;
+ Log.d(TAG, "There is an existing cache. Do not override until new query is back");
+ } else {
+ ContactCacheEntry initialCacheEntry =
+ updateCallerInfoInCacheOnAnyThread(
+ callId, call.getNumberPresentation(), callerInfo, isIncoming, false, queryToken);
+ sendInfoNotifications(callId, initialCacheEntry);
+ }
}
@AnyThread
- private void updateCallerInfoInCacheOnAnyThread(
+ private ContactCacheEntry updateCallerInfoInCacheOnAnyThread(
String callId,
int numberPresentation,
CallerInfo callerInfo,
boolean isIncoming,
- boolean didLocalLookup) {
+ boolean didLocalLookup,
+ CallerInfoQueryToken queryToken) {
+ Log.d(
+ TAG,
+ "updateCallerInfoInCacheOnAnyThread: callId = "
+ + callId
+ + "; queryId = "
+ + queryToken.mQueryId
+ + "; didLocalLookup = "
+ + didLocalLookup);
+
int presentationMode = numberPresentation;
if (callerInfo.contactExists
|| callerInfo.isEmergencyNumber()
@@ -387,38 +422,57 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
presentationMode = TelecomManager.PRESENTATION_ALLOWED;
}
- synchronized (mInfoMap) {
- ContactCacheEntry cacheEntry = mInfoMap.get(callId);
- // Ensure we always have a cacheEntry. Replace the existing entry if
- // it has no name or if we found a local contact.
- if (cacheEntry == null
- || TextUtils.isEmpty(cacheEntry.namePrimary)
- || callerInfo.contactExists) {
- cacheEntry = buildEntry(mContext, callerInfo, presentationMode, isIncoming);
- mInfoMap.put(callId, cacheEntry);
- }
- if (didLocalLookup) {
- // Before issuing a request for more data from other services, we only check that the
- // contact wasn't found in the local DB. We don't check the if the cache entry already
- // has a name because we allow overriding cnap data with data from other services.
- if (!callerInfo.contactExists && mPhoneNumberService != null) {
- Log.d(TAG, "Contact lookup. Local contacts miss, checking remote");
- final PhoneNumberServiceListener listener = new PhoneNumberServiceListener(callId);
- mPhoneNumberService.getPhoneNumberInfo(cacheEntry.number, listener, listener, isIncoming);
- } else if (cacheEntry.displayPhotoUri != null) {
- Log.d(TAG, "Contact lookup. Local contact found, starting image load");
- // Load the image with a callback to update the image state.
- // When the load is finished, onImageLoadComplete() will be called.
- cacheEntry.hasPhotoToLoad = true;
- ContactsAsyncHelper.startObtainPhotoAsync(
- TOKEN_UPDATE_PHOTO_FOR_CALL_STATE,
- mContext,
- cacheEntry.displayPhotoUri,
- ContactInfoCache.this,
- callId);
+ // We always replace the entry. The only exception is the same photo case.
+ ContactCacheEntry cacheEntry = buildEntry(mContext, callerInfo, presentationMode, isIncoming);
+ cacheEntry.queryId = queryToken.mQueryId;
+
+ ContactCacheEntry existingCacheEntry = mInfoMap.get(callId);
+ Log.d(TAG, "Existing cacheEntry in hashMap " + existingCacheEntry);
+
+ if (didLocalLookup) {
+ // Before issuing a request for more data from other services, we only check that the
+ // contact wasn't found in the local DB. We don't check the if the cache entry already
+ // has a name because we allow overriding cnap data with data from other services.
+ if (!callerInfo.contactExists && mPhoneNumberService != null) {
+ Log.d(TAG, "Contact lookup. Local contacts miss, checking remote");
+ final PhoneNumberServiceListener listener =
+ new PhoneNumberServiceListener(callId, queryToken.mQueryId);
+ cacheEntry.hasPendingQuery = true;
+ mPhoneNumberService.getPhoneNumberInfo(cacheEntry.number, listener, listener, isIncoming);
+ } else if (cacheEntry.displayPhotoUri != null) {
+ // When the difference between 2 numbers is only the prefix (e.g. + or IDD),
+ // we will still trigger force query so that the number can be updated on
+ // the calling screen. We need not query the image again if the previous
+ // query already has the image to avoid flickering.
+ if (existingCacheEntry != null
+ && existingCacheEntry.displayPhotoUri != null
+ && existingCacheEntry.displayPhotoUri.equals(cacheEntry.displayPhotoUri)
+ && existingCacheEntry.photo != null) {
+ Log.d(TAG, "Same picture. Do not need start image load.");
+ cacheEntry.photo = existingCacheEntry.photo;
+ cacheEntry.photoType = existingCacheEntry.photoType;
+ return cacheEntry;
}
+
+ Log.d(TAG, "Contact lookup. Local contact found, starting image load");
+ // Load the image with a callback to update the image state.
+ // When the load is finished, onImageLoadComplete() will be called.
+ cacheEntry.hasPendingQuery = true;
+ ContactsAsyncHelper.startObtainPhotoAsync(
+ TOKEN_UPDATE_PHOTO_FOR_CALL_STATE,
+ mContext,
+ cacheEntry.displayPhotoUri,
+ ContactInfoCache.this,
+ queryToken);
}
+ Log.d(TAG, "put entry into map: " + cacheEntry);
+ mInfoMap.put(callId, cacheEntry);
+ } else {
+ // Don't overwrite if there is existing cache.
+ Log.d(TAG, "put entry into map if not exists: " + cacheEntry);
+ mInfoMap.putIfAbsent(callId, cacheEntry);
}
+ return cacheEntry;
}
/**
@@ -429,35 +483,42 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
@Override
public void onImageLoaded(int token, Drawable photo, Bitmap photoIcon, Object cookie) {
Assert.isWorkerThread();
+ CallerInfoQueryToken myCookie = (CallerInfoQueryToken) cookie;
+ final String callId = myCookie.mCallId;
+ final int queryId = myCookie.mQueryId;
+ if (!isWaitingForThisQuery(callId, queryId)) {
+ return;
+ }
loadImage(photo, photoIcon, cookie);
}
private void loadImage(Drawable photo, Bitmap photoIcon, Object cookie) {
- Log.d(this, "Image load complete with context: ", mContext);
+ Log.d(TAG, "Image load complete with context: ", mContext);
// TODO: may be nice to update the image view again once the newer one
// is available on contacts database.
- String callId = (String) cookie;
+ CallerInfoQueryToken myCookie = (CallerInfoQueryToken) cookie;
+ final String callId = myCookie.mCallId;
ContactCacheEntry entry = mInfoMap.get(callId);
if (entry == null) {
- Log.e(this, "Image Load received for empty search entry.");
+ Log.e(TAG, "Image Load received for empty search entry.");
clearCallbacks(callId);
return;
}
- Log.d(this, "setting photo for entry: ", entry);
+ Log.d(TAG, "setting photo for entry: ", entry);
// Conference call icons are being handled in CallCardPresenter.
if (photo != null) {
- Log.v(this, "direct drawable: ", photo);
+ Log.v(TAG, "direct drawable: ", photo);
entry.photo = photo;
entry.photoType = ContactPhotoType.CONTACT;
} else if (photoIcon != null) {
- Log.v(this, "photo icon: ", photoIcon);
+ Log.v(TAG, "photo icon: ", photoIcon);
entry.photo = new BitmapDrawable(mContext.getResources(), photoIcon);
entry.photoType = ContactPhotoType.CONTACT;
} else {
- Log.v(this, "unknown photo");
+ Log.v(TAG, "unknown photo");
entry.photo = null;
entry.photoType = ContactPhotoType.DEFAULT_PLACEHOLDER;
}
@@ -471,9 +532,13 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
@Override
public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie) {
Assert.isMainThread();
- String callId = (String) cookie;
- ContactCacheEntry entry = mInfoMap.get(callId);
- sendImageNotifications(callId, entry);
+ CallerInfoQueryToken myCookie = (CallerInfoQueryToken) cookie;
+ final String callId = myCookie.mCallId;
+ final int queryId = myCookie.mQueryId;
+ if (!isWaitingForThisQuery(callId, queryId)) {
+ return;
+ }
+ sendImageNotifications(callId, mInfoMap.get(callId));
clearCallbacks(callId);
}
@@ -482,6 +547,7 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
public void clearCache() {
mInfoMap.clear();
mCallBacks.clear();
+ mQueryId = 0;
}
private ContactCacheEntry buildEntry(
@@ -500,9 +566,6 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
cce.photo = getDefaultContactPhotoDrawable();
cce.photoType = ContactPhotoType.DEFAULT_PLACEHOLDER;
}
- } else if (info.contactDisplayPhotoUri == null) {
- cce.photo = getDefaultContactPhotoDrawable();
- cce.photoType = ContactPhotoType.DEFAULT_PLACEHOLDER;
} else {
cce.displayPhotoUri = info.contactDisplayPhotoUri;
cce.photo = null;
@@ -528,7 +591,9 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
}
/** Sends the updated information to call the callbacks for the entry. */
+ @MainThread
private void sendInfoNotifications(String callId, ContactCacheEntry entry) {
+ Assert.isMainThread();
final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
if (callBacks != null) {
for (ContactInfoCacheCallback callBack : callBacks) {
@@ -537,7 +602,9 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
}
}
+ @MainThread
private void sendImageNotifications(String callId, ContactCacheEntry entry) {
+ Assert.isMainThread();
final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
if (callBacks != null && entry.photo != null) {
for (ContactInfoCacheCallback callBack : callBacks) {
@@ -583,21 +650,26 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
public String location;
public String label;
public Drawable photo;
- @ContactPhotoType public int photoType;
- public boolean isSipCall;
+ @ContactPhotoType int photoType;
+ boolean isSipCall;
// Note in cache entry whether this is a pending async loading action to know whether to
// wait for its callback or not.
- public boolean hasPhotoToLoad;
+ boolean hasPendingQuery;
/** This will be used for the "view" notification. */
public Uri contactUri;
/** Either a display photo or a thumbnail URI. */
- public Uri displayPhotoUri;
+ Uri displayPhotoUri;
public Uri lookupUri; // Sent to NotificationMananger
public String lookupKey;
public int contactLookupResult = ContactLookupResult.Type.NOT_FOUND;
public long userType = ContactsUtils.USER_TYPE_CURRENT;
- public Uri contactRingtoneUri;
+ Uri contactRingtoneUri;
+ /** Query id to identify the query session. */
+ int queryId;
+ /** The phone number without any changes to display to the user (ex: cnap...) */
+ String originalPhoneNumber;
+ boolean isBusiness;
@Override
public String toString() {
@@ -631,6 +703,10 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
+ userType
+ ", contactRingtoneUri="
+ contactRingtoneUri
+ + ", queryId="
+ + queryId
+ + ", originalPhoneNumber="
+ + originalPhoneNumber
+ '}';
}
}
@@ -648,16 +724,22 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
private class FindInfoCallback implements OnQueryCompleteListener {
private final boolean mIsIncoming;
+ private final CallerInfoQueryToken mQueryToken;
- public FindInfoCallback(boolean isIncoming) {
+ public FindInfoCallback(boolean isIncoming, CallerInfoQueryToken queryToken) {
mIsIncoming = isIncoming;
+ mQueryToken = queryToken;
}
@Override
public void onDataLoaded(int token, Object cookie, CallerInfo ci) {
Assert.isWorkerThread();
DialerCallCookieWrapper cw = (DialerCallCookieWrapper) cookie;
- updateCallerInfoInCacheOnAnyThread(cw.callId, cw.numberPresentation, ci, mIsIncoming, true);
+ if (!isWaitingForThisQuery(cw.callId, mQueryToken.mQueryId)) {
+ return;
+ }
+ updateCallerInfoInCacheOnAnyThread(
+ cw.callId, cw.numberPresentation, ci, mIsIncoming, true, mQueryToken);
}
@Override
@@ -665,6 +747,9 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
Assert.isMainThread();
DialerCallCookieWrapper cw = (DialerCallCookieWrapper) cookie;
String callId = cw.callId;
+ if (!isWaitingForThisQuery(cw.callId, mQueryToken.mQueryId)) {
+ return;
+ }
ContactCacheEntry cacheEntry = mInfoMap.get(callId);
// This may happen only when InCallPresenter attempt to cleanup.
if (cacheEntry == null) {
@@ -673,7 +758,7 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
return;
}
sendInfoNotifications(callId, cacheEntry);
- if (!cacheEntry.hasPhotoToLoad) {
+ if (!cacheEntry.hasPendingQuery) {
if (callerInfo.contactExists) {
Log.d(TAG, "Contact lookup done. Local contact found, no image.");
} else {
@@ -691,13 +776,20 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
implements PhoneNumberService.NumberLookupListener, PhoneNumberService.ImageLookupListener {
private final String mCallId;
+ private final int mQueryIdOfRemoteLookup;
- PhoneNumberServiceListener(String callId) {
+ PhoneNumberServiceListener(String callId, int queryId) {
mCallId = callId;
+ mQueryIdOfRemoteLookup = queryId;
}
@Override
public void onPhoneNumberInfoComplete(final PhoneNumberService.PhoneNumberInfo info) {
+ Log.d(TAG, "PhoneNumberServiceListener.onPhoneNumberInfoComplete");
+ if (!isWaitingForThisQuery(mCallId, mQueryIdOfRemoteLookup)) {
+ return;
+ }
+
// If we got a miss, this is the end of the lookup pipeline,
// so clear the callbacks and return.
if (info == null) {
@@ -705,11 +797,11 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
clearCallbacks(mCallId);
return;
}
-
ContactCacheEntry entry = new ContactCacheEntry();
entry.namePrimary = info.getDisplayName();
entry.number = info.getNumber();
entry.contactLookupResult = info.getLookupSource();
+ entry.isBusiness = info.isBusiness();
final int type = info.getPhoneType();
final String label = info.getPhoneLabel();
if (type == Phone.TYPE_CUSTOM) {
@@ -718,33 +810,32 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
final CharSequence typeStr = Phone.getTypeLabel(mContext.getResources(), type, label);
entry.label = typeStr == null ? null : typeStr.toString();
}
- synchronized (mInfoMap) {
- final ContactCacheEntry oldEntry = mInfoMap.get(mCallId);
- if (oldEntry != null) {
- // Location is only obtained from local lookup so persist
- // the value for remote lookups. Once we have a name this
- // field is no longer used; it is persisted here in case
- // the UI is ever changed to use it.
- entry.location = oldEntry.location;
- // Contact specific ringtone is obtained from local lookup.
- entry.contactRingtoneUri = oldEntry.contactRingtoneUri;
- }
-
- // If no image and it's a business, switch to using the default business avatar.
- if (info.getImageUrl() == null && info.isBusiness()) {
- Log.d(TAG, "Business has no image. Using default.");
- entry.photo = mContext.getResources().getDrawable(R.drawable.img_business);
- entry.photoType = ContactPhotoType.BUSINESS;
- }
+ final ContactCacheEntry oldEntry = mInfoMap.get(mCallId);
+ if (oldEntry != null) {
+ // Location is only obtained from local lookup so persist
+ // the value for remote lookups. Once we have a name this
+ // field is no longer used; it is persisted here in case
+ // the UI is ever changed to use it.
+ entry.location = oldEntry.location;
+ // Contact specific ringtone is obtained from local lookup.
+ entry.contactRingtoneUri = oldEntry.contactRingtoneUri;
+ }
- mInfoMap.put(mCallId, entry);
+ // If no image and it's a business, switch to using the default business avatar.
+ if (info.getImageUrl() == null && info.isBusiness()) {
+ Log.d(TAG, "Business has no image. Using default.");
+ entry.photo = mContext.getResources().getDrawable(R.drawable.img_business);
+ entry.photoType = ContactPhotoType.BUSINESS;
}
+
+ Log.d(TAG, "put entry into map: " + entry);
+ mInfoMap.put(mCallId, entry);
sendInfoNotifications(mCallId, entry);
- entry.hasPhotoToLoad = info.getImageUrl() != null;
+ entry.hasPendingQuery = info.getImageUrl() != null;
// If there is no image then we should not expect another callback.
- if (!entry.hasPhotoToLoad) {
+ if (!entry.hasPendingQuery) {
// We're done, so clear callbacks
clearCallbacks(mCallId);
}
@@ -752,8 +843,59 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
@Override
public void onImageFetchComplete(Bitmap bitmap) {
- loadImage(null, bitmap, mCallId);
- onImageLoadComplete(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE, null, bitmap, mCallId);
+ Log.d(TAG, "PhoneNumberServiceListener.onImageFetchComplete");
+ if (!isWaitingForThisQuery(mCallId, mQueryIdOfRemoteLookup)) {
+ return;
+ }
+ CallerInfoQueryToken queryToken = new CallerInfoQueryToken(mQueryIdOfRemoteLookup, mCallId);
+ loadImage(null, bitmap, queryToken);
+ onImageLoadComplete(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE, null, bitmap, queryToken);
+ }
+ }
+
+ private boolean needForceQuery(DialerCall call, ContactCacheEntry cacheEntry) {
+ if (call == null || call.isConferenceCall()) {
+ return false;
+ }
+
+ String newPhoneNumber = PhoneNumberUtils.stripSeparators(call.getNumber());
+ if (cacheEntry == null) {
+ // No info in the map yet so it is the 1st query
+ Log.d(TAG, "needForceQuery: first query");
+ return true;
+ }
+ String oldPhoneNumber = PhoneNumberUtils.stripSeparators(cacheEntry.originalPhoneNumber);
+
+ if (!TextUtils.equals(oldPhoneNumber, newPhoneNumber)) {
+ Log.d(TAG, "phone number has changed: " + oldPhoneNumber + " -> " + newPhoneNumber);
+ return true;
+ }
+
+ return false;
+ }
+
+ private static final class CallerInfoQueryToken {
+ final int mQueryId;
+ final String mCallId;
+
+ CallerInfoQueryToken(int queryId, String callId) {
+ mQueryId = queryId;
+ mCallId = callId;
+ }
+ }
+
+ /** Check if the queryId in the cached map is the same as the one from query result. */
+ private boolean isWaitingForThisQuery(String callId, int queryId) {
+ final ContactCacheEntry existingCacheEntry = mInfoMap.get(callId);
+ if (existingCacheEntry == null) {
+ // This might happen if lookup on background thread comes back before the initial entry is
+ // created.
+ Log.d(TAG, "Cached entry is null.");
+ return true;
+ } else {
+ int waitingQueryId = existingCacheEntry.queryId;
+ Log.d(TAG, "waitingQueryId = " + waitingQueryId + "; queryId = " + queryId);
+ return waitingQueryId == queryId;
}
}
}
diff --git a/java/com/android/incallui/ExternalCallNotifier.java b/java/com/android/incallui/ExternalCallNotifier.java
index 466e12a6d..6ec94a631 100644
--- a/java/com/android/incallui/ExternalCallNotifier.java
+++ b/java/com/android/incallui/ExternalCallNotifier.java
@@ -41,6 +41,8 @@ import com.android.contacts.common.compat.CallCompat;
import com.android.contacts.common.preference.ContactsPreferences;
import com.android.contacts.common.util.BitmapUtil;
import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.dialer.notification.NotificationChannelManager;
+import com.android.dialer.notification.NotificationChannelManager.Channel;
import com.android.incallui.call.DialerCall;
import com.android.incallui.call.DialerCallDelegate;
import com.android.incallui.call.ExternalCallList;
@@ -57,9 +59,9 @@ import java.util.Map;
public class ExternalCallNotifier implements ExternalCallList.ExternalCallListener {
/** Tag used with the notification manager to uniquely identify external call notifications. */
- private static final String NOTIFICATION_TAG = "EXTERNAL_CALL";
+ private static final int NOTIFICATION_ID = R.id.notification_external_call;
- private static final int SUMMARY_ID = -1;
+ private static final String NOTIFICATION_GROUP = "ExternalCallNotifier";
private final Context mContext;
private final ContactInfoCache mContactInfoCache;
private Map<Call, NotificationInfo> mNotifications = new ArrayMap<>();
@@ -186,14 +188,15 @@ public class ExternalCallNotifier implements ExternalCallList.ExternalCallListen
NotificationManager notificationManager =
(NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
- notificationManager.cancel(NOTIFICATION_TAG, mNotifications.get(call).getNotificationId());
+ notificationManager.cancel(
+ String.valueOf(mNotifications.get(call).getNotificationId()), NOTIFICATION_ID);
mNotifications.remove(call);
if (mShowingSummary && mNotifications.size() <= 1) {
// Where a summary notification is showing and there is now not enough notifications to
// necessitate a summary, cancel the summary.
- notificationManager.cancel(NOTIFICATION_TAG, SUMMARY_ID);
+ notificationManager.cancel(NOTIFICATION_GROUP, NOTIFICATION_ID);
mShowingSummary = false;
// If there is still a single call requiring a notification, re-post the notification as a
@@ -234,7 +237,7 @@ public class ExternalCallNotifier implements ExternalCallList.ExternalCallListen
builder.setOngoing(true);
// Make the notification prioritized over the other normal notifications.
builder.setPriority(Notification.PRIORITY_HIGH);
- builder.setGroup(NOTIFICATION_TAG);
+ builder.setGroup(NOTIFICATION_GROUP);
boolean isVideoCall = VideoProfile.isVideo(info.getCall().getDetails().getVideoState());
// Set the content ("Ongoing call on another device")
@@ -249,6 +252,9 @@ public class ExternalCallNotifier implements ExternalCallList.ExternalCallListen
builder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color));
builder.addPerson(info.getPersonReference());
+ NotificationChannelManager.applyChannel(
+ builder, mContext, Channel.EXTERNAL_CALL, info.getCall().getDetails().getAccountHandle());
+
// Where the external call supports being transferred to the local device, add an action
// to the notification to initiate the call pull process.
if (CallCompat.canPullExternalCall(info.getCall())) {
@@ -281,12 +287,19 @@ public class ExternalCallNotifier implements ExternalCallList.ExternalCallListen
publicBuilder.setSmallIcon(R.drawable.quantum_ic_call_white_24);
publicBuilder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color));
+ NotificationChannelManager.applyChannel(
+ publicBuilder,
+ mContext,
+ Channel.EXTERNAL_CALL,
+ info.getCall().getDetails().getAccountHandle());
+
builder.setPublicVersion(publicBuilder.build());
Notification notification = builder.build();
NotificationManager notificationManager =
(NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
- notificationManager.notify(NOTIFICATION_TAG, info.getNotificationId(), notification);
+ notificationManager.notify(
+ String.valueOf(info.getNotificationId()), NOTIFICATION_ID, notification);
if (!mShowingSummary && mNotifications.size() > 1) {
// If the number of notifications shown is > 1, and we're not already showing a group summary,
@@ -297,10 +310,12 @@ public class ExternalCallNotifier implements ExternalCallList.ExternalCallListen
summary.setOngoing(true);
// Make the notification prioritized over the other normal notifications.
summary.setPriority(Notification.PRIORITY_HIGH);
- summary.setGroup(NOTIFICATION_TAG);
+ summary.setGroup(NOTIFICATION_GROUP);
summary.setGroupSummary(true);
summary.setSmallIcon(R.drawable.quantum_ic_call_white_24);
- notificationManager.notify(NOTIFICATION_TAG, SUMMARY_ID, summary.build());
+ NotificationChannelManager.applyChannel(
+ summary, mContext, Channel.EXTERNAL_CALL, info.getCall().getDetails().getAccountHandle());
+ notificationManager.notify(NOTIFICATION_GROUP, NOTIFICATION_ID, summary.build());
mShowingSummary = true;
}
}
diff --git a/java/com/android/incallui/InCallActivity.java b/java/com/android/incallui/InCallActivity.java
index 307415916..7c4394872 100644
--- a/java/com/android/incallui/InCallActivity.java
+++ b/java/com/android/incallui/InCallActivity.java
@@ -32,6 +32,7 @@ import android.view.KeyEvent;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
+import com.android.dialer.common.Assert;
import com.android.dialer.common.LogUtil;
import com.android.dialer.compat.ActivityCompat;
import com.android.dialer.logging.Logger;
@@ -44,7 +45,6 @@ import com.android.incallui.answerproximitysensor.PseudoScreenState;
import com.android.incallui.call.CallList;
import com.android.incallui.call.DialerCall;
import com.android.incallui.call.DialerCall.State;
-import com.android.incallui.call.VideoUtils;
import com.android.incallui.incall.bindings.InCallBindings;
import com.android.incallui.incall.protocol.InCallButtonUiDelegate;
import com.android.incallui.incall.protocol.InCallButtonUiDelegateFactory;
@@ -89,11 +89,7 @@ public class InCallActivity extends TransactionSafeFragmentActivity
}
public static Intent getIntent(
- Context context,
- boolean showDialpad,
- boolean newOutgoingCall,
- boolean isVideoCall,
- boolean isForFullScreen) {
+ Context context, boolean showDialpad, boolean newOutgoingCall, boolean isForFullScreen) {
Intent intent = new Intent(Intent.ACTION_MAIN, null);
intent.setFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION | Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setClass(context, InCallActivity.class);
@@ -192,7 +188,22 @@ public class InCallActivity extends TransactionSafeFragmentActivity
@Override
public void finish() {
if (shouldCloseActivityOnFinish()) {
- super.finish();
+ // When user select incall ui from recents after the call is disconnected, it tries to launch
+ // a new InCallActivity but InCallPresenter is already teared down at this point, which causes
+ // crash.
+ // By calling finishAndRemoveTask() instead of finish() the task associated with
+ // InCallActivity is cleared completely. So system won't try to create a new InCallActivity in
+ // this case.
+ //
+ // Calling finish won't clear the task and normally when an activity finishes it shouldn't
+ // clear the task since there could be parent activity in the same task that's still alive.
+ // But InCallActivity is special since it's singleInstance which means it's root activity and
+ // only instance of activity in the task. So it should be safe to also remove task when
+ // finishing.
+ // It's also necessary in the sense of it's excluded from recents. So whenever the activity
+ // finishes, the task should also be removed since it doesn't make sense to go back to it in
+ // anyway anymore.
+ super.finishAndRemoveTask();
}
}
@@ -260,18 +271,12 @@ public class InCallActivity extends TransactionSafeFragmentActivity
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
- if (common.onKeyUp(keyCode, event)) {
- return true;
- }
- return super.onKeyUp(keyCode, event);
+ return common.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
- if (common.onKeyDown(keyCode, event)) {
- return true;
- }
- return super.onKeyDown(keyCode, event);
+ return common.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event);
}
public boolean isInCallScreenAnimating() {
@@ -411,13 +416,6 @@ public class InCallActivity extends TransactionSafeFragmentActivity
common.setExcludeFromRecents(exclude);
}
- public void onResolveIntent(
- DialerCall outgoingCall, boolean isNewOutgoingCall, boolean didShowAccountSelectionDialog) {
- if (didShowAccountSelectionDialog) {
- hideMainInCallFragment();
- }
- }
-
@Nullable
public FragmentManager getDialpadFragmentManager() {
InCallScreen inCallScreen = getInCallScreen();
@@ -488,7 +486,7 @@ public class InCallActivity extends TransactionSafeFragmentActivity
enableInCallOrientationEventListener(allowOrientationChange);
}
- private void hideMainInCallFragment() {
+ public void hideMainInCallFragment() {
LogUtil.i("InCallActivity.hideMainInCallFragment", "");
if (didShowInCallScreen || didShowVideoCallScreen) {
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
@@ -513,8 +511,8 @@ public class InCallActivity extends TransactionSafeFragmentActivity
}
isInShowMainInCallFragment = true;
- ShouldShowAnswerUiResult shouldShowAnswerUi = getShouldShowAnswerUi();
- boolean shouldShowVideoUi = getShouldShowVideoUi();
+ ShouldShowUiResult shouldShowAnswerUi = getShouldShowAnswerUi();
+ ShouldShowUiResult shouldShowVideoUi = getShouldShowVideoUi();
LogUtil.i(
"InCallActivity.showMainInCallFragment",
"shouldShowAnswerUi: %b, shouldShowVideoUi: %b, "
@@ -525,7 +523,7 @@ public class InCallActivity extends TransactionSafeFragmentActivity
didShowInCallScreen,
didShowVideoCallScreen);
// Only video call ui allows orientation change.
- setAllowOrientationChange(shouldShowVideoUi);
+ setAllowOrientationChange(shouldShowVideoUi.shouldShow);
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
boolean didChangeInCall;
@@ -535,9 +533,9 @@ public class InCallActivity extends TransactionSafeFragmentActivity
didChangeInCall = hideInCallScreenFragment(transaction);
didChangeVideo = hideVideoCallScreenFragment(transaction);
didChangeAnswer = showAnswerScreenFragment(transaction, shouldShowAnswerUi.call);
- } else if (shouldShowVideoUi) {
+ } else if (shouldShowVideoUi.shouldShow) {
didChangeInCall = hideInCallScreenFragment(transaction);
- didChangeVideo = showVideoCallScreenFragment(transaction);
+ didChangeVideo = showVideoCallScreenFragment(transaction, shouldShowVideoUi.call);
didChangeAnswer = hideAnswerScreenFragment(transaction);
} else {
didChangeInCall = showInCallScreenFragment(transaction);
@@ -552,17 +550,17 @@ public class InCallActivity extends TransactionSafeFragmentActivity
isInShowMainInCallFragment = false;
}
- private ShouldShowAnswerUiResult getShouldShowAnswerUi() {
+ private ShouldShowUiResult getShouldShowAnswerUi() {
DialerCall call = CallList.getInstance().getIncomingCall();
if (call != null) {
LogUtil.i("InCallActivity.getShouldShowAnswerUi", "found incoming call");
- return new ShouldShowAnswerUiResult(true, call);
+ return new ShouldShowUiResult(true, call);
}
call = CallList.getInstance().getVideoUpgradeRequestCall();
if (call != null) {
LogUtil.i("InCallActivity.getShouldShowAnswerUi", "found video upgrade request");
- return new ShouldShowAnswerUiResult(true, call);
+ return new ShouldShowUiResult(true, call);
}
// Check if we're showing the answer screen and the call is disconnected. If this condition is
@@ -574,30 +572,30 @@ public class InCallActivity extends TransactionSafeFragmentActivity
}
if (didShowAnswerScreen && (call == null || call.getState() == State.DISCONNECTED)) {
LogUtil.i("InCallActivity.getShouldShowAnswerUi", "found disconnecting incoming call");
- return new ShouldShowAnswerUiResult(true, call);
+ return new ShouldShowUiResult(true, call);
}
- return new ShouldShowAnswerUiResult(false, null);
+ return new ShouldShowUiResult(false, null);
}
- private boolean getShouldShowVideoUi() {
+ private static ShouldShowUiResult getShouldShowVideoUi() {
DialerCall call = CallList.getInstance().getFirstCall();
if (call == null) {
LogUtil.i("InCallActivity.getShouldShowVideoUi", "null call");
- return false;
+ return new ShouldShowUiResult(false, null);
}
- if (VideoUtils.isVideoCall(call)) {
+ if (call.isVideoCall()) {
LogUtil.i("InCallActivity.getShouldShowVideoUi", "found video call");
- return true;
+ return new ShouldShowUiResult(true, call);
}
- if (VideoUtils.hasSentVideoUpgradeRequest(call)) {
+ if (call.hasSentVideoUpgradeRequest()) {
LogUtil.i("InCallActivity.getShouldShowVideoUi", "upgrading to video");
- return true;
+ return new ShouldShowUiResult(true, call);
}
- return false;
+ return new ShouldShowUiResult(false, null);
}
private boolean showAnswerScreenFragment(FragmentTransaction transaction, DialerCall call) {
@@ -607,14 +605,15 @@ public class InCallActivity extends TransactionSafeFragmentActivity
return false;
}
- boolean isVideoUpgradeRequest = VideoUtils.hasReceivedVideoUpgradeRequest(call);
- int videoState = isVideoUpgradeRequest ? call.getRequestedVideoState() : call.getVideoState();
+ Assert.checkArgument(call != null, "didShowAnswerScreen was false but call was still null");
+
+ boolean isVideoUpgradeRequest = call.hasReceivedVideoUpgradeRequest();
// Check if we're already showing an answer screen for this call.
if (didShowAnswerScreen) {
AnswerScreen answerScreen = getAnswerScreen();
if (answerScreen.getCallId().equals(call.getId())
- && answerScreen.getVideoState() == videoState
+ && answerScreen.isVideoCall() == call.isVideoCall()
&& answerScreen.isVideoUpgradeRequest() == isVideoUpgradeRequest) {
return false;
}
@@ -626,7 +625,7 @@ public class InCallActivity extends TransactionSafeFragmentActivity
// Show a new answer screen.
AnswerScreen answerScreen =
- AnswerBindings.createAnswerScreen(call.getId(), videoState, isVideoUpgradeRequest);
+ AnswerBindings.createAnswerScreen(call.getId(), call.isVideoCall(), isVideoUpgradeRequest);
transaction.add(R.id.main, answerScreen.getAnswerScreenFragment(), TAG_ANSWER_SCREEN);
Logger.get(this).logScreenView(ScreenEvent.Type.INCOMING_CALL, this);
@@ -675,12 +674,21 @@ public class InCallActivity extends TransactionSafeFragmentActivity
return true;
}
- private boolean showVideoCallScreenFragment(FragmentTransaction transaction) {
+ private boolean showVideoCallScreenFragment(FragmentTransaction transaction, DialerCall call) {
if (didShowVideoCallScreen) {
- return false;
+ VideoCallScreen videoCallScreen = getVideoCallScreen();
+ if (videoCallScreen.getCallId().equals(call.getId())) {
+ return false;
+ }
+ LogUtil.i(
+ "InCallActivity.showVideoCallScreenFragment",
+ "video call fragment exists but arguments do not match");
+ hideVideoCallScreenFragment(transaction);
}
- VideoCallScreen videoCallScreen = VideoBindings.createVideoCallScreen();
+ LogUtil.i("InCallActivity.showVideoCallScreenFragment", "call: %s", call);
+
+ VideoCallScreen videoCallScreen = VideoBindings.createVideoCallScreen(call.getId());
transaction.add(R.id.main, videoCallScreen.getVideoCallScreenFragment(), TAG_VIDEO_CALL_SCREEN);
Logger.get(this).logScreenView(ScreenEvent.Type.INCALL, this);
@@ -744,11 +752,11 @@ public class InCallActivity extends TransactionSafeFragmentActivity
return super.dispatchTouchEvent(event);
}
- private static class ShouldShowAnswerUiResult {
+ private static class ShouldShowUiResult {
public final boolean shouldShow;
public final DialerCall call;
- ShouldShowAnswerUiResult(boolean shouldShow, DialerCall call) {
+ ShouldShowUiResult(boolean shouldShow, DialerCall call) {
this.shouldShow = shouldShow;
this.call = call;
}
diff --git a/java/com/android/incallui/InCallActivityCommon.java b/java/com/android/incallui/InCallActivityCommon.java
index a2467dd72..2cdb913ce 100644
--- a/java/com/android/incallui/InCallActivityCommon.java
+++ b/java/com/android/incallui/InCallActivityCommon.java
@@ -21,7 +21,6 @@ import android.app.ActivityManager.AppTask;
import android.app.ActivityManager.TaskDescription;
import android.app.AlertDialog;
import android.app.Dialog;
-import android.app.DialogFragment;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
@@ -99,6 +98,7 @@ public class InCallActivityCommon {
private String showPostCharWaitDialogCallId;
private String showPostCharWaitDialogChars;
private Dialog dialog;
+ private SelectPhoneAccountDialogFragment selectPhoneAccountDialogFragment;
private InCallOrientationEventListener inCallOrientationEventListener;
private Animation dialpadSlideInAnimation;
private Animation dialpadSlideOutAnimation;
@@ -496,11 +496,15 @@ public class InCallActivityCommon {
}
}
- public void dismissPendingDialogs() {
+ void dismissPendingDialogs() {
if (dialog != null) {
dialog.dismiss();
dialog = null;
}
+ if (selectPhoneAccountDialogFragment != null) {
+ selectPhoneAccountDialogFragment.dismiss();
+ selectPhoneAccountDialogFragment = null;
+ }
}
private static boolean shouldShowDisconnectErrorDialog(@NonNull DisconnectCause cause) {
@@ -769,9 +773,7 @@ public class InCallActivityCommon {
outgoingCall = CallList.getInstance().getPendingOutgoingCall();
}
- boolean isNewOutgoingCall = false;
if (intent.getBooleanExtra(INTENT_EXTRA_NEW_OUTGOING_CALL, false)) {
- isNewOutgoingCall = true;
intent.removeExtra(INTENT_EXTRA_NEW_OUTGOING_CALL);
// InCallActivity is responsible for disconnecting a new outgoing call if there
@@ -789,16 +791,18 @@ public class InCallActivityCommon {
}
boolean didShowAccountSelectionDialog = maybeShowAccountSelectionDialog();
- inCallActivity.onResolveIntent(outgoingCall, isNewOutgoingCall, didShowAccountSelectionDialog);
+ if (didShowAccountSelectionDialog) {
+ inCallActivity.hideMainInCallFragment();
+ }
}
private boolean maybeShowAccountSelectionDialog() {
- DialerCall call = CallList.getInstance().getWaitingForAccountCall();
- if (call == null) {
+ DialerCall waitingForAccountCall = CallList.getInstance().getWaitingForAccountCall();
+ if (waitingForAccountCall == null) {
return false;
}
- Bundle extras = call.getIntentExtras();
+ Bundle extras = waitingForAccountCall.getIntentExtras();
List<PhoneAccountHandle> phoneAccountHandles;
if (extras != null) {
phoneAccountHandles =
@@ -807,14 +811,15 @@ public class InCallActivityCommon {
phoneAccountHandles = new ArrayList<>();
}
- DialogFragment dialogFragment =
+ selectPhoneAccountDialogFragment =
SelectPhoneAccountDialogFragment.newInstance(
R.string.select_phone_account_for_calls,
true,
phoneAccountHandles,
selectAccountListener,
- call.getId());
- dialogFragment.show(inCallActivity.getFragmentManager(), TAG_SELECT_ACCOUNT_FRAGMENT);
+ waitingForAccountCall.getId());
+ selectPhoneAccountDialogFragment.show(
+ inCallActivity.getFragmentManager(), TAG_SELECT_ACCOUNT_FRAGMENT);
return true;
}
}
diff --git a/java/com/android/incallui/InCallPresenter.java b/java/com/android/incallui/InCallPresenter.java
index 97105fb78..0f3982ce4 100644
--- a/java/com/android/incallui/InCallPresenter.java
+++ b/java/com/android/incallui/InCallPresenter.java
@@ -42,23 +42,22 @@ import com.android.dialer.blocking.FilteredNumbersUtil;
import com.android.dialer.common.LogUtil;
import com.android.dialer.logging.Logger;
import com.android.dialer.logging.nano.InteractionEvent;
+import com.android.dialer.postcall.PostCall;
import com.android.dialer.telecom.TelecomUtil;
import com.android.dialer.util.TouchPointManager;
import com.android.incallui.InCallOrientationEventListener.ScreenOrientation;
import com.android.incallui.answerproximitysensor.PseudoScreenState;
import com.android.incallui.call.CallList;
import com.android.incallui.call.DialerCall;
-import com.android.incallui.call.DialerCall.SessionModificationState;
import com.android.incallui.call.ExternalCallList;
-import com.android.incallui.call.InCallVideoCallCallbackNotifier;
import com.android.incallui.call.TelecomAdapter;
-import com.android.incallui.call.VideoUtils;
import com.android.incallui.latencyreport.LatencyReport;
import com.android.incallui.legacyblocking.BlockedNumberContentObserver;
import com.android.incallui.spam.SpamCallListListener;
import com.android.incallui.util.TelecomCallUtil;
import com.android.incallui.videosurface.bindings.VideoSurfaceBindings;
import com.android.incallui.videosurface.protocol.VideoSurfaceTexture;
+import com.android.incallui.videotech.VideoTech;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
@@ -74,8 +73,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
* presenters that want to listen in on the in-call state changes. TODO: This class has become more
* of a state machine at this point. Consider renaming.
*/
-public class InCallPresenter
- implements CallList.Listener, InCallVideoCallCallbackNotifier.SessionModificationListener {
+public class InCallPresenter implements CallList.Listener {
private static final String EXTRA_FIRST_TIME_SHOWN =
"com.android.incallui.intent.extra.FIRST_TIME_SHOWN";
@@ -173,7 +171,6 @@ public class InCallPresenter
private ProximitySensor mProximitySensor;
private final PseudoScreenState mPseudoScreenState = new PseudoScreenState();
private boolean mServiceConnected;
- private boolean mAccountSelectionCancelled;
private InCallCameraManager mInCallCameraManager;
private FilteredNumberAsyncQueryHandler mFilteredQueryHandler;
private CallList.Listener mSpamCallListListener;
@@ -347,7 +344,6 @@ public class InCallPresenter
mCallList.addListener(mSpamCallListListener);
VideoPauseController.getInstance().setUp(this);
- InCallVideoCallCallbackNotifier.getInstance().addSessionModificationListener(this);
mFilteredQueryHandler = new FilteredNumberAsyncQueryHandler(context);
mContext
@@ -376,7 +372,6 @@ public class InCallPresenter
attemptCleanup();
VideoPauseController.getInstance().tearDown();
- InCallVideoCallCallbackNotifier.getInstance().removeSessionModificationListener(this);
}
private void attemptFinishActivity() {
@@ -385,12 +380,6 @@ public class InCallPresenter
if (doFinish) {
mInCallActivity.setExcludeFromRecents(true);
mInCallActivity.finish();
-
- if (mAccountSelectionCancelled) {
- // This finish is a result of account selection cancellation
- // do not include activity ending transition
- mInCallActivity.overridePendingTransition(0, 0);
- }
}
}
@@ -664,6 +653,19 @@ public class InCallPresenter
InCallState newState = getPotentialStateFromCallList(callList);
InCallState oldState = mInCallState;
Log.d(this, "onCallListChange oldState= " + oldState + " newState=" + newState);
+
+ // If the user placed a call and was asked to choose the account, but then pressed "Home", the
+ // incall activity for that call will still exist (even if it's not visible). In the case of
+ // an incoming call in that situation, just disconnect that "waiting for account" call and
+ // dismiss the dialog. The same activity will be reused to handle the new incoming call. See
+ // b/33247755 for more details.
+ DialerCall waitingForAccountCall;
+ if (newState == InCallState.INCOMING
+ && (waitingForAccountCall = callList.getWaitingForAccountCall()) != null) {
+ waitingForAccountCall.disconnect();
+ mInCallActivity.dismissPendingDialogs();
+ }
+
newState = startOrFinishUi(newState);
Log.d(this, "onCallListChange newState changed to " + newState);
@@ -705,13 +707,13 @@ public class InCallPresenter
@Override
public void onUpgradeToVideo(DialerCall call) {
- if (call.getSessionModificationState()
- == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST
+ if (call.getVideoTech().getSessionModificationState()
+ == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST
&& mInCallState == InCallPresenter.InCallState.INCOMING) {
LogUtil.i(
"InCallPresenter.onUpgradeToVideo",
"rejecting upgrade request due to existing incoming call");
- call.declineUpgradeRequest();
+ call.getVideoTech().declineVideoRequest();
}
if (mInCallActivity != null) {
@@ -721,15 +723,15 @@ public class InCallPresenter
}
@Override
- public void onSessionModificationStateChange(@SessionModificationState int newState) {
+ public void onSessionModificationStateChange(DialerCall call) {
+ int newState = call.getVideoTech().getSessionModificationState();
LogUtil.i("InCallPresenter.onSessionModificationStateChange", "state: %d", newState);
if (mProximitySensor == null) {
LogUtil.i("InCallPresenter.onSessionModificationStateChange", "proximitySensor is null");
return;
}
mProximitySensor.setIsAttemptingVideoCall(
- VideoUtils.hasSentVideoUpgradeRequest(newState)
- || VideoUtils.hasReceivedVideoUpgradeRequest(newState));
+ call.hasSentVideoUpgradeRequest() || call.hasReceivedVideoUpgradeRequest());
if (mInCallActivity != null) {
// Re-evaluate which fragment is being shown.
mInCallActivity.onPrimaryCallStateChanged();
@@ -754,19 +756,10 @@ public class InCallPresenter
if (call.isEmergencyCall()) {
FilteredNumbersUtil.recordLastEmergencyCallTime(mContext);
}
- }
-
- @Override
- public void onUpgradeToVideoRequest(DialerCall call, int videoState) {
- LogUtil.d(
- "InCallPresenter.onUpgradeToVideoRequest",
- "call = " + call + " video state = " + videoState);
- if (call == null) {
- return;
+ if (!call.getLogState().isIncoming && !mCallList.hasLiveCall()) {
+ PostCall.onCallDisconnected(mContext, call.getNumber(), call.getConnectTimeMillis());
}
-
- call.setRequestedVideoState(videoState);
}
/** Given the call list, return the state in which the in-call screen should be. */
@@ -916,6 +909,24 @@ public class InCallPresenter
&& !mInCallActivity.isFinishing());
}
+ private boolean isActivityVisible() {
+ return mInCallActivity != null && mInCallActivity.isVisible();
+ }
+
+ boolean shouldShowFullScreenNotification() {
+ /**
+ * This is to cover the case where the incall activity is started but in the background, e.g.
+ * when the user pressed Home from the account selection dialog or an existing call. In the case
+ * that incall activity is already visible, there's no need to configure the notification with a
+ * full screen intent.
+ */
+ LogUtil.d(
+ "InCallPresenter.shouldShowFullScreenNotification",
+ "isActivityVisible: %b",
+ isActivityVisible());
+ return !isActivityVisible();
+ }
+
/**
* Determines if the In-Call app is currently changing configuration.
*
@@ -1018,7 +1029,7 @@ public class InCallPresenter
// present (e.g. a call was accepted by a bluetooth or wired headset), we want to
// bring it up the UI regardless.
if (!isShowingInCallUi() && mInCallState != InCallState.NO_CALLS) {
- showInCall(showDialpad, false /* newOutgoingCall */, false /* isVideoCall */);
+ showInCall(showDialpad, false /* newOutgoingCall */);
}
}
@@ -1281,7 +1292,7 @@ public class InCallPresenter
if (showCallUi || showAccountPicker) {
Log.i(this, "Start in call UI");
- showInCall(false /* showDialpad */, !showAccountPicker /* newOutgoingCall */, false);
+ showInCall(false /* showDialpad */, !showAccountPicker /* newOutgoingCall */);
} else if (startIncomingCallSequence) {
Log.i(this, "Start Full Screen in call UI");
@@ -1332,7 +1343,7 @@ public class InCallPresenter
mCallList.getActiveCall() != null && mCallList.getIncomingCall() != null;
if (isCallWaiting) {
- showInCall(false, false, false /* isVideoCall */);
+ showInCall(false, false);
} else {
mStatusBarNotifier.updateNotification(mCallList);
}
@@ -1403,11 +1414,11 @@ public class InCallPresenter
}
}
- public void showInCall(boolean showDialpad, boolean newOutgoingCall, boolean isVideoCall) {
+ public void showInCall(boolean showDialpad, boolean newOutgoingCall) {
Log.i(this, "Showing InCallActivity");
mContext.startActivity(
InCallActivity.getIntent(
- mContext, showDialpad, newOutgoingCall, isVideoCall, false /* forFullScreen */));
+ mContext, showDialpad, newOutgoingCall, false /* forFullScreen */));
}
public void onServiceBind() {
@@ -1441,15 +1452,11 @@ public class InCallPresenter
final PhoneAccountHandle accountHandle =
intent.getParcelableExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE);
final Point touchPoint = extras.getParcelable(TouchPointManager.TOUCH_POINT);
- int videoState =
- extras.getInt(
- TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, VideoProfile.STATE_AUDIO_ONLY);
InCallPresenter.getInstance().setBoundAndWaitingForOutgoingCall(true, accountHandle);
final Intent activityIntent =
- InCallActivity.getIntent(
- mContext, false, true, VideoUtils.isVideoCall(videoState), false /* forFullScreen */);
+ InCallActivity.getIntent(mContext, false, true, false /* forFullScreen */);
activityIntent.putExtra(TouchPointManager.TOUCH_POINT, touchPoint);
mContext.startActivity(activityIntent);
}
diff --git a/java/com/android/incallui/NotificationBroadcastReceiver.java b/java/com/android/incallui/NotificationBroadcastReceiver.java
index 5c5d255cc..cef18958e 100644
--- a/java/com/android/incallui/NotificationBroadcastReceiver.java
+++ b/java/com/android/incallui/NotificationBroadcastReceiver.java
@@ -27,7 +27,6 @@ import com.android.dialer.logging.Logger;
import com.android.dialer.logging.nano.DialerImpression;
import com.android.incallui.call.CallList;
import com.android.incallui.call.DialerCall;
-import com.android.incallui.call.VideoUtils;
/**
* Accepts broadcast Intents which will be prepared by {@link StatusBarNotifier} and thus sent from
@@ -96,7 +95,7 @@ public class NotificationBroadcastReceiver extends BroadcastReceiver {
} else {
DialerCall call = callList.getVideoUpgradeRequestCall();
if (call != null) {
- call.acceptUpgradeRequest(call.getRequestedVideoState());
+ call.getVideoTech().acceptVideoRequest();
}
}
}
@@ -109,7 +108,7 @@ public class NotificationBroadcastReceiver extends BroadcastReceiver {
} else {
DialerCall call = callList.getVideoUpgradeRequestCall();
if (call != null) {
- call.declineUpgradeRequest();
+ call.getVideoTech().declineVideoRequest();
}
}
}
@@ -142,10 +141,7 @@ public class NotificationBroadcastReceiver extends BroadcastReceiver {
if (call != null) {
call.answer(videoState);
InCallPresenter.getInstance()
- .showInCall(
- false /* showDialpad */,
- false /* newOutgoingCall */,
- VideoUtils.isVideoCall(videoState));
+ .showInCall(false /* showDialpad */, false /* newOutgoingCall */);
}
}
}
diff --git a/java/com/android/incallui/ProximitySensor.java b/java/com/android/incallui/ProximitySensor.java
index 91220627c..229b58ce7 100644
--- a/java/com/android/incallui/ProximitySensor.java
+++ b/java/com/android/incallui/ProximitySensor.java
@@ -28,7 +28,7 @@ import com.android.incallui.AudioModeProvider.AudioModeListener;
import com.android.incallui.InCallPresenter.InCallState;
import com.android.incallui.InCallPresenter.InCallStateListener;
import com.android.incallui.call.CallList;
-import com.android.incallui.call.VideoUtils;
+import com.android.incallui.call.DialerCall;
/**
* Class manages the proximity sensor for the in-call UI. We enable the proximity sensor while the
@@ -103,7 +103,8 @@ public class ProximitySensor
boolean hasOngoingCall = InCallState.INCALL == newState && callList.hasLiveCall();
boolean isOffhook = (InCallState.OUTGOING == newState) || hasOngoingCall;
- boolean isVideoCall = VideoUtils.isVideoCall(callList.getActiveCall());
+ DialerCall activeCall = callList.getActiveCall();
+ boolean isVideoCall = activeCall != null && activeCall.isVideoCall();
if (isOffhook != mIsPhoneOffhook || mIsVideoCall != isVideoCall) {
mIsPhoneOffhook = isOffhook;
diff --git a/java/com/android/incallui/StatusBarNotifier.java b/java/com/android/incallui/StatusBarNotifier.java
index c7226753f..d6262be18 100644
--- a/java/com/android/incallui/StatusBarNotifier.java
+++ b/java/com/android/incallui/StatusBarNotifier.java
@@ -24,8 +24,10 @@ import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_
import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_VIDEO_UPGRADE_REQUEST;
import static com.android.incallui.NotificationBroadcastReceiver.ACTION_HANG_UP_ONGOING_CALL;
+import android.Manifest;
import android.app.ActivityManager;
import android.app.Notification;
+import android.app.Notification.Builder;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
@@ -34,6 +36,7 @@ import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
import android.media.AudioAttributes;
import android.net.Uri;
import android.os.Build.VERSION;
@@ -41,10 +44,13 @@ import android.os.Build.VERSION_CODES;
import android.support.annotation.ColorRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.annotation.RequiresPermission;
import android.support.annotation.StringRes;
import android.support.annotation.VisibleForTesting;
+import android.support.v4.os.BuildCompat;
import android.telecom.Call.Details;
import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.text.BidiFormatter;
import android.text.Spannable;
@@ -54,10 +60,13 @@ import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import com.android.contacts.common.ContactsUtils;
import com.android.contacts.common.ContactsUtils.UserType;
+import com.android.contacts.common.lettertiles.LetterTileDrawable;
import com.android.contacts.common.preference.ContactsPreferences;
import com.android.contacts.common.util.BitmapUtil;
import com.android.contacts.common.util.ContactDisplayUtils;
import com.android.dialer.common.LogUtil;
+import com.android.dialer.notification.NotificationChannelManager;
+import com.android.dialer.notification.NotificationChannelManager.Channel;
import com.android.dialer.util.DrawableConverter;
import com.android.incallui.ContactInfoCache.ContactCacheEntry;
import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback;
@@ -65,11 +74,13 @@ import com.android.incallui.InCallPresenter.InCallState;
import com.android.incallui.async.PausableExecutorImpl;
import com.android.incallui.call.CallList;
import com.android.incallui.call.DialerCall;
-import com.android.incallui.call.DialerCall.SessionModificationState;
import com.android.incallui.call.DialerCallListener;
import com.android.incallui.ringtone.DialerRingtoneManager;
import com.android.incallui.ringtone.InCallTonePlayer;
import com.android.incallui.ringtone.ToneGeneratorFactory;
+import com.android.incallui.videotech.VideoTech;
+import java.util.List;
+import java.util.Locale;
import java.util.Objects;
/** This class adds Notifications to the status bar for the in-call experience. */
@@ -79,9 +90,9 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
// Indicates that no notification is currently showing.
private static final int NOTIFICATION_NONE = 0;
// Notification for an active call. This is non-interruptive, but cannot be dismissed.
- private static final int NOTIFICATION_IN_CALL = 1;
+ private static final int NOTIFICATION_IN_CALL = R.id.notification_ongoing_call;
// Notification for incoming calls. This is interruptive and will show up as a HUN.
- private static final int NOTIFICATION_INCOMING_CALL = 2;
+ private static final int NOTIFICATION_INCOMING_CALL = R.id.notification_incoming_call;
private static final int PENDING_INTENT_REQUEST_CODE_NON_FULL_SCREEN = 0;
private static final int PENDING_INTENT_REQUEST_CODE_FULL_SCREEN = 1;
@@ -101,8 +112,9 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
private String mSavedContentTitle;
private Uri mRingtone;
private StatusBarCallListener mStatusBarCallListener;
+ private boolean mShowFullScreenIntent;
- public StatusBarNotifier(@NonNull Context context, @NonNull ContactInfoCache contactInfoCache) {
+ StatusBarNotifier(@NonNull Context context, @NonNull ContactInfoCache contactInfoCache) {
Objects.requireNonNull(context);
mContext = context;
mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext);
@@ -120,9 +132,9 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
* notifications.
*/
static void clearAllCallNotifications(Context backupContext) {
- Log.i(
- StatusBarNotifier.class.getSimpleName(),
- "Something terrible happened. Clear all InCall notifications");
+ LogUtil.i(
+ "StatusBarNotifier.clearAllCallNotifications",
+ "something terrible happened, clear all InCall notifications");
NotificationManager notificationManager =
backupContext.getSystemService(NotificationManager.class);
@@ -153,10 +165,17 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
return PendingIntent.getBroadcast(context, 0, intent, 0);
}
+ private static void setColorized(@NonNull Builder builder) {
+ if (BuildCompat.isAtLeastO()) {
+ builder.setColorized(true);
+ }
+ }
+
/** Creates notifications according to the state we receive from {@link InCallPresenter}. */
@Override
+ @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
- Log.d(this, "onStateChange");
+ LogUtil.d("StatusBarNotifier.onStateChange", "%s->%s", oldState, newState);
updateNotification(callList);
}
@@ -177,7 +196,8 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
*
* @see #updateInCallNotification(CallList)
*/
- public void updateNotification(CallList callList) {
+ @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
+ void updateNotification(CallList callList) {
updateInCallNotification(callList);
}
@@ -191,7 +211,7 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
setStatusBarCallListener(null);
}
if (mCurrentNotification != NOTIFICATION_NONE) {
- Log.d(this, "cancelInCall()...");
+ LogUtil.d("StatusBarNotifier.cancelNotification", "cancel");
mNotificationManager.cancel(mCurrentNotification);
}
mCurrentNotification = NOTIFICATION_NONE;
@@ -202,8 +222,9 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
* status bar notification based on the current telephony state, or cancels the notification if
* the phone is totally idle.
*/
+ @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
private void updateInCallNotification(CallList callList) {
- Log.d(this, "updateInCallNotification...");
+ LogUtil.d("StatusBarNotifier.updateInCallNotification", "");
final DialerCall call = getCallToShow(callList);
@@ -214,6 +235,7 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
}
}
+ @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
private void showNotification(final CallList callList, final DialerCall call) {
final boolean isIncoming =
(call.getState() == DialerCall.State.INCOMING
@@ -230,6 +252,7 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
isIncoming,
new ContactInfoCacheCallback() {
@Override
+ @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
public void onContactInfoComplete(String callId, ContactCacheEntry entry) {
DialerCall call = callList.getCallById(callId);
if (call != null) {
@@ -239,6 +262,7 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
}
@Override
+ @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
public void onImageLoadComplete(String callId, ContactCacheEntry entry) {
DialerCall call = callList.getCallById(callId);
if (call != null) {
@@ -249,6 +273,7 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
}
/** Sets up the main Ui for the notification */
+ @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
private void buildAndSendNotification(
CallList callList, DialerCall originalCall, ContactCacheEntry contactInfo) {
// This can get called to update an existing notification after contact information has come
@@ -268,8 +293,8 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
final String contentTitle = getContentTitle(contactInfo, call);
final boolean isVideoUpgradeRequest =
- call.getSessionModificationState()
- == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
+ call.getVideoTech().getSessionModificationState()
+ == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
final int notificationType;
if (callState == DialerCall.State.INCOMING
|| callState == DialerCall.State.CALL_WAITING
@@ -286,7 +311,8 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
contentTitle,
callState,
notificationType,
- contactInfo.contactRingtoneUri)) {
+ contactInfo.contactRingtoneUri,
+ InCallPresenter.getInstance().shouldShowFullScreenNotification())) {
return;
}
@@ -300,9 +326,10 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
Notification.Builder publicBuilder = new Notification.Builder(mContext);
publicBuilder
.setSmallIcon(iconResId)
- .setColor(mContext.getResources().getColor(R.color.dialer_theme_color))
+ .setColor(mContext.getResources().getColor(R.color.dialer_theme_color, mContext.getTheme()))
// Hide work call state for the lock screen notification
.setContentTitle(getContentString(call, ContactsUtils.USER_TYPE_CURRENT));
+ setColorized(publicBuilder);
setNotificationWhen(call, callState, publicBuilder);
// Builder for the notification shown when the device is unlocked or the user has set their
@@ -311,28 +338,26 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
builder.setPublicVersion(publicBuilder.build());
// Set up the main intent to send the user to the in-call screen
- builder.setContentIntent(
- createLaunchPendingIntent(false /* isFullScreen */, call.isVideoCall()));
+ builder.setContentIntent(createLaunchPendingIntent(false /* isFullScreen */));
// Set the intent as a full screen intent as well if a call is incoming
+ PhoneAccountHandle accountHandle = call.getAccountHandle();
+ if (accountHandle == null) {
+ accountHandle = getAnyPhoneAccount();
+ }
if (notificationType == NOTIFICATION_INCOMING_CALL) {
- if (!InCallPresenter.getInstance().isActivityStarted()) {
+ NotificationChannelManager.applyChannel(
+ builder, mContext, Channel.INCOMING_CALL, accountHandle);
+ if (InCallPresenter.getInstance().shouldShowFullScreenNotification()) {
configureFullScreenIntent(
- builder,
- createLaunchPendingIntent(true /* isFullScreen */, call.isVideoCall()),
- callList,
- call);
- } else {
- // If the incall screen is already up, we don't want to show HUN but regular notification
- // should still be shown. In order to do that the previous one with full screen intent
- // needs to be cancelled.
- LogUtil.d(
- "StatusBarNotifier.buildAndSendNotification",
- "cancel previous incoming call notification");
- mNotificationManager.cancel(NOTIFICATION_INCOMING_CALL);
+ builder, createLaunchPendingIntent(true /* isFullScreen */), callList, call);
}
- // Set the notification category for incoming calls
+ // Set the notification category and bump the priority for incoming calls
builder.setCategory(Notification.CATEGORY_CALL);
+ builder.setPriority(Notification.PRIORITY_MAX);
+ } else {
+ NotificationChannelManager.applyChannel(
+ builder, mContext, Channel.ONGOING_CALL, accountHandle);
}
// Set the content
@@ -340,7 +365,9 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
builder.setSmallIcon(iconResId);
builder.setContentTitle(contentTitle);
builder.setLargeIcon(largeIcon);
- builder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color));
+ builder.setColor(
+ mContext.getResources().getColor(R.color.dialer_theme_color, mContext.getTheme()));
+ setColorized(builder);
if (isVideoUpgradeRequest) {
builder.setUsesChronometer(false);
@@ -367,15 +394,20 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
}
}
if (mDialerRingtoneManager.shouldPlayCallWaitingTone(callState)) {
- Log.v(this, "Playing call waiting tone");
+ LogUtil.v("StatusBarNotifier.buildAndSendNotification", "playing call waiting tone");
mDialerRingtoneManager.playCallWaitingTone();
}
if (mCurrentNotification != notificationType && mCurrentNotification != NOTIFICATION_NONE) {
- Log.i(this, "Previous notification already showing - cancelling " + mCurrentNotification);
+ LogUtil.i(
+ "StatusBarNotifier.buildAndSendNotification",
+ "previous notification already showing - cancelling " + mCurrentNotification);
mNotificationManager.cancel(mCurrentNotification);
}
- Log.i(this, "Displaying notification for " + notificationType);
+ LogUtil.i(
+ "StatusBarNotifier.buildAndSendNotification",
+ "displaying notification for " + notificationType);
+
try {
mNotificationManager.notify(notificationType, notification);
} catch (RuntimeException e) {
@@ -385,14 +417,32 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
activityManager.getMemoryInfo(memoryInfo);
throw new RuntimeException(
String.format(
+ Locale.US,
"Error displaying notification with photo type: %d (low memory? %b, availMem: %d)",
- contactInfo.photoType, memoryInfo.lowMemory, memoryInfo.availMem),
+ contactInfo.photoType,
+ memoryInfo.lowMemory,
+ memoryInfo.availMem),
e);
}
call.getLatencyReport().onNotificationShown();
mCurrentNotification = notificationType;
}
+ @Nullable
+ @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
+ private PhoneAccountHandle getAnyPhoneAccount() {
+ PhoneAccountHandle accountHandle;
+ TelecomManager telecomManager = mContext.getSystemService(TelecomManager.class);
+ accountHandle = telecomManager.getDefaultOutgoingPhoneAccount(PhoneAccount.SCHEME_TEL);
+ if (accountHandle == null) {
+ List<PhoneAccountHandle> accountHandles = telecomManager.getCallCapablePhoneAccounts();
+ if (!accountHandles.isEmpty()) {
+ accountHandle = accountHandles.get(0);
+ }
+ }
+ return accountHandle;
+ }
+
private void createIncomingCallNotification(
DialerCall call, int state, Notification.Builder builder) {
setNotificationWhen(call, state, builder);
@@ -438,7 +488,8 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
String contentTitle,
int state,
int notificationType,
- Uri ringtone) {
+ Uri ringtone,
+ boolean showFullScreenIntent) {
// The two are different:
// if new title is not null, it should be different from saved version OR
@@ -454,13 +505,15 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
|| (mCallState != state)
|| (mSavedLargeIcon != largeIcon)
|| contentTitleChanged
- || !Objects.equals(mRingtone, ringtone);
+ || !Objects.equals(mRingtone, ringtone)
+ || mShowFullScreenIntent != showFullScreenIntent;
// If we aren't showing a notification right now or the notification type is changing,
// definitely do an update.
if (mCurrentNotification != notificationType) {
if (mCurrentNotification == NOTIFICATION_NONE) {
- Log.d(this, "Showing notification for first time.");
+ LogUtil.d(
+ "StatusBarNotifier.checkForChangeAndSaveData", "showing notification for first time.");
}
retval = true;
}
@@ -471,9 +524,11 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
mSavedLargeIcon = largeIcon;
mSavedContentTitle = contentTitle;
mRingtone = ringtone;
+ mShowFullScreenIntent = showFullScreenIntent;
if (retval) {
- Log.d(this, "Data changed. Showing notification");
+ LogUtil.d(
+ "StatusBarNotifier.checkForChangeAndSaveData", "data changed. Showing notification");
}
return retval;
@@ -520,8 +575,34 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) {
largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap();
}
+ if (contactInfo.photo == null) {
+ int width =
+ (int) mContext.getResources().getDimension(android.R.dimen.notification_large_icon_width);
+ int height =
+ (int)
+ mContext.getResources().getDimension(android.R.dimen.notification_large_icon_height);
+ int contactType = LetterTileDrawable.TYPE_DEFAULT;
+ LetterTileDrawable lettertile = new LetterTileDrawable(mContext.getResources());
+
+ // TODO: Deduplicate across Dialer. b/36195917
+ if (CallerInfoUtils.isVoiceMailNumber(mContext, call)) {
+ contactType = LetterTileDrawable.TYPE_VOICEMAIL;
+ } else if (contactInfo.isBusiness) {
+ contactType = LetterTileDrawable.TYPE_BUSINESS;
+ } else if (call.getNumberPresentation() == TelecomManager.PRESENTATION_RESTRICTED) {
+ contactType = LetterTileDrawable.TYPE_GENERIC_AVATAR;
+ }
+ lettertile.setCanonicalDialerLetterTileDetails(
+ contactInfo.namePrimary == null ? contactInfo.number : contactInfo.namePrimary,
+ contactInfo.lookupKey,
+ LetterTileDrawable.SHAPE_CIRCLE,
+ contactType);
+ largeIcon = lettertile.getBitmap(width, height);
+ }
+
if (call.isSpam()) {
- Drawable drawable = mContext.getResources().getDrawable(R.drawable.blocked_contact);
+ Drawable drawable =
+ mContext.getResources().getDrawable(R.drawable.blocked_contact, mContext.getTheme());
largeIcon = DrawableConverter.drawableToBitmap(drawable);
}
return largeIcon;
@@ -552,8 +633,8 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
// display that regardless of the state of the other calls.
if (call.getState() == DialerCall.State.ONHOLD) {
return R.drawable.ic_phone_paused_white_24dp;
- } else if (call.getSessionModificationState()
- == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+ } else if (call.getVideoTech().getSessionModificationState()
+ == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
return R.drawable.ic_videocam;
}
return R.anim.on_going_call;
@@ -594,8 +675,8 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
resId = R.string.notification_on_hold;
} else if (DialerCall.State.isDialing(call.getState())) {
resId = R.string.notification_dialing;
- } else if (call.getSessionModificationState()
- == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+ } else if (call.getVideoTech().getSessionModificationState()
+ == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
resId = R.string.notification_requesting_video_call;
}
@@ -639,64 +720,98 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
}
private void addAnswerAction(Notification.Builder builder) {
- Log.d(this, "Will show \"answer\" action in the incoming call Notification");
+ LogUtil.d(
+ "StatusBarNotifier.addAnswerAction",
+ "will show \"answer\" action in the incoming call Notification");
PendingIntent answerVoicePendingIntent =
createNotificationPendingIntent(mContext, ACTION_ANSWER_VOICE_INCOMING_CALL);
+ // We put animation resources in "anim" folder instead of "drawable", which causes Android
+ // Studio to complain.
+ // TODO: Move "anim" resources to "drawable" as recommended in AnimationDrawable doc?
+ //noinspection ResourceType
builder.addAction(
- R.anim.on_going_call,
- getActionText(R.string.notification_action_answer, R.color.notification_action_accept),
- answerVoicePendingIntent);
+ new Notification.Action.Builder(
+ Icon.createWithResource(mContext, R.anim.on_going_call),
+ getActionText(
+ R.string.notification_action_answer, R.color.notification_action_accept),
+ answerVoicePendingIntent)
+ .build());
}
private void addDismissAction(Notification.Builder builder) {
- Log.d(this, "Will show \"decline\" action in the incoming call Notification");
+ LogUtil.d(
+ "StatusBarNotifier.addDismissAction",
+ "will show \"decline\" action in the incoming call Notification");
PendingIntent declinePendingIntent =
createNotificationPendingIntent(mContext, ACTION_DECLINE_INCOMING_CALL);
builder.addAction(
- R.drawable.ic_close_dk,
- getActionText(R.string.notification_action_dismiss, R.color.notification_action_dismiss),
- declinePendingIntent);
+ new Notification.Action.Builder(
+ Icon.createWithResource(mContext, R.drawable.ic_close_dk),
+ getActionText(
+ R.string.notification_action_dismiss, R.color.notification_action_dismiss),
+ declinePendingIntent)
+ .build());
}
private void addHangupAction(Notification.Builder builder) {
- Log.d(this, "Will show \"hang-up\" action in the ongoing active call Notification");
+ LogUtil.d(
+ "StatusBarNotifier.addHangupAction",
+ "will show \"hang-up\" action in the ongoing active call Notification");
PendingIntent hangupPendingIntent =
createNotificationPendingIntent(mContext, ACTION_HANG_UP_ONGOING_CALL);
builder.addAction(
- R.drawable.ic_call_end_white_24dp,
- getActionText(R.string.notification_action_end_call, R.color.notification_action_end_call),
- hangupPendingIntent);
+ new Notification.Action.Builder(
+ Icon.createWithResource(mContext, R.drawable.ic_call_end_white_24dp),
+ getActionText(
+ R.string.notification_action_end_call, R.color.notification_action_end_call),
+ hangupPendingIntent)
+ .build());
}
private void addVideoCallAction(Notification.Builder builder) {
- Log.i(this, "Will show \"video\" action in the incoming call Notification");
+ LogUtil.i(
+ "StatusBarNotifier.addVideoCallAction",
+ "will show \"video\" action in the incoming call Notification");
PendingIntent answerVideoPendingIntent =
createNotificationPendingIntent(mContext, ACTION_ANSWER_VIDEO_INCOMING_CALL);
builder.addAction(
- R.drawable.ic_videocam,
- getActionText(
- R.string.notification_action_answer_video, R.color.notification_action_answer_video),
- answerVideoPendingIntent);
+ new Notification.Action.Builder(
+ Icon.createWithResource(mContext, R.drawable.ic_videocam),
+ getActionText(
+ R.string.notification_action_answer_video,
+ R.color.notification_action_answer_video),
+ answerVideoPendingIntent)
+ .build());
}
private void addAcceptUpgradeRequestAction(Notification.Builder builder) {
- Log.i(this, "Will show \"accept upgrade\" action in the incoming call Notification");
+ LogUtil.i(
+ "StatusBarNotifier.addAcceptUpgradeRequestAction",
+ "will show \"accept upgrade\" action in the incoming call Notification");
PendingIntent acceptVideoPendingIntent =
createNotificationPendingIntent(mContext, ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST);
builder.addAction(
- R.drawable.ic_videocam,
- getActionText(R.string.notification_action_accept, R.color.notification_action_accept),
- acceptVideoPendingIntent);
+ new Notification.Action.Builder(
+ Icon.createWithResource(mContext, R.drawable.ic_videocam),
+ getActionText(
+ R.string.notification_action_accept, R.color.notification_action_accept),
+ acceptVideoPendingIntent)
+ .build());
}
private void addDismissUpgradeRequestAction(Notification.Builder builder) {
- Log.i(this, "Will show \"dismiss upgrade\" action in the incoming call Notification");
+ LogUtil.i(
+ "StatusBarNotifier.addDismissUpgradeRequestAction",
+ "will show \"dismiss upgrade\" action in the incoming call Notification");
PendingIntent declineVideoPendingIntent =
createNotificationPendingIntent(mContext, ACTION_DECLINE_VIDEO_UPGRADE_REQUEST);
builder.addAction(
- R.drawable.ic_videocam,
- getActionText(R.string.notification_action_dismiss, R.color.notification_action_dismiss),
- declineVideoPendingIntent);
+ new Notification.Action.Builder(
+ Icon.createWithResource(mContext, R.drawable.ic_videocam),
+ getActionText(
+ R.string.notification_action_dismiss, R.color.notification_action_dismiss),
+ declineVideoPendingIntent)
+ .build());
}
/** Adds fullscreen intent to the builder. */
@@ -707,7 +822,7 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
// to the status bar). Setting fullScreenIntent will cause
// the InCallScreen to be launched immediately *unless* the
// current foreground activity is marked as "immersive".
- Log.d(this, "- Setting fullScreenIntent: " + intent);
+ LogUtil.d("StatusBarNotifier.configureFullScreenIntent", "setting fullScreenIntent: " + intent);
builder.setFullScreenIntent(intent, true);
// Ugly hack alert:
@@ -740,7 +855,9 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
&& callList.getBackgroundCall() != null));
if (isCallWaiting) {
- Log.i(this, "updateInCallNotification: call-waiting! force relaunch...");
+ LogUtil.i(
+ "StatusBarNotifier.configureFullScreenIntent",
+ "updateInCallNotification: call-waiting! force relaunch...");
// Cancel the IN_CALL_NOTIFICATION immediately before
// (re)posting it; this seems to force the
// NotificationManager to launch the fullScreenIntent.
@@ -751,21 +868,15 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
private Notification.Builder getNotificationBuilder() {
final Notification.Builder builder = new Notification.Builder(mContext);
builder.setOngoing(true);
-
- // Make the notification prioritized over the other normal notifications.
- builder.setPriority(Notification.PRIORITY_HIGH);
+ builder.setOnlyAlertOnce(true);
return builder;
}
- private PendingIntent createLaunchPendingIntent(boolean isFullScreen, boolean isVideoCall) {
+ private PendingIntent createLaunchPendingIntent(boolean isFullScreen) {
Intent intent =
InCallActivity.getIntent(
- mContext,
- false /* showDialpad */,
- false /* newOutgoingCall */,
- isVideoCall,
- isFullScreen);
+ mContext, false /* showDialpad */, false /* newOutgoingCall */, isFullScreen);
int requestCode = PENDING_INTENT_REQUEST_CODE_NON_FULL_SCREEN;
if (isFullScreen) {
@@ -832,8 +943,9 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
* bar notification as required.
*/
@Override
- public void onDialerCallSessionModificationStateChange(@SessionModificationState int state) {
- if (state == DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST) {
+ public void onDialerCallSessionModificationStateChange() {
+ if (mDialerCall.getVideoTech().getSessionModificationState()
+ == VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST) {
cleanup();
updateNotification(CallList.getInstance());
}
diff --git a/java/com/android/incallui/VideoCallPresenter.java b/java/com/android/incallui/VideoCallPresenter.java
index 971b6957a..20dc987da 100644
--- a/java/com/android/incallui/VideoCallPresenter.java
+++ b/java/com/android/incallui/VideoCallPresenter.java
@@ -21,7 +21,6 @@ import android.content.Context;
import android.graphics.Point;
import android.os.Handler;
import android.support.annotation.Nullable;
-import android.telecom.Connection;
import android.telecom.InCallService.VideoCall;
import android.telecom.VideoProfile;
import android.telecom.VideoProfile.CameraCapabilities;
@@ -36,17 +35,18 @@ import com.android.incallui.InCallPresenter.InCallStateListener;
import com.android.incallui.InCallPresenter.IncomingCallListener;
import com.android.incallui.call.CallList;
import com.android.incallui.call.DialerCall;
-import com.android.incallui.call.DialerCall.SessionModificationState;
+import com.android.incallui.call.DialerCall.CameraDirection;
import com.android.incallui.call.DialerCall.State;
import com.android.incallui.call.InCallVideoCallCallbackNotifier;
import com.android.incallui.call.InCallVideoCallCallbackNotifier.SurfaceChangeListener;
-import com.android.incallui.call.InCallVideoCallCallbackNotifier.VideoEventListener;
import com.android.incallui.call.VideoUtils;
import com.android.incallui.util.AccessibilityUtil;
import com.android.incallui.video.protocol.VideoCallScreen;
import com.android.incallui.video.protocol.VideoCallScreenDelegate;
import com.android.incallui.videosurface.protocol.VideoSurfaceDelegate;
import com.android.incallui.videosurface.protocol.VideoSurfaceTexture;
+import com.android.incallui.videotech.VideoTech;
+import com.android.incallui.videotech.VideoTech.SessionModificationState;
import java.util.Objects;
/**
@@ -78,7 +78,6 @@ public class VideoCallPresenter
InCallStateListener,
InCallDetailsListener,
SurfaceChangeListener,
- VideoEventListener,
InCallPresenter.InCallEventListener,
VideoCallScreenDelegate {
@@ -90,32 +89,6 @@ public class VideoCallPresenter
/** The current context. */
private Context mContext;
- @Override
- public boolean shouldShowCameraPermissionDialog() {
- if (mPrimaryCall == null) {
- LogUtil.i("VideoCallPresenter.shouldShowCameraPermissionDialog", "null call");
- return false;
- }
- if (mPrimaryCall.didShowCameraPermission()) {
- LogUtil.i(
- "VideoCallPresenter.shouldShowCameraPermissionDialog", "already shown for this call");
- return false;
- }
- if (!ConfigProviderBindings.get(mContext)
- .getBoolean("camera_permission_dialog_allowed", true)) {
- LogUtil.i("VideoCallPresenter.shouldShowCameraPermissionDialog", "disabled by config");
- return false;
- }
- return !VideoUtils.hasCameraPermission(mContext) || !VideoUtils.isCameraAllowedByUser(mContext);
- }
-
- @Override
- public void onCameraPermissionDialogShown() {
- if (mPrimaryCall != null) {
- mPrimaryCall.setDidShowCameraPermission(true);
- }
- }
-
/** The call the video surfaces are currently related to */
private DialerCall mPrimaryCall;
/**
@@ -231,49 +204,49 @@ public class VideoCallPresenter
// this function should never be called with null call object, however if it happens we
// should handle it gracefully.
if (call == null) {
- cameraDir = DialerCall.VideoSettings.CAMERA_DIRECTION_UNKNOWN;
+ cameraDir = CameraDirection.CAMERA_DIRECTION_UNKNOWN;
LogUtil.e(
"VideoCallPresenter.updateCameraSelection",
"call is null. Setting camera direction to default value (CAMERA_DIRECTION_UNKNOWN)");
}
// Clear camera direction if this is not a video call.
- else if (VideoUtils.isAudioCall(call) && !isVideoUpgrade(call)) {
- cameraDir = DialerCall.VideoSettings.CAMERA_DIRECTION_UNKNOWN;
- call.getVideoSettings().setCameraDir(cameraDir);
+ else if (isAudioCall(call) && !isVideoUpgrade(call)) {
+ cameraDir = CameraDirection.CAMERA_DIRECTION_UNKNOWN;
+ call.setCameraDir(cameraDir);
}
// If this is a waiting video call, default to active call's camera,
// since we don't want to change the current camera for waiting call
// without user's permission.
- else if (VideoUtils.isVideoCall(activeCall) && VideoUtils.isIncomingVideoCall(call)) {
- cameraDir = activeCall.getVideoSettings().getCameraDir();
+ else if (isVideoCall(activeCall) && isIncomingVideoCall(call)) {
+ cameraDir = activeCall.getCameraDir();
}
// Infer the camera direction from the video state and store it,
// if this is an outgoing video call.
- else if (VideoUtils.isOutgoingVideoCall(call) && !isCameraDirectionSet(call)) {
+ else if (isOutgoingVideoCall(call) && !isCameraDirectionSet(call)) {
cameraDir = toCameraDirection(call.getVideoState());
- call.getVideoSettings().setCameraDir(cameraDir);
+ call.setCameraDir(cameraDir);
}
// Use the stored camera dir if this is an outgoing video call for which camera direction
// is set.
- else if (VideoUtils.isOutgoingVideoCall(call)) {
- cameraDir = call.getVideoSettings().getCameraDir();
+ else if (isOutgoingVideoCall(call)) {
+ cameraDir = call.getCameraDir();
}
// Infer the camera direction from the video state and store it,
// if this is an active video call and camera direction is not set.
- else if (VideoUtils.isActiveVideoCall(call) && !isCameraDirectionSet(call)) {
+ else if (isActiveVideoCall(call) && !isCameraDirectionSet(call)) {
cameraDir = toCameraDirection(call.getVideoState());
- call.getVideoSettings().setCameraDir(cameraDir);
+ call.setCameraDir(cameraDir);
}
// Use the stored camera dir if this is an active video call for which camera direction
// is set.
- else if (VideoUtils.isActiveVideoCall(call)) {
- cameraDir = call.getVideoSettings().getCameraDir();
+ else if (isActiveVideoCall(call)) {
+ cameraDir = call.getCameraDir();
}
// For all other cases infer the camera direction but don't store it in the call object.
@@ -289,20 +262,18 @@ public class VideoCallPresenter
final InCallCameraManager cameraManager =
InCallPresenter.getInstance().getInCallCameraManager();
cameraManager.setUseFrontFacingCamera(
- cameraDir == DialerCall.VideoSettings.CAMERA_DIRECTION_FRONT_FACING);
+ cameraDir == CameraDirection.CAMERA_DIRECTION_FRONT_FACING);
}
private static int toCameraDirection(int videoState) {
return VideoProfile.isTransmissionEnabled(videoState)
&& !VideoProfile.isBidirectional(videoState)
- ? DialerCall.VideoSettings.CAMERA_DIRECTION_BACK_FACING
- : DialerCall.VideoSettings.CAMERA_DIRECTION_FRONT_FACING;
+ ? CameraDirection.CAMERA_DIRECTION_BACK_FACING
+ : CameraDirection.CAMERA_DIRECTION_FRONT_FACING;
}
private static boolean isCameraDirectionSet(DialerCall call) {
- return VideoUtils.isVideoCall(call)
- && call.getVideoSettings().getCameraDir()
- != DialerCall.VideoSettings.CAMERA_DIRECTION_UNKNOWN;
+ return isVideoCall(call) && call.getCameraDir() != CameraDirection.CAMERA_DIRECTION_UNKNOWN;
}
private static String toSimpleString(DialerCall call) {
@@ -350,7 +321,6 @@ public class VideoCallPresenter
// Register for surface and video events from {@link InCallVideoCallListener}s.
InCallVideoCallCallbackNotifier.getInstance().addSurfaceChangeListener(this);
- InCallVideoCallCallbackNotifier.getInstance().addVideoEventListener(this);
mCurrentVideoState = VideoProfile.STATE_AUDIO_ONLY;
mCurrentCallState = DialerCall.State.INVALID;
@@ -379,7 +349,6 @@ public class VideoCallPresenter
InCallPresenter.getInstance().getLocalVideoSurfaceTexture().setDelegate(null);
InCallVideoCallCallbackNotifier.getInstance().removeSurfaceChangeListener(this);
- InCallVideoCallCallbackNotifier.getInstance().removeVideoEventListener(this);
// Ensure that the call's camera direction is updated (most likely to UNKNOWN). Normally this
// happens after any call state changes but we're unregistering from InCallPresenter above so
@@ -447,7 +416,7 @@ public class VideoCallPresenter
showVideoUi(
mPrimaryCall.getVideoState(),
mPrimaryCall.getState(),
- mPrimaryCall.getSessionModificationState(),
+ mPrimaryCall.getVideoTech().getSessionModificationState(),
mPrimaryCall.isRemotelyHeld());
InCallPresenter.getInstance().getInCallCameraManager().onCameraPermissionGranted();
}
@@ -521,7 +490,7 @@ public class VideoCallPresenter
// change the camera or UI unless the waiting VT call becomes active.
primary = callList.getActiveCall();
currentCall = callList.getIncomingCall();
- if (!VideoUtils.isActiveVideoCall(primary)) {
+ if (!isActiveVideoCall(primary)) {
primary = callList.getIncomingCall();
}
} else if (newState == InCallPresenter.InCallState.OUTGOING) {
@@ -564,10 +533,10 @@ public class VideoCallPresenter
cancelAutoFullScreen();
if (mPrimaryCall != null) {
updateFullscreenAndGreenScreenMode(
- mPrimaryCall.getState(), mPrimaryCall.getSessionModificationState());
+ mPrimaryCall.getState(), mPrimaryCall.getVideoTech().getSessionModificationState());
} else {
updateFullscreenAndGreenScreenMode(
- State.INVALID, DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ State.INVALID, VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
}
}
@@ -622,7 +591,7 @@ public class VideoCallPresenter
updateCameraSelection(call);
String newCameraId = cameraManager.getActiveCameraId();
- if (!Objects.equals(prevCameraId, newCameraId) && VideoUtils.isActiveVideoCall(call)) {
+ if (!Objects.equals(prevCameraId, newCameraId) && isActiveVideoCall(call)) {
enableCamera(call.getVideoCall(), true);
}
}
@@ -631,7 +600,7 @@ public class VideoCallPresenter
showVideoUi(
call.getVideoState(),
call.getState(),
- call.getSessionModificationState(),
+ call.getVideoTech().getSessionModificationState(),
call.isRemotelyHeld());
}
@@ -711,12 +680,13 @@ public class VideoCallPresenter
checkForVideoStateChange(call);
checkForCallStateChange(call);
checkForOrientationAllowedChange(call);
- updateFullscreenAndGreenScreenMode(call.getState(), call.getSessionModificationState());
+ updateFullscreenAndGreenScreenMode(
+ call.getState(), call.getVideoTech().getSessionModificationState());
}
private void checkForOrientationAllowedChange(@Nullable DialerCall call) {
InCallPresenter.getInstance()
- .setInCallAllowsOrientationChange(VideoUtils.isVideoCall(call) || isVideoUpgrade(call));
+ .setInCallAllowsOrientationChange(isVideoCall(call) || isVideoUpgrade(call));
}
private void updateFullscreenAndGreenScreenMode(
@@ -775,7 +745,8 @@ public class VideoCallPresenter
private boolean isCameraRequired() {
return mPrimaryCall != null
&& isCameraRequired(
- mPrimaryCall.getVideoState(), mPrimaryCall.getSessionModificationState());
+ mPrimaryCall.getVideoState(),
+ mPrimaryCall.getVideoTech().getSessionModificationState());
}
/**
@@ -799,7 +770,10 @@ public class VideoCallPresenter
}
showVideoUi(
- newVideoState, call.getState(), call.getSessionModificationState(), call.isRemotelyHeld());
+ newVideoState,
+ call.getState(),
+ call.getVideoTech().getSessionModificationState(),
+ call.isRemotelyHeld());
// Communicate the current camera to telephony and make a request for the camera
// capabilities.
@@ -814,7 +788,9 @@ public class VideoCallPresenter
Assert.checkState(
mDeviceOrientation != InCallOrientationEventListener.SCREEN_ORIENTATION_UNKNOWN);
videoCall.setDeviceOrientation(mDeviceOrientation);
- enableCamera(videoCall, isCameraRequired(newVideoState, call.getSessionModificationState()));
+ enableCamera(
+ videoCall,
+ isCameraRequired(newVideoState, call.getVideoTech().getSessionModificationState()));
}
int previousVideoState = mCurrentVideoState;
mCurrentVideoState = newVideoState;
@@ -822,7 +798,7 @@ public class VideoCallPresenter
// adjustVideoMode may be called if we are already in a 1-way video state. In this case
// we do not want to trigger auto-fullscreen mode.
- if (!VideoUtils.isVideoCall(previousVideoState) && VideoUtils.isVideoCall(newVideoState)) {
+ if (!isVideoCall(previousVideoState) && isVideoCall(newVideoState)) {
maybeAutoEnterFullscreen(call);
}
}
@@ -832,7 +808,7 @@ public class VideoCallPresenter
return false;
}
- if (VideoUtils.isVideoCall(call)) {
+ if (isVideoCall(call)) {
return true;
}
@@ -877,7 +853,7 @@ public class VideoCallPresenter
showVideoUi(
VideoProfile.STATE_AUDIO_ONLY,
DialerCall.State.ACTIVE,
- DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST,
+ VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST,
false /* isRemotelyHeld */);
enableCamera(mVideoCall, false);
InCallPresenter.getInstance().setFullScreen(false);
@@ -918,20 +894,6 @@ public class VideoCallPresenter
}
/**
- * Handles peer video pause state changes.
- *
- * @param call The call which paused or un-pausedvideo transmission.
- * @param paused {@code True} when the video transmission is paused, {@code false} when video
- * transmission resumes.
- */
- @Override
- public void onPeerPauseStateChanged(DialerCall call, boolean paused) {
- if (!call.equals(mPrimaryCall)) {
- return;
- }
- }
-
- /**
* Handles peer video dimension changes.
*
* @param call The call which experienced a peer video dimension change.
@@ -959,17 +921,6 @@ public class VideoCallPresenter
}
/**
- * Handles any video quality changes in the call.
- *
- * @param call The call which experienced a video quality change.
- * @param videoQuality The new video call quality.
- */
- @Override
- public void onVideoQualityChanged(DialerCall call, int videoQuality) {
- // No-op
- }
-
- /**
* Handles a change to the dimensions of the local camera. Receiving the camera capabilities
* triggers the creation of the video
*
@@ -1024,42 +975,6 @@ public class VideoCallPresenter
}
/**
- * Called when call session event is raised.
- *
- * @param event The call session event.
- */
- @Override
- public void onCallSessionEvent(int event) {
- switch (event) {
- case Connection.VideoProvider.SESSION_EVENT_RX_PAUSE:
- LogUtil.v("VideoCallPresenter.onCallSessionEvent", "rx_pause");
- break;
- case Connection.VideoProvider.SESSION_EVENT_RX_RESUME:
- LogUtil.v("VideoCallPresenter.onCallSessionEvent", "rx_resume");
- break;
- case Connection.VideoProvider.SESSION_EVENT_CAMERA_FAILURE:
- LogUtil.v("VideoCallPresenter.onCallSessionEvent", "camera_failure");
- break;
- case Connection.VideoProvider.SESSION_EVENT_CAMERA_READY:
- LogUtil.v("VideoCallPresenter.onCallSessionEvent", "camera_ready");
- break;
- default:
- LogUtil.v("VideoCallPresenter.onCallSessionEvent", "unknown event = : " + event);
- break;
- }
- }
-
- /**
- * Handles a change to the call data usage
- *
- * @param dataUsage call data usage value
- */
- @Override
- public void onCallDataUsageChange(long dataUsage) {
- LogUtil.v("VideoCallPresenter.onCallDataUsageChange", "dataUsage=" + dataUsage);
- }
-
- /**
* Handles changes to the device orientation.
*
* @param orientation The screen orientation of the device (one of: {@link
@@ -1106,7 +1021,7 @@ public class VideoCallPresenter
return;
}
- if (!VideoUtils.isVideoCall(call) || call.getState() == DialerCall.State.INCOMING) {
+ if (!isVideoCall(call) || call.getState() == DialerCall.State.INCOMING) {
LogUtil.i("VideoCallPresenter.maybeExitFullscreen", "exiting fullscreen");
InCallPresenter.getInstance().setFullScreen(false);
}
@@ -1126,7 +1041,7 @@ public class VideoCallPresenter
if (call == null
|| call.getState() != DialerCall.State.ACTIVE
- || !VideoUtils.isBidirectionalVideoCall(call)
+ || !isBidirectionalVideoCall(call)
|| InCallPresenter.getInstance().isFullscreen()
|| (mContext != null && AccessibilityUtil.isTouchExplorationEnabled(mContext))) {
// Ensure any previously scheduled attempt to enter fullscreen is cancelled.
@@ -1156,6 +1071,32 @@ public class VideoCallPresenter
mHandler.removeCallbacks(mAutoFullscreenRunnable);
}
+ @Override
+ public boolean shouldShowCameraPermissionDialog() {
+ if (mPrimaryCall == null) {
+ LogUtil.i("VideoCallPresenter.shouldShowCameraPermissionDialog", "null call");
+ return false;
+ }
+ if (mPrimaryCall.didShowCameraPermission()) {
+ LogUtil.i(
+ "VideoCallPresenter.shouldShowCameraPermissionDialog", "already shown for this call");
+ return false;
+ }
+ if (!ConfigProviderBindings.get(mContext)
+ .getBoolean("camera_permission_dialog_allowed", true)) {
+ LogUtil.i("VideoCallPresenter.shouldShowCameraPermissionDialog", "disabled by config");
+ return false;
+ }
+ return !VideoUtils.hasCameraPermission(mContext) || !VideoUtils.isCameraAllowedByUser(mContext);
+ }
+
+ @Override
+ public void onCameraPermissionDialogShown() {
+ if (mPrimaryCall != null) {
+ mPrimaryCall.setDidShowCameraPermission(true);
+ }
+ }
+
private void updateRemoteVideoSurfaceDimensions() {
Activity activity = mVideoCallScreen.getVideoCallScreenFragment().getActivity();
if (activity != null) {
@@ -1166,8 +1107,8 @@ public class VideoCallPresenter
}
private static boolean isVideoUpgrade(DialerCall call) {
- return VideoUtils.hasSentVideoUpgradeRequest(call)
- || VideoUtils.hasReceivedVideoUpgradeRequest(call);
+ return call != null
+ && (call.hasSentVideoUpgradeRequest() || call.hasReceivedVideoUpgradeRequest());
}
private static boolean isVideoUpgrade(@SessionModificationState int state) {
@@ -1286,4 +1227,48 @@ public class VideoCallPresenter
/** The surface has been set on the {@link VideoCall}. */
private static final int SURFACE_SET = 3;
}
+
+ private static boolean isBidirectionalVideoCall(DialerCall call) {
+ return CompatUtils.isVideoCompatible() && VideoProfile.isBidirectional(call.getVideoState());
+ }
+
+ private static boolean isIncomingVideoCall(DialerCall call) {
+ if (!isVideoCall(call)) {
+ return false;
+ }
+ final int state = call.getState();
+ return (state == DialerCall.State.INCOMING) || (state == DialerCall.State.CALL_WAITING);
+ }
+
+ private static boolean isActiveVideoCall(DialerCall call) {
+ return isVideoCall(call) && call.getState() == DialerCall.State.ACTIVE;
+ }
+
+ private static boolean isOutgoingVideoCall(DialerCall call) {
+ if (!isVideoCall(call)) {
+ return false;
+ }
+ final int state = call.getState();
+ return DialerCall.State.isDialing(state)
+ || state == DialerCall.State.CONNECTING
+ || state == DialerCall.State.SELECT_PHONE_ACCOUNT;
+ }
+
+ private static boolean isAudioCall(DialerCall call) {
+ if (!CompatUtils.isVideoCompatible()) {
+ return true;
+ }
+
+ return call != null && VideoProfile.isAudioOnly(call.getVideoState());
+ }
+
+ private static boolean isVideoCall(@Nullable DialerCall call) {
+ return call != null && call.isVideoCall();
+ }
+
+ private static boolean isVideoCall(int videoState) {
+ return CompatUtils.isVideoCompatible()
+ && (VideoProfile.isTransmissionEnabled(videoState)
+ || VideoProfile.isReceptionEnabled(videoState));
+ }
}
diff --git a/java/com/android/incallui/VideoPauseController.java b/java/com/android/incallui/VideoPauseController.java
index 2b4357704..2595e2f8b 100644
--- a/java/com/android/incallui/VideoPauseController.java
+++ b/java/com/android/incallui/VideoPauseController.java
@@ -17,14 +17,14 @@
package com.android.incallui;
import android.support.annotation.NonNull;
-import android.telecom.VideoProfile;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
import com.android.incallui.InCallPresenter.InCallState;
import com.android.incallui.InCallPresenter.InCallStateListener;
import com.android.incallui.InCallPresenter.IncomingCallListener;
import com.android.incallui.call.CallList;
import com.android.incallui.call.DialerCall;
import com.android.incallui.call.DialerCall.State;
-import com.android.incallui.call.VideoUtils;
import java.util.Objects;
/**
@@ -32,12 +32,21 @@ import java.util.Objects;
* to the background and subsequently brought back to the foreground.
*/
class VideoPauseController implements InCallStateListener, IncomingCallListener {
-
- private static final String TAG = "VideoPauseController";
private static VideoPauseController sVideoPauseController;
private InCallPresenter mInCallPresenter;
- /** The current call context, if applicable. */
- private CallContext mPrimaryCallContext = null;
+
+ /** The current call, if applicable. */
+ private DialerCall mPrimaryCall = null;
+
+ /**
+ * The cached state of primary call, updated after onStateChange has processed.
+ *
+ * <p>These values are stored to detect specific changes in state between onStateChange calls.
+ */
+ private int mPrevCallState = State.INVALID;
+
+ private boolean mWasVideoCall = false;
+
/**
* Tracks whether the application is in the background. {@code True} if the application is in the
* background, {@code false} otherwise.
@@ -57,51 +66,9 @@ class VideoPauseController implements InCallStateListener, IncomingCallListener
return sVideoPauseController;
}
- /**
- * Determines if a given call is the same one stored in a {@link CallContext}.
- *
- * @param call The call.
- * @param callContext The call context.
- * @return {@code true} if the {@link DialerCall} is the same as the one referenced in the {@link
- * CallContext}.
- */
- private static boolean areSame(DialerCall call, CallContext callContext) {
- if (call == null && callContext == null) {
- return true;
- } else if (call == null || callContext == null) {
- return false;
- }
- return call.equals(callContext.getCall());
- }
-
- /**
- * Determines if a video call can be paused. Only a video call which is active can be paused.
- *
- * @param callContext The call context to check.
- * @return {@code true} if the call is an active video call.
- */
- private static boolean canVideoPause(CallContext callContext) {
- return isVideoCall(callContext) && callContext.getState() == DialerCall.State.ACTIVE;
- }
-
- /**
- * Determines if a call referenced by a {@link CallContext} is a video call.
- *
- * @param callContext The call context.
- * @return {@code true} if the call is a video call, {@code false} otherwise.
- */
- private static boolean isVideoCall(CallContext callContext) {
- return callContext != null && VideoUtils.isVideoCall(callContext.getVideoState());
- }
-
- /**
- * Determines if call is in incoming/waiting state.
- *
- * @param call The call context.
- * @return {@code true} if the call is in incoming or waiting state, {@code false} otherwise.
- */
- private static boolean isIncomingCall(CallContext call) {
- return call != null && isIncomingCall(call.getCall());
+ private boolean wasIncomingCall() {
+ return (mPrevCallState == DialerCall.State.CALL_WAITING
+ || mPrevCallState == DialerCall.State.INCOMING);
}
/**
@@ -119,11 +86,10 @@ class VideoPauseController implements InCallStateListener, IncomingCallListener
/**
* Determines if a call is dialing.
*
- * @param call The call context.
* @return {@code true} if the call is dialing, {@code false} otherwise.
*/
- private static boolean isDialing(CallContext call) {
- return call != null && DialerCall.State.isDialing(call.getState());
+ private boolean wasDialing() {
+ return DialerCall.State.isDialing(mPrevCallState);
}
/**
@@ -133,8 +99,8 @@ class VideoPauseController implements InCallStateListener, IncomingCallListener
* @param inCallPresenter The {@link com.android.incallui.InCallPresenter}.
*/
public void setUp(@NonNull InCallPresenter inCallPresenter) {
- log("setUp");
- mInCallPresenter = Objects.requireNonNull(inCallPresenter);
+ LogUtil.enterBlock("VideoPauseController.setUp");
+ mInCallPresenter = Assert.isNotNull(inCallPresenter);
mInCallPresenter.addListener(this);
mInCallPresenter.addIncomingCallListener(this);
}
@@ -144,7 +110,7 @@ class VideoPauseController implements InCallStateListener, IncomingCallListener
* state. Called from {@link com.android.incallui.InCallPresenter}.
*/
public void tearDown() {
- log("tearDown...");
+ LogUtil.enterBlock("VideoPauseController.tearDown");
mInCallPresenter.removeListener(this);
mInCallPresenter.removeIncomingCallListener(this);
clear();
@@ -153,7 +119,9 @@ class VideoPauseController implements InCallStateListener, IncomingCallListener
/** Clears the internal state for the {@link VideoPauseController}. */
private void clear() {
mInCallPresenter = null;
- mPrimaryCallContext = null;
+ mPrimaryCall = null;
+ mPrevCallState = State.INVALID;
+ mWasVideoCall = false;
mIsInBackground = false;
}
@@ -167,8 +135,6 @@ class VideoPauseController implements InCallStateListener, IncomingCallListener
*/
@Override
public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
- log("onStateChange, OldState=" + oldState + " NewState=" + newState);
-
DialerCall call;
if (newState == InCallState.INCOMING) {
call = callList.getIncomingCall();
@@ -182,22 +148,26 @@ class VideoPauseController implements InCallStateListener, IncomingCallListener
call = callList.getActiveCall();
}
- boolean hasPrimaryCallChanged = !areSame(call, mPrimaryCallContext);
- boolean canVideoPause = VideoUtils.canVideoPause(call);
- log("onStateChange, hasPrimaryCallChanged=" + hasPrimaryCallChanged);
- log("onStateChange, canVideoPause=" + canVideoPause);
- log("onStateChange, IsInBackground=" + mIsInBackground);
+ boolean hasPrimaryCallChanged = !Objects.equals(call, mPrimaryCall);
+ boolean canVideoPause = videoCanPause(call);
+
+ LogUtil.i(
+ "VideoPauseController.onStateChange",
+ "hasPrimaryCallChanged: %b, videoCanPause: %b, isInBackground: %b",
+ hasPrimaryCallChanged,
+ canVideoPause,
+ mIsInBackground);
if (hasPrimaryCallChanged) {
onPrimaryCallChanged(call);
return;
}
- if (isDialing(mPrimaryCallContext) && canVideoPause && mIsInBackground) {
+ if (wasDialing() && canVideoPause && mIsInBackground) {
// Bring UI to foreground if outgoing request becomes active while UI is in
// background.
bringToForeground();
- } else if (!isVideoCall(mPrimaryCallContext) && canVideoPause && mIsInBackground) {
+ } else if (!mWasVideoCall && canVideoPause && mIsInBackground) {
// Bring UI to foreground if VoLTE call becomes active while UI is in
// background.
bringToForeground();
@@ -216,27 +186,26 @@ class VideoPauseController implements InCallStateListener, IncomingCallListener
* @param call The new primary call.
*/
private void onPrimaryCallChanged(DialerCall call) {
- log("onPrimaryCallChanged: New call = " + call);
- log("onPrimaryCallChanged: Old call = " + mPrimaryCallContext);
- log("onPrimaryCallChanged, IsInBackground=" + mIsInBackground);
-
- if (areSame(call, mPrimaryCallContext)) {
+ LogUtil.i(
+ "VideoPauseController.onPrimaryCallChanged",
+ "new call: %s, old call: %s, mIsInBackground: %b",
+ call,
+ mPrimaryCall,
+ mIsInBackground);
+
+ if (Objects.equals(call, mPrimaryCall)) {
throw new IllegalStateException();
}
- final boolean canVideoPause = VideoUtils.canVideoPause(call);
+ final boolean canVideoPause = videoCanPause(call);
- if ((isIncomingCall(mPrimaryCallContext)
- || isDialing(mPrimaryCallContext)
- || (call != null && VideoProfile.isPaused(call.getVideoState())))
- && canVideoPause
- && !mIsInBackground) {
+ if ((wasIncomingCall() || wasDialing()) && canVideoPause && !mIsInBackground) {
// Send resume request for the active call, if user rejects incoming call, ends dialing
// call, or the call was previously in a paused state and UI is in the foreground.
sendRequest(call, true);
- } else if (isIncomingCall(call) && canVideoPause(mPrimaryCallContext)) {
+ } else if (isIncomingCall(call) && videoCanPause(mPrimaryCall)) {
// Send pause request if there is an active video call, and we just received a new
// incoming call.
- sendRequest(mPrimaryCallContext.getCall(), false);
+ sendRequest(mPrimaryCall, false);
}
updatePrimaryCallContext(call);
@@ -251,9 +220,14 @@ class VideoPauseController implements InCallStateListener, IncomingCallListener
*/
@Override
public void onIncomingCall(InCallState oldState, InCallState newState, DialerCall call) {
- log("onIncomingCall, OldState=" + oldState + " NewState=" + newState + " DialerCall=" + call);
-
- if (areSame(call, mPrimaryCallContext)) {
+ LogUtil.i(
+ "VideoPauseController.onIncomingCall",
+ "oldState: %s, newState: %s, call: %s",
+ oldState,
+ newState,
+ call);
+
+ if (Objects.equals(call, mPrimaryCall)) {
return;
}
@@ -267,11 +241,13 @@ class VideoPauseController implements InCallStateListener, IncomingCallListener
*/
private void updatePrimaryCallContext(DialerCall call) {
if (call == null) {
- mPrimaryCallContext = null;
- } else if (mPrimaryCallContext != null) {
- mPrimaryCallContext.update(call);
+ mPrimaryCall = null;
+ mPrevCallState = State.INVALID;
+ mWasVideoCall = false;
} else {
- mPrimaryCallContext = new CallContext(call);
+ mPrimaryCall = call;
+ mPrevCallState = call.getState();
+ mWasVideoCall = call.isVideoCall();
}
}
@@ -301,13 +277,9 @@ class VideoPauseController implements InCallStateListener, IncomingCallListener
* video provider if we are in a call.
*/
private void onResume(boolean isInCall) {
- log("onResume");
-
mIsInBackground = false;
- if (canVideoPause(mPrimaryCallContext) && isInCall) {
- sendRequest(mPrimaryCallContext.getCall(), true);
- } else {
- log("onResume. Ignoring...");
+ if (isInCall) {
+ sendRequest(mPrimaryCall, true);
}
}
@@ -319,22 +291,20 @@ class VideoPauseController implements InCallStateListener, IncomingCallListener
* video provider if we are in a call.
*/
private void onPause(boolean isInCall) {
- log("onPause");
-
mIsInBackground = true;
- if (canVideoPause(mPrimaryCallContext) && isInCall) {
- sendRequest(mPrimaryCallContext.getCall(), false);
- } else {
- log("onPause, Ignoring...");
+ if (isInCall) {
+ sendRequest(mPrimaryCall, false);
}
}
private void bringToForeground() {
+ LogUtil.enterBlock("VideoPauseController.bringToForeground");
if (mInCallPresenter != null) {
- log("Bringing UI to foreground");
mInCallPresenter.bringToForeground(false);
} else {
- loge("InCallPresenter is null. Cannot bring UI to foreground");
+ LogUtil.e(
+ "VideoPauseController.bringToForeground",
+ "InCallPresenter is null. Cannot bring UI to foreground");
}
}
@@ -345,72 +315,18 @@ class VideoPauseController implements InCallStateListener, IncomingCallListener
* @param resume If true resume request will be sent, otherwise pause request.
*/
private void sendRequest(DialerCall call, boolean resume) {
- // Check if this call supports pause/un-pause.
- if (!call.can(android.telecom.Call.Details.CAPABILITY_CAN_PAUSE_VIDEO)) {
+ if (call == null) {
return;
}
if (resume) {
- log("sending resume request, call=" + call);
- call.getVideoCall().sendSessionModifyRequest(VideoUtils.makeVideoUnPauseProfile(call));
+ call.getVideoTech().unpause();
} else {
- log("sending pause request, call=" + call);
- call.getVideoCall().sendSessionModifyRequest(VideoUtils.makeVideoPauseProfile(call));
+ call.getVideoTech().pause();
}
}
- /**
- * Logs a debug message.
- *
- * @param msg The message.
- */
- private void log(String msg) {
- Log.d(this, TAG + msg);
- }
-
- /**
- * Logs an error message.
- *
- * @param msg The message.
- */
- private void loge(String msg) {
- Log.e(this, TAG + msg);
- }
-
- /** Keeps track of the current active/foreground call. */
- private static class CallContext {
-
- private int mState = State.INVALID;
- private int mVideoState;
- private DialerCall mCall;
-
- public CallContext(@NonNull DialerCall call) {
- Objects.requireNonNull(call);
- update(call);
- }
-
- public void update(@NonNull DialerCall call) {
- mCall = Objects.requireNonNull(call);
- mState = call.getState();
- mVideoState = call.getVideoState();
- }
-
- public int getState() {
- return mState;
- }
-
- public int getVideoState() {
- return mVideoState;
- }
-
- @Override
- public String toString() {
- return String.format(
- "CallContext {CallId=%s, State=%s, VideoState=%d}", mCall.getId(), mState, mVideoState);
- }
-
- public DialerCall getCall() {
- return mCall;
- }
+ private static boolean videoCanPause(DialerCall call) {
+ return call != null && call.isVideoCall() && call.getState() == DialerCall.State.ACTIVE;
}
}
diff --git a/java/com/android/incallui/answer/bindings/AnswerBindings.java b/java/com/android/incallui/answer/bindings/AnswerBindings.java
index f7a7a0a95..442e207a0 100644
--- a/java/com/android/incallui/answer/bindings/AnswerBindings.java
+++ b/java/com/android/incallui/answer/bindings/AnswerBindings.java
@@ -23,7 +23,7 @@ import com.android.incallui.answer.protocol.AnswerScreen;
public class AnswerBindings {
public static AnswerScreen createAnswerScreen(
- String callId, int videoState, boolean isVideoUpgradeRequest) {
- return AnswerFragment.newInstance(callId, videoState, isVideoUpgradeRequest);
+ String callId, boolean isVideoCall, boolean isVideoUpgradeRequest) {
+ return AnswerFragment.newInstance(callId, isVideoCall, isVideoUpgradeRequest);
}
}
diff --git a/java/com/android/incallui/answer/impl/AnswerFragment.java b/java/com/android/incallui/answer/impl/AnswerFragment.java
index 98439ee7f..6874daea3 100644
--- a/java/com/android/incallui/answer/impl/AnswerFragment.java
+++ b/java/com/android/incallui/answer/impl/AnswerFragment.java
@@ -37,7 +37,6 @@ import android.support.annotation.StringRes;
import android.support.annotation.VisibleForTesting;
import android.support.transition.TransitionManager;
import android.support.v4.app.Fragment;
-import android.telecom.VideoProfile;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
@@ -79,10 +78,11 @@ import com.android.incallui.incall.protocol.InCallScreenDelegateFactory;
import com.android.incallui.incall.protocol.PrimaryCallState;
import com.android.incallui.incall.protocol.PrimaryInfo;
import com.android.incallui.incall.protocol.SecondaryInfo;
-import com.android.incallui.maps.StaticMapBinding;
+import com.android.incallui.maps.MapsComponent;
import com.android.incallui.sessiondata.AvatarPresenter;
import com.android.incallui.sessiondata.MultimediaFragment;
import com.android.incallui.util.AccessibilityUtil;
+import com.android.incallui.video.protocol.VideoCallScreen;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@@ -101,7 +101,7 @@ public class AnswerFragment extends Fragment
static final String ARG_CALL_ID = "call_id";
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- static final String ARG_VIDEO_STATE = "video_state";
+ static final String ARG_IS_VIDEO_CALL = "is_video_call";
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
static final String ARG_IS_VIDEO_UPGRADE_REQUEST = "is_video_upgrade_request";
@@ -143,7 +143,7 @@ public class AnswerFragment extends Fragment
private CreateCustomSmsDialogFragment createCustomSmsDialogFragment;
private SecondaryBehavior secondaryBehavior = SecondaryBehavior.REJECT_WITH_SMS;
private ContactGridManager contactGridManager;
- private AnswerVideoCallScreen answerVideoCallScreen;
+ private VideoCallScreen answerVideoCallScreen;
private Handler handler = new Handler(Looper.getMainLooper());
private enum SecondaryBehavior {
@@ -288,10 +288,10 @@ public class AnswerFragment extends Fragment
}
public static AnswerFragment newInstance(
- String callId, int videoState, boolean isVideoUpgradeRequest) {
+ String callId, boolean isVideoCall, boolean isVideoUpgradeRequest) {
Bundle bundle = new Bundle();
bundle.putString(ARG_CALL_ID, Assert.isNotNull(callId));
- bundle.putInt(ARG_VIDEO_STATE, videoState);
+ bundle.putBoolean(ARG_IS_VIDEO_CALL, isVideoCall);
bundle.putBoolean(ARG_IS_VIDEO_UPGRADE_REQUEST, isVideoUpgradeRequest);
AnswerFragment instance = new AnswerFragment();
@@ -306,18 +306,13 @@ public class AnswerFragment extends Fragment
}
@Override
- public int getVideoState() {
- return getArguments().getInt(ARG_VIDEO_STATE);
- }
-
- @Override
public boolean isVideoUpgradeRequest() {
return getArguments().getBoolean(ARG_IS_VIDEO_UPGRADE_REQUEST);
}
@Override
public void setTextResponses(List<String> textResponses) {
- if (isVideoCall()) {
+ if (isVideoCall() || isVideoUpgradeRequest()) {
LogUtil.i("AnswerFragment.setTextResponses", "no-op for video calls");
} else if (textResponses == null) {
LogUtil.i("AnswerFragment.setTextResponses", "no text responses, hiding secondary button");
@@ -336,7 +331,9 @@ public class AnswerFragment extends Fragment
private void initSecondaryButton() {
secondaryBehavior =
- isVideoCall() ? SecondaryBehavior.ANSWER_VIDEO_AS_AUDIO : SecondaryBehavior.REJECT_WITH_SMS;
+ isVideoCall() || isVideoUpgradeRequest()
+ ? SecondaryBehavior.ANSWER_VIDEO_AS_AUDIO
+ : SecondaryBehavior.REJECT_WITH_SMS;
secondaryBehavior.applyToView(secondaryButton);
secondaryButton.setOnClickListener(
@@ -351,12 +348,9 @@ public class AnswerFragment extends Fragment
secondaryButton.setAccessibilityDelegate(accessibilityDelegate);
if (isVideoCall()) {
- //noinspection WrongConstant
- if (!isVideoUpgradeRequest() && VideoProfile.isTransmissionEnabled(getVideoState())) {
- secondaryButton.setVisibility(View.VISIBLE);
- } else {
- secondaryButton.setVisibility(View.INVISIBLE);
- }
+ secondaryButton.setVisibility(View.VISIBLE);
+ } else {
+ secondaryButton.setVisibility(View.INVISIBLE);
}
}
@@ -448,11 +442,11 @@ public class AnswerFragment extends Fragment
MultimediaData multimediaData = getSessionData();
if (multimediaData != null
- && (!TextUtils.isEmpty(multimediaData.getSubject())
+ && (!TextUtils.isEmpty(multimediaData.getText())
|| (multimediaData.getImageUri() != null)
|| (multimediaData.getLocation() != null && canShowMap()))) {
// Need message fragment
- String subject = multimediaData.getSubject();
+ String subject = multimediaData.getText();
Uri imageUri = multimediaData.getImageUri();
Location location = multimediaData.getLocation();
if (!(current instanceof MultimediaFragment)
@@ -487,11 +481,11 @@ public class AnswerFragment extends Fragment
}
private boolean shouldShowAvatar() {
- return !isVideoCall();
+ return !isVideoCall() && !isVideoUpgradeRequest();
}
private boolean canShowMap() {
- return StaticMapBinding.get(getActivity().getApplication()) != null;
+ return MapsComponent.get(getContext()).getMaps().isAvailable();
}
@Override
@@ -564,7 +558,7 @@ public class AnswerFragment extends Fragment
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Bundle arguments = getArguments();
Assert.checkState(arguments.containsKey(ARG_CALL_ID));
- Assert.checkState(arguments.containsKey(ARG_VIDEO_STATE));
+ Assert.checkState(arguments.containsKey(ARG_IS_VIDEO_CALL));
Assert.checkState(arguments.containsKey(ARG_IS_VIDEO_UPGRADE_REQUEST));
buttonAcceptClicked = false;
@@ -596,7 +590,6 @@ public class AnswerFragment extends Fragment
});
updateImportanceBadgeVisibility();
- boolean isVideoCall = isVideoCall();
contactGridManager = new ContactGridManager(view, null, 0, false /* showAnonymousAvatar */);
Fragment answerMethod =
@@ -625,9 +618,9 @@ public class AnswerFragment extends Fragment
flags |= STATUS_BAR_DISABLE_BACK | STATUS_BAR_DISABLE_HOME | STATUS_BAR_DISABLE_RECENT;
}
view.setSystemUiVisibility(flags);
- if (isVideoCall) {
+ if (isVideoCall() || isVideoUpgradeRequest()) {
if (VideoUtils.hasCameraPermissionAndAllowedByUser(getContext())) {
- answerVideoCallScreen = new AnswerVideoCallScreen(this, view);
+ answerVideoCallScreen = new AnswerVideoCallScreen(getCallId(), this, view);
} else {
view.findViewById(R.id.videocall_video_off).setVisibility(View.VISIBLE);
}
@@ -649,7 +642,7 @@ public class AnswerFragment extends Fragment
updateUI();
if (savedInstanceState == null || !savedInstanceState.getBoolean(STATE_HAS_ANIMATED_ENTRY)) {
- ViewUtil.doOnPreDraw(view, false, this::animateEntry);
+ ViewUtil.doOnGlobalLayout(view, this::animateEntry);
}
}
@@ -667,7 +660,7 @@ public class AnswerFragment extends Fragment
updateUI();
if (answerVideoCallScreen != null) {
- answerVideoCallScreen.onStart();
+ answerVideoCallScreen.onVideoScreenStart();
}
}
@@ -678,7 +671,7 @@ public class AnswerFragment extends Fragment
handler.removeCallbacks(swipeHintRestoreTimer);
if (answerVideoCallScreen != null) {
- answerVideoCallScreen.onStop();
+ answerVideoCallScreen.onVideoScreenStop();
}
}
@@ -722,7 +715,7 @@ public class AnswerFragment extends Fragment
@Override
public boolean isVideoCall() {
- return VideoUtils.isVideoCall(getVideoState());
+ return getArguments().getBoolean(ARG_IS_VIDEO_CALL);
}
@Override
@@ -775,14 +768,12 @@ public class AnswerFragment extends Fragment
Animator dataContainer = createTranslation(rootView.findViewById(R.id.incall_data_container));
AnimatorSet animatorSet = new AnimatorSet();
- animatorSet
- .play(alpha)
- .with(topRow)
- .with(contactName)
- .with(bottomRow)
- .with(important)
- .with(dataContainer);
- animatorSet.setDuration(getResources().getInteger(android.R.integer.config_longAnimTime));
+ AnimatorSet.Builder builder = animatorSet.play(alpha);
+ builder.with(topRow).with(contactName).with(bottomRow).with(important).with(dataContainer);
+ if (isShowingLocationUi()) {
+ builder.with(createTranslation(rootView.findViewById(R.id.incall_location_holder)));
+ }
+ animatorSet.setDuration(getResources().getInteger(R.integer.answer_animate_entry_millis));
animatorSet.addListener(
new AnimatorListenerAdapter() {
@Override
@@ -803,14 +794,7 @@ public class AnswerFragment extends Fragment
private void acceptCallByUser(boolean answerVideoAsAudio) {
LogUtil.i("AnswerFragment.acceptCallByUser", answerVideoAsAudio ? " answerVideoAsAudio" : "");
if (!buttonAcceptClicked) {
- int desiredVideoState = getVideoState();
- if (answerVideoAsAudio) {
- desiredVideoState = VideoProfile.STATE_AUDIO_ONLY;
- }
-
- // Notify the lower layer first to start signaling ASAP.
- answerScreenDelegate.onAnswer(desiredVideoState);
-
+ answerScreenDelegate.onAnswer(answerVideoAsAudio);
buttonAcceptClicked = true;
}
}
diff --git a/java/com/android/incallui/answer/impl/AnswerVideoCallScreen.java b/java/com/android/incallui/answer/impl/AnswerVideoCallScreen.java
index 0316a5fab..06502daab 100644
--- a/java/com/android/incallui/answer/impl/AnswerVideoCallScreen.java
+++ b/java/com/android/incallui/answer/impl/AnswerVideoCallScreen.java
@@ -32,12 +32,15 @@ import com.android.incallui.videosurface.bindings.VideoSurfaceBindings;
/** Shows a video preview for an incoming call. */
public class AnswerVideoCallScreen implements VideoCallScreen {
+ @NonNull private final String callId;
@NonNull private final Fragment fragment;
@NonNull private final TextureView textureView;
@NonNull private final VideoCallScreenDelegate delegate;
- public AnswerVideoCallScreen(@NonNull Fragment fragment, @NonNull View view) {
- this.fragment = fragment;
+ public AnswerVideoCallScreen(
+ @NonNull String callId, @NonNull Fragment fragment, @NonNull View view) {
+ this.callId = Assert.isNotNull(callId);
+ this.fragment = Assert.isNotNull(fragment);
textureView =
Assert.isNotNull((TextureView) view.findViewById(R.id.incoming_preview_texture_view));
@@ -53,13 +56,15 @@ public class AnswerVideoCallScreen implements VideoCallScreen {
overlayView.setVisibility(View.VISIBLE);
}
- public void onStart() {
+ @Override
+ public void onVideoScreenStart() {
LogUtil.i("AnswerVideoCallScreen.onStart", null);
delegate.onVideoCallScreenUiReady();
delegate.getLocalVideoSurfaceTexture().attachToTextureView(textureView);
}
- public void onStop() {
+ @Override
+ public void onVideoScreenStop() {
LogUtil.i("AnswerVideoCallScreen.onStop", null);
delegate.onVideoCallScreenUiUnready();
}
@@ -98,6 +103,12 @@ public class AnswerVideoCallScreen implements VideoCallScreen {
return fragment;
}
+ @NonNull
+ @Override
+ public String getCallId() {
+ return callId;
+ }
+
private void updatePreviewVideoScaling() {
if (textureView.getWidth() == 0 || textureView.getHeight() == 0) {
LogUtil.i(
diff --git a/java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java b/java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java
index 62845b748..ff20d3a05 100644
--- a/java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java
+++ b/java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java
@@ -190,7 +190,7 @@ public class SwipeButtonHelper {
case MotionEvent.ACTION_UP:
isUp = true;
- //fallthrough_intended
+ // fall through
case MotionEvent.ACTION_CANCEL:
boolean hintOnTheRight = targetedView == rightIcon;
trackMovement(event);
diff --git a/java/com/android/incallui/answer/impl/answermethod/AnswerMethodHolder.java b/java/com/android/incallui/answer/impl/answermethod/AnswerMethodHolder.java
index 4052281b7..afa194f2e 100644
--- a/java/com/android/incallui/answer/impl/answermethod/AnswerMethodHolder.java
+++ b/java/com/android/incallui/answer/impl/answermethod/AnswerMethodHolder.java
@@ -44,4 +44,6 @@ public interface AnswerMethodHolder {
* @return true iff the current call is a video call.
*/
boolean isVideoCall();
+
+ boolean isVideoUpgradeRequest();
}
diff --git a/java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java b/java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java
index 0bc65818c..6e8e1f7bf 100644
--- a/java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java
+++ b/java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java
@@ -60,7 +60,7 @@ import com.android.incallui.answer.impl.answermethod.FlingUpDownTouchHandler.OnP
import com.android.incallui.answer.impl.classifier.FalsingManager;
import com.android.incallui.answer.impl.hint.AnswerHint;
import com.android.incallui.answer.impl.hint.AnswerHintFactory;
-import com.android.incallui.answer.impl.hint.EventPayloadLoaderImpl;
+import com.android.incallui.answer.impl.hint.PawImageLoaderImpl;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -228,7 +228,7 @@ public class FlingUpDownMethod extends AnswerMethod implements OnProgressChanged
touchHandler = FlingUpDownTouchHandler.attach(view, this, falsingManager);
answerHint =
- new AnswerHintFactory(new EventPayloadLoaderImpl())
+ new AnswerHintFactory(new PawImageLoaderImpl())
.create(getContext(), ANIMATE_DURATION_LONG_MILLIS, BOUNCE_ANIMATION_DELAY);
answerHint.onCreateView(
layoutInflater,
@@ -319,7 +319,7 @@ public class FlingUpDownMethod extends AnswerMethod implements OnProgressChanged
if (contactPuckIcon == null) {
return;
}
- if (getParent().isVideoCall()) {
+ if (getParent().isVideoCall() || getParent().isVideoUpgradeRequest()) {
contactPuckIcon.setImageResource(R.drawable.quantum_ic_videocam_white_24);
} else {
contactPuckIcon.setImageResource(R.drawable.quantum_ic_call_white_24);
@@ -348,7 +348,8 @@ public class FlingUpDownMethod extends AnswerMethod implements OnProgressChanged
}
private boolean shouldShowPhotoInPuck() {
- return getParent().isVideoCall() && contactPhoto != null;
+ return (getParent().isVideoCall() || getParent().isVideoUpgradeRequest())
+ && contactPhoto != null;
}
@Override
@@ -387,6 +388,10 @@ public class FlingUpDownMethod extends AnswerMethod implements OnProgressChanged
// Since the animation progression is controlled by user gesture instead of real timeline, the
// spec timeline can be divided into 9 slots. Each slot is equivalent to 83ms in the spec.
// Therefore, we use 9 slots of 83ms to map user gesture into the spec timeline.
+ //
+ // See specs -
+ // Accept: https://direct.googleplex.com/#/spec/8510001
+ // Decline: https://direct.googleplex.com/#/spec/3850001
final float progressSlots = 9;
// Fade out the "swipe up to answer". It only takes 1 slot to complete the fade.
@@ -414,7 +419,7 @@ public class FlingUpDownMethod extends AnswerMethod implements OnProgressChanged
contactPuckBackground.setColorFilter(destPuckColor);
// Animate decline icon
- if (isAcceptingFlow || getParent().isVideoCall()) {
+ if (isAcceptingFlow || getParent().isVideoCall() || getParent().isVideoUpgradeRequest()) {
rotateToward(contactPuckIcon, 0f);
} else {
rotateToward(contactPuckIcon, positiveAdjustedProgress * ICON_END_CALL_ROTATION_DEGREES);
diff --git a/java/com/android/incallui/answer/impl/hint/AndroidManifest.xml b/java/com/android/incallui/answer/impl/hint/AndroidManifest.xml
index b5fa6da8f..dfbba1cbf 100644
--- a/java/com/android/incallui/answer/impl/hint/AndroidManifest.xml
+++ b/java/com/android/incallui/answer/impl/hint/AndroidManifest.xml
@@ -3,7 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android">
<application>
- <receiver android:name=".EventSecretCodeListener">
+ <receiver android:name=".PawSecretCodeListener">
<intent-filter>
<action android:name="android.provider.Telephony.SECRET_CODE" />
<data android:scheme="android_secret_code" />
diff --git a/java/com/android/incallui/answer/impl/hint/AnswerHintFactory.java b/java/com/android/incallui/answer/impl/hint/AnswerHintFactory.java
index 45395a71f..eaf5b74e5 100644
--- a/java/com/android/incallui/answer/impl/hint/AnswerHintFactory.java
+++ b/java/com/android/incallui/answer/impl/hint/AnswerHintFactory.java
@@ -28,7 +28,6 @@ import com.android.dialer.common.ConfigProvider;
import com.android.dialer.common.ConfigProviderBindings;
import com.android.dialer.common.LogUtil;
import com.android.incallui.util.AccessibilityUtil;
-import java.util.Calendar;
/**
* Selects a AnswerHint to show. If there's no suitable hints {@link EmptyAnswerHint} will be used,
@@ -51,10 +50,10 @@ public class AnswerHintFactory {
@VisibleForTesting
static final String ANSWERED_COUNT_PREFERENCE_KEY = "answer_hint_answered_count";
- private final EventPayloadLoader eventPayloadLoader;
+ private final PawImageLoader pawImageLoader;
- public AnswerHintFactory(@NonNull EventPayloadLoader eventPayloadLoader) {
- this.eventPayloadLoader = Assert.isNotNull(eventPayloadLoader);
+ public AnswerHintFactory(@NonNull PawImageLoader pawImageLoader) {
+ this.pawImageLoader = Assert.isNotNull(pawImageLoader);
}
@NonNull
@@ -69,11 +68,9 @@ public class AnswerHintFactory {
}
// Display the event answer hint if the payload is available.
- Drawable eventPayload =
- eventPayloadLoader.loadPayload(
- context, System.currentTimeMillis(), Calendar.getInstance().getTimeZone());
+ Drawable eventPayload = pawImageLoader.loadPayload(context);
if (eventPayload != null) {
- return new EventAnswerHint(context, eventPayload, puckUpDuration, puckUpDelay);
+ return new PawAnswerHint(context, eventPayload, puckUpDuration, puckUpDelay);
}
return new EmptyAnswerHint();
diff --git a/java/com/android/incallui/answer/impl/hint/EventPayloadLoaderImpl.java b/java/com/android/incallui/answer/impl/hint/EventPayloadLoaderImpl.java
deleted file mode 100644
index bd8d73645..000000000
--- a/java/com/android/incallui/answer/impl/hint/EventPayloadLoaderImpl.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.incallui.answer.impl.hint;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.graphics.BitmapFactory;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.os.Build.VERSION_CODES;
-import android.preference.PreferenceManager;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
-import com.android.dialer.common.Assert;
-import com.android.dialer.common.ConfigProvider;
-import com.android.dialer.common.ConfigProviderBindings;
-import com.android.dialer.common.LogUtil;
-import java.io.InputStream;
-import java.util.TimeZone;
-import javax.crypto.Cipher;
-import javax.crypto.SecretKey;
-import javax.crypto.SecretKeyFactory;
-import javax.crypto.spec.PBEKeySpec;
-
-/** Decrypt the event payload to be shown if in a specific time range and the key is received. */
-@TargetApi(VERSION_CODES.M)
-public final class EventPayloadLoaderImpl implements EventPayloadLoader {
-
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- static final String CONFIG_EVENT_KEY = "event_key";
-
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- static final String CONFIG_EVENT_BINARY = "event_binary";
-
- // Time is stored as a UTC UNIX timestamp in milliseconds, but interpreted as local time.
- // For example, 946684800 (2000/1/1 00:00:00 @UTC) is the new year midnight at every timezone.
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- static final String CONFIG_EVENT_START_UTC_AS_LOCAL_MILLIS = "event_time_start_millis";
-
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- static final String CONFIG_EVENT_TIME_END_UTC_AS_LOCAL_MILLIS = "event_time_end_millis";
-
- @Override
- @Nullable
- public Drawable loadPayload(
- @NonNull Context context, long currentTimeUtcMillis, @NonNull TimeZone timeZone) {
- Assert.isNotNull(context);
- Assert.isNotNull(timeZone);
- ConfigProvider configProvider = ConfigProviderBindings.get(context);
-
- String pbeKey = configProvider.getString(CONFIG_EVENT_KEY, null);
- if (pbeKey == null) {
- return null;
- }
- long timeRangeStart = configProvider.getLong(CONFIG_EVENT_START_UTC_AS_LOCAL_MILLIS, 0);
- long timeRangeEnd = configProvider.getLong(CONFIG_EVENT_TIME_END_UTC_AS_LOCAL_MILLIS, 0);
-
- String eventBinary = configProvider.getString(CONFIG_EVENT_BINARY, null);
- if (eventBinary == null) {
- return null;
- }
-
- SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
- if (!preferences.getBoolean(
- EventSecretCodeListener.EVENT_ENABLED_WITH_SECRET_CODE_KEY, false)) {
- long localTimestamp = currentTimeUtcMillis + timeZone.getRawOffset();
-
- if (localTimestamp < timeRangeStart) {
- return null;
- }
-
- if (localTimestamp > timeRangeEnd) {
- return null;
- }
- }
-
- // Use openssl aes-128-cbc -in <input> -out <output> -pass <PBEKey> to generate the asset
- try (InputStream input = context.getAssets().open(eventBinary)) {
- byte[] encryptedFile = new byte[input.available()];
- input.read(encryptedFile);
-
- Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding", "BC");
-
- byte[] salt = new byte[8];
- System.arraycopy(encryptedFile, 8, salt, 0, 8);
- SecretKey key =
- SecretKeyFactory.getInstance("PBEWITHMD5AND128BITAES-CBC-OPENSSL", "BC")
- .generateSecret(new PBEKeySpec(pbeKey.toCharArray(), salt, 100));
- cipher.init(Cipher.DECRYPT_MODE, key);
-
- byte[] decryptedFile = cipher.doFinal(encryptedFile, 16, encryptedFile.length - 16);
-
- return new BitmapDrawable(
- context.getResources(),
- BitmapFactory.decodeByteArray(decryptedFile, 0, decryptedFile.length));
- } catch (Exception e) {
- // Avoid crashing dialer for any reason.
- LogUtil.e("EventPayloadLoader.loadPayload", "error decrypting payload:", e);
- return null;
- }
- }
-}
diff --git a/java/com/android/incallui/answer/impl/hint/EventAnswerHint.java b/java/com/android/incallui/answer/impl/hint/PawAnswerHint.java
index 7ee327d50..36b761f57 100644
--- a/java/com/android/incallui/answer/impl/hint/EventAnswerHint.java
+++ b/java/com/android/incallui/answer/impl/hint/PawAnswerHint.java
@@ -39,7 +39,7 @@ import com.android.dialer.common.Assert;
* An Answer hint that animates a {@link Drawable} payload with animation similar to {@link
* DotAnswerHint}.
*/
-public final class EventAnswerHint implements AnswerHint {
+public final class PawAnswerHint implements AnswerHint {
private static final long FADE_IN_DELAY_SCALE_MILLIS = 380;
private static final long FADE_IN_DURATION_SCALE_MILLIS = 200;
@@ -53,7 +53,8 @@ public final class EventAnswerHint implements AnswerHint {
private static final long FADE_OUT_DELAY_ALPHA_MILLIS = 130;
private static final long FADE_OUT_DURATION_ALPHA_MILLIS = 170;
- private static final float FADE_SCALE = 1.2f;
+ private static final float IMAGE_SCALE = 1.5f;
+ private static final float FADE_SCALE = 2.0f;
private final Context context;
private final Drawable payload;
@@ -65,7 +66,7 @@ public final class EventAnswerHint implements AnswerHint {
private View answerHintContainer;
private AnimatorSet answerGestureHintAnim;
- public EventAnswerHint(
+ public PawAnswerHint(
@NonNull Context context,
@NonNull Drawable payload,
long puckUpDurationMillis,
@@ -80,9 +81,9 @@ public final class EventAnswerHint implements AnswerHint {
public void onCreateView(
LayoutInflater inflater, ViewGroup container, View puck, TextView hintText) {
this.puck = puck;
- View view = inflater.inflate(R.layout.event_hint, container, true);
+ View view = inflater.inflate(R.layout.paw_hint, container, true);
answerHintContainer = view.findViewById(R.id.answer_hint_container);
- payloadView = view.findViewById(R.id.payload);
+ payloadView = view.findViewById(R.id.paw_image);
hintText.setTextSize(
TypedValue.COMPLEX_UNIT_PX, context.getResources().getDimension(R.dimen.hint_text_size));
((ImageView) payloadView).setImageDrawable(payload);
@@ -143,7 +144,7 @@ public final class EventAnswerHint implements AnswerHint {
createUniformScaleAnimator(
target,
FADE_SCALE,
- 1.0f,
+ IMAGE_SCALE,
FADE_IN_DURATION_SCALE_MILLIS,
FADE_IN_DELAY_SCALE_MILLIS,
new LinearInterpolator());
@@ -170,7 +171,7 @@ public final class EventAnswerHint implements AnswerHint {
Animator scale =
createUniformScaleAnimator(
target,
- 1.0f,
+ IMAGE_SCALE,
FADE_SCALE,
FADE_OUT_DURATION_SCALE_MILLIS,
scaleDelay,
@@ -178,7 +179,7 @@ public final class EventAnswerHint implements AnswerHint {
Animator alpha =
createAlphaAnimator(
target,
- 01.0f,
+ 1.0f,
0.0f,
FADE_OUT_DURATION_ALPHA_MILLIS,
FADE_OUT_DELAY_ALPHA_MILLIS,
diff --git a/java/com/android/incallui/answer/impl/hint/EventPayloadLoader.java b/java/com/android/incallui/answer/impl/hint/PawImageLoader.java
index 09e3bedf2..09e700fe0 100644
--- a/java/com/android/incallui/answer/impl/hint/EventPayloadLoader.java
+++ b/java/com/android/incallui/answer/impl/hint/PawImageLoader.java
@@ -20,11 +20,9 @@ import android.content.Context;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
-import java.util.TimeZone;
-/** Loads a {@link Drawable} payload for the {@link EventAnswerHint} if it should be displayed. */
-public interface EventPayloadLoader {
+/** Loads a {@link Drawable} payload for the {@link PawAnswerHint} if it should be displayed. */
+public interface PawImageLoader {
@Nullable
- Drawable loadPayload(
- @NonNull Context context, long currentTimeUtcMillis, @NonNull TimeZone timeZone);
+ Drawable loadPayload(@NonNull Context context);
}
diff --git a/java/com/android/incallui/answer/impl/hint/PawImageLoaderImpl.java b/java/com/android/incallui/answer/impl/hint/PawImageLoaderImpl.java
new file mode 100644
index 000000000..485a9ae37
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/PawImageLoaderImpl.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.hint;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.drawable.Drawable;
+import android.os.Build.VERSION_CODES;
+import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.dialer.common.Assert;
+
+/** Decrypt the event payload to be shown if in a specific time range and the key is received. */
+@TargetApi(VERSION_CODES.M)
+public final class PawImageLoaderImpl implements PawImageLoader {
+
+ @Override
+ @Nullable
+ public Drawable loadPayload(@NonNull Context context) {
+ Assert.isNotNull(context);
+
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ if (!preferences.getBoolean(PawSecretCodeListener.PAW_ENABLED_WITH_SECRET_CODE_KEY, false)) {
+ return null;
+ }
+ int drawableId = preferences.getInt(PawSecretCodeListener.PAW_DRAWABLE_ID_KEY, 0);
+ if (drawableId == 0) {
+ return null;
+ }
+ return context.getDrawable(drawableId);
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/hint/EventSecretCodeListener.java b/java/com/android/incallui/answer/impl/hint/PawSecretCodeListener.java
index 7cf4054a9..b4fc19c0d 100644
--- a/java/com/android/incallui/answer/impl/hint/EventSecretCodeListener.java
+++ b/java/com/android/incallui/answer/impl/hint/PawSecretCodeListener.java
@@ -24,26 +24,30 @@ import android.preference.PreferenceManager;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.widget.Toast;
+import com.android.dialer.common.Assert;
import com.android.dialer.common.ConfigProviderBindings;
import com.android.dialer.common.LogUtil;
import com.android.dialer.logging.Logger;
-import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.logging.nano.DialerImpression.Type;
+import java.util.Random;
/**
* Listen to the broadcast when the user dials "*#*#[number]#*#*" to toggle the event answer hint.
*/
-public class EventSecretCodeListener extends BroadcastReceiver {
+public class PawSecretCodeListener extends BroadcastReceiver {
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- static final String CONFIG_EVENT_SECRET_CODE = "event_secret_code";
+ static final String CONFIG_PAW_SECRET_CODE = "paw_secret_code";
- public static final String EVENT_ENABLED_WITH_SECRET_CODE_KEY = "event_enabled_with_secret_code";
+ public static final String PAW_ENABLED_WITH_SECRET_CODE_KEY = "paw_enabled_with_secret_code";
+ public static final String PAW_DRAWABLE_ID_KEY = "paw_drawable_id";
@Override
public void onReceive(Context context, Intent intent) {
String host = intent.getData().getHost();
+ Assert.checkState(!TextUtils.isEmpty(host));
String secretCode =
- ConfigProviderBindings.get(context).getString(CONFIG_EVENT_SECRET_CODE, null);
+ ConfigProviderBindings.get(context).getString(CONFIG_PAW_SECRET_CODE, "729");
if (secretCode == null) {
return;
}
@@ -51,17 +55,27 @@ public class EventSecretCodeListener extends BroadcastReceiver {
return;
}
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
- boolean wasEnabled = preferences.getBoolean(EVENT_ENABLED_WITH_SECRET_CODE_KEY, false);
+ boolean wasEnabled = preferences.getBoolean(PAW_ENABLED_WITH_SECRET_CODE_KEY, false);
if (wasEnabled) {
- preferences.edit().putBoolean(EVENT_ENABLED_WITH_SECRET_CODE_KEY, false).apply();
+ preferences.edit().putBoolean(PAW_ENABLED_WITH_SECRET_CODE_KEY, false).apply();
Toast.makeText(context, R.string.event_deactivated, Toast.LENGTH_SHORT).show();
- Logger.get(context).logImpression(DialerImpression.Type.EVENT_ANSWER_HINT_DEACTIVATED);
- LogUtil.i("EventSecretCodeListener.onReceive", "EventAnswerHint disabled");
+ Logger.get(context).logImpression(Type.EVENT_ANSWER_HINT_DEACTIVATED);
+ LogUtil.i("PawSecretCodeListener.onReceive", "PawAnswerHint disabled");
} else {
- preferences.edit().putBoolean(EVENT_ENABLED_WITH_SECRET_CODE_KEY, true).apply();
+ int drawableId;
+ if (new Random().nextBoolean()) {
+ drawableId = R.drawable.cat_paw;
+ } else {
+ drawableId = R.drawable.dog_paw;
+ }
+ preferences
+ .edit()
+ .putBoolean(PAW_ENABLED_WITH_SECRET_CODE_KEY, true)
+ .putInt(PAW_DRAWABLE_ID_KEY, drawableId)
+ .apply();
Toast.makeText(context, R.string.event_activated, Toast.LENGTH_SHORT).show();
- Logger.get(context).logImpression(DialerImpression.Type.EVENT_ANSWER_HINT_ACTIVATED);
- LogUtil.i("EventSecretCodeListener.onReceive", "EventAnswerHint enabled");
+ Logger.get(context).logImpression(Type.EVENT_ANSWER_HINT_ACTIVATED);
+ LogUtil.i("PawSecretCodeListener.onReceive", "PawAnswerHint enabled");
}
}
}
diff --git a/java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/cat_paw.webp b/java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/cat_paw.webp
new file mode 100644
index 000000000..f7ff6eb54
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/cat_paw.webp
Binary files differ
diff --git a/java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/dog_paw.webp b/java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/dog_paw.webp
new file mode 100644
index 000000000..3a232542c
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/dog_paw.webp
Binary files differ
diff --git a/java/com/android/incallui/answer/impl/hint/res/layout/event_hint.xml b/java/com/android/incallui/answer/impl/hint/res/layout/paw_hint.xml
index d505014c1..c3b12a01d 100644
--- a/java/com/android/incallui/answer/impl/hint/res/layout/event_hint.xml
+++ b/java/com/android/incallui/answer/impl/hint/res/layout/paw_hint.xml
@@ -24,9 +24,10 @@
android:clipToPadding="false"
android:visibility="gone">
<ImageView
- android:id="@+id/payload"
- android:layout_width="191dp"
- android:layout_height="773dp"
+ android:id="@+id/paw_image"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@drawable/cat_paw"
android:layout_gravity="center"
android:alpha="0"
android:rotation="-30"
diff --git a/java/com/android/incallui/answer/impl/proguard.flags b/java/com/android/incallui/answer/impl/proguard.flags
new file mode 100644
index 000000000..016352857
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/proguard.flags
@@ -0,0 +1,5 @@
+# Used in com.android.dialer.answer.impl.SmsBottomSheetFragment
+-keep class android.support.design.widget.BottomSheetBehavior {
+ public <init>(android.content.Context, android.util.AttributeSet);
+ public <init>();
+} \ No newline at end of file
diff --git a/java/com/android/incallui/answer/impl/res/values/dimens.xml b/java/com/android/incallui/answer/impl/res/values/dimens.xml
index c48b68f93..8329707a6 100644
--- a/java/com/android/incallui/answer/impl/res/values/dimens.xml
+++ b/java/com/android/incallui/answer/impl/res/values/dimens.xml
@@ -22,4 +22,5 @@
<dimen name="answer_avatar_size">0dp</dimen>
<dimen name="answer_importance_margin_bottom">0dp</dimen>
<bool name="answer_important_call_allowed">false</bool>
+ <integer name="answer_animate_entry_millis">1000</integer>
</resources>
diff --git a/java/com/android/incallui/answer/protocol/AnswerScreen.java b/java/com/android/incallui/answer/protocol/AnswerScreen.java
index 0c374eb7f..f03efefc4 100644
--- a/java/com/android/incallui/answer/protocol/AnswerScreen.java
+++ b/java/com/android/incallui/answer/protocol/AnswerScreen.java
@@ -24,7 +24,7 @@ public interface AnswerScreen {
String getCallId();
- int getVideoState();
+ boolean isVideoCall();
boolean isVideoUpgradeRequest();
diff --git a/java/com/android/incallui/answer/protocol/AnswerScreenDelegate.java b/java/com/android/incallui/answer/protocol/AnswerScreenDelegate.java
index 9934497cf..36b4e3a6b 100644
--- a/java/com/android/incallui/answer/protocol/AnswerScreenDelegate.java
+++ b/java/com/android/incallui/answer/protocol/AnswerScreenDelegate.java
@@ -27,7 +27,7 @@ public interface AnswerScreenDelegate {
void onRejectCallWithMessage(String message);
- void onAnswer(int videoState);
+ void onAnswer(boolean answerVideoAsAudio);
void onReject();
diff --git a/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java b/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java
index edc3db34b..6a2c4b493 100644
--- a/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java
+++ b/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java
@@ -23,7 +23,6 @@ import android.view.Display;
import com.android.dialer.common.ConfigProviderBindings;
import com.android.dialer.common.LogUtil;
import com.android.incallui.call.DialerCall;
-import com.android.incallui.call.DialerCall.SessionModificationState;
import com.android.incallui.call.DialerCall.State;
import com.android.incallui.call.DialerCallListener;
@@ -141,7 +140,7 @@ public class AnswerProximitySensor
public void onHandoverToWifiFailure() {}
@Override
- public void onDialerCallSessionModificationStateChange(@SessionModificationState int state) {}
+ public void onDialerCallSessionModificationStateChange() {}
@Override
public void onScreenOn() {
diff --git a/java/com/android/incallui/call/CallList.java b/java/com/android/incallui/call/CallList.java
index 862c71cf9..c88802f14 100644
--- a/java/com/android/incallui/call/CallList.java
+++ b/java/com/android/incallui/call/CallList.java
@@ -38,10 +38,10 @@ import com.android.dialer.logging.nano.DialerImpression;
import com.android.dialer.shortcuts.ShortcutUsageReporter;
import com.android.dialer.spam.Spam;
import com.android.dialer.spam.SpamBindings;
-import com.android.incallui.call.DialerCall.SessionModificationState;
import com.android.incallui.call.DialerCall.State;
import com.android.incallui.latencyreport.LatencyReport;
import com.android.incallui.util.TelecomCallUtil;
+import com.android.incallui.videotech.VideoTech;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
@@ -110,6 +110,8 @@ public class CallList implements DialerCallDelegate {
Trace.beginSection("onCallAdded");
final DialerCall call =
new DialerCall(context, this, telecomCall, latencyReport, true /* registerCallback */);
+ logSecondIncomingCall(context, call);
+
final DialerCallListenerImpl dialerCallListener = new DialerCallListenerImpl(call);
call.addListener(dialerCallListener);
LogUtil.d("CallList.onCallAdded", "callState=" + call.getState());
@@ -184,6 +186,30 @@ public class CallList implements DialerCallDelegate {
Trace.endSection();
}
+ private void logSecondIncomingCall(@NonNull Context context, @NonNull DialerCall incomingCall) {
+ DialerCall firstCall = getFirstCall();
+ if (firstCall != null) {
+ int impression = 0;
+ if (firstCall.isVideoCall()) {
+ if (incomingCall.isVideoCall()) {
+ impression = DialerImpression.Type.VIDEO_CALL_WITH_INCOMING_VIDEO_CALL;
+ } else {
+ impression = DialerImpression.Type.VIDEO_CALL_WITH_INCOMING_VOICE_CALL;
+ }
+ } else {
+ if (incomingCall.isVideoCall()) {
+ impression = DialerImpression.Type.VOICE_CALL_WITH_INCOMING_VIDEO_CALL;
+ } else {
+ impression = DialerImpression.Type.VOICE_CALL_WITH_INCOMING_VOICE_CALL;
+ }
+ }
+ Assert.checkArgument(impression != 0);
+ Logger.get(context)
+ .logCallImpression(
+ impression, incomingCall.getUniqueCallId(), incomingCall.getTimeAddedMs());
+ }
+ }
+
private static boolean isPotentialEmergencyCallback(Context context, DialerCall call) {
if (BuildCompat.isAtLeastO()) {
return call.isPotentialEmergencyCallback();
@@ -440,8 +466,8 @@ public class CallList implements DialerCallDelegate {
*/
public DialerCall getVideoUpgradeRequestCall() {
for (DialerCall call : mCallById.values()) {
- if (call.getSessionModificationState()
- == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+ if (call.getVideoTech().getSessionModificationState()
+ == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
return call;
}
}
@@ -637,17 +663,7 @@ public class CallList implements DialerCallDelegate {
*/
public void notifyCallsOfDeviceRotation(int rotation) {
for (DialerCall call : mCallById.values()) {
- // First, ensure that the call videoState has video enabled (there is no need to set
- // device orientation on a voice call which has not yet been upgraded to video).
- // Second, ensure a VideoCall is set on the call so that the change can be sent to the
- // provider (a VideoCall can be present for a call that does not currently have video,
- // but can be upgraded to video).
-
- // NOTE: is it necessary to use this order because getVideoCall references the class
- // VideoProfile which is not available on APIs <23 (M).
- if (VideoUtils.isVideoCall(call) && call.getVideoCall() != null) {
- call.getVideoCall().setDeviceOrientation(rotation);
- }
+ call.getVideoTech().setDeviceOrientation(rotation);
}
}
@@ -675,7 +691,7 @@ public class CallList implements DialerCallDelegate {
void onUpgradeToVideo(DialerCall call);
/** Called when the session modification state of a call changes. */
- void onSessionModificationStateChange(@SessionModificationState int newState);
+ void onSessionModificationStateChange(DialerCall call);
/**
* Called anytime there are changes to the call list. The change can be switching call states,
@@ -754,9 +770,9 @@ public class CallList implements DialerCallDelegate {
}
@Override
- public void onDialerCallSessionModificationStateChange(@SessionModificationState int state) {
+ public void onDialerCallSessionModificationStateChange() {
for (Listener listener : mListeners) {
- listener.onSessionModificationStateChange(state);
+ listener.onSessionModificationStateChange(mCall);
}
}
}
diff --git a/java/com/android/incallui/call/DialerCall.java b/java/com/android/incallui/call/DialerCall.java
index bd8f006dd..15a0233e8 100644
--- a/java/com/android/incallui/call/DialerCall.java
+++ b/java/com/android/incallui/call/DialerCall.java
@@ -24,6 +24,7 @@ import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Trace;
import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.telecom.Call;
import android.telecom.Call.Details;
@@ -47,10 +48,15 @@ import com.android.dialer.callintent.nano.CallSpecificAppData;
import com.android.dialer.common.Assert;
import com.android.dialer.common.ConfigProviderBindings;
import com.android.dialer.common.LogUtil;
+import com.android.dialer.enrichedcall.EnrichedCallComponent;
import com.android.dialer.logging.nano.ContactLookupResult;
-import com.android.dialer.util.CallUtil;
import com.android.incallui.latencyreport.LatencyReport;
import com.android.incallui.util.TelecomCallUtil;
+import com.android.incallui.videotech.VideoTech;
+import com.android.incallui.videotech.VideoTech.VideoTechListener;
+import com.android.incallui.videotech.empty.EmptyVideoTech;
+import com.android.incallui.videotech.ims.ImsVideoTech;
+import com.android.incallui.videotech.rcs.RcsVideoShare;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
@@ -62,11 +68,16 @@ import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
/** Describes a single call and its state. */
-public class DialerCall {
+public class DialerCall implements VideoTechListener {
public static final int CALL_HISTORY_STATUS_UNKNOWN = 0;
public static final int CALL_HISTORY_STATUS_PRESENT = 1;
public static final int CALL_HISTORY_STATUS_NOT_PRESENT = 2;
+
+ // Hard coded property for {@code Call}. Upstreamed change from Motorola.
+ // TODO(b/35359461): Move it to Telecom in framework.
+ public static final int PROPERTY_CODEC_KNOWN = 0x04000000;
+
private static final String ID_PREFIX = "DialerCall_";
private static final String CONFIG_EMERGENCY_CALLBACK_WINDOW_MILLIS =
"emergency_callback_window_millis";
@@ -82,13 +93,13 @@ public class DialerCall {
private final LatencyReport mLatencyReport;
private final String mId;
private final List<String> mChildCallIds = new ArrayList<>();
- private final VideoSettings mVideoSettings = new VideoSettings();
private final LogState mLogState = new LogState();
private final Context mContext;
private final DialerCallDelegate mDialerCallDelegate;
private final List<DialerCallListener> mListeners = new CopyOnWriteArrayList<>();
private final List<CannedTextResponsesLoadedListener> mCannedTextResponsesLoadedListeners =
new CopyOnWriteArrayList<>();
+ private final VideoTechManager mVideoTechManager;
private boolean mIsEmergencyCall;
private Uri mHandle;
@@ -98,13 +109,6 @@ public class DialerCall {
private boolean hasShownWiFiToLteHandoverToast;
private boolean doNotShowDialogForHandoffToWifiFailure;
- @SessionModificationState private int mSessionModificationState;
- private int mVideoState;
- /** mRequestedVideoState is used to store requested upgrade / downgrade video state */
- private int mRequestedVideoState = VideoProfile.STATE_AUDIO_ONLY;
-
- private InCallVideoCallCallback mVideoCallCallback;
- private boolean mIsVideoCallCallbackRegistered;
private String mChildNumber;
private String mLastForwardedNumber;
private String mCallSubject;
@@ -118,6 +122,7 @@ public class DialerCall {
private boolean didShowCameraPermission;
private String callProviderLabel;
private String callbackNumber;
+ private int mCameraDirection = CameraDirection.CAMERA_DIRECTION_UNKNOWN;
public static String getNumberFromHandle(Uri handle) {
return handle == null ? "" : handle.getSchemeSpecificPart();
@@ -125,7 +130,7 @@ public class DialerCall {
/**
* Whether the call is put on hold by remote party. This is different than the {@link
- * State.ONHOLD} state which indicates that the call is being held locally on the device.
+ * State#ONHOLD} state which indicates that the call is being held locally on the device.
*/
private boolean isRemotelyHeld;
@@ -189,7 +194,7 @@ public class DialerCall {
@Override
public void onCallDestroyed(Call call) {
LogUtil.v("TelecomCallCallback.onStateChanged", "call=" + call);
- call.unregisterCallback(this);
+ unregisterCallback();
}
@Override
@@ -248,7 +253,10 @@ public class DialerCall {
mLatencyReport = latencyReport;
mId = ID_PREFIX + Integer.toString(sIdCounter++);
- updateFromTelecomCall(registerCallback);
+ // Must be after assigning mTelecomCall
+ mVideoTechManager = new VideoTechManager(this);
+
+ updateFromTelecomCall();
if (registerCallback) {
mTelecomCall.registerCallback(mTelecomCallCallback);
@@ -348,19 +356,24 @@ public class DialerCall {
return mTelecomCall.getDetails().getStatusHints();
}
- /**
- * @return video settings of the call, null if the call is not a video call.
- * @see VideoProfile
- */
- public VideoSettings getVideoSettings() {
- return mVideoSettings;
+ public int getCameraDir() {
+ return mCameraDirection;
+ }
+
+ public void setCameraDir(int cameraDir) {
+ if (cameraDir == CameraDirection.CAMERA_DIRECTION_FRONT_FACING
+ || cameraDir == CameraDirection.CAMERA_DIRECTION_BACK_FACING) {
+ mCameraDirection = cameraDir;
+ } else {
+ mCameraDirection = CameraDirection.CAMERA_DIRECTION_UNKNOWN;
+ }
}
private void update() {
Trace.beginSection("Update");
int oldState = getState();
// We want to potentially register a video call callback here.
- updateFromTelecomCall(true /* registerCallback */);
+ updateFromTelecomCall();
if (oldState != getState() && getState() == DialerCall.State.DISCONNECTED) {
for (DialerCallListener listener : mListeners) {
listener.onDialerCallDisconnect();
@@ -373,21 +386,15 @@ public class DialerCall {
Trace.endSection();
}
- private void updateFromTelecomCall(boolean registerCallback) {
+ private void updateFromTelecomCall() {
LogUtil.v("DialerCall.updateFromTelecomCall", mTelecomCall.toString());
+
+ mVideoTechManager.dispatchCallStateChanged(mTelecomCall.getState());
+
final int translatedState = translateState(mTelecomCall.getState());
if (mState != State.BLOCKED) {
setState(translatedState);
setDisconnectCause(mTelecomCall.getDetails().getDisconnectCause());
- maybeCancelVideoUpgrade(mTelecomCall.getDetails().getVideoState());
- }
-
- if (registerCallback && mTelecomCall.getVideoCall() != null) {
- if (mVideoCallCallback == null) {
- mVideoCallCallback = new InCallVideoCallCallback(this);
- }
- mTelecomCall.getVideoCall().registerCallback(mVideoCallCallback);
- mIsVideoCallCallbackRegistered = true;
}
mChildCallIds.clear();
@@ -428,19 +435,6 @@ public class DialerCall {
}
}
}
-
- if (mSessionModificationState
- == DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE
- && isVideoCall()) {
- // We find out in {@link InCallVideoCallCallback.onSessionModifyResponseReceived}
- // whether the video upgrade request was accepted. We don't clear the session modification
- // state right away though to avoid having the UI switch from video to voice to video.
- // Once the underlying telecom call updates to video mode it's safe to clear the state.
- LogUtil.i(
- "DialerCall.updateFromTelecomCall",
- "upgraded to video, clearing session modification state");
- setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
- }
}
/**
@@ -518,25 +512,6 @@ public class DialerCall {
}
}
- /**
- * Determines if a received upgrade to video request should be cancelled. This can happen if
- * another InCall UI responds to the upgrade to video request.
- *
- * @param newVideoState The new video state.
- */
- private void maybeCancelVideoUpgrade(int newVideoState) {
- boolean isVideoStateChanged = mVideoState != newVideoState;
-
- if (mSessionModificationState
- == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST
- && isVideoStateChanged) {
-
- LogUtil.i("DialerCall.maybeCancelVideoUpgrade", "cancelling upgrade notification");
- setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
- }
- mVideoState = newVideoState;
- }
-
public String getId() {
return mId;
}
@@ -710,6 +685,7 @@ public class DialerCall {
return mTelecomCall.getDetails().hasProperty(property);
}
+ @NonNull
public String getUniqueCallId() {
return uniqueCallId;
}
@@ -733,15 +709,9 @@ public class DialerCall {
return mTelecomCall == null ? null : mTelecomCall.getDetails().getAccountHandle();
}
- /**
- * @return The {@link VideoCall} instance associated with the {@link Call}. Will return {@code
- * null} until {@link #updateFromTelecomCall(boolean)} has registered a valid callback on the
- * {@link VideoCall}.
- */
+ /** @return The {@link VideoCall} instance associated with the {@link Call}. */
public VideoCall getVideoCall() {
- return mTelecomCall == null || !mIsVideoCallCallbackRegistered
- ? null
- : mTelecomCall.getVideoCall();
+ return mTelecomCall == null ? null : mTelecomCall.getVideoCall();
}
public List<String> getChildCallIds() {
@@ -761,85 +731,23 @@ public class DialerCall {
}
public boolean isVideoCall() {
- return CallUtil.isVideoEnabled(mContext) && VideoUtils.isVideoCall(getVideoState());
+ return getVideoTech().isTransmittingOrReceiving();
}
- /**
- * Determines if the call handle is an emergency number or not and caches the result to avoid
- * repeated calls to isEmergencyNumber.
- */
- private void updateEmergencyCallState() {
- mIsEmergencyCall = TelecomCallUtil.isEmergencyCall(mTelecomCall);
+ public boolean hasReceivedVideoUpgradeRequest() {
+ return VideoUtils.hasReceivedVideoUpgradeRequest(getVideoTech().getSessionModificationState());
}
- /**
- * Gets the video state which was requested via a session modification request.
- *
- * @return The video state.
- */
- public int getRequestedVideoState() {
- return mRequestedVideoState;
+ public boolean hasSentVideoUpgradeRequest() {
+ return VideoUtils.hasSentVideoUpgradeRequest(getVideoTech().getSessionModificationState());
}
/**
- * Handles incoming session modification requests. Stores the pending video request and sets the
- * session modification state to {@link
- * DialerCall#SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST} so that we can keep
- * track of the fact the request was received. Only upgrade requests require user confirmation and
- * will be handled by this method. The remote user can turn off their own camera without
- * confirmation.
- *
- * @param videoState The requested video state.
- */
- public void setRequestedVideoState(int videoState) {
- LogUtil.v("DialerCall.setRequestedVideoState", "videoState: " + videoState);
- if (videoState == getVideoState()) {
- LogUtil.e("DialerCall.setRequestedVideoState", "clearing session modification state");
- setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
- return;
- }
-
- mRequestedVideoState = videoState;
- setSessionModificationState(
- DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST);
- for (DialerCallListener listener : mListeners) {
- listener.onDialerCallUpgradeToVideo();
- }
-
- LogUtil.i(
- "DialerCall.setRequestedVideoState",
- "mSessionModificationState: %d, videoState: %d",
- mSessionModificationState,
- videoState);
- update();
- }
-
- /**
- * Gets the current video session modification state.
- *
- * @return The session modification state.
- */
- @SessionModificationState
- public int getSessionModificationState() {
- return mSessionModificationState;
- }
-
- /**
- * Set the session modification state. Used to keep track of pending video session modification
- * operations and to inform listeners of these changes.
- *
- * @param state the new session modification state.
+ * Determines if the call handle is an emergency number or not and caches the result to avoid
+ * repeated calls to isEmergencyNumber.
*/
- public void setSessionModificationState(@SessionModificationState int state) {
- boolean hasChanged = mSessionModificationState != state;
- if (hasChanged) {
- LogUtil.i(
- "DialerCall.setSessionModificationState", "%d -> %d", mSessionModificationState, state);
- mSessionModificationState = state;
- for (DialerCallListener listener : mListeners) {
- listener.onDialerCallSessionModificationStateChange(state);
- }
- }
+ private void updateEmergencyCallState() {
+ mIsEmergencyCall = TelecomCallUtil.isEmergencyCall(mTelecomCall);
}
public LogState getLogState() {
@@ -862,24 +770,6 @@ public class DialerCall {
}
/**
- * Determines if the external call is pullable.
- *
- * <p>An external call is one which does not exist locally for the {@link
- * android.telecom.ConnectionService} it is associated with. An external call may be "pullable",
- * which means that the user can request it be transferred to the current device.
- *
- * <p>External calls are only supported in N and higher.
- *
- * @return {@code true} if the call is an external call, {@code false} otherwise.
- */
- public boolean isPullableExternalCall() {
- return VERSION.SDK_INT >= VERSION_CODES.N
- && (mTelecomCall.getDetails().getCallCapabilities()
- & CallCompat.Details.CAPABILITY_CAN_PULL_CALL)
- == CallCompat.Details.CAPABILITY_CAN_PULL_CALL;
- }
-
- /**
* Determines if answering this call will cause an ongoing video call to be dropped.
*
* @return {@code true} if answering this call will drop an ongoing video call, {@code false}
@@ -922,7 +812,7 @@ public class DialerCall {
return String.format(
Locale.US,
"[%s, %s, %s, %s, children:%s, parent:%s, "
- + "conferenceable:%s, videoState:%s, mSessionModificationState:%d, VideoSettings:%s]",
+ + "conferenceable:%s, videoState:%s, mSessionModificationState:%d, CameraDir:%s]",
mId,
State.toString(getState()),
Details.capabilitiesToString(mTelecomCall.getDetails().getCallCapabilities()),
@@ -931,8 +821,8 @@ public class DialerCall {
getParentId(),
this.mTelecomCall.getConferenceableCalls(),
VideoProfile.videoStateToString(mTelecomCall.getDetails().getVideoState()),
- mSessionModificationState,
- getVideoSettings());
+ getVideoTech().getSessionModificationState(),
+ getCameraDir());
}
public String toSimpleString() {
@@ -1012,20 +902,6 @@ public class DialerCall {
mTelecomCall.unregisterCallback(mTelecomCallCallback);
}
- public void acceptUpgradeRequest(int videoState) {
- LogUtil.i("DialerCall.acceptUpgradeRequest", "videoState: " + videoState);
- VideoProfile videoProfile = new VideoProfile(videoState);
- getVideoCall().sendSessionModifyResponse(videoProfile);
- setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
- }
-
- public void declineUpgradeRequest() {
- LogUtil.i("DialerCall.declineUpgradeRequest", "");
- VideoProfile videoProfile = new VideoProfile(getVideoState());
- getVideoCall().sendSessionModifyResponse(videoProfile);
- setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
- }
-
public void phoneAccountSelected(PhoneAccountHandle accountHandle, boolean setDefault) {
LogUtil.i(
"DialerCall.phoneAccountSelected",
@@ -1064,6 +940,10 @@ public class DialerCall {
mTelecomCall.answer(videoState);
}
+ public void answer() {
+ answer(mTelecomCall.getDetails().getVideoState());
+ }
+
public void reject(boolean rejectWithMessage, String message) {
LogUtil.i("DialerCall.reject", "");
mTelecomCall.reject(rejectWithMessage, message);
@@ -1095,6 +975,10 @@ public class DialerCall {
return mContext.getSystemService(TelecomManager.class).getPhoneAccount(accountHandle);
}
+ public VideoTech getVideoTech() {
+ return mVideoTechManager.getVideoTech();
+ }
+
public String getCallbackNumber() {
if (callbackNumber == null) {
// Show the emergency callback number if either:
@@ -1146,6 +1030,39 @@ public class DialerCall {
return null;
}
+ @Override
+ public void onVideoTechStateChanged() {
+ update();
+ }
+
+ @Override
+ public void onSessionModificationStateChanged() {
+ for (DialerCallListener listener : mListeners) {
+ listener.onDialerCallSessionModificationStateChange();
+ }
+ }
+
+ @Override
+ public void onCameraDimensionsChanged(int width, int height) {
+ InCallVideoCallCallbackNotifier.getInstance().cameraDimensionsChanged(this, width, height);
+ }
+
+ @Override
+ public void onPeerDimensionsChanged(int width, int height) {
+ InCallVideoCallCallbackNotifier.getInstance().peerDimensionsChanged(this, width, height);
+ }
+
+ @Override
+ public void onVideoUpgradeRequestReceived() {
+ LogUtil.enterBlock("DialerCall.onVideoUpgradeRequestReceived");
+
+ for (DialerCallListener listener : mListeners) {
+ listener.onDialerCallUpgradeToVideo();
+ }
+
+ update();
+ }
+
/**
* Specifies whether a number is in the call history or not. {@link #CALL_HISTORY_STATUS_UNKNOWN}
* means there is no result.
@@ -1191,8 +1108,8 @@ public class DialerCall {
case CONFERENCED:
return true;
default:
+ return false;
}
- return false;
}
public static boolean isDialing(int state) {
@@ -1239,71 +1156,11 @@ public class DialerCall {
}
}
- /**
- * Defines different states of session modify requests, which are used to upgrade to video, or
- * downgrade to audio.
- */
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({
- SESSION_MODIFICATION_STATE_NO_REQUEST,
- SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE,
- SESSION_MODIFICATION_STATE_REQUEST_FAILED,
- SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST,
- SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT,
- SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED,
- SESSION_MODIFICATION_STATE_REQUEST_REJECTED,
- SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE
- })
- public @interface SessionModificationState {}
-
- public static final int SESSION_MODIFICATION_STATE_NO_REQUEST = 0;
- public static final int SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE = 1;
- public static final int SESSION_MODIFICATION_STATE_REQUEST_FAILED = 2;
- public static final int SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST = 3;
- public static final int SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT = 4;
- public static final int SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED = 5;
- public static final int SESSION_MODIFICATION_STATE_REQUEST_REJECTED = 6;
- public static final int SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE = 7;
-
- public static class VideoSettings {
-
+ /** Camera direction constants */
+ public static class CameraDirection {
public static final int CAMERA_DIRECTION_UNKNOWN = -1;
public static final int CAMERA_DIRECTION_FRONT_FACING = CameraCharacteristics.LENS_FACING_FRONT;
public static final int CAMERA_DIRECTION_BACK_FACING = CameraCharacteristics.LENS_FACING_BACK;
-
- private int mCameraDirection = CAMERA_DIRECTION_UNKNOWN;
-
- /**
- * Gets the camera direction. if camera direction is set to CAMERA_DIRECTION_UNKNOWN, the video
- * state of the call should be used to infer the camera direction.
- *
- * @see {@link CameraCharacteristics#LENS_FACING_FRONT}
- * @see {@link CameraCharacteristics#LENS_FACING_BACK}
- */
- public int getCameraDir() {
- return mCameraDirection;
- }
-
- /**
- * Sets the camera direction. if camera direction is set to CAMERA_DIRECTION_UNKNOWN, the video
- * state of the call should be used to infer the camera direction.
- *
- * @see {@link CameraCharacteristics#LENS_FACING_FRONT}
- * @see {@link CameraCharacteristics#LENS_FACING_BACK}
- */
- public void setCameraDir(int cameraDirection) {
- if (cameraDirection == CAMERA_DIRECTION_FRONT_FACING
- || cameraDirection == CAMERA_DIRECTION_BACK_FACING) {
- mCameraDirection = cameraDirection;
- } else {
- mCameraDirection = CAMERA_DIRECTION_UNKNOWN;
- }
- }
-
- @Override
- public String toString() {
- return "(CameraDir:" + getCameraDir() + ")";
- }
}
/**
@@ -1394,6 +1251,48 @@ public class DialerCall {
}
}
+ private static class VideoTechManager {
+ private final EmptyVideoTech emptyVideoTech = new EmptyVideoTech();
+ private final VideoTech[] videoTechs;
+ private VideoTech savedTech;
+
+ VideoTechManager(DialerCall call) {
+ String phoneNumber = call.getNumber();
+
+ // Insert order here determines the priority of that video tech option
+ videoTechs =
+ new VideoTech[] {
+ new ImsVideoTech(call, call.mTelecomCall),
+ new RcsVideoShare(
+ EnrichedCallComponent.get(call.mContext).getEnrichedCallManager(),
+ call,
+ phoneNumber != null ? phoneNumber : "")
+ };
+ }
+
+ VideoTech getVideoTech() {
+ if (savedTech != null) {
+ return savedTech;
+ }
+
+ for (VideoTech tech : videoTechs) {
+ if (tech.isAvailable()) {
+ // Remember the first VideoTech that becomes available and always use it
+ savedTech = tech;
+ return savedTech;
+ }
+ }
+
+ return emptyVideoTech;
+ }
+
+ void dispatchCallStateChanged(int newState) {
+ for (VideoTech videoTech : videoTechs) {
+ videoTech.onCallStateChanged(newState);
+ }
+ }
+ }
+
/** Called when canned text responses have been loaded. */
public interface CannedTextResponsesLoadedListener {
void onCannedTextResponsesLoaded(DialerCall call);
diff --git a/java/com/android/incallui/call/DialerCallListener.java b/java/com/android/incallui/call/DialerCallListener.java
index b426cd72e..fece103fa 100644
--- a/java/com/android/incallui/call/DialerCallListener.java
+++ b/java/com/android/incallui/call/DialerCallListener.java
@@ -16,8 +16,6 @@
package com.android.incallui.call;
-import com.android.incallui.call.DialerCall.SessionModificationState;
-
/** Used to monitor state changes in a dialer call. */
public interface DialerCallListener {
@@ -31,7 +29,7 @@ public interface DialerCallListener {
void onDialerCallUpgradeToVideo();
- void onDialerCallSessionModificationStateChange(@SessionModificationState int state);
+ void onDialerCallSessionModificationStateChange();
void onWiFiToLteHandover();
diff --git a/java/com/android/incallui/call/InCallVideoCallCallback.java b/java/com/android/incallui/call/InCallVideoCallCallback.java
deleted file mode 100644
index f897ac9dd..000000000
--- a/java/com/android/incallui/call/InCallVideoCallCallback.java
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.incallui.call;
-
-import android.os.Handler;
-import android.support.annotation.Nullable;
-import android.telecom.Connection;
-import android.telecom.Connection.VideoProvider;
-import android.telecom.InCallService.VideoCall;
-import android.telecom.VideoProfile;
-import android.telecom.VideoProfile.CameraCapabilities;
-import com.android.dialer.common.LogUtil;
-import com.android.incallui.call.DialerCall.SessionModificationState;
-
-/** Implements the InCallUI VideoCall Callback. */
-public class InCallVideoCallCallback extends VideoCall.Callback implements Runnable {
-
- private static final int CLEAR_FAILED_REQUEST_TIMEOUT_MILLIS = 4000;
-
- private final DialerCall call;
- @Nullable private Handler handler;
- @SessionModificationState private int newSessionModificationState;
-
- public InCallVideoCallCallback(DialerCall call) {
- this.call = call;
- }
-
- @Override
- public void onSessionModifyRequestReceived(VideoProfile videoProfile) {
- LogUtil.i(
- "InCallVideoCallCallback.onSessionModifyRequestReceived", "videoProfile: " + videoProfile);
- int previousVideoState = VideoUtils.getUnPausedVideoState(call.getVideoState());
- int newVideoState = VideoUtils.getUnPausedVideoState(videoProfile.getVideoState());
-
- boolean wasVideoCall = VideoUtils.isVideoCall(previousVideoState);
- boolean isVideoCall = VideoUtils.isVideoCall(newVideoState);
-
- if (wasVideoCall && !isVideoCall) {
- LogUtil.v(
- "InCallVideoCallCallback.onSessionModifyRequestReceived",
- "call downgraded to " + newVideoState);
- } else if (previousVideoState != newVideoState) {
- InCallVideoCallCallbackNotifier.getInstance().upgradeToVideoRequest(call, newVideoState);
- }
- }
-
- /**
- * @param status Status of the session modify request. Valid values are {@link
- * Connection.VideoProvider#SESSION_MODIFY_REQUEST_SUCCESS}, {@link
- * Connection.VideoProvider#SESSION_MODIFY_REQUEST_FAIL}, {@link
- * Connection.VideoProvider#SESSION_MODIFY_REQUEST_INVALID}
- * @param responseProfile The actual profile changes made by the peer device.
- */
- @Override
- public void onSessionModifyResponseReceived(
- int status, VideoProfile requestedProfile, VideoProfile responseProfile) {
- LogUtil.i(
- "InCallVideoCallCallback.onSessionModifyResponseReceived",
- "status: %d, "
- + "requestedProfile: %s, responseProfile: %s, current session modification state: %d",
- status,
- requestedProfile,
- responseProfile,
- call.getSessionModificationState());
-
- if (call.getSessionModificationState()
- == DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE) {
- if (handler == null) {
- handler = new Handler();
- } else {
- handler.removeCallbacks(this);
- }
-
- newSessionModificationState = getDialerSessionModifyStateTelecomStatus(status);
- if (status != VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS) {
- // This will update the video UI to display the error message.
- call.setSessionModificationState(newSessionModificationState);
- }
-
- // Wait for 4 seconds and then clean the session modification state. This allows the video UI
- // to stay up so that the user can read the error message.
- //
- // If the other person accepted the upgrade request then this will keep the video UI up until
- // the call's video state change. Without this we would switch to the voice call and then
- // switch back to video UI.
- handler.postDelayed(this, CLEAR_FAILED_REQUEST_TIMEOUT_MILLIS);
- } else if (call.getSessionModificationState()
- == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
- call.setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
- } else if (call.getSessionModificationState()
- == DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE) {
- call.setSessionModificationState(getDialerSessionModifyStateTelecomStatus(status));
- } else {
- LogUtil.i(
- "InCallVideoCallCallback.onSessionModifyResponseReceived",
- "call is not waiting for " + "response, doing nothing");
- }
- }
-
- @SessionModificationState
- private int getDialerSessionModifyStateTelecomStatus(int telecomStatus) {
- switch (telecomStatus) {
- case VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS:
- return DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST;
- case VideoProvider.SESSION_MODIFY_REQUEST_FAIL:
- case VideoProvider.SESSION_MODIFY_REQUEST_INVALID:
- // Check if it's already video call, which means the request is not video upgrade request.
- if (VideoUtils.isVideoCall(call.getVideoState())) {
- return DialerCall.SESSION_MODIFICATION_STATE_REQUEST_FAILED;
- } else {
- return DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED;
- }
- case VideoProvider.SESSION_MODIFY_REQUEST_TIMED_OUT:
- return DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT;
- case VideoProvider.SESSION_MODIFY_REQUEST_REJECTED_BY_REMOTE:
- return DialerCall.SESSION_MODIFICATION_STATE_REQUEST_REJECTED;
- default:
- LogUtil.e(
- "InCallVideoCallCallback.getDialerSessionModifyStateTelecomStatus",
- "unknown status: %d",
- telecomStatus);
- return DialerCall.SESSION_MODIFICATION_STATE_REQUEST_FAILED;
- }
- }
-
- @Override
- public void onCallSessionEvent(int event) {
- InCallVideoCallCallbackNotifier.getInstance().callSessionEvent(event);
- }
-
- @Override
- public void onPeerDimensionsChanged(int width, int height) {
- InCallVideoCallCallbackNotifier.getInstance().peerDimensionsChanged(call, width, height);
- }
-
- @Override
- public void onVideoQualityChanged(int videoQuality) {
- InCallVideoCallCallbackNotifier.getInstance().videoQualityChanged(call, videoQuality);
- }
-
- /**
- * Handles a change to the call data usage. No implementation as the in-call UI does not display
- * data usage.
- *
- * @param dataUsage The updated data usage.
- */
- @Override
- public void onCallDataUsageChanged(long dataUsage) {
- LogUtil.v("InCallVideoCallCallback.onCallDataUsageChanged", "dataUsage = " + dataUsage);
- InCallVideoCallCallbackNotifier.getInstance().callDataUsageChanged(dataUsage);
- }
-
- /**
- * Handles changes to the camera capabilities. No implementation as the in-call UI does not make
- * use of camera capabilities.
- *
- * @param cameraCapabilities The changed camera capabilities.
- */
- @Override
- public void onCameraCapabilitiesChanged(CameraCapabilities cameraCapabilities) {
- if (cameraCapabilities != null) {
- InCallVideoCallCallbackNotifier.getInstance()
- .cameraDimensionsChanged(
- call, cameraCapabilities.getWidth(), cameraCapabilities.getHeight());
- }
- }
-
- /**
- * Called 4 seconds after the remote user responds to the video upgrade request. We use this to
- * clear the session modify state.
- */
- @Override
- public void run() {
- if (call.getSessionModificationState() == newSessionModificationState) {
- LogUtil.i("InCallVideoCallCallback.onSessionModifyResponseReceived", "clearing state");
- call.setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
- } else {
- LogUtil.i(
- "InCallVideoCallCallback.onSessionModifyResponseReceived",
- "session modification state has changed, not clearing state");
- }
- }
-}
diff --git a/java/com/android/incallui/call/InCallVideoCallCallbackNotifier.java b/java/com/android/incallui/call/InCallVideoCallCallbackNotifier.java
index 4a949263c..1cb9f742e 100644
--- a/java/com/android/incallui/call/InCallVideoCallCallbackNotifier.java
+++ b/java/com/android/incallui/call/InCallVideoCallCallbackNotifier.java
@@ -18,16 +18,12 @@ package com.android.incallui.call;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
-import com.android.dialer.common.LogUtil;
import java.util.Collections;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
-/**
- * Class used by {@link InCallService.VideoCallCallback} to notify interested parties of incoming
- * events.
- */
+/** Class used to notify interested parties of incoming video related events. */
public class InCallVideoCallCallbackNotifier {
/** Singleton instance of this class. */
@@ -37,12 +33,6 @@ public class InCallVideoCallCallbackNotifier {
* ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is load factor before
* resizing, 1 means we only expect a single thread to access the map so make only a single shard
*/
- private final Set<SessionModificationListener> mSessionModificationListeners =
- Collections.newSetFromMap(
- new ConcurrentHashMap<SessionModificationListener, Boolean>(8, 0.9f, 1));
-
- private final Set<VideoEventListener> mVideoEventListeners =
- Collections.newSetFromMap(new ConcurrentHashMap<VideoEventListener, Boolean>(8, 0.9f, 1));
private final Set<SurfaceChangeListener> mSurfaceChangeListeners =
Collections.newSetFromMap(new ConcurrentHashMap<SurfaceChangeListener, Boolean>(8, 0.9f, 1));
@@ -55,48 +45,6 @@ public class InCallVideoCallCallbackNotifier {
}
/**
- * Adds a new {@link SessionModificationListener}.
- *
- * @param listener The listener.
- */
- public void addSessionModificationListener(@NonNull SessionModificationListener listener) {
- Objects.requireNonNull(listener);
- mSessionModificationListeners.add(listener);
- }
-
- /**
- * Remove a {@link SessionModificationListener}.
- *
- * @param listener The listener.
- */
- public void removeSessionModificationListener(@Nullable SessionModificationListener listener) {
- if (listener != null) {
- mSessionModificationListeners.remove(listener);
- }
- }
-
- /**
- * Adds a new {@link VideoEventListener}.
- *
- * @param listener The listener.
- */
- public void addVideoEventListener(@NonNull VideoEventListener listener) {
- Objects.requireNonNull(listener);
- mVideoEventListeners.add(listener);
- }
-
- /**
- * Remove a {@link VideoEventListener}.
- *
- * @param listener The listener.
- */
- public void removeVideoEventListener(@Nullable VideoEventListener listener) {
- if (listener != null) {
- mVideoEventListeners.remove(listener);
- }
- }
-
- /**
* Adds a new {@link SurfaceChangeListener}.
*
* @param listener The listener.
@@ -118,56 +66,6 @@ public class InCallVideoCallCallbackNotifier {
}
/**
- * Inform listeners of an upgrade to video request for a call.
- *
- * @param call The call.
- * @param videoState The video state we want to upgrade to.
- */
- public void upgradeToVideoRequest(DialerCall call, int videoState) {
- LogUtil.v(
- "InCallVideoCallCallbackNotifier.upgradeToVideoRequest",
- "call = " + call + " new video state = " + videoState);
- for (SessionModificationListener listener : mSessionModificationListeners) {
- listener.onUpgradeToVideoRequest(call, videoState);
- }
- }
-
- /**
- * Inform listeners of a call session event.
- *
- * @param event The call session event.
- */
- public void callSessionEvent(int event) {
- for (VideoEventListener listener : mVideoEventListeners) {
- listener.onCallSessionEvent(event);
- }
- }
-
- /**
- * Inform listeners of a downgrade to audio.
- *
- * @param call The call.
- * @param paused The paused state.
- */
- public void peerPausedStateChanged(DialerCall call, boolean paused) {
- for (VideoEventListener listener : mVideoEventListeners) {
- listener.onPeerPauseStateChanged(call, paused);
- }
- }
-
- /**
- * Inform listeners of any change in the video quality of the call
- *
- * @param call The call.
- * @param videoQuality The updated video quality of the call.
- */
- public void videoQualityChanged(DialerCall call, int videoQuality) {
- for (VideoEventListener listener : mVideoEventListeners) {
- listener.onVideoQualityChanged(call, videoQuality);
- }
- }
-
- /**
* Inform listeners of a change to peer dimensions.
*
* @param call The call.
@@ -194,67 +92,6 @@ public class InCallVideoCallCallbackNotifier {
}
/**
- * Inform listeners of a change to call data usage.
- *
- * @param dataUsage data usage value
- */
- public void callDataUsageChanged(long dataUsage) {
- for (VideoEventListener listener : mVideoEventListeners) {
- listener.onCallDataUsageChange(dataUsage);
- }
- }
-
- /** Listener interface for any class that wants to be notified of upgrade to video request. */
- public interface SessionModificationListener {
-
- /**
- * Called when a peer request is received to upgrade an audio-only call to a video call.
- *
- * @param call The call the request was received for.
- * @param videoState The requested video state.
- */
- void onUpgradeToVideoRequest(DialerCall call, int videoState);
- }
-
- /**
- * Listener interface for any class that wants to be notified of video events, including pause and
- * un-pause of peer video, video quality changes.
- */
- public interface VideoEventListener {
-
- /**
- * Called when the peer pauses or un-pauses video transmission.
- *
- * @param call The call which paused or un-paused video transmission.
- * @param paused {@code True} when the video transmission is paused, {@code false} otherwise.
- */
- void onPeerPauseStateChanged(DialerCall call, boolean paused);
-
- /**
- * Called when the video quality changes.
- *
- * @param call The call whose video quality changes.
- * @param videoCallQuality - values are QUALITY_HIGH, MEDIUM, LOW and UNKNOWN.
- */
- void onVideoQualityChanged(DialerCall call, int videoCallQuality);
-
- /*
- * Called when call data usage value is requested or when call data usage value is updated
- * because of a call state change
- *
- * @param dataUsage call data usage value
- */
- void onCallDataUsageChange(long dataUsage);
-
- /**
- * Called when call session event is raised.
- *
- * @param event The call session event.
- */
- void onCallSessionEvent(int event);
- }
-
- /**
* Listener interface for any class that wants to be notified of changes to the video surfaces.
*/
public interface SurfaceChangeListener {
diff --git a/java/com/android/incallui/call/VideoUtils.java b/java/com/android/incallui/call/VideoUtils.java
index 80fbfb1cc..b99b73222 100644
--- a/java/com/android/incallui/call/VideoUtils.java
+++ b/java/com/android/incallui/call/VideoUtils.java
@@ -19,113 +19,24 @@ package com.android.incallui.call;
import android.content.Context;
import android.content.pm.PackageManager;
import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
-import android.telecom.VideoProfile;
-import com.android.dialer.compat.CompatUtils;
import com.android.dialer.util.DialerUtils;
-import com.android.incallui.call.DialerCall.SessionModificationState;
-import java.util.Objects;
+import com.android.incallui.videotech.VideoTech;
+import com.android.incallui.videotech.VideoTech.SessionModificationState;
public class VideoUtils {
private static final String PREFERENCE_CAMERA_ALLOWED_BY_USER = "camera_allowed_by_user";
- public static boolean isVideoCall(@Nullable DialerCall call) {
- return call != null && isVideoCall(call.getVideoState());
- }
-
- public static boolean isVideoCall(int videoState) {
- return CompatUtils.isVideoCompatible()
- && (VideoProfile.isTransmissionEnabled(videoState)
- || VideoProfile.isReceptionEnabled(videoState));
- }
-
- public static boolean hasSentVideoUpgradeRequest(@Nullable DialerCall call) {
- return call != null && hasSentVideoUpgradeRequest(call.getSessionModificationState());
- }
-
public static boolean hasSentVideoUpgradeRequest(@SessionModificationState int state) {
- return state == DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE
- || state == DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED
- || state == DialerCall.SESSION_MODIFICATION_STATE_REQUEST_REJECTED
- || state == DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT;
- }
-
- public static boolean hasReceivedVideoUpgradeRequest(@Nullable DialerCall call) {
- return call != null && hasReceivedVideoUpgradeRequest(call.getSessionModificationState());
+ return state == VideoTech.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE
+ || state == VideoTech.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED
+ || state == VideoTech.SESSION_MODIFICATION_STATE_REQUEST_REJECTED
+ || state == VideoTech.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT;
}
public static boolean hasReceivedVideoUpgradeRequest(@SessionModificationState int state) {
- return state == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
- }
-
- public static boolean isBidirectionalVideoCall(DialerCall call) {
- return CompatUtils.isVideoCompatible() && VideoProfile.isBidirectional(call.getVideoState());
- }
-
- public static boolean isTransmissionEnabled(DialerCall call) {
- if (!CompatUtils.isVideoCompatible()) {
- return false;
- }
-
- return VideoProfile.isTransmissionEnabled(call.getVideoState());
- }
-
- public static boolean isIncomingVideoCall(DialerCall call) {
- if (!VideoUtils.isVideoCall(call)) {
- return false;
- }
- final int state = call.getState();
- return (state == DialerCall.State.INCOMING) || (state == DialerCall.State.CALL_WAITING);
- }
-
- public static boolean isActiveVideoCall(DialerCall call) {
- return VideoUtils.isVideoCall(call) && call.getState() == DialerCall.State.ACTIVE;
- }
-
- public static boolean isOutgoingVideoCall(DialerCall call) {
- if (!VideoUtils.isVideoCall(call)) {
- return false;
- }
- final int state = call.getState();
- return DialerCall.State.isDialing(state)
- || state == DialerCall.State.CONNECTING
- || state == DialerCall.State.SELECT_PHONE_ACCOUNT;
- }
-
- public static boolean isAudioCall(DialerCall call) {
- if (!CompatUtils.isVideoCompatible()) {
- return true;
- }
-
- return call != null && VideoProfile.isAudioOnly(call.getVideoState());
- }
-
- // TODO (ims-vt) Check if special handling is needed for CONF calls.
- public static boolean canVideoPause(DialerCall call) {
- return isVideoCall(call) && call.getState() == DialerCall.State.ACTIVE;
- }
-
- public static VideoProfile makeVideoPauseProfile(@NonNull DialerCall call) {
- Objects.requireNonNull(call);
- if (VideoProfile.isAudioOnly(call.getVideoState())) {
- throw new IllegalStateException();
- }
- return new VideoProfile(getPausedVideoState(call.getVideoState()));
- }
-
- public static VideoProfile makeVideoUnPauseProfile(@NonNull DialerCall call) {
- Objects.requireNonNull(call);
- return new VideoProfile(getUnPausedVideoState(call.getVideoState()));
- }
-
- public static int getUnPausedVideoState(int videoState) {
- return videoState & (~VideoProfile.STATE_PAUSED);
- }
-
- public static int getPausedVideoState(int videoState) {
- return videoState | VideoProfile.STATE_PAUSED;
+ return state == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
}
public static boolean hasCameraPermissionAndAllowedByUser(@NonNull Context context) {
diff --git a/java/com/android/incallui/calllocation/CallLocation.java b/java/com/android/incallui/calllocation/CallLocation.java
new file mode 100644
index 000000000..15a6a8e49
--- /dev/null
+++ b/java/com/android/incallui/calllocation/CallLocation.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.v4.app.Fragment;
+
+/** Used to show the user's location during an emergency call. */
+public interface CallLocation {
+
+ boolean canGetLocation(@NonNull Context context);
+
+ @NonNull
+ Fragment getLocationFragment(@NonNull Context context);
+
+ void close();
+}
diff --git a/java/com/android/incallui/calllocation/CallLocationComponent.java b/java/com/android/incallui/calllocation/CallLocationComponent.java
new file mode 100644
index 000000000..6b1faf299
--- /dev/null
+++ b/java/com/android/incallui/calllocation/CallLocationComponent.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation;
+
+import android.content.Context;
+import dagger.Subcomponent;
+import com.android.incallui.calllocation.stub.StubCallLocationModule;
+
+/** Subcomponent that can be used to access the call location implementation. */
+public class CallLocationComponent {
+ private static CallLocationComponent instance;
+ private CallLocation callLocation;
+
+ public CallLocation getCallLocation(){
+ if (callLocation == null) {
+ callLocation = new StubCallLocationModule.StubCallLocation();
+ }
+ return callLocation;
+ }
+
+ public static CallLocationComponent get(Context context) {
+ if (instance == null) {
+ instance = new CallLocationComponent();
+ }
+ return instance;
+ }
+
+ /** Used to refer to the root application component. */
+ public interface HasComponent {
+ CallLocationComponent callLocationComponent();
+ }
+}
diff --git a/java/com/android/incallui/calllocation/impl/AndroidManifest.xml b/java/com/android/incallui/calllocation/impl/AndroidManifest.xml
new file mode 100644
index 000000000..550c5808c
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.incallui.calllocation.impl">
+
+ <application>
+ <meta-data
+ android:name="com.google.android.gms.version"
+ android:value="@integer/google_play_services_version"/>
+ </application>
+</manifest>
diff --git a/java/com/android/incallui/calllocation/impl/AuthException.java b/java/com/android/incallui/calllocation/impl/AuthException.java
new file mode 100644
index 000000000..26def2fc9
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/AuthException.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+/** For detecting backend authorization errors */
+public class AuthException extends Exception {
+
+ public AuthException(String detailMessage) {
+ super(detailMessage);
+ }
+}
diff --git a/java/com/android/incallui/calllocation/impl/CallLocationImpl.java b/java/com/android/incallui/calllocation/impl/CallLocationImpl.java
new file mode 100644
index 000000000..20f5ffb0f
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/CallLocationImpl.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import android.content.Context;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.v4.app.Fragment;
+import com.android.dialer.common.Assert;
+import com.android.incallui.calllocation.CallLocation;
+import javax.inject.Inject;
+
+/** Uses Google Play Services to show the user's location during an emergency call. */
+public class CallLocationImpl implements CallLocation {
+
+ private LocationHelper locationHelper;
+ private LocationFragment locationFragment;
+
+ @Inject
+ public CallLocationImpl() {}
+
+ @MainThread
+ @Override
+ public boolean canGetLocation(@NonNull Context context) {
+ Assert.isMainThread();
+ return LocationHelper.canGetLocation(context);
+ }
+
+ @MainThread
+ @NonNull
+ @Override
+ public Fragment getLocationFragment(@NonNull Context context) {
+ Assert.isMainThread();
+ if (locationFragment == null) {
+ locationFragment = new LocationFragment();
+ locationHelper = new LocationHelper(context);
+ locationHelper.addLocationListener(locationFragment.getPresenter());
+ }
+ return locationFragment;
+ }
+
+ @MainThread
+ @Override
+ public void close() {
+ Assert.isMainThread();
+ if (locationFragment != null) {
+ locationHelper.removeLocationListener(locationFragment.getPresenter());
+ locationHelper.close();
+ locationFragment = null;
+ locationHelper = null;
+ }
+ }
+}
diff --git a/java/com/android/incallui/calllocation/impl/CallLocationModule.java b/java/com/android/incallui/calllocation/impl/CallLocationModule.java
new file mode 100644
index 000000000..73e85554e
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/CallLocationModule.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import com.android.incallui.calllocation.CallLocation;
+import dagger.Binds;
+import dagger.Module;
+
+/** This module provides an instance of call location. */
+@Module
+public abstract class CallLocationModule {
+
+ @Binds
+ public abstract CallLocation bindCallLocation(CallLocationImpl callLocation);
+}
diff --git a/java/com/android/incallui/calllocation/impl/DownloadMapImageTask.java b/java/com/android/incallui/calllocation/impl/DownloadMapImageTask.java
new file mode 100644
index 000000000..801b0d35c
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/DownloadMapImageTask.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import android.graphics.drawable.Drawable;
+import android.location.Location;
+import android.net.TrafficStats;
+import android.os.AsyncTask;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.calllocation.impl.LocationPresenter.LocationUi;
+import java.io.InputStream;
+import java.lang.ref.WeakReference;
+import java.net.URL;
+
+class DownloadMapImageTask extends AsyncTask<Location, Void, Drawable> {
+
+ private static final String STATIC_MAP_SRC_NAME = "src";
+
+ private final WeakReference<LocationUi> mUiReference;
+
+ public DownloadMapImageTask(WeakReference<LocationUi> uiReference) {
+ mUiReference = uiReference;
+ }
+
+ @Override
+ protected Drawable doInBackground(Location... locations) {
+ LocationUi ui = mUiReference.get();
+ if (ui == null) {
+ return null;
+ }
+ if (locations == null || locations.length == 0) {
+ LogUtil.e("DownloadMapImageTask.doInBackground", "No location provided");
+ return null;
+ }
+
+ try {
+ URL mapUrl = new URL(LocationUrlBuilder.getStaticMapUrl(ui.getContext(), locations[0]));
+ InputStream content = (InputStream) mapUrl.getContent();
+
+ TrafficStats.setThreadStatsTag(TrafficStatsTags.DOWNLOAD_LOCATION_MAP_TAG);
+ return Drawable.createFromStream(content, STATIC_MAP_SRC_NAME);
+ } catch (Exception ex) {
+ LogUtil.e("DownloadMapImageTask.doInBackground", "Exception!!!", ex);
+ return null;
+ } finally {
+ TrafficStats.clearThreadStatsTag();
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Drawable mapImage) {
+ LocationUi ui = mUiReference.get();
+ if (ui == null) {
+ return;
+ }
+
+ try {
+ ui.setMap(mapImage);
+ } catch (Exception ex) {
+ LogUtil.e("DownloadMapImageTask.onPostExecute", "Exception!!!", ex);
+ }
+ }
+}
diff --git a/java/com/android/incallui/calllocation/impl/GoogleLocationSettingHelper.java b/java/com/android/incallui/calllocation/impl/GoogleLocationSettingHelper.java
new file mode 100644
index 000000000..18a80b8ce
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/GoogleLocationSettingHelper.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.Settings.Secure;
+import android.provider.Settings.SettingNotFoundException;
+import com.android.dialer.common.LogUtil;
+
+/**
+ * Helper class to check if Google Location Services is enabled. This class is based on
+ * https://docs.google.com/a/google.com/document/d/1sGm8pHgGY1QmxbLCwTZuWQASEDN7CFW9EPSZXAuGQfo
+ */
+public class GoogleLocationSettingHelper {
+
+ /** User has disagreed to use location for Google services. */
+ public static final int USE_LOCATION_FOR_SERVICES_OFF = 0;
+ /** User has agreed to use location for Google services. */
+ public static final int USE_LOCATION_FOR_SERVICES_ON = 1;
+ /** The user has neither agreed nor disagreed to use location for Google services yet. */
+ public static final int USE_LOCATION_FOR_SERVICES_NOT_SET = 2;
+
+ private static final String GOOGLE_SETTINGS_AUTHORITY = "com.google.settings";
+ private static final Uri GOOGLE_SETTINGS_CONTENT_URI =
+ Uri.parse("content://" + GOOGLE_SETTINGS_AUTHORITY + "/partner");
+ private static final String NAME = "name";
+ private static final String VALUE = "value";
+ private static final String USE_LOCATION_FOR_SERVICES = "use_location_for_services";
+
+ /** Determine if Google apps need to conform to the USE_LOCATION_FOR_SERVICES setting. */
+ public static boolean isEnforceable(Context context) {
+ final ResolveInfo ri =
+ context
+ .getPackageManager()
+ .resolveActivity(
+ new Intent("com.google.android.gsf.GOOGLE_APPS_LOCATION_SETTINGS"),
+ PackageManager.MATCH_DEFAULT_ONLY);
+ return ri != null;
+ }
+
+ /**
+ * Get the current value for the 'Use value for location' setting.
+ *
+ * @return One of {@link #USE_LOCATION_FOR_SERVICES_NOT_SET}, {@link
+ * #USE_LOCATION_FOR_SERVICES_OFF} or {@link #USE_LOCATION_FOR_SERVICES_ON}.
+ */
+ private static int getUseLocationForServices(Context context) {
+ final ContentResolver resolver = context.getContentResolver();
+ Cursor c = null;
+ String stringValue = null;
+ try {
+ c =
+ resolver.query(
+ GOOGLE_SETTINGS_CONTENT_URI,
+ new String[] {VALUE},
+ NAME + "=?",
+ new String[] {USE_LOCATION_FOR_SERVICES},
+ null);
+ if (c != null && c.moveToNext()) {
+ stringValue = c.getString(0);
+ }
+ } catch (final RuntimeException e) {
+ LogUtil.e(
+ "GoogleLocationSettingHelper.getUseLocationForServices",
+ "Failed to get 'Use My Location' setting",
+ e);
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ if (stringValue == null) {
+ return USE_LOCATION_FOR_SERVICES_NOT_SET;
+ }
+ int value;
+ try {
+ value = Integer.parseInt(stringValue);
+ } catch (final NumberFormatException nfe) {
+ value = USE_LOCATION_FOR_SERVICES_NOT_SET;
+ }
+ return value;
+ }
+
+ /** Whether or not the system location setting is enable */
+ public static boolean isSystemLocationSettingEnabled(Context context) {
+ try {
+ return Secure.getInt(context.getContentResolver(), Secure.LOCATION_MODE)
+ != Secure.LOCATION_MODE_OFF;
+ } catch (SettingNotFoundException e) {
+ LogUtil.e(
+ "GoogleLocationSettingHelper.isSystemLocationSettingEnabled",
+ "Failed to get System Location setting",
+ e);
+ return false;
+ }
+ }
+
+ /** Convenience method that returns true is GLS is ON or if it's not enforceable. */
+ public static boolean isGoogleLocationServicesEnabled(Context context) {
+ return !isEnforceable(context)
+ || getUseLocationForServices(context) == USE_LOCATION_FOR_SERVICES_ON;
+ }
+}
diff --git a/java/com/android/incallui/calllocation/impl/HttpFetcher.java b/java/com/android/incallui/calllocation/impl/HttpFetcher.java
new file mode 100644
index 000000000..7bfbaa6ef
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/HttpFetcher.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import static com.android.dialer.util.DialerUtils.closeQuietly;
+
+import android.content.Context;
+import android.net.Uri;
+import android.net.Uri.Builder;
+import android.os.SystemClock;
+import android.util.Pair;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.util.MoreStrings;
+import com.google.android.common.http.UrlRules;
+import java.io.ByteArrayOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.ProtocolException;
+import java.net.URL;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/** Utility for making http requests. */
+public class HttpFetcher {
+
+ // Phone number
+ public static final String PARAM_ID = "id";
+ // auth token
+ public static final String PARAM_ACCESS_TOKEN = "access_token";
+ private static final String TAG = HttpFetcher.class.getSimpleName();
+
+ /**
+ * Send a http request to the given url.
+ *
+ * @param urlString The url to request.
+ * @return The response body as a byte array. Or {@literal null} if status code is not 2xx.
+ * @throws java.io.IOException when an error occurs.
+ */
+ public static byte[] sendRequestAsByteArray(
+ Context context, String urlString, String requestMethod, List<Pair<String, String>> headers)
+ throws IOException, AuthException {
+ Objects.requireNonNull(urlString);
+
+ URL url = reWriteUrl(context, urlString);
+ if (url == null) {
+ return null;
+ }
+
+ HttpURLConnection conn = null;
+ InputStream is = null;
+ boolean isError = false;
+ final long start = SystemClock.uptimeMillis();
+ try {
+ conn = (HttpURLConnection) url.openConnection();
+ setMethodAndHeaders(conn, requestMethod, headers);
+ int responseCode = conn.getResponseCode();
+ LogUtil.i("HttpFetcher.sendRequestAsByteArray", "response code: " + responseCode);
+ // All 2xx codes are successful.
+ if (responseCode / 100 == 2) {
+ is = conn.getInputStream();
+ } else {
+ is = conn.getErrorStream();
+ isError = true;
+ }
+
+ final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ final byte[] buffer = new byte[1024];
+ int bytesRead;
+
+ while ((bytesRead = is.read(buffer)) != -1) {
+ baos.write(buffer, 0, bytesRead);
+ }
+
+ if (isError) {
+ handleBadResponse(url.toString(), baos.toByteArray());
+ if (responseCode == 401) {
+ throw new AuthException("Auth error");
+ }
+ return null;
+ }
+
+ byte[] response = baos.toByteArray();
+ LogUtil.i("HttpFetcher.sendRequestAsByteArray", "received " + response.length + " bytes");
+ long end = SystemClock.uptimeMillis();
+ LogUtil.i("HttpFetcher.sendRequestAsByteArray", "fetch took " + (end - start) + " ms");
+ return response;
+ } finally {
+ closeQuietly(is);
+ if (conn != null) {
+ conn.disconnect();
+ }
+ }
+ }
+
+ /**
+ * Send a http request to the given url.
+ *
+ * @return The response body as a InputStream. Or {@literal null} if status code is not 2xx.
+ * @throws java.io.IOException when an error occurs.
+ */
+ public static InputStream sendRequestAsInputStream(
+ Context context, String urlString, String requestMethod, List<Pair<String, String>> headers)
+ throws IOException, AuthException {
+ Objects.requireNonNull(urlString);
+
+ URL url = reWriteUrl(context, urlString);
+ if (url == null) {
+ return null;
+ }
+
+ HttpURLConnection httpUrlConnection = null;
+ boolean isSuccess = false;
+ try {
+ httpUrlConnection = (HttpURLConnection) url.openConnection();
+ setMethodAndHeaders(httpUrlConnection, requestMethod, headers);
+ int responseCode = httpUrlConnection.getResponseCode();
+ LogUtil.i("HttpFetcher.sendRequestAsInputStream", "response code: " + responseCode);
+
+ if (responseCode == 401) {
+ throw new AuthException("Auth error");
+ } else if (responseCode / 100 == 2) { // All 2xx codes are successful.
+ InputStream is = httpUrlConnection.getInputStream();
+ if (is != null) {
+ is = new HttpInputStreamWrapper(httpUrlConnection, is);
+ isSuccess = true;
+ return is;
+ }
+ }
+
+ return null;
+ } finally {
+ if (httpUrlConnection != null && !isSuccess) {
+ httpUrlConnection.disconnect();
+ }
+ }
+ }
+
+ /**
+ * Set http method and headers.
+ *
+ * @param conn The connection to add headers to.
+ * @param requestMethod request method
+ * @param headers http headers where the first item in the pair is the key and second item is the
+ * value.
+ */
+ private static void setMethodAndHeaders(
+ HttpURLConnection conn, String requestMethod, List<Pair<String, String>> headers)
+ throws ProtocolException {
+ conn.setRequestMethod(requestMethod);
+ if (headers != null) {
+ for (Pair<String, String> pair : headers) {
+ conn.setRequestProperty(pair.first, pair.second);
+ }
+ }
+ }
+
+ private static String obfuscateUrl(String urlString) {
+ final Uri uri = Uri.parse(urlString);
+ final Builder builder =
+ new Builder().scheme(uri.getScheme()).authority(uri.getAuthority()).path(uri.getPath());
+ final Set<String> names = uri.getQueryParameterNames();
+ for (String name : names) {
+ if (PARAM_ACCESS_TOKEN.equals(name)) {
+ builder.appendQueryParameter(name, "token");
+ } else {
+ final String value = uri.getQueryParameter(name);
+ if (PARAM_ID.equals(name)) {
+ builder.appendQueryParameter(name, MoreStrings.toSafeString(value));
+ } else {
+ builder.appendQueryParameter(name, value);
+ }
+ }
+ }
+ return builder.toString();
+ }
+
+ /** Same as {@link #getRequestAsString(Context, String, String, List)} with null headers. */
+ public static String getRequestAsString(Context context, String urlString)
+ throws IOException, AuthException {
+ return getRequestAsString(context, urlString, "GET" /* Default to get. */, null);
+ }
+
+ /**
+ * Send a http request to the given url.
+ *
+ * @param context The android context.
+ * @param urlString The url to request.
+ * @param headers Http headers to pass in the request. {@literal null} is allowed.
+ * @return The response body as a String. Or {@literal null} if status code is not 2xx.
+ * @throws java.io.IOException when an error occurs.
+ */
+ public static String getRequestAsString(
+ Context context, String urlString, String requestMethod, List<Pair<String, String>> headers)
+ throws IOException, AuthException {
+ final byte[] byteArr = sendRequestAsByteArray(context, urlString, requestMethod, headers);
+ if (byteArr == null) {
+ // Encountered error response... just return.
+ return null;
+ }
+ final String response = new String(byteArr);
+ LogUtil.i("HttpFetcher.getRequestAsString", "response body: " + response);
+ return response;
+ }
+
+ /**
+ * Lookup up url re-write rules from gServices and apply to the given url.
+ *
+ * <p>https://wiki.corp.google.com/twiki/bin/view/Main/AndroidGservices#URL_Rewriting_Rules
+ *
+ * @return The new url.
+ */
+ private static URL reWriteUrl(Context context, String url) {
+ final UrlRules rules = UrlRules.getRules(context.getContentResolver());
+ final UrlRules.Rule rule = rules.matchRule(url);
+ final String newUrl = rule.apply(url);
+
+ if (newUrl == null) {
+ if (LogUtil.isDebugEnabled()) {
+ // Url is blocked by re-write.
+ LogUtil.i(
+ "HttpFetcher.reWriteUrl",
+ "url " + obfuscateUrl(url) + " is blocked. Ignoring request.");
+ }
+ return null;
+ }
+
+ if (LogUtil.isDebugEnabled()) {
+ LogUtil.i("HttpFetcher.reWriteUrl", "fetching " + obfuscateUrl(newUrl));
+ if (!newUrl.equals(url)) {
+ LogUtil.i(
+ "HttpFetcher.reWriteUrl",
+ "Original url: " + obfuscateUrl(url) + ", after re-write: " + obfuscateUrl(newUrl));
+ }
+ }
+
+ URL urlObject = null;
+ try {
+ urlObject = new URL(newUrl);
+ } catch (MalformedURLException e) {
+ LogUtil.e("HttpFetcher.reWriteUrl", "failed to parse url: " + url, e);
+ }
+ return urlObject;
+ }
+
+ private static void handleBadResponse(String url, byte[] response) {
+ LogUtil.i("HttpFetcher.handleBadResponse", "Got bad response code from url: " + url);
+ LogUtil.i("HttpFetcher.handleBadResponse", new String(response));
+ }
+
+ /** Disconnect {@link HttpURLConnection} when InputStream is closed */
+ private static class HttpInputStreamWrapper extends FilterInputStream {
+
+ final HttpURLConnection mHttpUrlConnection;
+ final long mStartMillis = SystemClock.uptimeMillis();
+
+ public HttpInputStreamWrapper(HttpURLConnection conn, InputStream in) {
+ super(in);
+ mHttpUrlConnection = conn;
+ }
+
+ @Override
+ public void close() throws IOException {
+ super.close();
+ mHttpUrlConnection.disconnect();
+ if (LogUtil.isDebugEnabled()) {
+ long endMillis = SystemClock.uptimeMillis();
+ LogUtil.i("HttpFetcher.close", "fetch took " + (endMillis - mStartMillis) + " ms");
+ }
+ }
+ }
+}
diff --git a/java/com/android/incallui/calllocation/impl/LocationFragment.java b/java/com/android/incallui/calllocation/impl/LocationFragment.java
new file mode 100644
index 000000000..b152cd683
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/LocationFragment.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import android.animation.LayoutTransition;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.location.Location;
+import android.os.Bundle;
+import android.os.Handler;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.ViewAnimator;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.baseui.BaseFragment;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Fragment which shows location during E911 calls, to supplement the user with accurate location
+ * information in case the user is asked for their location by the emergency responder.
+ *
+ * <p>If location data is inaccurate, stale, or unavailable, this should not be shown.
+ */
+public class LocationFragment extends BaseFragment<LocationPresenter, LocationPresenter.LocationUi>
+ implements LocationPresenter.LocationUi {
+
+ private static final String ADDRESS_DELIMITER = ",";
+
+ // Indexes used to animate fading between views
+ private static final int LOADING_VIEW_INDEX = 0;
+ private static final int LOCATION_VIEW_INDEX = 1;
+ private static final long TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(5);
+
+ private ViewAnimator viewAnimator;
+ private ImageView locationMap;
+ private TextView addressLine1;
+ private TextView addressLine2;
+ private TextView latLongLine;
+ private Location location;
+ private ViewGroup locationLayout;
+
+ private boolean isMapSet;
+ private boolean isAddressSet;
+ private boolean isLocationSet;
+ private boolean hasTimeoutStarted;
+
+ private final Handler handler = new Handler();
+ private final Runnable dataTimeoutRunnable =
+ () -> {
+ LogUtil.i(
+ "LocationFragment.dataTimeoutRunnable",
+ "timed out so animate any future layout changes");
+ locationLayout.setLayoutTransition(new LayoutTransition());
+ showLocationNow();
+ };
+
+ @Override
+ public LocationPresenter createPresenter() {
+ return new LocationPresenter();
+ }
+
+ @Override
+ public LocationPresenter.LocationUi getUi() {
+ return this;
+ }
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final View view = inflater.inflate(R.layout.location_fragment, container, false);
+ viewAnimator = (ViewAnimator) view.findViewById(R.id.location_view_animator);
+ locationMap = (ImageView) view.findViewById(R.id.location_map);
+ addressLine1 = (TextView) view.findViewById(R.id.address_line_one);
+ addressLine2 = (TextView) view.findViewById(R.id.address_line_two);
+ latLongLine = (TextView) view.findViewById(R.id.lat_long_line);
+ locationLayout = (ViewGroup) view.findViewById(R.id.location_layout);
+ view.setOnClickListener(
+ v -> {
+ LogUtil.enterBlock("LocationFragment.onCreateView");
+ launchMap();
+ });
+ return view;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ handler.removeCallbacks(dataTimeoutRunnable);
+ }
+
+ @Override
+ public void setMap(Drawable mapImage) {
+ LogUtil.enterBlock("LocationFragment.setMap");
+ isMapSet = true;
+ locationMap.setVisibility(View.VISIBLE);
+ locationMap.setImageDrawable(mapImage);
+ displayWhenReady();
+ }
+
+ @Override
+ public void setAddress(String address) {
+ LogUtil.i("LocationFragment.setAddress", address);
+ isAddressSet = true;
+ addressLine1.setVisibility(View.VISIBLE);
+ addressLine2.setVisibility(View.VISIBLE);
+ if (TextUtils.isEmpty(address)) {
+ addressLine1.setText(null);
+ addressLine2.setText(null);
+ } else {
+
+ // Split the address after the first delimiter for display, if present.
+ // For example, "1600 Amphitheatre Parkway, Mountain View, CA 94043"
+ // => "1600 Amphitheatre Parkway"
+ // => "Mountain View, CA 94043"
+ int splitIndex = address.indexOf(ADDRESS_DELIMITER);
+ if (splitIndex >= 0) {
+ updateText(addressLine1, address.substring(0, splitIndex).trim());
+ updateText(addressLine2, address.substring(splitIndex + 1).trim());
+ } else {
+ updateText(addressLine1, address);
+ updateText(addressLine2, null);
+ }
+ }
+ displayWhenReady();
+ }
+
+ @Override
+ public void setLocation(Location location) {
+ LogUtil.i("LocationFragment.setLocation", String.valueOf(location));
+ isLocationSet = true;
+ this.location = location;
+
+ if (location != null) {
+ latLongLine.setVisibility(View.VISIBLE);
+ latLongLine.setText(
+ getContext()
+ .getString(
+ R.string.lat_long_format, location.getLatitude(), location.getLongitude()));
+ }
+ displayWhenReady();
+ }
+
+ private void displayWhenReady() {
+ // Show the location if all data has loaded, otherwise prime the timeout
+ if (isMapSet && isAddressSet && isLocationSet) {
+ showLocationNow();
+ } else if (!hasTimeoutStarted) {
+ handler.postDelayed(dataTimeoutRunnable, TIMEOUT_MILLIS);
+ hasTimeoutStarted = true;
+ }
+ }
+
+ private void showLocationNow() {
+ handler.removeCallbacks(dataTimeoutRunnable);
+ if (viewAnimator.getDisplayedChild() != LOCATION_VIEW_INDEX) {
+ viewAnimator.setDisplayedChild(LOCATION_VIEW_INDEX);
+ }
+ }
+
+ @Override
+ public Context getContext() {
+ return getActivity();
+ }
+
+ private void launchMap() {
+ if (location != null) {
+ startActivity(
+ LocationUrlBuilder.getShowMapIntent(
+ location, addressLine1.getText(), addressLine2.getText()));
+ }
+ }
+
+ private static void updateText(TextView view, String text) {
+ if (!Objects.equals(text, view.getText())) {
+ view.setText(text);
+ }
+ }
+}
diff --git a/java/com/android/incallui/calllocation/impl/LocationHelper.java b/java/com/android/incallui/calllocation/impl/LocationHelper.java
new file mode 100644
index 000000000..645e9b86a
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/LocationHelper.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import android.content.Context;
+import android.location.Location;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.annotation.MainThread;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.util.PermissionsUtil;
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.api.GoogleApiClient;
+import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks;
+import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener;
+import com.google.android.gms.common.api.ResultCallback;
+import com.google.android.gms.common.api.Status;
+import com.google.android.gms.location.LocationListener;
+import com.google.android.gms.location.LocationRequest;
+import com.google.android.gms.location.LocationServices;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Uses the Fused location service to get location and pass updates on to listeners. */
+public class LocationHelper {
+
+ private static final int MIN_UPDATE_INTERVAL_MS = 30 * 1000;
+ private static final int LAST_UPDATE_THRESHOLD_MS = 60 * 1000;
+ private static final int LOCATION_ACCURACY_THRESHOLD_METERS = 100;
+
+ private final LocationHelperInternal locationHelperInternal;
+ private final List<LocationListener> listeners = new ArrayList<>();
+
+ @MainThread
+ LocationHelper(Context context) {
+ Assert.isMainThread();
+ Assert.checkArgument(canGetLocation(context));
+ locationHelperInternal = new LocationHelperInternal(context);
+ }
+
+ static boolean canGetLocation(Context context) {
+ if (!PermissionsUtil.hasLocationPermissions(context)) {
+ LogUtil.i("LocationHelper.canGetLocation", "no location permissions.");
+ return false;
+ }
+
+ // Ensure that both system location setting is on and google location services are enabled.
+ if (!GoogleLocationSettingHelper.isGoogleLocationServicesEnabled(context)
+ || !GoogleLocationSettingHelper.isSystemLocationSettingEnabled(context)) {
+ LogUtil.i("LocationHelper.canGetLocation", "location service is disabled.");
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Whether the location is valid. We consider it valid if it was recorded within the specified
+ * time threshold of the present and has an accuracy less than the specified distance threshold.
+ *
+ * @param location The location to determine the validity of.
+ * @return {@code true} if the location is valid, and {@code false} otherwise.
+ */
+ static boolean isValidLocation(Location location) {
+ if (location != null) {
+ long locationTimeMs = location.getTime();
+ long elapsedTimeMs = System.currentTimeMillis() - locationTimeMs;
+ if (elapsedTimeMs > LAST_UPDATE_THRESHOLD_MS) {
+ LogUtil.i("LocationHelper.isValidLocation", "stale location, age: " + elapsedTimeMs);
+ return false;
+ }
+ if (location.getAccuracy() > LOCATION_ACCURACY_THRESHOLD_METERS) {
+ LogUtil.i("LocationHelper.isValidLocation", "poor accuracy: " + location.getAccuracy());
+ return false;
+ }
+ return true;
+ }
+ LogUtil.i("LocationHelper.isValidLocation", "no location");
+ return false;
+ }
+
+ @MainThread
+ void addLocationListener(LocationListener listener) {
+ Assert.isMainThread();
+ listeners.add(listener);
+ }
+
+ @MainThread
+ void removeLocationListener(LocationListener listener) {
+ Assert.isMainThread();
+ listeners.remove(listener);
+ }
+
+ @MainThread
+ void close() {
+ Assert.isMainThread();
+ LogUtil.enterBlock("LocationHelper.close");
+ listeners.clear();
+
+ if (locationHelperInternal != null) {
+ locationHelperInternal.close();
+ }
+ }
+
+ @MainThread
+ void onLocationChanged(Location location, boolean isConnected) {
+ Assert.isMainThread();
+ LogUtil.i("LocationHelper.onLocationChanged", "location: " + location);
+
+ for (LocationListener listener : listeners) {
+ listener.onLocationChanged(location);
+ }
+ }
+
+ /**
+ * This class contains all the asynchronous callbacks. It only posts location changes back to the
+ * outer class on the main thread.
+ */
+ private class LocationHelperInternal
+ implements ConnectionCallbacks, OnConnectionFailedListener, LocationListener {
+
+ private final GoogleApiClient apiClient;
+ private final ConnectivityManager connectivityManager;
+ private final Handler mainThreadHandler = new Handler();
+
+ @MainThread
+ LocationHelperInternal(Context context) {
+ Assert.isMainThread();
+ apiClient =
+ new GoogleApiClient.Builder(context)
+ .addApi(LocationServices.API)
+ .addConnectionCallbacks(this)
+ .addOnConnectionFailedListener(this)
+ .build();
+
+ LogUtil.i("LocationHelperInternal", "Connecting to location service...");
+ apiClient.connect();
+
+ connectivityManager =
+ (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ }
+
+ void close() {
+ if (apiClient.isConnected()) {
+ LogUtil.i("LocationHelperInternal", "disconnecting");
+ LocationServices.FusedLocationApi.removeLocationUpdates(apiClient, this);
+ apiClient.disconnect();
+ }
+ }
+
+ @Override
+ public void onConnected(Bundle bundle) {
+ LogUtil.enterBlock("LocationHelperInternal.onConnected");
+ LocationRequest locationRequest =
+ LocationRequest.create()
+ .setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY)
+ .setInterval(MIN_UPDATE_INTERVAL_MS)
+ .setFastestInterval(MIN_UPDATE_INTERVAL_MS);
+
+ LocationServices.FusedLocationApi.requestLocationUpdates(apiClient, locationRequest, this)
+ .setResultCallback(
+ new ResultCallback<Status>() {
+ @Override
+ public void onResult(Status status) {
+ if (status.getStatus().isSuccess()) {
+ onLocationChanged(LocationServices.FusedLocationApi.getLastLocation(apiClient));
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onConnectionSuspended(int i) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onConnectionFailed(ConnectionResult result) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onLocationChanged(Location location) {
+ // Post new location on main thread
+ mainThreadHandler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ LocationHelper.this.onLocationChanged(location, isConnected());
+ }
+ });
+ }
+
+ /** @return Whether the phone is connected to data. */
+ private boolean isConnected() {
+ if (connectivityManager == null) {
+ return false;
+ }
+ NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
+ return networkInfo != null && networkInfo.isConnectedOrConnecting();
+ }
+ }
+}
diff --git a/java/com/android/incallui/calllocation/impl/LocationPresenter.java b/java/com/android/incallui/calllocation/impl/LocationPresenter.java
new file mode 100644
index 000000000..a56fd3b3c
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/LocationPresenter.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.location.Location;
+import android.os.AsyncTask;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.baseui.Presenter;
+import com.android.incallui.baseui.Ui;
+import com.google.android.gms.location.LocationListener;
+import java.lang.ref.WeakReference;
+import java.util.Objects;
+
+/**
+ * Presenter for the {@code LocationFragment}.
+ *
+ * <p>Performs lookup for the address and map image to show.
+ */
+public class LocationPresenter extends Presenter<LocationPresenter.LocationUi>
+ implements LocationListener {
+
+ private Location mLastLocation;
+ private AsyncTask mDownloadMapTask;
+ private AsyncTask mReverseGeocodeTask;
+
+ LocationPresenter() {}
+
+ @Override
+ public void onUiReady(LocationUi ui) {
+ LogUtil.i("LocationPresenter.onUiReady", "");
+ super.onUiReady(ui);
+ updateLocation(mLastLocation, true);
+ }
+
+ @Override
+ public void onUiUnready(LocationUi ui) {
+ LogUtil.i("LocationPresenter.onUiUnready", "");
+ super.onUiUnready(ui);
+
+ if (mDownloadMapTask != null) {
+ mDownloadMapTask.cancel(true);
+ }
+ if (mReverseGeocodeTask != null) {
+ mReverseGeocodeTask.cancel(true);
+ }
+ }
+
+ @Override
+ public void onLocationChanged(Location location) {
+ LogUtil.i("LocationPresenter.onLocationChanged", "");
+ updateLocation(location, false);
+ }
+
+ private void updateLocation(Location location, boolean forceUpdate) {
+ LogUtil.i("LocationPresenter.updateLocation", "location: " + location);
+ if (forceUpdate || !Objects.equals(mLastLocation, location)) {
+ mLastLocation = location;
+ if (LocationHelper.isValidLocation(location)) {
+ LocationUi ui = getUi();
+ mDownloadMapTask = new DownloadMapImageTask(new WeakReference<>(ui)).execute(location);
+ mReverseGeocodeTask = new ReverseGeocodeTask(new WeakReference<>(ui)).execute(location);
+ if (ui != null) {
+ ui.setLocation(location);
+ } else {
+ LogUtil.i("LocationPresenter.updateLocation", "no Ui");
+ }
+ }
+ }
+ }
+
+ /** UI interface */
+ public interface LocationUi extends Ui {
+
+ void setAddress(String address);
+
+ void setMap(Drawable mapImage);
+
+ void setLocation(Location location);
+
+ Context getContext();
+ }
+}
diff --git a/java/com/android/incallui/calllocation/impl/LocationUrlBuilder.java b/java/com/android/incallui/calllocation/impl/LocationUrlBuilder.java
new file mode 100644
index 000000000..a57bdf613
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/LocationUrlBuilder.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.location.Location;
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import java.util.Locale;
+
+class LocationUrlBuilder {
+
+ // Static Map API path constants.
+ private static final String HTTPS_SCHEME = "https";
+ private static final String MAPS_API_DOMAIN = "maps.googleapis.com";
+ private static final String MAPS_PATH = "maps";
+ private static final String API_PATH = "api";
+ private static final String STATIC_MAP_PATH = "staticmap";
+ private static final String GEOCODE_PATH = "geocode";
+ private static final String GEOCODE_OUTPUT_TYPE = "json";
+
+ // Static Map API parameter constants.
+ private static final String KEY_PARAM_KEY = "key";
+ private static final String CENTER_PARAM_KEY = "center";
+ private static final String ZOOM_PARAM_KEY = "zoom";
+ private static final String SCALE_PARAM_KEY = "scale";
+ private static final String SIZE_PARAM_KEY = "size";
+ private static final String MARKERS_PARAM_KEY = "markers";
+
+ private static final String ZOOM_PARAM_VALUE = Integer.toString(16);
+
+ private static final String LAT_LONG_DELIMITER = ",";
+
+ private static final String MARKER_DELIMITER = "|";
+ private static final String MARKER_STYLE_DELIMITER = ":";
+ private static final String MARKER_STYLE_COLOR = "color";
+ private static final String MARKER_STYLE_COLOR_RED = "red";
+
+ private static final String LAT_LNG_PARAM_KEY = "latlng";
+
+ private static final String ANDROID_API_KEY_VALUE = "AIzaSyAXdDnif6B7sBYxU8hzw9qAp3pRPVHs060";
+ private static final String BROWSER_API_KEY_VALUE = "AIzaSyBfLlvWYndiQ3RFEHli65qGQH36QIxdyCI";
+
+ /**
+ * Generates the URL to a static map image for the given location.
+ *
+ * <p>This image has the following characteristics:
+ *
+ * <p>- It is centered at the given latitude and longitutde. - It is scaled according to the
+ * device's pixel density. - There is a red marker at the given latitude and longitude.
+ *
+ * <p>Source: https://developers.google.com/maps/documentation/staticmaps/
+ *
+ * @param contxt The context.
+ * @param Location A location.
+ * @return The URL of a static map image url of the given location.
+ */
+ public static String getStaticMapUrl(Context context, Location location) {
+ final Uri.Builder builder = new Uri.Builder();
+ Resources res = context.getResources();
+ String size =
+ res.getDimensionPixelSize(R.dimen.location_map_width)
+ + "x"
+ + res.getDimensionPixelSize(R.dimen.location_map_height);
+
+ builder
+ .scheme(HTTPS_SCHEME)
+ .authority(MAPS_API_DOMAIN)
+ .appendPath(MAPS_PATH)
+ .appendPath(API_PATH)
+ .appendPath(STATIC_MAP_PATH)
+ .appendQueryParameter(CENTER_PARAM_KEY, getFormattedLatLng(location))
+ .appendQueryParameter(ZOOM_PARAM_KEY, ZOOM_PARAM_VALUE)
+ .appendQueryParameter(SIZE_PARAM_KEY, size)
+ .appendQueryParameter(SCALE_PARAM_KEY, Float.toString(res.getDisplayMetrics().density))
+ .appendQueryParameter(MARKERS_PARAM_KEY, getMarkerUrlParamValue(location))
+ .appendQueryParameter(KEY_PARAM_KEY, ANDROID_API_KEY_VALUE);
+
+ return builder.build().toString();
+ }
+
+ /**
+ * Generates the URL for a request to reverse geocode the given location.
+ *
+ * <p>Source: https://developers.google.com/maps/documentation/geocoding/#ReverseGeocoding
+ *
+ * @param Location A location.
+ */
+ public static String getReverseGeocodeUrl(Location location) {
+ final Uri.Builder builder = new Uri.Builder();
+
+ builder
+ .scheme(HTTPS_SCHEME)
+ .authority(MAPS_API_DOMAIN)
+ .appendPath(MAPS_PATH)
+ .appendPath(API_PATH)
+ .appendPath(GEOCODE_PATH)
+ .appendPath(GEOCODE_OUTPUT_TYPE)
+ .appendQueryParameter(LAT_LNG_PARAM_KEY, getFormattedLatLng(location))
+ .appendQueryParameter(KEY_PARAM_KEY, BROWSER_API_KEY_VALUE);
+
+ return builder.build().toString();
+ }
+
+ public static Intent getShowMapIntent(
+ Location location, @Nullable CharSequence addressLine1, @Nullable CharSequence addressLine2) {
+
+ String latLong = getFormattedLatLng(location);
+ String url = String.format(Locale.US, "geo: %s?q=%s", latLong, latLong);
+
+ // Add a map label
+ if (addressLine1 != null) {
+ if (addressLine2 != null) {
+ url +=
+ String.format(Locale.US, "(%s, %s)", addressLine1.toString(), addressLine2.toString());
+ } else {
+ url += String.format(Locale.US, "(%s)", addressLine1.toString());
+ }
+ } else {
+ // TODO: i18n
+ url +=
+ String.format(
+ Locale.US,
+ "(Latitude: %f, Longitude: %f)",
+ location.getLatitude(),
+ location.getLongitude());
+ }
+
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+ intent.setPackage("com.google.android.apps.maps");
+ return intent;
+ }
+
+ /**
+ * Returns a comma-separated latitude and longitude pair, formatted for use as a URL parameter
+ * value.
+ *
+ * @param location A location.
+ * @return The comma-separated latitude and longitude pair of that location.
+ */
+ @VisibleForTesting
+ static String getFormattedLatLng(Location location) {
+ return location.getLatitude() + LAT_LONG_DELIMITER + location.getLongitude();
+ }
+
+ /**
+ * Returns the URL parameter value for the marker, specifying its style and position.
+ *
+ * @param location A location.
+ * @return The URL parameter value for the marker.
+ */
+ @VisibleForTesting
+ static String getMarkerUrlParamValue(Location location) {
+ return MARKER_STYLE_COLOR
+ + MARKER_STYLE_DELIMITER
+ + MARKER_STYLE_COLOR_RED
+ + MARKER_DELIMITER
+ + getFormattedLatLng(location);
+ }
+}
diff --git a/java/com/android/incallui/calllocation/impl/ReverseGeocodeTask.java b/java/com/android/incallui/calllocation/impl/ReverseGeocodeTask.java
new file mode 100644
index 000000000..eb5957b05
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/ReverseGeocodeTask.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import android.location.Location;
+import android.net.TrafficStats;
+import android.os.AsyncTask;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.calllocation.impl.LocationPresenter.LocationUi;
+import java.lang.ref.WeakReference;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+class ReverseGeocodeTask extends AsyncTask<Location, Void, String> {
+
+ // Below are the JSON keys for the reverse geocode response.
+ // Source: https://developers.google.com/maps/documentation/geocoding/#ReverseGeocoding
+ private static final String JSON_KEY_RESULTS = "results";
+ private static final String JSON_KEY_ADDRESS = "formatted_address";
+ private static final String JSON_KEY_ADDRESS_COMPONENTS = "address_components";
+ private static final String JSON_KEY_PREMISE = "premise";
+ private static final String JSON_KEY_TYPES = "types";
+ private static final String JSON_KEY_LONG_NAME = "long_name";
+ private static final String JSON_KEY_SHORT_NAME = "short_name";
+
+ private WeakReference<LocationUi> mUiReference;
+
+ public ReverseGeocodeTask(WeakReference<LocationUi> uiReference) {
+ mUiReference = uiReference;
+ }
+
+ @Override
+ protected String doInBackground(Location... locations) {
+ LocationUi ui = mUiReference.get();
+ if (ui == null) {
+ return null;
+ }
+ if (locations == null || locations.length == 0) {
+ LogUtil.e("ReverseGeocodeTask.onLocationChanged", "No location provided");
+ return null;
+ }
+
+ try {
+ String address = null;
+ String url = LocationUrlBuilder.getReverseGeocodeUrl(locations[0]);
+
+ TrafficStats.setThreadStatsTag(TrafficStatsTags.REVERSE_GEOCODE_TAG);
+ String jsonResponse = HttpFetcher.getRequestAsString(ui.getContext(), url);
+
+ // Parse the JSON response for the formatted address of the first result.
+ JSONObject responseObject = new JSONObject(jsonResponse);
+ if (responseObject != null) {
+ JSONArray results = responseObject.optJSONArray(JSON_KEY_RESULTS);
+ if (results != null && results.length() > 0) {
+ JSONObject topResult = results.optJSONObject(0);
+ if (topResult != null) {
+ address = topResult.getString(JSON_KEY_ADDRESS);
+
+ // Strip off the Premise component from the address, if present.
+ JSONArray components = topResult.optJSONArray(JSON_KEY_ADDRESS_COMPONENTS);
+ if (components != null) {
+ boolean stripped = false;
+ for (int i = 0; !stripped && i < components.length(); i++) {
+ JSONObject component = components.optJSONObject(i);
+ JSONArray types = component.optJSONArray(JSON_KEY_TYPES);
+ if (types != null) {
+ for (int j = 0; !stripped && j < types.length(); j++) {
+ if (JSON_KEY_PREMISE.equals(types.getString(j))) {
+ String premise = null;
+ if (component.has(JSON_KEY_SHORT_NAME)
+ && address.startsWith(component.getString(JSON_KEY_SHORT_NAME))) {
+ premise = component.getString(JSON_KEY_SHORT_NAME);
+ } else if (component.has(JSON_KEY_LONG_NAME)
+ && address.startsWith(component.getString(JSON_KEY_LONG_NAME))) {
+ premise = component.getString(JSON_KEY_SHORT_NAME);
+ }
+ if (premise != null) {
+ int index = address.indexOf(',', premise.length());
+ if (index > 0 && index < address.length()) {
+ address = address.substring(index + 1).trim();
+ }
+ stripped = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Strip off the country, if its USA. Note: unfortunately the country in the formatted
+ // address field doesn't match the country in the address component fields (USA != US)
+ // so we can't easily strip off the country for all cases, thus this hack.
+ if (address.endsWith(", USA")) {
+ address = address.substring(0, address.length() - 5);
+ }
+ }
+ }
+ }
+
+ return address;
+ } catch (AuthException ex) {
+ LogUtil.e("ReverseGeocodeTask.onLocationChanged", "AuthException", ex);
+ return null;
+ } catch (JSONException ex) {
+ LogUtil.e("ReverseGeocodeTask.onLocationChanged", "JSONException", ex);
+ return null;
+ } catch (Exception ex) {
+ LogUtil.e("ReverseGeocodeTask.onLocationChanged", "Exception!!!", ex);
+ return null;
+ } finally {
+ TrafficStats.clearThreadStatsTag();
+ }
+ }
+
+ @Override
+ protected void onPostExecute(String address) {
+ LocationUi ui = mUiReference.get();
+ if (ui == null) {
+ return;
+ }
+
+ try {
+ ui.setAddress(address);
+ } catch (Exception ex) {
+ LogUtil.e("ReverseGeocodeTask.onPostExecute", "Exception!!!", ex);
+ }
+ }
+}
diff --git a/java/com/android/incallui/calllocation/impl/TrafficStatsTags.java b/java/com/android/incallui/calllocation/impl/TrafficStatsTags.java
new file mode 100644
index 000000000..02cc2e083
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/TrafficStatsTags.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+/** Constants used for logging */
+public class TrafficStatsTags {
+
+ /**
+ * Must be greater than {@link com.android.contacts.common.util.TrafficStatsTags#TAG_MAX}, to
+ * respect the namespace of the tags in ContactsCommon.
+ */
+ public static final int DOWNLOAD_LOCATION_MAP_TAG = 0xd000;
+
+ public static final int REVERSE_GEOCODE_TAG = 0xd001;
+}
diff --git a/java/com/android/incallui/calllocation/impl/res/layout/location_fragment.xml b/java/com/android/incallui/calllocation/impl/res/layout/location_fragment.xml
new file mode 100644
index 000000000..a6bd07542
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/res/layout/location_fragment.xml
@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+~ Copyright (C) 2015 The Android Open Source Project
+~
+~ Licensed under the Apache License, Version 2.0 (the "License");
+~ you may not use this file except in compliance with the License.
+~ You may obtain a copy of the License at
+~
+~ http://www.apache.org/licenses/LICENSE-2.0
+~
+~ Unless required by applicable law or agreed to in writing, software
+~ distributed under the License is distributed on an "AS IS" BASIS,
+~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+~ See the License for the specific language governing permissions and
+~ limitations under the License
+-->
+
+<ViewAnimator xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/location_view_animator"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:layout_marginBottom="16dp"
+ android:background="@android:color/white"
+ android:elevation="2dp"
+ android:inAnimation="@android:anim/fade_in"
+ android:measureAllChildren="true"
+ android:outAnimation="@android:anim/fade_out">
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/location_loading_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:orientation="vertical">
+
+ <ProgressBar
+ android:id="@+id/location_loading_spinner"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="28dp"
+ android:layout_marginBottom="12dp"
+ android:layout_gravity="center_horizontal"/>
+
+ <TextView
+ android:id="@+id/location_loading_text"
+ style="@style/LocationLoadingTextStyle"
+ android:layout_width="match_parent"
+ android:layout_height="24sp"
+ android:layout_marginBottom="20dp"
+ android:layout_marginStart="24dp"
+ android:layout_marginEnd="24dp"
+ android:gravity="center"
+ android:text="@string/location_loading"/>
+
+ </LinearLayout>
+
+ <GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/location_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:columnCount="2"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/location_address_title"
+ style="@style/LocationAddressTitleTextStyle"
+ android:layout_width="0dp"
+ android:layout_height="20sp"
+ android:layout_marginTop="16dp"
+ android:layout_marginBottom="4dp"
+ android:layout_marginStart="16dp"
+ android:layout_columnWeight="1"
+ android:text="@string/location_title"/>
+
+ <ImageView
+ android:id="@+id/location_map"
+ android:layout_width="@dimen/location_map_width"
+ android:layout_height="@dimen/location_map_height"
+ android:layout_margin="16dp"
+ android:layout_gravity="end|center_vertical"
+ android:layout_rowSpan="4"
+ android:contentDescription="@string/location_map_description"
+ android:scaleType="centerCrop"
+ android:visibility="invisible"
+ tools:src="?android:colorPrimaryDark"
+ tools:visibility="visible"/>
+
+ <TextView
+ android:id="@+id/address_line_one"
+ style="@style/LocationAddressTextStyle"
+ android:layout_width="0dp"
+ android:layout_height="24sp"
+ android:layout_marginStart="16dp"
+ android:layout_columnWeight="1"
+ android:ellipsize="end"
+ android:lines="1"
+ android:visibility="invisible"
+ tools:text="1600 Amphitheatre Pkwy And a bit"
+ tools:visibility="visible"/>
+
+ <TextView
+ android:id="@+id/address_line_two"
+ style="@style/LocationAddressTextStyle"
+ android:layout_width="0dp"
+ android:layout_height="24sp"
+ android:layout_marginStart="16dp"
+ android:layout_columnWeight="1"
+ android:ellipsize="end"
+ android:lines="1"
+ android:visibility="invisible"
+ tools:text="Mountain View, CA 94043"
+ tools:visibility="visible"/>
+
+ <TextView
+ android:id="@+id/lat_long_line"
+ style="@style/LocationLatLongTextStyle"
+ android:layout_width="0dp"
+ android:layout_height="24sp"
+ android:layout_marginBottom="12dp"
+ android:layout_marginStart="16dp"
+ android:layout_columnWeight="1"
+ android:ellipsize="end"
+ android:lines="1"
+ android:visibility="invisible"
+ tools:text="Lat: 37.421719, Long: -122.085297"
+ tools:visibility="visible"/>
+
+ </GridLayout>
+
+</ViewAnimator>
diff --git a/java/com/android/incallui/calllocation/impl/res/values/dimens.xml b/java/com/android/incallui/calllocation/impl/res/values/dimens.xml
new file mode 100644
index 000000000..1f4181607
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/res/values/dimens.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2014 Google Inc. All Rights Reserved. -->
+<resources>
+ <dimen name="location_map_width">92dp</dimen>
+ <dimen name="location_map_height">92dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/calllocation/impl/res/values/strings.xml b/java/com/android/incallui/calllocation/impl/res/values/strings.xml
new file mode 100644
index 000000000..ef7c1624c
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/res/values/strings.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+ <!-- Description for location map shown during emergency calls. [CHAR LIMIT=NONE] -->
+ <string name="location_map_description">Emergency Location Map</string>
+
+ <!-- Label for the address and map shown during emergency calls. [CHAR LIMIT=20] -->
+ <string name="location_title">You are here</string>
+
+ <string name="lat_long_format"><xliff:g id="latitude">%f</xliff:g>, <xliff:g id="longitude">%f</xliff:g></string>
+
+ <!-- Progress indicator loading text. [CHAR LIMIT=20] -->
+ <string name="location_loading">Finding your location</string>
+
+</resources>
diff --git a/java/com/android/incallui/calllocation/impl/res/values/styles.xml b/java/com/android/incallui/calllocation/impl/res/values/styles.xml
new file mode 100644
index 000000000..866a4edb6
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/res/values/styles.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2015 Google Inc. All Rights Reserved. -->
+<resources>
+
+ <style name="LocationAddressTitleTextStyle">
+ <item name="android:textSize">14sp</item>
+ <item name="android:textColor">#dd000000</item>
+ <item name="android:fontFamily">sans-serif-medium</item>
+ </style>
+
+ <style name="LocationAddressTextStyle">
+ <item name="android:textSize">16sp</item>
+ <item name="android:textColor">#dd000000</item>
+ <item name="android:fontFamily">sans-serif</item>
+ </style>
+
+ <style name="LocationLatLongTextStyle">
+ <item name="android:textSize">14sp</item>
+ <item name="android:textColor">#88000000</item>
+ <item name="android:fontFamily">sans-serif</item>
+ </style>
+
+ <style name="LocationLoadingTextStyle">
+ <item name="android:textSize">14sp</item>
+ <item name="android:textColor">#dd000000</item>
+ <item name="android:fontFamily">sans-serif</item>
+ </style>
+</resources>
diff --git a/java/com/android/incallui/calllocation/stub/StubCallLocationModule.java b/java/com/android/incallui/calllocation/stub/StubCallLocationModule.java
new file mode 100644
index 000000000..fc198c724
--- /dev/null
+++ b/java/com/android/incallui/calllocation/stub/StubCallLocationModule.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.stub;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.v4.app.Fragment;
+import com.android.dialer.common.Assert;
+import com.android.incallui.calllocation.CallLocation;
+import dagger.Binds;
+import dagger.Module;
+import javax.inject.Inject;
+
+/** This module provides an instance of call location. */
+@Module
+public abstract class StubCallLocationModule {
+
+ @Binds
+ public abstract CallLocation bindCallLocation(StubCallLocation callLocation);
+
+ static public class StubCallLocation implements CallLocation {
+ @Inject
+ public StubCallLocation() {}
+
+ @Override
+ public boolean canGetLocation(@NonNull Context context) {
+ return false;
+ }
+
+ @Override
+ @NonNull
+ public Fragment getLocationFragment(@NonNull Context context) {
+ return null;
+ }
+
+ @Override
+ public void close() {
+ }
+ }
+}
diff --git a/java/com/android/incallui/commontheme/res/anim/blinking.xml b/java/com/android/incallui/commontheme/res/anim/blinking.xml
new file mode 100644
index 000000000..aaec18c56
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/anim/blinking.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <alpha
+ android:duration="@android:integer/config_mediumAnimTime"
+ android:fromAlpha="0.0"
+ android:interpolator="@android:anim/linear_interpolator"
+ android:repeatCount="infinite"
+ android:repeatMode="reverse"
+ android:toAlpha="1.0"/>
+</set> \ No newline at end of file
diff --git a/java/com/android/incallui/contactgrid/BottomRow.java b/java/com/android/incallui/contactgrid/BottomRow.java
index aaf7e8214..6ddce4533 100644
--- a/java/com/android/incallui/contactgrid/BottomRow.java
+++ b/java/com/android/incallui/contactgrid/BottomRow.java
@@ -31,10 +31,10 @@ import com.android.incallui.incall.protocol.PrimaryInfo;
* Gets the content of the bottom row. For example:
*
* <ul>
- * <li>Mobile +1 (650) 253-0000
- * <li>[HD icon] 00:15
- * <li>Call ended
- * <li>Hanging up
+ * <li>Mobile +1 (650) 253-0000
+ * <li>[HD attempting icon]/[HD icon] 00:15
+ * <li>Call ended
+ * <li>Hanging up
* </ul>
*/
public class BottomRow {
@@ -45,6 +45,7 @@ public class BottomRow {
@Nullable public final CharSequence label;
public final boolean isTimerVisible;
public final boolean isWorkIconVisible;
+ public final boolean isHdAttemptinIconVisible;
public final boolean isHdIconVisible;
public final boolean isForwardIconVisible;
public final boolean isSpamIconVisible;
@@ -54,6 +55,7 @@ public class BottomRow {
@Nullable CharSequence label,
boolean isTimerVisible,
boolean isWorkIconVisible,
+ boolean isHdAttemptinIconVisible,
boolean isHdIconVisible,
boolean isForwardIconVisible,
boolean isSpamIconVisible,
@@ -61,6 +63,7 @@ public class BottomRow {
this.label = label;
this.isTimerVisible = isTimerVisible;
this.isWorkIconVisible = isWorkIconVisible;
+ this.isHdAttemptinIconVisible = isHdAttemptinIconVisible;
this.isHdIconVisible = isHdIconVisible;
this.isForwardIconVisible = isForwardIconVisible;
this.isSpamIconVisible = isSpamIconVisible;
@@ -76,6 +79,7 @@ public class BottomRow {
boolean isForwardIconVisible = state.isForwardedNumber;
boolean isWorkIconVisible = state.isWorkCall;
boolean isHdIconVisible = state.isHdAudioCall && !isForwardIconVisible;
+ boolean isHdAttemptingIconVisible = state.isHdAttempting;
boolean isSpamIconVisible = false;
boolean shouldPopulateAccessibilityEvent = true;
@@ -110,6 +114,7 @@ public class BottomRow {
label,
isTimerVisible,
isWorkIconVisible,
+ isHdAttemptingIconVisible,
isHdIconVisible,
isForwardIconVisible,
isSpamIconVisible,
diff --git a/java/com/android/incallui/contactgrid/ContactGridManager.java b/java/com/android/incallui/contactgrid/ContactGridManager.java
index 81c225163..a0b687c2d 100644
--- a/java/com/android/incallui/contactgrid/ContactGridManager.java
+++ b/java/com/android/incallui/contactgrid/ContactGridManager.java
@@ -18,10 +18,14 @@ package com.android.incallui.contactgrid;
import android.content.Context;
import android.os.SystemClock;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.telecom.TelecomManager;
import android.text.TextUtils;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
import android.widget.Chronometer;
import android.widget.ImageView;
import android.widget.TextView;
@@ -56,7 +60,7 @@ public class ContactGridManager {
@Nullable private ImageView avatarImageView;
// Row 2: Mobile +1 (650) 253-0000
- // Row 2: [HD icon] 00:15
+ // Row 2: [HD attempting icon]/[HD icon] 00:15
// Row 2: Call ended
// Row 2: Hanging up
// Row 2: [Alert sign] Suspected spam caller
@@ -77,7 +81,6 @@ public class ContactGridManager {
private PrimaryCallState primaryCallState = PrimaryCallState.createEmptyPrimaryCallState();
private final LetterTileDrawable letterTile;
-
public ContactGridManager(
View view, @Nullable ImageView avatarImageView, int avatarSize, boolean showAnonymousAvatar) {
context = view.getContext();
@@ -227,6 +230,24 @@ public class ContactGridManager {
}
/**
+ * Returns the appropriate LetterTileDrawable.TYPE_ based on a given call state.
+ *
+ * <p>If no special state is detected, yields TYPE_DEFAULT.
+ */
+ private static @LetterTileDrawable.ContactType int getContactTypeForPrimaryCallState(
+ @NonNull PrimaryCallState callState, @NonNull PrimaryInfo primaryInfo) {
+ if (callState.isVoiceMailNumber) {
+ return LetterTileDrawable.TYPE_VOICEMAIL;
+ } else if (callState.isBusinessNumber) {
+ return LetterTileDrawable.TYPE_BUSINESS;
+ } else if (primaryInfo.numberPresentation == TelecomManager.PRESENTATION_RESTRICTED) {
+ return LetterTileDrawable.TYPE_GENERIC_AVATAR;
+ } else {
+ return LetterTileDrawable.TYPE_DEFAULT;
+ }
+ }
+
+ /**
* Updates row 1. For example:
*
* <ul>
@@ -255,7 +276,7 @@ public class ContactGridManager {
if (avatarImageView != null) {
if (hideAvatar) {
avatarImageView.setVisibility(View.GONE);
- } else if (avatarImageView != null && avatarSize > 0 && updateAvatarVisibility()) {
+ } else if (avatarSize > 0 && updateAvatarVisibility()) {
boolean hasPhoto =
primaryInfo.photo != null && primaryInfo.photoType == ContactPhotoType.CONTACT;
// Contact has a photo, don't render a letter tile.
@@ -265,27 +286,29 @@ public class ContactGridManager {
context, primaryInfo.photo, avatarSize, avatarSize));
// Contact has a name, that isn't a number.
} else {
- int contactType =
- primaryCallState.isVoiceMailNumber
- ? LetterTileDrawable.TYPE_VOICEMAIL
- : LetterTileDrawable.TYPE_DEFAULT;
letterTile.setCanonicalDialerLetterTileDetails(
primaryInfo.name,
primaryInfo.contactInfoLookupKey,
LetterTileDrawable.SHAPE_CIRCLE,
- contactType);
+ getContactTypeForPrimaryCallState(primaryCallState, primaryInfo));
+
+ // By invalidating the avatarImageView we force a redraw of the letter tile.
+ // This is required to properly display the updated letter tile iconography based on the
+ // contact type, because the background drawable reference cached in the view, and the
+ // view is not aware of the mutations made to the background.
+ avatarImageView.invalidate();
avatarImageView.setBackground(letterTile);
+ }
}
}
}
- }
/**
* Updates row 2. For example:
*
* <ul>
* <li>Mobile +1 (650) 253-0000
- * <li>[HD icon] 00:15
+ * <li>[HD attempting icon]/[HD icon] 00:15
* <li>Call ended
* <li>Hanging up
* </ul>
@@ -296,7 +319,15 @@ public class ContactGridManager {
bottomTextView.setText(info.label);
bottomTextView.setAllCaps(info.isSpamIconVisible);
workIconImageView.setVisibility(info.isWorkIconVisible ? View.VISIBLE : View.GONE);
- hdIconImageView.setVisibility(info.isHdIconVisible ? View.VISIBLE : View.GONE);
+ boolean wasHdIconVisible = hdIconImageView.getVisibility() == View.VISIBLE;
+ if (!wasHdIconVisible && info.isHdAttemptinIconVisible) {
+ Animation animation = AnimationUtils.loadAnimation(context, R.anim.blinking);
+ hdIconImageView.startAnimation(animation);
+ } else if (wasHdIconVisible && !info.isHdAttemptinIconVisible) {
+ hdIconImageView.clearAnimation();
+ }
+ hdIconImageView.setVisibility(
+ info.isHdIconVisible || info.isHdAttemptinIconVisible ? View.VISIBLE : View.GONE);
forwardIconImageView.setVisibility(info.isForwardIconVisible ? View.VISIBLE : View.GONE);
spamIconImageView.setVisibility(info.isSpamIconVisible ? View.VISIBLE : View.GONE);
diff --git a/java/com/android/incallui/contactgrid/TopRow.java b/java/com/android/incallui/contactgrid/TopRow.java
index a340fd0a0..ecd5eea64 100644
--- a/java/com/android/incallui/contactgrid/TopRow.java
+++ b/java/com/android/incallui/contactgrid/TopRow.java
@@ -21,10 +21,10 @@ import android.graphics.drawable.Drawable;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.android.dialer.common.Assert;
-import com.android.incallui.call.DialerCall;
import com.android.incallui.call.DialerCall.State;
import com.android.incallui.call.VideoUtils;
import com.android.incallui.incall.protocol.PrimaryCallState;
+import com.android.incallui.videotech.VideoTech;
/**
* Gets the content of the top row. For example:
@@ -95,7 +95,7 @@ public class TopRow {
}
private static CharSequence getLabelForIncoming(Context context, PrimaryCallState state) {
- if (VideoUtils.isVideoCall(state.videoState)) {
+ if (state.isVideoCall) {
return getLabelForIncomingVideo(context, state.isWifi);
} else if (state.isWifi && !TextUtils.isEmpty(state.connectionLabel)) {
return state.connectionLabel;
@@ -120,7 +120,7 @@ public class TopRow {
if (!TextUtils.isEmpty(state.connectionLabel) && !state.isWifi) {
return context.getString(R.string.incall_calling_via_template, state.connectionLabel);
} else {
- if (VideoUtils.isVideoCall(state.videoState)) {
+ if (state.isVideoCall) {
if (state.isWifi) {
return context.getString(R.string.incall_wifi_video_call_requesting);
} else {
@@ -144,18 +144,18 @@ public class TopRow {
private static CharSequence getLabelForVideoRequest(Context context, PrimaryCallState state) {
switch (state.sessionModificationState) {
- case DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE:
+ case VideoTech.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE:
return context.getString(R.string.incall_video_call_requesting);
- case DialerCall.SESSION_MODIFICATION_STATE_REQUEST_FAILED:
- case DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED:
+ case VideoTech.SESSION_MODIFICATION_STATE_REQUEST_FAILED:
+ case VideoTech.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED:
return context.getString(R.string.incall_video_call_request_failed);
- case DialerCall.SESSION_MODIFICATION_STATE_REQUEST_REJECTED:
+ case VideoTech.SESSION_MODIFICATION_STATE_REQUEST_REJECTED:
return context.getString(R.string.incall_video_call_request_rejected);
- case DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT:
+ case VideoTech.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT:
return context.getString(R.string.incall_video_call_request_timed_out);
- case DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST:
+ case VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST:
return getLabelForIncomingVideo(context, state.isWifi);
- case DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST:
+ case VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST:
default:
Assert.fail();
return null;
diff --git a/java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_bottom_row.xml b/java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_bottom_row.xml
index 3900be556..b7a3fe7d4 100644
--- a/java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_bottom_row.xml
+++ b/java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_bottom_row.xml
@@ -4,8 +4,8 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:orientation="horizontal"
android:gravity="center_horizontal"
+ android:orientation="horizontal"
tools:showIn="@layout/incall_contact_grid">
<ImageView
android:id="@id/contactgrid_workIcon"
diff --git a/java/com/android/incallui/hold/res/layout/incall_on_hold_banner.xml b/java/com/android/incallui/hold/res/layout/incall_on_hold_banner.xml
index c213af5da..6128ae585 100644
--- a/java/com/android/incallui/hold/res/layout/incall_on_hold_banner.xml
+++ b/java/com/android/incallui/hold/res/layout/incall_on_hold_banner.xml
@@ -39,7 +39,6 @@
style="@style/Dialer.Incall.TextAppearance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:textAllCaps="true"
android:textColor="@android:color/white"
android:text="@string/incall_on_hold"/>
</LinearLayout>
diff --git a/java/com/android/incallui/incall/impl/AutoValue_MappedButtonConfig_MappingInfo.java b/java/com/android/incallui/incall/impl/AutoValue_MappedButtonConfig_MappingInfo.java
deleted file mode 100644
index addebc484..000000000
--- a/java/com/android/incallui/incall/impl/AutoValue_MappedButtonConfig_MappingInfo.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.incallui.incall.impl;
-
-import javax.annotation.Generated;
-
-@Generated("com.google.auto.value.processor.AutoValueProcessor")
- final class AutoValue_MappedButtonConfig_MappingInfo extends MappedButtonConfig.MappingInfo {
-
- private final int slot;
- private final int slotOrder;
- private final int conflictOrder;
-
- private AutoValue_MappedButtonConfig_MappingInfo(
- int slot,
- int slotOrder,
- int conflictOrder) {
- this.slot = slot;
- this.slotOrder = slotOrder;
- this.conflictOrder = conflictOrder;
- }
-
- @Override
- public int getSlot() {
- return slot;
- }
-
- @Override
- public int getSlotOrder() {
- return slotOrder;
- }
-
- @Override
- public int getConflictOrder() {
- return conflictOrder;
- }
-
- @Override
- public String toString() {
- return "MappingInfo{"
- + "slot=" + slot + ", "
- + "slotOrder=" + slotOrder + ", "
- + "conflictOrder=" + conflictOrder
- + "}";
- }
-
- @Override
- public boolean equals(Object o) {
- if (o == this) {
- return true;
- }
- if (o instanceof MappedButtonConfig.MappingInfo) {
- MappedButtonConfig.MappingInfo that = (MappedButtonConfig.MappingInfo) o;
- return (this.slot == that.getSlot())
- && (this.slotOrder == that.getSlotOrder())
- && (this.conflictOrder == that.getConflictOrder());
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- int h = 1;
- h *= 1000003;
- h ^= this.slot;
- h *= 1000003;
- h ^= this.slotOrder;
- h *= 1000003;
- h ^= this.conflictOrder;
- return h;
- }
-
- static final class Builder extends MappedButtonConfig.MappingInfo.Builder {
- private Integer slot;
- private Integer slotOrder;
- private Integer conflictOrder;
- Builder() {
- }
- private Builder(MappedButtonConfig.MappingInfo source) {
- this.slot = source.getSlot();
- this.slotOrder = source.getSlotOrder();
- this.conflictOrder = source.getConflictOrder();
- }
- @Override
- public MappedButtonConfig.MappingInfo.Builder setSlot(int slot) {
- this.slot = slot;
- return this;
- }
- @Override
- public MappedButtonConfig.MappingInfo.Builder setSlotOrder(int slotOrder) {
- this.slotOrder = slotOrder;
- return this;
- }
- @Override
- public MappedButtonConfig.MappingInfo.Builder setConflictOrder(int conflictOrder) {
- this.conflictOrder = conflictOrder;
- return this;
- }
- @Override
- public MappedButtonConfig.MappingInfo build() {
- String missing = "";
- if (this.slot == null) {
- missing += " slot";
- }
- if (this.slotOrder == null) {
- missing += " slotOrder";
- }
- if (this.conflictOrder == null) {
- missing += " conflictOrder";
- }
- if (!missing.isEmpty()) {
- throw new IllegalStateException("Missing required properties:" + missing);
- }
- return new AutoValue_MappedButtonConfig_MappingInfo(
- this.slot,
- this.slotOrder,
- this.conflictOrder);
- }
- }
-
-}
diff --git a/java/com/android/incallui/incall/impl/FakeDragAnimation.java b/java/com/android/incallui/incall/impl/FakeDragAnimation.java
new file mode 100644
index 000000000..c84c3c409
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/FakeDragAnimation.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.incall.impl;
+
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.support.v4.view.ViewPager;
+import android.support.v4.view.animation.FastOutSlowInInterpolator;
+
+/**
+ * An animation that controls the fake drag of a {@link ViewPager}. See {@link
+ * ViewPager#fakeDragBy(float)} for more details.
+ */
+public class FakeDragAnimation implements AnimatorUpdateListener {
+
+ /** The view to animate. */
+ private final ViewPager pager;
+
+ private final ValueAnimator animator;
+ private int oldDragPosition;
+
+ public FakeDragAnimation(ViewPager pager) {
+ this.pager = pager;
+ animator = ValueAnimator.ofInt(0, pager.getWidth());
+ animator.addUpdateListener(this);
+ animator.setInterpolator(new FastOutSlowInInterpolator());
+ animator.setDuration(600);
+ }
+
+ public void start() {
+ animator.start();
+ }
+
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ if (!pager.isFakeDragging()) {
+ pager.beginFakeDrag();
+ }
+ int dragPosition = (Integer) animation.getAnimatedValue();
+ int dragOffset = dragPosition - oldDragPosition;
+ oldDragPosition = dragPosition;
+ pager.fakeDragBy(-dragOffset);
+
+ if (animation.getAnimatedFraction() == 1) {
+ pager.endFakeDrag();
+ }
+ }
+}
diff --git a/java/com/android/incallui/incall/impl/InCallFragment.java b/java/com/android/incallui/incall/impl/InCallFragment.java
index ef8a1edd8..3f31651a0 100644
--- a/java/com/android/incallui/incall/impl/InCallFragment.java
+++ b/java/com/android/incallui/incall/impl/InCallFragment.java
@@ -213,9 +213,7 @@ public class InCallFragment extends Fragment
@Override
public void setPrimary(@NonNull PrimaryInfo primaryInfo) {
LogUtil.i("InCallFragment.setPrimary", primaryInfo.toString());
- if (adapter == null) {
- initAdapter(primaryInfo.multimediaData);
- }
+ setAdapterMedia(primaryInfo.multimediaData);
contactGridManager.setPrimary(primaryInfo);
if (primaryInfo.shouldShowLocation) {
@@ -241,9 +239,13 @@ public class InCallFragment extends Fragment
}
}
- private void initAdapter(MultimediaData multimediaData) {
- adapter = new InCallPagerAdapter(getChildFragmentManager(), multimediaData);
- pager.setAdapter(adapter);
+ private void setAdapterMedia(MultimediaData multimediaData) {
+ if (adapter == null) {
+ adapter = new InCallPagerAdapter(getChildFragmentManager(), multimediaData);
+ pager.setAdapter(adapter);
+ } else {
+ adapter.setAttachments(multimediaData);
+ }
if (adapter.getCount() > 1) {
tabLayout.setVisibility(pager.getVisibility());
@@ -251,16 +253,13 @@ public class InCallFragment extends Fragment
if (!stateRestored) {
new Handler()
.postDelayed(
- new Runnable() {
- @Override
- public void run() {
- // In order to prevent user confusion and educate the user on our UI, we animate
- // the view pager to the button grid after 2 seconds show them when the UI is
- // that they are more familiar with.
- pager.setCurrentItem(adapter.getButtonGridPosition());
- }
+ () -> {
+ // In order to prevent user confusion and educate the user on our UI, we animate
+ // the view pager to the button grid after a short period to show them where the
+ // UI that they are more familiar with is located.
+ new FakeDragAnimation(pager).start();
},
- 2000);
+ 333);
}
} else {
tabLayout.setVisibility(View.GONE);
@@ -479,23 +478,39 @@ public class InCallFragment extends Fragment
@Override
public boolean isShowingLocationUi() {
- Fragment fragment = getChildFragmentManager().findFragmentById(R.id.incall_location_holder);
+ Fragment fragment = getLocationFragment();
return fragment != null && fragment.isVisible();
}
@Override
public void showLocationUi(@Nullable Fragment locationUi) {
- boolean isShowing = isShowingLocationUi();
- if (!isShowing && locationUi != null) {
+ boolean isVisible = isShowingLocationUi();
+ if (locationUi != null && !isVisible) {
// Show the location fragment.
getChildFragmentManager()
.beginTransaction()
.replace(R.id.incall_location_holder, locationUi)
.commitAllowingStateLoss();
- } else if (isShowing && locationUi == null) {
+ } else if (locationUi == null && isVisible) {
// Hide the location fragment
- Fragment fragment = getChildFragmentManager().findFragmentById(R.id.incall_location_holder);
- getChildFragmentManager().beginTransaction().remove(fragment).commitAllowingStateLoss();
+ getChildFragmentManager()
+ .beginTransaction()
+ .remove(getLocationFragment())
+ .commitAllowingStateLoss();
}
}
+
+ @Override
+ public void onMultiWindowModeChanged(boolean isInMultiWindowMode) {
+ super.onMultiWindowModeChanged(isInMultiWindowMode);
+ if (isInMultiWindowMode == isShowingLocationUi()) {
+ LogUtil.i("InCallFragment.onMultiWindowModeChanged", "hide = " + isInMultiWindowMode);
+ // Need to show or hide location
+ showLocationUi(isInMultiWindowMode ? null : getLocationFragment());
+ }
+ }
+
+ private Fragment getLocationFragment() {
+ return getChildFragmentManager().findFragmentById(R.id.incall_location_holder);
+ }
}
diff --git a/java/com/android/incallui/incall/impl/InCallPagerAdapter.java b/java/com/android/incallui/incall/impl/InCallPagerAdapter.java
index 50eb4c8c3..2e2183565 100644
--- a/java/com/android/incallui/incall/impl/InCallPagerAdapter.java
+++ b/java/com/android/incallui/incall/impl/InCallPagerAdapter.java
@@ -19,17 +19,18 @@ package com.android.incallui.incall.impl;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
-import android.support.v4.app.FragmentPagerAdapter;
+import android.support.v4.app.FragmentStatePagerAdapter;
+import android.support.v4.view.PagerAdapter;
import android.text.TextUtils;
import com.android.dialer.multimedia.MultimediaData;
import com.android.incallui.sessiondata.MultimediaFragment;
/** View pager adapter for in call ui. */
-public class InCallPagerAdapter extends FragmentPagerAdapter {
+public class InCallPagerAdapter extends FragmentStatePagerAdapter {
- @Nullable private final MultimediaData attachments;
+ @Nullable private MultimediaData attachments;
- public InCallPagerAdapter(FragmentManager fragmentManager, MultimediaData attachments) {
+ public InCallPagerAdapter(FragmentManager fragmentManager, @Nullable MultimediaData attachments) {
super(fragmentManager);
this.attachments = attachments;
}
@@ -47,13 +48,27 @@ public class InCallPagerAdapter extends FragmentPagerAdapter {
@Override
public int getCount() {
if (attachments != null
- && (!TextUtils.isEmpty(attachments.getSubject()) || attachments.hasImageData())) {
+ && (!TextUtils.isEmpty(attachments.getText()) || attachments.hasImageData())) {
return 2;
}
return 1;
}
+ public void setAttachments(@Nullable MultimediaData attachments) {
+ if (this.attachments != attachments) {
+ this.attachments = attachments;
+ notifyDataSetChanged();
+ }
+ }
+
public int getButtonGridPosition() {
return getCount() - 1;
}
+
+ //this is called when notifyDataSetChanged() is called
+ @Override
+ public int getItemPosition(Object object) {
+ // refresh all fragments when data set changed
+ return PagerAdapter.POSITION_NONE;
+ }
}
diff --git a/java/com/android/incallui/incall/impl/MappedButtonConfig.java b/java/com/android/incallui/incall/impl/MappedButtonConfig.java
index ecdb5dfea..722983796 100644
--- a/java/com/android/incallui/incall/impl/MappedButtonConfig.java
+++ b/java/com/android/incallui/incall/impl/MappedButtonConfig.java
@@ -22,7 +22,7 @@ import android.util.ArraySet;
import com.android.dialer.common.Assert;
import com.android.incallui.incall.protocol.InCallButtonIds;
import com.android.incallui.incall.protocol.InCallButtonIdsExtension;
-
+import com.google.auto.value.AutoValue;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -151,7 +151,7 @@ final class MappedButtonConfig {
}
/** Holds information about button mapping. */
-
+ @AutoValue
abstract static class MappingInfo {
/** The Ui slot into which a given button desires to be placed. */
@@ -179,7 +179,7 @@ final class MappedButtonConfig {
}
/** Class used to build instances of {@link MappingInfo}. */
-
+ @AutoValue.Builder
abstract static class Builder {
public abstract Builder setSlot(int slot);
diff --git a/java/com/android/incallui/incall/protocol/PrimaryCallState.java b/java/com/android/incallui/incall/protocol/PrimaryCallState.java
index 782090832..6e1680b4b 100644
--- a/java/com/android/incallui/incall/protocol/PrimaryCallState.java
+++ b/java/com/android/incallui/incall/protocol/PrimaryCallState.java
@@ -18,15 +18,15 @@ package com.android.incallui.incall.protocol;
import android.graphics.drawable.Drawable;
import android.telecom.DisconnectCause;
-import android.telecom.VideoProfile;
import com.android.incallui.call.DialerCall;
-import com.android.incallui.call.DialerCall.SessionModificationState;
+import com.android.incallui.videotech.VideoTech;
+import com.android.incallui.videotech.VideoTech.SessionModificationState;
import java.util.Locale;
/** State of the primary call. */
public class PrimaryCallState {
public final int state;
- public final int videoState;
+ public final boolean isVideoCall;
@SessionModificationState public final int sessionModificationState;
public final DisconnectCause disconnectCause;
public final String connectionLabel;
@@ -37,19 +37,21 @@ public class PrimaryCallState {
public final boolean isWifi;
public final boolean isConference;
public final boolean isWorkCall;
+ public final boolean isHdAttempting;
public final boolean isHdAudioCall;
public final boolean isForwardedNumber;
public final boolean shouldShowContactPhoto;
public final long connectTimeMillis;
public final boolean isVoiceMailNumber;
public final boolean isRemotelyHeld;
+ public final boolean isBusinessNumber;
// TODO: Convert to autovalue. b/34502119
public static PrimaryCallState createEmptyPrimaryCallState() {
return new PrimaryCallState(
DialerCall.State.IDLE,
- VideoProfile.STATE_AUDIO_ONLY,
- DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST,
+ false, /* isVideoCall */
+ VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST,
new DisconnectCause(DisconnectCause.UNKNOWN),
null, /* connectionLabel */
null, /* connectionIcon */
@@ -59,17 +61,19 @@ public class PrimaryCallState {
false /* isWifi */,
false /* isConference */,
false /* isWorkCall */,
+ false /* isHdAttempting */,
false /* isHdAudioCall */,
false /* isForwardedNumber */,
false /* shouldShowContactPhoto */,
0,
false /* isVoiceMailNumber */,
- false /* isRemotelyHeld */);
+ false /* isRemotelyHeld */,
+ false /* isBusinessNumber */);
}
public PrimaryCallState(
int state,
- int videoState,
+ boolean isVideoCall,
@SessionModificationState int sessionModificationState,
DisconnectCause disconnectCause,
String connectionLabel,
@@ -80,14 +84,16 @@ public class PrimaryCallState {
boolean isWifi,
boolean isConference,
boolean isWorkCall,
+ boolean isHdAttempting,
boolean isHdAudioCall,
boolean isForwardedNumber,
boolean shouldShowContactPhoto,
long connectTimeMillis,
boolean isVoiceMailNumber,
- boolean isRemotelyHeld) {
+ boolean isRemotelyHeld,
+ boolean isBusinessNumber) {
this.state = state;
- this.videoState = videoState;
+ this.isVideoCall = isVideoCall;
this.sessionModificationState = sessionModificationState;
this.disconnectCause = disconnectCause;
this.connectionLabel = connectionLabel;
@@ -98,12 +104,14 @@ public class PrimaryCallState {
this.isWifi = isWifi;
this.isConference = isConference;
this.isWorkCall = isWorkCall;
+ this.isHdAttempting = isHdAttempting;
this.isHdAudioCall = isHdAudioCall;
this.isForwardedNumber = isForwardedNumber;
this.shouldShowContactPhoto = shouldShowContactPhoto;
this.connectTimeMillis = connectTimeMillis;
this.isVoiceMailNumber = isVoiceMailNumber;
this.isRemotelyHeld = isRemotelyHeld;
+ this.isBusinessNumber = isBusinessNumber;
}
@Override
diff --git a/java/com/android/incallui/incall/protocol/PrimaryInfo.java b/java/com/android/incallui/incall/protocol/PrimaryInfo.java
index 1833ed22e..c1709501d 100644
--- a/java/com/android/incallui/incall/protocol/PrimaryInfo.java
+++ b/java/com/android/incallui/incall/protocol/PrimaryInfo.java
@@ -41,6 +41,7 @@ public class PrimaryInfo {
// Used for consistent LetterTile coloring.
@Nullable public final String contactInfoLookupKey;
@Nullable public final MultimediaData multimediaData;
+ public final int numberPresentation;
// TODO: Convert to autovalue. b/34502119
public static PrimaryInfo createEmptyPrimaryInfo() {
@@ -59,7 +60,8 @@ public class PrimaryInfo {
false,
false,
null,
- null);
+ null,
+ -1);
}
public PrimaryInfo(
@@ -77,7 +79,8 @@ public class PrimaryInfo {
boolean answeringDisconnectsOngoingCall,
boolean shouldShowLocation,
@Nullable String contactInfoLookupKey,
- @Nullable MultimediaData multimediaData) {
+ @Nullable MultimediaData multimediaData,
+ int numberPresentation) {
this.number = number;
this.name = name;
this.nameIsNumber = nameIsNumber;
@@ -93,6 +96,7 @@ public class PrimaryInfo {
this.shouldShowLocation = shouldShowLocation;
this.contactInfoLookupKey = contactInfoLookupKey;
this.multimediaData = multimediaData;
+ this.numberPresentation = numberPresentation;
}
@Override
diff --git a/java/com/android/incallui/maps/Maps.java b/java/com/android/incallui/maps/Maps.java
new file mode 100644
index 000000000..648cf9f24
--- /dev/null
+++ b/java/com/android/incallui/maps/Maps.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.maps;
+
+import android.location.Location;
+import android.support.annotation.NonNull;
+import android.support.v4.app.Fragment;
+
+/** Used to create a fragment that can display a static map at the given location. */
+public interface Maps {
+ /**
+ * Used to check if maps is available. This will return false if Dialer was compiled without
+ * support for Google Play Services.
+ */
+ boolean isAvailable();
+
+ @NonNull
+ Fragment createStaticMapFragment(@NonNull Location location);
+}
diff --git a/java/com/android/incallui/maps/MapsComponent.java b/java/com/android/incallui/maps/MapsComponent.java
new file mode 100644
index 000000000..1ca17b781
--- /dev/null
+++ b/java/com/android/incallui/maps/MapsComponent.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.maps;
+
+import android.content.Context;
+import com.android.dialer.inject.HasRootComponent;
+import dagger.Subcomponent;
+import com.android.incallui.maps.stub.StubMapsModule;
+
+/** Subcomponent that can be used to access the maps implementation. */
+public class MapsComponent {
+
+ private static MapsComponent instance;
+ private Maps maps;
+
+ public Maps getMaps() {
+ if (maps == null) {
+ maps = new StubMapsModule.StubMaps();
+ }
+ return maps;
+ }
+
+ public static MapsComponent get(Context context) {
+ if (instance == null) {
+ instance = new MapsComponent();
+ }
+ return instance;
+ }
+
+
+ /** Used to refer to the root application component. */
+ public interface HasComponent {
+ MapsComponent mapsComponent();
+ }
+}
diff --git a/java/com/android/incallui/maps/StaticMapBinding.java b/java/com/android/incallui/maps/StaticMapBinding.java
deleted file mode 100644
index 9d24ef27a..000000000
--- a/java/com/android/incallui/maps/StaticMapBinding.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.incallui.maps;
-
-import android.app.Application;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
-
-/** Utility for getting a {@link StaticMapFactory} */
-public class StaticMapBinding {
-
- @Nullable
- public static StaticMapFactory get(@NonNull Application application) {
- if (useTestingInstance) {
- return testingInstance;
- }
- if (application instanceof StaticMapFactory) {
- return ((StaticMapFactory) application);
- }
- return null;
- }
-
- private static StaticMapFactory testingInstance;
- private static boolean useTestingInstance;
-
- @VisibleForTesting
- public static void setForTesting(@Nullable StaticMapFactory staticMapFactory) {
- testingInstance = staticMapFactory;
- useTestingInstance = true;
- }
-
- @VisibleForTesting
- public static void clearForTesting() {
- useTestingInstance = false;
- }
-}
diff --git a/java/com/android/incallui/maps/impl/AndroidManifest.xml b/java/com/android/incallui/maps/impl/AndroidManifest.xml
new file mode 100644
index 000000000..4ad0b3b7e
--- /dev/null
+++ b/java/com/android/incallui/maps/impl/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.incallui.maps.impl">
+
+ <application>
+ <meta-data
+ android:name="com.google.android.gms.version"
+ android:value="@integer/google_play_services_version"/>
+ </application>
+</manifest>
diff --git a/java/com/android/incallui/maps/impl/MapsImpl.java b/java/com/android/incallui/maps/impl/MapsImpl.java
new file mode 100644
index 000000000..2cecee93e
--- /dev/null
+++ b/java/com/android/incallui/maps/impl/MapsImpl.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.maps.impl;
+
+import android.location.Location;
+import android.support.annotation.NonNull;
+import android.support.v4.app.Fragment;
+import com.android.incallui.maps.Maps;
+import javax.inject.Inject;
+
+/** Uses Google Play Services APIs to create a static map fragment. */
+final class MapsImpl implements Maps {
+ @Inject
+ public MapsImpl() {}
+
+ @Override
+ public boolean isAvailable() {
+ return true;
+ }
+
+ @Override
+ @NonNull
+ public Fragment createStaticMapFragment(@NonNull Location location) {
+ return StaticMapFragment.newInstance(location);
+ }
+}
diff --git a/java/com/android/incallui/maps/impl/MapsModule.java b/java/com/android/incallui/maps/impl/MapsModule.java
new file mode 100644
index 000000000..22f2f32a7
--- /dev/null
+++ b/java/com/android/incallui/maps/impl/MapsModule.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.maps.impl;
+
+import com.android.incallui.maps.Maps;
+import dagger.Binds;
+import dagger.Module;
+import javax.inject.Singleton;
+
+/** This module provides an instance of maps. */
+@Module
+public abstract class MapsModule {
+
+ @Binds
+ @Singleton
+ public abstract Maps bindMaps(MapsImpl maps);
+}
diff --git a/java/com/android/incallui/maps/impl/StaticMapFragment.java b/java/com/android/incallui/maps/impl/StaticMapFragment.java
new file mode 100644
index 000000000..38a4c156b
--- /dev/null
+++ b/java/com/android/incallui/maps/impl/StaticMapFragment.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.maps.impl;
+
+import android.location.Location;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.google.android.gms.maps.CameraUpdateFactory;
+import com.google.android.gms.maps.GoogleMap;
+import com.google.android.gms.maps.OnMapReadyCallback;
+import com.google.android.gms.maps.SupportMapFragment;
+import com.google.android.gms.maps.model.LatLng;
+import com.google.android.gms.maps.model.MarkerOptions;
+
+/** Shows a static map centered on a specified location */
+public class StaticMapFragment extends Fragment implements OnMapReadyCallback {
+
+ private static final String ARG_LOCATION = "location";
+
+ public static StaticMapFragment newInstance(@NonNull Location location) {
+ Bundle args = new Bundle();
+ args.putParcelable(ARG_LOCATION, Assert.isNotNull(location));
+ StaticMapFragment fragment = new StaticMapFragment();
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
+ return layoutInflater.inflate(R.layout.static_map_fragment, viewGroup, false);
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle bundle) {
+ super.onViewCreated(view, bundle);
+ SupportMapFragment mapFragment =
+ (SupportMapFragment) getChildFragmentManager().findFragmentById(R.id.static_map);
+ if (mapFragment != null) {
+ mapFragment.getMapAsync(this);
+ } else {
+ LogUtil.w("StaticMapFragment.onViewCreated", "No map fragment found!");
+ }
+ }
+
+ @Override
+ public void onMapReady(GoogleMap googleMap) {
+ Location location = getArguments().getParcelable(ARG_LOCATION);
+ LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude());
+ googleMap.addMarker(new MarkerOptions().position(latLng).flat(true).draggable(false));
+ googleMap.getUiSettings().setMapToolbarEnabled(false);
+ googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, 15f));
+ }
+}
diff --git a/java/com/android/incallui/maps/impl/res/layout/static_map_fragment.xml b/java/com/android/incallui/maps/impl/res/layout/static_map_fragment.xml
new file mode 100644
index 000000000..54f41cb6e
--- /dev/null
+++ b/java/com/android/incallui/maps/impl/res/layout/static_map_fragment.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:map="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <fragment
+ android:id="@+id/static_map"
+ class="com.google.android.gms.maps.SupportMapFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ map:liteMode="true"
+ map:mapType="normal"/>
+</FrameLayout>
diff --git a/java/com/android/incallui/maps/stub/StubMapsModule.java b/java/com/android/incallui/maps/stub/StubMapsModule.java
new file mode 100644
index 000000000..72678143c
--- /dev/null
+++ b/java/com/android/incallui/maps/stub/StubMapsModule.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.maps.stub;
+
+import android.location.Location;
+import android.support.annotation.NonNull;
+import android.support.v4.app.Fragment;
+import com.android.dialer.common.Assert;
+import com.android.incallui.maps.Maps;
+import dagger.Binds;
+import dagger.Module;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/** Stub for the maps module for build variants that don't support Google Play Services. */
+@Module
+public abstract class StubMapsModule {
+
+ @Binds
+ @Singleton
+ public abstract Maps bindMaps(StubMaps maps);
+
+ static public final class StubMaps implements Maps {
+ @Inject
+ public StubMaps() {}
+
+ @Override
+ public boolean isAvailable() {
+ return false;
+ }
+
+ @NonNull
+ @Override
+ public Fragment createStaticMapFragment(@NonNull Location location) {
+ throw Assert.createUnsupportedOperationFailException();
+ }
+ }
+}
diff --git a/java/com/android/incallui/maps/testing/TestMapsModule.java b/java/com/android/incallui/maps/testing/TestMapsModule.java
new file mode 100644
index 000000000..bb096812b
--- /dev/null
+++ b/java/com/android/incallui/maps/testing/TestMapsModule.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.maps.testing;
+
+import android.support.annotation.Nullable;
+import com.android.incallui.maps.Maps;
+import dagger.Module;
+import dagger.Provides;
+
+/** This module provides a instance of maps for testing. */
+@Module
+public final class TestMapsModule {
+
+ @Nullable private static Maps maps;
+
+ public static void setMaps(@Nullable Maps maps) {
+ TestMapsModule.maps = maps;
+ }
+
+ @Provides
+ static Maps getMaps() {
+ return maps;
+ }
+
+ private TestMapsModule() {}
+}
diff --git a/java/com/android/incallui/res/values/strings.xml b/java/com/android/incallui/res/values/strings.xml
index 252d131de..0b95a9cc6 100644
--- a/java/com/android/incallui/res/values/strings.xml
+++ b/java/com/android/incallui/res/values/strings.xml
@@ -223,17 +223,6 @@
<item>ABSENTNUMBER</item>
</string-array>
- <!-- Preference for Voicemail service provider under "Voicemail" settings.
- [CHAR LIMIT=40] -->
- <string name="voicemail_provider">Service</string>
-
- <!-- Preference for Voicemail setting of each provider.
- [CHAR LIMIT=40] -->
- <string name="voicemail_settings">Setup</string>
-
- <!-- String to display in voicemail number summary when no voicemail num is set -->
- <string name="voicemail_number_not_set">&lt;Not set&gt;</string>
-
<!-- Title displayed above settings coming after voicemail in the call features screen -->
<string name="other_settings">Other call settings</string>
@@ -242,26 +231,6 @@
<!-- Use this to describe the select contact button in EditPhoneNumberPreference; currently for screen readers through accessibility. -->
<string name="selectContact">select contact</string>
- <!-- Dialog title for the vibration settings for voicemail notifications [CHAR LIMIT=40] -->
- <string msgid="8731372580674292759" name="voicemail_notification_vibrate_when_title">Vibrate</string>
- <!-- Dialog title for the vibration settings for voice mail notifications [CHAR LIMIT=40]-->
- <string msgid="8995274609647451109" name="voicemail_notification_vibarte_when_dialog_title">Vibrate</string>
-
- <!-- Voicemail ringtone title. The user clicks on this preference to select
- which sound to play when a voicemail notification is received.
- [CHAR LIMIT=30] -->
- <string name="voicemail_notification_ringtone_title">Sound</string>
-
- <!-- The default value value for voicemail notification. -->
- <string name="voicemail_notification_vibrate_when_default" translatable="false">never</string>
-
- <!-- Actual values used in our code for voicemail notifications. DO NOT TRANSLATE -->
- <string-array name="voicemail_notification_vibrate_when_values" translatable="false">
- <item>always</item>
- <item>silent</item>
- <item>never</item>
- </string-array>
-
<!-- Title for the category "ringtone", which is shown above ringtone and vibration
related settings.
[CHAR LIMIT=30] -->
diff --git a/java/com/android/incallui/sessiondata/MultimediaFragment.java b/java/com/android/incallui/sessiondata/MultimediaFragment.java
index d6f671d58..14aa0a3aa 100644
--- a/java/com/android/incallui/sessiondata/MultimediaFragment.java
+++ b/java/com/android/incallui/sessiondata/MultimediaFragment.java
@@ -31,12 +31,10 @@ import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
-import com.android.dialer.common.Assert;
import com.android.dialer.common.FragmentUtils;
import com.android.dialer.common.LogUtil;
import com.android.dialer.multimedia.MultimediaData;
-import com.android.incallui.maps.StaticMapBinding;
-import com.android.incallui.maps.StaticMapFactory;
+import com.android.incallui.maps.MapsComponent;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.GlideException;
@@ -58,17 +56,13 @@ public class MultimediaFragment extends Fragment implements AvatarPresenter {
private static final String ARG_INTERACTIVE = "interactive";
private static final String ARG_SHOW_AVATAR = "show_avatar";
private ImageView avatarImageView;
- // TODO: add click listeners
- @SuppressWarnings("unused")
- private boolean isInteractive;
private boolean showAvatar;
- private StaticMapFactory mapFactory;
public static MultimediaFragment newInstance(
@NonNull MultimediaData multimediaData, boolean isInteractive, boolean showAvatar) {
return newInstance(
- multimediaData.getSubject(),
+ multimediaData.getText(),
multimediaData.getImageUri(),
multimediaData.getLocation(),
isInteractive,
@@ -96,7 +90,6 @@ public class MultimediaFragment extends Fragment implements AvatarPresenter {
@Override
public void onCreate(@Nullable Bundle bundle) {
super.onCreate(bundle);
- isInteractive = getArguments().getBoolean(ARG_INTERACTIVE);
showAvatar = getArguments().getBoolean(ARG_SHOW_AVATAR);
}
@@ -107,10 +100,7 @@ public class MultimediaFragment extends Fragment implements AvatarPresenter {
boolean hasImage = getImageUri() != null;
boolean hasSubject = !TextUtils.isEmpty(getSubject());
boolean hasMap = getLocation() != null;
- if (hasMap) {
- mapFactory = StaticMapBinding.get(getActivity().getApplication());
- }
- if (mapFactory != null) {
+ if (hasMap && MapsComponent.get(getContext()).getMaps().isAvailable()) {
if (hasImage) {
if (hasSubject) {
return layoutInflater.inflate(
@@ -178,7 +168,7 @@ public class MultimediaFragment extends Fragment implements AvatarPresenter {
if (fragmentHolder != null) {
fragmentHolder.setClipToOutline(true);
Fragment mapFragment =
- Assert.isNotNull(mapFactory).getStaticMap(Assert.isNotNull(getLocation()));
+ MapsComponent.get(getContext()).getMaps().createStaticMapFragment(getLocation());
getChildFragmentManager()
.beginTransaction()
.replace(R.id.answer_message_frag, mapFragment)
diff --git a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image.xml b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image.xml
index 7000f83b5..0882781e7 100644
--- a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image.xml
+++ b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image.xml
@@ -46,5 +46,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/loading_spinner"
- android:layout_centerInParent="true"/>
+ android:layout_centerInParent="true"
+ android:elevation="2dp"/>
</RelativeLayout>
diff --git a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image_frag.xml b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image_frag.xml
index 9959f4dcc..c816418fc 100644
--- a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image_frag.xml
+++ b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image_frag.xml
@@ -42,6 +42,14 @@
android:outlineProvider="background"
android:scaleType="centerCrop"/>
+ <ProgressBar
+ android:id="@+id/loading_spinner"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_column="1"
+ android:layout_gravity="center"
+ android:elevation="2dp"/>
+
<FrameLayout
android:id="@id/answer_message_frag"
android:layout_width="0dp"
diff --git a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image.xml b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image.xml
index 995565455..4e6fcbadb 100644
--- a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image.xml
+++ b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image.xml
@@ -59,4 +59,12 @@
android:elevation="@dimen/answer_data_elevation"
android:outlineProvider="background"
android:scaleType="centerCrop"/>
+
+ <ProgressBar
+ android:id="@+id/loading_spinner"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_column="1"
+ android:layout_gravity="center"
+ android:elevation="2dp"/>
</GridLayout>
diff --git a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image_frag.xml b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image_frag.xml
index 387c5cf68..ffbe41bbd 100644
--- a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image_frag.xml
+++ b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image_frag.xml
@@ -61,6 +61,14 @@
android:outlineProvider="background"
android:scaleType="centerCrop"/>
+ <ProgressBar
+ android:id="@+id/loading_spinner"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_column="1"
+ android:layout_gravity="center"
+ android:elevation="2dp"/>
+
<FrameLayout
android:id="@id/answer_message_frag"
android:layout_width="0dp"
diff --git a/java/com/android/incallui/spam/SpamCallListListener.java b/java/com/android/incallui/spam/SpamCallListListener.java
index 0897842de..ed0a99e2a 100644
--- a/java/com/android/incallui/spam/SpamCallListListener.java
+++ b/java/com/android/incallui/spam/SpamCallListListener.java
@@ -17,6 +17,7 @@
package com.android.incallui.spam;
import android.app.Notification;
+import android.app.Notification.Builder;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
@@ -33,12 +34,13 @@ import com.android.dialer.common.LogUtil;
import com.android.dialer.logging.Logger;
import com.android.dialer.logging.nano.ContactLookupResult;
import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.notification.NotificationChannelManager;
+import com.android.dialer.notification.NotificationChannelManager.Channel;
import com.android.dialer.spam.Spam;
import com.android.incallui.R;
import com.android.incallui.call.CallList;
import com.android.incallui.call.DialerCall;
import com.android.incallui.call.DialerCall.CallHistoryStatus;
-import com.android.incallui.call.DialerCall.SessionModificationState;
import java.util.Random;
/**
@@ -47,7 +49,7 @@ import java.util.Random;
*/
public class SpamCallListListener implements CallList.Listener {
- static final int NOTIFICATION_ID = 1;
+ static final int NOTIFICATION_ID = R.id.notification_spam_call;
private static final String TAG = "SpamCallListListener";
private final Context context;
private final Random random;
@@ -87,7 +89,7 @@ public class SpamCallListListener implements CallList.Listener {
public void onUpgradeToVideo(DialerCall call) {}
@Override
- public void onSessionModificationStateChange(@SessionModificationState int newState) {}
+ public void onSessionModificationStateChange(DialerCall call) {}
@Override
public void onCallListChange(CallList callList) {}
@@ -173,13 +175,16 @@ public class SpamCallListListener implements CallList.Listener {
* Creates a notification builder with properties common among the two after call notifications.
*/
private Notification.Builder createAfterCallNotificationBuilder(DialerCall call) {
- return new Notification.Builder(context)
- .setContentIntent(
- createActivityPendingIntent(call, SpamNotificationActivity.ACTION_SHOW_DIALOG))
- .setCategory(Notification.CATEGORY_STATUS)
- .setPriority(Notification.PRIORITY_DEFAULT)
- .setColor(context.getColor(R.color.dialer_theme_color))
- .setSmallIcon(R.drawable.ic_call_end_white_24dp);
+ Builder builder =
+ new Builder(context)
+ .setContentIntent(
+ createActivityPendingIntent(call, SpamNotificationActivity.ACTION_SHOW_DIALOG))
+ .setCategory(Notification.CATEGORY_STATUS)
+ .setPriority(Notification.PRIORITY_DEFAULT)
+ .setColor(context.getColor(R.color.dialer_theme_color))
+ .setSmallIcon(R.drawable.ic_call_end_white_24dp);
+ NotificationChannelManager.applyChannel(builder, context, Channel.MISC, null);
+ return builder;
}
private CharSequence getDisplayNumber(DialerCall call) {
diff --git a/java/com/android/incallui/video/bindings/VideoBindings.java b/java/com/android/incallui/video/bindings/VideoBindings.java
index 934ff078a..a80a6c702 100644
--- a/java/com/android/incallui/video/bindings/VideoBindings.java
+++ b/java/com/android/incallui/video/bindings/VideoBindings.java
@@ -22,7 +22,7 @@ import com.android.incallui.video.protocol.VideoCallScreen;
/** Bindings for video module. */
public class VideoBindings {
- public static VideoCallScreen createVideoCallScreen() {
- return new VideoCallFragment();
+ public static VideoCallScreen createVideoCallScreen(String callId) {
+ return VideoCallFragment.newInstance(callId);
}
}
diff --git a/java/com/android/incallui/video/impl/VideoCallFragment.java b/java/com/android/incallui/video/impl/VideoCallFragment.java
index 77a67d032..92c8b375e 100644
--- a/java/com/android/incallui/video/impl/VideoCallFragment.java
+++ b/java/com/android/incallui/video/impl/VideoCallFragment.java
@@ -32,6 +32,7 @@ import android.renderscript.RenderScript;
import android.renderscript.ScriptIntrinsicBlur;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.view.animation.FastOutLinearInInterpolator;
@@ -92,6 +93,9 @@ public class VideoCallFragment extends Fragment
AudioRouteSelectorPresenter,
OnSystemUiVisibilityChangeListener {
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ static final String ARG_CALL_ID = "call_id";
+
private static final float BLUR_PREVIEW_RADIUS = 16.0f;
private static final float BLUR_PREVIEW_SCALE_FACTOR = 1.0f;
private static final float BLUR_REMOTE_RADIUS = 25.0f;
@@ -156,6 +160,15 @@ public class VideoCallFragment extends Fragment
}
};
+ public static VideoCallFragment newInstance(String callId) {
+ Bundle bundle = new Bundle();
+ bundle.putString(ARG_CALL_ID, Assert.isNotNull(callId));
+
+ VideoCallFragment instance = new VideoCallFragment();
+ instance.setArguments(bundle);
+ return instance;
+ }
+
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -308,22 +321,27 @@ public class VideoCallFragment extends Fragment
}
@Override
- public void onResume() {
- super.onResume();
- LogUtil.i("VideoCallFragment.onResume", null);
- inCallScreenDelegate.onInCallScreenResumed();
- }
-
- @Override
public void onStart() {
super.onStart();
LogUtil.i("VideoCallFragment.onStart", null);
+ onVideoScreenStart();
+ }
+
+ @Override
+ public void onVideoScreenStart() {
inCallButtonUiDelegate.refreshMuteState();
videoCallScreenDelegate.onVideoCallScreenUiReady();
getView().postDelayed(cameraPermissionDialogRunnable, CAMERA_PERMISSION_DIALOG_DELAY_IN_MILLIS);
}
@Override
+ public void onResume() {
+ super.onResume();
+ LogUtil.i("VideoCallFragment.onResume", null);
+ inCallScreenDelegate.onInCallScreenResumed();
+ }
+
+ @Override
public void onPause() {
super.onPause();
LogUtil.i("VideoCallFragment.onPause", null);
@@ -333,6 +351,11 @@ public class VideoCallFragment extends Fragment
public void onStop() {
super.onStop();
LogUtil.i("VideoCallFragment.onStop", null);
+ onVideoScreenStop();
+ }
+
+ @Override
+ public void onVideoScreenStop() {
getView().removeCallbacks(cameraPermissionDialogRunnable);
videoCallScreenDelegate.onVideoCallScreenUiUnready();
}
@@ -721,6 +744,12 @@ public class VideoCallFragment extends Fragment
}
@Override
+ @NonNull
+ public String getCallId() {
+ return Assert.isNotNull(getArguments().getString(ARG_CALL_ID));
+ }
+
+ @Override
public void showButton(@InCallButtonIds int buttonId, boolean show) {
LogUtil.v(
"VideoCallFragment.showButton",
diff --git a/java/com/android/incallui/video/impl/res/layout/frag_videocall.xml b/java/com/android/incallui/video/impl/res/layout/frag_videocall.xml
index dc663dda1..f8c6fc3c7 100644
--- a/java/com/android/incallui/video/impl/res/layout/frag_videocall.xml
+++ b/java/com/android/incallui/video/impl/res/layout/frag_videocall.xml
@@ -31,6 +31,7 @@
android:accessibilityTraversalBefore="@+id/videocall_speaker_button"
android:drawablePadding="8dp"
android:drawableTop="@drawable/quantum_ic_videocam_off_white_36"
+ android:drawableTint="@color/videocall_camera_off_tint"
android:padding="64dp"
android:text="@string/videocall_remote_video_off"
android:textAppearance="@style/Dialer.Incall.TextAppearance"
@@ -43,7 +44,8 @@
android:layout_height="match_parent"
android:layout_alignParentBottom="true"
android:layout_alignParentStart="true"
- android:background="@color/videocall_overlay_background_color"/>
+ android:background="@color/videocall_overlay_background_color"
+ tools:visibility="gone"/>
<TextureView
android:id="@+id/videocall_video_preview"
@@ -71,7 +73,8 @@
android:layout_height="match_parent"
android:layout_alignParentBottom="true"
android:layout_alignParentStart="true"
- android:background="@color/videocall_overlay_background_color"/>
+ android:background="@color/videocall_overlay_background_color"
+ tools:visibility="gone"/>
<ImageView
android:id="@+id/videocall_video_preview_off_overlay"
@@ -82,7 +85,9 @@
android:layout_alignRight="@+id/videocall_video_preview"
android:layout_alignTop="@+id/videocall_video_preview"
android:scaleType="center"
- android:src="@drawable/quantum_ic_videocam_off_white_36"
+ android:src="@drawable/quantum_ic_videocam_off_white_24"
+ android:tint="@color/videocall_camera_off_tint"
+ android:tintMode="src_in"
android:visibility="gone"
android:importantForAccessibility="no"
tools:visibility="visible"/>
diff --git a/java/com/android/incallui/video/impl/res/values/colors.xml b/java/com/android/incallui/video/impl/res/values/colors.xml
new file mode 100644
index 000000000..874bf9404
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/values/colors.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+ <color name="videocall_camera_off_tint">#89ffffff</color>
+</resources>
diff --git a/java/com/android/incallui/video/protocol/VideoCallScreen.java b/java/com/android/incallui/video/protocol/VideoCallScreen.java
index 0eaf692e2..bad050cd1 100644
--- a/java/com/android/incallui/video/protocol/VideoCallScreen.java
+++ b/java/com/android/incallui/video/protocol/VideoCallScreen.java
@@ -21,6 +21,10 @@ import android.support.v4.app.Fragment;
/** Interface for call video call module. */
public interface VideoCallScreen {
+ void onVideoScreenStart();
+
+ void onVideoScreenStop();
+
void showVideoViews(boolean shouldShowPreview, boolean shouldShowRemote, boolean isRemotelyHeld);
void onLocalVideoDimensionsChanged();
@@ -33,4 +37,6 @@ public interface VideoCallScreen {
boolean shouldShowFullscreen, boolean shouldShowGreenScreen);
Fragment getVideoCallScreenFragment();
+
+ String getCallId();
}
diff --git a/java/com/android/incallui/videotech/VideoTech.java b/java/com/android/incallui/videotech/VideoTech.java
new file mode 100644
index 000000000..fb2641793
--- /dev/null
+++ b/java/com/android/incallui/videotech/VideoTech.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.videotech;
+
+import android.support.annotation.IntDef;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Video calling interface. */
+public interface VideoTech {
+
+ boolean isAvailable();
+
+ boolean isTransmittingOrReceiving();
+
+ void onCallStateChanged(int newState);
+
+ @SessionModificationState
+ int getSessionModificationState();
+
+ void upgradeToVideo();
+
+ void acceptVideoRequest();
+
+ void acceptVideoRequestAsAudio();
+
+ void declineVideoRequest();
+
+ boolean isTransmitting();
+
+ void stopTransmission();
+
+ void resumeTransmission();
+
+ void pause();
+
+ void unpause();
+
+ void setCamera(String cameraId);
+
+ void setDeviceOrientation(int rotation);
+
+ /** Listener for video call events. */
+ interface VideoTechListener {
+
+ void onVideoTechStateChanged();
+
+ void onSessionModificationStateChanged();
+
+ void onCameraDimensionsChanged(int width, int height);
+
+ void onPeerDimensionsChanged(int width, int height);
+
+ void onVideoUpgradeRequestReceived();
+ }
+
+ /**
+ * Defines different states of session modify requests, which are used to upgrade to video, or
+ * downgrade to audio.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ SESSION_MODIFICATION_STATE_NO_REQUEST,
+ SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE,
+ SESSION_MODIFICATION_STATE_REQUEST_FAILED,
+ SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST,
+ SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT,
+ SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED,
+ SESSION_MODIFICATION_STATE_REQUEST_REJECTED,
+ SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE
+ })
+ @interface SessionModificationState {}
+
+ int SESSION_MODIFICATION_STATE_NO_REQUEST = 0;
+ int SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE = 1;
+ int SESSION_MODIFICATION_STATE_REQUEST_FAILED = 2;
+ int SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST = 3;
+ int SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT = 4;
+ int SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED = 5;
+ int SESSION_MODIFICATION_STATE_REQUEST_REJECTED = 6;
+ int SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE = 7;
+}
diff --git a/java/com/android/incallui/videotech/empty/EmptyVideoTech.java b/java/com/android/incallui/videotech/empty/EmptyVideoTech.java
new file mode 100644
index 000000000..bc8db4c07
--- /dev/null
+++ b/java/com/android/incallui/videotech/empty/EmptyVideoTech.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.videotech.empty;
+
+import com.android.incallui.videotech.VideoTech;
+
+/** Default video tech that is always available but doesn't do anything. */
+public class EmptyVideoTech implements VideoTech {
+
+ @Override
+ public boolean isAvailable() {
+ return false;
+ }
+
+ @Override
+ public boolean isTransmittingOrReceiving() {
+ return false;
+ }
+
+ @Override
+ public void onCallStateChanged(int newState) {}
+
+ @Override
+ public int getSessionModificationState() {
+ return VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST;
+ }
+
+ @Override
+ public void upgradeToVideo() {}
+
+ @Override
+ public void acceptVideoRequest() {}
+
+ @Override
+ public void acceptVideoRequestAsAudio() {}
+
+ @Override
+ public void declineVideoRequest() {}
+
+ @Override
+ public boolean isTransmitting() {
+ return false;
+ }
+
+ @Override
+ public void stopTransmission() {}
+
+ @Override
+ public void resumeTransmission() {}
+
+ @Override
+ public void pause() {}
+
+ @Override
+ public void unpause() {}
+
+ @Override
+ public void setCamera(String cameraId) {}
+
+ @Override
+ public void setDeviceOrientation(int rotation) {}
+}
diff --git a/java/com/android/incallui/videotech/ims/ImsVideoCallCallback.java b/java/com/android/incallui/videotech/ims/ImsVideoCallCallback.java
new file mode 100644
index 000000000..0a15f7e65
--- /dev/null
+++ b/java/com/android/incallui/videotech/ims/ImsVideoCallCallback.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.videotech.ims;
+
+import android.os.Handler;
+import android.telecom.Call;
+import android.telecom.Connection;
+import android.telecom.Connection.VideoProvider;
+import android.telecom.InCallService.VideoCall;
+import android.telecom.VideoProfile;
+import android.telecom.VideoProfile.CameraCapabilities;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.videotech.VideoTech;
+import com.android.incallui.videotech.VideoTech.SessionModificationState;
+import com.android.incallui.videotech.VideoTech.VideoTechListener;
+
+/** Receives IMS video call state updates. */
+public class ImsVideoCallCallback extends VideoCall.Callback {
+ private static final int CLEAR_FAILED_REQUEST_TIMEOUT_MILLIS = 4000;
+ private final Handler handler = new Handler();
+ private final Call call;
+ private final ImsVideoTech videoTech;
+ private final VideoTechListener listener;
+ private int requestedVideoState = VideoProfile.STATE_AUDIO_ONLY;
+
+ ImsVideoCallCallback(final Call call, ImsVideoTech videoTech, VideoTechListener listener) {
+ this.call = call;
+ this.videoTech = videoTech;
+ this.listener = listener;
+ }
+
+ @Override
+ public void onSessionModifyRequestReceived(VideoProfile videoProfile) {
+ LogUtil.i(
+ "ImsVideoCallCallback.onSessionModifyRequestReceived", "videoProfile: " + videoProfile);
+
+ int previousVideoState = ImsVideoTech.getUnpausedVideoState(call.getDetails().getVideoState());
+ int newVideoState = ImsVideoTech.getUnpausedVideoState(videoProfile.getVideoState());
+
+ boolean wasVideoCall = VideoProfile.isVideo(previousVideoState);
+ boolean isVideoCall = VideoProfile.isVideo(newVideoState);
+
+ if (wasVideoCall && !isVideoCall) {
+ LogUtil.i(
+ "ImsVideoTech.onSessionModifyRequestReceived", "call downgraded to %d", newVideoState);
+ } else if (previousVideoState != newVideoState) {
+ requestedVideoState = newVideoState;
+ videoTech.setSessionModificationState(
+ VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST);
+ listener.onVideoUpgradeRequestReceived();
+ }
+ }
+
+ /**
+ * @param status Status of the session modify request. Valid values are {@link
+ * Connection.VideoProvider#SESSION_MODIFY_REQUEST_SUCCESS}, {@link
+ * Connection.VideoProvider#SESSION_MODIFY_REQUEST_FAIL}, {@link
+ * Connection.VideoProvider#SESSION_MODIFY_REQUEST_INVALID}
+ * @param responseProfile The actual profile changes made by the peer device.
+ */
+ @Override
+ public void onSessionModifyResponseReceived(
+ int status, VideoProfile requestedProfile, VideoProfile responseProfile) {
+ LogUtil.i(
+ "ImsVideoCallCallback.onSessionModifyResponseReceived",
+ "status: %d, requestedProfile: %s, responseProfile: %s, session modification state: %d",
+ status,
+ requestedProfile,
+ responseProfile,
+ videoTech.getSessionModificationState());
+
+ if (videoTech.getSessionModificationState()
+ == VideoTech.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE) {
+ handler.removeCallbacksAndMessages(null); // Clear everything
+
+ final int newSessionModificationState = getSessionModificationStateFromTelecomStatus(status);
+ if (status != VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS) {
+ // This will update the video UI to display the error message.
+ videoTech.setSessionModificationState(newSessionModificationState);
+ }
+
+ // Wait for 4 seconds and then clean the session modification state. This allows the video UI
+ // to stay up so that the user can read the error message.
+ //
+ // If the other person accepted the upgrade request then this will keep the video UI up until
+ // the call's video state change. Without this we would switch to the voice call and then
+ // switch back to video UI.
+ handler.postDelayed(
+ () -> {
+ if (videoTech.getSessionModificationState() == newSessionModificationState) {
+ LogUtil.i("ImsVideoCallCallback.onSessionModifyResponseReceived", "clearing state");
+ videoTech.setSessionModificationState(
+ VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ } else {
+ LogUtil.i(
+ "ImsVideoCallCallback.onSessionModifyResponseReceived",
+ "session modification state has changed, not clearing state");
+ }
+ },
+ CLEAR_FAILED_REQUEST_TIMEOUT_MILLIS);
+ } else if (videoTech.getSessionModificationState()
+ == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+ videoTech.setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ } else if (videoTech.getSessionModificationState()
+ == VideoTech.SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE) {
+ videoTech.setSessionModificationState(getSessionModificationStateFromTelecomStatus(status));
+ } else {
+ LogUtil.i(
+ "ImsVideoCallCallback.onSessionModifyResponseReceived",
+ "call is not waiting for response, doing nothing");
+ }
+ }
+
+ @SessionModificationState
+ private int getSessionModificationStateFromTelecomStatus(int telecomStatus) {
+ switch (telecomStatus) {
+ case VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS:
+ return VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST;
+ case VideoProvider.SESSION_MODIFY_REQUEST_FAIL:
+ case VideoProvider.SESSION_MODIFY_REQUEST_INVALID:
+ // Check if it's already video call, which means the request is not video upgrade request.
+ if (VideoProfile.isVideo(call.getDetails().getVideoState())) {
+ return VideoTech.SESSION_MODIFICATION_STATE_REQUEST_FAILED;
+ } else {
+ return VideoTech.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED;
+ }
+ case VideoProvider.SESSION_MODIFY_REQUEST_TIMED_OUT:
+ return VideoTech.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT;
+ case VideoProvider.SESSION_MODIFY_REQUEST_REJECTED_BY_REMOTE:
+ return VideoTech.SESSION_MODIFICATION_STATE_REQUEST_REJECTED;
+ default:
+ LogUtil.e(
+ "ImsVideoCallCallback.getSessionModificationStateFromTelecomStatus",
+ "unknown status: %d",
+ telecomStatus);
+ return VideoTech.SESSION_MODIFICATION_STATE_REQUEST_FAILED;
+ }
+ }
+
+ @Override
+ public void onCallSessionEvent(int event) {
+ switch (event) {
+ case Connection.VideoProvider.SESSION_EVENT_RX_PAUSE:
+ LogUtil.i("ImsVideoCallCallback.onCallSessionEvent", "rx_pause");
+ break;
+ case Connection.VideoProvider.SESSION_EVENT_RX_RESUME:
+ LogUtil.i("ImsVideoCallCallback.onCallSessionEvent", "rx_resume");
+ break;
+ case Connection.VideoProvider.SESSION_EVENT_CAMERA_FAILURE:
+ LogUtil.i("ImsVideoCallCallback.onCallSessionEvent", "camera_failure");
+ break;
+ case Connection.VideoProvider.SESSION_EVENT_CAMERA_READY:
+ LogUtil.i("ImsVideoCallCallback.onCallSessionEvent", "camera_ready");
+ break;
+ default:
+ LogUtil.i("ImsVideoCallCallback.onCallSessionEvent", "unknown event = : " + event);
+ break;
+ }
+ }
+
+ @Override
+ public void onPeerDimensionsChanged(int width, int height) {
+ listener.onPeerDimensionsChanged(width, height);
+ }
+
+ @Override
+ public void onVideoQualityChanged(int videoQuality) {
+ LogUtil.i("ImsVideoCallCallback.onVideoQualityChanged", "videoQuality: %d", videoQuality);
+ }
+
+ @Override
+ public void onCallDataUsageChanged(long dataUsage) {
+ LogUtil.i("ImsVideoCallCallback.onCallDataUsageChanged", "dataUsage: %d", dataUsage);
+ }
+
+ @Override
+ public void onCameraCapabilitiesChanged(CameraCapabilities cameraCapabilities) {
+ if (cameraCapabilities != null) {
+ listener.onCameraDimensionsChanged(
+ cameraCapabilities.getWidth(), cameraCapabilities.getHeight());
+ }
+ }
+
+ int getRequestedVideoState() {
+ return requestedVideoState;
+ }
+}
diff --git a/java/com/android/incallui/videotech/ims/ImsVideoTech.java b/java/com/android/incallui/videotech/ims/ImsVideoTech.java
new file mode 100644
index 000000000..890e5c80c
--- /dev/null
+++ b/java/com/android/incallui/videotech/ims/ImsVideoTech.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.videotech.ims;
+
+import android.os.Build;
+import android.telecom.Call;
+import android.telecom.Call.Details;
+import android.telecom.VideoProfile;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.videotech.VideoTech;
+
+/** ViLTE implementation */
+public class ImsVideoTech implements VideoTech {
+ private final Call call;
+ private final VideoTechListener listener;
+ private ImsVideoCallCallback callback;
+ private @SessionModificationState int sessionModificationState =
+ VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST;
+ private int previousVideoState = VideoProfile.STATE_AUDIO_ONLY;
+
+ public ImsVideoTech(VideoTechListener listener, Call call) {
+ this.listener = listener;
+ this.call = call;
+ }
+
+ @Override
+ public boolean isAvailable() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ return false;
+ }
+
+ boolean hasCapabilities =
+ call.getDetails().can(Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_TX)
+ && call.getDetails().can(Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_RX);
+
+ return call.getVideoCall() != null
+ && (hasCapabilities || VideoProfile.isVideo(call.getDetails().getVideoState()));
+ }
+
+ @Override
+ public boolean isTransmittingOrReceiving() {
+ return VideoProfile.isVideo(call.getDetails().getVideoState());
+ }
+
+ @Override
+ public void onCallStateChanged(int newState) {
+ if (!isAvailable()) {
+ return;
+ }
+
+ if (callback == null) {
+ callback = new ImsVideoCallCallback(call, this, listener);
+ call.getVideoCall().registerCallback(callback);
+ }
+
+ if (getSessionModificationState()
+ == VideoTech.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE
+ && isTransmittingOrReceiving()) {
+ // We don't clear the session modification state right away when we find out the video upgrade
+ // request was accepted to avoid having the UI switch from video to voice to video.
+ // Once the underlying telecom call updates to video mode it's safe to clear the state.
+ LogUtil.i(
+ "ImsVideoTech.onCallStateChanged",
+ "upgraded to video, clearing session modification state");
+ setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+
+ // Determines if a received upgrade to video request should be cancelled. This can happen if
+ // another InCall UI responds to the upgrade to video request.
+ int newVideoState = call.getDetails().getVideoState();
+ if (newVideoState != previousVideoState
+ && sessionModificationState
+ == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+ LogUtil.i("ImsVideoTech.onCallStateChanged", "cancelling upgrade notification");
+ setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+ previousVideoState = newVideoState;
+ }
+
+ @Override
+ public int getSessionModificationState() {
+ return sessionModificationState;
+ }
+
+ void setSessionModificationState(@SessionModificationState int state) {
+ if (state != sessionModificationState) {
+ LogUtil.i(
+ "ImsVideoTech.setSessionModificationState", "%d -> %d", sessionModificationState, state);
+ sessionModificationState = state;
+ listener.onSessionModificationStateChanged();
+ }
+ }
+
+ @Override
+ public void upgradeToVideo() {
+ LogUtil.enterBlock("ImsVideoTech.upgradeToVideo");
+
+ int unpausedVideoState = getUnpausedVideoState(call.getDetails().getVideoState());
+ call.getVideoCall()
+ .sendSessionModifyRequest(
+ new VideoProfile(unpausedVideoState | VideoProfile.STATE_BIDIRECTIONAL));
+ setSessionModificationState(
+ VideoTech.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE);
+ }
+
+ @Override
+ public void acceptVideoRequest() {
+ int requestedVideoState = callback.getRequestedVideoState();
+ Assert.checkArgument(requestedVideoState != VideoProfile.STATE_AUDIO_ONLY);
+ LogUtil.i("ImsVideoTech.acceptUpgradeRequest", "videoState: " + requestedVideoState);
+ call.getVideoCall().sendSessionModifyResponse(new VideoProfile(requestedVideoState));
+ setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+
+ @Override
+ public void acceptVideoRequestAsAudio() {
+ LogUtil.enterBlock("ImsVideoTech.acceptVideoRequestAsAudio");
+ call.getVideoCall().sendSessionModifyResponse(new VideoProfile(VideoProfile.STATE_AUDIO_ONLY));
+ setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+
+ @Override
+ public void declineVideoRequest() {
+ LogUtil.enterBlock("ImsVideoTech.declineUpgradeRequest");
+ call.getVideoCall()
+ .sendSessionModifyResponse(new VideoProfile(call.getDetails().getVideoState()));
+ setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+
+ @Override
+ public boolean isTransmitting() {
+ return VideoProfile.isTransmissionEnabled(call.getDetails().getVideoState());
+ }
+
+ @Override
+ public void stopTransmission() {
+ LogUtil.enterBlock("ImsVideoTech.stopTransmission");
+
+ int unpausedVideoState = getUnpausedVideoState(call.getDetails().getVideoState());
+ call.getVideoCall()
+ .sendSessionModifyRequest(
+ new VideoProfile(unpausedVideoState & ~VideoProfile.STATE_TX_ENABLED));
+ }
+
+ @Override
+ public void resumeTransmission() {
+ LogUtil.enterBlock("ImsVideoTech.resumeTransmission");
+
+ int unpausedVideoState = getUnpausedVideoState(call.getDetails().getVideoState());
+ call.getVideoCall()
+ .sendSessionModifyRequest(
+ new VideoProfile(unpausedVideoState | VideoProfile.STATE_TX_ENABLED));
+ setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE);
+ }
+
+ @Override
+ public void pause() {
+ if (canPause()) {
+ LogUtil.i("ImsVideoTech.pause", "sending pause request");
+ int pausedVideoState = call.getDetails().getVideoState() | VideoProfile.STATE_PAUSED;
+ call.getVideoCall().sendSessionModifyRequest(new VideoProfile(pausedVideoState));
+ } else {
+ LogUtil.i("ImsVideoTech.pause", "not sending request: canPause: %b", canPause());
+ }
+ }
+
+ @Override
+ public void unpause() {
+ if (canPause()) {
+ LogUtil.i("ImsVideoTech.unpause", "sending unpause request");
+ int unpausedVideoState = getUnpausedVideoState(call.getDetails().getVideoState());
+ call.getVideoCall().sendSessionModifyRequest(new VideoProfile(unpausedVideoState));
+ } else {
+ LogUtil.i("ImsVideoTech.unpause", "not sending request: canPause: %b", canPause());
+ }
+ }
+
+ @Override
+ public void setCamera(String cameraId) {
+ call.getVideoCall().setCamera(cameraId);
+ call.getVideoCall().requestCameraCapabilities();
+ }
+
+ @Override
+ public void setDeviceOrientation(int rotation) {
+ call.getVideoCall().setDeviceOrientation(rotation);
+ }
+
+ private boolean canPause() {
+ return call.getDetails().can(Details.CAPABILITY_CAN_PAUSE_VIDEO)
+ && call.getState() == Call.STATE_ACTIVE;
+ }
+
+ static int getUnpausedVideoState(int videoState) {
+ return videoState & (~VideoProfile.STATE_PAUSED);
+ }
+}
diff --git a/java/com/android/incallui/videotech/rcs/RcsVideoShare.java b/java/com/android/incallui/videotech/rcs/RcsVideoShare.java
new file mode 100644
index 000000000..2cb43036f
--- /dev/null
+++ b/java/com/android/incallui/videotech/rcs/RcsVideoShare.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.videotech.rcs;
+
+import android.support.annotation.NonNull;
+import android.telecom.Call;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.enrichedcall.EnrichedCallCapabilities;
+import com.android.dialer.enrichedcall.EnrichedCallManager;
+import com.android.dialer.enrichedcall.EnrichedCallManager.CapabilitiesListener;
+import com.android.dialer.enrichedcall.Session;
+import com.android.dialer.enrichedcall.videoshare.VideoShareListener;
+import com.android.incallui.videotech.VideoTech;
+
+/** Allows the in-call UI to make video calls over RCS. */
+public class RcsVideoShare implements VideoTech, CapabilitiesListener, VideoShareListener {
+ private final EnrichedCallManager enrichedCallManager;
+ private final VideoTechListener listener;
+ private final String callingNumber;
+ private int previousCallState = Call.STATE_NEW;
+ private long inviteSessionId = Session.NO_SESSION_ID;
+ private long transmittingSessionId = Session.NO_SESSION_ID;
+ private long receivingSessionId = Session.NO_SESSION_ID;
+
+ private @SessionModificationState int sessionModificationState =
+ VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST;
+
+ public RcsVideoShare(
+ @NonNull EnrichedCallManager enrichedCallManager,
+ @NonNull VideoTechListener listener,
+ @NonNull String callingNumber) {
+ this.enrichedCallManager = Assert.isNotNull(enrichedCallManager);
+ this.listener = Assert.isNotNull(listener);
+ this.callingNumber = Assert.isNotNull(callingNumber);
+
+ enrichedCallManager.registerCapabilitiesListener(this);
+ enrichedCallManager.registerVideoShareListener(this);
+ }
+
+ @Override
+ public boolean isAvailable() {
+ EnrichedCallCapabilities capabilities = enrichedCallManager.getCapabilities(callingNumber);
+ return capabilities != null && capabilities.supportsVideoShare();
+ }
+
+ @Override
+ public boolean isTransmittingOrReceiving() {
+ return transmittingSessionId != Session.NO_SESSION_ID
+ || receivingSessionId != Session.NO_SESSION_ID;
+ }
+
+ @Override
+ public void onCallStateChanged(int newState) {
+ if (newState == Call.STATE_DISCONNECTING) {
+ enrichedCallManager.unregisterVideoShareListener(this);
+ enrichedCallManager.unregisterCapabilitiesListener(this);
+ }
+
+ if (newState != previousCallState && newState == Call.STATE_ACTIVE) {
+ // Per spec, request capabilities when the call becomes active
+ enrichedCallManager.requestCapabilities(callingNumber);
+ }
+
+ previousCallState = newState;
+ }
+
+ @Override
+ public int getSessionModificationState() {
+ return sessionModificationState;
+ }
+
+ private void setSessionModificationState(@SessionModificationState int state) {
+ if (state != sessionModificationState) {
+ LogUtil.i(
+ "RcsVideoShare.setSessionModificationState", "%d -> %d", sessionModificationState, state);
+ sessionModificationState = state;
+ listener.onSessionModificationStateChanged();
+ }
+ }
+
+ @Override
+ public void upgradeToVideo() {
+ LogUtil.enterBlock("RcsVideoShare.upgradeToVideo");
+ transmittingSessionId = enrichedCallManager.startVideoShareSession(callingNumber);
+ if (transmittingSessionId != Session.NO_SESSION_ID) {
+ setSessionModificationState(
+ VideoTech.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE);
+ }
+ }
+
+ @Override
+ public void acceptVideoRequest() {
+ LogUtil.enterBlock("RcsVideoShare.acceptVideoRequest");
+ if (enrichedCallManager.acceptVideoShareSession(inviteSessionId)) {
+ receivingSessionId = inviteSessionId;
+ }
+ inviteSessionId = Session.NO_SESSION_ID;
+ setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+
+ @Override
+ public void acceptVideoRequestAsAudio() {
+ throw Assert.createUnsupportedOperationFailException();
+ }
+
+ @Override
+ public void declineVideoRequest() {
+ LogUtil.enterBlock("RcsVideoTech.declineUpgradeRequest");
+ enrichedCallManager.endVideoShareSession(
+ enrichedCallManager.getVideoShareInviteSessionId(callingNumber));
+ inviteSessionId = Session.NO_SESSION_ID;
+ setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+
+ @Override
+ public boolean isTransmitting() {
+ return transmittingSessionId != Session.NO_SESSION_ID;
+ }
+
+ @Override
+ public void stopTransmission() {
+ LogUtil.enterBlock("RcsVideoTech.stopTransmission");
+ }
+
+ @Override
+ public void resumeTransmission() {
+ LogUtil.enterBlock("RcsVideoTech.resumeTransmission");
+ }
+
+ @Override
+ public void pause() {}
+
+ @Override
+ public void unpause() {}
+
+ @Override
+ public void setCamera(String cameraId) {}
+
+ @Override
+ public void setDeviceOrientation(int rotation) {}
+
+ @Override
+ public void onCapabilitiesUpdated() {
+ listener.onVideoTechStateChanged();
+ }
+
+ @Override
+ public void onVideoShareChanged() {
+ long existingInviteSessionId = inviteSessionId;
+
+ inviteSessionId = enrichedCallManager.getVideoShareInviteSessionId(callingNumber);
+ if (inviteSessionId != Session.NO_SESSION_ID) {
+ if (existingInviteSessionId == Session.NO_SESSION_ID) {
+ // This is a new invite
+ setSessionModificationState(
+ VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST);
+ listener.onVideoUpgradeRequestReceived();
+ }
+ } else {
+ setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+
+ if (sessionIsClosed(transmittingSessionId)) {
+ LogUtil.i("RcsVideoShare.onSessionClosed", "transmitting session closed");
+ transmittingSessionId = Session.NO_SESSION_ID;
+ }
+
+ if (sessionIsClosed(receivingSessionId)) {
+ LogUtil.i("RcsVideoShare.onSessionClosed", "receiving session closed");
+ receivingSessionId = Session.NO_SESSION_ID;
+ }
+
+ listener.onVideoTechStateChanged();
+ }
+
+ private boolean sessionIsClosed(long sessionId) {
+ return sessionId != Session.NO_SESSION_ID
+ && enrichedCallManager.getVideoShareSession(sessionId) == null;
+ }
+}
diff --git a/java/com/android/voicemail/VoicemailClient.java b/java/com/android/voicemail/VoicemailClient.java
new file mode 100644
index 000000000..b237f65f6
--- /dev/null
+++ b/java/com/android/voicemail/VoicemailClient.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail;
+
+import android.content.Context;
+import android.provider.VoicemailContract.Voicemails;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import java.util.List;
+
+/** Public interface for the voicemail module */
+public interface VoicemailClient {
+
+ /**
+ * Broadcast to tell the client to upload local database changes to the server. Since the dialer
+ * UI and the client are in the same package, the {@link
+ * android.content.Intent#ACTION_PROVIDER_CHANGED} will always be a self-change even if the UI is
+ * external to the client.
+ */
+ String ACTION_UPLOAD = "com.android.voicemailomtp.VoicemailClient.ACTION_UPLOAD";
+
+ /**
+ * Appends the selection to ignore voicemails from non-active OMTP voicemail package. In OC there
+ * can be multiple packages handling OMTP voicemails which represents the same source of truth.
+ * These packages should mark their voicemails as {@link Voicemails#IS_OMTP_VOICEMAIL} and only
+ * the voicemails from {@link TelephonyManager#getVisualVoicemailPackageName()} should be shown.
+ * For example, the user synced voicemails with DialerA, and then switched to DialerB, voicemails
+ * from DialerA should be ignored as they are no longer current. Voicemails from {@link
+ * #OMTP_VOICEMAIL_BLACKLIST} will also be ignored as they are voicemail source only valid pre-OC.
+ */
+ void appendOmtpVoicemailSelectionClause(
+ Context context, StringBuilder where, List<String> selectionArgs);
+ /**
+ * @return the class name of the {@link android.preference.PreferenceFragment} for voicemail
+ * settings, or {@code null} if dialer cannot control voicemail settings. Always return {@code
+ * null} before OC.
+ */
+ @Nullable
+ String getSettingsFragment();
+
+ boolean isVoicemailArchiveEnabled(Context context, PhoneAccountHandle phoneAccountHandle);
+
+ void setVoicemailArchiveEnabled(
+ Context context, PhoneAccountHandle phoneAccountHandle, boolean value);
+}
diff --git a/java/com/android/voicemail/VoicemailComponent.java b/java/com/android/voicemail/VoicemailComponent.java
new file mode 100644
index 000000000..6dd6f9d90
--- /dev/null
+++ b/java/com/android/voicemail/VoicemailComponent.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail;
+
+import android.content.Context;
+import dagger.Subcomponent;
+import com.android.voicemail.impl.VoicemailClientImpl;
+
+/** Subcomponent that can be used to access the voicemail implementation. */
+public class VoicemailComponent {
+ private static VoicemailComponent instance;
+ private VoicemailClientImpl voicemailClient;
+
+ public VoicemailClient getVoicemailClient() {
+ if (voicemailClient == null) {
+ voicemailClient = new VoicemailClientImpl();
+ }
+ return voicemailClient;
+ }
+
+ public static VoicemailComponent get(Context context) {
+ if (instance == null) {
+ instance = new VoicemailComponent();
+ }
+ return instance;
+ }
+
+ /** Used to refer to the root application component. */
+ public interface HasComponent {
+ VoicemailComponent voicemailComponent();
+ }
+}
diff --git a/java/com/android/voicemail/impl/ActivationTask.java b/java/com/android/voicemail/impl/ActivationTask.java
new file mode 100644
index 000000000..c4716116f
--- /dev/null
+++ b/java/com/android/voicemail/impl/ActivationTask.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.Intent;
+import android.database.ContentObserver;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.provider.Settings.Global;
+import android.support.annotation.Nullable;
+import android.support.annotation.WorkerThread;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.ServiceState;
+import android.telephony.TelephonyManager;
+import com.android.voicemail.impl.protocol.VisualVoicemailProtocol;
+import com.android.voicemail.impl.scheduling.BaseTask;
+import com.android.voicemail.impl.scheduling.RetryPolicy;
+import com.android.voicemail.impl.sms.StatusMessage;
+import com.android.voicemail.impl.sms.StatusSmsFetcher;
+import com.android.voicemail.impl.sync.OmtpVvmSyncService;
+import com.android.voicemail.impl.sync.SyncTask;
+import com.android.voicemail.impl.sync.VvmAccountManager;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Task to activate the visual voicemail service. A request to activate VVM will be sent to the
+ * carrier, which will respond with a STATUS SMS. The credentials will be updated from the SMS. If
+ * the user is not provisioned provisioning will be attempted. Activation happens when the phone
+ * boots, the SIM is inserted, signal returned when VVM is not activated yet, and when the carrier
+ * spontaneously sent a STATUS SMS.
+ */
+@TargetApi(VERSION_CODES.O)
+public class ActivationTask extends BaseTask {
+
+ private static final String TAG = "VvmActivationTask";
+
+ private static final int RETRY_TIMES = 4;
+ private static final int RETRY_INTERVAL_MILLIS = 5_000;
+
+ private static final String EXTRA_MESSAGE_DATA_BUNDLE = "extra_message_data_bundle";
+
+ @Nullable private static DeviceProvisionedObserver sDeviceProvisionedObserver;
+
+ private final RetryPolicy mRetryPolicy;
+
+ private Bundle mMessageData;
+
+ public ActivationTask() {
+ super(TASK_ACTIVATION);
+ mRetryPolicy = new RetryPolicy(RETRY_TIMES, RETRY_INTERVAL_MILLIS);
+ addPolicy(mRetryPolicy);
+ }
+
+ /** Has the user gone through the setup wizard yet. */
+ private static boolean isDeviceProvisioned(Context context) {
+ return Settings.Global.getInt(
+ context.getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 0)
+ == 1;
+ }
+
+ /**
+ * @param messageData The optional bundle from {@link android.provider.VoicemailContract#
+ * EXTRA_VOICEMAIL_SMS_FIELDS}, if the task is initiated by a status SMS. If null the task
+ * will request a status SMS itself.
+ */
+ public static void start(
+ Context context, PhoneAccountHandle phoneAccountHandle, @Nullable Bundle messageData) {
+ if (!isDeviceProvisioned(context)) {
+ VvmLog.i(TAG, "Activation requested while device is not provisioned, postponing");
+ // Activation might need information such as system language to be set, so wait until
+ // the setup wizard is finished. The data bundle from the SMS will be re-requested upon
+ // activation.
+ queueActivationAfterProvisioned(context, phoneAccountHandle);
+ return;
+ }
+
+ Intent intent = BaseTask.createIntent(context, ActivationTask.class, phoneAccountHandle);
+ if (messageData != null) {
+ intent.putExtra(EXTRA_MESSAGE_DATA_BUNDLE, messageData);
+ }
+ context.startService(intent);
+ }
+
+ @Override
+ public void onCreate(Context context, Intent intent, int flags, int startId) {
+ super.onCreate(context, intent, flags, startId);
+ mMessageData = intent.getParcelableExtra(EXTRA_MESSAGE_DATA_BUNDLE);
+ }
+
+ @Override
+ public Intent createRestartIntent() {
+ Intent intent = super.createRestartIntent();
+ // mMessageData is discarded, request a fresh STATUS SMS for retries.
+ return intent;
+ }
+
+ @Override
+ @WorkerThread
+ public void onExecuteInBackgroundThread() {
+ Assert.isNotMainThread();
+
+ PhoneAccountHandle phoneAccountHandle = getPhoneAccountHandle();
+ if (phoneAccountHandle == null) {
+ // This should never happen
+ VvmLog.e(TAG, "null PhoneAccountHandle");
+ return;
+ }
+
+ OmtpVvmCarrierConfigHelper helper =
+ new OmtpVvmCarrierConfigHelper(getContext(), phoneAccountHandle);
+ if (!helper.isValid()) {
+ VvmLog.i(TAG, "VVM not supported on phoneAccountHandle " + phoneAccountHandle);
+ VvmAccountManager.removeAccount(getContext(), phoneAccountHandle);
+ return;
+ }
+
+ // OmtpVvmCarrierConfigHelper can start the activation process; it will pass in a vvm
+ // content provider URI which we will use. On some occasions, setting that URI will
+ // fail, so we will perform a few attempts to ensure that the vvm content provider has
+ // a good chance of being started up.
+ if (!VoicemailStatus.edit(getContext(), phoneAccountHandle)
+ .setType(helper.getVvmType())
+ .apply()) {
+ VvmLog.e(TAG, "Failed to configure content provider - " + helper.getVvmType());
+ fail();
+ }
+ VvmLog.i(TAG, "VVM content provider configured - " + helper.getVvmType());
+
+ if (VvmAccountManager.isAccountActivated(getContext(), phoneAccountHandle)) {
+ VvmLog.i(TAG, "Account is already activated");
+ return;
+ }
+ helper.handleEvent(
+ VoicemailStatus.edit(getContext(), phoneAccountHandle), OmtpEvents.CONFIG_ACTIVATING);
+
+ if (!hasSignal(getContext(), phoneAccountHandle)) {
+ VvmLog.i(TAG, "Service lost during activation, aborting");
+ // Restore the "NO SIGNAL" state since it will be overwritten by the CONFIG_ACTIVATING
+ // event.
+ helper.handleEvent(
+ VoicemailStatus.edit(getContext(), phoneAccountHandle),
+ OmtpEvents.NOTIFICATION_SERVICE_LOST);
+ // Don't retry, a new activation will be started after the signal returned.
+ return;
+ }
+
+ helper.activateSmsFilter();
+ VoicemailStatus.Editor status = mRetryPolicy.getVoicemailStatusEditor();
+
+ VisualVoicemailProtocol protocol = helper.getProtocol();
+
+ Bundle data;
+ if (mMessageData != null) {
+ // The content of STATUS SMS is provided to launch this task, no need to request it
+ // again.
+ data = mMessageData;
+ } else {
+ try (StatusSmsFetcher fetcher = new StatusSmsFetcher(getContext(), phoneAccountHandle)) {
+ protocol.startActivation(helper, fetcher.getSentIntent());
+ // Both the fetcher and OmtpMessageReceiver will be triggered, but
+ // OmtpMessageReceiver will just route the SMS back to ActivationTask, which will be
+ // rejected because the task is still running.
+ data = fetcher.get();
+ } catch (TimeoutException e) {
+ // The carrier is expected to return an STATUS SMS within STATUS_SMS_TIMEOUT_MILLIS
+ // handleEvent() will do the logging.
+ helper.handleEvent(status, OmtpEvents.CONFIG_STATUS_SMS_TIME_OUT);
+ fail();
+ return;
+ } catch (CancellationException e) {
+ VvmLog.e(TAG, "Unable to send status request SMS");
+ fail();
+ return;
+ } catch (InterruptedException | ExecutionException | IOException e) {
+ VvmLog.e(TAG, "can't get future STATUS SMS", e);
+ fail();
+ return;
+ }
+ }
+
+ StatusMessage message = new StatusMessage(data);
+ VvmLog.d(
+ TAG,
+ "STATUS SMS received: st="
+ + message.getProvisioningStatus()
+ + ", rc="
+ + message.getReturnCode());
+
+ if (message.getProvisioningStatus().equals(OmtpConstants.SUBSCRIBER_READY)) {
+ VvmLog.d(TAG, "subscriber ready, no activation required");
+ updateSource(getContext(), phoneAccountHandle, status, message);
+ } else {
+ if (helper.supportsProvisioning()) {
+ VvmLog.i(TAG, "Subscriber not ready, start provisioning");
+ helper.startProvisioning(this, phoneAccountHandle, status, message, data);
+
+ } else if (message.getProvisioningStatus().equals(OmtpConstants.SUBSCRIBER_NEW)) {
+ VvmLog.i(TAG, "Subscriber new but provisioning is not supported");
+ // Ignore the non-ready state and attempt to use the provided info as is.
+ // This is probably caused by not completing the new user tutorial.
+ updateSource(getContext(), phoneAccountHandle, status, message);
+ } else {
+ VvmLog.i(TAG, "Subscriber not ready but provisioning is not supported");
+ helper.handleEvent(status, OmtpEvents.CONFIG_SERVICE_NOT_AVAILABLE);
+ }
+ }
+ }
+
+ public static void updateSource(
+ Context context,
+ PhoneAccountHandle phone,
+ VoicemailStatus.Editor status,
+ StatusMessage message) {
+
+ if (OmtpConstants.SUCCESS.equals(message.getReturnCode())) {
+ OmtpVvmCarrierConfigHelper helper = new OmtpVvmCarrierConfigHelper(context, phone);
+ helper.handleEvent(status, OmtpEvents.CONFIG_REQUEST_STATUS_SUCCESS);
+
+ // Save the IMAP credentials in preferences so they are persistent and can be retrieved.
+ VvmAccountManager.addAccount(context, phone, message);
+
+ SyncTask.start(context, phone, OmtpVvmSyncService.SYNC_FULL_SYNC);
+ } else {
+ VvmLog.e(TAG, "Visual voicemail not available for subscriber.");
+ }
+ }
+
+ private static boolean hasSignal(Context context, PhoneAccountHandle phoneAccountHandle) {
+ TelephonyManager telephonyManager =
+ context
+ .getSystemService(TelephonyManager.class)
+ .createForPhoneAccountHandle(phoneAccountHandle);
+ return telephonyManager.getServiceState().getState() == ServiceState.STATE_IN_SERVICE;
+ }
+
+ private static void queueActivationAfterProvisioned(
+ Context context, PhoneAccountHandle phoneAccountHandle) {
+ if (sDeviceProvisionedObserver == null) {
+ sDeviceProvisionedObserver = new DeviceProvisionedObserver(context);
+ context
+ .getContentResolver()
+ .registerContentObserver(
+ Settings.Global.getUriFor(Global.DEVICE_PROVISIONED),
+ false,
+ sDeviceProvisionedObserver);
+ }
+ sDeviceProvisionedObserver.addPhoneAcountHandle(phoneAccountHandle);
+ }
+
+ private static class DeviceProvisionedObserver extends ContentObserver {
+
+ private final Context mContext;
+ private final Set<PhoneAccountHandle> mPhoneAccountHandles = new HashSet<>();
+
+ private DeviceProvisionedObserver(Context context) {
+ super(null);
+ mContext = context;
+ }
+
+ public void addPhoneAcountHandle(PhoneAccountHandle phoneAccountHandle) {
+ mPhoneAccountHandles.add(phoneAccountHandle);
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ if (isDeviceProvisioned(mContext)) {
+ VvmLog.i(TAG, "device provisioned, resuming activation");
+ for (PhoneAccountHandle phoneAccountHandle : mPhoneAccountHandles) {
+ start(mContext, phoneAccountHandle, null);
+ }
+ mContext.getContentResolver().unregisterContentObserver(sDeviceProvisionedObserver);
+ sDeviceProvisionedObserver = null;
+ }
+ }
+ }
+}
diff --git a/java/com/android/voicemail/impl/AndroidManifest.xml b/java/com/android/voicemail/impl/AndroidManifest.xml
new file mode 100644
index 000000000..0d90d5932
--- /dev/null
+++ b/java/com/android/voicemail/impl/AndroidManifest.xml
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ package="com.android.voicemailomtp"
+ >
+
+ <application
+ android:allowBackup="false"
+ android:supportsRtl="true"
+ android:usesCleartextTraffic="true"
+ android:defaultToDeviceProtectedStorage="true"
+ android:directBootAware="true">
+
+ <!-- Causes the "Voicemail" item under "Calls" setting to be hidden. The voicemail module will
+ be handling the settings. Has no effect before OC where dialer cannot provide voicemail
+ settings-->
+ <meta-data android:name="android.telephony.HIDE_VOICEMAIL_SETTINGS_MENU" android:value="true"/>
+
+ <receiver
+ android:name="com.android.voicemail.impl.sms.OmtpMessageReceiver"
+ android:exported="false"
+ androidprv:systemUserOnly="true">
+ <intent-filter>
+ <action android:name="com.android.vociemailomtp.sms.sms_received"/>
+ </intent-filter>
+ </receiver>
+
+ <receiver android:name="com.android.voicemail.impl.VoicemailClientReceiver"
+ android:exported="false">
+ <intent-filter>
+ <action android:name="com.android.voicemailomtp.VoicemailClient.ACTION_UPLOAD"/>
+ </intent-filter>
+ </receiver>
+
+ <receiver
+ android:name="com.android.voicemail.impl.fetch.FetchVoicemailReceiver"
+ android:exported="true"
+ android:permission="com.android.voicemail.permission.READ_VOICEMAIL"
+ androidprv:systemUserOnly="true">
+ <intent-filter>
+ <action android:name="android.intent.action.FETCH_VOICEMAIL"/>
+ <data
+ android:scheme="content"
+ android:host="com.android.voicemail"
+ android:mimeType="vnd.android.cursor.item/voicemail"/>
+ </intent-filter>
+ </receiver>
+ <receiver
+ android:name="com.android.voicemail.impl.sync.OmtpVvmSyncReceiver"
+ android:exported="true"
+ android:permission="com.android.voicemail.permission.READ_VOICEMAIL"
+ androidprv:systemUserOnly="true">
+ <intent-filter>
+ <action android:name="android.provider.action.SYNC_VOICEMAIL"/>
+ </intent-filter>
+ </receiver>
+ <receiver
+ android:name="com.android.voicemail.impl.sync.VoicemailProviderChangeReceiver"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.PROVIDER_CHANGED"/>
+ <data
+ android:scheme="content"
+ android:host="com.android.voicemail"
+ android:mimeType="vnd.android.cursor.dir/voicemails"/>
+ </intent-filter>
+ </receiver>
+
+ <service
+ android:name="com.android.voicemail.impl.scheduling.TaskSchedulerService"
+ android:exported="false"/>
+
+ <service
+ android:name="com.android.voicemail.impl.OmtpService"
+ android:permission="android.permission.BIND_VISUAL_VOICEMAIL_SERVICE"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.telephony.VisualVoicemailService"/>
+ </intent-filter>
+ </service>
+
+ <activity
+ android:name="com.android.voicemail.impl.settings.VoicemailChangePinActivity"
+ android:exported="false"
+ android:windowSoftInputMode="stateVisible|adjustResize">
+ </activity>
+ </application>
+</manifest>
diff --git a/java/com/android/voicemail/impl/Assert.java b/java/com/android/voicemail/impl/Assert.java
new file mode 100644
index 000000000..fe063727a
--- /dev/null
+++ b/java/com/android/voicemail/impl/Assert.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl;
+
+import android.os.Looper;
+
+/** Assertions which will result in program termination. */
+public class Assert {
+
+ private static Boolean sIsMainThreadForTest;
+
+ public static void isTrue(boolean condition) {
+ if (!condition) {
+ throw new AssertionError("Expected condition to be true");
+ }
+ }
+
+ public static void isMainThread() {
+ if (sIsMainThreadForTest != null) {
+ isTrue(sIsMainThreadForTest);
+ return;
+ }
+ isTrue(Looper.getMainLooper().equals(Looper.myLooper()));
+ }
+
+ public static void isNotMainThread() {
+ if (sIsMainThreadForTest != null) {
+ isTrue(!sIsMainThreadForTest);
+ return;
+ }
+ isTrue(!Looper.getMainLooper().equals(Looper.myLooper()));
+ }
+
+ public static void fail() {
+ throw new AssertionError("Fail");
+ }
+
+ /** Override the main thread status for tests. Set to null to revert to normal behavior */
+ @NeededForTesting
+ public static void setIsMainThreadForTesting(Boolean isMainThread) {
+ sIsMainThreadForTest = isMainThread;
+ }
+}
diff --git a/java/com/android/voicemail/impl/DefaultOmtpEventHandler.java b/java/com/android/voicemail/impl/DefaultOmtpEventHandler.java
new file mode 100644
index 000000000..13aaf0588
--- /dev/null
+++ b/java/com/android/voicemail/impl/DefaultOmtpEventHandler.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl;
+
+import android.content.Context;
+import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Status;
+import com.android.voicemail.impl.OmtpEvents.Type;
+
+public class DefaultOmtpEventHandler {
+
+ private static final String TAG = "DefErrorCodeHandler";
+
+ public static void handleEvent(
+ Context context,
+ OmtpVvmCarrierConfigHelper config,
+ VoicemailStatus.Editor status,
+ OmtpEvents event) {
+ switch (event.getType()) {
+ case Type.CONFIGURATION:
+ handleConfigurationEvent(context, status, event);
+ break;
+ case Type.DATA_CHANNEL:
+ handleDataChannelEvent(context, status, event);
+ break;
+ case Type.NOTIFICATION_CHANNEL:
+ handleNotificationChannelEvent(context, config, status, event);
+ break;
+ case Type.OTHER:
+ handleOtherEvent(context, status, event);
+ break;
+ default:
+ VvmLog.wtf(TAG, "invalid event type " + event.getType() + " for " + event);
+ }
+ }
+
+ private static void handleConfigurationEvent(
+ Context context, VoicemailStatus.Editor status, OmtpEvents event) {
+ switch (event) {
+ case CONFIG_DEFAULT_PIN_REPLACED:
+ case CONFIG_REQUEST_STATUS_SUCCESS:
+ case CONFIG_PIN_SET:
+ status
+ .setConfigurationState(VoicemailContract.Status.CONFIGURATION_STATE_OK)
+ .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_OK)
+ .apply();
+ break;
+ case CONFIG_ACTIVATING:
+ // Wipe all errors from the last activation. All errors shown should be new errors
+ // for this activation.
+ status
+ .setConfigurationState(Status.CONFIGURATION_STATE_CONFIGURING)
+ .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_OK)
+ .setDataChannelState(Status.DATA_CHANNEL_STATE_OK)
+ .apply();
+ break;
+ case CONFIG_ACTIVATING_SUBSEQUENT:
+ status
+ .setConfigurationState(Status.CONFIGURATION_STATE_OK)
+ .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_OK)
+ .setDataChannelState(Status.DATA_CHANNEL_STATE_OK)
+ .apply();
+ break;
+ case CONFIG_SERVICE_NOT_AVAILABLE:
+ status.setConfigurationState(Status.CONFIGURATION_STATE_FAILED).apply();
+ break;
+ case CONFIG_STATUS_SMS_TIME_OUT:
+ status.setConfigurationState(Status.CONFIGURATION_STATE_FAILED).apply();
+ break;
+ default:
+ VvmLog.wtf(TAG, "invalid configuration event " + event);
+ }
+ }
+
+ private static void handleDataChannelEvent(
+ Context context, VoicemailStatus.Editor status, OmtpEvents event) {
+ switch (event) {
+ case DATA_IMAP_OPERATION_STARTED:
+ case DATA_IMAP_OPERATION_COMPLETED:
+ status.setDataChannelState(Status.DATA_CHANNEL_STATE_OK).apply();
+ break;
+
+ case DATA_NO_CONNECTION:
+ status.setDataChannelState(Status.DATA_CHANNEL_STATE_NO_CONNECTION).apply();
+ break;
+
+ case DATA_NO_CONNECTION_CELLULAR_REQUIRED:
+ status
+ .setDataChannelState(Status.DATA_CHANNEL_STATE_NO_CONNECTION_CELLULAR_REQUIRED)
+ .apply();
+ break;
+ case DATA_INVALID_PORT:
+ status
+ .setDataChannelState(VoicemailContract.Status.DATA_CHANNEL_STATE_BAD_CONFIGURATION)
+ .apply();
+ break;
+ case DATA_CANNOT_RESOLVE_HOST_ON_NETWORK:
+ status
+ .setDataChannelState(
+ VoicemailContract.Status.DATA_CHANNEL_STATE_SERVER_CONNECTION_ERROR)
+ .apply();
+ break;
+ case DATA_SSL_INVALID_HOST_NAME:
+ case DATA_CANNOT_ESTABLISH_SSL_SESSION:
+ case DATA_IOE_ON_OPEN:
+ case DATA_GENERIC_IMAP_IOE:
+ status
+ .setDataChannelState(VoicemailContract.Status.DATA_CHANNEL_STATE_COMMUNICATION_ERROR)
+ .apply();
+ break;
+ case DATA_BAD_IMAP_CREDENTIAL:
+ case DATA_AUTH_UNKNOWN_USER:
+ case DATA_AUTH_UNKNOWN_DEVICE:
+ case DATA_AUTH_INVALID_PASSWORD:
+ case DATA_AUTH_MAILBOX_NOT_INITIALIZED:
+ case DATA_AUTH_SERVICE_NOT_PROVISIONED:
+ case DATA_AUTH_SERVICE_NOT_ACTIVATED:
+ case DATA_AUTH_USER_IS_BLOCKED:
+ status
+ .setDataChannelState(VoicemailContract.Status.DATA_CHANNEL_STATE_BAD_CONFIGURATION)
+ .apply();
+ break;
+
+ case DATA_REJECTED_SERVER_RESPONSE:
+ case DATA_INVALID_INITIAL_SERVER_RESPONSE:
+ case DATA_MAILBOX_OPEN_FAILED:
+ case DATA_SSL_EXCEPTION:
+ case DATA_ALL_SOCKET_CONNECTION_FAILED:
+ status
+ .setDataChannelState(VoicemailContract.Status.DATA_CHANNEL_STATE_SERVER_ERROR)
+ .apply();
+ break;
+
+ default:
+ VvmLog.wtf(TAG, "invalid data channel event " + event);
+ }
+ }
+
+ private static void handleNotificationChannelEvent(
+ Context context,
+ OmtpVvmCarrierConfigHelper config,
+ VoicemailStatus.Editor status,
+ OmtpEvents event) {
+ switch (event) {
+ case NOTIFICATION_IN_SERVICE:
+ status
+ .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_OK)
+ // Clear the error state. A sync should follow signal return so any error
+ // will be reposted.
+ .setDataChannelState(Status.DATA_CHANNEL_STATE_OK)
+ .apply();
+ break;
+ case NOTIFICATION_SERVICE_LOST:
+ status.setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION);
+ if (config.isCellularDataRequired()) {
+ status.setDataChannelState(Status.DATA_CHANNEL_STATE_NO_CONNECTION_CELLULAR_REQUIRED);
+ }
+ status.apply();
+ break;
+ default:
+ VvmLog.wtf(TAG, "invalid notification channel event " + event);
+ }
+ }
+
+ private static void handleOtherEvent(
+ Context context, VoicemailStatus.Editor status, OmtpEvents event) {
+ switch (event) {
+ case OTHER_SOURCE_REMOVED:
+ status
+ .setConfigurationState(Status.CONFIGURATION_STATE_NOT_CONFIGURED)
+ .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION)
+ .setDataChannelState(Status.DATA_CHANNEL_STATE_NO_CONNECTION)
+ .apply();
+ break;
+ default:
+ VvmLog.wtf(TAG, "invalid other event " + event);
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/NeededForTesting.java b/java/com/android/voicemail/impl/NeededForTesting.java
index 20517fed8..70e738385 100644
--- a/java/com/android/voicemailomtp/NeededForTesting.java
+++ b/java/com/android/voicemail/impl/NeededForTesting.java
@@ -14,12 +14,10 @@
* limitations under the License
*/
-package com.android.voicemailomtp;
+package com.android.voicemail.impl;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.SOURCE)
-public @interface NeededForTesting {
-
-}
+public @interface NeededForTesting {}
diff --git a/java/com/android/voicemail/impl/OmtpConstants.java b/java/com/android/voicemail/impl/OmtpConstants.java
new file mode 100644
index 000000000..599d0d5f0
--- /dev/null
+++ b/java/com/android/voicemail/impl/OmtpConstants.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl;
+
+import android.support.annotation.IntDef;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Wrapper class to hold relevant OMTP constants as defined in the OMTP spec.
+ *
+ * <p>In essence this is a programmatic representation of the relevant portions of OMTP spec.
+ */
+public class OmtpConstants {
+ public static final String SMS_FIELD_SEPARATOR = ";";
+ public static final String SMS_KEY_VALUE_SEPARATOR = "=";
+ public static final String SMS_PREFIX_SEPARATOR = ":";
+
+ public static final String SYNC_SMS_PREFIX = "SYNC";
+ public static final String STATUS_SMS_PREFIX = "STATUS";
+
+ // This is the format designated by the OMTP spec.
+ public static final String DATE_TIME_FORMAT = "dd/MM/yyyy HH:mm Z";
+
+ /** OMTP protocol versions. */
+ public static final String PROTOCOL_VERSION1_1 = "11";
+
+ public static final String PROTOCOL_VERSION1_2 = "12";
+ public static final String PROTOCOL_VERSION1_3 = "13";
+
+ ///////////////////////// Client/Mobile originated SMS //////////////////////
+
+ /** Mobile Originated requests */
+ public static final String ACTIVATE_REQUEST = "Activate";
+
+ public static final String DEACTIVATE_REQUEST = "Deactivate";
+ public static final String STATUS_REQUEST = "Status";
+
+ /** fields that can be present in a Mobile Originated OMTP SMS */
+ public static final String CLIENT_TYPE = "ct";
+
+ public static final String APPLICATION_PORT = "pt";
+ public static final String PROTOCOL_VERSION = "pv";
+
+ //////////////////////////////// Sync SMS fields ////////////////////////////
+
+ /**
+ * Sync SMS fields.
+ *
+ * <p>Each string constant is the field's key in the SMS body which is used by the parser to
+ * identify the field's value, if present, in the SMS body.
+ */
+
+ /** The event that triggered this SYNC SMS. See {@link OmtpConstants#SYNC_TRIGGER_EVENT_VALUES} */
+ public static final String SYNC_TRIGGER_EVENT = "ev";
+
+ public static final String MESSAGE_UID = "id";
+ public static final String MESSAGE_LENGTH = "l";
+ public static final String NUM_MESSAGE_COUNT = "c";
+ /** See {@link OmtpConstants#CONTENT_TYPE_VALUES} */
+ public static final String CONTENT_TYPE = "t";
+
+ public static final String SENDER = "s";
+ public static final String TIME = "dt";
+
+ /**
+ * SYNC message trigger events.
+ *
+ * <p>These are the possible values of {@link OmtpConstants#SYNC_TRIGGER_EVENT}.
+ */
+ public static final String NEW_MESSAGE = "NM";
+
+ public static final String MAILBOX_UPDATE = "MBU";
+ public static final String GREETINGS_UPDATE = "GU";
+
+ public static final String[] SYNC_TRIGGER_EVENT_VALUES = {
+ NEW_MESSAGE, MAILBOX_UPDATE, GREETINGS_UPDATE
+ };
+
+ /**
+ * Content types supported by OMTP VVM.
+ *
+ * <p>These are the possible values of {@link OmtpConstants#CONTENT_TYPE}.
+ */
+ public static final String VOICE = "v";
+
+ public static final String VIDEO = "o";
+ public static final String FAX = "f";
+ /** Voice message deposited by an external application */
+ public static final String INFOTAINMENT = "i";
+ /** Empty Call Capture - i.e. voicemail with no voice message. */
+ public static final String ECC = "e";
+
+ public static final String[] CONTENT_TYPE_VALUES = {VOICE, VIDEO, FAX, INFOTAINMENT, ECC};
+
+ ////////////////////////////// Status SMS fields ////////////////////////////
+
+ /**
+ * Status SMS fields.
+ *
+ * <p>Each string constant is the field's key in the SMS body which is used by the parser to
+ * identify the field's value, if present, in the SMS body.
+ */
+ /** See {@link OmtpConstants#PROVISIONING_STATUS_VALUES} */
+ public static final String PROVISIONING_STATUS = "st";
+ /** See {@link OmtpConstants#RETURN_CODE_VALUES} */
+ public static final String RETURN_CODE = "rc";
+ /** URL to send users to for activation VVM */
+ public static final String SUBSCRIPTION_URL = "rs";
+ /** IMAP4/SMTP server IP address or fully qualified domain name */
+ public static final String SERVER_ADDRESS = "srv";
+ /** Phone number to access voicemails through Telephony User Interface */
+ public static final String TUI_ACCESS_NUMBER = "tui";
+
+ public static final String TUI_PASSWORD_LENGTH = "pw_len";
+ /** Number to send client origination SMS */
+ public static final String CLIENT_SMS_DESTINATION_NUMBER = "dn";
+
+ public static final String IMAP_PORT = "ipt";
+ public static final String IMAP_USER_NAME = "u";
+ public static final String IMAP_PASSWORD = "pw";
+ public static final String SMTP_PORT = "spt";
+ public static final String SMTP_USER_NAME = "smtp_u";
+ public static final String SMTP_PASSWORD = "smtp_pw";
+
+ /**
+ * User provisioning status values.
+ *
+ * <p>Referred by {@link OmtpConstants#PROVISIONING_STATUS}.
+ */
+ public static final String SUBSCRIBER_NEW = "N";
+
+ public static final String SUBSCRIBER_READY = "R";
+ public static final String SUBSCRIBER_PROVISIONED = "P";
+ public static final String SUBSCRIBER_UNKNOWN = "U";
+ public static final String SUBSCRIBER_BLOCKED = "B";
+
+ public static final String[] PROVISIONING_STATUS_VALUES = {
+ SUBSCRIBER_NEW, SUBSCRIBER_READY, SUBSCRIBER_PROVISIONED, SUBSCRIBER_UNKNOWN, SUBSCRIBER_BLOCKED
+ };
+
+ /**
+ * The return code included in a status message.
+ *
+ * <p>These are the possible values of {@link OmtpConstants#RETURN_CODE}.
+ */
+ public static final String SUCCESS = "0";
+
+ public static final String SYSTEM_ERROR = "1";
+ public static final String SUBSCRIBER_ERROR = "2";
+ public static final String MAILBOX_UNKNOWN = "3";
+ public static final String VVM_NOT_ACTIVATED = "4";
+ public static final String VVM_NOT_PROVISIONED = "5";
+ public static final String VVM_CLIENT_UKNOWN = "6";
+ public static final String VVM_MAILBOX_NOT_INITIALIZED = "7";
+
+ public static final String[] RETURN_CODE_VALUES = {
+ SUCCESS,
+ SYSTEM_ERROR,
+ SUBSCRIBER_ERROR,
+ MAILBOX_UNKNOWN,
+ VVM_NOT_ACTIVATED,
+ VVM_NOT_PROVISIONED,
+ VVM_CLIENT_UKNOWN,
+ VVM_MAILBOX_NOT_INITIALIZED,
+ };
+
+ /** IMAP command extensions */
+
+ /**
+ * OMTP spec v1.3 2.3.1 Change password request syntax
+ *
+ * <p>This changes the PIN to access the Telephone User Interface, the traditional voicemail
+ * system.
+ */
+ public static final String IMAP_CHANGE_TUI_PWD_FORMAT = "XCHANGE_TUI_PWD PWD=%1$s OLD_PWD=%2$s";
+
+ /**
+ * OMTP spec v1.3 2.4.1 Change languate request syntax
+ *
+ * <p>This changes the language in the Telephone User Interface.
+ */
+ public static final String IMAP_CHANGE_VM_LANG_FORMAT = "XCHANGE_VM_LANG LANG=%1$s";
+
+ /**
+ * OMTP spec v1.3 2.5.1 Close NUT Request syntax
+ *
+ * <p>This disables the new user tutorial, the message played to new users calling in the
+ * Telephone User Interface.
+ */
+ public static final String IMAP_CLOSE_NUT = "XCLOSE_NUT";
+
+ /** Possible NO responses for CHANGE_TUI_PWD */
+ public static final String RESPONSE_CHANGE_PIN_TOO_SHORT = "password too short";
+
+ public static final String RESPONSE_CHANGE_PIN_TOO_LONG = "password too long";
+ public static final String RESPONSE_CHANGE_PIN_TOO_WEAK = "password too weak";
+ public static final String RESPONSE_CHANGE_PIN_MISMATCH = "old password mismatch";
+ public static final String RESPONSE_CHANGE_PIN_INVALID_CHARACTER =
+ "password contains invalid characters";
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ value = {
+ CHANGE_PIN_SUCCESS,
+ CHANGE_PIN_TOO_SHORT,
+ CHANGE_PIN_TOO_LONG,
+ CHANGE_PIN_TOO_WEAK,
+ CHANGE_PIN_MISMATCH,
+ CHANGE_PIN_INVALID_CHARACTER,
+ CHANGE_PIN_SYSTEM_ERROR
+ }
+ )
+ public @interface ChangePinResult {}
+
+ public static final int CHANGE_PIN_SUCCESS = 0;
+ public static final int CHANGE_PIN_TOO_SHORT = 1;
+ public static final int CHANGE_PIN_TOO_LONG = 2;
+ public static final int CHANGE_PIN_TOO_WEAK = 3;
+ public static final int CHANGE_PIN_MISMATCH = 4;
+ public static final int CHANGE_PIN_INVALID_CHARACTER = 5;
+ public static final int CHANGE_PIN_SYSTEM_ERROR = 6;
+
+ /** Indicates the client is Google visual voicemail version 1.0. */
+ public static final String CLIENT_TYPE_GOOGLE_10 = "google.vvm.10";
+}
diff --git a/java/com/android/voicemail/impl/OmtpEvents.java b/java/com/android/voicemail/impl/OmtpEvents.java
new file mode 100644
index 000000000..6807edcf0
--- /dev/null
+++ b/java/com/android/voicemail/impl/OmtpEvents.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl;
+
+import android.support.annotation.IntDef;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Events internal to the OMTP client. These should be translated into {@link
+ * android.provider.VoicemailContract.Status} error codes before writing into the voicemail status
+ * table.
+ */
+public enum OmtpEvents {
+
+ // Configuration State
+ CONFIG_REQUEST_STATUS_SUCCESS(Type.CONFIGURATION, true),
+
+ CONFIG_PIN_SET(Type.CONFIGURATION, true),
+ // The voicemail PIN is replaced with a generated PIN, user should change it.
+ CONFIG_DEFAULT_PIN_REPLACED(Type.CONFIGURATION, true),
+ CONFIG_ACTIVATING(Type.CONFIGURATION, true),
+ // There are already activation records, this is only a book-keeping activation.
+ CONFIG_ACTIVATING_SUBSEQUENT(Type.CONFIGURATION, true),
+ CONFIG_STATUS_SMS_TIME_OUT(Type.CONFIGURATION),
+ CONFIG_SERVICE_NOT_AVAILABLE(Type.CONFIGURATION),
+
+ // Data channel State
+
+ // A new sync has started, old errors in data channel should be cleared.
+ DATA_IMAP_OPERATION_STARTED(Type.DATA_CHANNEL, true),
+ // Successfully downloaded/uploaded data from the server, which means the data channel is clear.
+ DATA_IMAP_OPERATION_COMPLETED(Type.DATA_CHANNEL, true),
+ // The port provided in the STATUS SMS is invalid.
+ DATA_INVALID_PORT(Type.DATA_CHANNEL),
+ // No connection to the internet, and the carrier requires cellular data
+ DATA_NO_CONNECTION_CELLULAR_REQUIRED(Type.DATA_CHANNEL),
+ // No connection to the internet.
+ DATA_NO_CONNECTION(Type.DATA_CHANNEL),
+ // Address lookup for the server hostname failed. DNS error?
+ DATA_CANNOT_RESOLVE_HOST_ON_NETWORK(Type.DATA_CHANNEL),
+ // All destination address that resolves to the server hostname are rejected or timed out
+ DATA_ALL_SOCKET_CONNECTION_FAILED(Type.DATA_CHANNEL),
+ // Failed to establish SSL with the server, either with a direct SSL connection or by
+ // STARTTLS command
+ DATA_CANNOT_ESTABLISH_SSL_SESSION(Type.DATA_CHANNEL),
+ // Identity of the server cannot be verified.
+ DATA_SSL_INVALID_HOST_NAME(Type.DATA_CHANNEL),
+ // The server rejected our username/password
+ DATA_BAD_IMAP_CREDENTIAL(Type.DATA_CHANNEL),
+
+ DATA_AUTH_UNKNOWN_USER(Type.DATA_CHANNEL),
+ DATA_AUTH_UNKNOWN_DEVICE(Type.DATA_CHANNEL),
+ DATA_AUTH_INVALID_PASSWORD(Type.DATA_CHANNEL),
+ DATA_AUTH_MAILBOX_NOT_INITIALIZED(Type.DATA_CHANNEL),
+ DATA_AUTH_SERVICE_NOT_PROVISIONED(Type.DATA_CHANNEL),
+ DATA_AUTH_SERVICE_NOT_ACTIVATED(Type.DATA_CHANNEL),
+ DATA_AUTH_USER_IS_BLOCKED(Type.DATA_CHANNEL),
+
+ // A command to the server didn't result with an "OK" or continuation request
+ DATA_REJECTED_SERVER_RESPONSE(Type.DATA_CHANNEL),
+ // The server did not greet us with a "OK", possibly not a IMAP server.
+ DATA_INVALID_INITIAL_SERVER_RESPONSE(Type.DATA_CHANNEL),
+ // An IOException occurred while trying to open an ImapConnection
+ // TODO: reduce scope
+ DATA_IOE_ON_OPEN(Type.DATA_CHANNEL),
+ // The SELECT command on a mailbox is rejected
+ DATA_MAILBOX_OPEN_FAILED(Type.DATA_CHANNEL),
+ // An IOException has occurred
+ // TODO: reduce scope
+ DATA_GENERIC_IMAP_IOE(Type.DATA_CHANNEL),
+ // An SslException has occurred while opening an ImapConnection
+ // TODO: reduce scope
+ DATA_SSL_EXCEPTION(Type.DATA_CHANNEL),
+
+ // Notification Channel
+
+ // Cell signal restored, can received VVM SMSs
+ NOTIFICATION_IN_SERVICE(Type.NOTIFICATION_CHANNEL, true),
+ // Cell signal lost, cannot received VVM SMSs
+ NOTIFICATION_SERVICE_LOST(Type.NOTIFICATION_CHANNEL, false),
+
+ // Other
+ OTHER_SOURCE_REMOVED(Type.OTHER, false),
+
+ // VVM3
+ VVM3_NEW_USER_SETUP_FAILED,
+ // Table 4. client internal error handling
+ VVM3_VMG_DNS_FAILURE,
+ VVM3_SPG_DNS_FAILURE,
+ VVM3_VMG_CONNECTION_FAILED,
+ VVM3_SPG_CONNECTION_FAILED,
+ VVM3_VMG_TIMEOUT,
+ VVM3_STATUS_SMS_TIMEOUT,
+
+ VVM3_SUBSCRIBER_PROVISIONED,
+ VVM3_SUBSCRIBER_BLOCKED,
+ VVM3_SUBSCRIBER_UNKNOWN;
+
+ public static class Type {
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({CONFIGURATION, DATA_CHANNEL, NOTIFICATION_CHANNEL, OTHER})
+ public @interface Values {}
+
+ public static final int CONFIGURATION = 1;
+ public static final int DATA_CHANNEL = 2;
+ public static final int NOTIFICATION_CHANNEL = 3;
+ public static final int OTHER = 4;
+ }
+
+ private final int mType;
+ private final boolean mIsSuccess;
+
+ OmtpEvents(int type, boolean isSuccess) {
+ mType = type;
+ mIsSuccess = isSuccess;
+ }
+
+ OmtpEvents(int type) {
+ mType = type;
+ mIsSuccess = false;
+ }
+
+ OmtpEvents() {
+ mType = Type.OTHER;
+ mIsSuccess = false;
+ }
+
+ @Type.Values
+ public int getType() {
+ return mType;
+ }
+
+ public boolean isSuccess() {
+ return mIsSuccess;
+ }
+}
diff --git a/java/com/android/voicemail/impl/OmtpService.java b/java/com/android/voicemail/impl/OmtpService.java
new file mode 100644
index 000000000..dfbd4cf42
--- /dev/null
+++ b/java/com/android/voicemail/impl/OmtpService.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl;
+
+import android.content.Intent;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.VisualVoicemailService;
+import android.telephony.VisualVoicemailSms;
+import com.android.voicemail.impl.sync.VvmAccountManager;
+
+public class OmtpService extends VisualVoicemailService {
+
+ private static final String TAG = "VvmOmtpService";
+
+ public static final String ACTION_SMS_RECEIVED = "com.android.vociemailomtp.sms.sms_received";
+
+ public static final String EXTRA_VOICEMAIL_SMS = "extra_voicemail_sms";
+
+ @Override
+ public void onCellServiceConnected(
+ VisualVoicemailTask task, final PhoneAccountHandle phoneAccountHandle) {
+ VvmLog.i(TAG, "onCellServiceConnected");
+ ActivationTask.start(OmtpService.this, phoneAccountHandle, null);
+ task.finish();
+ }
+
+ @Override
+ public void onSmsReceived(VisualVoicemailTask task, final VisualVoicemailSms sms) {
+ VvmLog.i(TAG, "onSmsReceived");
+ Intent intent = new Intent(ACTION_SMS_RECEIVED);
+ intent.setPackage(getPackageName());
+ intent.putExtra(EXTRA_VOICEMAIL_SMS, sms);
+ sendBroadcast(intent);
+ task.finish();
+ }
+
+ @Override
+ public void onSimRemoved(
+ final VisualVoicemailTask task, final PhoneAccountHandle phoneAccountHandle) {
+ VvmLog.i(TAG, "onSimRemoved");
+ VvmAccountManager.removeAccount(this, phoneAccountHandle);
+ task.finish();
+ }
+
+ @Override
+ public void onStopped(VisualVoicemailTask task) {
+ VvmLog.i(TAG, "onStopped");
+ }
+}
diff --git a/java/com/android/voicemail/impl/OmtpVvmCarrierConfigHelper.java b/java/com/android/voicemail/impl/OmtpVvmCarrierConfigHelper.java
new file mode 100644
index 000000000..0296d208d
--- /dev/null
+++ b/java/com/android/voicemail/impl/OmtpVvmCarrierConfigHelper.java
@@ -0,0 +1,444 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
+import android.os.PersistableBundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.TelephonyManager;
+import android.telephony.VisualVoicemailService;
+import android.telephony.VisualVoicemailSmsFilterSettings;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import com.android.dialer.common.Assert;
+import com.android.voicemail.impl.protocol.VisualVoicemailProtocol;
+import com.android.voicemail.impl.protocol.VisualVoicemailProtocolFactory;
+import com.android.voicemail.impl.sms.StatusMessage;
+import com.android.voicemail.impl.sync.VvmAccountManager;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * Manages carrier dependent visual voicemail configuration values. The primary source is the value
+ * retrieved from CarrierConfigManager. If CarrierConfigManager does not provide the config
+ * (KEY_VVM_TYPE_STRING is empty, or "hidden" configs), then the value hardcoded in telephony will
+ * be used (in res/xml/vvm_config.xml)
+ *
+ * <p>Hidden configs are new configs that are planned for future APIs, or miscellaneous settings
+ * that may clutter CarrierConfigManager too much.
+ *
+ * <p>The current hidden configs are: {@link #getSslPort()} {@link #getDisabledCapabilities()}
+ */
+public class OmtpVvmCarrierConfigHelper {
+
+ private static final String TAG = "OmtpVvmCarrierCfgHlpr";
+
+ static final String KEY_VVM_TYPE_STRING = CarrierConfigManager.KEY_VVM_TYPE_STRING;
+ static final String KEY_VVM_DESTINATION_NUMBER_STRING =
+ CarrierConfigManager.KEY_VVM_DESTINATION_NUMBER_STRING;
+ static final String KEY_VVM_PORT_NUMBER_INT = CarrierConfigManager.KEY_VVM_PORT_NUMBER_INT;
+ static final String KEY_CARRIER_VVM_PACKAGE_NAME_STRING =
+ CarrierConfigManager.KEY_CARRIER_VVM_PACKAGE_NAME_STRING;
+ static final String KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY =
+ "carrier_vvm_package_name_string_array";
+ static final String KEY_VVM_PREFETCH_BOOL = CarrierConfigManager.KEY_VVM_PREFETCH_BOOL;
+ static final String KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL =
+ CarrierConfigManager.KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL;
+
+ /** @see #getSslPort() */
+ static final String KEY_VVM_SSL_PORT_NUMBER_INT = "vvm_ssl_port_number_int";
+
+ /** @see #isLegacyModeEnabled() */
+ static final String KEY_VVM_LEGACY_MODE_ENABLED_BOOL = "vvm_legacy_mode_enabled_bool";
+
+ /**
+ * Ban a capability reported by the server from being used. The array of string should be a subset
+ * of the capabilities returned IMAP CAPABILITY command.
+ *
+ * @see #getDisabledCapabilities()
+ */
+ static final String KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY =
+ "vvm_disabled_capabilities_string_array";
+
+ static final String KEY_VVM_CLIENT_PREFIX_STRING = "vvm_client_prefix_string";
+
+ private final Context mContext;
+ private final PersistableBundle mCarrierConfig;
+ private final String mVvmType;
+ private final VisualVoicemailProtocol mProtocol;
+ private final PersistableBundle mTelephonyConfig;
+
+ private PhoneAccountHandle mPhoneAccountHandle;
+
+ public OmtpVvmCarrierConfigHelper(Context context, PhoneAccountHandle handle) {
+ mContext = context;
+ mPhoneAccountHandle = handle;
+ TelephonyManager telephonyManager =
+ context
+ .getSystemService(TelephonyManager.class)
+ .createForPhoneAccountHandle(mPhoneAccountHandle);
+ if (telephonyManager == null) {
+ VvmLog.e(TAG, "PhoneAccountHandle is invalid");
+ mCarrierConfig = null;
+ mTelephonyConfig = null;
+ mVvmType = null;
+ mProtocol = null;
+ return;
+ }
+
+ mCarrierConfig = getCarrierConfig(telephonyManager);
+ mTelephonyConfig =
+ new TelephonyVvmConfigManager(context.getResources())
+ .getConfig(telephonyManager.getSimOperator());
+
+ mVvmType = getVvmType();
+ mProtocol = VisualVoicemailProtocolFactory.create(mContext.getResources(), mVvmType);
+ }
+
+ @VisibleForTesting
+ OmtpVvmCarrierConfigHelper(
+ Context context, PersistableBundle carrierConfig, PersistableBundle telephonyConfig) {
+ mContext = context;
+ mCarrierConfig = carrierConfig;
+ mTelephonyConfig = telephonyConfig;
+ mVvmType = getVvmType();
+ mProtocol = VisualVoicemailProtocolFactory.create(mContext.getResources(), mVvmType);
+ }
+
+ public Context getContext() {
+ return mContext;
+ }
+
+ @Nullable
+ public PhoneAccountHandle getPhoneAccountHandle() {
+ return mPhoneAccountHandle;
+ }
+
+ /**
+ * return whether the carrier's visual voicemail is supported, with KEY_VVM_TYPE_STRING set as a
+ * known protocol.
+ */
+ public boolean isValid() {
+ return mProtocol != null;
+ }
+
+ @Nullable
+ public String getVvmType() {
+ return (String) getValue(KEY_VVM_TYPE_STRING);
+ }
+
+ @Nullable
+ public VisualVoicemailProtocol getProtocol() {
+ return mProtocol;
+ }
+
+ /** @returns arbitrary String stored in the config file. Used for protocol specific values. */
+ @Nullable
+ public String getString(String key) {
+ Assert.checkArgument(isValid());
+ return (String) getValue(key);
+ }
+
+ @Nullable
+ public Set<String> getCarrierVvmPackageNames() {
+ Assert.checkArgument(isValid());
+ Set<String> names = getCarrierVvmPackageNames(mCarrierConfig);
+ if (names != null) {
+ return names;
+ }
+ return getCarrierVvmPackageNames(mTelephonyConfig);
+ }
+
+ private static Set<String> getCarrierVvmPackageNames(@Nullable PersistableBundle bundle) {
+ if (bundle == null) {
+ return null;
+ }
+ Set<String> names = new ArraySet<>();
+ if (bundle.containsKey(KEY_CARRIER_VVM_PACKAGE_NAME_STRING)) {
+ names.add(bundle.getString(KEY_CARRIER_VVM_PACKAGE_NAME_STRING));
+ }
+ if (bundle.containsKey(KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY)) {
+ String[] vvmPackages = bundle.getStringArray(KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY);
+ if (vvmPackages != null && vvmPackages.length > 0) {
+ Collections.addAll(names, vvmPackages);
+ }
+ }
+ if (names.isEmpty()) {
+ return null;
+ }
+ return names;
+ }
+
+ /**
+ * For checking upon sim insertion whether visual voicemail should be enabled. This method does so
+ * by checking if the carrier's voicemail app is installed.
+ */
+ public boolean isEnabledByDefault() {
+ if (!isValid()) {
+ return false;
+ }
+
+ Set<String> carrierPackages = getCarrierVvmPackageNames();
+ if (carrierPackages == null) {
+ return true;
+ }
+ for (String packageName : carrierPackages) {
+ try {
+ mContext.getPackageManager().getPackageInfo(packageName, 0);
+ return false;
+ } catch (NameNotFoundException e) {
+ // Do nothing.
+ }
+ }
+ return true;
+ }
+
+ public boolean isCellularDataRequired() {
+ Assert.checkArgument(isValid());
+ return (boolean) getValue(KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL, false);
+ }
+
+ public boolean isPrefetchEnabled() {
+ Assert.checkArgument(isValid());
+ return (boolean) getValue(KEY_VVM_PREFETCH_BOOL, true);
+ }
+
+ public int getApplicationPort() {
+ Assert.checkArgument(isValid());
+ return (int) getValue(KEY_VVM_PORT_NUMBER_INT, 0);
+ }
+
+ @Nullable
+ public String getDestinationNumber() {
+ Assert.checkArgument(isValid());
+ return (String) getValue(KEY_VVM_DESTINATION_NUMBER_STRING);
+ }
+
+ /**
+ * @return Port to start a SSL IMAP connection directly.
+ */
+ public int getSslPort() {
+ Assert.checkArgument(isValid());
+ return (int) getValue(KEY_VVM_SSL_PORT_NUMBER_INT, 0);
+ }
+
+ /**
+ * Hidden Config.
+ *
+ * <p>Sometimes the server states it supports a certain feature but we found they have bug on the
+ * server side. For example, in b/28717550 the server reported AUTH=DIGEST-MD5 capability but
+ * using it to login will cause subsequent response to be erroneous.
+ *
+ * @return A set of capabilities that is reported by the IMAP CAPABILITY command, but determined
+ * to have issues and should not be used.
+ */
+ @Nullable
+ public Set<String> getDisabledCapabilities() {
+ Assert.checkArgument(isValid());
+ Set<String> disabledCapabilities = getDisabledCapabilities(mCarrierConfig);
+ if (disabledCapabilities != null) {
+ return disabledCapabilities;
+ }
+ return getDisabledCapabilities(mTelephonyConfig);
+ }
+
+ @Nullable
+ private static Set<String> getDisabledCapabilities(@Nullable PersistableBundle bundle) {
+ if (bundle == null) {
+ return null;
+ }
+ if (!bundle.containsKey(KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY)) {
+ return null;
+ }
+ String[] disabledCapabilities =
+ bundle.getStringArray(KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY);
+ if (disabledCapabilities != null && disabledCapabilities.length > 0) {
+ ArraySet<String> result = new ArraySet<>();
+ Collections.addAll(result, disabledCapabilities);
+ return result;
+ }
+ return null;
+ }
+
+ public String getClientPrefix() {
+ Assert.checkArgument(isValid());
+ String prefix = (String) getValue(KEY_VVM_CLIENT_PREFIX_STRING);
+ if (prefix != null) {
+ return prefix;
+ }
+ return "//VVM";
+ }
+
+ /**
+ * Should legacy mode be used when the OMTP VVM client is disabled?
+ *
+ * <p>Legacy mode is a mode that on the carrier side visual voicemail is still activated, but on
+ * the client side all network operations are disabled. SMSs are still monitored so a new message
+ * SYNC SMS will be translated to show a message waiting indicator, like traditional voicemails.
+ *
+ * <p>This is for carriers that does not support VVM deactivation so voicemail can continue to
+ * function without the data cost.
+ */
+ public boolean isLegacyModeEnabled() {
+ Assert.checkArgument(isValid());
+ return (boolean) getValue(KEY_VVM_LEGACY_MODE_ENABLED_BOOL, false);
+ }
+
+ public void startActivation() {
+ Assert.checkArgument(isValid());
+ PhoneAccountHandle phoneAccountHandle = getPhoneAccountHandle();
+ if (phoneAccountHandle == null) {
+ // This should never happen
+ // Error logged in getPhoneAccountHandle().
+ return;
+ }
+
+ if (mVvmType == null || mVvmType.isEmpty()) {
+ // The VVM type is invalid; we should never have gotten here in the first place since
+ // this is loaded initially in the constructor, and callers should check isValid()
+ // before trying to start activation anyways.
+ VvmLog.e(TAG, "startActivation : vvmType is null or empty for account " + phoneAccountHandle);
+ return;
+ }
+
+ if (mProtocol != null) {
+ ActivationTask.start(mContext, mPhoneAccountHandle, null);
+ }
+ }
+
+ public void activateSmsFilter() {
+ Assert.checkArgument(isValid());
+ VisualVoicemailService.setSmsFilterSettings(
+ mContext,
+ getPhoneAccountHandle(),
+ new VisualVoicemailSmsFilterSettings.Builder().setClientPrefix(getClientPrefix()).build());
+ }
+
+ public void startDeactivation() {
+ Assert.checkArgument(isValid());
+ if (!isLegacyModeEnabled()) {
+ // SMS should still be filtered in legacy mode
+ VisualVoicemailService.setSmsFilterSettings(mContext, getPhoneAccountHandle(), null);
+ }
+ if (mProtocol != null) {
+ mProtocol.startDeactivation(this);
+ }
+ VvmAccountManager.removeAccount(mContext, getPhoneAccountHandle());
+ }
+
+ public boolean supportsProvisioning() {
+ Assert.checkArgument(isValid());
+ return mProtocol.supportsProvisioning();
+ }
+
+ public void startProvisioning(
+ ActivationTask task,
+ PhoneAccountHandle phone,
+ VoicemailStatus.Editor status,
+ StatusMessage message,
+ Bundle data) {
+ Assert.checkArgument(isValid());
+ mProtocol.startProvisioning(task, phone, this, status, message, data);
+ }
+
+ public void requestStatus(@Nullable PendingIntent sentIntent) {
+ Assert.checkArgument(isValid());
+ mProtocol.requestStatus(this, sentIntent);
+ }
+
+ public void handleEvent(VoicemailStatus.Editor status, OmtpEvents event) {
+ Assert.checkArgument(isValid());
+ VvmLog.i(TAG, "OmtpEvent:" + event);
+ mProtocol.handleEvent(mContext, this, status, event);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder("OmtpVvmCarrierConfigHelper [");
+ builder
+ .append("phoneAccountHandle: ")
+ .append(mPhoneAccountHandle)
+ .append(", carrierConfig: ")
+ .append(mCarrierConfig != null)
+ .append(", telephonyConfig: ")
+ .append(mTelephonyConfig != null)
+ .append(", type: ")
+ .append(getVvmType())
+ .append(", destinationNumber: ")
+ .append(getDestinationNumber())
+ .append(", applicationPort: ")
+ .append(getApplicationPort())
+ .append(", sslPort: ")
+ .append(getSslPort())
+ .append(", isEnabledByDefault: ")
+ .append(isEnabledByDefault())
+ .append(", isCellularDataRequired: ")
+ .append(isCellularDataRequired())
+ .append(", isPrefetchEnabled: ")
+ .append(isPrefetchEnabled())
+ .append(", isLegacyModeEnabled: ")
+ .append(isLegacyModeEnabled())
+ .append("]");
+ return builder.toString();
+ }
+
+ @Nullable
+ private PersistableBundle getCarrierConfig(@NonNull TelephonyManager telephonyManager) {
+ CarrierConfigManager carrierConfigManager =
+ (CarrierConfigManager) mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE);
+ if (carrierConfigManager == null) {
+ VvmLog.w(TAG, "No carrier config service found.");
+ return null;
+ }
+
+ PersistableBundle config = telephonyManager.getCarrierConfig();
+
+ if (TextUtils.isEmpty(config.getString(CarrierConfigManager.KEY_VVM_TYPE_STRING))) {
+ return null;
+ }
+ return config;
+ }
+
+ @Nullable
+ private Object getValue(String key) {
+ return getValue(key, null);
+ }
+
+ @Nullable
+ private Object getValue(String key, Object defaultValue) {
+ Object result;
+ if (mCarrierConfig != null) {
+ result = mCarrierConfig.get(key);
+ if (result != null) {
+ return result;
+ }
+ }
+ if (mTelephonyConfig != null) {
+ result = mTelephonyConfig.get(key);
+ if (result != null) {
+ return result;
+ }
+ }
+ return defaultValue;
+ }
+}
diff --git a/java/com/android/voicemail/impl/SubscriptionInfoHelper.java b/java/com/android/voicemail/impl/SubscriptionInfoHelper.java
new file mode 100644
index 000000000..d8a8423eb
--- /dev/null
+++ b/java/com/android/voicemail/impl/SubscriptionInfoHelper.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl;
+
+import android.app.ActionBar;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.text.TextUtils;
+
+/**
+ * Helper for manipulating intents or components with subscription-related information.
+ *
+ * <p>In settings, subscription ids and labels are passed along to indicate that settings are being
+ * changed for particular subscriptions. This helper provides functions for helping extract this
+ * info and perform common operations using this info.
+ */
+public class SubscriptionInfoHelper {
+ public static final int NO_SUB_ID = -1;
+
+ // Extra on intent containing the id of a subscription.
+ public static final String SUB_ID_EXTRA =
+ "com.android.voicemailomtp.settings.SubscriptionInfoHelper.SubscriptionId";
+ // Extra on intent containing the label of a subscription.
+ private static final String SUB_LABEL_EXTRA =
+ "com.android.voicemailomtp.settings.SubscriptionInfoHelper.SubscriptionLabel";
+
+ private static Context mContext;
+
+ private static int mSubId = NO_SUB_ID;
+ private static String mSubLabel;
+
+ /** Instantiates the helper, by extracting the subscription id and label from the intent. */
+ public SubscriptionInfoHelper(Context context, Intent intent) {
+ mContext = context;
+ mSubId = intent.getIntExtra(SUB_ID_EXTRA, NO_SUB_ID);
+ mSubLabel = intent.getStringExtra(SUB_LABEL_EXTRA);
+ }
+
+ /**
+ * Sets the action bar title to the string specified by the given resource id, formatting it with
+ * the subscription label. This assumes the resource string is formattable with a string-type
+ * specifier.
+ *
+ * <p>If the subscription label does not exists, leave the existing title.
+ */
+ public void setActionBarTitle(ActionBar actionBar, Resources res, int resId) {
+ if (actionBar == null || TextUtils.isEmpty(mSubLabel)) {
+ return;
+ }
+
+ String title = String.format(res.getString(resId), mSubLabel);
+ actionBar.setTitle(title);
+ }
+
+ public int getSubId() {
+ return mSubId;
+ }
+}
diff --git a/java/com/android/voicemail/impl/TelephonyManagerStub.java b/java/com/android/voicemail/impl/TelephonyManagerStub.java
new file mode 100644
index 000000000..4762e9023
--- /dev/null
+++ b/java/com/android/voicemail/impl/TelephonyManagerStub.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl;
+
+import android.annotation.TargetApi;
+import android.os.Build.VERSION_CODES;
+
+/**
+ * Temporary stub for public APIs that should be added into telephony manager.
+ *
+ * <p>TODO(b/32637799) remove this.
+ */
+@TargetApi(VERSION_CODES.O)
+public class TelephonyManagerStub {
+
+ public static void showVoicemailNotification(int voicemailCount) {}
+
+ /**
+ * Dismisses the message waiting (voicemail) indicator.
+ *
+ * @param subId the subscription id we should dismiss the notification for.
+ */
+ public static void clearMwiIndicator(int subId) {}
+
+ public static void setShouldCheckVisualVoicemailConfigurationForMwi(int subId, boolean enabled) {}
+}
diff --git a/java/com/android/voicemail/impl/TelephonyMangerCompat.java b/java/com/android/voicemail/impl/TelephonyMangerCompat.java
new file mode 100644
index 000000000..353cd69e3
--- /dev/null
+++ b/java/com/android/voicemail/impl/TelephonyMangerCompat.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl;
+
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import java.lang.reflect.Method;
+
+/** Handles {@link TelephonyManager} API changes in experimental SDK */
+public class TelephonyMangerCompat {
+
+ private static final String GET_VISUAL_VOICEMAIL_PACKGE_NAME = "getVisualVoicemailPackageName";
+
+ /**
+ * Changed from getVisualVoicemailPackageName(PhoneAccountHandle) to
+ * getVisualVoicemailPackageName()
+ */
+ public static String getVisualVoicemailPackageName(TelephonyManager telephonyManager) {
+ try {
+ Method method = TelephonyManager.class.getMethod(GET_VISUAL_VOICEMAIL_PACKGE_NAME);
+ try {
+ return (String) method.invoke(telephonyManager);
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException(e);
+ }
+ } catch (NoSuchMethodException e) {
+ // Do nothing, try the next version.
+ }
+
+ try {
+ Method method =
+ TelephonyManager.class.getMethod(
+ GET_VISUAL_VOICEMAIL_PACKGE_NAME, PhoneAccountHandle.class);
+ try {
+ return (String) method.invoke(telephonyManager, (Object) null);
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException(e);
+ }
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/java/com/android/voicemail/impl/TelephonyVvmConfigManager.java b/java/com/android/voicemail/impl/TelephonyVvmConfigManager.java
new file mode 100644
index 000000000..04012c9c2
--- /dev/null
+++ b/java/com/android/voicemail/impl/TelephonyVvmConfigManager.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl;
+
+import android.content.res.Resources;
+import android.os.PersistableBundle;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.util.ArrayMap;
+import com.android.voicemail.impl.utils.XmlUtils;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.Map.Entry;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+/** Load and caches telephony vvm config from res/xml/vvm_config.xml */
+public class TelephonyVvmConfigManager {
+
+ private static final String TAG = "TelephonyVvmCfgMgr";
+
+ private static final boolean USE_DEBUG_CONFIG = false;
+
+ private static final String TAG_PERSISTABLEMAP = "pbundle_as_map";
+
+ static final String KEY_MCCMNC = "mccmnc";
+
+ private static Map<String, PersistableBundle> sCachedConfigs;
+
+ private final Map<String, PersistableBundle> mConfigs;
+
+ public TelephonyVvmConfigManager(Resources resources) {
+ if (sCachedConfigs == null) {
+ sCachedConfigs = loadConfigs(resources.getXml(R.xml.vvm_config));
+ }
+ mConfigs = sCachedConfigs;
+ }
+
+ @VisibleForTesting
+ TelephonyVvmConfigManager(XmlPullParser parser) {
+ mConfigs = loadConfigs(parser);
+ }
+
+ @Nullable
+ public PersistableBundle getConfig(String mccMnc) {
+ if (USE_DEBUG_CONFIG) {
+ return mConfigs.get("TEST");
+ }
+ return mConfigs.get(mccMnc);
+ }
+
+ private static Map<String, PersistableBundle> loadConfigs(XmlPullParser parser) {
+ Map<String, PersistableBundle> configs = new ArrayMap<>();
+ try {
+ ArrayList list = readBundleList(parser);
+ for (Object object : list) {
+ if (!(object instanceof PersistableBundle)) {
+ throw new IllegalArgumentException("PersistableBundle expected, got " + object);
+ }
+ PersistableBundle bundle = (PersistableBundle) object;
+ String[] mccMncs = bundle.getStringArray(KEY_MCCMNC);
+ if (mccMncs == null) {
+ throw new IllegalArgumentException("MCCMNC is null");
+ }
+ for (String mccMnc : mccMncs) {
+ configs.put(mccMnc, bundle);
+ }
+ }
+ } catch (IOException | XmlPullParserException e) {
+ throw new RuntimeException(e);
+ }
+ return configs;
+ }
+
+ @Nullable
+ public static ArrayList readBundleList(XmlPullParser in)
+ throws IOException, XmlPullParserException {
+ final int outerDepth = in.getDepth();
+ int event;
+ while (((event = in.next()) != XmlPullParser.END_DOCUMENT)
+ && (event != XmlPullParser.END_TAG || in.getDepth() < outerDepth)) {
+ if (event == XmlPullParser.START_TAG) {
+ final String startTag = in.getName();
+ final String[] tagName = new String[1];
+ in.next();
+ return XmlUtils.readThisListXml(in, startTag, tagName, new MyReadMapCallback(), false);
+ }
+ }
+ return null;
+ }
+
+ public static PersistableBundle restoreFromXml(XmlPullParser in)
+ throws IOException, XmlPullParserException {
+ final int outerDepth = in.getDepth();
+ final String startTag = in.getName();
+ final String[] tagName = new String[1];
+ int event;
+ while (((event = in.next()) != XmlPullParser.END_DOCUMENT)
+ && (event != XmlPullParser.END_TAG || in.getDepth() < outerDepth)) {
+ if (event == XmlPullParser.START_TAG) {
+ ArrayMap<String, ?> map =
+ XmlUtils.readThisArrayMapXml(in, startTag, tagName, new MyReadMapCallback());
+ PersistableBundle result = new PersistableBundle();
+ for (Entry<String, ?> entry : map.entrySet()) {
+ Object value = entry.getValue();
+ if (value instanceof Integer) {
+ result.putInt(entry.getKey(), (int) value);
+ } else if (value instanceof Boolean) {
+ result.putBoolean(entry.getKey(), (boolean) value);
+ } else if (value instanceof String) {
+ result.putString(entry.getKey(), (String) value);
+ } else if (value instanceof String[]) {
+ result.putStringArray(entry.getKey(), (String[]) value);
+ } else if (value instanceof PersistableBundle) {
+ result.putPersistableBundle(entry.getKey(), (PersistableBundle) value);
+ }
+ }
+ return result;
+ }
+ }
+ return PersistableBundle.EMPTY;
+ }
+
+ static class MyReadMapCallback implements XmlUtils.ReadMapCallback {
+
+ @Override
+ public Object readThisUnknownObjectXml(XmlPullParser in, String tag)
+ throws XmlPullParserException, IOException {
+ if (TAG_PERSISTABLEMAP.equals(tag)) {
+ return restoreFromXml(in);
+ }
+ throw new XmlPullParserException("Unknown tag=" + tag);
+ }
+ }
+}
diff --git a/java/com/android/voicemail/impl/VisualVoicemailPreferences.java b/java/com/android/voicemail/impl/VisualVoicemailPreferences.java
new file mode 100644
index 000000000..72506eb93
--- /dev/null
+++ b/java/com/android/voicemail/impl/VisualVoicemailPreferences.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl;
+
+import android.content.Context;
+import android.preference.PreferenceManager;
+import android.telecom.PhoneAccountHandle;
+import com.android.dialer.common.PerAccountSharedPreferences;
+
+/**
+ * Save visual voicemail values in shared preferences to be retrieved later. Because a voicemail
+ * source is tied 1:1 to a phone account, the phone account handle is used in the key for each
+ * voicemail source and the associated data.
+ */
+public class VisualVoicemailPreferences extends PerAccountSharedPreferences {
+
+ public VisualVoicemailPreferences(Context context, PhoneAccountHandle phoneAccountHandle) {
+ super(
+ context,
+ phoneAccountHandle,
+ PreferenceManager.getDefaultSharedPreferences(context),
+ "visual_voicemail_");
+ }
+}
diff --git a/java/com/android/voicemail/impl/Voicemail.java b/java/com/android/voicemail/impl/Voicemail.java
new file mode 100644
index 000000000..f98d56f0a
--- /dev/null
+++ b/java/com/android/voicemail/impl/Voicemail.java
@@ -0,0 +1,341 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl;
+
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.telecom.PhoneAccountHandle;
+import android.text.TextUtils;
+
+/** Represents a single voicemail stored in the voicemail content provider. */
+public class Voicemail implements Parcelable {
+
+ private final Long mTimestamp;
+ private final String mNumber;
+ private final PhoneAccountHandle mPhoneAccount;
+ private final Long mId;
+ private final Long mDuration;
+ private final String mSource;
+ private final String mProviderData;
+ private final Uri mUri;
+ private final Boolean mIsRead;
+ private final Boolean mHasContent;
+ private final String mTranscription;
+
+ private Voicemail(
+ Long timestamp,
+ String number,
+ PhoneAccountHandle phoneAccountHandle,
+ Long id,
+ Long duration,
+ String source,
+ String providerData,
+ Uri uri,
+ Boolean isRead,
+ Boolean hasContent,
+ String transcription) {
+ mTimestamp = timestamp;
+ mNumber = number;
+ mPhoneAccount = phoneAccountHandle;
+ mId = id;
+ mDuration = duration;
+ mSource = source;
+ mProviderData = providerData;
+ mUri = uri;
+ mIsRead = isRead;
+ mHasContent = hasContent;
+ mTranscription = transcription;
+ }
+
+ /**
+ * Create a {@link Builder} for a new {@link Voicemail} to be inserted.
+ *
+ * <p>The number and the timestamp are mandatory for insertion.
+ */
+ public static Builder createForInsertion(long timestamp, String number) {
+ return new Builder().setNumber(number).setTimestamp(timestamp);
+ }
+
+ /**
+ * Create a {@link Builder} for a {@link Voicemail} to be updated (or deleted).
+ *
+ * <p>The id and source data fields are mandatory for update - id is necessary for updating the
+ * database and source data is necessary for updating the server.
+ */
+ public static Builder createForUpdate(long id, String sourceData) {
+ return new Builder().setId(id).setSourceData(sourceData);
+ }
+
+ /**
+ * Builder pattern for creating a {@link Voicemail}. The builder must be created with the {@link
+ * #createForInsertion(long, String)} method.
+ *
+ * <p>This class is <b>not thread safe</b>
+ */
+ public static class Builder {
+
+ private Long mBuilderTimestamp;
+ private String mBuilderNumber;
+ private PhoneAccountHandle mBuilderPhoneAccount;
+ private Long mBuilderId;
+ private Long mBuilderDuration;
+ private String mBuilderSourcePackage;
+ private String mBuilderSourceData;
+ private Uri mBuilderUri;
+ private Boolean mBuilderIsRead;
+ private boolean mBuilderHasContent;
+ private String mBuilderTranscription;
+
+ /** You should use the correct factory method to construct a builder. */
+ private Builder() {}
+
+ public Builder setNumber(String number) {
+ mBuilderNumber = number;
+ return this;
+ }
+
+ public Builder setTimestamp(long timestamp) {
+ mBuilderTimestamp = timestamp;
+ return this;
+ }
+
+ public Builder setPhoneAccount(PhoneAccountHandle phoneAccount) {
+ mBuilderPhoneAccount = phoneAccount;
+ return this;
+ }
+
+ public Builder setId(long id) {
+ mBuilderId = id;
+ return this;
+ }
+
+ public Builder setDuration(long duration) {
+ mBuilderDuration = duration;
+ return this;
+ }
+
+ public Builder setSourcePackage(String sourcePackage) {
+ mBuilderSourcePackage = sourcePackage;
+ return this;
+ }
+
+ public Builder setSourceData(String sourceData) {
+ mBuilderSourceData = sourceData;
+ return this;
+ }
+
+ public Builder setUri(Uri uri) {
+ mBuilderUri = uri;
+ return this;
+ }
+
+ public Builder setIsRead(boolean isRead) {
+ mBuilderIsRead = isRead;
+ return this;
+ }
+
+ public Builder setHasContent(boolean hasContent) {
+ mBuilderHasContent = hasContent;
+ return this;
+ }
+
+ public Builder setTranscription(String transcription) {
+ mBuilderTranscription = transcription;
+ return this;
+ }
+
+ public Voicemail build() {
+ mBuilderId = mBuilderId == null ? -1 : mBuilderId;
+ mBuilderTimestamp = mBuilderTimestamp == null ? 0 : mBuilderTimestamp;
+ mBuilderDuration = mBuilderDuration == null ? 0 : mBuilderDuration;
+ mBuilderIsRead = mBuilderIsRead == null ? false : mBuilderIsRead;
+ return new Voicemail(
+ mBuilderTimestamp,
+ mBuilderNumber,
+ mBuilderPhoneAccount,
+ mBuilderId,
+ mBuilderDuration,
+ mBuilderSourcePackage,
+ mBuilderSourceData,
+ mBuilderUri,
+ mBuilderIsRead,
+ mBuilderHasContent,
+ mBuilderTranscription);
+ }
+ }
+
+ /**
+ * The identifier of the voicemail in the content provider.
+ *
+ * <p>This may be missing in the case of a new {@link Voicemail} that we plan to insert into the
+ * content provider, since until it has been inserted we don't know what id it should have. If
+ * none is specified, we return -1.
+ */
+ public long getId() {
+ return mId;
+ }
+
+ /** The number of the person leaving the voicemail, empty string if unknown, null if not set. */
+ public String getNumber() {
+ return mNumber;
+ }
+
+ /** The phone account associated with the voicemail, null if not set. */
+ public PhoneAccountHandle getPhoneAccount() {
+ return mPhoneAccount;
+ }
+
+ /** The timestamp the voicemail was received, in millis since the epoch, zero if not set. */
+ public long getTimestampMillis() {
+ return mTimestamp;
+ }
+
+ /** Gets the duration of the voicemail in millis, or zero if the field is not set. */
+ public long getDuration() {
+ return mDuration;
+ }
+
+ /**
+ * Returns the package name of the source that added this voicemail, or null if this field is not
+ * set.
+ */
+ public String getSourcePackage() {
+ return mSource;
+ }
+
+ /**
+ * Returns the application-specific data type stored with the voicemail, or null if this field is
+ * not set.
+ *
+ * <p>Source data is typically used as an identifier to uniquely identify the voicemail against
+ * the voicemail server. This is likely to be something like the IMAP UID, or some other
+ * server-generated identifying string.
+ */
+ public String getSourceData() {
+ return mProviderData;
+ }
+
+ /**
+ * Gets the Uri that can be used to refer to this voicemail, and to make it play.
+ *
+ * <p>Returns null if we don't know the Uri.
+ */
+ public Uri getUri() {
+ return mUri;
+ }
+
+ /**
+ * Tells us if the voicemail message has been marked as read.
+ *
+ * <p>Always returns false if this field has not been set, i.e. if hasRead() returns false.
+ */
+ public boolean isRead() {
+ return mIsRead;
+ }
+
+ /** Tells us if there is content stored at the Uri. */
+ public boolean hasContent() {
+ return mHasContent;
+ }
+
+ /** Returns the text transcription of this voicemail, or null if this field is not set. */
+ public String getTranscription() {
+ return mTranscription;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(mTimestamp);
+ writeCharSequence(dest, mNumber);
+ if (mPhoneAccount == null) {
+ dest.writeInt(0);
+ } else {
+ dest.writeInt(1);
+ mPhoneAccount.writeToParcel(dest, flags);
+ }
+ dest.writeLong(mId);
+ dest.writeLong(mDuration);
+ writeCharSequence(dest, mSource);
+ writeCharSequence(dest, mProviderData);
+ if (mUri == null) {
+ dest.writeInt(0);
+ } else {
+ dest.writeInt(1);
+ mUri.writeToParcel(dest, flags);
+ }
+ if (mIsRead) {
+ dest.writeInt(1);
+ } else {
+ dest.writeInt(0);
+ }
+ if (mHasContent) {
+ dest.writeInt(1);
+ } else {
+ dest.writeInt(0);
+ }
+ writeCharSequence(dest, mTranscription);
+ }
+
+ public static final Creator<Voicemail> CREATOR =
+ new Creator<Voicemail>() {
+ @Override
+ public Voicemail createFromParcel(Parcel in) {
+ return new Voicemail(in);
+ }
+
+ @Override
+ public Voicemail[] newArray(int size) {
+ return new Voicemail[size];
+ }
+ };
+
+ private Voicemail(Parcel in) {
+ mTimestamp = in.readLong();
+ mNumber = (String) readCharSequence(in);
+ if (in.readInt() > 0) {
+ mPhoneAccount = PhoneAccountHandle.CREATOR.createFromParcel(in);
+ } else {
+ mPhoneAccount = null;
+ }
+ mId = in.readLong();
+ mDuration = in.readLong();
+ mSource = (String) readCharSequence(in);
+ mProviderData = (String) readCharSequence(in);
+ if (in.readInt() > 0) {
+ mUri = Uri.CREATOR.createFromParcel(in);
+ } else {
+ mUri = null;
+ }
+ mIsRead = in.readInt() > 0 ? true : false;
+ mHasContent = in.readInt() > 0 ? true : false;
+ mTranscription = (String) readCharSequence(in);
+ }
+
+ private static CharSequence readCharSequence(Parcel in) {
+ return TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+ }
+
+ public static void writeCharSequence(Parcel dest, CharSequence val) {
+ TextUtils.writeToParcel(val, dest, 0);
+ }
+}
diff --git a/java/com/android/voicemail/impl/VoicemailClientImpl.java b/java/com/android/voicemail/impl/VoicemailClientImpl.java
new file mode 100644
index 000000000..1ad12aeab
--- /dev/null
+++ b/java/com/android/voicemail/impl/VoicemailClientImpl.java
@@ -0,0 +1,90 @@
+/**
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build.VERSION_CODES;
+import android.provider.VoicemailContract.Voicemails;
+import android.support.annotation.Nullable;
+import android.support.v4.os.BuildCompat;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import com.android.dialer.common.Assert;
+import com.android.voicemail.VoicemailClient;
+import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil;
+import com.android.voicemail.impl.settings.VoicemailSettingsFragment;
+import java.util.List;
+import javax.inject.Inject;
+
+/**
+ * {@link VoicemailClient} to be used when the voicemail module is activated. May only be used above
+ * O.
+ */
+public class VoicemailClientImpl implements VoicemailClient {
+
+ /**
+ * List of legacy OMTP voicemail packages that should be ignored. It could never be the active VVM
+ * package anymore. For example, voicemails in OC will no longer be handled by telephony, but
+ * legacy voicemails might still exist in the database due to upgrading from NYC. Dialer will
+ * fetch these voicemails again so it should be ignored.
+ */
+ private static final String[] OMTP_VOICEMAIL_BLACKLIST = {"com.android.phone"};
+
+ @Inject
+ public VoicemailClientImpl() {
+ Assert.checkArgument(BuildCompat.isAtLeastO());
+ }
+
+ @Nullable
+ @Override
+ public String getSettingsFragment() {
+ return VoicemailSettingsFragment.class.getName();
+ }
+
+ @Override
+ public boolean isVoicemailArchiveEnabled(Context context, PhoneAccountHandle phoneAccountHandle) {
+ return VisualVoicemailSettingsUtil.isArchiveEnabled(context, phoneAccountHandle);
+ }
+
+ @Override
+ public void setVoicemailArchiveEnabled(
+ Context context, PhoneAccountHandle phoneAccountHandle, boolean value) {
+ VisualVoicemailSettingsUtil.setArchiveEnabled(context, phoneAccountHandle, value);
+ }
+
+ @TargetApi(VERSION_CODES.O)
+ @Override
+ public void appendOmtpVoicemailSelectionClause(
+ Context context, StringBuilder where, List<String> selectionArgs) {
+ TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class);
+ String omtpSource = TelephonyMangerCompat.getVisualVoicemailPackageName(telephonyManager);
+ where.append(
+ "AND ("
+ + "("
+ + Voicemails.IS_OMTP_VOICEMAIL
+ + " != 1)"
+ + "OR "
+ + "("
+ + Voicemails.SOURCE_PACKAGE
+ + " = ? )"
+ + ")");
+ selectionArgs.add(omtpSource);
+
+ for (String blacklistedPackage : OMTP_VOICEMAIL_BLACKLIST) {
+ where.append("AND (" + Voicemails.SOURCE_PACKAGE + "!= ?)");
+ selectionArgs.add(blacklistedPackage);
+ }
+ }
+}
diff --git a/java/com/android/voicemail/impl/VoicemailClientReceiver.java b/java/com/android/voicemail/impl/VoicemailClientReceiver.java
new file mode 100644
index 000000000..49a55a41b
--- /dev/null
+++ b/java/com/android/voicemail/impl/VoicemailClientReceiver.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.telecom.PhoneAccountHandle;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.voicemail.VoicemailClient;
+import com.android.voicemail.impl.sync.UploadTask;
+import com.android.voicemail.impl.sync.VvmAccountManager;
+
+/** Receiver for broadcasts in {@link VoicemailClient#ACTION_UPLOAD} */
+public class VoicemailClientReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ switch (intent.getAction()) {
+ case VoicemailClient.ACTION_UPLOAD:
+ doUpload(context);
+ break;
+ default:
+ Assert.fail("Unexpected action " + intent.getAction());
+ break;
+ }
+ }
+
+ /** Upload local database changes to the server. */
+ private static void doUpload(Context context) {
+ LogUtil.i("VoicemailClientReceiver.onReceive", "ACTION_UPLOAD received");
+ for (PhoneAccountHandle phoneAccountHandle : VvmAccountManager.getActiveAccounts(context)) {
+ UploadTask.start(context, phoneAccountHandle);
+ }
+ }
+}
diff --git a/java/com/android/voicemail/impl/VoicemailModule.java b/java/com/android/voicemail/impl/VoicemailModule.java
new file mode 100644
index 000000000..c3e5714d5
--- /dev/null
+++ b/java/com/android/voicemail/impl/VoicemailModule.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl;
+
+import android.support.v4.os.BuildCompat;
+import com.android.voicemail.VoicemailClient;
+import com.android.voicemail.stub.StubVoicemailClient;
+import dagger.Module;
+import dagger.Provides;
+import javax.inject.Singleton;
+
+/** This module provides an instance of the voicemail client. */
+@Module
+public final class VoicemailModule {
+
+ @Provides
+ @Singleton
+ static VoicemailClient provideVoicemailClient() {
+ if (BuildCompat.isAtLeastO()) {
+ return new VoicemailClientImpl();
+ } else {
+ return new StubVoicemailClient();
+ }
+ }
+
+ private VoicemailModule() {}
+}
diff --git a/java/com/android/voicemail/impl/VoicemailStatus.java b/java/com/android/voicemail/impl/VoicemailStatus.java
new file mode 100644
index 000000000..ec1ab4e70
--- /dev/null
+++ b/java/com/android/voicemail/impl/VoicemailStatus.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Status;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+
+public class VoicemailStatus {
+
+ private static final String TAG = "VvmStatus";
+
+ public static class Editor {
+
+ private final Context mContext;
+ @Nullable private final PhoneAccountHandle mPhoneAccountHandle;
+
+ private ContentValues mValues = new ContentValues();
+
+ private Editor(Context context, PhoneAccountHandle phoneAccountHandle) {
+ mContext = context;
+ mPhoneAccountHandle = phoneAccountHandle;
+ if (mPhoneAccountHandle == null) {
+ VvmLog.w(
+ TAG,
+ "VoicemailStatus.Editor created with null phone account, status will"
+ + " not be written");
+ }
+ }
+
+ @Nullable
+ public PhoneAccountHandle getPhoneAccountHandle() {
+ return mPhoneAccountHandle;
+ }
+
+ public Editor setType(String type) {
+ mValues.put(Status.SOURCE_TYPE, type);
+ return this;
+ }
+
+ public Editor setConfigurationState(int configurationState) {
+ mValues.put(Status.CONFIGURATION_STATE, configurationState);
+ return this;
+ }
+
+ public Editor setDataChannelState(int dataChannelState) {
+ mValues.put(Status.DATA_CHANNEL_STATE, dataChannelState);
+ return this;
+ }
+
+ public Editor setNotificationChannelState(int notificationChannelState) {
+ mValues.put(Status.NOTIFICATION_CHANNEL_STATE, notificationChannelState);
+ return this;
+ }
+
+ public Editor setQuota(int occupied, int total) {
+ if (occupied == VoicemailContract.Status.QUOTA_UNAVAILABLE
+ && total == VoicemailContract.Status.QUOTA_UNAVAILABLE) {
+ return this;
+ }
+
+ mValues.put(Status.QUOTA_OCCUPIED, occupied);
+ mValues.put(Status.QUOTA_TOTAL, total);
+ return this;
+ }
+
+ /**
+ * Apply the changes to the {@link VoicemailStatus} {@link #Editor}.
+ *
+ * @return {@code true} if the changes were successfully applied, {@code false} otherwise.
+ */
+ public boolean apply() {
+ if (mPhoneAccountHandle == null) {
+ return false;
+ }
+ mValues.put(
+ Status.PHONE_ACCOUNT_COMPONENT_NAME,
+ mPhoneAccountHandle.getComponentName().flattenToString());
+ mValues.put(Status.PHONE_ACCOUNT_ID, mPhoneAccountHandle.getId());
+ ContentResolver contentResolver = mContext.getContentResolver();
+ Uri statusUri = VoicemailContract.Status.buildSourceUri(mContext.getPackageName());
+ try {
+ contentResolver.insert(statusUri, mValues);
+ } catch (IllegalArgumentException iae) {
+ VvmLog.e(TAG, "apply :: failed to insert content resolver ", iae);
+ mValues.clear();
+ return false;
+ }
+ mValues.clear();
+ return true;
+ }
+
+ public ContentValues getValues() {
+ return mValues;
+ }
+ }
+
+ /**
+ * A voicemail status editor that the decision of whether to actually write to the database can be
+ * deferred. This object will be passed around as a usual {@link Editor}, but {@link #apply()}
+ * doesn't do anything. If later the creator of this object decides any status changes written to
+ * it should be committed, {@link #deferredApply()} should be called.
+ */
+ public static class DeferredEditor extends Editor {
+
+ private DeferredEditor(Context context, PhoneAccountHandle phoneAccountHandle) {
+ super(context, phoneAccountHandle);
+ }
+
+ @Override
+ public boolean apply() {
+ // Do nothing
+ return true;
+ }
+
+ public void deferredApply() {
+ super.apply();
+ }
+ }
+
+ public static Editor edit(Context context, PhoneAccountHandle phoneAccountHandle) {
+ return new Editor(context, phoneAccountHandle);
+ }
+
+ /**
+ * Reset the status to the "disabled" state, which the UI should not show anything for this
+ * phoneAccountHandle.
+ */
+ public static void disable(Context context, PhoneAccountHandle phoneAccountHandle) {
+ edit(context, phoneAccountHandle)
+ .setConfigurationState(Status.CONFIGURATION_STATE_NOT_CONFIGURED)
+ .setDataChannelState(Status.DATA_CHANNEL_STATE_NO_CONNECTION)
+ .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION)
+ .apply();
+ }
+
+ public static DeferredEditor deferredEdit(
+ Context context, PhoneAccountHandle phoneAccountHandle) {
+ return new DeferredEditor(context, phoneAccountHandle);
+ }
+}
diff --git a/java/com/android/voicemail/impl/VvmLog.java b/java/com/android/voicemail/impl/VvmLog.java
new file mode 100644
index 000000000..595207f92
--- /dev/null
+++ b/java/com/android/voicemail/impl/VvmLog.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl;
+
+import com.android.dialer.common.LogUtil;
+import com.android.voicemail.impl.utils.IndentingPrintWriter;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayDeque;
+import java.util.Calendar;
+import java.util.Deque;
+import java.util.Iterator;
+
+/** Helper methods for adding to OMTP visual voicemail local logs. */
+public class VvmLog {
+
+ private static final int MAX_OMTP_VVM_LOGS = 100;
+
+ private static final LocalLog sLocalLog = new LocalLog(MAX_OMTP_VVM_LOGS);
+
+ public static void log(String tag, String log) {
+ sLocalLog.log(tag + ": " + log);
+ }
+
+ public static void dump(FileDescriptor fd, PrintWriter printwriter, String[] args) {
+ IndentingPrintWriter indentingPrintWriter = new IndentingPrintWriter(printwriter, " ");
+ indentingPrintWriter.increaseIndent();
+ sLocalLog.dump(fd, indentingPrintWriter, args);
+ indentingPrintWriter.decreaseIndent();
+ }
+
+ public static void e(String tag, String log) {
+ log(tag, log);
+ LogUtil.e(tag, log);
+ }
+
+ public static void e(String tag, String log, Throwable e) {
+ log(tag, log + " " + e);
+ LogUtil.e(tag, log, e);
+ }
+
+ public static void w(String tag, String log) {
+ log(tag, log);
+ LogUtil.w(tag, log);
+ }
+
+ public static void w(String tag, String log, Throwable e) {
+ log(tag, log + " " + e);
+ LogUtil.w(tag, log, e);
+ }
+
+ public static void i(String tag, String log) {
+ log(tag, log);
+ LogUtil.i(tag, log);
+ }
+
+ public static void i(String tag, String log, Throwable e) {
+ log(tag, log + " " + e);
+ LogUtil.i(tag, log, e);
+ }
+
+ public static void d(String tag, String log) {
+ log(tag, log);
+ LogUtil.d(tag, log);
+ }
+
+ public static void d(String tag, String log, Throwable e) {
+ log(tag, log + " " + e);
+ LogUtil.d(tag, log, e);
+ }
+
+ public static void v(String tag, String log) {
+ log(tag, log);
+ LogUtil.v(tag, log);
+ }
+
+ public static void v(String tag, String log, Throwable e) {
+ log(tag, log + " " + e);
+ LogUtil.v(tag, log, e);
+ }
+
+ public static void wtf(String tag, String log) {
+ log(tag, log);
+ LogUtil.e(tag, log);
+ }
+
+ public static void wtf(String tag, String log, Throwable e) {
+ log(tag, log + " " + e);
+ LogUtil.e(tag, log, e);
+ }
+
+ /**
+ * Redact personally identifiable information for production users. If we are running in verbose
+ * mode, return the original string, otherwise return a SHA-1 hash of the input string.
+ */
+ public static String pii(Object pii) {
+ if (pii == null) {
+ return String.valueOf(pii);
+ }
+ return "[PII]";
+ }
+
+ public static class LocalLog {
+
+ private final Deque<String> mLog;
+ private final int mMaxLines;
+
+ public LocalLog(int maxLines) {
+ mMaxLines = Math.max(0, maxLines);
+ mLog = new ArrayDeque<>(mMaxLines);
+ }
+
+ public void log(String msg) {
+ if (mMaxLines <= 0) {
+ return;
+ }
+ Calendar c = Calendar.getInstance();
+ c.setTimeInMillis(System.currentTimeMillis());
+ append(String.format("%tm-%td %tH:%tM:%tS.%tL - %s", c, c, c, c, c, c, msg));
+ }
+
+ private synchronized void append(String logLine) {
+ while (mLog.size() >= mMaxLines) {
+ mLog.remove();
+ }
+ mLog.add(logLine);
+ }
+
+ public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ Iterator<String> itr = mLog.iterator();
+ while (itr.hasNext()) {
+ pw.println(itr.next());
+ }
+ }
+
+ public synchronized void reverseDump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ Iterator<String> itr = mLog.descendingIterator();
+ while (itr.hasNext()) {
+ pw.println(itr.next());
+ }
+ }
+
+ public static class ReadOnlyLocalLog {
+
+ private final LocalLog mLog;
+
+ ReadOnlyLocalLog(LocalLog log) {
+ mLog = log;
+ }
+
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ mLog.dump(fd, pw, args);
+ }
+
+ public void reverseDump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ mLog.reverseDump(fd, pw, args);
+ }
+ }
+
+ public ReadOnlyLocalLog readOnlyLocalLog() {
+ return new ReadOnlyLocalLog(this);
+ }
+ }
+}
diff --git a/java/com/android/voicemail/impl/VvmPackageInstallReceiver.java b/java/com/android/voicemail/impl/VvmPackageInstallReceiver.java
new file mode 100644
index 000000000..c5650b3ee
--- /dev/null
+++ b/java/com/android/voicemail/impl/VvmPackageInstallReceiver.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil;
+
+/**
+ * When a new package is installed, check if it matches any of the vvm carrier apps of the currently
+ * enabled dialer vvm sources.
+ */
+public class VvmPackageInstallReceiver extends BroadcastReceiver {
+
+ private static final String TAG = "VvmPkgInstallReceiver";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getData() == null) {
+ return;
+ }
+
+ String packageName = intent.getData().getSchemeSpecificPart();
+ if (packageName == null) {
+ return;
+ }
+
+ for (PhoneAccountHandle phoneAccount :
+ context.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts()) {
+ if (VisualVoicemailSettingsUtil.isEnabledUserSet(context, phoneAccount)) {
+ // Skip the check if this voicemail source's setting is overridden by the user.
+ continue;
+ }
+
+ OmtpVvmCarrierConfigHelper carrierConfigHelper =
+ new OmtpVvmCarrierConfigHelper(context, phoneAccount);
+ if (carrierConfigHelper.getCarrierVvmPackageNames() == null) {
+ continue;
+ }
+ if (carrierConfigHelper.getCarrierVvmPackageNames().contains(packageName)) {
+ // Force deactivate the client. The user can re-enable it in the settings.
+ // There is no need to update the settings for deactivation. At this point, if the
+ // default value is used it should be false because a carrier package is present.
+ VvmLog.i(TAG, "Carrier VVM package installed, disabling system VVM client");
+ VisualVoicemailSettingsUtil.setEnabled(context, phoneAccount, false);
+ }
+ }
+ }
+}
diff --git a/java/com/android/voicemail/impl/VvmPhoneStateListener.java b/java/com/android/voicemail/impl/VvmPhoneStateListener.java
new file mode 100644
index 000000000..48b72042c
--- /dev/null
+++ b/java/com/android/voicemail/impl/VvmPhoneStateListener.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl;
+
+import android.content.Context;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.PhoneStateListener;
+import android.telephony.ServiceState;
+import com.android.voicemail.impl.sync.OmtpVvmSyncService;
+import com.android.voicemail.impl.sync.SyncTask;
+import com.android.voicemail.impl.sync.VoicemailStatusQueryHelper;
+import com.android.voicemail.impl.sync.VvmAccountManager;
+
+/**
+ * Check if service is lost and indicate this in the voicemail status. TODO(b/35125657): Not used
+ * for now, restore it.
+ */
+public class VvmPhoneStateListener extends PhoneStateListener {
+
+ private static final String TAG = "VvmPhoneStateListener";
+
+ private PhoneAccountHandle mPhoneAccount;
+ private Context mContext;
+ private int mPreviousState = -1;
+
+ public VvmPhoneStateListener(Context context, PhoneAccountHandle accountHandle) {
+ // TODO: b/32637799 too much trouble to call super constructor through reflection,
+ // just use non-phoneAccountHandle version for now.
+ super();
+ mContext = context;
+ mPhoneAccount = accountHandle;
+ }
+
+ @Override
+ public void onServiceStateChanged(ServiceState serviceState) {
+ if (mPhoneAccount == null) {
+ VvmLog.e(
+ TAG,
+ "onServiceStateChanged on phoneAccount "
+ + mPhoneAccount
+ + " with invalid phoneAccountHandle, ignoring");
+ return;
+ }
+
+ int state = serviceState.getState();
+ if (state == mPreviousState
+ || (state != ServiceState.STATE_IN_SERVICE
+ && mPreviousState != ServiceState.STATE_IN_SERVICE)) {
+ // Only interested in state changes or transitioning into or out of "in service".
+ // Otherwise just quit.
+ mPreviousState = state;
+ return;
+ }
+
+ OmtpVvmCarrierConfigHelper helper = new OmtpVvmCarrierConfigHelper(mContext, mPhoneAccount);
+
+ if (state == ServiceState.STATE_IN_SERVICE) {
+ VoicemailStatusQueryHelper voicemailStatusQueryHelper =
+ new VoicemailStatusQueryHelper(mContext);
+ if (voicemailStatusQueryHelper.isVoicemailSourceConfigured(mPhoneAccount)) {
+ if (!voicemailStatusQueryHelper.isNotificationsChannelActive(mPhoneAccount)) {
+ VvmLog.v(TAG, "Notifications channel is active for " + mPhoneAccount);
+ helper.handleEvent(
+ VoicemailStatus.edit(mContext, mPhoneAccount), OmtpEvents.NOTIFICATION_IN_SERVICE);
+ }
+ }
+
+ if (VvmAccountManager.isAccountActivated(mContext, mPhoneAccount)) {
+ VvmLog.v(TAG, "Signal returned: requesting resync for " + mPhoneAccount);
+ // If the source is already registered, run a full sync in case something was missed
+ // while signal was down.
+ SyncTask.start(mContext, mPhoneAccount, OmtpVvmSyncService.SYNC_FULL_SYNC);
+ } else {
+ VvmLog.v(TAG, "Signal returned: reattempting activation for " + mPhoneAccount);
+ // Otherwise initiate an activation because this means that an OMTP source was
+ // recognized but either the activation text was not successfully sent or a response
+ // was not received.
+ helper.startActivation();
+ }
+ } else {
+ VvmLog.v(TAG, "Notifications channel is inactive for " + mPhoneAccount);
+
+ if (!VvmAccountManager.isAccountActivated(mContext, mPhoneAccount)) {
+ return;
+ }
+ helper.handleEvent(
+ VoicemailStatus.edit(mContext, mPhoneAccount), OmtpEvents.NOTIFICATION_SERVICE_LOST);
+ }
+ mPreviousState = state;
+ }
+}
diff --git a/java/com/android/voicemail/impl/fetch/FetchVoicemailReceiver.java b/java/com/android/voicemail/impl/fetch/FetchVoicemailReceiver.java
new file mode 100644
index 000000000..07e800836
--- /dev/null
+++ b/java/com/android/voicemail/impl/fetch/FetchVoicemailReceiver.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.fetch;
+
+import android.annotation.TargetApi;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Network;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Voicemails;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.os.BuildCompat;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.imap.ImapHelper;
+import com.android.voicemail.impl.imap.ImapHelper.InitializingException;
+import com.android.voicemail.impl.sync.VvmAccountManager;
+import com.android.voicemail.impl.sync.VvmNetworkRequestCallback;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+/** handles {@link VoicemailContract#ACTION_FETCH_VOICEMAIL} */
+@TargetApi(VERSION_CODES.O)
+public class FetchVoicemailReceiver extends BroadcastReceiver {
+
+ private static final String TAG = "FetchVoicemailReceiver";
+
+ static final String[] PROJECTION =
+ new String[] {
+ Voicemails.SOURCE_DATA, // 0
+ Voicemails.PHONE_ACCOUNT_ID, // 1
+ Voicemails.PHONE_ACCOUNT_COMPONENT_NAME, // 2
+ };
+
+ public static final int SOURCE_DATA = 0;
+ public static final int PHONE_ACCOUNT_ID = 1;
+ public static final int PHONE_ACCOUNT_COMPONENT_NAME = 2;
+
+ // Number of retries
+ private static final int NETWORK_RETRY_COUNT = 3;
+
+ private ContentResolver mContentResolver;
+ private Uri mUri;
+ private VvmNetworkRequestCallback mNetworkCallback;
+ private Context mContext;
+ private String mUid;
+ private PhoneAccountHandle mPhoneAccount;
+ private int mRetryCount = NETWORK_RETRY_COUNT;
+
+ @Override
+ public void onReceive(final Context context, Intent intent) {
+ if (VoicemailContract.ACTION_FETCH_VOICEMAIL.equals(intent.getAction())) {
+ VvmLog.i(TAG, "ACTION_FETCH_VOICEMAIL received");
+ mContext = context;
+ mContentResolver = context.getContentResolver();
+ mUri = intent.getData();
+
+ if (mUri == null) {
+ VvmLog.w(TAG, VoicemailContract.ACTION_FETCH_VOICEMAIL + " intent sent with no data");
+ return;
+ }
+
+ if (!context
+ .getPackageName()
+ .equals(mUri.getQueryParameter(VoicemailContract.PARAM_KEY_SOURCE_PACKAGE))) {
+ // Ignore if the fetch request is for a voicemail not from this package.
+ VvmLog.e(TAG, "ACTION_FETCH_VOICEMAIL from foreign pacakge " + context.getPackageName());
+ return;
+ }
+
+ Cursor cursor = mContentResolver.query(mUri, PROJECTION, null, null, null);
+ if (cursor == null) {
+ VvmLog.i(TAG, "ACTION_FETCH_VOICEMAIL query returned null");
+ return;
+ }
+ try {
+ if (cursor.moveToFirst()) {
+ mUid = cursor.getString(SOURCE_DATA);
+ String accountId = cursor.getString(PHONE_ACCOUNT_ID);
+ if (TextUtils.isEmpty(accountId)) {
+ TelephonyManager telephonyManager =
+ (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ accountId = telephonyManager.getSimSerialNumber();
+
+ if (TextUtils.isEmpty(accountId)) {
+ VvmLog.e(TAG, "Account null and no default sim found.");
+ return;
+ }
+ }
+
+ mPhoneAccount =
+ new PhoneAccountHandle(
+ ComponentName.unflattenFromString(cursor.getString(PHONE_ACCOUNT_COMPONENT_NAME)),
+ cursor.getString(PHONE_ACCOUNT_ID));
+ if (!VvmAccountManager.isAccountActivated(context, mPhoneAccount)) {
+ mPhoneAccount = getAccountFromMarshmallowAccount(context, mPhoneAccount);
+ if (mPhoneAccount == null) {
+ VvmLog.w(TAG, "Account not registered - cannot retrieve message.");
+ return;
+ }
+ VvmLog.i(TAG, "Fetching voicemail with Marshmallow PhoneAccountHandle");
+ }
+ VvmLog.i(TAG, "Requesting network to fetch voicemail");
+ mNetworkCallback = new fetchVoicemailNetworkRequestCallback(context, mPhoneAccount);
+ mNetworkCallback.requestNetwork();
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ }
+
+ /**
+ * In ag/930496 the format of PhoneAccountHandle has changed between Marshmallow and Nougat. This
+ * method attempts to search the account from the old database in registered sources using the old
+ * format. There's a chance of M phone account collisions on multi-SIM devices, but visual
+ * voicemail is not supported on M multi-SIM.
+ */
+ @Nullable
+ private static PhoneAccountHandle getAccountFromMarshmallowAccount(
+ Context context, PhoneAccountHandle oldAccount) {
+ if (!BuildCompat.isAtLeastN()) {
+ return null;
+ }
+ for (PhoneAccountHandle handle :
+ context.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts()) {
+ if (getIccSerialNumberFromFullIccSerialNumber(handle.getId()).equals(oldAccount.getId())) {
+ return handle;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * getIccSerialNumber() is used for ID before N, and getFullIccSerialNumber() after.
+ * getIccSerialNumber() stops at the first hex char.
+ */
+ @NonNull
+ private static String getIccSerialNumberFromFullIccSerialNumber(@NonNull String id) {
+ for (int i = 0; i < id.length(); i++) {
+ if (!Character.isDigit(id.charAt(i))) {
+ return id.substring(0, i);
+ }
+ }
+ return id;
+ }
+
+ private class fetchVoicemailNetworkRequestCallback extends VvmNetworkRequestCallback {
+
+ public fetchVoicemailNetworkRequestCallback(Context context, PhoneAccountHandle phoneAccount) {
+ super(context, phoneAccount, VoicemailStatus.edit(context, phoneAccount));
+ }
+
+ @Override
+ public void onAvailable(final Network network) {
+ super.onAvailable(network);
+ fetchVoicemail(network, getVoicemailStatusEditor());
+ }
+ }
+
+ private void fetchVoicemail(final Network network, final VoicemailStatus.Editor status) {
+ Executor executor = Executors.newCachedThreadPool();
+ executor.execute(
+ new Runnable() {
+ @Override
+ public void run() {
+ try {
+ while (mRetryCount > 0) {
+ VvmLog.i(TAG, "fetching voicemail, retry count=" + mRetryCount);
+ try (ImapHelper imapHelper =
+ new ImapHelper(mContext, mPhoneAccount, network, status)) {
+ boolean success =
+ imapHelper.fetchVoicemailPayload(
+ new VoicemailFetchedCallback(mContext, mUri, mPhoneAccount), mUid);
+ if (!success && mRetryCount > 0) {
+ VvmLog.i(TAG, "fetch voicemail failed, retrying");
+ mRetryCount--;
+ } else {
+ return;
+ }
+ } catch (InitializingException e) {
+ VvmLog.w(TAG, "Can't retrieve Imap credentials ", e);
+ return;
+ }
+ }
+ } finally {
+ if (mNetworkCallback != null) {
+ mNetworkCallback.releaseNetwork();
+ }
+ }
+ }
+ });
+ }
+}
diff --git a/java/com/android/voicemail/impl/fetch/VoicemailFetchedCallback.java b/java/com/android/voicemail/impl/fetch/VoicemailFetchedCallback.java
new file mode 100644
index 000000000..f386fce0e
--- /dev/null
+++ b/java/com/android/voicemail/impl/fetch/VoicemailFetchedCallback.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.fetch;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.VoicemailContract.Voicemails;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import com.android.voicemail.impl.R;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.imap.VoicemailPayload;
+import java.io.IOException;
+import java.io.OutputStream;
+import org.apache.commons.io.IOUtils;
+
+/**
+ * Callback for when a voicemail payload is fetched. It copies the returned stream to the data file
+ * corresponding to the voicemail.
+ */
+public class VoicemailFetchedCallback {
+ private static final String TAG = "VoicemailFetchedCallback";
+
+ private final Context mContext;
+ private final ContentResolver mContentResolver;
+ private final Uri mUri;
+ private final PhoneAccountHandle mPhoneAccountHandle;
+
+ public VoicemailFetchedCallback(Context context, Uri uri, PhoneAccountHandle phoneAccountHandle) {
+ mContext = context;
+ mContentResolver = context.getContentResolver();
+ mUri = uri;
+ mPhoneAccountHandle = phoneAccountHandle;
+ }
+
+ /**
+ * Saves the voicemail payload data into the voicemail provider then sets the "has_content" bit of
+ * the voicemail to "1".
+ *
+ * @param voicemailPayload The object containing the content data for the voicemail
+ */
+ public void setVoicemailContent(@Nullable VoicemailPayload voicemailPayload) {
+ if (voicemailPayload == null) {
+ VvmLog.i(TAG, "Payload not found, message has unsupported format");
+ ContentValues values = new ContentValues();
+ values.put(
+ Voicemails.TRANSCRIPTION,
+ mContext.getString(
+ R.string.vvm_unsupported_message_format,
+ mContext
+ .getSystemService(TelecomManager.class)
+ .getVoiceMailNumber(mPhoneAccountHandle)));
+ updateVoicemail(values);
+ return;
+ }
+
+ VvmLog.d(TAG, String.format("Writing new voicemail content: %s", mUri));
+ OutputStream outputStream = null;
+
+ try {
+ outputStream = mContentResolver.openOutputStream(mUri);
+ byte[] inputBytes = voicemailPayload.getBytes();
+ if (inputBytes != null) {
+ outputStream.write(inputBytes);
+ }
+ } catch (IOException e) {
+ VvmLog.w(TAG, String.format("File not found for %s", mUri));
+ return;
+ } finally {
+ IOUtils.closeQuietly(outputStream);
+ }
+
+ // Update mime_type & has_content after we are done with file update.
+ ContentValues values = new ContentValues();
+ values.put(Voicemails.MIME_TYPE, voicemailPayload.getMimeType());
+ values.put(Voicemails.HAS_CONTENT, true);
+ updateVoicemail(values);
+ }
+
+ private void updateVoicemail(ContentValues values) {
+ int updatedCount = mContentResolver.update(mUri, values, null, null);
+ if (updatedCount != 1) {
+ VvmLog.e(TAG, "Updating voicemail should have updated 1 row, was: " + updatedCount);
+ }
+ }
+}
diff --git a/java/com/android/voicemail/impl/imap/ImapHelper.java b/java/com/android/voicemail/impl/imap/ImapHelper.java
new file mode 100644
index 000000000..6aa415811
--- /dev/null
+++ b/java/com/android/voicemail/impl/imap/ImapHelper.java
@@ -0,0 +1,693 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.imap;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkInfo;
+import android.provider.VoicemailContract;
+import android.telecom.PhoneAccountHandle;
+import android.util.Base64;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.OmtpConstants.ChangePinResult;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.VisualVoicemailPreferences;
+import com.android.voicemail.impl.Voicemail;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VoicemailStatus.Editor;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.fetch.VoicemailFetchedCallback;
+import com.android.voicemail.impl.mail.Address;
+import com.android.voicemail.impl.mail.Body;
+import com.android.voicemail.impl.mail.BodyPart;
+import com.android.voicemail.impl.mail.FetchProfile;
+import com.android.voicemail.impl.mail.Flag;
+import com.android.voicemail.impl.mail.Message;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.Multipart;
+import com.android.voicemail.impl.mail.TempDirectory;
+import com.android.voicemail.impl.mail.internet.MimeMessage;
+import com.android.voicemail.impl.mail.store.ImapConnection;
+import com.android.voicemail.impl.mail.store.ImapFolder;
+import com.android.voicemail.impl.mail.store.ImapStore;
+import com.android.voicemail.impl.mail.store.imap.ImapConstants;
+import com.android.voicemail.impl.mail.store.imap.ImapResponse;
+import com.android.voicemail.impl.mail.utils.LogUtils;
+import com.android.voicemail.impl.sync.OmtpVvmSyncService.TranscriptionFetchedCallback;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import org.apache.commons.io.IOUtils;
+
+/** A helper interface to abstract commands sent across IMAP interface for a given account. */
+public class ImapHelper implements Closeable {
+
+ private static final String TAG = "ImapHelper";
+
+ private ImapFolder mFolder;
+ private ImapStore mImapStore;
+
+ private final Context mContext;
+ private final PhoneAccountHandle mPhoneAccount;
+ private final Network mNetwork;
+ private final Editor mStatus;
+
+ VisualVoicemailPreferences mPrefs;
+ private static final String PREF_KEY_QUOTA_OCCUPIED = "quota_occupied_";
+ private static final String PREF_KEY_QUOTA_TOTAL = "quota_total_";
+
+ private int mQuotaOccupied;
+ private int mQuotaTotal;
+
+ private final OmtpVvmCarrierConfigHelper mConfig;
+
+ /** InitializingException */
+ public static class InitializingException extends Exception {
+
+ public InitializingException(String message) {
+ super(message);
+ }
+ }
+
+ public ImapHelper(
+ Context context,
+ PhoneAccountHandle phoneAccount,
+ Network network,
+ Editor status)
+ throws InitializingException {
+ this(
+ context,
+ new OmtpVvmCarrierConfigHelper(context, phoneAccount),
+ phoneAccount,
+ network,
+ status);
+ }
+
+ public ImapHelper(
+ Context context,
+ OmtpVvmCarrierConfigHelper config,
+ PhoneAccountHandle phoneAccount,
+ Network network,
+ Editor status)
+ throws InitializingException {
+ mContext = context;
+ mPhoneAccount = phoneAccount;
+ mNetwork = network;
+ mStatus = status;
+ mConfig = config;
+ mPrefs = new VisualVoicemailPreferences(context, phoneAccount);
+
+ try {
+ TempDirectory.setTempDirectory(context);
+
+ String username = mPrefs.getString(OmtpConstants.IMAP_USER_NAME, null);
+ String password = mPrefs.getString(OmtpConstants.IMAP_PASSWORD, null);
+ String serverName = mPrefs.getString(OmtpConstants.SERVER_ADDRESS, null);
+ int port = Integer.parseInt(mPrefs.getString(OmtpConstants.IMAP_PORT, null));
+ int auth = ImapStore.FLAG_NONE;
+
+ int sslPort = mConfig.getSslPort();
+ if (sslPort != 0) {
+ port = sslPort;
+ auth = ImapStore.FLAG_SSL;
+ }
+
+ mImapStore =
+ new ImapStore(context, this, username, password, port, serverName, auth, network);
+ } catch (NumberFormatException e) {
+ handleEvent(OmtpEvents.DATA_INVALID_PORT);
+ LogUtils.w(TAG, "Could not parse port number");
+ throw new InitializingException("cannot initialize ImapHelper:" + e.toString());
+ }
+
+ mQuotaOccupied =
+ mPrefs.getInt(PREF_KEY_QUOTA_OCCUPIED, VoicemailContract.Status.QUOTA_UNAVAILABLE);
+ mQuotaTotal = mPrefs.getInt(PREF_KEY_QUOTA_TOTAL, VoicemailContract.Status.QUOTA_UNAVAILABLE);
+ }
+
+ @Override
+ public void close() {
+ mImapStore.closeConnection();
+ }
+
+ public boolean isRoaming() {
+ ConnectivityManager connectivityManager =
+ (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo info = connectivityManager.getNetworkInfo(mNetwork);
+ if (info == null) {
+ return false;
+ }
+ return info.isRoaming();
+ }
+
+ public OmtpVvmCarrierConfigHelper getConfig() {
+ return mConfig;
+ }
+
+ public ImapConnection connect() {
+ return mImapStore.getConnection();
+ }
+
+ /** The caller thread will block until the method returns. */
+ public boolean markMessagesAsRead(List<Voicemail> voicemails) {
+ return setFlags(voicemails, Flag.SEEN);
+ }
+
+ /** The caller thread will block until the method returns. */
+ public boolean markMessagesAsDeleted(List<Voicemail> voicemails) {
+ return setFlags(voicemails, Flag.DELETED);
+ }
+
+ public void handleEvent(OmtpEvents event) {
+ mConfig.handleEvent(mStatus, event);
+ }
+
+ /**
+ * Set flags on the server for a given set of voicemails.
+ *
+ * @param voicemails The voicemails to set flags for.
+ * @param flags The flags to set on the voicemails.
+ * @return {@code true} if the operation completes successfully, {@code false} otherwise.
+ */
+ private boolean setFlags(List<Voicemail> voicemails, String... flags) {
+ if (voicemails.size() == 0) {
+ return false;
+ }
+ try {
+ mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
+ if (mFolder != null) {
+ mFolder.setFlags(convertToImapMessages(voicemails), flags, true);
+ return true;
+ }
+ return false;
+ } catch (MessagingException e) {
+ LogUtils.e(TAG, e, "Messaging exception");
+ return false;
+ } finally {
+ closeImapFolder();
+ }
+ }
+
+ /**
+ * Fetch a list of voicemails from the server.
+ *
+ * @return A list of voicemail objects containing data about voicemails stored on the server.
+ */
+ public List<Voicemail> fetchAllVoicemails() {
+ List<Voicemail> result = new ArrayList<Voicemail>();
+ Message[] messages;
+ try {
+ mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
+ if (mFolder == null) {
+ // This means we were unable to successfully open the folder.
+ return null;
+ }
+
+ // This method retrieves lightweight messages containing only the uid of the message.
+ messages = mFolder.getMessages(null);
+
+ for (Message message : messages) {
+ // Get the voicemail details (message structure).
+ MessageStructureWrapper messageStructureWrapper = fetchMessageStructure(message);
+ if (messageStructureWrapper != null) {
+ result.add(getVoicemailFromMessageStructure(messageStructureWrapper));
+ }
+ }
+ return result;
+ } catch (MessagingException e) {
+ LogUtils.e(TAG, e, "Messaging Exception");
+ return null;
+ } finally {
+ closeImapFolder();
+ }
+ }
+
+ /**
+ * Extract voicemail details from the message structure. Also fetch transcription if a
+ * transcription exists.
+ */
+ private Voicemail getVoicemailFromMessageStructure(
+ MessageStructureWrapper messageStructureWrapper) throws MessagingException {
+ Message messageDetails = messageStructureWrapper.messageStructure;
+
+ TranscriptionFetchedListener listener = new TranscriptionFetchedListener();
+ if (messageStructureWrapper.transcriptionBodyPart != null) {
+ FetchProfile fetchProfile = new FetchProfile();
+ fetchProfile.add(messageStructureWrapper.transcriptionBodyPart);
+
+ mFolder.fetch(new Message[] {messageDetails}, fetchProfile, listener);
+ }
+
+ // Found an audio attachment, this is a valid voicemail.
+ long time = messageDetails.getSentDate().getTime();
+ String number = getNumber(messageDetails.getFrom());
+ boolean isRead = Arrays.asList(messageDetails.getFlags()).contains(Flag.SEEN);
+ return Voicemail.createForInsertion(time, number)
+ .setPhoneAccount(mPhoneAccount)
+ .setSourcePackage(mContext.getPackageName())
+ .setSourceData(messageDetails.getUid())
+ .setIsRead(isRead)
+ .setTranscription(listener.getVoicemailTranscription())
+ .build();
+ }
+
+ /**
+ * The "from" field of a visual voicemail IMAP message is the number of the caller who left the
+ * message. Extract this number from the list of "from" addresses.
+ *
+ * @param fromAddresses A list of addresses that comprise the "from" line.
+ * @return The number of the voicemail sender.
+ */
+ private String getNumber(Address[] fromAddresses) {
+ if (fromAddresses != null && fromAddresses.length > 0) {
+ if (fromAddresses.length != 1) {
+ LogUtils.w(TAG, "More than one from addresses found. Using the first one.");
+ }
+ String sender = fromAddresses[0].getAddress();
+ int atPos = sender.indexOf('@');
+ if (atPos != -1) {
+ // Strip domain part of the address.
+ sender = sender.substring(0, atPos);
+ }
+ return sender;
+ }
+ return null;
+ }
+
+ /**
+ * Fetches the structure of the given message and returns a wrapper containing the message
+ * structure and the transcription structure (if applicable).
+ *
+ * @throws MessagingException if fetching the structure of the message fails
+ */
+ private MessageStructureWrapper fetchMessageStructure(Message message) throws MessagingException {
+ LogUtils.d(TAG, "Fetching message structure for " + message.getUid());
+
+ MessageStructureFetchedListener listener = new MessageStructureFetchedListener();
+
+ FetchProfile fetchProfile = new FetchProfile();
+ fetchProfile.addAll(
+ Arrays.asList(
+ FetchProfile.Item.FLAGS, FetchProfile.Item.ENVELOPE, FetchProfile.Item.STRUCTURE));
+
+ // The IMAP folder fetch method will call "messageRetrieved" on the listener when the
+ // message is successfully retrieved.
+ mFolder.fetch(new Message[] {message}, fetchProfile, listener);
+ return listener.getMessageStructure();
+ }
+
+ public boolean fetchVoicemailPayload(VoicemailFetchedCallback callback, final String uid) {
+ try {
+ mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
+ if (mFolder == null) {
+ // This means we were unable to successfully open the folder.
+ return false;
+ }
+ Message message = mFolder.getMessage(uid);
+ if (message == null) {
+ return false;
+ }
+ VoicemailPayload voicemailPayload = fetchVoicemailPayload(message);
+ callback.setVoicemailContent(voicemailPayload);
+ return true;
+ } catch (MessagingException e) {
+ } finally {
+ closeImapFolder();
+ }
+ return false;
+ }
+
+ /**
+ * Fetches the body of the given message and returns the parsed voicemail payload.
+ *
+ * @throws MessagingException if fetching the body of the message fails
+ */
+ private VoicemailPayload fetchVoicemailPayload(Message message) throws MessagingException {
+ LogUtils.d(TAG, "Fetching message body for " + message.getUid());
+
+ MessageBodyFetchedListener listener = new MessageBodyFetchedListener();
+
+ FetchProfile fetchProfile = new FetchProfile();
+ fetchProfile.add(FetchProfile.Item.BODY);
+
+ mFolder.fetch(new Message[] {message}, fetchProfile, listener);
+ return listener.getVoicemailPayload();
+ }
+
+ public boolean fetchTranscription(TranscriptionFetchedCallback callback, String uid) {
+ try {
+ mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
+ if (mFolder == null) {
+ // This means we were unable to successfully open the folder.
+ return false;
+ }
+
+ Message message = mFolder.getMessage(uid);
+ if (message == null) {
+ return false;
+ }
+
+ MessageStructureWrapper messageStructureWrapper = fetchMessageStructure(message);
+ if (messageStructureWrapper != null) {
+ TranscriptionFetchedListener listener = new TranscriptionFetchedListener();
+ if (messageStructureWrapper.transcriptionBodyPart != null) {
+ FetchProfile fetchProfile = new FetchProfile();
+ fetchProfile.add(messageStructureWrapper.transcriptionBodyPart);
+
+ // This method is called synchronously so the transcription will be populated
+ // in the listener once the next method is called.
+ mFolder.fetch(new Message[] {message}, fetchProfile, listener);
+ callback.setVoicemailTranscription(listener.getVoicemailTranscription());
+ }
+ }
+ return true;
+ } catch (MessagingException e) {
+ LogUtils.e(TAG, e, "Messaging Exception");
+ return false;
+ } finally {
+ closeImapFolder();
+ }
+ }
+
+ @ChangePinResult
+ public int changePin(String oldPin, String newPin) throws MessagingException {
+ ImapConnection connection = mImapStore.getConnection();
+ try {
+ String command =
+ getConfig().getProtocol().getCommand(OmtpConstants.IMAP_CHANGE_TUI_PWD_FORMAT);
+ connection.sendCommand(String.format(Locale.US, command, newPin, oldPin), true);
+ return getChangePinResultFromImapResponse(connection.readResponse());
+ } catch (IOException ioe) {
+ VvmLog.e(TAG, "changePin: ", ioe);
+ return OmtpConstants.CHANGE_PIN_SYSTEM_ERROR;
+ } finally {
+ connection.destroyResponses();
+ }
+ }
+
+ public void changeVoicemailTuiLanguage(String languageCode) throws MessagingException {
+ ImapConnection connection = mImapStore.getConnection();
+ try {
+ String command =
+ getConfig().getProtocol().getCommand(OmtpConstants.IMAP_CHANGE_VM_LANG_FORMAT);
+ connection.sendCommand(String.format(Locale.US, command, languageCode), true);
+ } catch (IOException ioe) {
+ LogUtils.e(TAG, ioe.toString());
+ } finally {
+ connection.destroyResponses();
+ }
+ }
+
+ public void closeNewUserTutorial() throws MessagingException {
+ ImapConnection connection = mImapStore.getConnection();
+ try {
+ String command = getConfig().getProtocol().getCommand(OmtpConstants.IMAP_CLOSE_NUT);
+ connection.executeSimpleCommand(command, false);
+ } catch (IOException ioe) {
+ throw new MessagingException(MessagingException.SERVER_ERROR, ioe.toString());
+ } finally {
+ connection.destroyResponses();
+ }
+ }
+
+ @ChangePinResult
+ private static int getChangePinResultFromImapResponse(ImapResponse response)
+ throws MessagingException {
+ if (!response.isTagged()) {
+ throw new MessagingException(MessagingException.SERVER_ERROR, "tagged response expected");
+ }
+ if (!response.isOk()) {
+ String message = response.getStringOrEmpty(1).getString();
+ LogUtils.d(TAG, "change PIN failed: " + message);
+ if (OmtpConstants.RESPONSE_CHANGE_PIN_TOO_SHORT.equals(message)) {
+ return OmtpConstants.CHANGE_PIN_TOO_SHORT;
+ }
+ if (OmtpConstants.RESPONSE_CHANGE_PIN_TOO_LONG.equals(message)) {
+ return OmtpConstants.CHANGE_PIN_TOO_LONG;
+ }
+ if (OmtpConstants.RESPONSE_CHANGE_PIN_TOO_WEAK.equals(message)) {
+ return OmtpConstants.CHANGE_PIN_TOO_WEAK;
+ }
+ if (OmtpConstants.RESPONSE_CHANGE_PIN_MISMATCH.equals(message)) {
+ return OmtpConstants.CHANGE_PIN_MISMATCH;
+ }
+ if (OmtpConstants.RESPONSE_CHANGE_PIN_INVALID_CHARACTER.equals(message)) {
+ return OmtpConstants.CHANGE_PIN_INVALID_CHARACTER;
+ }
+ return OmtpConstants.CHANGE_PIN_SYSTEM_ERROR;
+ }
+ LogUtils.d(TAG, "change PIN succeeded");
+ return OmtpConstants.CHANGE_PIN_SUCCESS;
+ }
+
+ public void updateQuota() {
+ try {
+ mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
+ if (mFolder == null) {
+ // This means we were unable to successfully open the folder.
+ return;
+ }
+ updateQuota(mFolder);
+ } catch (MessagingException e) {
+ LogUtils.e(TAG, e, "Messaging Exception");
+ } finally {
+ closeImapFolder();
+ }
+ }
+
+ public int getOccuupiedQuota() {
+ return mQuotaOccupied;
+ }
+
+ public int getTotalQuota() {
+ return mQuotaTotal;
+ }
+
+ private void updateQuota(ImapFolder folder) throws MessagingException {
+ setQuota(folder.getQuota());
+ }
+
+ private void setQuota(ImapFolder.Quota quota) {
+ if (quota == null) {
+ return;
+ }
+ if (quota.occupied == mQuotaOccupied && quota.total == mQuotaTotal) {
+ VvmLog.v(TAG, "Quota hasn't changed");
+ return;
+ }
+ mQuotaOccupied = quota.occupied;
+ mQuotaTotal = quota.total;
+ VoicemailStatus.edit(mContext, mPhoneAccount).setQuota(mQuotaOccupied, mQuotaTotal).apply();
+ mPrefs
+ .edit()
+ .putInt(PREF_KEY_QUOTA_OCCUPIED, mQuotaOccupied)
+ .putInt(PREF_KEY_QUOTA_TOTAL, mQuotaTotal)
+ .apply();
+ VvmLog.v(TAG, "Quota changed to " + mQuotaOccupied + "/" + mQuotaTotal);
+ }
+
+ /**
+ * A wrapper to hold a message with its header details and the structure for transcriptions (so
+ * they can be fetched in the future).
+ */
+ public static class MessageStructureWrapper {
+
+ public Message messageStructure;
+ public BodyPart transcriptionBodyPart;
+
+ public MessageStructureWrapper() {}
+ }
+
+ /** Listener for the message structure being fetched. */
+ private final class MessageStructureFetchedListener
+ implements ImapFolder.MessageRetrievalListener {
+
+ private MessageStructureWrapper mMessageStructure;
+
+ public MessageStructureFetchedListener() {}
+
+ public MessageStructureWrapper getMessageStructure() {
+ return mMessageStructure;
+ }
+
+ @Override
+ public void messageRetrieved(Message message) {
+ LogUtils.d(TAG, "Fetched message structure for " + message.getUid());
+ LogUtils.d(TAG, "Message retrieved: " + message);
+ try {
+ mMessageStructure = getMessageOrNull(message);
+ if (mMessageStructure == null) {
+ LogUtils.d(TAG, "This voicemail does not have an attachment...");
+ return;
+ }
+ } catch (MessagingException e) {
+ LogUtils.e(TAG, e, "Messaging Exception");
+ closeImapFolder();
+ }
+ }
+
+ /**
+ * Check if this IMAP message is a valid voicemail and whether it contains a transcription.
+ *
+ * @param message The IMAP message.
+ * @return The MessageStructureWrapper object corresponding to an IMAP message and
+ * transcription.
+ */
+ private MessageStructureWrapper getMessageOrNull(Message message) throws MessagingException {
+ if (!message.getMimeType().startsWith("multipart/")) {
+ LogUtils.w(TAG, "Ignored non multi-part message");
+ return null;
+ }
+
+ MessageStructureWrapper messageStructureWrapper = new MessageStructureWrapper();
+
+ Multipart multipart = (Multipart) message.getBody();
+ for (int i = 0; i < multipart.getCount(); ++i) {
+ BodyPart bodyPart = multipart.getBodyPart(i);
+ String bodyPartMimeType = bodyPart.getMimeType().toLowerCase();
+ LogUtils.d(TAG, "bodyPart mime type: " + bodyPartMimeType);
+
+ if (bodyPartMimeType.startsWith("audio/")) {
+ messageStructureWrapper.messageStructure = message;
+ } else if (bodyPartMimeType.startsWith("text/")) {
+ messageStructureWrapper.transcriptionBodyPart = bodyPart;
+ } else {
+ VvmLog.v(TAG, "Unknown bodyPart MIME: " + bodyPartMimeType);
+ }
+ }
+
+ if (messageStructureWrapper.messageStructure != null) {
+ return messageStructureWrapper;
+ }
+
+ // No attachment found, this is not a voicemail.
+ return null;
+ }
+ }
+
+ /** Listener for the message body being fetched. */
+ private final class MessageBodyFetchedListener implements ImapFolder.MessageRetrievalListener {
+
+ private VoicemailPayload mVoicemailPayload;
+
+ /** Returns the fetch voicemail payload. */
+ public VoicemailPayload getVoicemailPayload() {
+ return mVoicemailPayload;
+ }
+
+ @Override
+ public void messageRetrieved(Message message) {
+ LogUtils.d(TAG, "Fetched message body for " + message.getUid());
+ LogUtils.d(TAG, "Message retrieved: " + message);
+ try {
+ mVoicemailPayload = getVoicemailPayloadFromMessage(message);
+ } catch (MessagingException e) {
+ LogUtils.e(TAG, "Messaging Exception:", e);
+ } catch (IOException e) {
+ LogUtils.e(TAG, "IO Exception:", e);
+ }
+ }
+
+ private VoicemailPayload getVoicemailPayloadFromMessage(Message message)
+ throws MessagingException, IOException {
+ Multipart multipart = (Multipart) message.getBody();
+ List<String> mimeTypes = new ArrayList<>();
+ for (int i = 0; i < multipart.getCount(); ++i) {
+ BodyPart bodyPart = multipart.getBodyPart(i);
+ String bodyPartMimeType = bodyPart.getMimeType().toLowerCase();
+ mimeTypes.add(bodyPartMimeType);
+ if (bodyPartMimeType.startsWith("audio/")) {
+ byte[] bytes = getDataFromBody(bodyPart.getBody());
+ LogUtils.d(TAG, String.format("Fetched %s bytes of data", bytes.length));
+ return new VoicemailPayload(bodyPartMimeType, bytes);
+ }
+ }
+ LogUtils.e(TAG, "No audio attachment found on this voicemail, mimeTypes:" + mimeTypes);
+ return null;
+ }
+ }
+
+ /** Listener for the transcription being fetched. */
+ private final class TranscriptionFetchedListener implements ImapFolder.MessageRetrievalListener {
+
+ private String mVoicemailTranscription;
+
+ /** Returns the fetched voicemail transcription. */
+ public String getVoicemailTranscription() {
+ return mVoicemailTranscription;
+ }
+
+ @Override
+ public void messageRetrieved(Message message) {
+ LogUtils.d(TAG, "Fetched transcription for " + message.getUid());
+ try {
+ mVoicemailTranscription = new String(getDataFromBody(message.getBody()));
+ } catch (MessagingException e) {
+ LogUtils.e(TAG, "Messaging Exception:", e);
+ } catch (IOException e) {
+ LogUtils.e(TAG, "IO Exception:", e);
+ }
+ }
+ }
+
+ private ImapFolder openImapFolder(String modeReadWrite) {
+ try {
+ if (mImapStore == null) {
+ return null;
+ }
+ ImapFolder folder = new ImapFolder(mImapStore, ImapConstants.INBOX);
+ folder.open(modeReadWrite);
+ return folder;
+ } catch (MessagingException e) {
+ LogUtils.e(TAG, e, "Messaging Exception");
+ }
+ return null;
+ }
+
+ private Message[] convertToImapMessages(List<Voicemail> voicemails) {
+ Message[] messages = new Message[voicemails.size()];
+ for (int i = 0; i < voicemails.size(); ++i) {
+ messages[i] = new MimeMessage();
+ messages[i].setUid(voicemails.get(i).getSourceData());
+ }
+ return messages;
+ }
+
+ private void closeImapFolder() {
+ if (mFolder != null) {
+ mFolder.close(true);
+ }
+ }
+
+ private byte[] getDataFromBody(Body body) throws IOException, MessagingException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ BufferedOutputStream bufferedOut = new BufferedOutputStream(out);
+ try {
+ body.writeTo(bufferedOut);
+ return Base64.decode(out.toByteArray(), Base64.DEFAULT);
+ } finally {
+ IOUtils.closeQuietly(bufferedOut);
+ IOUtils.closeQuietly(out);
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/imap/VoicemailPayload.java b/java/com/android/voicemail/impl/imap/VoicemailPayload.java
index 04c69dea5..69befb42f 100644
--- a/java/com/android/voicemailomtp/imap/VoicemailPayload.java
+++ b/java/com/android/voicemail/impl/imap/VoicemailPayload.java
@@ -14,25 +14,23 @@
* limitations under the License.
*/
-package com.android.voicemailomtp.imap;
+package com.android.voicemail.impl.imap;
-/**
- * The payload for a voicemail, usually audio data.
- */
+/** The payload for a voicemail, usually audio data. */
public class VoicemailPayload {
- private final String mMimeType;
- private final byte[] mBytes;
+ private final String mMimeType;
+ private final byte[] mBytes;
- public VoicemailPayload(String mimeType, byte[] bytes) {
- mMimeType = mimeType;
- mBytes = bytes;
- }
+ public VoicemailPayload(String mimeType, byte[] bytes) {
+ mMimeType = mimeType;
+ mBytes = bytes;
+ }
- public byte[] getBytes() {
- return mBytes;
- }
+ public byte[] getBytes() {
+ return mBytes;
+ }
- public String getMimeType() {
- return mMimeType;
- }
-} \ No newline at end of file
+ public String getMimeType() {
+ return mMimeType;
+ }
+}
diff --git a/java/com/android/voicemail/impl/mail/Address.java b/java/com/android/voicemail/impl/mail/Address.java
new file mode 100644
index 000000000..3a7a86607
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/Address.java
@@ -0,0 +1,520 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.VisibleForTesting;
+import android.text.Html;
+import android.text.TextUtils;
+import android.text.util.Rfc822Token;
+import android.text.util.Rfc822Tokenizer;
+import com.android.voicemail.impl.mail.utils.LogUtils;
+import java.util.ArrayList;
+import java.util.regex.Pattern;
+import org.apache.james.mime4j.codec.EncoderUtil;
+import org.apache.james.mime4j.decoder.DecoderUtil;
+
+/**
+ * This class represent email address.
+ *
+ * <p>RFC822 email address may have following format. "name" <address> (comment) "name" <address>
+ * name <address> address Name and comment part should be MIME/base64 encoded in header if
+ * necessary.
+ */
+public class Address implements Parcelable {
+ public static final String ADDRESS_DELIMETER = ",";
+ /** Address part, in the form local_part@domain_part. No surrounding angle brackets. */
+ private String mAddress;
+
+ /**
+ * Name part. No surrounding double quote, and no MIME/base64 encoding. This must be null if
+ * Address has no name part.
+ */
+ private String mPersonal;
+
+ /**
+ * When personal is set, it will return the first token of the personal string. Otherwise, it will
+ * return the e-mail address up to the '@' sign.
+ */
+ private String mSimplifiedName;
+
+ // Regex that matches address surrounded by '<>' optionally. '^<?([^>]+)>?$'
+ private static final Pattern REMOVE_OPTIONAL_BRACKET = Pattern.compile("^<?([^>]+)>?$");
+ // Regex that matches personal name surrounded by '""' optionally. '^"?([^"]+)"?$'
+ private static final Pattern REMOVE_OPTIONAL_DQUOTE = Pattern.compile("^\"?([^\"]*)\"?$");
+ // Regex that matches escaped character '\\([\\"])'
+ private static final Pattern UNQUOTE = Pattern.compile("\\\\([\\\\\"])");
+
+ // TODO: LOCAL_PART and DOMAIN_PART_PART are too permissive and can be improved.
+ // TODO: Fix this to better constrain comments.
+ /** Regex for the local part of an email address. */
+ private static final String LOCAL_PART = "[^@]+";
+ /** Regex for each part of the domain part, i.e. the thing between the dots. */
+ private static final String DOMAIN_PART_PART = "[[\\w][\\d]\\-\\(\\)\\[\\]]+";
+ /** Regex for the domain part, which is two or more {@link #DOMAIN_PART_PART} separated by . */
+ private static final String DOMAIN_PART = "(" + DOMAIN_PART_PART + "\\.)+" + DOMAIN_PART_PART;
+
+ /** Pattern to check if an email address is valid. */
+ private static final Pattern EMAIL_ADDRESS =
+ Pattern.compile("\\A" + LOCAL_PART + "@" + DOMAIN_PART + "\\z");
+
+ private static final Address[] EMPTY_ADDRESS_ARRAY = new Address[0];
+
+ // delimiters are chars that do not appear in an email address, used by fromHeader
+ private static final char LIST_DELIMITER_EMAIL = '\1';
+ private static final char LIST_DELIMITER_PERSONAL = '\2';
+
+ private static final String LOG_TAG = "Email Address";
+
+ @VisibleForTesting
+ public Address(String address) {
+ setAddress(address);
+ }
+
+ public Address(String address, String personal) {
+ setPersonal(personal);
+ setAddress(address);
+ }
+
+ /**
+ * Returns a simplified string for this e-mail address. When a name is known, it will return the
+ * first token of that name. Otherwise, it will return the e-mail address up to the '@' sign.
+ */
+ public String getSimplifiedName() {
+ if (mSimplifiedName == null) {
+ if (TextUtils.isEmpty(mPersonal) && !TextUtils.isEmpty(mAddress)) {
+ int atSign = mAddress.indexOf('@');
+ mSimplifiedName = (atSign != -1) ? mAddress.substring(0, atSign) : "";
+ } else if (!TextUtils.isEmpty(mPersonal)) {
+
+ // TODO: use Contacts' NameSplitter for more reliable first-name extraction
+
+ int end = mPersonal.indexOf(' ');
+ while (end > 0 && mPersonal.charAt(end - 1) == ',') {
+ end--;
+ }
+ mSimplifiedName = (end < 1) ? mPersonal : mPersonal.substring(0, end);
+
+ } else {
+ LogUtils.w(LOG_TAG, "Unable to get a simplified name");
+ mSimplifiedName = "";
+ }
+ }
+ return mSimplifiedName;
+ }
+
+ public static synchronized Address getEmailAddress(String rawAddress) {
+ if (TextUtils.isEmpty(rawAddress)) {
+ return null;
+ }
+ String name, address;
+ final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(rawAddress);
+ if (tokens.length > 0) {
+ final String tokenizedName = tokens[0].getName();
+ name = tokenizedName != null ? Html.fromHtml(tokenizedName.trim()).toString() : "";
+ address = Html.fromHtml(tokens[0].getAddress()).toString();
+ } else {
+ name = "";
+ address = rawAddress == null ? "" : Html.fromHtml(rawAddress).toString();
+ }
+ return new Address(address, name);
+ }
+
+ public String getAddress() {
+ return mAddress;
+ }
+
+ public void setAddress(String address) {
+ mAddress = REMOVE_OPTIONAL_BRACKET.matcher(address).replaceAll("$1");
+ }
+
+ /**
+ * Get name part as UTF-16 string. No surrounding double quote, and no MIME/base64 encoding.
+ *
+ * @return Name part of email address. Returns null if it is omitted.
+ */
+ public String getPersonal() {
+ return mPersonal;
+ }
+
+ /**
+ * Set personal part from UTF-16 string. Optional surrounding double quote will be removed. It
+ * will be also unquoted and MIME/base64 decoded.
+ *
+ * @param personal name part of email address as UTF-16 string. Null is acceptable.
+ */
+ public void setPersonal(String personal) {
+ mPersonal = decodeAddressPersonal(personal);
+ }
+
+ /**
+ * Decodes name from UTF-16 string. Optional surrounding double quote will be removed. It will be
+ * also unquoted and MIME/base64 decoded.
+ *
+ * @param personal name part of email address as UTF-16 string. Null is acceptable.
+ */
+ public static String decodeAddressPersonal(String personal) {
+ if (personal != null) {
+ personal = REMOVE_OPTIONAL_DQUOTE.matcher(personal).replaceAll("$1");
+ personal = UNQUOTE.matcher(personal).replaceAll("$1");
+ personal = DecoderUtil.decodeEncodedWords(personal);
+ if (personal.length() == 0) {
+ personal = null;
+ }
+ }
+ return personal;
+ }
+
+ /**
+ * This method is used to check that all the addresses that the user entered in a list (e.g. To:)
+ * are valid, so that none is dropped.
+ */
+ @VisibleForTesting
+ public static boolean isAllValid(String addressList) {
+ // This code mimics the parse() method below.
+ // I don't know how to better avoid the code-duplication.
+ if (addressList != null && addressList.length() > 0) {
+ Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
+ for (int i = 0, length = tokens.length; i < length; ++i) {
+ Rfc822Token token = tokens[i];
+ String address = token.getAddress();
+ if (!TextUtils.isEmpty(address) && !isValidAddress(address)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Parse a comma-delimited list of addresses in RFC822 format and return an array of Address
+ * objects.
+ *
+ * @param addressList Address list in comma-delimited string.
+ * @return An array of 0 or more Addresses.
+ */
+ public static Address[] parse(String addressList) {
+ if (addressList == null || addressList.length() == 0) {
+ return EMPTY_ADDRESS_ARRAY;
+ }
+ Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
+ ArrayList<Address> addresses = new ArrayList<Address>();
+ for (int i = 0, length = tokens.length; i < length; ++i) {
+ Rfc822Token token = tokens[i];
+ String address = token.getAddress();
+ if (!TextUtils.isEmpty(address)) {
+ if (isValidAddress(address)) {
+ String name = token.getName();
+ if (TextUtils.isEmpty(name)) {
+ name = null;
+ }
+ addresses.add(new Address(address, name));
+ }
+ }
+ }
+ return addresses.toArray(new Address[addresses.size()]);
+ }
+
+ /** Checks whether a string email address is valid. E.g. name@domain.com is valid. */
+ @VisibleForTesting
+ static boolean isValidAddress(final String address) {
+ return EMAIL_ADDRESS.matcher(address).find();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof Address) {
+ // It seems that the spec says that the "user" part is case-sensitive,
+ // while the domain part in case-insesitive.
+ // So foo@yahoo.com and Foo@yahoo.com are different.
+ // This may seem non-intuitive from the user POV, so we
+ // may re-consider it if it creates UI trouble.
+ // A problem case is "replyAll" sending to both
+ // a@b.c and to A@b.c, which turn out to be the same on the server.
+ // Leave unchanged for now (i.e. case-sensitive).
+ return getAddress().equals(((Address) o).getAddress());
+ }
+ return super.equals(o);
+ }
+
+ @Override
+ public int hashCode() {
+ return getAddress().hashCode();
+ }
+
+ /**
+ * Get human readable address string. Do not use this for email header.
+ *
+ * @return Human readable address string. Not quoted and not encoded.
+ */
+ @Override
+ public String toString() {
+ if (mPersonal != null && !mPersonal.equals(mAddress)) {
+ if (mPersonal.matches(".*[\\(\\)<>@,;:\\\\\".\\[\\]].*")) {
+ return ensureQuotedString(mPersonal) + " <" + mAddress + ">";
+ } else {
+ return mPersonal + " <" + mAddress + ">";
+ }
+ } else {
+ return mAddress;
+ }
+ }
+
+ /**
+ * Ensures that the given string starts and ends with the double quote character. The string is
+ * not modified in any way except to add the double quote character to start and end if it's not
+ * already there.
+ *
+ * <p>sample -> "sample" "sample" -> "sample" ""sample"" -> "sample" "sample"" -> "sample"
+ * sa"mp"le -> "sa"mp"le" "sa"mp"le" -> "sa"mp"le" (empty string) -> "" " -> ""
+ */
+ private static String ensureQuotedString(String s) {
+ if (s == null) {
+ return null;
+ }
+ if (!s.matches("^\".*\"$")) {
+ return "\"" + s + "\"";
+ } else {
+ return s;
+ }
+ }
+
+ /**
+ * Get human readable comma-delimited address string.
+ *
+ * @param addresses Address array
+ * @return Human readable comma-delimited address string.
+ */
+ @VisibleForTesting
+ public static String toString(Address[] addresses) {
+ return toString(addresses, ADDRESS_DELIMETER);
+ }
+
+ /**
+ * Get human readable address strings joined with the specified separator.
+ *
+ * @param addresses Address array
+ * @param separator Separator
+ * @return Human readable comma-delimited address string.
+ */
+ public static String toString(Address[] addresses, String separator) {
+ if (addresses == null || addresses.length == 0) {
+ return null;
+ }
+ if (addresses.length == 1) {
+ return addresses[0].toString();
+ }
+ StringBuilder sb = new StringBuilder(addresses[0].toString());
+ for (int i = 1; i < addresses.length; i++) {
+ sb.append(separator);
+ // TODO: investigate why this .trim() is needed.
+ sb.append(addresses[i].toString().trim());
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Get RFC822/MIME compatible address string.
+ *
+ * @return RFC822/MIME compatible address string. It may be surrounded by double quote or quoted
+ * and MIME/base64 encoded if necessary.
+ */
+ public String toHeader() {
+ if (mPersonal != null) {
+ return EncoderUtil.encodeAddressDisplayName(mPersonal) + " <" + mAddress + ">";
+ } else {
+ return mAddress;
+ }
+ }
+
+ /**
+ * Get RFC822/MIME compatible comma-delimited address string.
+ *
+ * @param addresses Address array
+ * @return RFC822/MIME compatible comma-delimited address string. it may be surrounded by double
+ * quoted or quoted and MIME/base64 encoded if necessary.
+ */
+ public static String toHeader(Address[] addresses) {
+ if (addresses == null || addresses.length == 0) {
+ return null;
+ }
+ if (addresses.length == 1) {
+ return addresses[0].toHeader();
+ }
+ StringBuilder sb = new StringBuilder(addresses[0].toHeader());
+ for (int i = 1; i < addresses.length; i++) {
+ // We need space character to be able to fold line.
+ sb.append(", ");
+ sb.append(addresses[i].toHeader());
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Get Human friendly address string.
+ *
+ * @return the personal part of this Address, or the address part if the personal part is not
+ * available
+ */
+ @VisibleForTesting
+ public String toFriendly() {
+ if (mPersonal != null && mPersonal.length() > 0) {
+ return mPersonal;
+ } else {
+ return mAddress;
+ }
+ }
+
+ /**
+ * Creates a comma-delimited list of addresses in the "friendly" format (see toFriendly() for
+ * details on the per-address conversion).
+ *
+ * @param addresses Array of Address[] values
+ * @return A comma-delimited string listing all of the addresses supplied. Null if source was null
+ * or empty.
+ */
+ @VisibleForTesting
+ public static String toFriendly(Address[] addresses) {
+ if (addresses == null || addresses.length == 0) {
+ return null;
+ }
+ if (addresses.length == 1) {
+ return addresses[0].toFriendly();
+ }
+ StringBuilder sb = new StringBuilder(addresses[0].toFriendly());
+ for (int i = 1; i < addresses.length; i++) {
+ sb.append(", ");
+ sb.append(addresses[i].toFriendly());
+ }
+ return sb.toString();
+ }
+
+ /** Returns exactly the same result as Address.toString(Address.fromHeader(addressList)). */
+ @VisibleForTesting
+ public static String fromHeaderToString(String addressList) {
+ return toString(fromHeader(addressList));
+ }
+
+ /** Returns exactly the same result as Address.toHeader(Address.parse(addressList)). */
+ @VisibleForTesting
+ public static String parseToHeader(String addressList) {
+ return Address.toHeader(Address.parse(addressList));
+ }
+
+ /**
+ * Returns null if the addressList has 0 addresses, otherwise returns the first address. The same
+ * as Address.fromHeader(addressList)[0] for non-empty list. This is an utility method that offers
+ * some performance optimization opportunities.
+ */
+ @VisibleForTesting
+ public static Address firstAddress(String addressList) {
+ Address[] array = fromHeader(addressList);
+ return array.length > 0 ? array[0] : null;
+ }
+
+ /**
+ * This method exists to convert an address list formatted in a deprecated legacy format to the
+ * standard RFC822 header format. {@link #fromHeader(String)} is capable of reading the legacy
+ * format and the RFC822 format. {@link #toHeader()} always produces the RFC822 format.
+ *
+ * <p>This implementation is brute-force, and could be replaced with a more efficient version if
+ * desired.
+ */
+ public static String reformatToHeader(String addressList) {
+ return toHeader(fromHeader(addressList));
+ }
+
+ /**
+ * @param addressList a CSV of RFC822 addresses or the deprecated legacy string format
+ * @return array of addresses parsed from <code>addressList</code>
+ */
+ @VisibleForTesting
+ public static Address[] fromHeader(String addressList) {
+ if (addressList == null || addressList.length() == 0) {
+ return EMPTY_ADDRESS_ARRAY;
+ }
+ // IF we're CSV, just parse
+ if ((addressList.indexOf(LIST_DELIMITER_PERSONAL) == -1)
+ && (addressList.indexOf(LIST_DELIMITER_EMAIL) == -1)) {
+ return Address.parse(addressList);
+ }
+ // Otherwise, do backward-compatible unpack
+ ArrayList<Address> addresses = new ArrayList<Address>();
+ int length = addressList.length();
+ int pairStartIndex = 0;
+ int pairEndIndex;
+
+ /* addressEndIndex is only re-scanned (indexOf()) when a LIST_DELIMITER_PERSONAL
+ is used, not for every email address; i.e. not for every iteration of the while().
+ This reduces the theoretical complexity from quadratic to linear,
+ and provides some speed-up in practice by removing redundant scans of the string.
+ */
+ int addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL);
+
+ while (pairStartIndex < length) {
+ pairEndIndex = addressList.indexOf(LIST_DELIMITER_EMAIL, pairStartIndex);
+ if (pairEndIndex == -1) {
+ pairEndIndex = length;
+ }
+ Address address;
+ if (addressEndIndex == -1 || pairEndIndex <= addressEndIndex) {
+ // in this case the DELIMITER_PERSONAL is in a future pair,
+ // so don't use personal, and don't update addressEndIndex
+ address = new Address(addressList.substring(pairStartIndex, pairEndIndex), null);
+ } else {
+ address =
+ new Address(
+ addressList.substring(pairStartIndex, addressEndIndex),
+ addressList.substring(addressEndIndex + 1, pairEndIndex));
+ // only update addressEndIndex when we use the LIST_DELIMITER_PERSONAL
+ addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL, pairEndIndex + 1);
+ }
+ addresses.add(address);
+ pairStartIndex = pairEndIndex + 1;
+ }
+ return addresses.toArray(new Address[addresses.size()]);
+ }
+
+ public static final Creator<Address> CREATOR =
+ new Creator<Address>() {
+ @Override
+ public Address createFromParcel(Parcel parcel) {
+ return new Address(parcel);
+ }
+
+ @Override
+ public Address[] newArray(int size) {
+ return new Address[size];
+ }
+ };
+
+ public Address(Parcel in) {
+ setPersonal(in.readString());
+ setAddress(in.readString());
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(mPersonal);
+ out.writeString(mAddress);
+ }
+}
diff --git a/java/com/android/voicemailomtp/mail/AuthenticationFailedException.java b/java/com/android/voicemail/impl/mail/AuthenticationFailedException.java
index 995d5d348..c9fa08750 100644
--- a/java/com/android/voicemailomtp/mail/AuthenticationFailedException.java
+++ b/java/com/android/voicemail/impl/mail/AuthenticationFailedException.java
@@ -14,20 +14,20 @@
* limitations under the License.
*/
-package com.android.voicemailomtp.mail;
+package com.android.voicemail.impl.mail;
public class AuthenticationFailedException extends MessagingException {
- public static final long serialVersionUID = -1;
+ public static final long serialVersionUID = -1;
- public AuthenticationFailedException(String message) {
- super(MessagingException.AUTHENTICATION_FAILED, message);
- }
+ public AuthenticationFailedException(String message) {
+ super(MessagingException.AUTHENTICATION_FAILED, message);
+ }
- public AuthenticationFailedException(int exceptionType, String message) {
- super(exceptionType, message);
- }
+ public AuthenticationFailedException(int exceptionType, String message) {
+ super(exceptionType, message);
+ }
- public AuthenticationFailedException(String message, Throwable throwable) {
- super(MessagingException.AUTHENTICATION_FAILED, message, throwable);
- }
-} \ No newline at end of file
+ public AuthenticationFailedException(String message, Throwable throwable) {
+ super(MessagingException.AUTHENTICATION_FAILED, message, throwable);
+ }
+}
diff --git a/java/com/android/voicemail/impl/mail/Base64Body.java b/java/com/android/voicemail/impl/mail/Base64Body.java
new file mode 100644
index 000000000..def94dbb5
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/Base64Body.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail;
+
+import android.util.Base64;
+import android.util.Base64OutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import org.apache.commons.io.IOUtils;
+
+public class Base64Body implements Body {
+ private final InputStream mSource;
+ // Because we consume the input stream, we can only write out once
+ private boolean mAlreadyWritten;
+
+ public Base64Body(InputStream source) {
+ mSource = source;
+ }
+
+ @Override
+ public InputStream getInputStream() throws MessagingException {
+ return mSource;
+ }
+
+ /**
+ * This method consumes the input stream, so can only be called once
+ *
+ * @param out Stream to write to
+ * @throws IllegalStateException If called more than once
+ * @throws IOException
+ * @throws MessagingException
+ */
+ @Override
+ public void writeTo(OutputStream out)
+ throws IllegalStateException, IOException, MessagingException {
+ if (mAlreadyWritten) {
+ throw new IllegalStateException("Base64Body can only be written once");
+ }
+ mAlreadyWritten = true;
+ try {
+ final Base64OutputStream b64out = new Base64OutputStream(out, Base64.DEFAULT);
+ IOUtils.copyLarge(mSource, b64out);
+ } finally {
+ mSource.close();
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/mail/Body.java b/java/com/android/voicemail/impl/mail/Body.java
index 393e1823c..3ad81bcc8 100644
--- a/java/com/android/voicemailomtp/mail/Body.java
+++ b/java/com/android/voicemail/impl/mail/Body.java
@@ -13,13 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.voicemailomtp.mail;
+package com.android.voicemail.impl.mail;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public interface Body {
- public InputStream getInputStream() throws MessagingException;
- public void writeTo(OutputStream out) throws IOException, MessagingException;
+ public InputStream getInputStream() throws MessagingException;
+
+ public void writeTo(OutputStream out) throws IOException, MessagingException;
}
diff --git a/java/com/android/voicemailomtp/mail/BodyPart.java b/java/com/android/voicemail/impl/mail/BodyPart.java
index 62390a43e..3d15d4bad 100644
--- a/java/com/android/voicemailomtp/mail/BodyPart.java
+++ b/java/com/android/voicemail/impl/mail/BodyPart.java
@@ -13,12 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.voicemailomtp.mail;
+package com.android.voicemail.impl.mail;
public abstract class BodyPart implements Part {
- protected Multipart mParent;
+ protected Multipart mParent;
- public Multipart getParent() {
- return mParent;
- }
+ public Multipart getParent() {
+ return mParent;
+ }
}
diff --git a/java/com/android/voicemailomtp/mail/CertificateValidationException.java b/java/com/android/voicemail/impl/mail/CertificateValidationException.java
index 8ebe5480b..6f3bb2ff4 100644
--- a/java/com/android/voicemailomtp/mail/CertificateValidationException.java
+++ b/java/com/android/voicemail/impl/mail/CertificateValidationException.java
@@ -14,16 +14,16 @@
* limitations under the License.
*/
-package com.android.voicemailomtp.mail;
+package com.android.voicemail.impl.mail;
public class CertificateValidationException extends MessagingException {
- public static final long serialVersionUID = -1;
+ public static final long serialVersionUID = -1;
- public CertificateValidationException(String message) {
- super(MessagingException.CERTIFICATE_VALIDATION_ERROR, message);
- }
+ public CertificateValidationException(String message) {
+ super(CERTIFICATE_VALIDATION_ERROR, message);
+ }
- public CertificateValidationException(String message, Throwable throwable) {
- super(MessagingException.CERTIFICATE_VALIDATION_ERROR, message, throwable);
- }
-} \ No newline at end of file
+ public CertificateValidationException(String message, Throwable throwable) {
+ super(CERTIFICATE_VALIDATION_ERROR, message, throwable);
+ }
+}
diff --git a/java/com/android/voicemail/impl/mail/FetchProfile.java b/java/com/android/voicemail/impl/mail/FetchProfile.java
new file mode 100644
index 000000000..28a7080e6
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/FetchProfile.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail;
+
+import java.util.ArrayList;
+
+/**
+ *
+ *
+ * <pre>
+ * A FetchProfile is a list of items that should be downloaded in bulk for a set of messages.
+ * FetchProfile can contain the following objects:
+ * FetchProfile.Item: Described below.
+ * Message: Indicates that the body of the entire message should be fetched.
+ * Synonymous with FetchProfile.Item.BODY.
+ * Part: Indicates that the given Part should be fetched. The provider
+ * is expected have previously created the given BodyPart and stored
+ * any information it needs to download the content.
+ * </pre>
+ */
+public class FetchProfile extends ArrayList<Fetchable> {
+ /**
+ * Default items available for pre-fetching. It should be expected that any item fetched by using
+ * these items could potentially include all of the previous items.
+ */
+ public enum Item implements Fetchable {
+ /** Download the flags of the message. */
+ FLAGS,
+
+ /**
+ * Download the envelope of the message. This should include at minimum the size and the
+ * following headers: date, subject, from, content-type, to, cc
+ */
+ ENVELOPE,
+
+ /**
+ * Download the structure of the message. This maps directly to IMAP's BODYSTRUCTURE and may map
+ * to other providers. The provider should, if possible, fill in a properly formatted MIME
+ * structure in the message without actually downloading any message data. If the provider is
+ * not capable of this operation it should specifically set the body of the message to null so
+ * that upper levels can detect that a full body download is needed.
+ */
+ STRUCTURE,
+
+ /**
+ * A sane portion of the entire message, cut off at a provider determined limit. This should
+ * generally be around 50kB.
+ */
+ BODY_SANE,
+
+ /** The entire message. */
+ BODY,
+ }
+
+ /**
+ * @return the first {@link Part} in this collection, or null if it doesn't contain {@link Part}.
+ */
+ public Part getFirstPart() {
+ for (Fetchable o : this) {
+ if (o instanceof Part) {
+ return (Part) o;
+ }
+ }
+ return null;
+ }
+}
diff --git a/java/com/android/voicemailomtp/mail/Fetchable.java b/java/com/android/voicemail/impl/mail/Fetchable.java
index 1d8d0005b..237ef6950 100644
--- a/java/com/android/voicemailomtp/mail/Fetchable.java
+++ b/java/com/android/voicemail/impl/mail/Fetchable.java
@@ -13,11 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.voicemailomtp.mail;
+package com.android.voicemail.impl.mail;
/**
- * Interface for classes that can be added to {@link FetchProfile}.
- * i.e. {@link Part} and its subclasses, and {@link FetchProfile.Item}.
+ * Interface for classes that can be added to {@link FetchProfile}. i.e. {@link Part} and its
+ * subclasses, and {@link FetchProfile.Item}.
*/
-public interface Fetchable {
-}
+public interface Fetchable {}
diff --git a/java/com/android/voicemail/impl/mail/FixedLengthInputStream.java b/java/com/android/voicemail/impl/mail/FixedLengthInputStream.java
new file mode 100644
index 000000000..bd3c16401
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/FixedLengthInputStream.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A filtering InputStream that stops allowing reads after the given length has been read. This is
+ * used to allow a client to read directly from an underlying protocol stream without reading past
+ * where the protocol handler intended the client to read.
+ */
+public class FixedLengthInputStream extends InputStream {
+ private final InputStream mIn;
+ private final int mLength;
+ private int mCount;
+
+ public FixedLengthInputStream(InputStream in, int length) {
+ this.mIn = in;
+ this.mLength = length;
+ }
+
+ @Override
+ public int available() throws IOException {
+ return mLength - mCount;
+ }
+
+ @Override
+ public int read() throws IOException {
+ if (mCount < mLength) {
+ mCount++;
+ return mIn.read();
+ } else {
+ return -1;
+ }
+ }
+
+ @Override
+ public int read(byte[] b, int offset, int length) throws IOException {
+ if (mCount < mLength) {
+ int d = mIn.read(b, offset, Math.min(mLength - mCount, length));
+ if (d == -1) {
+ return -1;
+ } else {
+ mCount += d;
+ return d;
+ }
+ } else {
+ return -1;
+ }
+ }
+
+ @Override
+ public int read(byte[] b) throws IOException {
+ return read(b, 0, b.length);
+ }
+
+ public int getLength() {
+ return mLength;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("FixedLengthInputStream(in=%s, length=%d)", mIn.toString(), mLength);
+ }
+}
diff --git a/java/com/android/voicemailomtp/mail/Flag.java b/java/com/android/voicemail/impl/mail/Flag.java
index a9f927099..72b5c1fa5 100644
--- a/java/com/android/voicemailomtp/mail/Flag.java
+++ b/java/com/android/voicemail/impl/mail/Flag.java
@@ -13,17 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.voicemailomtp.mail;
+package com.android.voicemail.impl.mail;
-/**
- * Flags that can be applied to Messages.
- */
+/** Flags that can be applied to Messages. */
public class Flag {
- // If adding new flags: ALL FLAGS MUST BE UPPER CASE.
- public static final String DELETED = "deleted";
- public static final String SEEN = "seen";
- public static final String ANSWERED = "answered";
- public static final String FLAGGED = "flagged";
- public static final String DRAFT = "draft";
- public static final String RECENT = "recent";
+ // If adding new flags: ALL FLAGS MUST BE UPPER CASE.
+ public static final String DELETED = "deleted";
+ public static final String SEEN = "seen";
+ public static final String ANSWERED = "answered";
+ public static final String FLAGGED = "flagged";
+ public static final String DRAFT = "draft";
+ public static final String RECENT = "recent";
}
diff --git a/java/com/android/voicemail/impl/mail/MailTransport.java b/java/com/android/voicemail/impl/mail/MailTransport.java
new file mode 100644
index 000000000..3df36d544
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/MailTransport.java
@@ -0,0 +1,343 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail;
+
+import android.content.Context;
+import android.net.Network;
+import android.support.annotation.VisibleForTesting;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.imap.ImapHelper;
+import com.android.voicemail.impl.mail.store.ImapStore;
+import com.android.voicemail.impl.mail.utils.LogUtils;
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.List;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocket;
+
+/** Make connection and perform operations on mail server by reading and writing lines. */
+public class MailTransport {
+ private static final String TAG = "MailTransport";
+
+ // TODO protected eventually
+ /*protected*/ public static final int SOCKET_CONNECT_TIMEOUT = 10000;
+ /*protected*/ public static final int SOCKET_READ_TIMEOUT = 60000;
+
+ private static final HostnameVerifier HOSTNAME_VERIFIER =
+ HttpsURLConnection.getDefaultHostnameVerifier();
+
+ private final Context mContext;
+ private final ImapHelper mImapHelper;
+ private final Network mNetwork;
+ private final String mHost;
+ private final int mPort;
+ private Socket mSocket;
+ private BufferedInputStream mIn;
+ private BufferedOutputStream mOut;
+ private final int mFlags;
+ private SocketCreator mSocketCreator;
+ private InetSocketAddress mAddress;
+
+ public MailTransport(
+ Context context,
+ ImapHelper imapHelper,
+ Network network,
+ String address,
+ int port,
+ int flags) {
+ mContext = context;
+ mImapHelper = imapHelper;
+ mNetwork = network;
+ mHost = address;
+ mPort = port;
+ mFlags = flags;
+ }
+
+ /**
+ * Returns a new transport, using the current transport as a model. The new transport is
+ * configured identically, but not opened or connected in any way.
+ */
+ @Override
+ public MailTransport clone() {
+ return new MailTransport(mContext, mImapHelper, mNetwork, mHost, mPort, mFlags);
+ }
+
+ public boolean canTrySslSecurity() {
+ return (mFlags & ImapStore.FLAG_SSL) != 0;
+ }
+
+ public boolean canTrustAllCertificates() {
+ return (mFlags & ImapStore.FLAG_TRUST_ALL) != 0;
+ }
+
+ /**
+ * Attempts to open a connection using the Uri supplied for connection parameters. Will attempt an
+ * SSL connection if indicated.
+ */
+ public void open() throws MessagingException {
+ LogUtils.d(TAG, "*** IMAP open " + mHost + ":" + String.valueOf(mPort));
+
+ List<InetSocketAddress> socketAddresses = new ArrayList<InetSocketAddress>();
+
+ if (mNetwork == null) {
+ socketAddresses.add(new InetSocketAddress(mHost, mPort));
+ } else {
+ try {
+ InetAddress[] inetAddresses = mNetwork.getAllByName(mHost);
+ if (inetAddresses.length == 0) {
+ throw new MessagingException(
+ MessagingException.IOERROR,
+ "Host name " + mHost + "cannot be resolved on designated network");
+ }
+ for (int i = 0; i < inetAddresses.length; i++) {
+ socketAddresses.add(new InetSocketAddress(inetAddresses[i], mPort));
+ }
+ } catch (IOException ioe) {
+ LogUtils.d(TAG, ioe.toString());
+ mImapHelper.handleEvent(OmtpEvents.DATA_CANNOT_RESOLVE_HOST_ON_NETWORK);
+ throw new MessagingException(MessagingException.IOERROR, ioe.toString());
+ }
+ }
+
+ boolean success = false;
+ while (socketAddresses.size() > 0) {
+ mSocket = createSocket();
+ try {
+ mAddress = socketAddresses.remove(0);
+ mSocket.connect(mAddress, SOCKET_CONNECT_TIMEOUT);
+
+ if (canTrySslSecurity()) {
+ /*
+ SSLSocket cannot be created with a connection timeout, so instead of doing a
+ direct SSL connection, we connect with a normal connection and upgrade it into
+ SSL
+ */
+ reopenTls();
+ } else {
+ mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
+ mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
+ mSocket.setSoTimeout(SOCKET_READ_TIMEOUT);
+ }
+ success = true;
+ return;
+ } catch (IOException ioe) {
+ LogUtils.d(TAG, ioe.toString());
+ if (socketAddresses.size() == 0) {
+ // Only throw an error when there are no more sockets to try.
+ mImapHelper.handleEvent(OmtpEvents.DATA_ALL_SOCKET_CONNECTION_FAILED);
+ throw new MessagingException(MessagingException.IOERROR, ioe.toString());
+ }
+ } finally {
+ if (!success) {
+ try {
+ mSocket.close();
+ mSocket = null;
+ } catch (IOException ioe) {
+ throw new MessagingException(MessagingException.IOERROR, ioe.toString());
+ }
+ }
+ }
+ }
+ }
+
+ // For testing. We need something that can replace the behavior of "new Socket()"
+ @VisibleForTesting
+ interface SocketCreator {
+
+ Socket createSocket() throws MessagingException;
+ }
+
+ @VisibleForTesting
+ void setSocketCreator(SocketCreator creator) {
+ mSocketCreator = creator;
+ }
+
+ protected Socket createSocket() throws MessagingException {
+ if (mSocketCreator != null) {
+ return mSocketCreator.createSocket();
+ }
+
+ if (mNetwork == null) {
+ LogUtils.v(TAG, "createSocket: network not specified");
+ return new Socket();
+ }
+
+ try {
+ LogUtils.v(TAG, "createSocket: network specified");
+ return mNetwork.getSocketFactory().createSocket();
+ } catch (IOException ioe) {
+ LogUtils.d(TAG, ioe.toString());
+ throw new MessagingException(MessagingException.IOERROR, ioe.toString());
+ }
+ }
+
+ /** Attempts to reopen a normal connection into a TLS connection. */
+ public void reopenTls() throws MessagingException {
+ try {
+ LogUtils.d(TAG, "open: converting to TLS socket");
+ mSocket =
+ HttpsURLConnection.getDefaultSSLSocketFactory()
+ .createSocket(mSocket, mAddress.getHostName(), mAddress.getPort(), true);
+ // After the socket connects to an SSL server, confirm that the hostname is as
+ // expected
+ if (!canTrustAllCertificates()) {
+ verifyHostname(mSocket, mHost);
+ }
+ mSocket.setSoTimeout(SOCKET_READ_TIMEOUT);
+ mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
+ mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
+
+ } catch (SSLException e) {
+ LogUtils.d(TAG, e.toString());
+ throw new CertificateValidationException(e.getMessage(), e);
+ } catch (IOException ioe) {
+ LogUtils.d(TAG, ioe.toString());
+ throw new MessagingException(MessagingException.IOERROR, ioe.toString());
+ }
+ }
+
+ /**
+ * Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this service
+ * but is not in the public API.
+ *
+ * <p>Verify the hostname of the certificate used by the other end of a connected socket. It is
+ * harmless to call this method redundantly if the hostname has already been verified.
+ *
+ * <p>Wildcard certificates are allowed to verify any matching hostname, so "foo.bar.example.com"
+ * is verified if the peer has a certificate for "*.example.com".
+ *
+ * @param socket An SSL socket which has been connected to a server
+ * @param hostname The expected hostname of the remote server
+ * @throws IOException if something goes wrong handshaking with the server
+ * @throws SSLPeerUnverifiedException if the server cannot prove its identity
+ */
+ private void verifyHostname(Socket socket, String hostname) throws IOException {
+ // The code at the start of OpenSSLSocketImpl.startHandshake()
+ // ensures that the call is idempotent, so we can safely call it.
+ SSLSocket ssl = (SSLSocket) socket;
+ ssl.startHandshake();
+
+ SSLSession session = ssl.getSession();
+ if (session == null) {
+ mImapHelper.handleEvent(OmtpEvents.DATA_CANNOT_ESTABLISH_SSL_SESSION);
+ throw new SSLException("Cannot verify SSL socket without session");
+ }
+ // TODO: Instead of reporting the name of the server we think we're connecting to,
+ // we should be reporting the bad name in the certificate. Unfortunately this is buried
+ // in the verifier code and is not available in the verifier API, and extracting the
+ // CN & alts is beyond the scope of this patch.
+ if (!HOSTNAME_VERIFIER.verify(hostname, session)) {
+ mImapHelper.handleEvent(OmtpEvents.DATA_SSL_INVALID_HOST_NAME);
+ throw new SSLPeerUnverifiedException(
+ "Certificate hostname not useable for server: " + session.getPeerPrincipal());
+ }
+ }
+
+ public boolean isOpen() {
+ return (mIn != null
+ && mOut != null
+ && mSocket != null
+ && mSocket.isConnected()
+ && !mSocket.isClosed());
+ }
+
+ /** Close the connection. MUST NOT return any exceptions - must be "best effort" and safe. */
+ public void close() {
+ try {
+ mIn.close();
+ } catch (Exception e) {
+ // May fail if the connection is already closed.
+ }
+ try {
+ mOut.close();
+ } catch (Exception e) {
+ // May fail if the connection is already closed.
+ }
+ try {
+ mSocket.close();
+ } catch (Exception e) {
+ // May fail if the connection is already closed.
+ }
+ mIn = null;
+ mOut = null;
+ mSocket = null;
+ }
+
+ public String getHost() {
+ return mHost;
+ }
+
+ public InputStream getInputStream() {
+ return mIn;
+ }
+
+ public OutputStream getOutputStream() {
+ return mOut;
+ }
+
+ /** Writes a single line to the server using \r\n termination. */
+ public void writeLine(String s, String sensitiveReplacement) throws IOException {
+ if (sensitiveReplacement != null) {
+ LogUtils.d(TAG, ">>> " + sensitiveReplacement);
+ } else {
+ LogUtils.d(TAG, ">>> " + s);
+ }
+
+ OutputStream out = getOutputStream();
+ out.write(s.getBytes());
+ out.write('\r');
+ out.write('\n');
+ out.flush();
+ }
+
+ /**
+ * Reads a single line from the server, using either \r\n or \n as the delimiter. The delimiter
+ * char(s) are not included in the result.
+ */
+ public String readLine(boolean loggable) throws IOException {
+ StringBuffer sb = new StringBuffer();
+ InputStream in = getInputStream();
+ int d;
+ while ((d = in.read()) != -1) {
+ if (((char) d) == '\r') {
+ continue;
+ } else if (((char) d) == '\n') {
+ break;
+ } else {
+ sb.append((char) d);
+ }
+ }
+ if (d == -1) {
+ LogUtils.d(TAG, "End of stream reached while trying to read line.");
+ }
+ String ret = sb.toString();
+ if (loggable) {
+ LogUtils.d(TAG, "<<< " + ret);
+ }
+ return ret;
+ }
+}
diff --git a/java/com/android/voicemailomtp/mail/MeetingInfo.java b/java/com/android/voicemail/impl/mail/MeetingInfo.java
index 0505bbf2c..9fe953d5d 100644
--- a/java/com/android/voicemailomtp/mail/MeetingInfo.java
+++ b/java/com/android/voicemail/impl/mail/MeetingInfo.java
@@ -13,17 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.voicemailomtp.mail;
+package com.android.voicemail.impl.mail;
public class MeetingInfo {
- // Predefined tags; others can be added
- public static final String MEETING_DTSTAMP = "DTSTAMP";
- public static final String MEETING_UID = "UID";
- public static final String MEETING_ORGANIZER_EMAIL = "ORGMAIL";
- public static final String MEETING_DTSTART = "DTSTART";
- public static final String MEETING_DTEND = "DTEND";
- public static final String MEETING_TITLE = "TITLE";
- public static final String MEETING_LOCATION = "LOC";
- public static final String MEETING_RESPONSE_REQUESTED = "RESPONSE";
- public static final String MEETING_ALL_DAY = "ALLDAY";
+ // Predefined tags; others can be added
+ public static final String MEETING_DTSTAMP = "DTSTAMP";
+ public static final String MEETING_UID = "UID";
+ public static final String MEETING_ORGANIZER_EMAIL = "ORGMAIL";
+ public static final String MEETING_DTSTART = "DTSTART";
+ public static final String MEETING_DTEND = "DTEND";
+ public static final String MEETING_TITLE = "TITLE";
+ public static final String MEETING_LOCATION = "LOC";
+ public static final String MEETING_RESPONSE_REQUESTED = "RESPONSE";
+ public static final String MEETING_ALL_DAY = "ALLDAY";
}
diff --git a/java/com/android/voicemail/impl/mail/Message.java b/java/com/android/voicemail/impl/mail/Message.java
new file mode 100644
index 000000000..aea5d3ead
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/Message.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail;
+
+import android.support.annotation.VisibleForTesting;
+import java.util.Date;
+import java.util.HashSet;
+
+public abstract class Message implements Part, Body {
+ public static final Message[] EMPTY_ARRAY = new Message[0];
+
+ public static final String RECIPIENT_TYPE_TO = "to";
+ public static final String RECIPIENT_TYPE_CC = "cc";
+ public static final String RECIPIENT_TYPE_BCC = "bcc";
+
+ public enum RecipientType {
+ TO,
+ CC,
+ BCC,
+ }
+
+ protected String mUid;
+
+ private HashSet<String> mFlags = null;
+
+ protected Date mInternalDate;
+
+ public String getUid() {
+ return mUid;
+ }
+
+ public void setUid(String uid) {
+ this.mUid = uid;
+ }
+
+ public abstract String getSubject() throws MessagingException;
+
+ public abstract void setSubject(String subject) throws MessagingException;
+
+ public Date getInternalDate() {
+ return mInternalDate;
+ }
+
+ public void setInternalDate(Date internalDate) {
+ this.mInternalDate = internalDate;
+ }
+
+ public abstract Date getReceivedDate() throws MessagingException;
+
+ public abstract Date getSentDate() throws MessagingException;
+
+ public abstract void setSentDate(Date sentDate) throws MessagingException;
+
+ public abstract Address[] getRecipients(String type) throws MessagingException;
+
+ public abstract void setRecipients(String type, Address[] addresses) throws MessagingException;
+
+ public void setRecipient(String type, Address address) throws MessagingException {
+ setRecipients(type, new Address[] {address});
+ }
+
+ public abstract Address[] getFrom() throws MessagingException;
+
+ public abstract void setFrom(Address from) throws MessagingException;
+
+ public abstract Address[] getReplyTo() throws MessagingException;
+
+ public abstract void setReplyTo(Address[] from) throws MessagingException;
+
+ // Always use these instead of getHeader("Message-ID") or setHeader("Message-ID");
+ public abstract void setMessageId(String messageId) throws MessagingException;
+
+ public abstract String getMessageId() throws MessagingException;
+
+ @Override
+ public boolean isMimeType(String mimeType) throws MessagingException {
+ return getContentType().startsWith(mimeType);
+ }
+
+ private HashSet<String> getFlagSet() {
+ if (mFlags == null) {
+ mFlags = new HashSet<String>();
+ }
+ return mFlags;
+ }
+
+ /*
+ * TODO Refactor Flags at some point to be able to store user defined flags.
+ */
+ public String[] getFlags() {
+ return getFlagSet().toArray(new String[] {});
+ }
+
+ /**
+ * Set/clear a flag directly, without involving overrides of {@link #setFlag} in subclasses. Only
+ * used for testing.
+ */
+ @VisibleForTesting
+ private final void setFlagDirectlyForTest(String flag, boolean set) throws MessagingException {
+ if (set) {
+ getFlagSet().add(flag);
+ } else {
+ getFlagSet().remove(flag);
+ }
+ }
+
+ public void setFlag(String flag, boolean set) throws MessagingException {
+ setFlagDirectlyForTest(flag, set);
+ }
+
+ /**
+ * This method calls setFlag(String, boolean)
+ *
+ * @param flags
+ * @param set
+ */
+ public void setFlags(String[] flags, boolean set) throws MessagingException {
+ for (String flag : flags) {
+ setFlag(flag, set);
+ }
+ }
+
+ public boolean isSet(String flag) {
+ return getFlagSet().contains(flag);
+ }
+
+ public abstract void saveChanges() throws MessagingException;
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + ':' + mUid;
+ }
+}
diff --git a/java/com/android/voicemailomtp/mail/MessageDateComparator.java b/java/com/android/voicemail/impl/mail/MessageDateComparator.java
index 37071034a..89231f6c2 100644
--- a/java/com/android/voicemailomtp/mail/MessageDateComparator.java
+++ b/java/com/android/voicemail/impl/mail/MessageDateComparator.java
@@ -13,22 +13,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.voicemailomtp.mail;
+package com.android.voicemail.impl.mail;
import java.util.Comparator;
public class MessageDateComparator implements Comparator<Message> {
- @Override
- public int compare(Message o1, Message o2) {
- try {
- if (o1.getSentDate() == null) {
- return 1;
- } else if (o2.getSentDate() == null) {
- return -1;
- } else
- return o2.getSentDate().compareTo(o1.getSentDate());
- } catch (Exception e) {
- return 0;
- }
+ @Override
+ public int compare(Message o1, Message o2) {
+ try {
+ if (o1.getSentDate() == null) {
+ return 1;
+ } else if (o2.getSentDate() == null) {
+ return -1;
+ } else {
+ return o2.getSentDate().compareTo(o1.getSentDate());
+ }
+ } catch (Exception e) {
+ return 0;
}
+ }
}
diff --git a/java/com/android/voicemail/impl/mail/MessagingException.java b/java/com/android/voicemail/impl/mail/MessagingException.java
new file mode 100644
index 000000000..c1e3051df
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/MessagingException.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl.mail;
+
+/**
+ * This exception is used for most types of failures that occur during server interactions.
+ *
+ * <p>Data passed through this exception should be considered non-localized. Any strings should
+ * either be internal-only (for debugging) or server-generated.
+ *
+ * <p>TO DO: Does it make sense to further collapse AuthenticationFailedException and
+ * CertificateValidationException and any others into this?
+ */
+public class MessagingException extends Exception {
+ public static final long serialVersionUID = -1;
+
+ public static final int NO_ERROR = -1;
+ /** Any exception that does not specify a specific issue */
+ public static final int UNSPECIFIED_EXCEPTION = 0;
+ /** Connection or IO errors */
+ public static final int IOERROR = 1;
+ /** The configuration requested TLS but the server did not support it. */
+ public static final int TLS_REQUIRED = 2;
+ /** Authentication is required but the server did not support it. */
+ public static final int AUTH_REQUIRED = 3;
+ /** General security failures */
+ public static final int GENERAL_SECURITY = 4;
+ /** Authentication failed */
+ public static final int AUTHENTICATION_FAILED = 5;
+ /** Attempt to create duplicate account */
+ public static final int DUPLICATE_ACCOUNT = 6;
+ /** Required security policies reported - advisory only */
+ public static final int SECURITY_POLICIES_REQUIRED = 7;
+ /** Required security policies not supported */
+ public static final int SECURITY_POLICIES_UNSUPPORTED = 8;
+ /** The protocol (or protocol version) isn't supported */
+ public static final int PROTOCOL_VERSION_UNSUPPORTED = 9;
+ /** The server's SSL certificate couldn't be validated */
+ public static final int CERTIFICATE_VALIDATION_ERROR = 10;
+ /** Authentication failed during autodiscover */
+ public static final int AUTODISCOVER_AUTHENTICATION_FAILED = 11;
+ /** Autodiscover completed with a result (non-error) */
+ public static final int AUTODISCOVER_AUTHENTICATION_RESULT = 12;
+ /** Ambiguous failure; server error or bad credentials */
+ public static final int AUTHENTICATION_FAILED_OR_SERVER_ERROR = 13;
+ /** The server refused access */
+ public static final int ACCESS_DENIED = 14;
+ /** The server refused access */
+ public static final int ATTACHMENT_NOT_FOUND = 15;
+ /** A client SSL certificate is required for connections to the server */
+ public static final int CLIENT_CERTIFICATE_REQUIRED = 16;
+ /** The client SSL certificate specified is invalid */
+ public static final int CLIENT_CERTIFICATE_ERROR = 17;
+ /** The server indicates it does not support OAuth authentication */
+ public static final int OAUTH_NOT_SUPPORTED = 18;
+ /** The server indicates it experienced an internal error */
+ public static final int SERVER_ERROR = 19;
+
+ protected int mExceptionType;
+ // Exception type-specific data
+ protected Object mExceptionData;
+
+ public MessagingException(String message, Throwable throwable) {
+ this(UNSPECIFIED_EXCEPTION, message, throwable);
+ }
+
+ public MessagingException(int exceptionType, String message, Throwable throwable) {
+ super(message, throwable);
+ mExceptionType = exceptionType;
+ mExceptionData = null;
+ }
+
+ /**
+ * Constructs a MessagingException with an exceptionType and a null message.
+ *
+ * @param exceptionType The exception type to set for this exception.
+ */
+ public MessagingException(int exceptionType) {
+ this(exceptionType, null, null);
+ }
+
+ /**
+ * Constructs a MessagingException with a message.
+ *
+ * @param message the message for this exception
+ */
+ public MessagingException(String message) {
+ this(UNSPECIFIED_EXCEPTION, message, null);
+ }
+
+ /**
+ * Constructs a MessagingException with an exceptionType and a message.
+ *
+ * @param exceptionType The exception type to set for this exception.
+ */
+ public MessagingException(int exceptionType, String message) {
+ this(exceptionType, message, null);
+ }
+
+ /**
+ * Constructs a MessagingException with an exceptionType, a message, and data
+ *
+ * @param exceptionType The exception type to set for this exception.
+ * @param message the message for the exception (or null)
+ * @param data exception-type specific data for the exception (or null)
+ */
+ public MessagingException(int exceptionType, String message, Object data) {
+ super(message);
+ mExceptionType = exceptionType;
+ mExceptionData = data;
+ }
+
+ /**
+ * Return the exception type. Will be OTHER_EXCEPTION if not explicitly set.
+ *
+ * @return Returns the exception type.
+ */
+ public int getExceptionType() {
+ return mExceptionType;
+ }
+ /**
+ * Return the exception data. Will be null if not explicitly set.
+ *
+ * @return Returns the exception data.
+ */
+ public Object getExceptionData() {
+ return mExceptionData;
+ }
+}
diff --git a/java/com/android/voicemail/impl/mail/Multipart.java b/java/com/android/voicemail/impl/mail/Multipart.java
new file mode 100644
index 000000000..e8d5046d5
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/Multipart.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail;
+
+import java.util.ArrayList;
+
+public abstract class Multipart implements Body {
+ protected Part mParent;
+
+ protected ArrayList<BodyPart> mParts = new ArrayList<BodyPart>();
+
+ protected String mContentType;
+
+ public void addBodyPart(BodyPart part) throws MessagingException {
+ mParts.add(part);
+ }
+
+ public void addBodyPart(BodyPart part, int index) throws MessagingException {
+ mParts.add(index, part);
+ }
+
+ public BodyPart getBodyPart(int index) throws MessagingException {
+ return mParts.get(index);
+ }
+
+ public String getContentType() throws MessagingException {
+ return mContentType;
+ }
+
+ public int getCount() throws MessagingException {
+ return mParts.size();
+ }
+
+ public boolean removeBodyPart(BodyPart part) throws MessagingException {
+ return mParts.remove(part);
+ }
+
+ public void removeBodyPart(int index) throws MessagingException {
+ mParts.remove(index);
+ }
+
+ public Part getParent() throws MessagingException {
+ return mParent;
+ }
+
+ public void setParent(Part parent) throws MessagingException {
+ this.mParent = parent;
+ }
+}
diff --git a/java/com/android/voicemail/impl/mail/PackedString.java b/java/com/android/voicemail/impl/mail/PackedString.java
new file mode 100644
index 000000000..701dab62b
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/PackedString.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail;
+
+import android.util.ArrayMap;
+import java.util.Map;
+
+/**
+ * A utility class for creating and modifying Strings that are tagged and packed together.
+ *
+ * <p>Uses non-printable (control chars) for internal delimiters; Intended for regular displayable
+ * strings only, so please use base64 or other encoding if you need to hide any binary data here.
+ *
+ * <p>Binary compatible with Address.pack() format, which should migrate to use this code.
+ */
+public class PackedString {
+
+ /**
+ * Packing format is: element : [ value ] or [ value TAG-DELIMITER tag ] packed-string : [ element
+ * ] [ ELEMENT-DELIMITER [ element ] ]*
+ */
+ private static final char DELIMITER_ELEMENT = '\1';
+
+ private static final char DELIMITER_TAG = '\2';
+
+ private String mString;
+ private ArrayMap<String, String> mExploded;
+ private static final ArrayMap<String, String> EMPTY_MAP = new ArrayMap<String, String>();
+
+ /**
+ * Create a packed string using an already-packed string (e.g. from database)
+ *
+ * @param string packed string
+ */
+ public PackedString(String string) {
+ mString = string;
+ mExploded = null;
+ }
+
+ /**
+ * Get the value referred to by a given tag. If the tag does not exist, return null.
+ *
+ * @param tag identifier of string of interest
+ * @return returns value, or null if no string is found
+ */
+ public String get(String tag) {
+ if (mExploded == null) {
+ mExploded = explode(mString);
+ }
+ return mExploded.get(tag);
+ }
+
+ /**
+ * Return a map of all of the values referred to by a given tag. This is a shallow copy, don't
+ * edit the values.
+ *
+ * @return a map of the values in the packed string
+ */
+ public Map<String, String> unpack() {
+ if (mExploded == null) {
+ mExploded = explode(mString);
+ }
+ return new ArrayMap<String, String>(mExploded);
+ }
+
+ /** Read out all values into a map. */
+ private static ArrayMap<String, String> explode(String packed) {
+ if (packed == null || packed.length() == 0) {
+ return EMPTY_MAP;
+ }
+ ArrayMap<String, String> map = new ArrayMap<String, String>();
+
+ int length = packed.length();
+ int elementStartIndex = 0;
+ int elementEndIndex = 0;
+ int tagEndIndex = packed.indexOf(DELIMITER_TAG);
+
+ while (elementStartIndex < length) {
+ elementEndIndex = packed.indexOf(DELIMITER_ELEMENT, elementStartIndex);
+ if (elementEndIndex == -1) {
+ elementEndIndex = length;
+ }
+ String tag;
+ String value;
+ if (tagEndIndex == -1 || elementEndIndex <= tagEndIndex) {
+ // in this case the DELIMITER_PERSONAL is in a future pair (or not found)
+ // so synthesize a positional tag for the value, and don't update tagEndIndex
+ value = packed.substring(elementStartIndex, elementEndIndex);
+ tag = Integer.toString(map.size());
+ } else {
+ value = packed.substring(elementStartIndex, tagEndIndex);
+ tag = packed.substring(tagEndIndex + 1, elementEndIndex);
+ // scan forward for next tag, if any
+ tagEndIndex = packed.indexOf(DELIMITER_TAG, elementEndIndex + 1);
+ }
+ map.put(tag, value);
+ elementStartIndex = elementEndIndex + 1;
+ }
+
+ return map;
+ }
+
+ /**
+ * Builder class for creating PackedString values. Can also be used for editing existing
+ * PackedString representations.
+ */
+ public static class Builder {
+ ArrayMap<String, String> mMap;
+
+ /** Create a builder that's empty (for filling) */
+ public Builder() {
+ mMap = new ArrayMap<String, String>();
+ }
+
+ /** Create a builder using the values of an existing PackedString (for editing). */
+ public Builder(String packed) {
+ mMap = explode(packed);
+ }
+
+ /**
+ * Add a tagged value
+ *
+ * @param tag identifier of string of interest
+ * @param value the value to record in this position. null to delete entry.
+ */
+ public void put(String tag, String value) {
+ if (value == null) {
+ mMap.remove(tag);
+ } else {
+ mMap.put(tag, value);
+ }
+ }
+
+ /**
+ * Get the value referred to by a given tag. If the tag does not exist, return null.
+ *
+ * @param tag identifier of string of interest
+ * @return returns value, or null if no string is found
+ */
+ public String get(String tag) {
+ return mMap.get(tag);
+ }
+
+ /** Pack the values and return a single, encoded string */
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ for (Map.Entry<String, String> entry : mMap.entrySet()) {
+ if (sb.length() > 0) {
+ sb.append(DELIMITER_ELEMENT);
+ }
+ sb.append(entry.getValue());
+ sb.append(DELIMITER_TAG);
+ sb.append(entry.getKey());
+ }
+ return sb.toString();
+ }
+ }
+}
diff --git a/java/com/android/voicemail/impl/mail/Part.java b/java/com/android/voicemail/impl/mail/Part.java
new file mode 100644
index 000000000..3be5c57b9
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/Part.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+public interface Part extends Fetchable {
+ public void addHeader(String name, String value) throws MessagingException;
+
+ public void removeHeader(String name) throws MessagingException;
+
+ public void setHeader(String name, String value) throws MessagingException;
+
+ public Body getBody() throws MessagingException;
+
+ public String getContentType() throws MessagingException;
+
+ public String getDisposition() throws MessagingException;
+
+ public String getContentId() throws MessagingException;
+
+ public String[] getHeader(String name) throws MessagingException;
+
+ public void setExtendedHeader(String name, String value) throws MessagingException;
+
+ public String getExtendedHeader(String name) throws MessagingException;
+
+ public int getSize() throws MessagingException;
+
+ public boolean isMimeType(String mimeType) throws MessagingException;
+
+ public String getMimeType() throws MessagingException;
+
+ public void setBody(Body body) throws MessagingException;
+
+ public void writeTo(OutputStream out) throws IOException, MessagingException;
+}
diff --git a/java/com/android/voicemail/impl/mail/PeekableInputStream.java b/java/com/android/voicemail/impl/mail/PeekableInputStream.java
new file mode 100644
index 000000000..08f867f82
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/PeekableInputStream.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl.mail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A filtering InputStream that allows single byte "peeks" without consuming the byte. The client of
+ * this stream can call peek() to see the next available byte in the stream and a subsequent read
+ * will still return the peeked byte.
+ */
+public class PeekableInputStream extends InputStream {
+ private final InputStream mIn;
+ private boolean mPeeked;
+ private int mPeekedByte;
+
+ public PeekableInputStream(InputStream in) {
+ this.mIn = in;
+ }
+
+ @Override
+ public int read() throws IOException {
+ if (!mPeeked) {
+ return mIn.read();
+ } else {
+ mPeeked = false;
+ return mPeekedByte;
+ }
+ }
+
+ public int peek() throws IOException {
+ if (!mPeeked) {
+ mPeekedByte = read();
+ mPeeked = true;
+ }
+ return mPeekedByte;
+ }
+
+ @Override
+ public int read(byte[] b, int offset, int length) throws IOException {
+ if (!mPeeked) {
+ return mIn.read(b, offset, length);
+ } else {
+ b[0] = (byte) mPeekedByte;
+ mPeeked = false;
+ int r = mIn.read(b, offset + 1, length - 1);
+ if (r == -1) {
+ return 1;
+ } else {
+ return r + 1;
+ }
+ }
+ }
+
+ @Override
+ public int read(byte[] b) throws IOException {
+ return read(b, 0, b.length);
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "PeekableInputStream(in=%s, peeked=%b, peekedByte=%d)",
+ mIn.toString(), mPeeked, mPeekedByte);
+ }
+}
diff --git a/java/com/android/voicemailomtp/mail/TempDirectory.java b/java/com/android/voicemail/impl/mail/TempDirectory.java
index dfae36026..42adbeb1f 100644
--- a/java/com/android/voicemailomtp/mail/TempDirectory.java
+++ b/java/com/android/voicemail/impl/mail/TempDirectory.java
@@ -13,29 +13,28 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.voicemailomtp.mail;
+package com.android.voicemail.impl.mail;
import android.content.Context;
-
import java.io.File;
/**
- * TempDirectory caches the directory used for caching file. It is set up during application
+ * TempDirectory caches the directory used for caching file. It is set up during application
* initialization.
*/
public class TempDirectory {
- private static File sTempDirectory = null;
+ private static File sTempDirectory = null;
- public static void setTempDirectory(Context context) {
- sTempDirectory = context.getCacheDir();
- }
+ public static void setTempDirectory(Context context) {
+ sTempDirectory = context.getCacheDir();
+ }
- public static File getTempDirectory() {
- if (sTempDirectory == null) {
- throw new RuntimeException(
- "TempDirectory not set. " +
- "If in a unit test, call Email.setTempDirectory(context) in setUp().");
- }
- return sTempDirectory;
+ public static File getTempDirectory() {
+ if (sTempDirectory == null) {
+ throw new RuntimeException(
+ "TempDirectory not set. "
+ + "If in a unit test, call Email.setTempDirectory(context) in setUp().");
}
-} \ No newline at end of file
+ return sTempDirectory;
+ }
+}
diff --git a/java/com/android/voicemail/impl/mail/internet/BinaryTempFileBody.java b/java/com/android/voicemail/impl/mail/internet/BinaryTempFileBody.java
new file mode 100644
index 000000000..753b70f23
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/internet/BinaryTempFileBody.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail.internet;
+
+import android.util.Base64;
+import android.util.Base64OutputStream;
+import com.android.voicemail.impl.mail.Body;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.TempDirectory;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import org.apache.commons.io.IOUtils;
+
+/**
+ * A Body that is backed by a temp file. The Body exposes a getOutputStream method that allows the
+ * user to write to the temp file. After the write the body is available via getInputStream and
+ * writeTo one time. After writeTo is called, or the InputStream returned from getInputStream is
+ * closed the file is deleted and the Body should be considered disposed of.
+ */
+public class BinaryTempFileBody implements Body {
+ private File mFile;
+
+ /**
+ * An alternate way to put data into a BinaryTempFileBody is to simply supply an already- created
+ * file. Note that this file will be deleted after it is read.
+ *
+ * @param filePath The file containing the data to be stored on disk temporarily
+ */
+ public void setFile(String filePath) {
+ mFile = new File(filePath);
+ }
+
+ public OutputStream getOutputStream() throws IOException {
+ mFile = File.createTempFile("body", null, TempDirectory.getTempDirectory());
+ mFile.deleteOnExit();
+ return new FileOutputStream(mFile);
+ }
+
+ @Override
+ public InputStream getInputStream() throws MessagingException {
+ try {
+ return new BinaryTempFileBodyInputStream(new FileInputStream(mFile));
+ } catch (IOException ioe) {
+ throw new MessagingException("Unable to open body", ioe);
+ }
+ }
+
+ @Override
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ InputStream in = getInputStream();
+ Base64OutputStream base64Out = new Base64OutputStream(out, Base64.CRLF | Base64.NO_CLOSE);
+ IOUtils.copy(in, base64Out);
+ base64Out.close();
+ mFile.delete();
+ in.close();
+ }
+
+ class BinaryTempFileBodyInputStream extends FilterInputStream {
+ public BinaryTempFileBodyInputStream(InputStream in) {
+ super(in);
+ }
+
+ @Override
+ public void close() throws IOException {
+ super.close();
+ mFile.delete();
+ }
+ }
+}
diff --git a/java/com/android/voicemail/impl/mail/internet/MimeBodyPart.java b/java/com/android/voicemail/impl/mail/internet/MimeBodyPart.java
new file mode 100644
index 000000000..2add76c72
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/internet/MimeBodyPart.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail.internet;
+
+import com.android.voicemail.impl.mail.Body;
+import com.android.voicemail.impl.mail.BodyPart;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.Multipart;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.util.regex.Pattern;
+
+/** TODO this is a close approximation of Message, need to update along with Message. */
+public class MimeBodyPart extends BodyPart {
+ protected MimeHeader mHeader = new MimeHeader();
+ protected MimeHeader mExtendedHeader;
+ protected Body mBody;
+ protected int mSize;
+
+ // regex that matches content id surrounded by "<>" optionally.
+ private static final Pattern REMOVE_OPTIONAL_BRACKETS = Pattern.compile("^<?([^>]+)>?$");
+ // regex that matches end of line.
+ private static final Pattern END_OF_LINE = Pattern.compile("\r?\n");
+
+ public MimeBodyPart() throws MessagingException {
+ this(null);
+ }
+
+ public MimeBodyPart(Body body) throws MessagingException {
+ this(body, null);
+ }
+
+ public MimeBodyPart(Body body, String mimeType) throws MessagingException {
+ if (mimeType != null) {
+ setHeader(MimeHeader.HEADER_CONTENT_TYPE, mimeType);
+ }
+ setBody(body);
+ }
+
+ protected String getFirstHeader(String name) throws MessagingException {
+ return mHeader.getFirstHeader(name);
+ }
+
+ @Override
+ public void addHeader(String name, String value) throws MessagingException {
+ mHeader.addHeader(name, value);
+ }
+
+ @Override
+ public void setHeader(String name, String value) throws MessagingException {
+ mHeader.setHeader(name, value);
+ }
+
+ @Override
+ public String[] getHeader(String name) throws MessagingException {
+ return mHeader.getHeader(name);
+ }
+
+ @Override
+ public void removeHeader(String name) throws MessagingException {
+ mHeader.removeHeader(name);
+ }
+
+ @Override
+ public Body getBody() throws MessagingException {
+ return mBody;
+ }
+
+ @Override
+ public void setBody(Body body) throws MessagingException {
+ this.mBody = body;
+ if (body instanceof Multipart) {
+ Multipart multipart =
+ ((Multipart) body);
+ multipart.setParent(this);
+ setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType());
+ } else if (body instanceof TextBody) {
+ String contentType = String.format("%s;\n charset=utf-8", getMimeType());
+ String name = MimeUtility.getHeaderParameter(getContentType(), "name");
+ if (name != null) {
+ contentType += String.format(";\n name=\"%s\"", name);
+ }
+ setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType);
+ setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
+ }
+ }
+
+ @Override
+ public String getContentType() throws MessagingException {
+ String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
+ if (contentType == null) {
+ return "text/plain";
+ } else {
+ return contentType;
+ }
+ }
+
+ @Override
+ public String getDisposition() throws MessagingException {
+ String contentDisposition = getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
+ if (contentDisposition == null) {
+ return null;
+ } else {
+ return contentDisposition;
+ }
+ }
+
+ @Override
+ public String getContentId() throws MessagingException {
+ String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID);
+ if (contentId == null) {
+ return null;
+ } else {
+ // remove optionally surrounding brackets.
+ return REMOVE_OPTIONAL_BRACKETS.matcher(contentId).replaceAll("$1");
+ }
+ }
+
+ @Override
+ public String getMimeType() throws MessagingException {
+ return MimeUtility.getHeaderParameter(getContentType(), null);
+ }
+
+ @Override
+ public boolean isMimeType(String mimeType) throws MessagingException {
+ return getMimeType().equals(mimeType);
+ }
+
+ public void setSize(int size) {
+ this.mSize = size;
+ }
+
+ @Override
+ public int getSize() throws MessagingException {
+ return mSize;
+ }
+
+ /**
+ * Set extended header
+ *
+ * @param name Extended header name
+ * @param value header value - flattened by removing CR-NL if any remove header if value is null
+ * @throws MessagingException
+ */
+ @Override
+ public void setExtendedHeader(String name, String value) throws MessagingException {
+ if (value == null) {
+ if (mExtendedHeader != null) {
+ mExtendedHeader.removeHeader(name);
+ }
+ return;
+ }
+ if (mExtendedHeader == null) {
+ mExtendedHeader = new MimeHeader();
+ }
+ mExtendedHeader.setHeader(name, END_OF_LINE.matcher(value).replaceAll(""));
+ }
+
+ /**
+ * Get extended header
+ *
+ * @param name Extended header name
+ * @return header value - null if header does not exist
+ * @throws MessagingException
+ */
+ @Override
+ public String getExtendedHeader(String name) throws MessagingException {
+ if (mExtendedHeader == null) {
+ return null;
+ }
+ return mExtendedHeader.getFirstHeader(name);
+ }
+
+ /** Write the MimeMessage out in MIME format. */
+ @Override
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
+ mHeader.writeTo(out);
+ writer.write("\r\n");
+ writer.flush();
+ if (mBody != null) {
+ mBody.writeTo(out);
+ }
+ }
+}
diff --git a/java/com/android/voicemail/impl/mail/internet/MimeHeader.java b/java/com/android/voicemail/impl/mail/internet/MimeHeader.java
new file mode 100644
index 000000000..d41cdb3e4
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/internet/MimeHeader.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail.internet;
+
+import com.android.voicemail.impl.mail.MessagingException;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.util.ArrayList;
+
+public class MimeHeader {
+ /**
+ * Application specific header that contains Store specific information about an attachment. In
+ * IMAP this contains the IMAP BODYSTRUCTURE part id so that the ImapStore can later retrieve the
+ * attachment at will from the server. The info is recorded from this header on
+ * LocalStore.appendMessage and is put back into the MIME data by LocalStore.fetch.
+ */
+ public static final String HEADER_ANDROID_ATTACHMENT_STORE_DATA =
+ "X-Android-Attachment-StoreData";
+
+ public static final String HEADER_CONTENT_TYPE = "Content-Type";
+ public static final String HEADER_CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding";
+ public static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition";
+ public static final String HEADER_CONTENT_ID = "Content-ID";
+
+ /** Fields that should be omitted when writing the header using writeTo() */
+ private static final String[] WRITE_OMIT_FIELDS = {
+ // HEADER_ANDROID_ATTACHMENT_DOWNLOADED,
+ // HEADER_ANDROID_ATTACHMENT_ID,
+ HEADER_ANDROID_ATTACHMENT_STORE_DATA
+ };
+
+ protected final ArrayList<Field> mFields = new ArrayList<Field>();
+
+ public void clear() {
+ mFields.clear();
+ }
+
+ public String getFirstHeader(String name) throws MessagingException {
+ String[] header = getHeader(name);
+ if (header == null) {
+ return null;
+ }
+ return header[0];
+ }
+
+ public void addHeader(String name, String value) throws MessagingException {
+ mFields.add(new Field(name, value));
+ }
+
+ public void setHeader(String name, String value) throws MessagingException {
+ if (name == null || value == null) {
+ return;
+ }
+ removeHeader(name);
+ addHeader(name, value);
+ }
+
+ public String[] getHeader(String name) throws MessagingException {
+ ArrayList<String> values = new ArrayList<String>();
+ for (Field field : mFields) {
+ if (field.name.equalsIgnoreCase(name)) {
+ values.add(field.value);
+ }
+ }
+ if (values.size() == 0) {
+ return null;
+ }
+ return values.toArray(new String[] {});
+ }
+
+ public void removeHeader(String name) throws MessagingException {
+ ArrayList<Field> removeFields = new ArrayList<Field>();
+ for (Field field : mFields) {
+ if (field.name.equalsIgnoreCase(name)) {
+ removeFields.add(field);
+ }
+ }
+ mFields.removeAll(removeFields);
+ }
+
+ /**
+ * Write header into String
+ *
+ * @return CR-NL separated header string except the headers in writeOmitFields null if header is
+ * empty
+ */
+ public String writeToString() {
+ if (mFields.size() == 0) {
+ return null;
+ }
+ StringBuilder builder = new StringBuilder();
+ for (Field field : mFields) {
+ if (!arrayContains(WRITE_OMIT_FIELDS, field.name)) {
+ builder.append(field.name + ": " + field.value + "\r\n");
+ }
+ }
+ return builder.toString();
+ }
+
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
+ for (Field field : mFields) {
+ if (!arrayContains(WRITE_OMIT_FIELDS, field.name)) {
+ writer.write(field.name + ": " + field.value + "\r\n");
+ }
+ }
+ writer.flush();
+ }
+
+ private static class Field {
+ final String name;
+ final String value;
+
+ public Field(String name, String value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ @Override
+ public String toString() {
+ return name + "=" + value;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return (mFields == null) ? null : mFields.toString();
+ }
+
+ public static final boolean arrayContains(Object[] a, Object o) {
+ int index = arrayIndex(a, o);
+ return (index >= 0);
+ }
+
+ public static final int arrayIndex(Object[] a, Object o) {
+ for (int i = 0, count = a.length; i < count; i++) {
+ if (a[i].equals(o)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+}
diff --git a/java/com/android/voicemail/impl/mail/internet/MimeMessage.java b/java/com/android/voicemail/impl/mail/internet/MimeMessage.java
new file mode 100644
index 000000000..dfb7d7c25
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/internet/MimeMessage.java
@@ -0,0 +1,676 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail.internet;
+
+import android.text.TextUtils;
+import com.android.voicemail.impl.mail.Address;
+import com.android.voicemail.impl.mail.Body;
+import com.android.voicemail.impl.mail.BodyPart;
+import com.android.voicemail.impl.mail.Message;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.Multipart;
+import com.android.voicemail.impl.mail.Part;
+import com.android.voicemail.impl.mail.utils.LogUtils;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.Stack;
+import java.util.regex.Pattern;
+import org.apache.james.mime4j.BodyDescriptor;
+import org.apache.james.mime4j.ContentHandler;
+import org.apache.james.mime4j.EOLConvertingInputStream;
+import org.apache.james.mime4j.MimeStreamParser;
+import org.apache.james.mime4j.field.DateTimeField;
+import org.apache.james.mime4j.field.Field;
+
+/**
+ * An implementation of Message that stores all of its metadata in RFC 822 and RFC 2045 style
+ * headers.
+ *
+ * <p>NOTE: Automatic generation of a local message-id is becoming unwieldy and should be removed.
+ * It would be better to simply do it explicitly on local creation of new outgoing messages.
+ */
+public class MimeMessage extends Message {
+ private MimeHeader mHeader;
+ private MimeHeader mExtendedHeader;
+
+ // NOTE: The fields here are transcribed out of headers, and values stored here will supersede
+ // the values found in the headers. Use caution to prevent any out-of-phase errors. In
+ // particular, any adds/changes/deletes here must be echoed by changes in the parse() function.
+ private Address[] mFrom;
+ private Address[] mTo;
+ private Address[] mCc;
+ private Address[] mBcc;
+ private Address[] mReplyTo;
+ private Date mSentDate;
+ private Body mBody;
+ protected int mSize;
+ private boolean mInhibitLocalMessageId = false;
+ private boolean mComplete = true;
+
+ // Shared random source for generating local message-id values
+ private static final java.util.Random sRandom = new java.util.Random();
+
+ // In MIME, en_US-like date format should be used. In other words "MMM" should be encoded to
+ // "Jan", not the other localized format like "Ene" (meaning January in locale es).
+ // This conversion is used when generating outgoing MIME messages. Incoming MIME date
+ // headers are parsed by org.apache.james.mime4j.field.DateTimeField which does not have any
+ // localization code.
+ private static final SimpleDateFormat DATE_FORMAT =
+ new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
+
+ // regex that matches content id surrounded by "<>" optionally.
+ private static final Pattern REMOVE_OPTIONAL_BRACKETS = Pattern.compile("^<?([^>]+)>?$");
+ // regex that matches end of line.
+ private static final Pattern END_OF_LINE = Pattern.compile("\r?\n");
+
+ public MimeMessage() {
+ mHeader = null;
+ }
+
+ /**
+ * Generate a local message id. This is only used when none has been assigned, and is installed
+ * lazily. Any remote (typically server-assigned) message id takes precedence.
+ *
+ * @return a long, locally-generated message-ID value
+ */
+ private static String generateMessageId() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("<");
+ for (int i = 0; i < 24; i++) {
+ // We'll use a 5-bit range (0..31)
+ final int value = sRandom.nextInt() & 31;
+ final char c = "0123456789abcdefghijklmnopqrstuv".charAt(value);
+ sb.append(c);
+ }
+ sb.append(".");
+ sb.append(Long.toString(System.currentTimeMillis()));
+ sb.append("@email.android.com>");
+ return sb.toString();
+ }
+
+ /**
+ * Parse the given InputStream using Apache Mime4J to build a MimeMessage.
+ *
+ * @param in InputStream providing message content
+ * @throws IOException
+ * @throws MessagingException
+ */
+ public MimeMessage(InputStream in) throws IOException, MessagingException {
+ parse(in);
+ }
+
+ private MimeStreamParser init() {
+ // Before parsing the input stream, clear all local fields that may be superceded by
+ // the new incoming message.
+ getMimeHeaders().clear();
+ mInhibitLocalMessageId = true;
+ mFrom = null;
+ mTo = null;
+ mCc = null;
+ mBcc = null;
+ mReplyTo = null;
+ mSentDate = null;
+ mBody = null;
+
+ final MimeStreamParser parser = new MimeStreamParser();
+ parser.setContentHandler(new MimeMessageBuilder());
+ return parser;
+ }
+
+ protected void parse(InputStream in) throws IOException, MessagingException {
+ final MimeStreamParser parser = init();
+ parser.parse(new EOLConvertingInputStream(in));
+ mComplete = !parser.getPrematureEof();
+ }
+
+ public void parse(InputStream in, EOLConvertingInputStream.Callback callback)
+ throws IOException, MessagingException {
+ final MimeStreamParser parser = init();
+ parser.parse(new EOLConvertingInputStream(in, getSize(), callback));
+ mComplete = !parser.getPrematureEof();
+ }
+
+ /**
+ * Return the internal mHeader value, with very lazy initialization. The goal is to save memory by
+ * not creating the headers until needed.
+ */
+ private MimeHeader getMimeHeaders() {
+ if (mHeader == null) {
+ mHeader = new MimeHeader();
+ }
+ return mHeader;
+ }
+
+ @Override
+ public Date getReceivedDate() throws MessagingException {
+ return null;
+ }
+
+ @Override
+ public Date getSentDate() throws MessagingException {
+ if (mSentDate == null) {
+ try {
+ DateTimeField field =
+ (DateTimeField)
+ Field.parse("Date: " + MimeUtility.unfoldAndDecode(getFirstHeader("Date")));
+ mSentDate = field.getDate();
+ // TODO: We should make it more clear what exceptions can be thrown here,
+ // and whether they reflect a normal or error condition.
+ } catch (Exception e) {
+ LogUtils.v(LogUtils.TAG, "Message missing Date header");
+ }
+ }
+ if (mSentDate == null) {
+ // If we still don't have a date, fall back to "Delivery-date"
+ try {
+ DateTimeField field =
+ (DateTimeField)
+ Field.parse(
+ "Date: " + MimeUtility.unfoldAndDecode(getFirstHeader("Delivery-date")));
+ mSentDate = field.getDate();
+ // TODO: We should make it more clear what exceptions can be thrown here,
+ // and whether they reflect a normal or error condition.
+ } catch (Exception e) {
+ LogUtils.v(LogUtils.TAG, "Message also missing Delivery-Date header");
+ }
+ }
+ return mSentDate;
+ }
+
+ @Override
+ public void setSentDate(Date sentDate) throws MessagingException {
+ setHeader("Date", DATE_FORMAT.format(sentDate));
+ this.mSentDate = sentDate;
+ }
+
+ @Override
+ public String getContentType() throws MessagingException {
+ final String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
+ if (contentType == null) {
+ return "text/plain";
+ } else {
+ return contentType;
+ }
+ }
+
+ @Override
+ public String getDisposition() throws MessagingException {
+ return getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
+ }
+
+ @Override
+ public String getContentId() throws MessagingException {
+ final String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID);
+ if (contentId == null) {
+ return null;
+ } else {
+ // remove optionally surrounding brackets.
+ return REMOVE_OPTIONAL_BRACKETS.matcher(contentId).replaceAll("$1");
+ }
+ }
+
+ public boolean isComplete() {
+ return mComplete;
+ }
+
+ @Override
+ public String getMimeType() throws MessagingException {
+ return MimeUtility.getHeaderParameter(getContentType(), null);
+ }
+
+ @Override
+ public int getSize() throws MessagingException {
+ return mSize;
+ }
+
+ /**
+ * Returns a list of the given recipient type from this message. If no addresses are found the
+ * method returns an empty array.
+ */
+ @Override
+ public Address[] getRecipients(String type) throws MessagingException {
+ if (type == RECIPIENT_TYPE_TO) {
+ if (mTo == null) {
+ mTo = Address.parse(MimeUtility.unfold(getFirstHeader("To")));
+ }
+ return mTo;
+ } else if (type == RECIPIENT_TYPE_CC) {
+ if (mCc == null) {
+ mCc = Address.parse(MimeUtility.unfold(getFirstHeader("CC")));
+ }
+ return mCc;
+ } else if (type == RECIPIENT_TYPE_BCC) {
+ if (mBcc == null) {
+ mBcc = Address.parse(MimeUtility.unfold(getFirstHeader("BCC")));
+ }
+ return mBcc;
+ } else {
+ throw new MessagingException("Unrecognized recipient type.");
+ }
+ }
+
+ @Override
+ public void setRecipients(String type, Address[] addresses) throws MessagingException {
+ final int toLength = 4; // "To: "
+ final int ccLength = 4; // "Cc: "
+ final int bccLength = 5; // "Bcc: "
+ if (type == RECIPIENT_TYPE_TO) {
+ if (addresses == null || addresses.length == 0) {
+ removeHeader("To");
+ this.mTo = null;
+ } else {
+ setHeader("To", MimeUtility.fold(Address.toHeader(addresses), toLength));
+ this.mTo = addresses;
+ }
+ } else if (type == RECIPIENT_TYPE_CC) {
+ if (addresses == null || addresses.length == 0) {
+ removeHeader("CC");
+ this.mCc = null;
+ } else {
+ setHeader("CC", MimeUtility.fold(Address.toHeader(addresses), ccLength));
+ this.mCc = addresses;
+ }
+ } else if (type == RECIPIENT_TYPE_BCC) {
+ if (addresses == null || addresses.length == 0) {
+ removeHeader("BCC");
+ this.mBcc = null;
+ } else {
+ setHeader("BCC", MimeUtility.fold(Address.toHeader(addresses), bccLength));
+ this.mBcc = addresses;
+ }
+ } else {
+ throw new MessagingException("Unrecognized recipient type.");
+ }
+ }
+
+ /** Returns the unfolded, decoded value of the Subject header. */
+ @Override
+ public String getSubject() throws MessagingException {
+ return MimeUtility.unfoldAndDecode(getFirstHeader("Subject"));
+ }
+
+ @Override
+ public void setSubject(String subject) throws MessagingException {
+ final int headerNameLength = 9; // "Subject: "
+ setHeader("Subject", MimeUtility.foldAndEncode2(subject, headerNameLength));
+ }
+
+ @Override
+ public Address[] getFrom() throws MessagingException {
+ if (mFrom == null) {
+ String list = MimeUtility.unfold(getFirstHeader("From"));
+ if (list == null || list.length() == 0) {
+ list = MimeUtility.unfold(getFirstHeader("Sender"));
+ }
+ mFrom = Address.parse(list);
+ }
+ return mFrom;
+ }
+
+ @Override
+ public void setFrom(Address from) throws MessagingException {
+ final int fromLength = 6; // "From: "
+ if (from != null) {
+ setHeader("From", MimeUtility.fold(from.toHeader(), fromLength));
+ this.mFrom = new Address[] {from};
+ } else {
+ this.mFrom = null;
+ }
+ }
+
+ @Override
+ public Address[] getReplyTo() throws MessagingException {
+ if (mReplyTo == null) {
+ mReplyTo = Address.parse(MimeUtility.unfold(getFirstHeader("Reply-to")));
+ }
+ return mReplyTo;
+ }
+
+ @Override
+ public void setReplyTo(Address[] replyTo) throws MessagingException {
+ final int replyToLength = 10; // "Reply-to: "
+ if (replyTo == null || replyTo.length == 0) {
+ removeHeader("Reply-to");
+ mReplyTo = null;
+ } else {
+ setHeader("Reply-to", MimeUtility.fold(Address.toHeader(replyTo), replyToLength));
+ mReplyTo = replyTo;
+ }
+ }
+
+ /**
+ * Set the mime "Message-ID" header
+ *
+ * @param messageId the new Message-ID value
+ * @throws MessagingException
+ */
+ @Override
+ public void setMessageId(String messageId) throws MessagingException {
+ setHeader("Message-ID", messageId);
+ }
+
+ /**
+ * Get the mime "Message-ID" header. This value will be preloaded with a locally-generated random
+ * ID, if the value has not previously been set. Local generation can be inhibited/ overridden by
+ * explicitly clearing the headers, removing the message-id header, etc.
+ *
+ * @return the Message-ID header string, or null if explicitly has been set to null
+ */
+ @Override
+ public String getMessageId() throws MessagingException {
+ String messageId = getFirstHeader("Message-ID");
+ if (messageId == null && !mInhibitLocalMessageId) {
+ messageId = generateMessageId();
+ setMessageId(messageId);
+ }
+ return messageId;
+ }
+
+ @Override
+ public void saveChanges() throws MessagingException {
+ throw new MessagingException("saveChanges not yet implemented");
+ }
+
+ @Override
+ public Body getBody() throws MessagingException {
+ return mBody;
+ }
+
+ @Override
+ public void setBody(Body body) throws MessagingException {
+ this.mBody = body;
+ if (body instanceof Multipart) {
+ final Multipart multipart = ((Multipart) body);
+ multipart.setParent(this);
+ setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType());
+ setHeader("MIME-Version", "1.0");
+ } else if (body instanceof TextBody) {
+ setHeader(
+ MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\n charset=utf-8", getMimeType()));
+ setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
+ }
+ }
+
+ protected String getFirstHeader(String name) throws MessagingException {
+ return getMimeHeaders().getFirstHeader(name);
+ }
+
+ @Override
+ public void addHeader(String name, String value) throws MessagingException {
+ getMimeHeaders().addHeader(name, value);
+ }
+
+ @Override
+ public void setHeader(String name, String value) throws MessagingException {
+ getMimeHeaders().setHeader(name, value);
+ }
+
+ @Override
+ public String[] getHeader(String name) throws MessagingException {
+ return getMimeHeaders().getHeader(name);
+ }
+
+ @Override
+ public void removeHeader(String name) throws MessagingException {
+ getMimeHeaders().removeHeader(name);
+ if ("Message-ID".equalsIgnoreCase(name)) {
+ mInhibitLocalMessageId = true;
+ }
+ }
+
+ /**
+ * Set extended header
+ *
+ * @param name Extended header name
+ * @param value header value - flattened by removing CR-NL if any remove header if value is null
+ * @throws MessagingException
+ */
+ @Override
+ public void setExtendedHeader(String name, String value) throws MessagingException {
+ if (value == null) {
+ if (mExtendedHeader != null) {
+ mExtendedHeader.removeHeader(name);
+ }
+ return;
+ }
+ if (mExtendedHeader == null) {
+ mExtendedHeader = new MimeHeader();
+ }
+ mExtendedHeader.setHeader(name, END_OF_LINE.matcher(value).replaceAll(""));
+ }
+
+ /**
+ * Get extended header
+ *
+ * @param name Extended header name
+ * @return header value - null if header does not exist
+ * @throws MessagingException
+ */
+ @Override
+ public String getExtendedHeader(String name) throws MessagingException {
+ if (mExtendedHeader == null) {
+ return null;
+ }
+ return mExtendedHeader.getFirstHeader(name);
+ }
+
+ /**
+ * Set entire extended headers from String
+ *
+ * @param headers Extended header and its value - "CR-NL-separated pairs if null or empty, remove
+ * entire extended headers
+ * @throws MessagingException
+ */
+ public void setExtendedHeaders(String headers) throws MessagingException {
+ if (TextUtils.isEmpty(headers)) {
+ mExtendedHeader = null;
+ } else {
+ mExtendedHeader = new MimeHeader();
+ for (final String header : END_OF_LINE.split(headers)) {
+ final String[] tokens = header.split(":", 2);
+ if (tokens.length != 2) {
+ throw new MessagingException("Illegal extended headers: " + headers);
+ }
+ mExtendedHeader.setHeader(tokens[0].trim(), tokens[1].trim());
+ }
+ }
+ }
+
+ /**
+ * Get entire extended headers as String
+ *
+ * @return "CR-NL-separated extended headers - null if extended header does not exist
+ */
+ public String getExtendedHeaders() {
+ if (mExtendedHeader != null) {
+ return mExtendedHeader.writeToString();
+ }
+ return null;
+ }
+
+ /**
+ * Write message header and body to output stream
+ *
+ * @param out Output steam to write message header and body.
+ */
+ @Override
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
+ // Force creation of local message-id
+ getMessageId();
+ getMimeHeaders().writeTo(out);
+ // mExtendedHeader will not be write out to external output stream,
+ // because it is intended to internal use.
+ writer.write("\r\n");
+ writer.flush();
+ if (mBody != null) {
+ mBody.writeTo(out);
+ }
+ }
+
+ @Override
+ public InputStream getInputStream() throws MessagingException {
+ return null;
+ }
+
+ class MimeMessageBuilder implements ContentHandler {
+ private final Stack<Object> stack = new Stack<Object>();
+
+ public MimeMessageBuilder() {}
+
+ private void expect(Class<?> c) {
+ if (!c.isInstance(stack.peek())) {
+ throw new IllegalStateException(
+ "Internal stack error: "
+ + "Expected '"
+ + c.getName()
+ + "' found '"
+ + stack.peek().getClass().getName()
+ + "'");
+ }
+ }
+
+ @Override
+ public void startMessage() {
+ if (stack.isEmpty()) {
+ stack.push(MimeMessage.this);
+ } else {
+ expect(Part.class);
+ try {
+ final MimeMessage m = new MimeMessage();
+ ((Part) stack.peek()).setBody(m);
+ stack.push(m);
+ } catch (MessagingException me) {
+ throw new Error(me);
+ }
+ }
+ }
+
+ @Override
+ public void endMessage() {
+ expect(MimeMessage.class);
+ stack.pop();
+ }
+
+ @Override
+ public void startHeader() {
+ expect(Part.class);
+ }
+
+ @Override
+ public void field(String fieldData) {
+ expect(Part.class);
+ try {
+ final String[] tokens = fieldData.split(":", 2);
+ ((Part) stack.peek()).addHeader(tokens[0], tokens[1].trim());
+ } catch (MessagingException me) {
+ throw new Error(me);
+ }
+ }
+
+ @Override
+ public void endHeader() {
+ expect(Part.class);
+ }
+
+ @Override
+ public void startMultipart(BodyDescriptor bd) {
+ expect(Part.class);
+
+ final Part e = (Part) stack.peek();
+ try {
+ final MimeMultipart multiPart = new MimeMultipart(e.getContentType());
+ e.setBody(multiPart);
+ stack.push(multiPart);
+ } catch (MessagingException me) {
+ throw new Error(me);
+ }
+ }
+
+ @Override
+ public void body(BodyDescriptor bd, InputStream in) throws IOException {
+ expect(Part.class);
+ final Body body = MimeUtility.decodeBody(in, bd.getTransferEncoding());
+ try {
+ ((Part) stack.peek()).setBody(body);
+ } catch (MessagingException me) {
+ throw new Error(me);
+ }
+ }
+
+ @Override
+ public void endMultipart() {
+ stack.pop();
+ }
+
+ @Override
+ public void startBodyPart() {
+ expect(MimeMultipart.class);
+
+ try {
+ final MimeBodyPart bodyPart = new MimeBodyPart();
+ ((MimeMultipart) stack.peek()).addBodyPart(bodyPart);
+ stack.push(bodyPart);
+ } catch (MessagingException me) {
+ throw new Error(me);
+ }
+ }
+
+ @Override
+ public void endBodyPart() {
+ expect(BodyPart.class);
+ stack.pop();
+ }
+
+ @Override
+ public void epilogue(InputStream is) throws IOException {
+ expect(MimeMultipart.class);
+ final StringBuilder sb = new StringBuilder();
+ int b;
+ while ((b = is.read()) != -1) {
+ sb.append((char) b);
+ }
+ // TODO: why is this commented out?
+ // ((Multipart) stack.peek()).setEpilogue(sb.toString());
+ }
+
+ @Override
+ public void preamble(InputStream is) throws IOException {
+ expect(MimeMultipart.class);
+ final StringBuilder sb = new StringBuilder();
+ int b;
+ while ((b = is.read()) != -1) {
+ sb.append((char) b);
+ }
+ try {
+ ((MimeMultipart) stack.peek()).setPreamble(sb.toString());
+ } catch (MessagingException me) {
+ throw new Error(me);
+ }
+ }
+
+ @Override
+ public void raw(InputStream is) throws IOException {
+ throw new UnsupportedOperationException("Not supported");
+ }
+ }
+}
diff --git a/java/com/android/voicemail/impl/mail/internet/MimeMultipart.java b/java/com/android/voicemail/impl/mail/internet/MimeMultipart.java
new file mode 100644
index 000000000..87b88b52a
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/internet/MimeMultipart.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail.internet;
+
+import com.android.voicemail.impl.mail.BodyPart;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.Multipart;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+
+public class MimeMultipart extends Multipart {
+ protected String mPreamble;
+
+ protected String mContentType;
+
+ protected String mBoundary;
+
+ protected String mSubType;
+
+ public MimeMultipart() throws MessagingException {
+ mBoundary = generateBoundary();
+ setSubType("mixed");
+ }
+
+ public MimeMultipart(String contentType) throws MessagingException {
+ this.mContentType = contentType;
+ try {
+ mSubType = MimeUtility.getHeaderParameter(contentType, null).split("/")[1];
+ mBoundary = MimeUtility.getHeaderParameter(contentType, "boundary");
+ if (mBoundary == null) {
+ throw new MessagingException("MultiPart does not contain boundary: " + contentType);
+ }
+ } catch (Exception e) {
+ throw new MessagingException(
+ "Invalid MultiPart Content-Type; must contain subtype and boundary. ("
+ + contentType
+ + ")",
+ e);
+ }
+ }
+
+ public String generateBoundary() {
+ StringBuffer sb = new StringBuffer();
+ sb.append("----");
+ for (int i = 0; i < 30; i++) {
+ sb.append(Integer.toString((int) (Math.random() * 35), 36));
+ }
+ return sb.toString().toUpperCase();
+ }
+
+ public String getPreamble() throws MessagingException {
+ return mPreamble;
+ }
+
+ public void setPreamble(String preamble) throws MessagingException {
+ this.mPreamble = preamble;
+ }
+
+ @Override
+ public String getContentType() throws MessagingException {
+ return mContentType;
+ }
+
+ public void setSubType(String subType) throws MessagingException {
+ this.mSubType = subType;
+ mContentType = String.format("multipart/%s; boundary=\"%s\"", subType, mBoundary);
+ }
+
+ @Override
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
+
+ if (mPreamble != null) {
+ writer.write(mPreamble + "\r\n");
+ }
+
+ for (int i = 0, count = mParts.size(); i < count; i++) {
+ BodyPart bodyPart = mParts.get(i);
+ writer.write("--" + mBoundary + "\r\n");
+ writer.flush();
+ bodyPart.writeTo(out);
+ writer.write("\r\n");
+ }
+
+ writer.write("--" + mBoundary + "--\r\n");
+ writer.flush();
+ }
+
+ @Override
+ public InputStream getInputStream() throws MessagingException {
+ return null;
+ }
+
+ public String getSubTypeForTest() {
+ return mSubType;
+ }
+}
diff --git a/java/com/android/voicemail/impl/mail/internet/MimeUtility.java b/java/com/android/voicemail/impl/mail/internet/MimeUtility.java
new file mode 100644
index 000000000..99846027b
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/internet/MimeUtility.java
@@ -0,0 +1,400 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail.internet;
+
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.Base64DataException;
+import android.util.Base64InputStream;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.mail.Body;
+import com.android.voicemail.impl.mail.BodyPart;
+import com.android.voicemail.impl.mail.Message;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.Multipart;
+import com.android.voicemail.impl.mail.Part;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.apache.commons.io.IOUtils;
+import org.apache.james.mime4j.codec.EncoderUtil;
+import org.apache.james.mime4j.decoder.DecoderUtil;
+import org.apache.james.mime4j.decoder.QuotedPrintableInputStream;
+import org.apache.james.mime4j.util.CharsetUtil;
+
+public class MimeUtility {
+ private static final String LOG_TAG = "Email";
+
+ public static final String MIME_TYPE_RFC822 = "message/rfc822";
+ private static final Pattern PATTERN_CR_OR_LF = Pattern.compile("\r|\n");
+
+ /**
+ * Replace sequences of CRLF+WSP with WSP. Tries to preserve original string object whenever
+ * possible.
+ */
+ public static String unfold(String s) {
+ if (s == null) {
+ return null;
+ }
+ Matcher patternMatcher = PATTERN_CR_OR_LF.matcher(s);
+ if (patternMatcher.find()) {
+ patternMatcher.reset();
+ s = patternMatcher.replaceAll("");
+ }
+ return s;
+ }
+
+ public static String decode(String s) {
+ if (s == null) {
+ return null;
+ }
+ return DecoderUtil.decodeEncodedWords(s);
+ }
+
+ public static String unfoldAndDecode(String s) {
+ return decode(unfold(s));
+ }
+
+ // TODO implement proper foldAndEncode
+ // NOTE: When this really works, we *must* remove all calls to foldAndEncode2() to prevent
+ // duplication of encoding.
+ public static String foldAndEncode(String s) {
+ return s;
+ }
+
+ /**
+ * INTERIM version of foldAndEncode that will be used only by Subject: headers. This is safer than
+ * implementing foldAndEncode() (see above) and risking unknown damage to other headers.
+ *
+ * <p>TODO: Copy this code to foldAndEncode(), get rid of this function, confirm all working OK.
+ *
+ * @param s original string to encode and fold
+ * @param usedCharacters number of characters already used up by header name
+ * @return the String ready to be transmitted
+ */
+ public static String foldAndEncode2(String s, int usedCharacters) {
+ // james.mime4j.codec.EncoderUtil.java
+ // encode: encodeIfNecessary(text, usage, numUsedInHeaderName)
+ // Usage.TEXT_TOKENlooks like the right thing for subjects
+ // use WORD_ENTITY for address/names
+
+ String encoded = EncoderUtil.encodeIfNecessary(s, EncoderUtil.Usage.TEXT_TOKEN, usedCharacters);
+
+ return fold(encoded, usedCharacters);
+ }
+
+ /**
+ * INTERIM: From newer version of org.apache.james (but we don't want to import the entire
+ * MimeUtil class).
+ *
+ * <p>Splits the specified string into a multiple-line representation with lines no longer than 76
+ * characters (because the line might contain encoded words; see <a
+ * href='http://www.faqs.org/rfcs/rfc2047.html'>RFC 2047</a> section 2). If the string contains
+ * non-whitespace sequences longer than 76 characters a line break is inserted at the whitespace
+ * character following the sequence resulting in a line longer than 76 characters.
+ *
+ * @param s string to split.
+ * @param usedCharacters number of characters already used up. Usually the number of characters
+ * for header field name plus colon and one space.
+ * @return a multiple-line representation of the given string.
+ */
+ public static String fold(String s, int usedCharacters) {
+ final int maxCharacters = 76;
+
+ final int length = s.length();
+ if (usedCharacters + length <= maxCharacters) {
+ return s;
+ }
+
+ StringBuilder sb = new StringBuilder();
+
+ int lastLineBreak = -usedCharacters;
+ int wspIdx = indexOfWsp(s, 0);
+ while (true) {
+ if (wspIdx == length) {
+ sb.append(s.substring(Math.max(0, lastLineBreak)));
+ return sb.toString();
+ }
+
+ int nextWspIdx = indexOfWsp(s, wspIdx + 1);
+
+ if (nextWspIdx - lastLineBreak > maxCharacters) {
+ sb.append(s.substring(Math.max(0, lastLineBreak), wspIdx));
+ sb.append("\r\n");
+ lastLineBreak = wspIdx;
+ }
+
+ wspIdx = nextWspIdx;
+ }
+ }
+
+ /**
+ * INTERIM: From newer version of org.apache.james (but we don't want to import the entire
+ * MimeUtil class).
+ *
+ * <p>Search for whitespace.
+ */
+ private static int indexOfWsp(String s, int fromIndex) {
+ final int len = s.length();
+ for (int index = fromIndex; index < len; index++) {
+ char c = s.charAt(index);
+ if (c == ' ' || c == '\t') {
+ return index;
+ }
+ }
+ return len;
+ }
+
+ /**
+ * Returns the named parameter of a header field. If name is null the first parameter is returned,
+ * or if there are no additional parameters in the field the entire field is returned. Otherwise
+ * the named parameter is searched for in a case insensitive fashion and returned. If the
+ * parameter cannot be found the method returns null.
+ *
+ * <p>TODO: quite inefficient with the inner trimming & splitting. TODO: Also has a latent bug:
+ * uses "startsWith" to match the name, which can false-positive. TODO: The doc says that for a
+ * null name you get the first param, but you get the header. Should probably just fix the doc,
+ * but if other code assumes that behavior, fix the code. TODO: Need to decode %-escaped strings,
+ * as in: filename="ab%22d". ('+' -> ' ' conversion too? check RFC)
+ *
+ * @param header
+ * @param name
+ * @return the entire header (if name=null), the found parameter, or null
+ */
+ public static String getHeaderParameter(String header, String name) {
+ if (header == null) {
+ return null;
+ }
+ String[] parts = unfold(header).split(";");
+ if (name == null) {
+ return parts[0].trim();
+ }
+ String lowerCaseName = name.toLowerCase();
+ for (String part : parts) {
+ if (part.trim().toLowerCase().startsWith(lowerCaseName)) {
+ String[] parameterParts = part.split("=", 2);
+ if (parameterParts.length < 2) {
+ return null;
+ }
+ String parameter = parameterParts[1].trim();
+ if (parameter.startsWith("\"") && parameter.endsWith("\"")) {
+ return parameter.substring(1, parameter.length() - 1);
+ } else {
+ return parameter;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Reads the Part's body and returns a String based on any charset conversion that needed to be
+ * done.
+ *
+ * @param part The part containing a body
+ * @return a String containing the converted text in the body, or null if there was no text or an
+ * error during conversion.
+ */
+ public static String getTextFromPart(Part part) {
+ try {
+ if (part != null && part.getBody() != null) {
+ InputStream in = part.getBody().getInputStream();
+ String mimeType = part.getMimeType();
+ if (mimeType != null && MimeUtility.mimeTypeMatches(mimeType, "text/*")) {
+ /*
+ * Now we read the part into a buffer for further processing. Because
+ * the stream is now wrapped we'll remove any transfer encoding at this point.
+ */
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ IOUtils.copy(in, out);
+ in.close();
+ in = null; // we want all of our memory back, and close might not release
+
+ /*
+ * We've got a text part, so let's see if it needs to be processed further.
+ */
+ String charset = getHeaderParameter(part.getContentType(), "charset");
+ if (charset != null) {
+ /*
+ * See if there is conversion from the MIME charset to the Java one.
+ */
+ charset = CharsetUtil.toJavaCharset(charset);
+ }
+ /*
+ * No encoding, so use us-ascii, which is the standard.
+ */
+ if (charset == null) {
+ charset = "ASCII";
+ }
+ /*
+ * Convert and return as new String
+ */
+ String result = out.toString(charset);
+ out.close();
+ return result;
+ }
+ }
+
+ } catch (OutOfMemoryError oom) {
+ /*
+ * If we are not able to process the body there's nothing we can do about it. Return
+ * null and let the upper layers handle the missing content.
+ */
+ VvmLog.e(LOG_TAG, "Unable to getTextFromPart " + oom.toString());
+ } catch (Exception e) {
+ /*
+ * If we are not able to process the body there's nothing we can do about it. Return
+ * null and let the upper layers handle the missing content.
+ */
+ VvmLog.e(LOG_TAG, "Unable to getTextFromPart " + e.toString());
+ }
+ return null;
+ }
+
+ /**
+ * Returns true if the given mimeType matches the matchAgainst specification. The comparison
+ * ignores case and the matchAgainst string may include "*" for a wildcard (e.g. "image/*").
+ *
+ * @param mimeType A MIME type to check.
+ * @param matchAgainst A MIME type to check against. May include wildcards.
+ * @return true if the mimeType matches
+ */
+ public static boolean mimeTypeMatches(String mimeType, String matchAgainst) {
+ Pattern p = Pattern.compile(matchAgainst.replaceAll("\\*", "\\.\\*"), Pattern.CASE_INSENSITIVE);
+ return p.matcher(mimeType).matches();
+ }
+
+ /**
+ * Returns true if the given mimeType matches any of the matchAgainst specifications. The
+ * comparison ignores case and the matchAgainst strings may include "*" for a wildcard (e.g.
+ * "image/*").
+ *
+ * @param mimeType A MIME type to check.
+ * @param matchAgainst An array of MIME types to check against. May include wildcards.
+ * @return true if the mimeType matches any of the matchAgainst strings
+ */
+ public static boolean mimeTypeMatches(String mimeType, String[] matchAgainst) {
+ for (String matchType : matchAgainst) {
+ if (mimeTypeMatches(mimeType, matchType)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Given an input stream and a transfer encoding, return a wrapped input stream for that encoding
+ * (or the original if none is required)
+ *
+ * @param in the input stream
+ * @param contentTransferEncoding the content transfer encoding
+ * @return a properly wrapped stream
+ */
+ public static InputStream getInputStreamForContentTransferEncoding(
+ InputStream in, String contentTransferEncoding) {
+ if (contentTransferEncoding != null) {
+ contentTransferEncoding = MimeUtility.getHeaderParameter(contentTransferEncoding, null);
+ if ("quoted-printable".equalsIgnoreCase(contentTransferEncoding)) {
+ in = new QuotedPrintableInputStream(in);
+ } else if ("base64".equalsIgnoreCase(contentTransferEncoding)) {
+ in = new Base64InputStream(in, Base64.DEFAULT);
+ }
+ }
+ return in;
+ }
+
+ /** Removes any content transfer encoding from the stream and returns a Body. */
+ public static Body decodeBody(InputStream in, String contentTransferEncoding) throws IOException {
+ /*
+ * We'll remove any transfer encoding by wrapping the stream.
+ */
+ in = getInputStreamForContentTransferEncoding(in, contentTransferEncoding);
+ BinaryTempFileBody tempBody = new BinaryTempFileBody();
+ OutputStream out = tempBody.getOutputStream();
+ try {
+ IOUtils.copy(in, out);
+ } catch (Base64DataException bde) {
+ // TODO Need to fix this somehow
+ //String warning = "\n\n" + Email.getMessageDecodeErrorString();
+ //out.write(warning.getBytes());
+ } finally {
+ out.close();
+ }
+ return tempBody;
+ }
+
+ /**
+ * Recursively scan a Part (usually a Message) and sort out which of its children will be
+ * "viewable" and which will be attachments.
+ *
+ * @param part The part to be broken down
+ * @param viewables This arraylist will be populated with all parts that appear to be the
+ * "message" (e.g. text/plain & text/html)
+ * @param attachments This arraylist will be populated with all parts that appear to be
+ * attachments (including inlines)
+ * @throws MessagingException
+ */
+ public static void collectParts(Part part, ArrayList<Part> viewables, ArrayList<Part> attachments)
+ throws MessagingException {
+ String disposition = part.getDisposition();
+ String dispositionType = MimeUtility.getHeaderParameter(disposition, null);
+ // If a disposition is not specified, default to "inline"
+ boolean inline =
+ TextUtils.isEmpty(dispositionType) || "inline".equalsIgnoreCase(dispositionType);
+ // The lower-case mime type
+ String mimeType = part.getMimeType().toLowerCase();
+
+ if (part.getBody() instanceof Multipart) {
+ // If the part is Multipart but not alternative it's either mixed or
+ // something we don't know about, which means we treat it as mixed
+ // per the spec. We just process its pieces recursively.
+ MimeMultipart mp = (MimeMultipart) part.getBody();
+ boolean foundHtml = false;
+ if (mp.getSubTypeForTest().equals("alternative")) {
+ for (int i = 0; i < mp.getCount(); i++) {
+ if (mp.getBodyPart(i).isMimeType("text/html")) {
+ foundHtml = true;
+ break;
+ }
+ }
+ }
+ for (int i = 0; i < mp.getCount(); i++) {
+ // See if we have text and html
+ BodyPart bp = mp.getBodyPart(i);
+ // If there's html, don't bother loading text
+ if (foundHtml && bp.isMimeType("text/plain")) {
+ continue;
+ }
+ collectParts(bp, viewables, attachments);
+ }
+ } else if (part.getBody() instanceof Message) {
+ // If the part is an embedded message we just continue to process
+ // it, pulling any viewables or attachments into the running list.
+ Message message = (Message) part.getBody();
+ collectParts(message, viewables, attachments);
+ } else if (inline && (mimeType.startsWith("text") || (mimeType.startsWith("image")))) {
+ // We'll treat text and images as viewables
+ viewables.add(part);
+ } else {
+ // Everything else is an attachment.
+ attachments.add(part);
+ }
+ }
+}
diff --git a/java/com/android/voicemail/impl/mail/internet/TextBody.java b/java/com/android/voicemail/impl/mail/internet/TextBody.java
new file mode 100644
index 000000000..dae562508
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/internet/TextBody.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail.internet;
+
+import android.util.Base64;
+import com.android.voicemail.impl.mail.Body;
+import com.android.voicemail.impl.mail.MessagingException;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+
+public class TextBody implements Body {
+ String mBody;
+
+ public TextBody(String body) {
+ this.mBody = body;
+ }
+
+ @Override
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ byte[] bytes = mBody.getBytes("UTF-8");
+ out.write(Base64.encode(bytes, Base64.CRLF));
+ }
+
+ /**
+ * Get the text of the body in it's unencoded format.
+ *
+ * @return
+ */
+ public String getText() {
+ return mBody;
+ }
+
+ /** Returns an InputStream that reads this body's text in UTF-8 format. */
+ @Override
+ public InputStream getInputStream() throws MessagingException {
+ try {
+ byte[] b = mBody.getBytes("UTF-8");
+ return new ByteArrayInputStream(b);
+ } catch (UnsupportedEncodingException usee) {
+ return null;
+ }
+ }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/ImapConnection.java b/java/com/android/voicemail/impl/mail/store/ImapConnection.java
new file mode 100644
index 000000000..0a48dfc69
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/ImapConnection.java
@@ -0,0 +1,400 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail.store;
+
+import android.util.ArraySet;
+import android.util.Base64;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.mail.AuthenticationFailedException;
+import com.android.voicemail.impl.mail.CertificateValidationException;
+import com.android.voicemail.impl.mail.MailTransport;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.store.ImapStore.ImapException;
+import com.android.voicemail.impl.mail.store.imap.DigestMd5Utils;
+import com.android.voicemail.impl.mail.store.imap.ImapConstants;
+import com.android.voicemail.impl.mail.store.imap.ImapResponse;
+import com.android.voicemail.impl.mail.store.imap.ImapResponseParser;
+import com.android.voicemail.impl.mail.store.imap.ImapUtility;
+import com.android.voicemail.impl.mail.utils.LogUtils;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.net.ssl.SSLException;
+
+/** A cacheable class that stores the details for a single IMAP connection. */
+public class ImapConnection {
+ private final String TAG = "ImapConnection";
+
+ private String mLoginPhrase;
+ private ImapStore mImapStore;
+ private MailTransport mTransport;
+ private ImapResponseParser mParser;
+ private Set<String> mCapabilities = new ArraySet<>();
+
+ static final String IMAP_REDACTED_LOG = "[IMAP command redacted]";
+
+ /**
+ * Next tag to use. All connections associated to the same ImapStore instance share the same
+ * counter to make tests simpler. (Some of the tests involve multiple connections but only have a
+ * single counter to track the tag.)
+ */
+ private final AtomicInteger mNextCommandTag = new AtomicInteger(0);
+
+ ImapConnection(ImapStore store) {
+ setStore(store);
+ }
+
+ void setStore(ImapStore store) {
+ // TODO: maybe we should throw an exception if the connection is not closed here,
+ // if it's not currently closed, then we won't reopen it, so if the credentials have
+ // changed, the connection will not be reestablished.
+ mImapStore = store;
+ mLoginPhrase = null;
+ }
+
+ /**
+ * Generates and returns the phrase to be used for authentication. This will be a LOGIN with
+ * username and password.
+ *
+ * @return the login command string to sent to the IMAP server
+ */
+ String getLoginPhrase() {
+ if (mLoginPhrase == null) {
+ if (mImapStore.getUsername() != null && mImapStore.getPassword() != null) {
+ // build the LOGIN string once (instead of over-and-over again.)
+ // apply the quoting here around the built-up password
+ mLoginPhrase =
+ ImapConstants.LOGIN
+ + " "
+ + mImapStore.getUsername()
+ + " "
+ + ImapUtility.imapQuoted(mImapStore.getPassword());
+ }
+ }
+ return mLoginPhrase;
+ }
+
+ public void open() throws IOException, MessagingException {
+ if (mTransport != null && mTransport.isOpen()) {
+ return;
+ }
+
+ try {
+ // copy configuration into a clean transport, if necessary
+ if (mTransport == null) {
+ mTransport = mImapStore.cloneTransport();
+ }
+
+ mTransport.open();
+
+ createParser();
+
+ // The server should greet us with something like
+ // * OK IMAP4rev1 Server
+ // consume the response before doing anything else.
+ ImapResponse response = mParser.readResponse(false);
+ if (!response.isOk()) {
+ mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_INVALID_INITIAL_SERVER_RESPONSE);
+ throw new MessagingException(
+ MessagingException.AUTHENTICATION_FAILED_OR_SERVER_ERROR,
+ "Invalid server initial response");
+ }
+
+ queryCapability();
+
+ maybeDoStartTls();
+
+ // LOGIN
+ doLogin();
+ } catch (SSLException e) {
+ LogUtils.d(TAG, "SSLException ", e);
+ mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_SSL_EXCEPTION);
+ throw new CertificateValidationException(e.getMessage(), e);
+ } catch (IOException ioe) {
+ LogUtils.d(TAG, "IOException", ioe);
+ mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_IOE_ON_OPEN);
+ throw ioe;
+ } finally {
+ destroyResponses();
+ }
+ }
+
+ void logout() {
+ try {
+ sendCommand(ImapConstants.LOGOUT, false);
+ if (!mParser.readResponse(true).is(0, ImapConstants.BYE)) {
+ VvmLog.e(TAG, "Server did not respond LOGOUT with BYE");
+ }
+ if (!mParser.readResponse(false).isOk()) {
+ VvmLog.e(TAG, "Server did not respond OK after LOGOUT");
+ }
+ } catch (IOException | MessagingException e) {
+ VvmLog.e(TAG, "Error while logging out:" + e);
+ }
+ }
+
+ /**
+ * Closes the connection and releases all resources. This connection can not be used again until
+ * {@link #setStore(ImapStore)} is called.
+ */
+ void close() {
+ if (mTransport != null) {
+ logout();
+ mTransport.close();
+ mTransport = null;
+ }
+ destroyResponses();
+ mParser = null;
+ mImapStore = null;
+ }
+
+ /** Attempts to convert the connection into secure connection. */
+ private void maybeDoStartTls() throws IOException, MessagingException {
+ // STARTTLS is required in the OMTP standard but not every implementation support it.
+ // Make sure the server does have this capability
+ if (hasCapability(ImapConstants.CAPABILITY_STARTTLS)) {
+ executeSimpleCommand(ImapConstants.STARTTLS);
+ mTransport.reopenTls();
+ createParser();
+ // The cached capabilities should be refreshed after TLS is established.
+ queryCapability();
+ }
+ }
+
+ /** Logs into the IMAP server */
+ private void doLogin() throws IOException, MessagingException, AuthenticationFailedException {
+ try {
+ if (mCapabilities.contains(ImapConstants.CAPABILITY_AUTH_DIGEST_MD5)) {
+ doDigestMd5Auth();
+ } else {
+ executeSimpleCommand(getLoginPhrase(), true);
+ }
+ } catch (ImapException ie) {
+ LogUtils.d(TAG, "ImapException", ie);
+ String status = ie.getStatus();
+ String statusMessage = ie.getStatusMessage();
+ String alertText = ie.getAlertText();
+
+ if (ImapConstants.NO.equals(status)) {
+ switch (statusMessage) {
+ case ImapConstants.NO_UNKNOWN_USER:
+ mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_UNKNOWN_USER);
+ break;
+ case ImapConstants.NO_UNKNOWN_CLIENT:
+ mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_UNKNOWN_DEVICE);
+ break;
+ case ImapConstants.NO_INVALID_PASSWORD:
+ mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_INVALID_PASSWORD);
+ break;
+ case ImapConstants.NO_MAILBOX_NOT_INITIALIZED:
+ mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_MAILBOX_NOT_INITIALIZED);
+ break;
+ case ImapConstants.NO_SERVICE_IS_NOT_PROVISIONED:
+ mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_SERVICE_NOT_PROVISIONED);
+ break;
+ case ImapConstants.NO_SERVICE_IS_NOT_ACTIVATED:
+ mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_SERVICE_NOT_ACTIVATED);
+ break;
+ case ImapConstants.NO_USER_IS_BLOCKED:
+ mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_USER_IS_BLOCKED);
+ break;
+ case ImapConstants.NO_APPLICATION_ERROR:
+ mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_REJECTED_SERVER_RESPONSE);
+ break;
+ default:
+ mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_BAD_IMAP_CREDENTIAL);
+ }
+ throw new AuthenticationFailedException(alertText, ie);
+ }
+
+ mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_REJECTED_SERVER_RESPONSE);
+ throw new MessagingException(alertText, ie);
+ }
+ }
+
+ private void doDigestMd5Auth() throws IOException, MessagingException {
+
+ // Initiate the authentication.
+ // The server will issue us a challenge, asking to run MD5 on the nonce with our password
+ // and other data, including the cnonce we randomly generated.
+ //
+ // C: a AUTHENTICATE DIGEST-MD5
+ // S: (BASE64) realm="elwood.innosoft.com",nonce="OA6MG9tEQGm2hh",qop="auth",
+ // algorithm=md5-sess,charset=utf-8
+ List<ImapResponse> responses =
+ executeSimpleCommand(ImapConstants.AUTHENTICATE + " " + ImapConstants.AUTH_DIGEST_MD5);
+ String decodedChallenge = decodeBase64(responses.get(0).getStringOrEmpty(0).getString());
+
+ Map<String, String> challenge = DigestMd5Utils.parseDigestMessage(decodedChallenge);
+ DigestMd5Utils.Data data = new DigestMd5Utils.Data(mImapStore, mTransport, challenge);
+
+ String response = data.createResponse();
+ // Respond to the challenge. If the server accepts it, it will reply a response-auth which
+ // is the MD5 of our password and the cnonce we've provided, to prove the server does know
+ // the password.
+ //
+ // C: (BASE64) charset=utf-8,username="chris",realm="elwood.innosoft.com",
+ // nonce="OA6MG9tEQGm2hh",nc=00000001,cnonce="OA6MHXh6VqTrRk",
+ // digest-uri="imap/elwood.innosoft.com",
+ // response=d388dad90d4bbd760a152321f2143af7,qop=auth
+ // S: (BASE64) rspauth=ea40f60335c427b5527b84dbabcdfffd
+
+ responses = executeContinuationResponse(encodeBase64(response), true);
+
+ // Verify response-auth.
+ // If failed verifyResponseAuth() will throw a MessagingException, terminating the
+ // connection
+ String decodedResponseAuth = decodeBase64(responses.get(0).getStringOrEmpty(0).getString());
+ data.verifyResponseAuth(decodedResponseAuth);
+
+ // Send a empty response to indicate we've accepted the response-auth
+ //
+ // C: (empty)
+ // S: a OK User logged in
+ executeContinuationResponse("", false);
+ }
+
+ private static String decodeBase64(String string) {
+ return new String(Base64.decode(string, Base64.DEFAULT));
+ }
+
+ private static String encodeBase64(String string) {
+ return Base64.encodeToString(string.getBytes(), Base64.NO_WRAP);
+ }
+
+ private void queryCapability() throws IOException, MessagingException {
+ List<ImapResponse> responses = executeSimpleCommand(ImapConstants.CAPABILITY);
+ mCapabilities.clear();
+ Set<String> disabledCapabilities =
+ mImapStore.getImapHelper().getConfig().getDisabledCapabilities();
+ for (ImapResponse response : responses) {
+ if (response.isTagged()) {
+ continue;
+ }
+ for (int i = 0; i < response.size(); i++) {
+ String capability = response.getStringOrEmpty(i).getString();
+ if (disabledCapabilities != null) {
+ if (!disabledCapabilities.contains(capability)) {
+ mCapabilities.add(capability);
+ }
+ } else {
+ mCapabilities.add(capability);
+ }
+ }
+ }
+
+ LogUtils.d(TAG, "Capabilities: " + mCapabilities.toString());
+ }
+
+ private boolean hasCapability(String capability) {
+ return mCapabilities.contains(capability);
+ }
+ /**
+ * Create an {@link ImapResponseParser} from {@code mTransport.getInputStream()} and set it to
+ * {@link #mParser}.
+ *
+ * <p>If we already have an {@link ImapResponseParser}, we {@link #destroyResponses()} and throw
+ * it away.
+ */
+ private void createParser() {
+ destroyResponses();
+ mParser = new ImapResponseParser(mTransport.getInputStream());
+ }
+
+ public void destroyResponses() {
+ if (mParser != null) {
+ mParser.destroyResponses();
+ }
+ }
+
+ public ImapResponse readResponse() throws IOException, MessagingException {
+ return mParser.readResponse(false);
+ }
+
+ public List<ImapResponse> executeSimpleCommand(String command)
+ throws IOException, MessagingException {
+ return executeSimpleCommand(command, false);
+ }
+
+ /**
+ * Send a single command to the server. The command will be preceded by an IMAP command tag and
+ * followed by \r\n (caller need not supply them). Execute a simple command at the server, a
+ * simple command being one that is sent in a single line of text
+ *
+ * @param command the command to send to the server
+ * @param sensitive whether the command should be redacted in logs (used for login)
+ * @return a list of ImapResponses
+ * @throws IOException
+ * @throws MessagingException
+ */
+ public List<ImapResponse> executeSimpleCommand(String command, boolean sensitive)
+ throws IOException, MessagingException {
+ // TODO: It may be nice to catch IOExceptions and close the connection here.
+ // Currently, we expect callers to do that, but if they fail to we'll be in a broken state.
+ sendCommand(command, sensitive);
+ return getCommandResponses();
+ }
+
+ public String sendCommand(String command, boolean sensitive)
+ throws IOException, MessagingException {
+ open();
+
+ if (mTransport == null) {
+ throw new IOException("Null transport");
+ }
+ String tag = Integer.toString(mNextCommandTag.incrementAndGet());
+ String commandToSend = tag + " " + command;
+ mTransport.writeLine(commandToSend, (sensitive ? IMAP_REDACTED_LOG : command));
+ return tag;
+ }
+
+ List<ImapResponse> executeContinuationResponse(String response, boolean sensitive)
+ throws IOException, MessagingException {
+ mTransport.writeLine(response, (sensitive ? IMAP_REDACTED_LOG : response));
+ return getCommandResponses();
+ }
+
+ /**
+ * Read and return all of the responses from the most recent command sent to the server
+ *
+ * @return a list of ImapResponses
+ * @throws IOException
+ * @throws MessagingException
+ */
+ List<ImapResponse> getCommandResponses() throws IOException, MessagingException {
+ final List<ImapResponse> responses = new ArrayList<ImapResponse>();
+ ImapResponse response;
+ do {
+ response = mParser.readResponse(false);
+ responses.add(response);
+ } while (!(response.isTagged() || response.isContinuationRequest()));
+
+ if (!(response.isOk() || response.isContinuationRequest())) {
+ final String toString = response.toString();
+ final String status = response.getStatusOrEmpty().getString();
+ final String statusMessage = response.getStatusResponseTextOrEmpty().getString();
+ final String alert = response.getAlertTextOrEmpty().getString();
+ final String responseCode = response.getResponseCodeOrEmpty().getString();
+ destroyResponses();
+ throw new ImapException(toString, status, statusMessage, alert, responseCode);
+ }
+ return responses;
+ }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/ImapFolder.java b/java/com/android/voicemail/impl/mail/store/ImapFolder.java
new file mode 100644
index 000000000..1d9b01120
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/ImapFolder.java
@@ -0,0 +1,797 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail.store;
+
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Base64DataException;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.mail.AuthenticationFailedException;
+import com.android.voicemail.impl.mail.Body;
+import com.android.voicemail.impl.mail.FetchProfile;
+import com.android.voicemail.impl.mail.Flag;
+import com.android.voicemail.impl.mail.Message;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.Part;
+import com.android.voicemail.impl.mail.internet.BinaryTempFileBody;
+import com.android.voicemail.impl.mail.internet.MimeBodyPart;
+import com.android.voicemail.impl.mail.internet.MimeHeader;
+import com.android.voicemail.impl.mail.internet.MimeMultipart;
+import com.android.voicemail.impl.mail.internet.MimeUtility;
+import com.android.voicemail.impl.mail.store.ImapStore.ImapException;
+import com.android.voicemail.impl.mail.store.ImapStore.ImapMessage;
+import com.android.voicemail.impl.mail.store.imap.ImapConstants;
+import com.android.voicemail.impl.mail.store.imap.ImapElement;
+import com.android.voicemail.impl.mail.store.imap.ImapList;
+import com.android.voicemail.impl.mail.store.imap.ImapResponse;
+import com.android.voicemail.impl.mail.store.imap.ImapString;
+import com.android.voicemail.impl.mail.utils.LogUtils;
+import com.android.voicemail.impl.mail.utils.Utility;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+
+public class ImapFolder {
+ private static final String TAG = "ImapFolder";
+ private static final String[] PERMANENT_FLAGS = {
+ Flag.DELETED, Flag.SEEN, Flag.FLAGGED, Flag.ANSWERED
+ };
+ private static final int COPY_BUFFER_SIZE = 16 * 1024;
+
+ private final ImapStore mStore;
+ private final String mName;
+ private int mMessageCount = -1;
+ private ImapConnection mConnection;
+ private String mMode;
+ private boolean mExists;
+ /** A set of hashes that can be used to track dirtiness */
+ Object mHash[];
+
+ public static final String MODE_READ_ONLY = "mode_read_only";
+ public static final String MODE_READ_WRITE = "mode_read_write";
+
+ public ImapFolder(ImapStore store, String name) {
+ mStore = store;
+ mName = name;
+ }
+
+ /** Callback for each message retrieval. */
+ public interface MessageRetrievalListener {
+ public void messageRetrieved(Message message);
+ }
+
+ private void destroyResponses() {
+ if (mConnection != null) {
+ mConnection.destroyResponses();
+ }
+ }
+
+ public void open(String mode) throws MessagingException {
+ try {
+ if (isOpen()) {
+ throw new AssertionError("Duplicated open on ImapFolder");
+ }
+ synchronized (this) {
+ mConnection = mStore.getConnection();
+ }
+ // * FLAGS (\Answered \Flagged \Deleted \Seen \Draft NonJunk
+ // $MDNSent)
+ // * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft
+ // NonJunk $MDNSent \*)] Flags permitted.
+ // * 23 EXISTS
+ // * 0 RECENT
+ // * OK [UIDVALIDITY 1125022061] UIDs valid
+ // * OK [UIDNEXT 57576] Predicted next UID
+ // 2 OK [READ-WRITE] Select completed.
+ try {
+ doSelect();
+ } catch (IOException ioe) {
+ throw ioExceptionHandler(mConnection, ioe);
+ } finally {
+ destroyResponses();
+ }
+ } catch (AuthenticationFailedException e) {
+ // Don't cache this connection, so we're forced to try connecting/login again
+ mConnection = null;
+ close(false);
+ throw e;
+ } catch (MessagingException e) {
+ mExists = false;
+ close(false);
+ throw e;
+ }
+ }
+
+ public boolean isOpen() {
+ return mExists && mConnection != null;
+ }
+
+ public String getMode() {
+ return mMode;
+ }
+
+ public void close(boolean expunge) {
+ if (expunge) {
+ try {
+ expunge();
+ } catch (MessagingException e) {
+ LogUtils.e(TAG, e, "Messaging Exception");
+ }
+ }
+ mMessageCount = -1;
+ synchronized (this) {
+ mConnection = null;
+ }
+ }
+
+ public int getMessageCount() {
+ return mMessageCount;
+ }
+
+ String[] getSearchUids(List<ImapResponse> responses) {
+ // S: * SEARCH 2 3 6
+ final ArrayList<String> uids = new ArrayList<String>();
+ for (ImapResponse response : responses) {
+ if (!response.isDataResponse(0, ImapConstants.SEARCH)) {
+ continue;
+ }
+ // Found SEARCH response data
+ for (int i = 1; i < response.size(); i++) {
+ ImapString s = response.getStringOrEmpty(i);
+ if (s.isString()) {
+ uids.add(s.getString());
+ }
+ }
+ }
+ return uids.toArray(Utility.EMPTY_STRINGS);
+ }
+
+ @VisibleForTesting
+ String[] searchForUids(String searchCriteria) throws MessagingException {
+ checkOpen();
+ try {
+ try {
+ final String command = ImapConstants.UID_SEARCH + " " + searchCriteria;
+ final String[] result = getSearchUids(mConnection.executeSimpleCommand(command));
+ LogUtils.d(TAG, "searchForUids '" + searchCriteria + "' results: " + result.length);
+ return result;
+ } catch (ImapException me) {
+ LogUtils.d(TAG, "ImapException in search: " + searchCriteria, me);
+ return Utility.EMPTY_STRINGS; // Not found
+ } catch (IOException ioe) {
+ LogUtils.d(TAG, "IOException in search: " + searchCriteria, ioe);
+ mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
+ throw ioExceptionHandler(mConnection, ioe);
+ }
+ } finally {
+ destroyResponses();
+ }
+ }
+
+ @Nullable
+ public Message getMessage(String uid) throws MessagingException {
+ checkOpen();
+
+ final String[] uids = searchForUids(ImapConstants.UID + " " + uid);
+ for (int i = 0; i < uids.length; i++) {
+ if (uids[i].equals(uid)) {
+ return new ImapMessage(uid, this);
+ }
+ }
+ LogUtils.e(TAG, "UID " + uid + " not found on server");
+ return null;
+ }
+
+ @VisibleForTesting
+ protected static boolean isAsciiString(String str) {
+ int len = str.length();
+ for (int i = 0; i < len; i++) {
+ char c = str.charAt(i);
+ if (c >= 128) return false;
+ }
+ return true;
+ }
+
+ public Message[] getMessages(String[] uids) throws MessagingException {
+ if (uids == null) {
+ uids = searchForUids("1:* NOT DELETED");
+ }
+ return getMessagesInternal(uids);
+ }
+
+ public Message[] getMessagesInternal(String[] uids) {
+ final ArrayList<Message> messages = new ArrayList<Message>(uids.length);
+ for (int i = 0; i < uids.length; i++) {
+ final String uid = uids[i];
+ final ImapMessage message = new ImapMessage(uid, this);
+ messages.add(message);
+ }
+ return messages.toArray(Message.EMPTY_ARRAY);
+ }
+
+ public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)
+ throws MessagingException {
+ try {
+ fetchInternal(messages, fp, listener);
+ } catch (RuntimeException e) { // Probably a parser error.
+ LogUtils.w(TAG, "Exception detected: " + e.getMessage());
+ throw e;
+ }
+ }
+
+ public void fetchInternal(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)
+ throws MessagingException {
+ if (messages.length == 0) {
+ return;
+ }
+ checkOpen();
+ ArrayMap<String, Message> messageMap = new ArrayMap<String, Message>();
+ for (Message m : messages) {
+ messageMap.put(m.getUid(), m);
+ }
+
+ /*
+ * Figure out what command we are going to run:
+ * FLAGS - UID FETCH (FLAGS)
+ * ENVELOPE - UID FETCH (INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[
+ * HEADER.FIELDS (date subject from content-type to cc)])
+ * STRUCTURE - UID FETCH (BODYSTRUCTURE)
+ * BODY_SANE - UID FETCH (BODY.PEEK[]<0.N>) where N = max bytes returned
+ * BODY - UID FETCH (BODY.PEEK[])
+ * Part - UID FETCH (BODY.PEEK[ID]) where ID = mime part ID
+ */
+
+ final LinkedHashSet<String> fetchFields = new LinkedHashSet<String>();
+
+ fetchFields.add(ImapConstants.UID);
+ if (fp.contains(FetchProfile.Item.FLAGS)) {
+ fetchFields.add(ImapConstants.FLAGS);
+ }
+ if (fp.contains(FetchProfile.Item.ENVELOPE)) {
+ fetchFields.add(ImapConstants.INTERNALDATE);
+ fetchFields.add(ImapConstants.RFC822_SIZE);
+ fetchFields.add(ImapConstants.FETCH_FIELD_HEADERS);
+ }
+ if (fp.contains(FetchProfile.Item.STRUCTURE)) {
+ fetchFields.add(ImapConstants.BODYSTRUCTURE);
+ }
+
+ if (fp.contains(FetchProfile.Item.BODY_SANE)) {
+ fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_SANE);
+ }
+ if (fp.contains(FetchProfile.Item.BODY)) {
+ fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK);
+ }
+
+ // TODO Why are we only fetching the first part given?
+ final Part fetchPart = fp.getFirstPart();
+ if (fetchPart != null) {
+ final String[] partIds = fetchPart.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA);
+ // TODO Why can a single part have more than one Id? And why should we only fetch
+ // the first id if there are more than one?
+ if (partIds != null) {
+ fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_BARE + "[" + partIds[0] + "]");
+ }
+ }
+
+ try {
+ mConnection.sendCommand(
+ String.format(
+ Locale.US,
+ ImapConstants.UID_FETCH + " %s (%s)",
+ ImapStore.joinMessageUids(messages),
+ Utility.combine(fetchFields.toArray(new String[fetchFields.size()]), ' ')),
+ false);
+ ImapResponse response;
+ do {
+ response = null;
+ try {
+ response = mConnection.readResponse();
+
+ if (!response.isDataResponse(1, ImapConstants.FETCH)) {
+ continue; // Ignore
+ }
+ final ImapList fetchList = response.getListOrEmpty(2);
+ final String uid = fetchList.getKeyedStringOrEmpty(ImapConstants.UID).getString();
+ if (TextUtils.isEmpty(uid)) continue;
+
+ ImapMessage message = (ImapMessage) messageMap.get(uid);
+ if (message == null) continue;
+
+ if (fp.contains(FetchProfile.Item.FLAGS)) {
+ final ImapList flags = fetchList.getKeyedListOrEmpty(ImapConstants.FLAGS);
+ for (int i = 0, count = flags.size(); i < count; i++) {
+ final ImapString flag = flags.getStringOrEmpty(i);
+ if (flag.is(ImapConstants.FLAG_DELETED)) {
+ message.setFlagInternal(Flag.DELETED, true);
+ } else if (flag.is(ImapConstants.FLAG_ANSWERED)) {
+ message.setFlagInternal(Flag.ANSWERED, true);
+ } else if (flag.is(ImapConstants.FLAG_SEEN)) {
+ message.setFlagInternal(Flag.SEEN, true);
+ } else if (flag.is(ImapConstants.FLAG_FLAGGED)) {
+ message.setFlagInternal(Flag.FLAGGED, true);
+ }
+ }
+ }
+ if (fp.contains(FetchProfile.Item.ENVELOPE)) {
+ final Date internalDate =
+ fetchList.getKeyedStringOrEmpty(ImapConstants.INTERNALDATE).getDateOrNull();
+ final int size =
+ fetchList.getKeyedStringOrEmpty(ImapConstants.RFC822_SIZE).getNumberOrZero();
+ final String header =
+ fetchList
+ .getKeyedStringOrEmpty(ImapConstants.BODY_BRACKET_HEADER, true)
+ .getString();
+
+ message.setInternalDate(internalDate);
+ message.setSize(size);
+ message.parse(Utility.streamFromAsciiString(header));
+ }
+ if (fp.contains(FetchProfile.Item.STRUCTURE)) {
+ ImapList bs = fetchList.getKeyedListOrEmpty(ImapConstants.BODYSTRUCTURE);
+ if (!bs.isEmpty()) {
+ try {
+ parseBodyStructure(bs, message, ImapConstants.TEXT);
+ } catch (MessagingException e) {
+ LogUtils.v(TAG, e, "Error handling message");
+ message.setBody(null);
+ }
+ }
+ }
+ if (fp.contains(FetchProfile.Item.BODY) || fp.contains(FetchProfile.Item.BODY_SANE)) {
+ // Body is keyed by "BODY[]...".
+ // Previously used "BODY[..." but this can be confused with "BODY[HEADER..."
+ // TODO Should we accept "RFC822" as well??
+ ImapString body = fetchList.getKeyedStringOrEmpty("BODY[]", true);
+ InputStream bodyStream = body.getAsStream();
+ message.parse(bodyStream);
+ }
+ if (fetchPart != null) {
+ InputStream bodyStream = fetchList.getKeyedStringOrEmpty("BODY[", true).getAsStream();
+ String encodings[] = fetchPart.getHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING);
+
+ String contentTransferEncoding = null;
+ if (encodings != null && encodings.length > 0) {
+ contentTransferEncoding = encodings[0];
+ } else {
+ // According to http://tools.ietf.org/html/rfc2045#section-6.1
+ // "7bit" is the default.
+ contentTransferEncoding = "7bit";
+ }
+
+ try {
+ // TODO Don't create 2 temp files.
+ // decodeBody creates BinaryTempFileBody, but we could avoid this
+ // if we implement ImapStringBody.
+ // (We'll need to share a temp file. Protect it with a ref-count.)
+ message.setBody(
+ decodeBody(
+ mStore.getContext(),
+ bodyStream,
+ contentTransferEncoding,
+ fetchPart.getSize(),
+ listener));
+ } catch (Exception e) {
+ // TODO: Figure out what kinds of exceptions might actually be thrown
+ // from here. This blanket catch-all is because we're not sure what to
+ // do if we don't have a contentTransferEncoding, and we don't have
+ // time to figure out what exceptions might be thrown.
+ LogUtils.e(TAG, "Error fetching body %s", e);
+ }
+ }
+
+ if (listener != null) {
+ listener.messageRetrieved(message);
+ }
+ } finally {
+ destroyResponses();
+ }
+ } while (!response.isTagged());
+ } catch (IOException ioe) {
+ mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
+ throw ioExceptionHandler(mConnection, ioe);
+ }
+ }
+
+ /**
+ * Removes any content transfer encoding from the stream and returns a Body. This code is
+ * taken/condensed from MimeUtility.decodeBody
+ */
+ private static Body decodeBody(
+ Context context,
+ InputStream in,
+ String contentTransferEncoding,
+ int size,
+ MessageRetrievalListener listener)
+ throws IOException {
+ // Get a properly wrapped input stream
+ in = MimeUtility.getInputStreamForContentTransferEncoding(in, contentTransferEncoding);
+ BinaryTempFileBody tempBody = new BinaryTempFileBody();
+ OutputStream out = tempBody.getOutputStream();
+ try {
+ byte[] buffer = new byte[COPY_BUFFER_SIZE];
+ int n = 0;
+ int count = 0;
+ while (-1 != (n = in.read(buffer))) {
+ out.write(buffer, 0, n);
+ count += n;
+ }
+ } catch (Base64DataException bde) {
+ String warning = "\n\nThere was an error while decoding the message.";
+ out.write(warning.getBytes());
+ } finally {
+ out.close();
+ }
+ return tempBody;
+ }
+
+ public String[] getPermanentFlags() {
+ return PERMANENT_FLAGS;
+ }
+
+ /**
+ * Handle any untagged responses that the caller doesn't care to handle themselves.
+ *
+ * @param responses
+ */
+ private void handleUntaggedResponses(List<ImapResponse> responses) {
+ for (ImapResponse response : responses) {
+ handleUntaggedResponse(response);
+ }
+ }
+
+ /**
+ * Handle an untagged response that the caller doesn't care to handle themselves.
+ *
+ * @param response
+ */
+ private void handleUntaggedResponse(ImapResponse response) {
+ if (response.isDataResponse(1, ImapConstants.EXISTS)) {
+ mMessageCount = response.getStringOrEmpty(0).getNumberOrZero();
+ }
+ }
+
+ private static void parseBodyStructure(ImapList bs, Part part, String id)
+ throws MessagingException {
+ if (bs.getElementOrNone(0).isList()) {
+ /*
+ * This is a multipart/*
+ */
+ MimeMultipart mp = new MimeMultipart();
+ for (int i = 0, count = bs.size(); i < count; i++) {
+ ImapElement e = bs.getElementOrNone(i);
+ if (e.isList()) {
+ /*
+ * For each part in the message we're going to add a new BodyPart and parse
+ * into it.
+ */
+ MimeBodyPart bp = new MimeBodyPart();
+ if (id.equals(ImapConstants.TEXT)) {
+ parseBodyStructure(bs.getListOrEmpty(i), bp, Integer.toString(i + 1));
+
+ } else {
+ parseBodyStructure(bs.getListOrEmpty(i), bp, id + "." + (i + 1));
+ }
+ mp.addBodyPart(bp);
+
+ } else {
+ if (e.isString()) {
+ mp.setSubType(bs.getStringOrEmpty(i).getString().toLowerCase(Locale.US));
+ }
+ break; // Ignore the rest of the list.
+ }
+ }
+ part.setBody(mp);
+ } else {
+ /*
+ * This is a body. We need to add as much information as we can find out about
+ * it to the Part.
+ */
+
+ /*
+ body type
+ body subtype
+ body parameter parenthesized list
+ body id
+ body description
+ body encoding
+ body size
+ */
+
+ final ImapString type = bs.getStringOrEmpty(0);
+ final ImapString subType = bs.getStringOrEmpty(1);
+ final String mimeType = (type.getString() + "/" + subType.getString()).toLowerCase(Locale.US);
+
+ final ImapList bodyParams = bs.getListOrEmpty(2);
+ final ImapString cid = bs.getStringOrEmpty(3);
+ final ImapString encoding = bs.getStringOrEmpty(5);
+ final int size = bs.getStringOrEmpty(6).getNumberOrZero();
+
+ if (MimeUtility.mimeTypeMatches(mimeType, MimeUtility.MIME_TYPE_RFC822)) {
+ // A body type of type MESSAGE and subtype RFC822
+ // contains, immediately after the basic fields, the
+ // envelope structure, body structure, and size in
+ // text lines of the encapsulated message.
+ // [MESSAGE, RFC822, [NAME, filename.eml], NIL, NIL, 7BIT, 5974, NIL,
+ // [INLINE, [FILENAME*0, Fwd: Xxx..., FILENAME*1, filename.eml]], NIL]
+ /*
+ * This will be caught by fetch and handled appropriately.
+ */
+ throw new MessagingException(
+ "BODYSTRUCTURE " + MimeUtility.MIME_TYPE_RFC822 + " not yet supported.");
+ }
+
+ /*
+ * Set the content type with as much information as we know right now.
+ */
+ final StringBuilder contentType = new StringBuilder(mimeType);
+
+ /*
+ * If there are body params we might be able to get some more information out
+ * of them.
+ */
+ for (int i = 1, count = bodyParams.size(); i < count; i += 2) {
+
+ // TODO We need to convert " into %22, but
+ // because MimeUtility.getHeaderParameter doesn't recognize it,
+ // we can't fix it for now.
+ contentType.append(
+ String.format(
+ ";\n %s=\"%s\"",
+ bodyParams.getStringOrEmpty(i - 1).getString(),
+ bodyParams.getStringOrEmpty(i).getString()));
+ }
+
+ part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType.toString());
+
+ // Extension items
+ final ImapList bodyDisposition;
+
+ if (type.is(ImapConstants.TEXT) && bs.getElementOrNone(9).isList()) {
+ // If media-type is TEXT, 9th element might be: [body-fld-lines] := number
+ // So, if it's not a list, use 10th element.
+ // (Couldn't find evidence in the RFC if it's ALWAYS 10th element.)
+ bodyDisposition = bs.getListOrEmpty(9);
+ } else {
+ bodyDisposition = bs.getListOrEmpty(8);
+ }
+
+ final StringBuilder contentDisposition = new StringBuilder();
+
+ if (bodyDisposition.size() > 0) {
+ final String bodyDisposition0Str =
+ bodyDisposition.getStringOrEmpty(0).getString().toLowerCase(Locale.US);
+ if (!TextUtils.isEmpty(bodyDisposition0Str)) {
+ contentDisposition.append(bodyDisposition0Str);
+ }
+
+ final ImapList bodyDispositionParams = bodyDisposition.getListOrEmpty(1);
+ if (!bodyDispositionParams.isEmpty()) {
+ /*
+ * If there is body disposition information we can pull some more
+ * information about the attachment out.
+ */
+ for (int i = 1, count = bodyDispositionParams.size(); i < count; i += 2) {
+
+ // TODO We need to convert " into %22. See above.
+ contentDisposition.append(
+ String.format(
+ Locale.US,
+ ";\n %s=\"%s\"",
+ bodyDispositionParams
+ .getStringOrEmpty(i - 1)
+ .getString()
+ .toLowerCase(Locale.US),
+ bodyDispositionParams.getStringOrEmpty(i).getString()));
+ }
+ }
+ }
+
+ if ((size > 0)
+ && (MimeUtility.getHeaderParameter(contentDisposition.toString(), "size") == null)) {
+ contentDisposition.append(String.format(Locale.US, ";\n size=%d", size));
+ }
+
+ if (contentDisposition.length() > 0) {
+ /*
+ * Set the content disposition containing at least the size. Attachment
+ * handling code will use this down the road.
+ */
+ part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, contentDisposition.toString());
+ }
+
+ /*
+ * Set the Content-Transfer-Encoding header. Attachment code will use this
+ * to parse the body.
+ */
+ if (!encoding.isEmpty()) {
+ part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, encoding.getString());
+ }
+
+ /*
+ * Set the Content-ID header.
+ */
+ if (!cid.isEmpty()) {
+ part.setHeader(MimeHeader.HEADER_CONTENT_ID, cid.getString());
+ }
+
+ if (size > 0) {
+ if (part instanceof ImapMessage) {
+ ((ImapMessage) part).setSize(size);
+ } else if (part instanceof MimeBodyPart) {
+ ((MimeBodyPart) part).setSize(size);
+ } else {
+ throw new MessagingException("Unknown part type " + part.toString());
+ }
+ }
+ part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id);
+ }
+ }
+
+ public Message[] expunge() throws MessagingException {
+ checkOpen();
+ try {
+ handleUntaggedResponses(mConnection.executeSimpleCommand(ImapConstants.EXPUNGE));
+ } catch (IOException ioe) {
+ mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
+ throw ioExceptionHandler(mConnection, ioe);
+ } finally {
+ destroyResponses();
+ }
+ return null;
+ }
+
+ public void setFlags(Message[] messages, String[] flags, boolean value)
+ throws MessagingException {
+ checkOpen();
+
+ String allFlags = "";
+ if (flags.length > 0) {
+ StringBuilder flagList = new StringBuilder();
+ for (int i = 0, count = flags.length; i < count; i++) {
+ String flag = flags[i];
+ if (flag == Flag.SEEN) {
+ flagList.append(" " + ImapConstants.FLAG_SEEN);
+ } else if (flag == Flag.DELETED) {
+ flagList.append(" " + ImapConstants.FLAG_DELETED);
+ } else if (flag == Flag.FLAGGED) {
+ flagList.append(" " + ImapConstants.FLAG_FLAGGED);
+ } else if (flag == Flag.ANSWERED) {
+ flagList.append(" " + ImapConstants.FLAG_ANSWERED);
+ }
+ }
+ allFlags = flagList.substring(1);
+ }
+ try {
+ mConnection.executeSimpleCommand(
+ String.format(
+ Locale.US,
+ ImapConstants.UID_STORE + " %s %s" + ImapConstants.FLAGS_SILENT + " (%s)",
+ ImapStore.joinMessageUids(messages),
+ value ? "+" : "-",
+ allFlags));
+
+ } catch (IOException ioe) {
+ mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
+ throw ioExceptionHandler(mConnection, ioe);
+ } finally {
+ destroyResponses();
+ }
+ }
+
+ /**
+ * Selects the folder for use. Before performing any operations on this folder, it must be
+ * selected.
+ */
+ private void doSelect() throws IOException, MessagingException {
+ final List<ImapResponse> responses =
+ mConnection.executeSimpleCommand(
+ String.format(Locale.US, ImapConstants.SELECT + " \"%s\"", mName));
+
+ // Assume the folder is opened read-write; unless we are notified otherwise
+ mMode = MODE_READ_WRITE;
+ int messageCount = -1;
+ for (ImapResponse response : responses) {
+ if (response.isDataResponse(1, ImapConstants.EXISTS)) {
+ messageCount = response.getStringOrEmpty(0).getNumberOrZero();
+ } else if (response.isOk()) {
+ final ImapString responseCode = response.getResponseCodeOrEmpty();
+ if (responseCode.is(ImapConstants.READ_ONLY)) {
+ mMode = MODE_READ_ONLY;
+ } else if (responseCode.is(ImapConstants.READ_WRITE)) {
+ mMode = MODE_READ_WRITE;
+ }
+ } else if (response.isTagged()) { // Not OK
+ mStore.getImapHelper().handleEvent(OmtpEvents.DATA_MAILBOX_OPEN_FAILED);
+ throw new MessagingException(
+ "Can't open mailbox: " + response.getStatusResponseTextOrEmpty());
+ }
+ }
+ if (messageCount == -1) {
+ throw new MessagingException("Did not find message count during select");
+ }
+ mMessageCount = messageCount;
+ mExists = true;
+ }
+
+ public class Quota {
+
+ public final int occupied;
+ public final int total;
+
+ public Quota(int occupied, int total) {
+ this.occupied = occupied;
+ this.total = total;
+ }
+ }
+
+ public Quota getQuota() throws MessagingException {
+ try {
+ final List<ImapResponse> responses =
+ mConnection.executeSimpleCommand(
+ String.format(Locale.US, ImapConstants.GETQUOTAROOT + " \"%s\"", mName));
+
+ for (ImapResponse response : responses) {
+ if (!response.isDataResponse(0, ImapConstants.QUOTA)) {
+ continue;
+ }
+ ImapList list = response.getListOrEmpty(2);
+ for (int i = 0; i < list.size(); i += 3) {
+ if (!list.getStringOrEmpty(i).is("voice")) {
+ continue;
+ }
+ return new Quota(
+ list.getStringOrEmpty(i + 1).getNumber(-1),
+ list.getStringOrEmpty(i + 2).getNumber(-1));
+ }
+ }
+ } catch (IOException ioe) {
+ mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
+ throw ioExceptionHandler(mConnection, ioe);
+ } finally {
+ destroyResponses();
+ }
+ return null;
+ }
+
+ private void checkOpen() throws MessagingException {
+ if (!isOpen()) {
+ throw new MessagingException("Folder " + mName + " is not open.");
+ }
+ }
+
+ private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) {
+ LogUtils.d(TAG, "IO Exception detected: ", ioe);
+ connection.close();
+ if (connection == mConnection) {
+ mConnection = null; // To prevent close() from returning the connection to the pool.
+ close(false);
+ }
+ return new MessagingException(MessagingException.IOERROR, "IO Error", ioe);
+ }
+
+ public Message createMessage(String uid) {
+ return new ImapMessage(uid, this);
+ }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/ImapStore.java b/java/com/android/voicemail/impl/mail/store/ImapStore.java
new file mode 100644
index 000000000..cadbe593f
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/ImapStore.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl.mail.store;
+
+import android.content.Context;
+import android.net.Network;
+import com.android.voicemail.impl.imap.ImapHelper;
+import com.android.voicemail.impl.mail.MailTransport;
+import com.android.voicemail.impl.mail.Message;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.internet.MimeMessage;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class ImapStore {
+ /**
+ * A global suggestion to Store implementors on how much of the body should be returned on
+ * FetchProfile.Item.BODY_SANE requests. We'll use 125k now.
+ */
+ public static final int FETCH_BODY_SANE_SUGGESTED_SIZE = (125 * 1024);
+
+ private final Context mContext;
+ private final ImapHelper mHelper;
+ private final String mUsername;
+ private final String mPassword;
+ private final MailTransport mTransport;
+ private ImapConnection mConnection;
+
+ public static final int FLAG_NONE = 0x00; // No flags
+ public static final int FLAG_SSL = 0x01; // Use SSL
+ public static final int FLAG_TLS = 0x02; // Use TLS
+ public static final int FLAG_AUTHENTICATE = 0x04; // Use name/password for authentication
+ public static final int FLAG_TRUST_ALL = 0x08; // Trust all certificates
+ public static final int FLAG_OAUTH = 0x10; // Use OAuth for authentication
+
+ /** Contains all the information necessary to log into an imap server */
+ public ImapStore(
+ Context context,
+ ImapHelper helper,
+ String username,
+ String password,
+ int port,
+ String serverName,
+ int flags,
+ Network network) {
+ mContext = context;
+ mHelper = helper;
+ mUsername = username;
+ mPassword = password;
+ mTransport = new MailTransport(context, this.getImapHelper(), network, serverName, port, flags);
+ }
+
+ public Context getContext() {
+ return mContext;
+ }
+
+ public ImapHelper getImapHelper() {
+ return mHelper;
+ }
+
+ public String getUsername() {
+ return mUsername;
+ }
+
+ public String getPassword() {
+ return mPassword;
+ }
+
+ /** Returns a clone of the transport associated with this store. */
+ MailTransport cloneTransport() {
+ return mTransport.clone();
+ }
+
+ /** Returns UIDs of Messages joined with "," as the separator. */
+ static String joinMessageUids(Message[] messages) {
+ StringBuilder sb = new StringBuilder();
+ boolean notFirst = false;
+ for (Message m : messages) {
+ if (notFirst) {
+ sb.append(',');
+ }
+ sb.append(m.getUid());
+ notFirst = true;
+ }
+ return sb.toString();
+ }
+
+ static class ImapMessage extends MimeMessage {
+ private ImapFolder mFolder;
+
+ ImapMessage(String uid, ImapFolder folder) {
+ mUid = uid;
+ mFolder = folder;
+ }
+
+ public void setSize(int size) {
+ mSize = size;
+ }
+
+ @Override
+ public void parse(InputStream in) throws IOException, MessagingException {
+ super.parse(in);
+ }
+
+ public void setFlagInternal(String flag, boolean set) throws MessagingException {
+ super.setFlag(flag, set);
+ }
+
+ @Override
+ public void setFlag(String flag, boolean set) throws MessagingException {
+ super.setFlag(flag, set);
+ mFolder.setFlags(new Message[] {this}, new String[] {flag}, set);
+ }
+ }
+
+ static class ImapException extends MessagingException {
+ private static final long serialVersionUID = 1L;
+
+ private final String mStatus;
+ private final String mStatusMessage;
+ private final String mAlertText;
+ private final String mResponseCode;
+
+ public ImapException(
+ String message,
+ String status,
+ String statusMessage,
+ String alertText,
+ String responseCode) {
+ super(message);
+ mStatus = status;
+ mStatusMessage = statusMessage;
+ mAlertText = alertText;
+ mResponseCode = responseCode;
+ }
+
+ public String getStatus() {
+ return mStatus;
+ }
+
+ public String getStatusMessage() {
+ return mStatusMessage;
+ }
+
+ public String getAlertText() {
+ return mAlertText;
+ }
+
+ public String getResponseCode() {
+ return mResponseCode;
+ }
+ }
+
+ public void closeConnection() {
+ if (mConnection != null) {
+ mConnection.close();
+ mConnection = null;
+ }
+ }
+
+ public ImapConnection getConnection() {
+ if (mConnection == null) {
+ mConnection = new ImapConnection(this);
+ }
+ return mConnection;
+ }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/imap/DigestMd5Utils.java b/java/com/android/voicemail/impl/mail/store/imap/DigestMd5Utils.java
new file mode 100644
index 000000000..f156f67c1
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/DigestMd5Utils.java
@@ -0,0 +1,335 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl.mail.store.imap;
+
+import android.annotation.TargetApi;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.util.ArrayMap;
+import android.util.Base64;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.mail.MailTransport;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.store.ImapStore;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Map;
+
+@SuppressWarnings("AndroidApiChecker") // Map.getOrDefault() is java8
+@TargetApi(VERSION_CODES.O)
+public class DigestMd5Utils {
+
+ private static final String TAG = "DigestMd5Utils";
+
+ private static final String DIGEST_CHARSET = "CHARSET";
+ private static final String DIGEST_USERNAME = "username";
+ private static final String DIGEST_REALM = "realm";
+ private static final String DIGEST_NONCE = "nonce";
+ private static final String DIGEST_NC = "nc";
+ private static final String DIGEST_CNONCE = "cnonce";
+ private static final String DIGEST_URI = "digest-uri";
+ private static final String DIGEST_RESPONSE = "response";
+ private static final String DIGEST_QOP = "qop";
+
+ private static final String RESPONSE_AUTH_HEADER = "rspauth=";
+ private static final String HEX_CHARS = "0123456789abcdef";
+
+ /** Represents the set of data we need to generate the DIGEST-MD5 response. */
+ public static class Data {
+
+ private static final String CHARSET = "utf-8";
+
+ public String username;
+ public String password;
+ public String realm;
+ public String nonce;
+ public String nc;
+ public String cnonce;
+ public String digestUri;
+ public String qop;
+
+ @VisibleForTesting
+ Data() {
+ // Do nothing
+ }
+
+ public Data(ImapStore imapStore, MailTransport transport, Map<String, String> challenge) {
+ username = imapStore.getUsername();
+ password = imapStore.getPassword();
+ realm = challenge.getOrDefault(DIGEST_REALM, "");
+ nonce = challenge.get(DIGEST_NONCE);
+ cnonce = createCnonce();
+ nc = "00000001"; // Subsequent Authentication not supported, nounce count always 1.
+ qop = "auth"; // Other config not supported
+ digestUri = "imap/" + transport.getHost();
+ }
+
+ private static String createCnonce() {
+ SecureRandom generator = new SecureRandom();
+
+ // At least 64 bits of entropy is required
+ byte[] rawBytes = new byte[8];
+ generator.nextBytes(rawBytes);
+
+ return Base64.encodeToString(rawBytes, Base64.NO_WRAP);
+ }
+
+ /** Verify the response-auth returned by the server is correct. */
+ public void verifyResponseAuth(String response) throws MessagingException {
+ if (!response.startsWith(RESPONSE_AUTH_HEADER)) {
+ throw new MessagingException("response-auth expected");
+ }
+ if (!response
+ .substring(RESPONSE_AUTH_HEADER.length())
+ .equals(DigestMd5Utils.getResponse(this, true))) {
+ throw new MessagingException("invalid response-auth return from the server.");
+ }
+ }
+
+ public String createResponse() {
+ String response = getResponse(this, false);
+ ResponseBuilder builder = new ResponseBuilder();
+ builder
+ .append(DIGEST_CHARSET, CHARSET)
+ .appendQuoted(DIGEST_USERNAME, username)
+ .appendQuoted(DIGEST_REALM, realm)
+ .appendQuoted(DIGEST_NONCE, nonce)
+ .append(DIGEST_NC, nc)
+ .appendQuoted(DIGEST_CNONCE, cnonce)
+ .appendQuoted(DIGEST_URI, digestUri)
+ .append(DIGEST_RESPONSE, response)
+ .append(DIGEST_QOP, qop);
+ return builder.toString();
+ }
+
+ private static class ResponseBuilder {
+
+ private StringBuilder mBuilder = new StringBuilder();
+
+ public ResponseBuilder appendQuoted(String key, String value) {
+ if (mBuilder.length() != 0) {
+ mBuilder.append(",");
+ }
+ mBuilder.append(key).append("=\"").append(value).append("\"");
+ return this;
+ }
+
+ public ResponseBuilder append(String key, String value) {
+ if (mBuilder.length() != 0) {
+ mBuilder.append(",");
+ }
+ mBuilder.append(key).append("=").append(value);
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return mBuilder.toString();
+ }
+ }
+ }
+
+ /*
+ response-value =
+ toHex( getKeyDigest ( toHex(getMd5(a1)),
+ { nonce-value, ":" nc-value, ":",
+ cnonce-value, ":", qop-value, ":", toHex(getMd5(a2)) }))
+ * @param isResponseAuth is the response the one the server is returning us. response-auth has
+ * different a2 format.
+ */
+ @VisibleForTesting
+ static String getResponse(Data data, boolean isResponseAuth) {
+ StringBuilder a1 = new StringBuilder();
+ a1.append(
+ new String(
+ getMd5(data.username + ":" + data.realm + ":" + data.password),
+ StandardCharsets.ISO_8859_1));
+ a1.append(":").append(data.nonce).append(":").append(data.cnonce);
+
+ StringBuilder a2 = new StringBuilder();
+ if (!isResponseAuth) {
+ a2.append("AUTHENTICATE");
+ }
+ a2.append(":").append(data.digestUri);
+
+ return toHex(
+ getKeyDigest(
+ toHex(getMd5(a1.toString())),
+ data.nonce
+ + ":"
+ + data.nc
+ + ":"
+ + data.cnonce
+ + ":"
+ + data.qop
+ + ":"
+ + toHex(getMd5(a2.toString()))));
+ }
+
+ /** Let getMd5(s) be the 16 octet MD5 hash [RFC 1321] of the octet string s. */
+ private static byte[] getMd5(String s) {
+ try {
+ MessageDigest digester = MessageDigest.getInstance("MD5");
+ digester.update(s.getBytes(StandardCharsets.ISO_8859_1));
+ return digester.digest();
+ } catch (NoSuchAlgorithmException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ /**
+ * Let getKeyDigest(k, s) be getMd5({k, ":", s}), i.e., the 16 octet hash of the string k, a colon
+ * and the string s.
+ */
+ private static byte[] getKeyDigest(String k, String s) {
+ StringBuilder builder = new StringBuilder(k).append(":").append(s);
+ return getMd5(builder.toString());
+ }
+
+ /**
+ * Let toHex(n) be the representation of the 16 octet MD5 hash n as a string of 32 hex digits
+ * (with alphabetic characters always in lower case, since MD5 is case sensitive).
+ */
+ private static String toHex(byte[] n) {
+ StringBuilder result = new StringBuilder();
+ for (byte b : n) {
+ int unsignedByte = b & 0xFF;
+ result
+ .append(HEX_CHARS.charAt(unsignedByte / 16))
+ .append(HEX_CHARS.charAt(unsignedByte % 16));
+ }
+ return result.toString();
+ }
+
+ public static Map<String, String> parseDigestMessage(String message) throws MessagingException {
+ Map<String, String> result = new DigestMessageParser(message).parse();
+ if (!result.containsKey(DIGEST_NONCE)) {
+ throw new MessagingException("nonce missing from server DIGEST-MD5 challenge");
+ }
+ return result;
+ }
+
+ /** Parse the key-value pair returned by the server. */
+ private static class DigestMessageParser {
+
+ private final String mMessage;
+ private int mPosition = 0;
+ private Map<String, String> mResult = new ArrayMap<>();
+
+ public DigestMessageParser(String message) {
+ mMessage = message;
+ }
+
+ @Nullable
+ public Map<String, String> parse() {
+ try {
+ while (mPosition < mMessage.length()) {
+ parsePair();
+ if (mPosition != mMessage.length()) {
+ expect(',');
+ }
+ }
+ } catch (IndexOutOfBoundsException e) {
+ VvmLog.e(TAG, e.toString());
+ return null;
+ }
+ return mResult;
+ }
+
+ private void parsePair() {
+ String key = parseKey();
+ expect('=');
+ String value = parseValue();
+ mResult.put(key, value);
+ }
+
+ private void expect(char c) {
+ if (pop() != c) {
+ throw new IllegalStateException("unexpected character " + mMessage.charAt(mPosition));
+ }
+ }
+
+ private char pop() {
+ char result = peek();
+ mPosition++;
+ return result;
+ }
+
+ private char peek() {
+ return mMessage.charAt(mPosition);
+ }
+
+ private void goToNext(char c) {
+ while (peek() != c) {
+ mPosition++;
+ }
+ }
+
+ private String parseKey() {
+ int start = mPosition;
+ goToNext('=');
+ return mMessage.substring(start, mPosition);
+ }
+
+ private String parseValue() {
+ if (peek() == '"') {
+ return parseQuotedValue();
+ } else {
+ return parseUnquotedValue();
+ }
+ }
+
+ private String parseQuotedValue() {
+ expect('"');
+ StringBuilder result = new StringBuilder();
+ while (true) {
+ char c = pop();
+ if (c == '\\') {
+ result.append(pop());
+ } else if (c == '"') {
+ break;
+ } else {
+ result.append(c);
+ }
+ }
+ return result.toString();
+ }
+
+ private String parseUnquotedValue() {
+ StringBuilder result = new StringBuilder();
+ while (true) {
+ char c = pop();
+ if (c == '\\') {
+ result.append(pop());
+ } else if (c == ',') {
+ mPosition--;
+ break;
+ } else {
+ result.append(c);
+ }
+
+ if (mPosition == mMessage.length()) {
+ break;
+ }
+ }
+ return result.toString();
+ }
+ }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/imap/ImapConstants.java b/java/com/android/voicemail/impl/mail/store/imap/ImapConstants.java
new file mode 100644
index 000000000..88ec0ed90
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/ImapConstants.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl.mail.store.imap;
+
+import com.android.voicemail.impl.mail.store.ImapStore;
+import java.util.Locale;
+
+public final class ImapConstants {
+ private ImapConstants() {}
+
+ public static final String FETCH_FIELD_BODY_PEEK_BARE = "BODY.PEEK";
+ public static final String FETCH_FIELD_BODY_PEEK = FETCH_FIELD_BODY_PEEK_BARE + "[]";
+ public static final String FETCH_FIELD_BODY_PEEK_SANE =
+ String.format(Locale.US, "BODY.PEEK[]<0.%d>", ImapStore.FETCH_BODY_SANE_SUGGESTED_SIZE);
+ public static final String FETCH_FIELD_HEADERS =
+ "BODY.PEEK[HEADER.FIELDS (date subject from content-type to cc message-id)]";
+
+ public static final String ALERT = "ALERT";
+ public static final String APPEND = "APPEND";
+ public static final String AUTHENTICATE = "AUTHENTICATE";
+ public static final String BAD = "BAD";
+ public static final String BADCHARSET = "BADCHARSET";
+ public static final String BODY = "BODY";
+ public static final String BODY_BRACKET_HEADER = "BODY[HEADER";
+ public static final String BODYSTRUCTURE = "BODYSTRUCTURE";
+ public static final String BYE = "BYE";
+ public static final String CAPABILITY = "CAPABILITY";
+ public static final String CHECK = "CHECK";
+ public static final String CLOSE = "CLOSE";
+ public static final String COPY = "COPY";
+ public static final String COPYUID = "COPYUID";
+ public static final String CREATE = "CREATE";
+ public static final String DELETE = "DELETE";
+ public static final String EXAMINE = "EXAMINE";
+ public static final String EXISTS = "EXISTS";
+ public static final String EXPUNGE = "EXPUNGE";
+ public static final String FETCH = "FETCH";
+ public static final String FLAG_ANSWERED = "\\ANSWERED";
+ public static final String FLAG_DELETED = "\\DELETED";
+ public static final String FLAG_FLAGGED = "\\FLAGGED";
+ public static final String FLAG_NO_SELECT = "\\NOSELECT";
+ public static final String FLAG_SEEN = "\\SEEN";
+ public static final String FLAGS = "FLAGS";
+ public static final String FLAGS_SILENT = "FLAGS.SILENT";
+ public static final String ID = "ID";
+ public static final String INBOX = "INBOX";
+ public static final String INTERNALDATE = "INTERNALDATE";
+ public static final String LIST = "LIST";
+ public static final String LOGIN = "LOGIN";
+ public static final String LOGOUT = "LOGOUT";
+ public static final String LSUB = "LSUB";
+ public static final String NAMESPACE = "NAMESPACE";
+ public static final String NO = "NO";
+ public static final String NOOP = "NOOP";
+ public static final String OK = "OK";
+ public static final String PARSE = "PARSE";
+ public static final String PERMANENTFLAGS = "PERMANENTFLAGS";
+ public static final String PREAUTH = "PREAUTH";
+ public static final String READ_ONLY = "READ-ONLY";
+ public static final String READ_WRITE = "READ-WRITE";
+ public static final String RENAME = "RENAME";
+ public static final String RFC822_SIZE = "RFC822.SIZE";
+ public static final String SEARCH = "SEARCH";
+ public static final String SELECT = "SELECT";
+ public static final String STARTTLS = "STARTTLS";
+ public static final String STATUS = "STATUS";
+ public static final String STORE = "STORE";
+ public static final String SUBSCRIBE = "SUBSCRIBE";
+ public static final String TEXT = "TEXT";
+ public static final String TRYCREATE = "TRYCREATE";
+ public static final String UID = "UID";
+ public static final String UID_COPY = "UID COPY";
+ public static final String UID_FETCH = "UID FETCH";
+ public static final String UID_SEARCH = "UID SEARCH";
+ public static final String UID_STORE = "UID STORE";
+ public static final String UIDNEXT = "UIDNEXT";
+ public static final String UIDPLUS = "UIDPLUS";
+ public static final String UIDVALIDITY = "UIDVALIDITY";
+ public static final String UNSEEN = "UNSEEN";
+ public static final String UNSUBSCRIBE = "UNSUBSCRIBE";
+ public static final String XOAUTH2 = "XOAUTH2";
+ public static final String APPENDUID = "APPENDUID";
+ public static final String NIL = "NIL";
+
+ /** NO responses */
+ public static final String NO_COMMAND_NOT_ALLOWED = "command not allowed";
+
+ public static final String NO_RESERVATION_FAILED = "reservation failed";
+ public static final String NO_APPLICATION_ERROR = "application error";
+ public static final String NO_INVALID_PARAMETER = "invalid parameter";
+ public static final String NO_INVALID_COMMAND = "invalid command";
+ public static final String NO_UNKNOWN_COMMAND = "unknown command";
+ // AUTHENTICATE
+ // The subscriber can not be located in the system.
+ public static final String NO_UNKNOWN_USER = "unknown user";
+ // The Client Type or Protocol Version is unknown.
+ public static final String NO_UNKNOWN_CLIENT = "unknown client";
+ // The password received from the client does not match the password defined in the subscriber's
+ // profile.
+ public static final String NO_INVALID_PASSWORD = "invalid password";
+ // The subscriber's mailbox has not yet been initialised via the TUI
+ public static final String NO_MAILBOX_NOT_INITIALIZED = "mailbox not initialized";
+ // The subscriber has not been provisioned for the VVM service.
+ public static final String NO_SERVICE_IS_NOT_PROVISIONED = "service is not provisioned";
+ // The subscriber is provisioned for the VVM service but the VVM service is currently not active
+ public static final String NO_SERVICE_IS_NOT_ACTIVATED = "service is not activated";
+ // The Voice Mail Blocked flag in the subscriber's profile is set to YES.
+ public static final String NO_USER_IS_BLOCKED = "user is blocked";
+
+ /** extensions */
+ public static final String GETQUOTA = "GETQUOTA";
+
+ public static final String GETQUOTAROOT = "GETQUOTAROOT";
+ public static final String QUOTAROOT = "QUOTAROOT";
+ public static final String QUOTA = "QUOTA";
+
+ /** capabilities */
+ public static final String CAPABILITY_AUTH_DIGEST_MD5 = "AUTH=DIGEST-MD5";
+
+ public static final String CAPABILITY_STARTTLS = "STARTTLS";
+
+ /** authentication */
+ public static final String AUTH_DIGEST_MD5 = "DIGEST-MD5";
+}
diff --git a/java/com/android/voicemail/impl/mail/store/imap/ImapElement.java b/java/com/android/voicemail/impl/mail/store/imap/ImapElement.java
new file mode 100644
index 000000000..ee255d1eb
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/ImapElement.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl.mail.store.imap;
+
+/**
+ * Class representing "element"s in IMAP responses.
+ *
+ * <p>Class hierarchy:
+ *
+ * <pre>
+ * ImapElement
+ * |
+ * |-- ImapElement.NONE (for 'index out of range')
+ * |
+ * |-- ImapList (isList() == true)
+ * | |
+ * | |-- ImapList.EMPTY
+ * | |
+ * | --- ImapResponse
+ * |
+ * --- ImapString (isString() == true)
+ * |
+ * |-- ImapString.EMPTY
+ * |
+ * |-- ImapSimpleString
+ * |
+ * |-- ImapMemoryLiteral
+ * |
+ * --- ImapTempFileLiteral
+ * </pre>
+ */
+public abstract class ImapElement {
+ /**
+ * An element that is returned by {@link ImapList#getElementOrNone} to indicate an index is out of
+ * range.
+ */
+ public static final ImapElement NONE =
+ new ImapElement() {
+ @Override
+ public void destroy() {
+ // Don't call super.destroy().
+ // It's a shared object. We don't want the mDestroyed to be set on this.
+ }
+
+ @Override
+ public boolean isList() {
+ return false;
+ }
+
+ @Override
+ public boolean isString() {
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "[NO ELEMENT]";
+ }
+
+ @Override
+ public boolean equalsForTest(ImapElement that) {
+ return super.equalsForTest(that);
+ }
+ };
+
+ private boolean mDestroyed = false;
+
+ public abstract boolean isList();
+
+ public abstract boolean isString();
+
+ protected boolean isDestroyed() {
+ return mDestroyed;
+ }
+
+ /**
+ * Clean up the resources used by the instance. It's for removing a temp file used by {@link
+ * ImapTempFileLiteral}.
+ */
+ public void destroy() {
+ mDestroyed = true;
+ }
+
+ /** Throws {@link RuntimeException} if it's already destroyed. */
+ protected final void checkNotDestroyed() {
+ if (mDestroyed) {
+ throw new RuntimeException("Already destroyed");
+ }
+ }
+
+ /**
+ * Return a string that represents this object; it's purely for the debug purpose. Don't mistake
+ * it for {@link ImapString#getString}.
+ *
+ * <p>Abstract to force subclasses to implement it.
+ */
+ @Override
+ public abstract String toString();
+
+ /**
+ * The equals implementation that is intended to be used only for unit testing. (Because it may be
+ * heavy and has a special sense of "equal" for testing.)
+ */
+ public boolean equalsForTest(ImapElement that) {
+ if (that == null) {
+ return false;
+ }
+ return this.getClass() == that.getClass(); // Has to be the same class.
+ }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/imap/ImapList.java b/java/com/android/voicemail/impl/mail/store/imap/ImapList.java
new file mode 100644
index 000000000..e4a6ec0ac
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/ImapList.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl.mail.store.imap;
+
+import java.util.ArrayList;
+
+/** Class represents an IMAP list. */
+public class ImapList extends ImapElement {
+ /** {@link ImapList} representing an empty list. */
+ public static final ImapList EMPTY =
+ new ImapList() {
+ @Override
+ public void destroy() {
+ // Don't call super.destroy().
+ // It's a shared object. We don't want the mDestroyed to be set on this.
+ }
+
+ @Override
+ void add(ImapElement e) {
+ throw new RuntimeException();
+ }
+ };
+
+ private ArrayList<ImapElement> mList = new ArrayList<ImapElement>();
+
+ /* package */ void add(ImapElement e) {
+ if (e == null) {
+ throw new RuntimeException("Can't add null");
+ }
+ mList.add(e);
+ }
+
+ @Override
+ public final boolean isString() {
+ return false;
+ }
+
+ @Override
+ public final boolean isList() {
+ return true;
+ }
+
+ public final int size() {
+ return mList.size();
+ }
+
+ public final boolean isEmpty() {
+ return size() == 0;
+ }
+
+ /**
+ * Return true if the element at {@code index} exists, is string, and equals to {@code s}. (case
+ * insensitive)
+ */
+ public final boolean is(int index, String s) {
+ return is(index, s, false);
+ }
+
+ /** Same as {@link #is(int, String)}, but does the prefix match if {@code prefixMatch}. */
+ public final boolean is(int index, String s, boolean prefixMatch) {
+ if (!prefixMatch) {
+ return getStringOrEmpty(index).is(s);
+ } else {
+ return getStringOrEmpty(index).startsWith(s);
+ }
+ }
+
+ /**
+ * Return the element at {@code index}. If {@code index} is out of range, returns {@link
+ * ImapElement#NONE}.
+ */
+ public final ImapElement getElementOrNone(int index) {
+ return (index >= mList.size()) ? ImapElement.NONE : mList.get(index);
+ }
+
+ /**
+ * Return the element at {@code index} if it's a list. If {@code index} is out of range or not a
+ * list, returns {@link ImapList#EMPTY}.
+ */
+ public final ImapList getListOrEmpty(int index) {
+ ImapElement el = getElementOrNone(index);
+ return el.isList() ? (ImapList) el : EMPTY;
+ }
+
+ /**
+ * Return the element at {@code index} if it's a string. If {@code index} is out of range or not a
+ * string, returns {@link ImapString#EMPTY}.
+ */
+ public final ImapString getStringOrEmpty(int index) {
+ ImapElement el = getElementOrNone(index);
+ return el.isString() ? (ImapString) el : ImapString.EMPTY;
+ }
+
+ /**
+ * Return an element keyed by {@code key}. Return null if not found. {@code key} has to be at an
+ * even index.
+ */
+ /* package */ final ImapElement getKeyedElementOrNull(String key, boolean prefixMatch) {
+ for (int i = 1; i < size(); i += 2) {
+ if (is(i - 1, key, prefixMatch)) {
+ return mList.get(i);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Return an {@link ImapList} keyed by {@code key}. Return {@link ImapList#EMPTY} if not found.
+ */
+ public final ImapList getKeyedListOrEmpty(String key) {
+ return getKeyedListOrEmpty(key, false);
+ }
+
+ /**
+ * Return an {@link ImapList} keyed by {@code key}. Return {@link ImapList#EMPTY} if not found.
+ */
+ public final ImapList getKeyedListOrEmpty(String key, boolean prefixMatch) {
+ ImapElement e = getKeyedElementOrNull(key, prefixMatch);
+ return (e != null) ? ((ImapList) e) : ImapList.EMPTY;
+ }
+
+ /**
+ * Return an {@link ImapString} keyed by {@code key}. Return {@link ImapString#EMPTY} if not
+ * found.
+ */
+ public final ImapString getKeyedStringOrEmpty(String key) {
+ return getKeyedStringOrEmpty(key, false);
+ }
+
+ /**
+ * Return an {@link ImapString} keyed by {@code key}. Return {@link ImapString#EMPTY} if not
+ * found.
+ */
+ public final ImapString getKeyedStringOrEmpty(String key, boolean prefixMatch) {
+ ImapElement e = getKeyedElementOrNull(key, prefixMatch);
+ return (e != null) ? ((ImapString) e) : ImapString.EMPTY;
+ }
+
+ /** Return true if it contains {@code s}. */
+ public final boolean contains(String s) {
+ for (int i = 0; i < size(); i++) {
+ if (getStringOrEmpty(i).is(s)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void destroy() {
+ if (mList != null) {
+ for (ImapElement e : mList) {
+ e.destroy();
+ }
+ mList = null;
+ }
+ super.destroy();
+ }
+
+ @Override
+ public String toString() {
+ return mList.toString();
+ }
+
+ /** Return the text representations of the contents concatenated with ",". */
+ public final String flatten() {
+ return flatten(new StringBuilder()).toString();
+ }
+
+ /**
+ * Returns text representations (i.e. getString()) of contents joined together with "," as the
+ * separator.
+ *
+ * <p>Only used for building the capability string passed to vendor policies.
+ *
+ * <p>We can't use toString(), because it's for debugging (meaning the format may change any
+ * time), and it won't expand literals.
+ */
+ private final StringBuilder flatten(StringBuilder sb) {
+ sb.append('[');
+ for (int i = 0; i < mList.size(); i++) {
+ if (i > 0) {
+ sb.append(',');
+ }
+ final ImapElement e = getElementOrNone(i);
+ if (e.isList()) {
+ getListOrEmpty(i).flatten(sb);
+ } else if (e.isString()) {
+ sb.append(getStringOrEmpty(i).getString());
+ }
+ }
+ sb.append(']');
+ return sb;
+ }
+
+ @Override
+ public boolean equalsForTest(ImapElement that) {
+ if (!super.equalsForTest(that)) {
+ return false;
+ }
+ ImapList thatList = (ImapList) that;
+ if (size() != thatList.size()) {
+ return false;
+ }
+ for (int i = 0; i < size(); i++) {
+ if (!mList.get(i).equalsForTest(thatList.getElementOrNone(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/imap/ImapMemoryLiteral.java b/java/com/android/voicemail/impl/mail/store/imap/ImapMemoryLiteral.java
new file mode 100644
index 000000000..96a8c4ae5
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/ImapMemoryLiteral.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl.mail.store.imap;
+
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.mail.FixedLengthInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+
+/** Subclass of {@link ImapString} used for literals backed by an in-memory byte array. */
+public class ImapMemoryLiteral extends ImapString {
+ private final String TAG = "ImapMemoryLiteral";
+ private byte[] mData;
+
+ /* package */ ImapMemoryLiteral(FixedLengthInputStream in) throws IOException {
+ // We could use ByteArrayOutputStream and IOUtils.copy, but it'd perform an unnecessary
+ // copy....
+ mData = new byte[in.getLength()];
+ int pos = 0;
+ while (pos < mData.length) {
+ int read = in.read(mData, pos, mData.length - pos);
+ if (read < 0) {
+ break;
+ }
+ pos += read;
+ }
+ if (pos != mData.length) {
+ VvmLog.w(TAG, "length mismatch");
+ }
+ }
+
+ @Override
+ public void destroy() {
+ mData = null;
+ super.destroy();
+ }
+
+ @Override
+ public String getString() {
+ try {
+ return new String(mData, "US-ASCII");
+ } catch (UnsupportedEncodingException e) {
+ VvmLog.e(TAG, "Unsupported encoding: ", e);
+ }
+ return null;
+ }
+
+ @Override
+ public InputStream getAsStream() {
+ return new ByteArrayInputStream(mData);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("{%d byte literal(memory)}", mData.length);
+ }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/imap/ImapResponse.java b/java/com/android/voicemail/impl/mail/store/imap/ImapResponse.java
new file mode 100644
index 000000000..d53d458da
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/ImapResponse.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl.mail.store.imap;
+
+/** Class represents an IMAP response. */
+public class ImapResponse extends ImapList {
+ private final String mTag;
+ private final boolean mIsContinuationRequest;
+
+ /* package */ ImapResponse(String tag, boolean isContinuationRequest) {
+ mTag = tag;
+ mIsContinuationRequest = isContinuationRequest;
+ }
+
+ /* package */ static boolean isStatusResponse(String symbol) {
+ return ImapConstants.OK.equalsIgnoreCase(symbol)
+ || ImapConstants.NO.equalsIgnoreCase(symbol)
+ || ImapConstants.BAD.equalsIgnoreCase(symbol)
+ || ImapConstants.PREAUTH.equalsIgnoreCase(symbol)
+ || ImapConstants.BYE.equalsIgnoreCase(symbol);
+ }
+
+ /** @return whether it's a tagged response. */
+ public boolean isTagged() {
+ return mTag != null;
+ }
+
+ /** @return whether it's a continuation request. */
+ public boolean isContinuationRequest() {
+ return mIsContinuationRequest;
+ }
+
+ public boolean isStatusResponse() {
+ return isStatusResponse(getStringOrEmpty(0).getString());
+ }
+
+ /** @return whether it's an OK response. */
+ public boolean isOk() {
+ return is(0, ImapConstants.OK);
+ }
+
+ /** @return whether it's an BAD response. */
+ public boolean isBad() {
+ return is(0, ImapConstants.BAD);
+ }
+
+ /** @return whether it's an NO response. */
+ public boolean isNo() {
+ return is(0, ImapConstants.NO);
+ }
+
+ /**
+ * @return whether it's an {@code responseType} data response. (i.e. not tagged).
+ * @param index where {@code responseType} should appear. e.g. 1 for "FETCH"
+ * @param responseType e.g. "FETCH"
+ */
+ public final boolean isDataResponse(int index, String responseType) {
+ return !isTagged() && getStringOrEmpty(index).is(responseType);
+ }
+
+ /**
+ * @return Response code (RFC 3501 7.1) if it's a status response.
+ * <p>e.g. "ALERT" for "* OK [ALERT] System shutdown in 10 minutes"
+ */
+ public ImapString getResponseCodeOrEmpty() {
+ if (!isStatusResponse()) {
+ return ImapString.EMPTY; // Not a status response.
+ }
+ return getListOrEmpty(1).getStringOrEmpty(0);
+ }
+
+ /**
+ * @return Alert message it it has ALERT response code.
+ * <p>e.g. "System shutdown in 10 minutes" for "* OK [ALERT] System shutdown in 10 minutes"
+ */
+ public ImapString getAlertTextOrEmpty() {
+ if (!getResponseCodeOrEmpty().is(ImapConstants.ALERT)) {
+ return ImapString.EMPTY; // Not an ALERT
+ }
+ // The 3rd element contains all the rest of line.
+ return getStringOrEmpty(2);
+ }
+
+ /** @return Response text in a status response. */
+ public ImapString getStatusResponseTextOrEmpty() {
+ if (!isStatusResponse()) {
+ return ImapString.EMPTY;
+ }
+ return getStringOrEmpty(getElementOrNone(1).isList() ? 2 : 1);
+ }
+
+ public ImapString getStatusOrEmpty() {
+ if (!isStatusResponse()) {
+ return ImapString.EMPTY;
+ }
+ return getStringOrEmpty(0);
+ }
+
+ @Override
+ public String toString() {
+ String tag = mTag;
+ if (isContinuationRequest()) {
+ tag = "+";
+ }
+ return "#" + tag + "# " + super.toString();
+ }
+
+ @Override
+ public boolean equalsForTest(ImapElement that) {
+ if (!super.equalsForTest(that)) {
+ return false;
+ }
+ final ImapResponse thatResponse = (ImapResponse) that;
+ if (mTag == null) {
+ if (thatResponse.mTag != null) {
+ return false;
+ }
+ } else {
+ if (!mTag.equals(thatResponse.mTag)) {
+ return false;
+ }
+ }
+ if (mIsContinuationRequest != thatResponse.mIsContinuationRequest) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/imap/ImapResponseParser.java b/java/com/android/voicemail/impl/mail/store/imap/ImapResponseParser.java
new file mode 100644
index 000000000..e37106a69
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/ImapResponseParser.java
@@ -0,0 +1,424 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl.mail.store.imap;
+
+import android.text.TextUtils;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.mail.FixedLengthInputStream;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.PeekableInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+/** IMAP response parser. */
+public class ImapResponseParser {
+ private static final String TAG = "ImapResponseParser";
+
+ /** Literal larger than this will be stored in temp file. */
+ public static final int LITERAL_KEEP_IN_MEMORY_THRESHOLD = 2 * 1024 * 1024;
+
+ /** Input stream */
+ private final PeekableInputStream mIn;
+
+ private final int mLiteralKeepInMemoryThreshold;
+
+ /** StringBuilder used by readUntil() */
+ private final StringBuilder mBufferReadUntil = new StringBuilder();
+
+ /** StringBuilder used by parseBareString() */
+ private final StringBuilder mParseBareString = new StringBuilder();
+
+ /**
+ * We store all {@link ImapResponse} in it. {@link #destroyResponses()} must be called from time
+ * to time to destroy them and clear it.
+ */
+ private final ArrayList<ImapResponse> mResponsesToDestroy = new ArrayList<ImapResponse>();
+
+ /**
+ * Exception thrown when we receive BYE. It derives from IOException, so it'll be treated in the
+ * same way EOF does.
+ */
+ public static class ByeException extends IOException {
+ public static final String MESSAGE = "Received BYE";
+
+ public ByeException() {
+ super(MESSAGE);
+ }
+ }
+
+ /** Public constructor for normal use. */
+ public ImapResponseParser(InputStream in) {
+ this(in, LITERAL_KEEP_IN_MEMORY_THRESHOLD);
+ }
+
+ /** Constructor for testing to override the literal size threshold. */
+ /* package for test */ ImapResponseParser(InputStream in, int literalKeepInMemoryThreshold) {
+ mIn = new PeekableInputStream(in);
+ mLiteralKeepInMemoryThreshold = literalKeepInMemoryThreshold;
+ }
+
+ private static IOException newEOSException() {
+ final String message = "End of stream reached";
+ VvmLog.d(TAG, message);
+ return new IOException(message);
+ }
+
+ /**
+ * Peek next one byte.
+ *
+ * <p>Throws IOException() if reaches EOF. As long as logical response lines end with \r\n, we
+ * shouldn't see EOF during parsing.
+ */
+ private int peek() throws IOException {
+ final int next = mIn.peek();
+ if (next == -1) {
+ throw newEOSException();
+ }
+ return next;
+ }
+
+ /**
+ * Read and return one byte from {@link #mIn}, and put it in {@link #mDiscourseLogger}.
+ *
+ * <p>Throws IOException() if reaches EOF. As long as logical response lines end with \r\n, we
+ * shouldn't see EOF during parsing.
+ */
+ private int readByte() throws IOException {
+ int next = mIn.read();
+ if (next == -1) {
+ throw newEOSException();
+ }
+ return next;
+ }
+
+ /**
+ * Destroy all the {@link ImapResponse}s stored in the internal storage and clear it.
+ *
+ * @see #readResponse()
+ */
+ public void destroyResponses() {
+ for (ImapResponse r : mResponsesToDestroy) {
+ r.destroy();
+ }
+ mResponsesToDestroy.clear();
+ }
+
+ /**
+ * Reads the next response available on the stream and returns an {@link ImapResponse} object that
+ * represents it.
+ *
+ * <p>When this method successfully returns an {@link ImapResponse}, the {@link ImapResponse} is
+ * stored in the internal storage. When the {@link ImapResponse} is no longer used {@link
+ * #destroyResponses} should be called to destroy all the responses in the array.
+ *
+ * @param byeExpected is a untagged BYE response expected? If not proper cleanup will be done and
+ * {@link ByeException} will be thrown.
+ * @return the parsed {@link ImapResponse} object.
+ * @exception ByeException when detects BYE and <code>byeExpected</code> is false.
+ */
+ public ImapResponse readResponse(boolean byeExpected) throws IOException, MessagingException {
+ ImapResponse response = null;
+ try {
+ response = parseResponse();
+ } catch (RuntimeException e) {
+ // Parser crash -- log network activities.
+ onParseError(e);
+ throw e;
+ } catch (IOException e) {
+ // Network error, or received an unexpected char.
+ onParseError(e);
+ throw e;
+ }
+
+ // Handle this outside of try-catch. We don't have to dump protocol log when getting BYE.
+ if (!byeExpected && response.is(0, ImapConstants.BYE)) {
+ VvmLog.w(TAG, ByeException.MESSAGE);
+ response.destroy();
+ throw new ByeException();
+ }
+ mResponsesToDestroy.add(response);
+ return response;
+ }
+
+ private void onParseError(Exception e) {
+ // Read a few more bytes, so that the log will contain some more context, even if the parser
+ // crashes in the middle of a response.
+ // This also makes sure the byte in question will be logged, no matter where it crashes.
+ // e.g. when parseAtom() peeks and finds at an unexpected char, it throws an exception
+ // before actually reading it.
+ // However, we don't want to read too much, because then it may get into an email message.
+ try {
+ for (int i = 0; i < 4; i++) {
+ int b = readByte();
+ if (b == -1 || b == '\n') {
+ break;
+ }
+ }
+ } catch (IOException ignore) {
+ }
+ VvmLog.w(TAG, "Exception detected: " + e.getMessage());
+ }
+
+ /**
+ * Read next byte from stream and throw it away. If the byte is different from {@code expected}
+ * throw {@link MessagingException}.
+ */
+ /* package for test */ void expect(char expected) throws IOException {
+ final int next = readByte();
+ if (expected != next) {
+ throw new IOException(
+ String.format(
+ "Expected %04x (%c) but got %04x (%c)", (int) expected, expected, next, (char) next));
+ }
+ }
+
+ /**
+ * Read bytes until we find {@code end}, and return all as string. The {@code end} will be read
+ * (rather than peeked) and won't be included in the result.
+ */
+ /* package for test */ String readUntil(char end) throws IOException {
+ mBufferReadUntil.setLength(0);
+ for (; ; ) {
+ final int ch = readByte();
+ if (ch != end) {
+ mBufferReadUntil.append((char) ch);
+ } else {
+ return mBufferReadUntil.toString();
+ }
+ }
+ }
+
+ /** Read all bytes until \r\n. */
+ /* package */ String readUntilEol() throws IOException {
+ String ret = readUntil('\r');
+ expect('\n'); // TODO Should this really be error?
+ return ret;
+ }
+
+ /** Parse and return the response line. */
+ private ImapResponse parseResponse() throws IOException, MessagingException {
+ // We need to destroy the response if we get an exception.
+ // So, we first store the response that's being built in responseToDestroy, until it's
+ // completely built, at which point we copy it into responseToReturn and null out
+ // responseToDestroyt.
+ // If responseToDestroy is not null in finally, we destroy it because that means
+ // we got an exception somewhere.
+ ImapResponse responseToDestroy = null;
+ final ImapResponse responseToReturn;
+
+ try {
+ final int ch = peek();
+ if (ch == '+') { // Continuation request
+ readByte(); // skip +
+ expect(' ');
+ responseToDestroy = new ImapResponse(null, true);
+
+ // If it's continuation request, we don't really care what's in it.
+ responseToDestroy.add(new ImapSimpleString(readUntilEol()));
+
+ // Response has successfully been built. Let's return it.
+ responseToReturn = responseToDestroy;
+ responseToDestroy = null;
+ } else {
+ // Status response or response data
+ final String tag;
+ if (ch == '*') {
+ tag = null;
+ readByte(); // skip *
+ expect(' ');
+ } else {
+ tag = readUntil(' ');
+ }
+ responseToDestroy = new ImapResponse(tag, false);
+
+ final ImapString firstString = parseBareString();
+ responseToDestroy.add(firstString);
+
+ // parseBareString won't eat a space after the string, so we need to skip it,
+ // if exists.
+ // If the next char is not ' ', it should be EOL.
+ if (peek() == ' ') {
+ readByte(); // skip ' '
+
+ if (responseToDestroy.isStatusResponse()) { // It's a status response
+
+ // Is there a response code?
+ final int next = peek();
+ if (next == '[') {
+ responseToDestroy.add(parseList('[', ']'));
+ if (peek() == ' ') { // Skip following space
+ readByte();
+ }
+ }
+
+ String rest = readUntilEol();
+ if (!TextUtils.isEmpty(rest)) {
+ // The rest is free-form text.
+ responseToDestroy.add(new ImapSimpleString(rest));
+ }
+ } else { // It's a response data.
+ parseElements(responseToDestroy, '\0');
+ }
+ } else {
+ expect('\r');
+ expect('\n');
+ }
+
+ // Response has successfully been built. Let's return it.
+ responseToReturn = responseToDestroy;
+ responseToDestroy = null;
+ }
+ } finally {
+ if (responseToDestroy != null) {
+ // We get an exception.
+ responseToDestroy.destroy();
+ }
+ }
+
+ return responseToReturn;
+ }
+
+ private ImapElement parseElement() throws IOException, MessagingException {
+ final int next = peek();
+ switch (next) {
+ case '(':
+ return parseList('(', ')');
+ case '[':
+ return parseList('[', ']');
+ case '"':
+ readByte(); // Skip "
+ return new ImapSimpleString(readUntil('"'));
+ case '{':
+ return parseLiteral();
+ case '\r': // CR
+ readByte(); // Consume \r
+ expect('\n'); // Should be followed by LF.
+ return null;
+ case '\n': // LF // There shouldn't be a bare LF, but just in case.
+ readByte(); // Consume \n
+ return null;
+ default:
+ return parseBareString();
+ }
+ }
+
+ /**
+ * Parses an atom.
+ *
+ * <p>Special case: If an atom contains '[', everything until the next ']' will be considered a
+ * part of the atom. (e.g. "BODY[HEADER.FIELDS ("DATE" ...)]" will become a single ImapString)
+ *
+ * <p>If the value is "NIL", returns an empty string.
+ */
+ private ImapString parseBareString() throws IOException, MessagingException {
+ mParseBareString.setLength(0);
+ for (; ; ) {
+ final int ch = peek();
+
+ // TODO Can we clean this up? (This condition is from the old parser.)
+ if (ch == '('
+ || ch == ')'
+ || ch == '{'
+ || ch == ' '
+ ||
+ // ']' is not part of atom (it's in resp-specials)
+ ch == ']'
+ ||
+ // docs claim that flags are \ atom but atom isn't supposed to
+ // contain
+ // * and some flags contain *
+ // ch == '%' || ch == '*' ||
+ ch == '%'
+ ||
+ // TODO probably should not allow \ and should recognize
+ // it as a flag instead
+ // ch == '"' || ch == '\' ||
+ ch == '"'
+ || (0x00 <= ch && ch <= 0x1f)
+ || ch == 0x7f) {
+ if (mParseBareString.length() == 0) {
+ throw new MessagingException("Expected string, none found.");
+ }
+ String s = mParseBareString.toString();
+
+ // NIL will be always converted into the empty string.
+ if (ImapConstants.NIL.equalsIgnoreCase(s)) {
+ return ImapString.EMPTY;
+ }
+ return new ImapSimpleString(s);
+ } else if (ch == '[') {
+ // Eat all until next ']'
+ mParseBareString.append((char) readByte());
+ mParseBareString.append(readUntil(']'));
+ mParseBareString.append(']'); // readUntil won't include the end char.
+ } else {
+ mParseBareString.append((char) readByte());
+ }
+ }
+ }
+
+ private void parseElements(ImapList list, char end) throws IOException, MessagingException {
+ for (; ; ) {
+ for (; ; ) {
+ final int next = peek();
+ if (next == end) {
+ return;
+ }
+ if (next != ' ') {
+ break;
+ }
+ // Skip space
+ readByte();
+ }
+ final ImapElement el = parseElement();
+ if (el == null) { // EOL
+ return;
+ }
+ list.add(el);
+ }
+ }
+
+ private ImapList parseList(char opening, char closing) throws IOException, MessagingException {
+ expect(opening);
+ final ImapList list = new ImapList();
+ parseElements(list, closing);
+ expect(closing);
+ return list;
+ }
+
+ private ImapString parseLiteral() throws IOException, MessagingException {
+ expect('{');
+ final int size;
+ try {
+ size = Integer.parseInt(readUntil('}'));
+ } catch (NumberFormatException nfe) {
+ throw new MessagingException("Invalid length in literal");
+ }
+ if (size < 0) {
+ throw new MessagingException("Invalid negative length in literal");
+ }
+ expect('\r');
+ expect('\n');
+ FixedLengthInputStream in = new FixedLengthInputStream(mIn, size);
+ if (size > mLiteralKeepInMemoryThreshold) {
+ return new ImapTempFileLiteral(in);
+ } else {
+ return new ImapMemoryLiteral(in);
+ }
+ }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/imap/ImapSimpleString.java b/java/com/android/voicemail/impl/mail/store/imap/ImapSimpleString.java
new file mode 100644
index 000000000..7cc866b74
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/ImapSimpleString.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl.mail.store.imap;
+
+import com.android.voicemail.impl.VvmLog;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+
+/** Subclass of {@link ImapString} used for non literals. */
+public class ImapSimpleString extends ImapString {
+ private final String TAG = "ImapSimpleString";
+ private String mString;
+
+ /* package */ ImapSimpleString(String string) {
+ mString = (string != null) ? string : "";
+ }
+
+ @Override
+ public void destroy() {
+ mString = null;
+ super.destroy();
+ }
+
+ @Override
+ public String getString() {
+ return mString;
+ }
+
+ @Override
+ public InputStream getAsStream() {
+ try {
+ return new ByteArrayInputStream(mString.getBytes("US-ASCII"));
+ } catch (UnsupportedEncodingException e) {
+ VvmLog.e(TAG, "Unsupported encoding: ", e);
+ }
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ // Purposefully not return just mString, in order to prevent using it instead of getString.
+ return "\"" + mString + "\"";
+ }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/imap/ImapString.java b/java/com/android/voicemail/impl/mail/store/imap/ImapString.java
new file mode 100644
index 000000000..d5c555126
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/ImapString.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl.mail.store.imap;
+
+import com.android.voicemail.impl.VvmLog;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * Class represents an IMAP "element" that is not a list.
+ *
+ * <p>An atom, quoted string, literal, are all represented by this. Values like OK, STATUS are too.
+ * Also, this class class may contain more arbitrary value like "BODY[HEADER.FIELDS ("DATE")]". See
+ * {@link ImapResponseParser}.
+ */
+public abstract class ImapString extends ImapElement {
+ private static final byte[] EMPTY_BYTES = new byte[0];
+
+ public static final ImapString EMPTY =
+ new ImapString() {
+ @Override
+ public void destroy() {
+ // Don't call super.destroy().
+ // It's a shared object. We don't want the mDestroyed to be set on this.
+ }
+
+ @Override
+ public String getString() {
+ return "";
+ }
+
+ @Override
+ public InputStream getAsStream() {
+ return new ByteArrayInputStream(EMPTY_BYTES);
+ }
+
+ @Override
+ public String toString() {
+ return "";
+ }
+ };
+
+ // This is used only for parsing IMAP's FETCH ENVELOPE command, in which
+ // en_US-like date format is used like "01-Jan-2009 11:20:39 -0800", so this should be
+ // handled by Locale.US
+ private static final SimpleDateFormat DATE_TIME_FORMAT =
+ new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss Z", Locale.US);
+
+ private boolean mIsInteger;
+ private int mParsedInteger;
+ private Date mParsedDate;
+
+ @Override
+ public final boolean isList() {
+ return false;
+ }
+
+ @Override
+ public final boolean isString() {
+ return true;
+ }
+
+ /**
+ * @return true if and only if the length of the string is larger than 0.
+ * <p>Note: IMAP NIL is considered an empty string. See {@link ImapResponseParser
+ * #parseBareString}. On the other hand, a quoted/literal string with value NIL (i.e. "NIL"
+ * and {3}\r\nNIL) is treated literally.
+ */
+ public final boolean isEmpty() {
+ return getString().length() == 0;
+ }
+
+ public abstract String getString();
+
+ public abstract InputStream getAsStream();
+
+ /** @return whether it can be parsed as a number. */
+ public final boolean isNumber() {
+ if (mIsInteger) {
+ return true;
+ }
+ try {
+ mParsedInteger = Integer.parseInt(getString());
+ mIsInteger = true;
+ return true;
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+
+ /** @return value parsed as a number, or 0 if the string is not a number. */
+ public final int getNumberOrZero() {
+ return getNumber(0);
+ }
+
+ /** @return value parsed as a number, or {@code defaultValue} if the string is not a number. */
+ public final int getNumber(int defaultValue) {
+ if (!isNumber()) {
+ return defaultValue;
+ }
+ return mParsedInteger;
+ }
+
+ /** @return whether it can be parsed as a date using {@link #DATE_TIME_FORMAT}. */
+ public final boolean isDate() {
+ if (mParsedDate != null) {
+ return true;
+ }
+ if (isEmpty()) {
+ return false;
+ }
+ try {
+ mParsedDate = DATE_TIME_FORMAT.parse(getString());
+ return true;
+ } catch (ParseException e) {
+ VvmLog.w("ImapString", getString() + " can't be parsed as a date.");
+ return false;
+ }
+ }
+
+ /** @return value it can be parsed as a {@link Date}, or null otherwise. */
+ public final Date getDateOrNull() {
+ if (!isDate()) {
+ return null;
+ }
+ return mParsedDate;
+ }
+
+ /** @return whether the value case-insensitively equals to {@code s}. */
+ public final boolean is(String s) {
+ if (s == null) {
+ return false;
+ }
+ return getString().equalsIgnoreCase(s);
+ }
+
+ /** @return whether the value case-insensitively starts with {@code s}. */
+ public final boolean startsWith(String prefix) {
+ if (prefix == null) {
+ return false;
+ }
+ final String me = this.getString();
+ if (me.length() < prefix.length()) {
+ return false;
+ }
+ return me.substring(0, prefix.length()).equalsIgnoreCase(prefix);
+ }
+
+ // To force subclasses to implement it.
+ @Override
+ public abstract String toString();
+
+ @Override
+ public final boolean equalsForTest(ImapElement that) {
+ if (!super.equalsForTest(that)) {
+ return false;
+ }
+ ImapString thatString = (ImapString) that;
+ return getString().equals(thatString.getString());
+ }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/imap/ImapTempFileLiteral.java b/java/com/android/voicemail/impl/mail/store/imap/ImapTempFileLiteral.java
new file mode 100644
index 000000000..ab64d8537
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/ImapTempFileLiteral.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail.store.imap;
+
+import com.android.voicemail.impl.mail.FixedLengthInputStream;
+import com.android.voicemail.impl.mail.TempDirectory;
+import com.android.voicemail.impl.mail.utils.LogUtils;
+import com.android.voicemail.impl.mail.utils.Utility;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import org.apache.commons.io.IOUtils;
+
+/** Subclass of {@link ImapString} used for literals backed by a temp file. */
+public class ImapTempFileLiteral extends ImapString {
+ private final String TAG = "ImapTempFileLiteral";
+
+ /* package for test */ final File mFile;
+
+ /** Size is purely for toString() */
+ private final int mSize;
+
+ /* package */ ImapTempFileLiteral(FixedLengthInputStream stream) throws IOException {
+ mSize = stream.getLength();
+ mFile = File.createTempFile("imap", ".tmp", TempDirectory.getTempDirectory());
+
+ // Unfortunately, we can't really use deleteOnExit(), because temp filenames are random
+ // so it'd simply cause a memory leak.
+ // deleteOnExit() simply adds filenames to a static list and the list will never shrink.
+ // mFile.deleteOnExit();
+ OutputStream out = new FileOutputStream(mFile);
+ IOUtils.copy(stream, out);
+ out.close();
+ }
+
+ /**
+ * Make sure we delete the temp file.
+ *
+ * <p>We should always be calling {@link ImapResponse#destroy()}, but it's here as a last resort.
+ */
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ destroy();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ @Override
+ public InputStream getAsStream() {
+ checkNotDestroyed();
+ try {
+ return new FileInputStream(mFile);
+ } catch (FileNotFoundException e) {
+ // It's probably possible if we're low on storage and the system clears the cache dir.
+ LogUtils.w(TAG, "ImapTempFileLiteral: Temp file not found");
+
+ // Return 0 byte stream as a dummy...
+ return new ByteArrayInputStream(new byte[0]);
+ }
+ }
+
+ @Override
+ public String getString() {
+ checkNotDestroyed();
+ try {
+ byte[] bytes = IOUtils.toByteArray(getAsStream());
+ // Prevent crash from OOM; we've seen this, but only rarely and not reproducibly
+ if (bytes.length > ImapResponseParser.LITERAL_KEEP_IN_MEMORY_THRESHOLD) {
+ throw new IOException();
+ }
+ return Utility.fromAscii(bytes);
+ } catch (IOException e) {
+ LogUtils.w(TAG, "ImapTempFileLiteral: Error while reading temp file", e);
+ return "";
+ }
+ }
+
+ @Override
+ public void destroy() {
+ try {
+ if (!isDestroyed() && mFile.exists()) {
+ mFile.delete();
+ }
+ } catch (RuntimeException re) {
+ // Just log and ignore.
+ LogUtils.w(TAG, "Failed to remove temp file: " + re.getMessage());
+ }
+ super.destroy();
+ }
+
+ @Override
+ public String toString() {
+ return String.format("{%d byte literal(file)}", mSize);
+ }
+
+ public boolean tempFileExistsForTest() {
+ return mFile.exists();
+ }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/imap/ImapUtility.java b/java/com/android/voicemail/impl/mail/store/imap/ImapUtility.java
new file mode 100644
index 000000000..a325cc295
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/ImapUtility.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail.store.imap;
+
+import com.android.voicemail.impl.mail.utils.LogUtils;
+import java.util.ArrayList;
+
+/** Utility methods for use with IMAP. */
+public class ImapUtility {
+ public static final String TAG = "ImapUtility";
+ /**
+ * Apply quoting rules per IMAP RFC, quoted = DQUOTE *QUOTED-CHAR DQUOTE QUOTED-CHAR = <any
+ * TEXT-CHAR except quoted-specials> / "\" quoted-specials quoted-specials = DQUOTE / "\"
+ *
+ * <p>This is used primarily for IMAP login, but might be useful elsewhere.
+ *
+ * <p>NOTE: Not very efficient - you may wish to preflight this, or perhaps it should check for
+ * trouble chars before calling the replace functions.
+ *
+ * @param s The string to be quoted.
+ * @return A copy of the string, having undergone quoting as described above
+ */
+ public static String imapQuoted(String s) {
+
+ // First, quote any backslashes by replacing \ with \\
+ // regex Pattern: \\ (Java string const = \\\\)
+ // Substitute: \\\\ (Java string const = \\\\\\\\)
+ String result = s.replaceAll("\\\\", "\\\\\\\\");
+
+ // Then, quote any double-quotes by replacing " with \"
+ // regex Pattern: " (Java string const = \")
+ // Substitute: \\" (Java string const = \\\\\")
+ result = result.replaceAll("\"", "\\\\\"");
+
+ // return string with quotes around it
+ return "\"" + result + "\"";
+ }
+
+ /**
+ * Gets all of the values in a sequence set per RFC 3501. Any ranges are expanded into a list of
+ * individual numbers. If the set is invalid, an empty array is returned.
+ *
+ * <pre>
+ * sequence-number = nz-number / "*"
+ * sequence-range = sequence-number ":" sequence-number
+ * sequence-set = (sequence-number / sequence-range) *("," sequence-set)
+ * </pre>
+ */
+ public static String[] getImapSequenceValues(String set) {
+ ArrayList<String> list = new ArrayList<String>();
+ if (set != null) {
+ String[] setItems = set.split(",");
+ for (String item : setItems) {
+ if (item.indexOf(':') == -1) {
+ // simple item
+ try {
+ Integer.parseInt(item); // Don't need the value; just ensure it's valid
+ list.add(item);
+ } catch (NumberFormatException e) {
+ LogUtils.d(TAG, "Invalid UID value", e);
+ }
+ } else {
+ // range
+ for (String rangeItem : getImapRangeValues(item)) {
+ list.add(rangeItem);
+ }
+ }
+ }
+ }
+ String[] stringList = new String[list.size()];
+ return list.toArray(stringList);
+ }
+
+ /**
+ * Expand the given number range into a list of individual numbers. If the range is not valid, an
+ * empty array is returned.
+ *
+ * <pre>
+ * sequence-number = nz-number / "*"
+ * sequence-range = sequence-number ":" sequence-number
+ * sequence-set = (sequence-number / sequence-range) *("," sequence-set)
+ * </pre>
+ */
+ public static String[] getImapRangeValues(String range) {
+ ArrayList<String> list = new ArrayList<String>();
+ try {
+ if (range != null) {
+ int colonPos = range.indexOf(':');
+ if (colonPos > 0) {
+ int first = Integer.parseInt(range.substring(0, colonPos));
+ int second = Integer.parseInt(range.substring(colonPos + 1));
+ if (first < second) {
+ for (int i = first; i <= second; i++) {
+ list.add(Integer.toString(i));
+ }
+ } else {
+ for (int i = first; i >= second; i--) {
+ list.add(Integer.toString(i));
+ }
+ }
+ }
+ }
+ } catch (NumberFormatException e) {
+ LogUtils.d(TAG, "Invalid range value", e);
+ }
+ String[] stringList = new String[list.size()];
+ return list.toArray(stringList);
+ }
+}
diff --git a/java/com/android/voicemailomtp/mail/utility/CountingOutputStream.java b/java/com/android/voicemail/impl/mail/utility/CountingOutputStream.java
index fdf81d44a..c3586105f 100644
--- a/java/com/android/voicemailomtp/mail/utility/CountingOutputStream.java
+++ b/java/com/android/voicemail/impl/mail/utility/CountingOutputStream.java
@@ -13,36 +13,36 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.voicemailomtp.mail.utility;
+package com.android.voicemail.impl.mail.utility;
import java.io.IOException;
import java.io.OutputStream;
/**
- * A simple pass-thru OutputStream that also counts how many bytes are written to it and
- * makes that count available to callers.
+ * A simple pass-thru OutputStream that also counts how many bytes are written to it and makes that
+ * count available to callers.
*/
public class CountingOutputStream extends OutputStream {
- private long mCount;
- private final OutputStream mOutputStream;
+ private long mCount;
+ private final OutputStream mOutputStream;
- public CountingOutputStream(OutputStream outputStream) {
- mOutputStream = outputStream;
- }
+ public CountingOutputStream(OutputStream outputStream) {
+ mOutputStream = outputStream;
+ }
- public long getCount() {
- return mCount;
- }
+ public long getCount() {
+ return mCount;
+ }
- @Override
- public void write(byte[] buffer, int offset, int count) throws IOException {
- mOutputStream.write(buffer, offset, count);
- mCount += count;
- }
+ @Override
+ public void write(byte[] buffer, int offset, int count) throws IOException {
+ mOutputStream.write(buffer, offset, count);
+ mCount += count;
+ }
- @Override
- public void write(int oneByte) throws IOException {
- mOutputStream.write(oneByte);
- mCount++;
- }
-} \ No newline at end of file
+ @Override
+ public void write(int oneByte) throws IOException {
+ mOutputStream.write(oneByte);
+ mCount++;
+ }
+}
diff --git a/java/com/android/voicemailomtp/mail/utility/EOLConvertingOutputStream.java b/java/com/android/voicemail/impl/mail/utility/EOLConvertingOutputStream.java
index 5b93a92ab..72649ac4d 100644
--- a/java/com/android/voicemailomtp/mail/utility/EOLConvertingOutputStream.java
+++ b/java/com/android/voicemail/impl/mail/utility/EOLConvertingOutputStream.java
@@ -13,36 +13,36 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.voicemailomtp.mail.utility;
+package com.android.voicemail.impl.mail.utility;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class EOLConvertingOutputStream extends FilterOutputStream {
- int lastChar;
+ int lastChar;
- public EOLConvertingOutputStream(OutputStream out) {
- super(out);
- }
+ public EOLConvertingOutputStream(OutputStream out) {
+ super(out);
+ }
- @Override
- public void write(int oneByte) throws IOException {
- if (oneByte == '\n') {
- if (lastChar != '\r') {
- super.write('\r');
- }
- }
- super.write(oneByte);
- lastChar = oneByte;
+ @Override
+ public void write(int oneByte) throws IOException {
+ if (oneByte == '\n') {
+ if (lastChar != '\r') {
+ super.write('\r');
+ }
}
+ super.write(oneByte);
+ lastChar = oneByte;
+ }
- @Override
- public void flush() throws IOException {
- if (lastChar == '\r') {
- super.write('\n');
- lastChar = '\n';
- }
- super.flush();
+ @Override
+ public void flush() throws IOException {
+ if (lastChar == '\r') {
+ super.write('\n');
+ lastChar = '\n';
}
-} \ No newline at end of file
+ super.flush();
+ }
+}
diff --git a/java/com/android/voicemail/impl/mail/utils/LogUtils.java b/java/com/android/voicemail/impl/mail/utils/LogUtils.java
new file mode 100644
index 000000000..f6c3c6ba3
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/utils/LogUtils.java
@@ -0,0 +1,345 @@
+/**
+ * Copyright (c) 2015 The Android Open Source Project
+ *
+ * <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail.utils;
+
+import android.net.Uri;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.voicemail.impl.VvmLog;
+import java.util.List;
+
+public class LogUtils {
+ public static final String TAG = "Email Log";
+
+ private static final String ACCOUNT_PREFIX = "account:";
+
+ /** Priority constant for the println method; use LogUtils.v. */
+ public static final int VERBOSE = Log.VERBOSE;
+
+ /** Priority constant for the println method; use LogUtils.d. */
+ public static final int DEBUG = Log.DEBUG;
+
+ /** Priority constant for the println method; use LogUtils.i. */
+ public static final int INFO = Log.INFO;
+
+ /** Priority constant for the println method; use LogUtils.w. */
+ public static final int WARN = Log.WARN;
+
+ /** Priority constant for the println method; use LogUtils.e. */
+ public static final int ERROR = Log.ERROR;
+
+ /**
+ * Used to enable/disable logging that we don't want included in production releases. This should
+ * be set to DEBUG for production releases, and VERBOSE for internal builds.
+ */
+ private static final int MAX_ENABLED_LOG_LEVEL = DEBUG;
+
+ private static Boolean sDebugLoggingEnabledForTests = null;
+
+ /** Enable debug logging for unit tests. */
+ @VisibleForTesting
+ public static void setDebugLoggingEnabledForTests(boolean enabled) {
+ setDebugLoggingEnabledForTestsInternal(enabled);
+ }
+
+ protected static void setDebugLoggingEnabledForTestsInternal(boolean enabled) {
+ sDebugLoggingEnabledForTests = Boolean.valueOf(enabled);
+ }
+
+ /** Returns true if the build configuration prevents debug logging. */
+ @VisibleForTesting
+ public static boolean buildPreventsDebugLogging() {
+ return MAX_ENABLED_LOG_LEVEL > VERBOSE;
+ }
+
+ /** Returns a boolean indicating whether debug logging is enabled. */
+ protected static boolean isDebugLoggingEnabled(String tag) {
+ if (buildPreventsDebugLogging()) {
+ return false;
+ }
+ if (sDebugLoggingEnabledForTests != null) {
+ return sDebugLoggingEnabledForTests.booleanValue();
+ }
+ return Log.isLoggable(tag, Log.DEBUG) || Log.isLoggable(TAG, Log.DEBUG);
+ }
+
+ /**
+ * Returns a String for the specified content provider uri. This will do sanitation of the uri to
+ * remove PII if debug logging is not enabled.
+ */
+ public static String contentUriToString(final Uri uri) {
+ return contentUriToString(TAG, uri);
+ }
+
+ /**
+ * Returns a String for the specified content provider uri. This will do sanitation of the uri to
+ * remove PII if debug logging is not enabled.
+ */
+ public static String contentUriToString(String tag, Uri uri) {
+ if (isDebugLoggingEnabled(tag)) {
+ // Debug logging has been enabled, so log the uri as is
+ return uri.toString();
+ } else {
+ // Debug logging is not enabled, we want to remove the email address from the uri.
+ List<String> pathSegments = uri.getPathSegments();
+
+ Uri.Builder builder =
+ new Uri.Builder()
+ .scheme(uri.getScheme())
+ .authority(uri.getAuthority())
+ .query(uri.getQuery())
+ .fragment(uri.getFragment());
+
+ // This assumes that the first path segment is the account
+ final String account = pathSegments.get(0);
+
+ builder = builder.appendPath(sanitizeAccountName(account));
+ for (int i = 1; i < pathSegments.size(); i++) {
+ builder.appendPath(pathSegments.get(i));
+ }
+ return builder.toString();
+ }
+ }
+
+ /** Sanitizes an account name. If debug logging is not enabled, a sanitized name is returned. */
+ public static String sanitizeAccountName(String accountName) {
+ if (TextUtils.isEmpty(accountName)) {
+ return "";
+ }
+
+ return ACCOUNT_PREFIX + sanitizeName(TAG, accountName);
+ }
+
+ public static String sanitizeName(final String tag, final String name) {
+ if (TextUtils.isEmpty(name)) {
+ return "";
+ }
+
+ if (isDebugLoggingEnabled(tag)) {
+ return name;
+ }
+
+ return String.valueOf(name.hashCode());
+ }
+
+ /**
+ * Checks to see whether or not a log for the specified tag is loggable at the specified level.
+ */
+ public static boolean isLoggable(String tag, int level) {
+ if (MAX_ENABLED_LOG_LEVEL > level) {
+ return false;
+ }
+ return Log.isLoggable(tag, level) || Log.isLoggable(TAG, level);
+ }
+
+ /**
+ * Send a {@link #VERBOSE} log message.
+ *
+ * @param tag Used to identify the source of a log message. It usually identifies the class or
+ * activity where the log call occurs.
+ * @param format the format string (see {@link java.util.Formatter#format})
+ * @param args the list of arguments passed to the formatter. If there are more arguments than
+ * required by {@code format}, additional arguments are ignored.
+ */
+ public static void v(String tag, String format, Object... args) {
+ if (isLoggable(tag, VERBOSE)) {
+ VvmLog.v(tag, String.format(format, args));
+ }
+ }
+
+ /**
+ * Send a {@link #VERBOSE} log message.
+ *
+ * @param tag Used to identify the source of a log message. It usually identifies the class or
+ * activity where the log call occurs.
+ * @param tr An exception to log
+ * @param format the format string (see {@link java.util.Formatter#format})
+ * @param args the list of arguments passed to the formatter. If there are more arguments than
+ * required by {@code format}, additional arguments are ignored.
+ */
+ public static void v(String tag, Throwable tr, String format, Object... args) {
+ if (isLoggable(tag, VERBOSE)) {
+ VvmLog.v(tag, String.format(format, args), tr);
+ }
+ }
+
+ /**
+ * Send a {@link #DEBUG} log message.
+ *
+ * @param tag Used to identify the source of a log message. It usually identifies the class or
+ * activity where the log call occurs.
+ * @param format the format string (see {@link java.util.Formatter#format})
+ * @param args the list of arguments passed to the formatter. If there are more arguments than
+ * required by {@code format}, additional arguments are ignored.
+ */
+ public static void d(String tag, String format, Object... args) {
+ if (isLoggable(tag, DEBUG)) {
+ VvmLog.d(tag, String.format(format, args));
+ }
+ }
+
+ /**
+ * Send a {@link #DEBUG} log message.
+ *
+ * @param tag Used to identify the source of a log message. It usually identifies the class or
+ * activity where the log call occurs.
+ * @param tr An exception to log
+ * @param format the format string (see {@link java.util.Formatter#format})
+ * @param args the list of arguments passed to the formatter. If there are more arguments than
+ * required by {@code format}, additional arguments are ignored.
+ */
+ public static void d(String tag, Throwable tr, String format, Object... args) {
+ if (isLoggable(tag, DEBUG)) {
+ VvmLog.d(tag, String.format(format, args), tr);
+ }
+ }
+
+ /**
+ * Send a {@link #INFO} log message.
+ *
+ * @param tag Used to identify the source of a log message. It usually identifies the class or
+ * activity where the log call occurs.
+ * @param format the format string (see {@link java.util.Formatter#format})
+ * @param args the list of arguments passed to the formatter. If there are more arguments than
+ * required by {@code format}, additional arguments are ignored.
+ */
+ public static void i(String tag, String format, Object... args) {
+ if (isLoggable(tag, INFO)) {
+ VvmLog.i(tag, String.format(format, args));
+ }
+ }
+
+ /**
+ * Send a {@link #INFO} log message.
+ *
+ * @param tag Used to identify the source of a log message. It usually identifies the class or
+ * activity where the log call occurs.
+ * @param tr An exception to log
+ * @param format the format string (see {@link java.util.Formatter#format})
+ * @param args the list of arguments passed to the formatter. If there are more arguments than
+ * required by {@code format}, additional arguments are ignored.
+ */
+ public static void i(String tag, Throwable tr, String format, Object... args) {
+ if (isLoggable(tag, INFO)) {
+ VvmLog.i(tag, String.format(format, args), tr);
+ }
+ }
+
+ /**
+ * Send a {@link #WARN} log message.
+ *
+ * @param tag Used to identify the source of a log message. It usually identifies the class or
+ * activity where the log call occurs.
+ * @param format the format string (see {@link java.util.Formatter#format})
+ * @param args the list of arguments passed to the formatter. If there are more arguments than
+ * required by {@code format}, additional arguments are ignored.
+ */
+ public static void w(String tag, String format, Object... args) {
+ if (isLoggable(tag, WARN)) {
+ VvmLog.w(tag, String.format(format, args));
+ }
+ }
+
+ /**
+ * Send a {@link #WARN} log message.
+ *
+ * @param tag Used to identify the source of a log message. It usually identifies the class or
+ * activity where the log call occurs.
+ * @param tr An exception to log
+ * @param format the format string (see {@link java.util.Formatter#format})
+ * @param args the list of arguments passed to the formatter. If there are more arguments than
+ * required by {@code format}, additional arguments are ignored.
+ */
+ public static void w(String tag, Throwable tr, String format, Object... args) {
+ if (isLoggable(tag, WARN)) {
+ VvmLog.w(tag, String.format(format, args), tr);
+ }
+ }
+
+ /**
+ * Send a {@link #ERROR} log message.
+ *
+ * @param tag Used to identify the source of a log message. It usually identifies the class or
+ * activity where the log call occurs.
+ * @param format the format string (see {@link java.util.Formatter#format})
+ * @param args the list of arguments passed to the formatter. If there are more arguments than
+ * required by {@code format}, additional arguments are ignored.
+ */
+ public static void e(String tag, String format, Object... args) {
+ if (isLoggable(tag, ERROR)) {
+ VvmLog.e(tag, String.format(format, args));
+ }
+ }
+
+ /**
+ * Send a {@link #ERROR} log message.
+ *
+ * @param tag Used to identify the source of a log message. It usually identifies the class or
+ * activity where the log call occurs.
+ * @param tr An exception to log
+ * @param format the format string (see {@link java.util.Formatter#format})
+ * @param args the list of arguments passed to the formatter. If there are more arguments than
+ * required by {@code format}, additional arguments are ignored.
+ */
+ public static void e(String tag, Throwable tr, String format, Object... args) {
+ if (isLoggable(tag, ERROR)) {
+ VvmLog.e(tag, String.format(format, args), tr);
+ }
+ }
+
+ /**
+ * What a Terrible Failure: Report a condition that should never happen. The error will always be
+ * logged at level ASSERT with the call stack. Depending on system configuration, a report may be
+ * added to the {@link android.os.DropBoxManager} and/or the process may be terminated immediately
+ * with an error dialog.
+ *
+ * @param tag Used to identify the source of a log message. It usually identifies the class or
+ * activity where the log call occurs.
+ * @param format the format string (see {@link java.util.Formatter#format})
+ * @param args the list of arguments passed to the formatter. If there are more arguments than
+ * required by {@code format}, additional arguments are ignored.
+ */
+ public static void wtf(String tag, String format, Object... args) {
+ VvmLog.wtf(tag, String.format(format, args), new Error());
+ }
+
+ /**
+ * What a Terrible Failure: Report a condition that should never happen. The error will always be
+ * logged at level ASSERT with the call stack. Depending on system configuration, a report may be
+ * added to the {@link android.os.DropBoxManager} and/or the process may be terminated immediately
+ * with an error dialog.
+ *
+ * @param tag Used to identify the source of a log message. It usually identifies the class or
+ * activity where the log call occurs.
+ * @param tr An exception to log
+ * @param format the format string (see {@link java.util.Formatter#format})
+ * @param args the list of arguments passed to the formatter. If there are more arguments than
+ * required by {@code format}, additional arguments are ignored.
+ */
+ public static void wtf(String tag, Throwable tr, String format, Object... args) {
+ VvmLog.wtf(tag, String.format(format, args), tr);
+ }
+
+ public static String byteToHex(int b) {
+ return byteToHex(new StringBuilder(), b).toString();
+ }
+
+ public static StringBuilder byteToHex(StringBuilder sb, int b) {
+ b &= 0xFF;
+ sb.append("0123456789ABCDEF".charAt(b >> 4));
+ sb.append("0123456789ABCDEF".charAt(b & 0xF));
+ return sb;
+ }
+}
diff --git a/java/com/android/voicemail/impl/mail/utils/Utility.java b/java/com/android/voicemail/impl/mail/utils/Utility.java
new file mode 100644
index 000000000..4db1681fb
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/utils/Utility.java
@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2015 The Android Open Source Project
+ *
+ * <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail.utils;
+
+import java.io.ByteArrayInputStream;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+
+/** Simple utility methods used in email functions. */
+public class Utility {
+ public static final Charset ASCII = Charset.forName("US-ASCII");
+
+ public static final String[] EMPTY_STRINGS = new String[0];
+
+ /**
+ * Returns a concatenated string containing the output of every Object's toString() method, each
+ * separated by the given separator character.
+ */
+ public static String combine(Object[] parts, char separator) {
+ if (parts == null) {
+ return null;
+ }
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < parts.length; i++) {
+ sb.append(parts[i].toString());
+ if (i < parts.length - 1) {
+ sb.append(separator);
+ }
+ }
+ return sb.toString();
+ }
+
+ /** Converts a String to ASCII bytes */
+ public static byte[] toAscii(String s) {
+ return encode(ASCII, s);
+ }
+
+ /** Builds a String from ASCII bytes */
+ public static String fromAscii(byte[] b) {
+ return decode(ASCII, b);
+ }
+
+ private static byte[] encode(Charset charset, String s) {
+ if (s == null) {
+ return null;
+ }
+ final ByteBuffer buffer = charset.encode(CharBuffer.wrap(s));
+ final byte[] bytes = new byte[buffer.limit()];
+ buffer.get(bytes);
+ return bytes;
+ }
+
+ private static String decode(Charset charset, byte[] b) {
+ if (b == null) {
+ return null;
+ }
+ final CharBuffer cb = charset.decode(ByteBuffer.wrap(b));
+ return new String(cb.array(), 0, cb.length());
+ }
+
+ public static ByteArrayInputStream streamFromAsciiString(String ascii) {
+ return new ByteArrayInputStream(toAscii(ascii));
+ }
+}
diff --git a/java/com/android/voicemail/impl/protocol/CvvmProtocol.java b/java/com/android/voicemail/impl/protocol/CvvmProtocol.java
new file mode 100644
index 000000000..a4b54f68c
--- /dev/null
+++ b/java/com/android/voicemail/impl/protocol/CvvmProtocol.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.protocol;
+
+import android.content.Context;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.sms.OmtpCvvmMessageSender;
+import com.android.voicemail.impl.sms.OmtpMessageSender;
+
+/**
+ * A flavor of OMTP protocol with a different mobile originated (MO) format
+ *
+ * <p>Used by carriers such as T-Mobile
+ */
+public class CvvmProtocol extends VisualVoicemailProtocol {
+
+ private static String IMAP_CHANGE_TUI_PWD_FORMAT = "CHANGE_TUI_PWD PWD=%1$s OLD_PWD=%2$s";
+ private static String IMAP_CHANGE_VM_LANG_FORMAT = "CHANGE_VM_LANG Lang=%1$s";
+ private static String IMAP_CLOSE_NUT = "CLOSE_NUT";
+
+ @Override
+ public OmtpMessageSender createMessageSender(
+ Context context,
+ PhoneAccountHandle phoneAccountHandle,
+ short applicationPort,
+ String destinationNumber) {
+ return new OmtpCvvmMessageSender(
+ context, phoneAccountHandle, applicationPort, destinationNumber);
+ }
+
+ @Override
+ public String getCommand(String command) {
+ if (command == OmtpConstants.IMAP_CHANGE_TUI_PWD_FORMAT) {
+ return IMAP_CHANGE_TUI_PWD_FORMAT;
+ }
+ if (command == OmtpConstants.IMAP_CLOSE_NUT) {
+ return IMAP_CLOSE_NUT;
+ }
+ if (command == OmtpConstants.IMAP_CHANGE_VM_LANG_FORMAT) {
+ return IMAP_CHANGE_VM_LANG_FORMAT;
+ }
+ return super.getCommand(command);
+ }
+}
diff --git a/java/com/android/voicemailomtp/protocol/OmtpProtocol.java b/java/com/android/voicemail/impl/protocol/OmtpProtocol.java
index d88a23285..27aab8a7c 100644
--- a/java/com/android/voicemailomtp/protocol/OmtpProtocol.java
+++ b/java/com/android/voicemail/impl/protocol/OmtpProtocol.java
@@ -14,24 +14,29 @@
* limitations under the License
*/
-package com.android.voicemailomtp.protocol;
+package com.android.voicemail.impl.protocol;
import android.content.Context;
import android.telecom.PhoneAccountHandle;
-import android.telephony.SmsManager;
-
-import com.android.voicemailomtp.OmtpConstants;
-import com.android.voicemailomtp.sms.OmtpMessageSender;
-import com.android.voicemailomtp.sms.OmtpStandardMessageSender;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.sms.OmtpMessageSender;
+import com.android.voicemail.impl.sms.OmtpStandardMessageSender;
public class OmtpProtocol extends VisualVoicemailProtocol {
- @Override
- public OmtpMessageSender createMessageSender(Context context,
- PhoneAccountHandle phoneAccountHandle, short applicationPort,
- String destinationNumber) {
- return new OmtpStandardMessageSender(context, phoneAccountHandle, applicationPort,
- destinationNumber,
- null, OmtpConstants.PROTOCOL_VERSION1_1, null);
- }
+ @Override
+ public OmtpMessageSender createMessageSender(
+ Context context,
+ PhoneAccountHandle phoneAccountHandle,
+ short applicationPort,
+ String destinationNumber) {
+ return new OmtpStandardMessageSender(
+ context,
+ phoneAccountHandle,
+ applicationPort,
+ destinationNumber,
+ OmtpConstants.CLIENT_TYPE_GOOGLE_10,
+ OmtpConstants.PROTOCOL_VERSION1_1,
+ null /*clientPrefix*/);
+ }
}
diff --git a/java/com/android/voicemail/impl/protocol/ProtocolHelper.java b/java/com/android/voicemail/impl/protocol/ProtocolHelper.java
new file mode 100644
index 000000000..4d2e7cce4
--- /dev/null
+++ b/java/com/android/voicemail/impl/protocol/ProtocolHelper.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.protocol;
+
+import android.text.TextUtils;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.sms.OmtpMessageSender;
+
+public class ProtocolHelper {
+
+ private static final String TAG = "ProtocolHelper";
+
+ public static OmtpMessageSender getMessageSender(
+ VisualVoicemailProtocol protocol, OmtpVvmCarrierConfigHelper config) {
+
+ int applicationPort = config.getApplicationPort();
+ String destinationNumber = config.getDestinationNumber();
+ if (TextUtils.isEmpty(destinationNumber)) {
+ VvmLog.w(TAG, "No destination number for this carrier.");
+ return null;
+ }
+
+ return protocol.createMessageSender(
+ config.getContext(),
+ config.getPhoneAccountHandle(),
+ (short) applicationPort,
+ destinationNumber);
+ }
+}
diff --git a/java/com/android/voicemail/impl/protocol/VisualVoicemailProtocol.java b/java/com/android/voicemail/impl/protocol/VisualVoicemailProtocol.java
new file mode 100644
index 000000000..6cf82f1b8
--- /dev/null
+++ b/java/com/android/voicemail/impl/protocol/VisualVoicemailProtocol.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.protocol;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.ActivationTask;
+import com.android.voicemail.impl.DefaultOmtpEventHandler;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.sms.OmtpMessageSender;
+import com.android.voicemail.impl.sms.StatusMessage;
+
+public abstract class VisualVoicemailProtocol {
+
+ /** Activation should cause the carrier to respond with a STATUS SMS. */
+ public void startActivation(OmtpVvmCarrierConfigHelper config, PendingIntent sentIntent) {
+ OmtpMessageSender messageSender = ProtocolHelper.getMessageSender(this, config);
+ if (messageSender != null) {
+ messageSender.requestVvmActivation(sentIntent);
+ }
+ }
+
+ public void startDeactivation(OmtpVvmCarrierConfigHelper config) {
+ OmtpMessageSender messageSender = ProtocolHelper.getMessageSender(this, config);
+ if (messageSender != null) {
+ messageSender.requestVvmDeactivation(null);
+ }
+ }
+
+ public boolean supportsProvisioning() {
+ return false;
+ }
+
+ public void startProvisioning(
+ ActivationTask task,
+ PhoneAccountHandle handle,
+ OmtpVvmCarrierConfigHelper config,
+ VoicemailStatus.Editor editor,
+ StatusMessage message,
+ Bundle data) {
+ // Do nothing
+ }
+
+ public void requestStatus(OmtpVvmCarrierConfigHelper config, @Nullable PendingIntent sentIntent) {
+ OmtpMessageSender messageSender = ProtocolHelper.getMessageSender(this, config);
+ if (messageSender != null) {
+ messageSender.requestVvmStatus(sentIntent);
+ }
+ }
+
+ public abstract OmtpMessageSender createMessageSender(
+ Context context,
+ PhoneAccountHandle phoneAccountHandle,
+ short applicationPort,
+ String destinationNumber);
+
+ /**
+ * Translate an OMTP IMAP command to the protocol specific one. For example, changing the TUI
+ * password on OMTP is XCHANGE_TUI_PWD, but on CVVM and VVM3 it is CHANGE_TUI_PWD.
+ *
+ * @param command A String command in {@link OmtpConstants}, the exact
+ * instance should be used instead of its' value.
+ * @returns Translated command, or {@code null} if not available in this protocol
+ */
+ public String getCommand(String command) {
+ return command;
+ }
+
+ public void handleEvent(
+ Context context,
+ OmtpVvmCarrierConfigHelper config,
+ VoicemailStatus.Editor status,
+ OmtpEvents event) {
+ DefaultOmtpEventHandler.handleEvent(context, config, status, event);
+ }
+
+ /**
+ * Given an VVM SMS with an unknown {@code event}, let the protocol attempt to translate it into
+ * an equivalent STATUS SMS. Returns {@code null} if it cannot be translated.
+ */
+ @Nullable
+ public Bundle translateStatusSmsBundle(
+ OmtpVvmCarrierConfigHelper config, String event, Bundle data) {
+ return null;
+ }
+}
diff --git a/java/com/android/voicemailomtp/protocol/VisualVoicemailProtocolFactory.java b/java/com/android/voicemail/impl/protocol/VisualVoicemailProtocolFactory.java
index b74f503c6..056fb2eaf 100644
--- a/java/com/android/voicemailomtp/protocol/VisualVoicemailProtocolFactory.java
+++ b/java/com/android/voicemail/impl/protocol/VisualVoicemailProtocolFactory.java
@@ -14,34 +14,34 @@
* limitations under the License
*/
-package com.android.voicemailomtp.protocol;
+package com.android.voicemail.impl.protocol;
import android.content.res.Resources;
import android.support.annotation.Nullable;
import android.telephony.TelephonyManager;
-import com.android.voicemailomtp.VvmLog;
+import com.android.voicemail.impl.VvmLog;
public class VisualVoicemailProtocolFactory {
- private static final String TAG = "VvmProtocolFactory";
+ private static final String TAG = "VvmProtocolFactory";
- private static final String VVM_TYPE_VVM3 = "vvm_type_vvm3";
+ private static final String VVM_TYPE_VVM3 = "vvm_type_vvm3";
- @Nullable
- public static VisualVoicemailProtocol create(Resources resources, String type) {
- if (type == null) {
- return null;
- }
- switch (type) {
- case TelephonyManager.VVM_TYPE_OMTP:
- return new OmtpProtocol();
- case TelephonyManager.VVM_TYPE_CVVM:
- return new CvvmProtocol();
- case VVM_TYPE_VVM3:
- return new Vvm3Protocol();
- default:
- VvmLog.e(TAG, "Unexpected visual voicemail type: " + type);
- }
- return null;
+ @Nullable
+ public static VisualVoicemailProtocol create(Resources resources, String type) {
+ if (type == null) {
+ return null;
}
+ switch (type) {
+ case TelephonyManager.VVM_TYPE_OMTP:
+ return new OmtpProtocol();
+ case TelephonyManager.VVM_TYPE_CVVM:
+ return new CvvmProtocol();
+ case VVM_TYPE_VVM3:
+ return new Vvm3Protocol();
+ default:
+ VvmLog.e(TAG, "Unexpected visual voicemail type: " + type);
+ }
+ return null;
+ }
}
diff --git a/java/com/android/voicemail/impl/protocol/Vvm3EventHandler.java b/java/com/android/voicemail/impl/protocol/Vvm3EventHandler.java
new file mode 100644
index 000000000..8bc3cc21c
--- /dev/null
+++ b/java/com/android/voicemail/impl/protocol/Vvm3EventHandler.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.protocol;
+
+import android.content.Context;
+import android.provider.VoicemailContract.Status;
+import android.support.annotation.IntDef;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.DefaultOmtpEventHandler;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.OmtpEvents.Type;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.settings.VoicemailChangePinActivity;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Handles {@link OmtpEvents} when {@link Vvm3Protocol} is being used. This handler writes custom
+ * error codes into the voicemail status table so support on the dialer side is required.
+ *
+ * <p>TODO(b/29577838) disable VVM3 by default so support on system dialer can be ensured.
+ */
+public class Vvm3EventHandler {
+
+ private static final String TAG = "Vvm3EventHandler";
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ VMS_DNS_FAILURE,
+ VMG_DNS_FAILURE,
+ SPG_DNS_FAILURE,
+ VMS_NO_CELLULAR,
+ VMG_NO_CELLULAR,
+ SPG_NO_CELLULAR,
+ VMS_TIMEOUT,
+ VMG_TIMEOUT,
+ STATUS_SMS_TIMEOUT,
+ SUBSCRIBER_BLOCKED,
+ UNKNOWN_USER,
+ UNKNOWN_DEVICE,
+ INVALID_PASSWORD,
+ MAILBOX_NOT_INITIALIZED,
+ SERVICE_NOT_PROVISIONED,
+ SERVICE_NOT_ACTIVATED,
+ USER_BLOCKED,
+ IMAP_GETQUOTA_ERROR,
+ IMAP_SELECT_ERROR,
+ IMAP_ERROR,
+ VMG_INTERNAL_ERROR,
+ VMG_DB_ERROR,
+ VMG_COMMUNICATION_ERROR,
+ SPG_URL_NOT_FOUND,
+ VMG_UNKNOWN_ERROR,
+ PIN_NOT_SET
+ })
+ public @interface ErrorCode {}
+
+ public static final int VMS_DNS_FAILURE = -9001;
+ public static final int VMG_DNS_FAILURE = -9002;
+ public static final int SPG_DNS_FAILURE = -9003;
+ public static final int VMS_NO_CELLULAR = -9004;
+ public static final int VMG_NO_CELLULAR = -9005;
+ public static final int SPG_NO_CELLULAR = -9006;
+ public static final int VMS_TIMEOUT = -9007;
+ public static final int VMG_TIMEOUT = -9008;
+ public static final int STATUS_SMS_TIMEOUT = -9009;
+
+ public static final int SUBSCRIBER_BLOCKED = -9990;
+ public static final int UNKNOWN_USER = -9991;
+ public static final int UNKNOWN_DEVICE = -9992;
+ public static final int INVALID_PASSWORD = -9993;
+ public static final int MAILBOX_NOT_INITIALIZED = -9994;
+ public static final int SERVICE_NOT_PROVISIONED = -9995;
+ public static final int SERVICE_NOT_ACTIVATED = -9996;
+ public static final int USER_BLOCKED = -9998;
+ public static final int IMAP_GETQUOTA_ERROR = -9997;
+ public static final int IMAP_SELECT_ERROR = -9989;
+ public static final int IMAP_ERROR = -9999;
+
+ public static final int VMG_INTERNAL_ERROR = -101;
+ public static final int VMG_DB_ERROR = -102;
+ public static final int VMG_COMMUNICATION_ERROR = -103;
+ public static final int SPG_URL_NOT_FOUND = -301;
+
+ // Non VVM3 codes:
+ public static final int VMG_UNKNOWN_ERROR = -1;
+ public static final int PIN_NOT_SET = -100;
+ // STATUS SMS returned st=U and rc!=2. The user cannot be provisioned and must contact customer
+ // support.
+ public static final int SUBSCRIBER_UNKNOWN = -99;
+
+ public static void handleEvent(
+ Context context,
+ OmtpVvmCarrierConfigHelper config,
+ VoicemailStatus.Editor status,
+ OmtpEvents event) {
+ boolean handled = false;
+ switch (event.getType()) {
+ case Type.CONFIGURATION:
+ handled = handleConfigurationEvent(context, status, event);
+ break;
+ case Type.DATA_CHANNEL:
+ handled = handleDataChannelEvent(status, event);
+ break;
+ case Type.NOTIFICATION_CHANNEL:
+ handled = handleNotificationChannelEvent(status, event);
+ break;
+ case Type.OTHER:
+ handled = handleOtherEvent(status, event);
+ break;
+ default:
+ VvmLog.wtf(TAG, "invalid event type " + event.getType() + " for " + event);
+ }
+ if (!handled) {
+ DefaultOmtpEventHandler.handleEvent(context, config, status, event);
+ }
+ }
+
+ private static boolean handleConfigurationEvent(
+ Context context, VoicemailStatus.Editor status, OmtpEvents event) {
+ switch (event) {
+ case CONFIG_REQUEST_STATUS_SUCCESS:
+ if (!isPinRandomized(context, status.getPhoneAccountHandle())) {
+ return false;
+ } else {
+ postError(status, PIN_NOT_SET);
+ }
+ break;
+ case CONFIG_ACTIVATING_SUBSEQUENT:
+ if (isPinRandomized(context, status.getPhoneAccountHandle())) {
+ status.setConfigurationState(PIN_NOT_SET);
+ } else {
+ status.setConfigurationState(Status.CONFIGURATION_STATE_OK);
+ }
+ status
+ .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_OK)
+ .setDataChannelState(Status.DATA_CHANNEL_STATE_OK)
+ .apply();
+ break;
+ case CONFIG_DEFAULT_PIN_REPLACED:
+ postError(status, PIN_NOT_SET);
+ break;
+ case CONFIG_STATUS_SMS_TIME_OUT:
+ postError(status, STATUS_SMS_TIMEOUT);
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+
+ private static boolean handleDataChannelEvent(VoicemailStatus.Editor status, OmtpEvents event) {
+ switch (event) {
+ case DATA_NO_CONNECTION:
+ case DATA_NO_CONNECTION_CELLULAR_REQUIRED:
+ case DATA_ALL_SOCKET_CONNECTION_FAILED:
+ postError(status, VMS_NO_CELLULAR);
+ break;
+ case DATA_SSL_INVALID_HOST_NAME:
+ case DATA_CANNOT_ESTABLISH_SSL_SESSION:
+ case DATA_IOE_ON_OPEN:
+ postError(status, VMS_TIMEOUT);
+ break;
+ case DATA_CANNOT_RESOLVE_HOST_ON_NETWORK:
+ postError(status, VMS_DNS_FAILURE);
+ break;
+ case DATA_BAD_IMAP_CREDENTIAL:
+ postError(status, IMAP_ERROR);
+ break;
+ case DATA_AUTH_UNKNOWN_USER:
+ postError(status, UNKNOWN_USER);
+ break;
+ case DATA_AUTH_UNKNOWN_DEVICE:
+ postError(status, UNKNOWN_DEVICE);
+ break;
+ case DATA_AUTH_INVALID_PASSWORD:
+ postError(status, INVALID_PASSWORD);
+ break;
+ case DATA_AUTH_MAILBOX_NOT_INITIALIZED:
+ postError(status, MAILBOX_NOT_INITIALIZED);
+ break;
+ case DATA_AUTH_SERVICE_NOT_PROVISIONED:
+ postError(status, SERVICE_NOT_PROVISIONED);
+ break;
+ case DATA_AUTH_SERVICE_NOT_ACTIVATED:
+ postError(status, SERVICE_NOT_ACTIVATED);
+ break;
+ case DATA_AUTH_USER_IS_BLOCKED:
+ postError(status, USER_BLOCKED);
+ break;
+ case DATA_REJECTED_SERVER_RESPONSE:
+ case DATA_INVALID_INITIAL_SERVER_RESPONSE:
+ case DATA_SSL_EXCEPTION:
+ postError(status, IMAP_ERROR);
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+
+ private static boolean handleNotificationChannelEvent(
+ VoicemailStatus.Editor unusedStatus, OmtpEvents unusedEvent) {
+ return false;
+ }
+
+ private static boolean handleOtherEvent(VoicemailStatus.Editor status, OmtpEvents event) {
+ switch (event) {
+ case VVM3_NEW_USER_SETUP_FAILED:
+ postError(status, MAILBOX_NOT_INITIALIZED);
+ break;
+ case VVM3_VMG_DNS_FAILURE:
+ postError(status, VMG_DNS_FAILURE);
+ break;
+ case VVM3_SPG_DNS_FAILURE:
+ postError(status, SPG_DNS_FAILURE);
+ break;
+ case VVM3_VMG_CONNECTION_FAILED:
+ postError(status, VMG_NO_CELLULAR);
+ break;
+ case VVM3_SPG_CONNECTION_FAILED:
+ postError(status, SPG_NO_CELLULAR);
+ break;
+ case VVM3_VMG_TIMEOUT:
+ postError(status, VMG_TIMEOUT);
+ break;
+ case VVM3_SUBSCRIBER_PROVISIONED:
+ postError(status, SERVICE_NOT_ACTIVATED);
+ break;
+ case VVM3_SUBSCRIBER_BLOCKED:
+ postError(status, SUBSCRIBER_BLOCKED);
+ break;
+ case VVM3_SUBSCRIBER_UNKNOWN:
+ postError(status, SUBSCRIBER_UNKNOWN);
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+
+ private static void postError(VoicemailStatus.Editor editor, @ErrorCode int errorCode) {
+ switch (errorCode) {
+ case VMG_DNS_FAILURE:
+ case SPG_DNS_FAILURE:
+ case VMG_NO_CELLULAR:
+ case SPG_NO_CELLULAR:
+ case VMG_TIMEOUT:
+ case SUBSCRIBER_BLOCKED:
+ case UNKNOWN_USER:
+ case UNKNOWN_DEVICE:
+ case INVALID_PASSWORD:
+ case MAILBOX_NOT_INITIALIZED:
+ case SERVICE_NOT_PROVISIONED:
+ case SERVICE_NOT_ACTIVATED:
+ case USER_BLOCKED:
+ case VMG_UNKNOWN_ERROR:
+ case SPG_URL_NOT_FOUND:
+ case VMG_INTERNAL_ERROR:
+ case VMG_DB_ERROR:
+ case VMG_COMMUNICATION_ERROR:
+ case PIN_NOT_SET:
+ case SUBSCRIBER_UNKNOWN:
+ editor.setConfigurationState(errorCode);
+ break;
+ case VMS_NO_CELLULAR:
+ case VMS_DNS_FAILURE:
+ case VMS_TIMEOUT:
+ case IMAP_GETQUOTA_ERROR:
+ case IMAP_SELECT_ERROR:
+ case IMAP_ERROR:
+ editor.setDataChannelState(errorCode);
+ break;
+ case STATUS_SMS_TIMEOUT:
+ editor.setNotificationChannelState(errorCode);
+ break;
+ default:
+ VvmLog.wtf(TAG, "unknown error code: " + errorCode);
+ }
+ editor.apply();
+ }
+
+ private static boolean isPinRandomized(Context context, PhoneAccountHandle phoneAccountHandle) {
+ if (phoneAccountHandle == null) {
+ // This should never happen.
+ VvmLog.e(TAG, "status editor has null phone account handle");
+ return false;
+ }
+ return VoicemailChangePinActivity.isDefaultOldPinSet(context, phoneAccountHandle);
+ }
+}
diff --git a/java/com/android/voicemail/impl/protocol/Vvm3Protocol.java b/java/com/android/voicemail/impl/protocol/Vvm3Protocol.java
new file mode 100644
index 000000000..f293a4cdb
--- /dev/null
+++ b/java/com/android/voicemail/impl/protocol/Vvm3Protocol.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.protocol;
+
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.net.Network;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.text.TextUtils;
+import com.android.voicemail.impl.ActivationTask;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.VisualVoicemailPreferences;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.imap.ImapHelper;
+import com.android.voicemail.impl.imap.ImapHelper.InitializingException;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil;
+import com.android.voicemail.impl.settings.VoicemailChangePinActivity;
+import com.android.voicemail.impl.sms.OmtpMessageSender;
+import com.android.voicemail.impl.sms.StatusMessage;
+import com.android.voicemail.impl.sms.Vvm3MessageSender;
+import com.android.voicemail.impl.sync.VvmNetworkRequest;
+import com.android.voicemail.impl.sync.VvmNetworkRequest.NetworkWrapper;
+import com.android.voicemail.impl.sync.VvmNetworkRequest.RequestFailedException;
+import java.io.IOException;
+import java.security.SecureRandom;
+import java.util.Locale;
+
+/**
+ * A flavor of OMTP protocol with a different provisioning process
+ *
+ * <p>Used by carriers such as Verizon Wireless
+ */
+@TargetApi(VERSION_CODES.O)
+public class Vvm3Protocol extends VisualVoicemailProtocol {
+
+ private static final String TAG = "Vvm3Protocol";
+
+ private static final String SMS_EVENT_UNRECOGNIZED = "UNRECOGNIZED";
+ private static final String SMS_EVENT_UNRECOGNIZED_CMD = "cmd";
+ private static final String SMS_EVENT_UNRECOGNIZED_STATUS = "STATUS";
+ private static final String DEFAULT_VMG_URL_KEY = "default_vmg_url";
+
+ private static final String IMAP_CHANGE_TUI_PWD_FORMAT = "CHANGE_TUI_PWD PWD=%1$s OLD_PWD=%2$s";
+ private static final String IMAP_CHANGE_VM_LANG_FORMAT = "CHANGE_VM_LANG Lang=%1$s";
+ private static final String IMAP_CLOSE_NUT = "CLOSE_NUT";
+
+ private static final String ISO639_Spanish = "es";
+
+ /**
+ * For VVM3, if the STATUS SMS returns {@link StatusMessage#getProvisioningStatus()} of {@link
+ * OmtpConstants#SUBSCRIBER_UNKNOWN} and {@link StatusMessage#getReturnCode()} of this value, the
+ * user can self-provision visual voicemail service. For other response codes, the user must
+ * contact customer support to resolve the issue.
+ */
+ private static final String VVM3_UNKNOWN_SUBSCRIBER_CAN_SUBSCRIBE_RESPONSE_CODE = "2";
+
+ // Default prompt level when using the telephone user interface.
+ // Standard prompt when the user call into the voicemail, and no prompts when someone else is
+ // leaving a voicemail.
+ private static final String VVM3_VM_LANGUAGE_ENGLISH_STANDARD_NO_GUEST_PROMPTS = "5";
+ private static final String VVM3_VM_LANGUAGE_SPANISH_STANDARD_NO_GUEST_PROMPTS = "6";
+
+ private static final int DEFAULT_PIN_LENGTH = 6;
+
+ @Override
+ public void startActivation(
+ OmtpVvmCarrierConfigHelper config, @Nullable PendingIntent sentIntent) {
+ // VVM3 does not support activation SMS.
+ // Send a status request which will start the provisioning process if the user is not
+ // provisioned.
+ VvmLog.i(TAG, "Activating");
+ config.requestStatus(sentIntent);
+ }
+
+ @Override
+ public void startDeactivation(OmtpVvmCarrierConfigHelper config) {
+ // VVM3 does not support deactivation.
+ // do nothing.
+ }
+
+ @Override
+ public boolean supportsProvisioning() {
+ return true;
+ }
+
+ @Override
+ public void startProvisioning(
+ ActivationTask task,
+ PhoneAccountHandle phoneAccountHandle,
+ OmtpVvmCarrierConfigHelper config,
+ VoicemailStatus.Editor status,
+ StatusMessage message,
+ Bundle data) {
+ VvmLog.i(TAG, "start vvm3 provisioning");
+ if (OmtpConstants.SUBSCRIBER_UNKNOWN.equals(message.getProvisioningStatus())) {
+ VvmLog.i(TAG, "Provisioning status: Unknown");
+ if (VVM3_UNKNOWN_SUBSCRIBER_CAN_SUBSCRIBE_RESPONSE_CODE.equals(message.getReturnCode())) {
+ VvmLog.i(TAG, "Self provisioning available, subscribing");
+ new Vvm3Subscriber(task, phoneAccountHandle, config, status, data).subscribe();
+ } else {
+ config.handleEvent(status, OmtpEvents.VVM3_SUBSCRIBER_UNKNOWN);
+ }
+ } else if (OmtpConstants.SUBSCRIBER_NEW.equals(message.getProvisioningStatus())) {
+ VvmLog.i(TAG, "setting up new user");
+ // Save the IMAP credentials in preferences so they are persistent and can be retrieved.
+ VisualVoicemailPreferences prefs =
+ new VisualVoicemailPreferences(config.getContext(), phoneAccountHandle);
+ message.putStatus(prefs.edit()).apply();
+
+ startProvisionNewUser(task, phoneAccountHandle, config, status, message);
+ } else if (OmtpConstants.SUBSCRIBER_PROVISIONED.equals(message.getProvisioningStatus())) {
+ VvmLog.i(TAG, "User provisioned but not activated, disabling VVM");
+ VisualVoicemailSettingsUtil.setEnabled(config.getContext(), phoneAccountHandle, false);
+ } else if (OmtpConstants.SUBSCRIBER_BLOCKED.equals(message.getProvisioningStatus())) {
+ VvmLog.i(TAG, "User blocked");
+ config.handleEvent(status, OmtpEvents.VVM3_SUBSCRIBER_BLOCKED);
+ }
+ }
+
+ @Override
+ public OmtpMessageSender createMessageSender(
+ Context context,
+ PhoneAccountHandle phoneAccountHandle,
+ short applicationPort,
+ String destinationNumber) {
+ return new Vvm3MessageSender(context, phoneAccountHandle, applicationPort, destinationNumber);
+ }
+
+ @Override
+ public void handleEvent(
+ Context context,
+ OmtpVvmCarrierConfigHelper config,
+ VoicemailStatus.Editor status,
+ OmtpEvents event) {
+ Vvm3EventHandler.handleEvent(context, config, status, event);
+ }
+
+ @Override
+ public String getCommand(String command) {
+ switch (command) {
+ case OmtpConstants.IMAP_CHANGE_TUI_PWD_FORMAT:
+ return IMAP_CHANGE_TUI_PWD_FORMAT;
+ case OmtpConstants.IMAP_CLOSE_NUT:
+ return IMAP_CLOSE_NUT;
+ case OmtpConstants.IMAP_CHANGE_VM_LANG_FORMAT:
+ return IMAP_CHANGE_VM_LANG_FORMAT;
+ default:
+ return super.getCommand(command);
+ }
+ }
+
+ @Override
+ public Bundle translateStatusSmsBundle(
+ OmtpVvmCarrierConfigHelper config, String event, Bundle data) {
+ // UNRECOGNIZED?cmd=STATUS is the response of a STATUS request when the user is provisioned
+ // with iPhone visual voicemail without VoLTE. Translate it into an unprovisioned status
+ // so provisioning can be done.
+ if (!SMS_EVENT_UNRECOGNIZED.equals(event)) {
+ return null;
+ }
+ if (!SMS_EVENT_UNRECOGNIZED_STATUS.equals(data.getString(SMS_EVENT_UNRECOGNIZED_CMD))) {
+ return null;
+ }
+ Bundle bundle = new Bundle();
+ bundle.putString(OmtpConstants.PROVISIONING_STATUS, OmtpConstants.SUBSCRIBER_UNKNOWN);
+ bundle.putString(
+ OmtpConstants.RETURN_CODE, VVM3_UNKNOWN_SUBSCRIBER_CAN_SUBSCRIBE_RESPONSE_CODE);
+ String vmgUrl = config.getString(DEFAULT_VMG_URL_KEY);
+ if (TextUtils.isEmpty(vmgUrl)) {
+ VvmLog.e(TAG, "Unable to translate STATUS SMS: VMG URL is not set in config");
+ return null;
+ }
+ bundle.putString(Vvm3Subscriber.VMG_URL_KEY, vmgUrl);
+ VvmLog.i(TAG, "UNRECOGNIZED?cmd=STATUS translated into unprovisioned STATUS SMS");
+ return bundle;
+ }
+
+ private void startProvisionNewUser(
+ ActivationTask task,
+ PhoneAccountHandle phoneAccountHandle,
+ OmtpVvmCarrierConfigHelper config,
+ VoicemailStatus.Editor status,
+ StatusMessage message) {
+ try (NetworkWrapper wrapper =
+ VvmNetworkRequest.getNetwork(config, phoneAccountHandle, status)) {
+ Network network = wrapper.get();
+
+ VvmLog.i(TAG, "new user: network available");
+ try (ImapHelper helper =
+ new ImapHelper(config.getContext(), phoneAccountHandle, network, status)) {
+ // VVM3 has inconsistent error language code to OMTP. Just issue a raw command
+ // here.
+ // TODO(b/29082671): use LocaleList
+ if (Locale.getDefault().getLanguage().equals(new Locale(ISO639_Spanish).getLanguage())) {
+ // Spanish
+ helper.changeVoicemailTuiLanguage(VVM3_VM_LANGUAGE_SPANISH_STANDARD_NO_GUEST_PROMPTS);
+ } else {
+ // English
+ helper.changeVoicemailTuiLanguage(VVM3_VM_LANGUAGE_ENGLISH_STANDARD_NO_GUEST_PROMPTS);
+ }
+ VvmLog.i(TAG, "new user: language set");
+
+ if (setPin(config.getContext(), phoneAccountHandle, helper, message)) {
+ // Only close new user tutorial if the PIN has been changed.
+ helper.closeNewUserTutorial();
+ VvmLog.i(TAG, "new user: NUT closed");
+
+ config.requestStatus(null);
+ }
+ } catch (InitializingException | MessagingException | IOException e) {
+ config.handleEvent(status, OmtpEvents.VVM3_NEW_USER_SETUP_FAILED);
+ task.fail();
+ VvmLog.e(TAG, e.toString());
+ }
+ } catch (RequestFailedException e) {
+ config.handleEvent(status, OmtpEvents.DATA_NO_CONNECTION_CELLULAR_REQUIRED);
+ task.fail();
+ }
+ }
+
+ private static boolean setPin(
+ Context context,
+ PhoneAccountHandle phoneAccountHandle,
+ ImapHelper helper,
+ StatusMessage message)
+ throws IOException, MessagingException {
+ String defaultPin = getDefaultPin(message);
+ if (defaultPin == null) {
+ VvmLog.i(TAG, "cannot generate default PIN");
+ return false;
+ }
+
+ if (VoicemailChangePinActivity.isDefaultOldPinSet(context, phoneAccountHandle)) {
+ // The pin was already set
+ VvmLog.i(TAG, "PIN already set");
+ return true;
+ }
+ String newPin = generatePin(getMinimumPinLength(context, phoneAccountHandle));
+ if (helper.changePin(defaultPin, newPin) == OmtpConstants.CHANGE_PIN_SUCCESS) {
+ VoicemailChangePinActivity.setDefaultOldPIN(context, phoneAccountHandle, newPin);
+ helper.handleEvent(OmtpEvents.CONFIG_DEFAULT_PIN_REPLACED);
+ }
+ VvmLog.i(TAG, "new user: PIN set");
+ return true;
+ }
+
+ @Nullable
+ private static String getDefaultPin(StatusMessage message) {
+ // The IMAP username is [phone number]@example.com
+ String username = message.getImapUserName();
+ try {
+ String number = username.substring(0, username.indexOf('@'));
+ if (number.length() < 4) {
+ VvmLog.e(TAG, "unable to extract number from IMAP username");
+ return null;
+ }
+ return "1" + number.substring(number.length() - 4);
+ } catch (StringIndexOutOfBoundsException e) {
+ VvmLog.e(TAG, "unable to extract number from IMAP username");
+ return null;
+ }
+ }
+
+ private static int getMinimumPinLength(Context context, PhoneAccountHandle phoneAccountHandle) {
+ VisualVoicemailPreferences preferences =
+ new VisualVoicemailPreferences(context, phoneAccountHandle);
+ // The OMTP pin length format is {min}-{max}
+ String[] lengths = preferences.getString(OmtpConstants.TUI_PASSWORD_LENGTH, "").split("-");
+ if (lengths.length == 2) {
+ try {
+ return Integer.parseInt(lengths[0]);
+ } catch (NumberFormatException e) {
+ return DEFAULT_PIN_LENGTH;
+ }
+ }
+ return DEFAULT_PIN_LENGTH;
+ }
+
+ private static String generatePin(int length) {
+ SecureRandom random = new SecureRandom();
+ return String.format(Locale.US, "%010d", Math.abs(random.nextLong())).substring(0, length);
+ }
+}
diff --git a/java/com/android/voicemail/impl/protocol/Vvm3Subscriber.java b/java/com/android/voicemail/impl/protocol/Vvm3Subscriber.java
new file mode 100644
index 000000000..c8a74c8d5
--- /dev/null
+++ b/java/com/android/voicemail/impl/protocol/Vvm3Subscriber.java
@@ -0,0 +1,334 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.protocol;
+
+import android.annotation.TargetApi;
+import android.net.Network;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.support.annotation.WorkerThread;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import android.text.Html;
+import android.text.Spanned;
+import android.text.style.URLSpan;
+import android.util.ArrayMap;
+import com.android.voicemail.impl.ActivationTask;
+import com.android.voicemail.impl.Assert;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.sync.VvmNetworkRequest;
+import com.android.voicemail.impl.sync.VvmNetworkRequest.NetworkWrapper;
+import com.android.voicemail.impl.sync.VvmNetworkRequest.RequestFailedException;
+import com.android.volley.AuthFailureError;
+import com.android.volley.Request;
+import com.android.volley.RequestQueue;
+import com.android.volley.toolbox.HurlStack;
+import com.android.volley.toolbox.RequestFuture;
+import com.android.volley.toolbox.StringRequest;
+import com.android.volley.toolbox.Volley;
+import java.io.IOException;
+import java.net.CookieHandler;
+import java.net.CookieManager;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Class to subscribe to basic VVM3 visual voicemail, for example, Verizon. Subscription is required
+ * when the user is unprovisioned. This could happen when the user is on a legacy service, or
+ * switched over from devices that used other type of visual voicemail.
+ *
+ * <p>The STATUS SMS will come with a URL to the voicemail management gateway. From it we can find
+ * the self provisioning gateway URL that we can modify voicemail services.
+ *
+ * <p>A request to the self provisioning gateway to activate basic visual voicemail will return us
+ * with a web page. If the user hasn't subscribe to it yet it will contain a link to confirm the
+ * subscription. This link should be clicked through cellular network, and have cookies enabled.
+ *
+ * <p>After the process is completed, the carrier should send us another STATUS SMS with a new or
+ * ready user.
+ */
+@TargetApi(VERSION_CODES.O)
+public class Vvm3Subscriber {
+
+ private static final String TAG = "Vvm3Subscriber";
+
+ private static final String OPERATION_GET_SPG_URL = "retrieveSPGURL";
+ private static final String SPG_URL_TAG = "spgurl";
+ private static final String TRANSACTION_ID_TAG = "transactionid";
+ //language=XML
+ private static final String VMG_XML_REQUEST_FORMAT =
+ ""
+ + "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+ + "<VMGVVMRequest>"
+ + " <MessageHeader>"
+ + " <transactionid>%1$s</transactionid>"
+ + " </MessageHeader>"
+ + " <MessageBody>"
+ + " <mdn>%2$s</mdn>"
+ + " <operation>%3$s</operation>"
+ + " <source>Device</source>"
+ + " <devicemodel>%4$s</devicemodel>"
+ + " </MessageBody>"
+ + "</VMGVVMRequest>";
+
+ static final String VMG_URL_KEY = "vmg_url";
+
+ // Self provisioning POST key/values. VVM3 API 2.1.0 12.3
+ private static final String SPG_VZW_MDN_PARAM = "VZW_MDN";
+ private static final String SPG_VZW_SERVICE_PARAM = "VZW_SERVICE";
+ private static final String SPG_VZW_SERVICE_BASIC = "BVVM";
+ private static final String SPG_DEVICE_MODEL_PARAM = "DEVICE_MODEL";
+ // Value for all android device
+ private static final String SPG_DEVICE_MODEL_ANDROID = "DROID_4G";
+ private static final String SPG_APP_TOKEN_PARAM = "APP_TOKEN";
+ private static final String SPG_APP_TOKEN = "q8e3t5u2o1";
+ private static final String SPG_LANGUAGE_PARAM = "SPG_LANGUAGE_PARAM";
+ private static final String SPG_LANGUAGE_EN = "ENGLISH";
+
+ private static final String BASIC_SUBSCRIBE_LINK_TEXT = "Subscribe to Basic Visual Voice Mail";
+
+ private static final int REQUEST_TIMEOUT_SECONDS = 30;
+
+ private final ActivationTask mTask;
+ private final PhoneAccountHandle mHandle;
+ private final OmtpVvmCarrierConfigHelper mHelper;
+ private final VoicemailStatus.Editor mStatus;
+ private final Bundle mData;
+
+ private final String mNumber;
+
+ private RequestQueue mRequestQueue;
+
+ private static class ProvisioningException extends Exception {
+
+ public ProvisioningException(String message) {
+ super(message);
+ }
+ }
+
+ static {
+ // Set the default cookie handler to retain session data for the self provisioning gateway.
+ // Note; this is not ideal as it is application-wide, and can easily get clobbered.
+ // But it seems to be the preferred way to manage cookie for HttpURLConnection, and manually
+ // managing cookies will greatly increase complexity.
+ CookieManager cookieManager = new CookieManager();
+ CookieHandler.setDefault(cookieManager);
+ }
+
+ @WorkerThread
+ public Vvm3Subscriber(
+ ActivationTask task,
+ PhoneAccountHandle handle,
+ OmtpVvmCarrierConfigHelper helper,
+ VoicemailStatus.Editor status,
+ Bundle data) {
+ Assert.isNotMainThread();
+ mTask = task;
+ mHandle = handle;
+ mHelper = helper;
+ mStatus = status;
+ mData = data;
+
+ // Assuming getLine1Number() will work with VVM3. For unprovisioned users the IMAP username
+ // is not included in the status SMS, thus no other way to get the current phone number.
+ mNumber =
+ mHelper
+ .getContext()
+ .getSystemService(TelephonyManager.class)
+ .createForPhoneAccountHandle(mHandle)
+ .getLine1Number();
+ }
+
+ @WorkerThread
+ public void subscribe() {
+ Assert.isNotMainThread();
+ // Cellular data is required to subscribe.
+ // processSubscription() is called after network is available.
+ VvmLog.i(TAG, "Subscribing");
+
+ try (NetworkWrapper wrapper = VvmNetworkRequest.getNetwork(mHelper, mHandle, mStatus)) {
+ Network network = wrapper.get();
+ VvmLog.d(TAG, "provisioning: network available");
+ mRequestQueue =
+ Volley.newRequestQueue(mHelper.getContext(), new NetworkSpecifiedHurlStack(network));
+ processSubscription();
+ } catch (RequestFailedException e) {
+ mHelper.handleEvent(mStatus, OmtpEvents.VVM3_VMG_CONNECTION_FAILED);
+ mTask.fail();
+ }
+ }
+
+ private void processSubscription() {
+ try {
+ String gatewayUrl = getSelfProvisioningGateway();
+ String selfProvisionResponse = getSelfProvisionResponse(gatewayUrl);
+ String subscribeLink = findSubscribeLink(selfProvisionResponse);
+ clickSubscribeLink(subscribeLink);
+ } catch (ProvisioningException e) {
+ VvmLog.e(TAG, e.toString());
+ mTask.fail();
+ }
+ }
+
+ /** Get the URL to perform self-provisioning from the voicemail management gateway. */
+ private String getSelfProvisioningGateway() throws ProvisioningException {
+ VvmLog.i(TAG, "retrieving SPG URL");
+ String response = vvm3XmlRequest(OPERATION_GET_SPG_URL);
+ return extractText(response, SPG_URL_TAG);
+ }
+
+ /**
+ * Sent a request to the self-provisioning gateway, which will return us with a webpage. The page
+ * might contain a "Subscribe to Basic Visual Voice Mail" link to complete the subscription. The
+ * cookie from this response and cellular data is required to click the link.
+ */
+ private String getSelfProvisionResponse(String url) throws ProvisioningException {
+ VvmLog.i(TAG, "Retrieving self provisioning response");
+
+ RequestFuture<String> future = RequestFuture.newFuture();
+
+ StringRequest stringRequest =
+ new StringRequest(Request.Method.POST, url, future, future) {
+ @Override
+ protected Map<String, String> getParams() {
+ Map<String, String> params = new ArrayMap<>();
+ params.put(SPG_VZW_MDN_PARAM, mNumber);
+ params.put(SPG_VZW_SERVICE_PARAM, SPG_VZW_SERVICE_BASIC);
+ params.put(SPG_DEVICE_MODEL_PARAM, SPG_DEVICE_MODEL_ANDROID);
+ params.put(SPG_APP_TOKEN_PARAM, SPG_APP_TOKEN);
+ // Language to display the subscription page. The page is never shown to the user
+ // so just use English.
+ params.put(SPG_LANGUAGE_PARAM, SPG_LANGUAGE_EN);
+ return params;
+ }
+ };
+
+ mRequestQueue.add(stringRequest);
+ try {
+ return future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ } catch (InterruptedException | ExecutionException | TimeoutException e) {
+ mHelper.handleEvent(mStatus, OmtpEvents.VVM3_SPG_CONNECTION_FAILED);
+ throw new ProvisioningException(e.toString());
+ }
+ }
+
+ private void clickSubscribeLink(String subscribeLink) throws ProvisioningException {
+ VvmLog.i(TAG, "Clicking subscribe link");
+ RequestFuture<String> future = RequestFuture.newFuture();
+
+ StringRequest stringRequest =
+ new StringRequest(Request.Method.POST, subscribeLink, future, future);
+ mRequestQueue.add(stringRequest);
+ try {
+ // A new STATUS SMS will be sent after this request.
+ future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ } catch (TimeoutException | ExecutionException | InterruptedException e) {
+ mHelper.handleEvent(mStatus, OmtpEvents.VVM3_SPG_CONNECTION_FAILED);
+ throw new ProvisioningException(e.toString());
+ }
+ // It could take very long for the STATUS SMS to return. Waiting for it is unreliable.
+ // Just leave the CONFIG STATUS as CONFIGURING and end the task. The user can always
+ // manually retry if it took too long.
+ }
+
+ private String vvm3XmlRequest(String operation) throws ProvisioningException {
+ VvmLog.d(TAG, "Sending vvm3XmlRequest for " + operation);
+ String voicemailManagementGateway = mData.getString(VMG_URL_KEY);
+ if (voicemailManagementGateway == null) {
+ VvmLog.e(TAG, "voicemailManagementGateway url unknown");
+ return null;
+ }
+ String transactionId = createTransactionId();
+ String body =
+ String.format(
+ Locale.US, VMG_XML_REQUEST_FORMAT, transactionId, mNumber, operation, Build.MODEL);
+
+ RequestFuture<String> future = RequestFuture.newFuture();
+ StringRequest stringRequest =
+ new StringRequest(Request.Method.POST, voicemailManagementGateway, future, future) {
+ @Override
+ public byte[] getBody() throws AuthFailureError {
+ return body.getBytes();
+ }
+ };
+ mRequestQueue.add(stringRequest);
+
+ try {
+ String response = future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ if (!transactionId.equals(extractText(response, TRANSACTION_ID_TAG))) {
+ throw new ProvisioningException("transactionId mismatch");
+ }
+ return response;
+ } catch (InterruptedException | ExecutionException | TimeoutException e) {
+ mHelper.handleEvent(mStatus, OmtpEvents.VVM3_VMG_CONNECTION_FAILED);
+ throw new ProvisioningException(e.toString());
+ }
+ }
+
+ private String findSubscribeLink(String response) throws ProvisioningException {
+ Spanned doc = Html.fromHtml(response, Html.FROM_HTML_MODE_LEGACY);
+ URLSpan[] spans = doc.getSpans(0, doc.length(), URLSpan.class);
+ StringBuilder fulltext = new StringBuilder();
+ for (URLSpan span : spans) {
+ String text = doc.subSequence(doc.getSpanStart(span), doc.getSpanEnd(span)).toString();
+ if (BASIC_SUBSCRIBE_LINK_TEXT.equals(text)) {
+ return span.getURL();
+ }
+ fulltext.append(text);
+ }
+ throw new ProvisioningException("Subscribe link not found: " + fulltext);
+ }
+
+ private String createTransactionId() {
+ return String.valueOf(Math.abs(new Random().nextLong()));
+ }
+
+ private String extractText(String xml, String tag) throws ProvisioningException {
+ Pattern pattern = Pattern.compile("<" + tag + ">(.*)<\\/" + tag + ">");
+ Matcher matcher = pattern.matcher(xml);
+ if (matcher.find()) {
+ return matcher.group(1);
+ }
+ throw new ProvisioningException("Tag " + tag + " not found in xml response");
+ }
+
+ private static class NetworkSpecifiedHurlStack extends HurlStack {
+
+ private final Network mNetwork;
+
+ public NetworkSpecifiedHurlStack(Network network) {
+ mNetwork = network;
+ }
+
+ @Override
+ protected HttpURLConnection createConnection(URL url) throws IOException {
+ return (HttpURLConnection) mNetwork.openConnection(url);
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/res/layout/voicemail_change_pin.xml b/java/com/android/voicemail/impl/res/layout/voicemail_change_pin.xml
index b0db64b12..50c92777e 100644
--- a/java/com/android/voicemailomtp/res/layout/voicemail_change_pin.xml
+++ b/java/com/android/voicemail/impl/res/layout/voicemail_change_pin.xml
@@ -46,7 +46,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
- android:lines="2" />
+ android:lines="2"/>
<!-- error text ('PIN too short') -->
<TextView
diff --git a/java/com/android/voicemailomtp/res/values/arrays.xml b/java/com/android/voicemail/impl/res/values/arrays.xml
index 95714cf4d..95714cf4d 100644
--- a/java/com/android/voicemailomtp/res/values/arrays.xml
+++ b/java/com/android/voicemail/impl/res/values/arrays.xml
diff --git a/java/com/android/voicemailomtp/res/values/attrs.xml b/java/com/android/voicemail/impl/res/values/attrs.xml
index d1c7329d5..a1195c7ae 100644
--- a/java/com/android/voicemailomtp/res/values/attrs.xml
+++ b/java/com/android/voicemail/impl/res/values/attrs.xml
@@ -16,5 +16,5 @@
<resources>
- <attr name="preferenceBackgroundColor" format="color" />
+ <attr name="preferenceBackgroundColor" format="color"/>
</resources>
diff --git a/java/com/android/voicemailomtp/res/values/colors.xml b/java/com/android/voicemail/impl/res/values/colors.xml
index 8a897ab94..8a897ab94 100644
--- a/java/com/android/voicemailomtp/res/values/colors.xml
+++ b/java/com/android/voicemail/impl/res/values/colors.xml
diff --git a/java/com/android/voicemailomtp/res/values/config.xml b/java/com/android/voicemail/impl/res/values/config.xml
index 2f5603083..2f5603083 100644
--- a/java/com/android/voicemailomtp/res/values/config.xml
+++ b/java/com/android/voicemail/impl/res/values/config.xml
diff --git a/java/com/android/voicemailomtp/res/values/dimens.xml b/java/com/android/voicemail/impl/res/values/dimens.xml
index e66ca0921..e66ca0921 100644
--- a/java/com/android/voicemailomtp/res/values/dimens.xml
+++ b/java/com/android/voicemail/impl/res/values/dimens.xml
diff --git a/java/com/android/voicemailomtp/res/values/ids.xml b/java/com/android/voicemail/impl/res/values/ids.xml
index 84c685a14..84c685a14 100644
--- a/java/com/android/voicemailomtp/res/values/ids.xml
+++ b/java/com/android/voicemail/impl/res/values/ids.xml
diff --git a/java/com/android/voicemail/impl/res/values/strings.xml b/java/com/android/voicemail/impl/res/values/strings.xml
new file mode 100644
index 000000000..6c3d5527b
--- /dev/null
+++ b/java/com/android/voicemail/impl/res/values/strings.xml
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+ <!-- Title of the "Voicemail" settings screen, with a text label identifying which SIM the settings are for. -->
+ <string translatable="false" name="voicemail_settings_with_label">Voicemail (<xliff:g id="subscriptionlabel" example="Mock Carrier">%s</xliff:g>)</string>
+
+ <!-- Call settings screen, setting option name -->
+ <string translatable="false" name="voicemail_settings_title">Voicemail</string>
+
+ <!-- DO NOT TRANSLATE. Internal key for a voicemail notification preference. -->
+ <string translatable="false" name="voicemail_notification_ringtone_key">voicemail_notification_ringtone_key</string>
+ <!-- DO NOT TRANSLATE. Internal key for a voicemail notification preference. -->
+ <string translatable="false" name="voicemail_notification_vibrate_key">voicemail_notification_vibrate_key</string>
+
+ <!-- Title for the vibration settings for voicemail notifications [CHAR LIMIT=40] -->
+ <string name="voicemail_notification_vibrate_when_title">Vibrate</string>
+ <!-- Dialog title for the vibration settings for voice mail notifications [CHAR LIMIT=40]-->
+ <string name="voicemail_notification_vibarte_when_dialog_title">Vibrate</string>
+
+ <!-- Voicemail ringtone title. The user clicks on this preference to select
+ which sound to play when a voicemail notification is received.
+ [CHAR LIMIT=30] -->
+ <string name="voicemail_notification_ringtone_title">Sound</string>
+ <string translatable="false" name="voicemail_advanced_settings_key">voicemail_advanced_settings_key</string>
+
+ <!-- Title for advanced settings in the voicemail settings -->
+ <string name="voicemail_advanced_settings_title">Advanced Settings</string>
+
+ <!-- DO NOT TRANSLATE. Internal key for a visual voicemail preference. -->
+ <string translatable="false" name="voicemail_visual_voicemail_key">
+ voicemail_visual_voicemail_key
+ </string>
+ <!-- DO NOT TRANSLATE. Internal key for a visual voicemail archive preference. -->
+ <string translatable="false" name="voicemail_visual_voicemail_archive_key">
+ archive_is_enabled
+ </string>
+ <!-- DO NOT TRANSLATE. Internal key for a voicemail change pin preference. -->
+ <string translatable="false" name="voicemail_change_pin_key">voicemail_change_pin_key</string>
+
+ <!-- Visual voicemail on/off title [CHAR LIMIT=40] -->
+ <string translatable="false" name="voicemail_visual_voicemail_switch_title">Visual Voicemail</string>
+
+ <!-- Visual voicemail archive on/off title [CHAR LIMIT=40] -->
+ <string translatable="false" name="voicemail_visual_voicemail_auto_archive_switch_title">
+ Voicemail Auto Archive
+ </string>
+
+ <!-- Voicemail change PIN dialog title [CHAR LIMIT=40] -->
+ <string translatable="false" name="voicemail_set_pin_dialog_title">Set PIN</string>
+ <!-- Voicemail change PIN dialog title [CHAR LIMIT=40] -->
+ <string translatable="false" name="voicemail_change_pin_dialog_title">Change PIN</string>
+
+ <!-- Hint for the old PIN field in the change vociemail PIN dialog -->
+ <string translatable="false" name="vm_change_pin_old_pin">Old PIN</string>
+ <!-- Hint for the new PIN field in the change vociemail PIN dialog -->
+ <string translatable="false" name="vm_change_pin_new_pin">New PIN</string>
+
+ <!-- Message on the dialog when PIN changing is in progress -->
+ <string translatable="false" name="vm_change_pin_progress_message">Please wait.</string>
+ <!-- Error message for the voicemail PIN change if the PIN is too short -->
+ <string translatable="false" name="vm_change_pin_error_too_short">The new PIN is too short.</string>
+ <!-- Error message for the voicemail PIN change if the PIN is too long -->
+ <string translatable="false" name="vm_change_pin_error_too_long">The new PIN is too long.</string>
+ <!-- Error message for the voicemail PIN change if the PIN is too weak -->
+ <string translatable="false" name="vm_change_pin_error_too_weak">The new PIN is too weak. A strong password should not have continuous sequence or repeated digits.</string>
+ <!-- Error message for the voicemail PIN change if the old PIN entered doesn't match -->
+ <string translatable="false" name="vm_change_pin_error_mismatch">The old PIN does not match.</string>
+ <!-- Error message for the voicemail PIN change if the new PIN contains invalid character -->
+ <string translatable="false" name="vm_change_pin_error_invalid">The new PIN contains invalid characters.</string>
+ <!-- Error message for the voicemail PIN change if operation has failed -->
+ <string translatable="false" name="vm_change_pin_error_system_error">Unable to change PIN</string>
+ <!-- Message to replace the transcription if a visual voicemail message is not supported-->
+ <string translatable="false" name="vvm_unsupported_message_format">Unsupported message type, call <xliff:g id="number" example="*86">%s</xliff:g> to listen.</string>
+
+ <!-- The title for the change voicemail PIN activity -->
+ <string translatable="false" name="change_pin_title">Change Voicemail PIN</string>
+ <!-- The label for the continue button in change voicemail PIN activity -->
+ <string translatable="false" name="change_pin_continue_label">Continue</string>
+ <!-- The label for the cancel button in change voicemail PIN activity -->
+ <string translatable="false" name="change_pin_cancel_label">Cancel</string>
+ <!-- The label for the ok button in change voicemail PIN activity -->
+ <string translatable="false" name="change_pin_ok_label">Ok</string>
+ <!-- The title for the enter old pin step in change voicemail PIN activity -->
+ <string translatable="false" name="change_pin_enter_old_pin_header">Confirm your old PIN</string>
+ <!-- The hint for the enter old pin step in change voicemail PIN activity -->
+ <string translatable="false" name="change_pin_enter_old_pin_hint">Enter your voicemail PIN to continue.</string>
+ <!-- The title for the enter new pin step in change voicemail PIN activity -->
+ <string translatable="false" name="change_pin_enter_new_pin_header">Set a new PIN</string>
+ <!-- The hint for the enter new pin step in change voicemail PIN activity -->
+ <string translatable="false" name="change_pin_enter_new_pin_hint">PIN must be <xliff:g id="min" example="4">%1$d</xliff:g>-<xliff:g id="max" example="7">%2$d</xliff:g> digits.</string>
+ <!-- The title for the confirm new pin step in change voicemail PIN activity -->
+ <string translatable="false" name="change_pin_confirm_pin_header">Confirm your PIN</string>
+ <!-- The error message for th confirm new pin step in change voicemail PIN activity, if the pin doen't match the one previously entered -->
+ <string translatable="false" name="change_pin_confirm_pins_dont_match">PINs don\'t match</string>
+ <!-- The toast to show after the voicemail PIN has been successfully changed -->
+ <string translatable="false" name="change_pin_succeeded">Voicemail PIN updated</string>
+ <!-- The error message to show if the server reported an error while attempting to change the voicemail PIN -->
+ <string translatable="false" name="change_pin_system_error">Unable to set PIN</string>
+</resources>
diff --git a/java/com/android/voicemailomtp/res/values/styles.xml b/java/com/android/voicemail/impl/res/values/styles.xml
index 8a897ab94..8a897ab94 100644
--- a/java/com/android/voicemailomtp/res/values/styles.xml
+++ b/java/com/android/voicemail/impl/res/values/styles.xml
diff --git a/java/com/android/voicemail/impl/res/xml/voicemail_settings.xml b/java/com/android/voicemail/impl/res/xml/voicemail_settings.xml
new file mode 100644
index 000000000..22437337c
--- /dev/null
+++ b/java/com/android/voicemail/impl/res/xml/voicemail_settings.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+ android:title="@string/voicemail_settings_title">
+
+ <com.android.voicemail.impl.settings.VoicemailRingtonePreference
+ android:key="@string/voicemail_notification_ringtone_key"
+ android:title="@string/voicemail_notification_ringtone_title"
+ android:persistent="false"
+ android:ringtoneType="notification" />
+
+ <CheckBoxPreference
+ android:key="@string/voicemail_notification_vibrate_key"
+ android:title="@string/voicemail_notification_vibrate_when_title"
+ android:persistent="true" />
+
+ <SwitchPreference
+ android:key="@string/voicemail_visual_voicemail_key"
+ android:title="@string/voicemail_visual_voicemail_switch_title"/>"
+
+ <SwitchPreference
+ android:key="@string/voicemail_visual_voicemail_archive_key"
+ android:dependency="@string/voicemail_visual_voicemail_key"
+ android:title="@string/voicemail_visual_voicemail_auto_archive_switch_title"/>"
+ <Preference
+ android:key="@string/voicemail_change_pin_key"
+ android:title="@string/voicemail_change_pin_dialog_title"/>
+
+ <PreferenceScreen
+ android:key="@string/voicemail_advanced_settings_key"
+ android:title="@string/voicemail_advanced_settings_title">
+ </PreferenceScreen>
+</PreferenceScreen>
diff --git a/java/com/android/voicemailomtp/res/xml/vvm_config.xml b/java/com/android/voicemail/impl/res/xml/vvm_config.xml
index 19c667e13..230d40f90 100644
--- a/java/com/android/voicemailomtp/res/xml/vvm_config.xml
+++ b/java/com/android/voicemail/impl/res/xml/vvm_config.xml
@@ -29,13 +29,17 @@
<item value="20802"/>
</string-array>
- <int name="vvm_port_number_int" value="20481"/>
+ <int
+ name="vvm_port_number_int"
+ value="20481"/>
<string name="vvm_destination_number_string">21101</string>
<string-array name="carrier_vvm_package_name_string_array">
<item value="com.orange.vvm"/>
</string-array>
<string name="vvm_type_string">vvm_type_omtp</string>
- <boolean name="vvm_cellular_data_required_bool" value="true"/>
+ <boolean
+ name="vvm_cellular_data_required_bool"
+ value="true"/>
<string-array name="vvm_disabled_capabilities_string_array">
<!-- b/32365569 -->
<item value="STARTTLS"/>
@@ -64,8 +68,12 @@
<item value="310800"/>
</string-array>
- <int name="vvm_port_number_int" value="1808"/>
- <int name="vvm_ssl_port_number_int" value="993"/>
+ <int
+ name="vvm_port_number_int"
+ value="1808"/>
+ <int
+ name="vvm_ssl_port_number_int"
+ value="993"/>
<string name="vvm_destination_number_string">122</string>
<string-array name="carrier_vvm_package_name_string_array">
<item value="com.tmobile.vvm.application"/>
@@ -121,12 +129,18 @@
<item value="311489"/>
</string-array>
- <int name="vvm_port_number_int" value="0"/>
+ <int
+ name="vvm_port_number_int"
+ value="0"/>
<string name="vvm_destination_number_string">900080006200</string>
<string name="vvm_type_string">vvm_type_vvm3</string>
<string name="vvm_client_prefix_string">//VZWVVM</string>
- <boolean name="vvm_cellular_data_required_bool" value="true"/>
- <boolean name="vvm_legacy_mode_enabled_bool" value="true"/>
+ <boolean
+ name="vvm_cellular_data_required_bool"
+ value="true"/>
+ <boolean
+ name="vvm_legacy_mode_enabled_bool"
+ value="true"/>
<!-- VVM3 specific value for the voicemail management gateway to use if the SMS didn't provide
one -->
<string name="default_vmg_url">https://mobile.vzw.com/VMGIMS/VMServices</string>
diff --git a/java/com/android/voicemail/impl/scheduling/BaseTask.java b/java/com/android/voicemail/impl/scheduling/BaseTask.java
new file mode 100644
index 000000000..4cc6dd59e
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/BaseTask.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.scheduling;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.SystemClock;
+import android.support.annotation.CallSuper;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.WorkerThread;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.Assert;
+import com.android.voicemail.impl.NeededForTesting;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Provides common utilities for task implementations, such as execution time and managing {@link
+ * Policy}
+ */
+public abstract class BaseTask implements Task {
+
+ private static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle";
+
+ private Context mContext;
+
+ private int mId;
+ private PhoneAccountHandle mPhoneAccountHandle;
+
+ private boolean mHasStarted;
+ private volatile boolean mHasFailed;
+
+ @NonNull private final List<Policy> mPolicies = new ArrayList<>();
+
+ private long mExecutionTime;
+
+ private static Clock sClock = new Clock();
+
+ protected BaseTask(int id) {
+ mId = id;
+ mExecutionTime = getTimeMillis();
+ }
+
+ /**
+ * Modify the task ID to prevent arbitrary task from executing. Can only be called before {@link
+ * #onCreate(Context, Intent, int, int)} returns.
+ */
+ @MainThread
+ public void setId(int id) {
+ Assert.isMainThread();
+ mId = id;
+ }
+
+ @MainThread
+ public boolean hasStarted() {
+ Assert.isMainThread();
+ return mHasStarted;
+ }
+
+ @MainThread
+ public boolean hasFailed() {
+ Assert.isMainThread();
+ return mHasFailed;
+ }
+
+ public Context getContext() {
+ return mContext;
+ }
+
+ public PhoneAccountHandle getPhoneAccountHandle() {
+ return mPhoneAccountHandle;
+ }
+ /**
+ * Should be call in the constructor or {@link Policy#onCreate(BaseTask, Intent, int, int)} will
+ * be missed.
+ */
+ @MainThread
+ public BaseTask addPolicy(Policy policy) {
+ Assert.isMainThread();
+ mPolicies.add(policy);
+ return this;
+ }
+
+ /**
+ * Indicate the task has failed. {@link Policy#onFail()} will be triggered once the execution
+ * ends. This mechanism is used by policies for actions such as determining whether to schedule a
+ * retry. Must be call inside {@link #onExecuteInBackgroundThread()}
+ */
+ @WorkerThread
+ public void fail() {
+ Assert.isNotMainThread();
+ mHasFailed = true;
+ }
+
+ @MainThread
+ public void setExecutionTime(long timeMillis) {
+ Assert.isMainThread();
+ mExecutionTime = timeMillis;
+ }
+
+ public long getTimeMillis() {
+ return sClock.getTimeMillis();
+ }
+
+ /**
+ * Creates an intent that can be used to restart the current task. Derived class should build
+ * their intent upon this.
+ */
+ public Intent createRestartIntent() {
+ return createIntent(getContext(), this.getClass(), mPhoneAccountHandle);
+ }
+
+ /**
+ * Creates an intent that can be used to start the {@link TaskSchedulerService}. Derived class
+ * should build their intent upon this.
+ */
+ public static Intent createIntent(
+ Context context, Class<? extends BaseTask> task, PhoneAccountHandle phoneAccountHandle) {
+ Intent intent = TaskSchedulerService.createIntent(context, task);
+ intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle);
+ return intent;
+ }
+
+ @Override
+ public TaskId getId() {
+ return new TaskId(mId, mPhoneAccountHandle);
+ }
+
+ @Override
+ @CallSuper
+ public void onCreate(Context context, Intent intent, int flags, int startId) {
+ mContext = context;
+ mPhoneAccountHandle = intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE);
+ for (Policy policy : mPolicies) {
+ policy.onCreate(this, intent, flags, startId);
+ }
+ }
+
+ @Override
+ public long getReadyInMilliSeconds() {
+ return mExecutionTime - getTimeMillis();
+ }
+
+ @Override
+ @CallSuper
+ public void onBeforeExecute() {
+ for (Policy policy : mPolicies) {
+ policy.onBeforeExecute();
+ }
+ mHasStarted = true;
+ }
+
+ @Override
+ @CallSuper
+ public void onCompleted() {
+ if (mHasFailed) {
+ for (Policy policy : mPolicies) {
+ policy.onFail();
+ }
+ }
+
+ for (Policy policy : mPolicies) {
+ policy.onCompleted();
+ }
+ }
+
+ @Override
+ public void onDuplicatedTaskAdded(Task task) {
+ for (Policy policy : mPolicies) {
+ policy.onDuplicatedTaskAdded();
+ }
+ }
+
+ @NeededForTesting
+ static class Clock {
+
+ public long getTimeMillis() {
+ return SystemClock.elapsedRealtime();
+ }
+ }
+
+ /** Used to replace the clock with an deterministic clock */
+ @NeededForTesting
+ static void setClockForTesting(Clock clock) {
+ sClock = clock;
+ }
+}
diff --git a/java/com/android/voicemail/impl/scheduling/BlockerTask.java b/java/com/android/voicemail/impl/scheduling/BlockerTask.java
new file mode 100644
index 000000000..353508d56
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/BlockerTask.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.scheduling;
+
+import android.content.Context;
+import android.content.Intent;
+import com.android.voicemail.impl.VvmLog;
+
+/** Task to block another task of the same ID from being queued for a certain amount of time. */
+public class BlockerTask extends BaseTask {
+
+ private static final String TAG = "BlockerTask";
+
+ public static final String EXTRA_TASK_ID = "extra_task_id";
+ public static final String EXTRA_BLOCK_FOR_MILLIS = "extra_block_for_millis";
+
+ public BlockerTask() {
+ super(TASK_INVALID);
+ }
+
+ @Override
+ public void onCreate(Context context, Intent intent, int flags, int startId) {
+ super.onCreate(context, intent, flags, startId);
+ setId(intent.getIntExtra(EXTRA_TASK_ID, TASK_INVALID));
+ setExecutionTime(getTimeMillis() + intent.getIntExtra(EXTRA_BLOCK_FOR_MILLIS, 0));
+ }
+
+ @Override
+ public void onExecuteInBackgroundThread() {
+ // Do nothing.
+ }
+
+ @Override
+ public void onDuplicatedTaskAdded(Task task) {
+ VvmLog.v(TAG, task.toString() + "blocked, " + getReadyInMilliSeconds() + "millis remaining");
+ }
+}
diff --git a/java/com/android/voicemail/impl/scheduling/MinimalIntervalPolicy.java b/java/com/android/voicemail/impl/scheduling/MinimalIntervalPolicy.java
new file mode 100644
index 000000000..8b2fe7098
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/MinimalIntervalPolicy.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.scheduling;
+
+import android.content.Intent;
+import com.android.voicemail.impl.scheduling.Task.TaskId;
+
+/**
+ * If a task with this policy succeeds, a {@link BlockerTask} with the same {@link TaskId} of the
+ * task will be queued immediately, preventing the same task from running for a certain amount of
+ * time.
+ */
+public class MinimalIntervalPolicy implements Policy {
+
+ BaseTask mTask;
+ TaskId mId;
+ int mBlockForMillis;
+
+ public MinimalIntervalPolicy(int blockForMillis) {
+ mBlockForMillis = blockForMillis;
+ }
+
+ @Override
+ public void onCreate(BaseTask task, Intent intent, int flags, int startId) {
+ mTask = task;
+ mId = mTask.getId();
+ }
+
+ @Override
+ public void onBeforeExecute() {}
+
+ @Override
+ public void onCompleted() {
+ if (!mTask.hasFailed()) {
+ Intent intent =
+ mTask.createIntent(mTask.getContext(), BlockerTask.class, mId.phoneAccountHandle);
+ intent.putExtra(BlockerTask.EXTRA_TASK_ID, mId.id);
+ intent.putExtra(BlockerTask.EXTRA_BLOCK_FOR_MILLIS, mBlockForMillis);
+ mTask.getContext().startService(intent);
+ }
+ }
+
+ @Override
+ public void onFail() {}
+
+ @Override
+ public void onDuplicatedTaskAdded() {}
+}
diff --git a/java/com/android/voicemailomtp/scheduling/Policy.java b/java/com/android/voicemail/impl/scheduling/Policy.java
index 4a475d2ed..607782191 100644
--- a/java/com/android/voicemailomtp/scheduling/Policy.java
+++ b/java/com/android/voicemail/impl/scheduling/Policy.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.voicemailomtp.scheduling;
+package com.android.voicemail.impl.scheduling;
import android.content.Intent;
@@ -24,13 +24,13 @@ import android.content.Intent;
*/
public interface Policy {
- void onCreate(BaseTask task, Intent intent, int flags, int startId);
+ void onCreate(BaseTask task, Intent intent, int flags, int startId);
- void onBeforeExecute();
+ void onBeforeExecute();
- void onCompleted();
+ void onCompleted();
- void onFail();
+ void onFail();
- void onDuplicatedTaskAdded();
+ void onDuplicatedTaskAdded();
}
diff --git a/java/com/android/voicemailomtp/scheduling/PostponePolicy.java b/java/com/android/voicemail/impl/scheduling/PostponePolicy.java
index 27a82f0ef..e24df0c7a 100644
--- a/java/com/android/voicemailomtp/scheduling/PostponePolicy.java
+++ b/java/com/android/voicemail/impl/scheduling/PostponePolicy.java
@@ -14,11 +14,10 @@
* limitations under the License
*/
-package com.android.voicemailomtp.scheduling;
+package com.android.voicemail.impl.scheduling;
import android.content.Intent;
-
-import com.android.voicemailomtp.VvmLog;
+import com.android.voicemail.impl.VvmLog;
/**
* A task with Postpone policy will not be executed immediately. It will wait for a while and if a
@@ -28,42 +27,42 @@ import com.android.voicemailomtp.VvmLog;
*/
public class PostponePolicy implements Policy {
- private static final String TAG = "PostponePolicy";
+ private static final String TAG = "PostponePolicy";
- private final int mPostponeMillis;
- private BaseTask mTask;
+ private final int mPostponeMillis;
+ private BaseTask mTask;
- public PostponePolicy(int postponeMillis) {
- mPostponeMillis = postponeMillis;
- }
+ public PostponePolicy(int postponeMillis) {
+ mPostponeMillis = postponeMillis;
+ }
- @Override
- public void onCreate(BaseTask task, Intent intent, int flags, int startId) {
- mTask = task;
- mTask.setExecutionTime(mTask.getTimeMillis() + mPostponeMillis);
- }
+ @Override
+ public void onCreate(BaseTask task, Intent intent, int flags, int startId) {
+ mTask = task;
+ mTask.setExecutionTime(mTask.getTimeMillis() + mPostponeMillis);
+ }
- @Override
- public void onBeforeExecute() {
- // Do nothing
- }
+ @Override
+ public void onBeforeExecute() {
+ // Do nothing
+ }
- @Override
- public void onCompleted() {
- // Do nothing
- }
+ @Override
+ public void onCompleted() {
+ // Do nothing
+ }
- @Override
- public void onFail() {
- // Do nothing
- }
+ @Override
+ public void onFail() {
+ // Do nothing
+ }
- @Override
- public void onDuplicatedTaskAdded() {
- if (mTask.hasStarted()) {
- return;
- }
- VvmLog.d(TAG, "postponing " + mTask);
- mTask.setExecutionTime(mTask.getTimeMillis() + mPostponeMillis);
+ @Override
+ public void onDuplicatedTaskAdded() {
+ if (mTask.hasStarted()) {
+ return;
}
+ VvmLog.d(TAG, "postponing " + mTask);
+ mTask.setExecutionTime(mTask.getTimeMillis() + mPostponeMillis);
+ }
}
diff --git a/java/com/android/voicemail/impl/scheduling/RetryPolicy.java b/java/com/android/voicemail/impl/scheduling/RetryPolicy.java
new file mode 100644
index 000000000..a8e4a3d3c
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/RetryPolicy.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.scheduling;
+
+import android.content.Intent;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VvmLog;
+
+/**
+ * A task with this policy will automatically re-queue itself if {@link BaseTask#fail()} has been
+ * called during {@link BaseTask#onExecuteInBackgroundThread()}. A task will be retried at most
+ * <code>retryLimit</code> times and with a <code>retryDelayMillis</code> interval in between.
+ */
+public class RetryPolicy implements Policy {
+
+ private static final String TAG = "RetryPolicy";
+ private static final String EXTRA_RETRY_COUNT = "extra_retry_count";
+
+ private final int mRetryLimit;
+ private final int mRetryDelayMillis;
+
+ private BaseTask mTask;
+
+ private int mRetryCount;
+ private boolean mFailed;
+
+ private VoicemailStatus.DeferredEditor mVoicemailStatusEditor;
+
+ public RetryPolicy(int retryLimit, int retryDelayMillis) {
+ mRetryLimit = retryLimit;
+ mRetryDelayMillis = retryDelayMillis;
+ }
+
+ private boolean hasMoreRetries() {
+ return mRetryCount < mRetryLimit;
+ }
+
+ /**
+ * Error status should only be set if retries has exhausted or the task is successful. Status
+ * writes to this editor will be deferred until the task has ended, and will only be committed if
+ * the task is successful or there are no retries left.
+ */
+ public VoicemailStatus.Editor getVoicemailStatusEditor() {
+ return mVoicemailStatusEditor;
+ }
+
+ @Override
+ public void onCreate(BaseTask task, Intent intent, int flags, int startId) {
+ mTask = task;
+ mRetryCount = intent.getIntExtra(EXTRA_RETRY_COUNT, 0);
+ if (mRetryCount > 0) {
+ VvmLog.d(
+ TAG,
+ "retry #" + mRetryCount + " for " + mTask + " queued, executing in " + mRetryDelayMillis);
+ mTask.setExecutionTime(mTask.getTimeMillis() + mRetryDelayMillis);
+ }
+ PhoneAccountHandle phoneAccountHandle = task.getPhoneAccountHandle();
+ if (phoneAccountHandle == null) {
+ VvmLog.e(TAG, "null phone account for phoneAccountHandle " + task.getPhoneAccountHandle());
+ // This should never happen, but continue on if it does. The status write will be
+ // discarded.
+ }
+ mVoicemailStatusEditor = VoicemailStatus.deferredEdit(task.getContext(), phoneAccountHandle);
+ }
+
+ @Override
+ public void onBeforeExecute() {}
+
+ @Override
+ public void onCompleted() {
+ if (!mFailed || !hasMoreRetries()) {
+ if (!mFailed) {
+ VvmLog.d(TAG, mTask.toString() + " completed successfully");
+ }
+ if (!hasMoreRetries()) {
+ VvmLog.d(TAG, "Retry limit for " + mTask + " reached");
+ }
+ VvmLog.i(TAG, "committing deferred status: " + mVoicemailStatusEditor.getValues());
+ mVoicemailStatusEditor.deferredApply();
+ return;
+ }
+ VvmLog.i(TAG, "discarding deferred status: " + mVoicemailStatusEditor.getValues());
+ Intent intent = mTask.createRestartIntent();
+ intent.putExtra(EXTRA_RETRY_COUNT, mRetryCount + 1);
+
+ mTask.getContext().startService(intent);
+ }
+
+ @Override
+ public void onFail() {
+ mFailed = true;
+ }
+
+ @Override
+ public void onDuplicatedTaskAdded() {}
+}
diff --git a/java/com/android/voicemail/impl/scheduling/Task.java b/java/com/android/voicemail/impl/scheduling/Task.java
new file mode 100644
index 000000000..2d08f5b03
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/Task.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.scheduling;
+
+import android.content.Context;
+import android.content.Intent;
+import android.support.annotation.MainThread;
+import android.support.annotation.WorkerThread;
+import android.telecom.PhoneAccountHandle;
+import java.util.Objects;
+
+/**
+ * A task for {@link TaskSchedulerService} to execute. Since the task is sent through a intent to
+ * the scheduler, The task must be constructable with the intent. Specifically, It must have a
+ * constructor with zero arguments, and have all relevant data packed inside the intent. Use {@link
+ * TaskSchedulerService#createIntent(Context, Class)} to create a intent that will construct the
+ * Task.
+ *
+ * <p>Only {@link #onExecuteInBackgroundThread()} is run on the worker thread.
+ */
+public interface Task {
+
+ /**
+ * TaskId to indicate it has not be set. If a task does not provide a default TaskId it should be
+ * set before {@link Task#onCreate(Context, Intent, int, int) returns}
+ */
+ int TASK_INVALID = -1;
+
+ /**
+ * TaskId to indicate it should always be queued regardless of duplicates. {@link
+ * Task#onDuplicatedTaskAdded(Task)} will never be called on tasks with this TaskId.
+ */
+ int TASK_ALLOW_DUPLICATES = -2;
+
+ int TASK_UPLOAD = 1;
+ int TASK_SYNC = 2;
+ int TASK_ACTIVATION = 3;
+
+ /**
+ * Used to differentiate between types of tasks. If a task with the same TaskId is already in the
+ * queue the new task will be rejected.
+ */
+ class TaskId {
+
+ /** Indicates the operation type of the task. */
+ public final int id;
+ /**
+ * Same operation for a different phoneAccountHandle is allowed. phoneAccountHandle is used to
+ * differentiate phone accounts in multi-SIM scenario. For example, each SIM can queue a sync
+ * task for their own.
+ */
+ public final PhoneAccountHandle phoneAccountHandle;
+
+ public TaskId(int id, PhoneAccountHandle phoneAccountHandle) {
+ this.id = id;
+ this.phoneAccountHandle = phoneAccountHandle;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (!(object instanceof TaskId)) {
+ return false;
+ }
+ TaskId other = (TaskId) object;
+ return id == other.id && phoneAccountHandle.equals(other.phoneAccountHandle);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, phoneAccountHandle);
+ }
+ }
+
+ TaskId getId();
+
+ @MainThread
+ void onCreate(Context context, Intent intent, int flags, int startId);
+
+ /**
+ * @return number of milliSeconds the scheduler should wait before running this task. A value less
+ * than {@link TaskSchedulerService#READY_TOLERANCE_MILLISECONDS} will be considered ready. If
+ * no tasks are ready, the scheduler will sleep for this amount of time before doing another
+ * check (it will still wake if a new task is added). The first task in the queue that is
+ * ready will be executed.
+ */
+ @MainThread
+ long getReadyInMilliSeconds();
+
+ /**
+ * Called on the main thread when the scheduler is about to send the task into the worker thread,
+ * calling {@link #onExecuteInBackgroundThread()}
+ */
+ @MainThread
+ void onBeforeExecute();
+
+ /** The actual payload of the task, executed on the worker thread. */
+ @WorkerThread
+ void onExecuteInBackgroundThread();
+
+ /**
+ * Called on the main thread when {@link #onExecuteInBackgroundThread()} has finished or thrown an
+ * uncaught exception. The task is already removed from the queue at this point, and a same task
+ * can be queued again.
+ */
+ @MainThread
+ void onCompleted();
+
+ /**
+ * Another task with the same TaskId has been added. Necessary data can be retrieved from the
+ * other task, and after this returns the task will be discarded.
+ */
+ @MainThread
+ void onDuplicatedTaskAdded(Task task);
+}
diff --git a/java/com/android/voicemail/impl/scheduling/TaskSchedulerService.java b/java/com/android/voicemail/impl/scheduling/TaskSchedulerService.java
new file mode 100644
index 000000000..81bd36fee
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/TaskSchedulerService.java
@@ -0,0 +1,396 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.scheduling;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.os.SystemClock;
+import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.annotation.WorkerThread;
+import com.android.voicemail.impl.Assert;
+import com.android.voicemail.impl.NeededForTesting;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.scheduling.Task.TaskId;
+import java.util.ArrayDeque;
+import java.util.Queue;
+
+/**
+ * A service to queue and run {@link Task} on a worker thread. Only one task will be ran at a time,
+ * and same task cannot exist in the queue at the same time. The service will be started when a
+ * intent is received, and stopped when there are no more tasks in the queue.
+ */
+public class TaskSchedulerService extends Service {
+
+ private static final String TAG = "VvmTaskScheduler";
+
+ private static final String ACTION_WAKEUP = "action_wakeup";
+
+ private static final int READY_TOLERANCE_MILLISECONDS = 100;
+
+ /**
+ * Threshold to determine whether to do a short or long sleep when a task is scheduled in the
+ * future.
+ *
+ * <p>A short sleep will continue to held the wake lock and use {@link
+ * Handler#postDelayed(Runnable, long)} to wait for the next task.
+ *
+ * <p>A long sleep will release the wake lock and set a {@link AlarmManager} alarm. The alarm is
+ * exact and will wake up the device. Note: as this service is run in the telephony process it
+ * does not seem to be restricted by doze or sleep, it will fire exactly at the moment. The
+ * unbundled version should take doze into account.
+ */
+ private static final int SHORT_SLEEP_THRESHOLD_MILLISECONDS = 60_000;
+ /**
+ * When there are no more tasks to be run the service should be stopped. But when all tasks has
+ * finished there might still be more tasks in the message queue waiting to be processed,
+ * especially the ones submitted in {@link Task#onCompleted()}. Wait for a while before stopping
+ * the service to make sure there are no pending messages.
+ */
+ private static final int STOP_DELAY_MILLISECONDS = 5_000;
+
+ private static final String EXTRA_CLASS_NAME = "extra_class_name";
+
+ private static final String WAKE_LOCK_TAG = "TaskSchedulerService_wakelock";
+
+ // The thread to run tasks on
+ private volatile WorkerThreadHandler mWorkerThreadHandler;
+
+ private Context mContext = this;
+ /**
+ * Used by tests to turn task handling into a single threaded process by calling {@link
+ * Handler#handleMessage(Message)} directly
+ */
+ private MessageSender mMessageSender = new MessageSender();
+
+ private MainThreadHandler mMainThreadHandler;
+
+ private WakeLock mWakeLock;
+
+ /** Main thread only, access through {@link #getTasks()} */
+ private final Queue<Task> mTasks = new ArrayDeque<>();
+
+ private boolean mWorkerThreadIsBusy = false;
+
+ private final Runnable mStopServiceWithDelay =
+ new Runnable() {
+ @Override
+ public void run() {
+ VvmLog.d(TAG, "Stopping service");
+ stopSelf();
+ }
+ };
+ /** Should attempt to run the next task when a task has finished or been added. */
+ private boolean mTaskAutoRunDisabledForTesting = false;
+
+ @VisibleForTesting
+ final class WorkerThreadHandler extends Handler {
+
+ public WorkerThreadHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ @WorkerThread
+ public void handleMessage(Message msg) {
+ Assert.isNotMainThread();
+ Task task = (Task) msg.obj;
+ try {
+ VvmLog.v(TAG, "executing task " + task);
+ task.onExecuteInBackgroundThread();
+ } catch (Throwable throwable) {
+ VvmLog.e(TAG, "Exception while executing task " + task + ":", throwable);
+ }
+
+ Message schedulerMessage = mMainThreadHandler.obtainMessage();
+ schedulerMessage.obj = task;
+ mMessageSender.send(schedulerMessage);
+ }
+ }
+
+ @VisibleForTesting
+ final class MainThreadHandler extends Handler {
+
+ public MainThreadHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ @MainThread
+ public void handleMessage(Message msg) {
+ Assert.isMainThread();
+ Task task = (Task) msg.obj;
+ getTasks().remove(task);
+ task.onCompleted();
+ mWorkerThreadIsBusy = false;
+ maybeRunNextTask();
+ }
+ }
+
+ @Override
+ @MainThread
+ public void onCreate() {
+ super.onCreate();
+ mWakeLock =
+ getSystemService(PowerManager.class)
+ .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG);
+ mWakeLock.setReferenceCounted(false);
+ HandlerThread thread = new HandlerThread("VvmTaskSchedulerService");
+ thread.start();
+
+ mWorkerThreadHandler = new WorkerThreadHandler(thread.getLooper());
+ mMainThreadHandler = new MainThreadHandler(Looper.getMainLooper());
+ }
+
+ @Override
+ public void onDestroy() {
+ mWorkerThreadHandler.getLooper().quit();
+ mWakeLock.release();
+ }
+
+ @Override
+ @MainThread
+ public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
+ Assert.isMainThread();
+ // maybeRunNextTask() will release the wakelock either by entering a long sleep or stopping
+ // the service.
+ mWakeLock.acquire();
+ if (ACTION_WAKEUP.equals(intent.getAction())) {
+ VvmLog.d(TAG, "woke up by AlarmManager");
+ } else {
+ Task task = createTask(intent, flags, startId);
+ if (task == null) {
+ VvmLog.e(TAG, "cannot create task form intent");
+ } else {
+ addTask(task);
+ }
+ }
+ maybeRunNextTask();
+ // STICKY means the service will be automatically restarted will the last intent if it is
+ // killed.
+ return START_NOT_STICKY;
+ }
+
+ @MainThread
+ @VisibleForTesting
+ void addTask(Task task) {
+ Assert.isMainThread();
+ if (task.getId().id == Task.TASK_INVALID) {
+ throw new AssertionError("Task id was not set to a valid value before adding.");
+ }
+ if (task.getId().id != Task.TASK_ALLOW_DUPLICATES) {
+ Task oldTask = getTask(task.getId());
+ if (oldTask != null) {
+ oldTask.onDuplicatedTaskAdded(task);
+ return;
+ }
+ }
+ mMainThreadHandler.removeCallbacks(mStopServiceWithDelay);
+ getTasks().add(task);
+ maybeRunNextTask();
+ }
+
+ @MainThread
+ @Nullable
+ private Task getTask(TaskId taskId) {
+ Assert.isMainThread();
+ for (Task task : getTasks()) {
+ if (task.getId().equals(taskId)) {
+ return task;
+ }
+ }
+ return null;
+ }
+
+ @MainThread
+ private Queue<Task> getTasks() {
+ Assert.isMainThread();
+ return mTasks;
+ }
+
+ /** Create an intent that will queue the <code>task</code> */
+ public static Intent createIntent(Context context, Class<? extends Task> task) {
+ Intent intent = new Intent(context, TaskSchedulerService.class);
+ intent.putExtra(EXTRA_CLASS_NAME, task.getName());
+ return intent;
+ }
+
+ @VisibleForTesting
+ @MainThread
+ @Nullable
+ Task createTask(@Nullable Intent intent, int flags, int startId) {
+ Assert.isMainThread();
+ if (intent == null) {
+ return null;
+ }
+ String className = intent.getStringExtra(EXTRA_CLASS_NAME);
+ VvmLog.d(TAG, "create task:" + className);
+ if (className == null) {
+ throw new IllegalArgumentException("EXTRA_CLASS_NAME expected");
+ }
+ try {
+ Task task = (Task) Class.forName(className).newInstance();
+ task.onCreate(mContext, intent, flags, startId);
+ return task;
+ } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ @MainThread
+ private void maybeRunNextTask() {
+ Assert.isMainThread();
+ if (mWorkerThreadIsBusy) {
+ return;
+ }
+ if (mTaskAutoRunDisabledForTesting) {
+ // If mTaskAutoRunDisabledForTesting is true, runNextTask() must be explicitly called
+ // to run the next task.
+ return;
+ }
+
+ runNextTask();
+ }
+
+ @VisibleForTesting
+ @MainThread
+ void runNextTask() {
+ Assert.isMainThread();
+ // The current alarm is no longer valid, a new one will be set up if required.
+ getSystemService(AlarmManager.class).cancel(getWakeupIntent());
+ if (getTasks().isEmpty()) {
+ prepareStop();
+ return;
+ }
+ Long minimalWaitTime = null;
+ for (Task task : getTasks()) {
+ long waitTime = task.getReadyInMilliSeconds();
+ if (waitTime < READY_TOLERANCE_MILLISECONDS) {
+ task.onBeforeExecute();
+ Message message = mWorkerThreadHandler.obtainMessage();
+ message.obj = task;
+ mWorkerThreadIsBusy = true;
+ mMessageSender.send(message);
+ return;
+ } else {
+ if (minimalWaitTime == null || waitTime < minimalWaitTime) {
+ minimalWaitTime = waitTime;
+ }
+ }
+ }
+ VvmLog.d(TAG, "minimal wait time:" + minimalWaitTime);
+ if (!mTaskAutoRunDisabledForTesting && minimalWaitTime != null) {
+ // No tasks are currently ready. Sleep until the next one should be.
+ // If a new task is added during the sleep the service will wake immediately.
+ sleep(minimalWaitTime);
+ }
+ }
+
+ private void sleep(long timeMillis) {
+ if (timeMillis < SHORT_SLEEP_THRESHOLD_MILLISECONDS) {
+ mMainThreadHandler.postDelayed(
+ new Runnable() {
+ @Override
+ public void run() {
+ maybeRunNextTask();
+ }
+ },
+ timeMillis);
+ return;
+ }
+
+ // Tasks does not have a strict timing requirement, use AlarmManager.set() so the OS could
+ // optimize the battery usage. As this service currently run in the telephony process the
+ // OS give it privileges to behave the same as setExact(), but set() is the targeted
+ // behavior once this is unbundled.
+ getSystemService(AlarmManager.class)
+ .set(
+ AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ SystemClock.elapsedRealtime() + timeMillis,
+ getWakeupIntent());
+ mWakeLock.release();
+ VvmLog.d(TAG, "Long sleep for " + timeMillis + " millis");
+ }
+
+ private PendingIntent getWakeupIntent() {
+ Intent intent = new Intent(ACTION_WAKEUP, null, this, getClass());
+ return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
+ }
+
+ private void prepareStop() {
+ VvmLog.d(
+ TAG,
+ "No more tasks, stopping service if no task are added in "
+ + STOP_DELAY_MILLISECONDS
+ + " millis");
+ mMainThreadHandler.postDelayed(mStopServiceWithDelay, STOP_DELAY_MILLISECONDS);
+ }
+
+ static class MessageSender {
+
+ public void send(Message message) {
+ message.sendToTarget();
+ }
+ }
+
+ @NeededForTesting
+ void setContextForTest(Context context) {
+ mContext = context;
+ }
+
+ @NeededForTesting
+ void setTaskAutoRunDisabledForTest(boolean value) {
+ mTaskAutoRunDisabledForTesting = value;
+ }
+
+ @NeededForTesting
+ void setMessageSenderForTest(MessageSender sender) {
+ mMessageSender = sender;
+ }
+
+ @NeededForTesting
+ void clearTasksForTest() {
+ mTasks.clear();
+ }
+
+ @Override
+ @Nullable
+ public IBinder onBind(Intent intent) {
+ return new LocalBinder();
+ }
+
+ @NeededForTesting
+ class LocalBinder extends Binder {
+
+ @NeededForTesting
+ public TaskSchedulerService getService() {
+ return TaskSchedulerService.this;
+ }
+ }
+}
diff --git a/java/com/android/voicemail/impl/settings/VisualVoicemailSettingsUtil.java b/java/com/android/voicemail/impl/settings/VisualVoicemailSettingsUtil.java
new file mode 100644
index 000000000..7e4a6a7dc
--- /dev/null
+++ b/java/com/android/voicemail/impl/settings/VisualVoicemailSettingsUtil.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.settings;
+
+import android.content.Context;
+import android.telecom.PhoneAccountHandle;
+import com.android.dialer.common.Assert;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.R;
+import com.android.voicemail.impl.VisualVoicemailPreferences;
+import com.android.voicemail.impl.sync.VvmAccountManager;
+
+/** Save whether or not a particular account is enabled in shared to be retrieved later. */
+public class VisualVoicemailSettingsUtil {
+
+ private static final String IS_ENABLED_KEY = "is_enabled";
+ // Flag name used for configuration
+ public static final String ALLOW_VOICEMAIL_ARCHIVE = "allow_voicemail_archive";
+
+ public static void setEnabled(
+ Context context, PhoneAccountHandle phoneAccount, boolean isEnabled) {
+ new VisualVoicemailPreferences(context, phoneAccount)
+ .edit()
+ .putBoolean(IS_ENABLED_KEY, isEnabled)
+ .apply();
+ OmtpVvmCarrierConfigHelper config = new OmtpVvmCarrierConfigHelper(context, phoneAccount);
+ if (isEnabled) {
+ config.startActivation();
+ } else {
+ VvmAccountManager.removeAccount(context, phoneAccount);
+ config.startDeactivation();
+ }
+ }
+
+ public static void setArchiveEnabled(
+ Context context, PhoneAccountHandle phoneAccount, boolean isEnabled) {
+ new VisualVoicemailPreferences(context, phoneAccount)
+ .edit()
+ .putBoolean(context.getString(R.string.voicemail_visual_voicemail_archive_key), isEnabled)
+ .apply();
+ }
+
+ public static boolean isEnabled(Context context, PhoneAccountHandle phoneAccount) {
+ if (phoneAccount == null) {
+ return false;
+ }
+
+ VisualVoicemailPreferences prefs = new VisualVoicemailPreferences(context, phoneAccount);
+ if (prefs.contains(IS_ENABLED_KEY)) {
+ // isEnableByDefault is a bit expensive, so don't use it as default value of
+ // getBoolean(). The "false" here should never be actually used.
+ return prefs.getBoolean(IS_ENABLED_KEY, false);
+ }
+ return new OmtpVvmCarrierConfigHelper(context, phoneAccount).isEnabledByDefault();
+ }
+
+ public static boolean isArchiveEnabled(Context context, PhoneAccountHandle phoneAccount) {
+ Assert.isNotNull(phoneAccount);
+
+ VisualVoicemailPreferences prefs = new VisualVoicemailPreferences(context, phoneAccount);
+ return prefs.getBoolean(
+ context.getString(R.string.voicemail_visual_voicemail_archive_key), false);
+ }
+
+ /**
+ * Whether the client enabled status is explicitly set by user or by default(Whether carrier VVM
+ * app is installed). This is used to determine whether to disable the client when the carrier VVM
+ * app is installed. If the carrier VVM app is installed the client should give priority to it if
+ * the settings are not touched.
+ */
+ public static boolean isEnabledUserSet(Context context, PhoneAccountHandle phoneAccount) {
+ if (phoneAccount == null) {
+ return false;
+ }
+ VisualVoicemailPreferences prefs = new VisualVoicemailPreferences(context, phoneAccount);
+ return prefs.contains(IS_ENABLED_KEY);
+ }
+}
diff --git a/java/com/android/voicemail/impl/settings/VoicemailChangePinActivity.java b/java/com/android/voicemail/impl/settings/VoicemailChangePinActivity.java
new file mode 100644
index 000000000..f288a5b75
--- /dev/null
+++ b/java/com/android/voicemail/impl/settings/VoicemailChangePinActivity.java
@@ -0,0 +1,624 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.settings;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnDismissListener;
+import android.net.Network;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.InputFilter.LengthFilter;
+import android.text.TextWatcher;
+import android.view.KeyEvent;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.WindowManager;
+import android.view.inputmethod.EditorInfo;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.TextView.OnEditorActionListener;
+import android.widget.Toast;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.OmtpConstants.ChangePinResult;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.R;
+import com.android.voicemail.impl.VisualVoicemailPreferences;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.imap.ImapHelper;
+import com.android.voicemail.impl.imap.ImapHelper.InitializingException;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.sync.VvmNetworkRequestCallback;
+
+/**
+ * Dialog to change the voicemail PIN. The TUI (Telephony User Interface) PIN is used when accessing
+ * traditional voicemail through phone call. The intent to launch this activity must contain {@link
+ * #EXTRA_PHONE_ACCOUNT_HANDLE}
+ */
+@TargetApi(VERSION_CODES.O)
+public class VoicemailChangePinActivity extends Activity
+ implements OnClickListener, OnEditorActionListener, TextWatcher {
+
+ private static final String TAG = "VmChangePinActivity";
+
+ public static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle";
+
+ private static final String KEY_DEFAULT_OLD_PIN = "default_old_pin";
+
+ private static final int MESSAGE_HANDLE_RESULT = 1;
+
+ private PhoneAccountHandle mPhoneAccountHandle;
+ private OmtpVvmCarrierConfigHelper mConfig;
+
+ private int mPinMinLength;
+ private int mPinMaxLength;
+
+ private State mUiState = State.Initial;
+ private String mOldPin;
+ private String mFirstPin;
+
+ private ProgressDialog mProgressDialog;
+
+ private TextView mHeaderText;
+ private TextView mHintText;
+ private TextView mErrorText;
+ private EditText mPinEntry;
+ private Button mCancelButton;
+ private Button mNextButton;
+
+ private Handler mHandler =
+ new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == MESSAGE_HANDLE_RESULT) {
+ mUiState.handleResult(VoicemailChangePinActivity.this, message.arg1);
+ }
+ }
+ };
+
+ private enum State {
+ /**
+ * Empty state to handle initial state transition. Will immediately switch into {@link
+ * #VerifyOldPin} if a default PIN has been set by the OMTP client, or {@link #EnterOldPin} if
+ * not.
+ */
+ Initial,
+ /**
+ * Prompt the user to enter old PIN. The PIN will be verified with the server before proceeding
+ * to {@link #EnterNewPin}.
+ */
+ EnterOldPin {
+ @Override
+ public void onEnter(VoicemailChangePinActivity activity) {
+ activity.setHeader(R.string.change_pin_enter_old_pin_header);
+ activity.mHintText.setText(R.string.change_pin_enter_old_pin_hint);
+ activity.mNextButton.setText(R.string.change_pin_continue_label);
+ activity.mErrorText.setText(null);
+ }
+
+ @Override
+ public void onInputChanged(VoicemailChangePinActivity activity) {
+ activity.setNextEnabled(activity.getCurrentPasswordInput().length() > 0);
+ }
+
+ @Override
+ public void handleNext(VoicemailChangePinActivity activity) {
+ activity.mOldPin = activity.getCurrentPasswordInput();
+ activity.verifyOldPin();
+ }
+
+ @Override
+ public void handleResult(VoicemailChangePinActivity activity, @ChangePinResult int result) {
+ if (result == OmtpConstants.CHANGE_PIN_SUCCESS) {
+ activity.updateState(State.EnterNewPin);
+ } else {
+ CharSequence message = activity.getChangePinResultMessage(result);
+ activity.showError(message);
+ activity.mPinEntry.setText("");
+ }
+ }
+ },
+ /**
+ * The default old PIN is found. Show a blank screen while verifying with the server to make
+ * sure the PIN is still valid. If the PIN is still valid, proceed to {@link #EnterNewPin}. If
+ * not, the user probably changed the PIN through other means, proceed to {@link #EnterOldPin}.
+ * If any other issue caused the verifying to fail, show an error and exit.
+ */
+ VerifyOldPin {
+ @Override
+ public void onEnter(VoicemailChangePinActivity activity) {
+ activity.findViewById(android.R.id.content).setVisibility(View.INVISIBLE);
+ activity.verifyOldPin();
+ }
+
+ @Override
+ public void handleResult(
+ final VoicemailChangePinActivity activity, @ChangePinResult int result) {
+ if (result == OmtpConstants.CHANGE_PIN_SUCCESS) {
+ activity.updateState(State.EnterNewPin);
+ } else if (result == OmtpConstants.CHANGE_PIN_SYSTEM_ERROR) {
+ activity
+ .getWindow()
+ .setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
+ activity.showError(
+ activity.getString(R.string.change_pin_system_error),
+ new OnDismissListener() {
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ activity.finish();
+ }
+ });
+ } else {
+ VvmLog.e(TAG, "invalid default old PIN: " + activity.getChangePinResultMessage(result));
+ // If the default old PIN is rejected by the server, the PIN is probably changed
+ // through other means, or the generated pin is invalid
+ // Wipe the default old PIN so the old PIN input box will be shown to the user
+ // on the next time.
+ setDefaultOldPIN(activity, activity.mPhoneAccountHandle, null);
+ activity.handleOmtpEvent(OmtpEvents.CONFIG_PIN_SET);
+ activity.updateState(State.EnterOldPin);
+ }
+ }
+
+ @Override
+ public void onLeave(VoicemailChangePinActivity activity) {
+ activity.findViewById(android.R.id.content).setVisibility(View.VISIBLE);
+ }
+ },
+ /**
+ * Let the user enter the new PIN and validate the format. Only length is enforced, PIN strength
+ * check relies on the server. After a valid PIN is entered, proceed to {@link #ConfirmNewPin}
+ */
+ EnterNewPin {
+ @Override
+ public void onEnter(VoicemailChangePinActivity activity) {
+ activity.mHeaderText.setText(R.string.change_pin_enter_new_pin_header);
+ activity.mNextButton.setText(R.string.change_pin_continue_label);
+ activity.mHintText.setText(
+ activity.getString(
+ R.string.change_pin_enter_new_pin_hint,
+ activity.mPinMinLength,
+ activity.mPinMaxLength));
+ }
+
+ @Override
+ public void onInputChanged(VoicemailChangePinActivity activity) {
+ String password = activity.getCurrentPasswordInput();
+ if (password.length() == 0) {
+ activity.setNextEnabled(false);
+ return;
+ }
+ CharSequence error = activity.validatePassword(password);
+ if (error != null) {
+ activity.mErrorText.setText(error);
+ activity.setNextEnabled(false);
+ } else {
+ activity.mErrorText.setText(null);
+ activity.setNextEnabled(true);
+ }
+ }
+
+ @Override
+ public void handleNext(VoicemailChangePinActivity activity) {
+ CharSequence errorMsg;
+ errorMsg = activity.validatePassword(activity.getCurrentPasswordInput());
+ if (errorMsg != null) {
+ activity.showError(errorMsg);
+ return;
+ }
+ activity.mFirstPin = activity.getCurrentPasswordInput();
+ activity.updateState(State.ConfirmNewPin);
+ }
+ },
+ /**
+ * Let the user type in the same PIN again to avoid typos. If the PIN matches then perform a PIN
+ * change to the server. Finish the activity if succeeded. Return to {@link #EnterOldPin} if the
+ * old PIN is rejected, {@link #EnterNewPin} for other failure.
+ */
+ ConfirmNewPin {
+ @Override
+ public void onEnter(VoicemailChangePinActivity activity) {
+ activity.mHeaderText.setText(R.string.change_pin_confirm_pin_header);
+ activity.mHintText.setText(null);
+ activity.mNextButton.setText(R.string.change_pin_ok_label);
+ }
+
+ @Override
+ public void onInputChanged(VoicemailChangePinActivity activity) {
+ if (activity.getCurrentPasswordInput().length() == 0) {
+ activity.setNextEnabled(false);
+ return;
+ }
+ if (activity.getCurrentPasswordInput().equals(activity.mFirstPin)) {
+ activity.setNextEnabled(true);
+ activity.mErrorText.setText(null);
+ } else {
+ activity.setNextEnabled(false);
+ activity.mErrorText.setText(R.string.change_pin_confirm_pins_dont_match);
+ }
+ }
+
+ @Override
+ public void handleResult(VoicemailChangePinActivity activity, @ChangePinResult int result) {
+ if (result == OmtpConstants.CHANGE_PIN_SUCCESS) {
+ // If the PIN change succeeded we no longer know what the old (current) PIN is.
+ // Wipe the default old PIN so the old PIN input box will be shown to the user
+ // on the next time.
+ setDefaultOldPIN(activity, activity.mPhoneAccountHandle, null);
+ activity.handleOmtpEvent(OmtpEvents.CONFIG_PIN_SET);
+
+ activity.finish();
+
+ Toast.makeText(
+ activity, activity.getString(R.string.change_pin_succeeded), Toast.LENGTH_SHORT)
+ .show();
+ } else {
+ CharSequence message = activity.getChangePinResultMessage(result);
+ VvmLog.i(TAG, "Change PIN failed: " + message);
+ activity.showError(message);
+ if (result == OmtpConstants.CHANGE_PIN_MISMATCH) {
+ // Somehow the PIN has changed, prompt to enter the old PIN again.
+ activity.updateState(State.EnterOldPin);
+ } else {
+ // The new PIN failed to fulfil other restrictions imposed by the server.
+ activity.updateState(State.EnterNewPin);
+ }
+ }
+ }
+
+ @Override
+ public void handleNext(VoicemailChangePinActivity activity) {
+ activity.processPinChange(activity.mOldPin, activity.mFirstPin);
+ }
+ };
+
+ /** The activity has switched from another state to this one. */
+ public void onEnter(VoicemailChangePinActivity activity) {
+ // Do nothing
+ }
+
+ /**
+ * The user has typed something into the PIN input field. Also called after {@link
+ * #onEnter(VoicemailChangePinActivity)}
+ */
+ public void onInputChanged(VoicemailChangePinActivity activity) {
+ // Do nothing
+ }
+
+ /** The asynchronous call to change the PIN on the server has returned. */
+ public void handleResult(VoicemailChangePinActivity activity, @ChangePinResult int result) {
+ // Do nothing
+ }
+
+ /** The user has pressed the "next" button. */
+ public void handleNext(VoicemailChangePinActivity activity) {
+ // Do nothing
+ }
+
+ /** The activity has switched from this state to another one. */
+ public void onLeave(VoicemailChangePinActivity activity) {
+ // Do nothing
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mPhoneAccountHandle = getIntent().getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE);
+ mConfig = new OmtpVvmCarrierConfigHelper(this, mPhoneAccountHandle);
+ setContentView(R.layout.voicemail_change_pin);
+ setTitle(R.string.change_pin_title);
+
+ readPinLength();
+
+ View view = findViewById(android.R.id.content);
+
+ mCancelButton = (Button) view.findViewById(R.id.cancel_button);
+ mCancelButton.setOnClickListener(this);
+ mNextButton = (Button) view.findViewById(R.id.next_button);
+ mNextButton.setOnClickListener(this);
+
+ mPinEntry = (EditText) view.findViewById(R.id.pin_entry);
+ mPinEntry.setOnEditorActionListener(this);
+ mPinEntry.addTextChangedListener(this);
+ if (mPinMaxLength != 0) {
+ mPinEntry.setFilters(new InputFilter[] {new LengthFilter(mPinMaxLength)});
+ }
+
+ mHeaderText = (TextView) view.findViewById(R.id.headerText);
+ mHintText = (TextView) view.findViewById(R.id.hintText);
+ mErrorText = (TextView) view.findViewById(R.id.errorText);
+
+ if (isDefaultOldPinSet(this, mPhoneAccountHandle)) {
+ mOldPin = getDefaultOldPin(this, mPhoneAccountHandle);
+ updateState(State.VerifyOldPin);
+ } else {
+ updateState(State.EnterOldPin);
+ }
+ }
+
+ private void handleOmtpEvent(OmtpEvents event) {
+ mConfig.handleEvent(getVoicemailStatusEditor(), event);
+ }
+
+ private VoicemailStatus.Editor getVoicemailStatusEditor() {
+ // This activity does not have any automatic retry mechanism, errors should be written right
+ // away.
+ return VoicemailStatus.edit(this, mPhoneAccountHandle);
+ }
+
+ /** Extracts the pin length requirement sent by the server with a STATUS SMS. */
+ private void readPinLength() {
+ VisualVoicemailPreferences preferences =
+ new VisualVoicemailPreferences(this, mPhoneAccountHandle);
+ // The OMTP pin length format is {min}-{max}
+ String[] lengths = preferences.getString(OmtpConstants.TUI_PASSWORD_LENGTH, "").split("-");
+ if (lengths.length == 2) {
+ try {
+ mPinMinLength = Integer.parseInt(lengths[0]);
+ mPinMaxLength = Integer.parseInt(lengths[1]);
+ } catch (NumberFormatException e) {
+ mPinMinLength = 0;
+ mPinMaxLength = 0;
+ }
+ } else {
+ mPinMinLength = 0;
+ mPinMaxLength = 0;
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ updateState(mUiState);
+ }
+
+ public void handleNext() {
+ if (mPinEntry.length() == 0) {
+ return;
+ }
+ mUiState.handleNext(this);
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v.getId() == R.id.next_button) {
+ handleNext();
+ } else if (v.getId() == R.id.cancel_button) {
+ finish();
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ onBackPressed();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (!mNextButton.isEnabled()) {
+ return true;
+ }
+ // Check if this was the result of hitting the enter or "done" key
+ if (actionId == EditorInfo.IME_NULL
+ || actionId == EditorInfo.IME_ACTION_DONE
+ || actionId == EditorInfo.IME_ACTION_NEXT) {
+ handleNext();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ mUiState.onInputChanged(this);
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ // Do nothing
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ // Do nothing
+ }
+
+ /**
+ * After replacing the default PIN with a random PIN, call this to store the random PIN. The
+ * stored PIN will be automatically entered when the user attempts to change the PIN.
+ */
+ public static void setDefaultOldPIN(
+ Context context, PhoneAccountHandle phoneAccountHandle, String pin) {
+ new VisualVoicemailPreferences(context, phoneAccountHandle)
+ .edit()
+ .putString(KEY_DEFAULT_OLD_PIN, pin)
+ .apply();
+ }
+
+ public static boolean isDefaultOldPinSet(Context context, PhoneAccountHandle phoneAccountHandle) {
+ return getDefaultOldPin(context, phoneAccountHandle) != null;
+ }
+
+ private static String getDefaultOldPin(Context context, PhoneAccountHandle phoneAccountHandle) {
+ return new VisualVoicemailPreferences(context, phoneAccountHandle)
+ .getString(KEY_DEFAULT_OLD_PIN);
+ }
+
+ private String getCurrentPasswordInput() {
+ return mPinEntry.getText().toString();
+ }
+
+ private void updateState(State state) {
+ State previousState = mUiState;
+ mUiState = state;
+ if (previousState != state) {
+ previousState.onLeave(this);
+ mPinEntry.setText("");
+ mUiState.onEnter(this);
+ }
+ mUiState.onInputChanged(this);
+ }
+
+ /**
+ * Validates PIN and returns a message to display if PIN fails test.
+ *
+ * @param password the raw password the user typed in
+ * @return error message to show to user or null if password is OK
+ */
+ private CharSequence validatePassword(String password) {
+ if (mPinMinLength == 0 && mPinMaxLength == 0) {
+ // Invalid length requirement is sent by the server, just accept anything and let the
+ // server decide.
+ return null;
+ }
+
+ if (password.length() < mPinMinLength) {
+ return getString(R.string.vm_change_pin_error_too_short);
+ }
+ return null;
+ }
+
+ private void setHeader(int text) {
+ mHeaderText.setText(text);
+ mPinEntry.setContentDescription(mHeaderText.getText());
+ }
+
+ /**
+ * Get the corresponding message for the {@link ChangePinResult}.<code>result</code> must not
+ * {@link OmtpConstants#CHANGE_PIN_SUCCESS}
+ */
+ private CharSequence getChangePinResultMessage(@ChangePinResult int result) {
+ switch (result) {
+ case OmtpConstants.CHANGE_PIN_TOO_SHORT:
+ return getString(R.string.vm_change_pin_error_too_short);
+ case OmtpConstants.CHANGE_PIN_TOO_LONG:
+ return getString(R.string.vm_change_pin_error_too_long);
+ case OmtpConstants.CHANGE_PIN_TOO_WEAK:
+ return getString(R.string.vm_change_pin_error_too_weak);
+ case OmtpConstants.CHANGE_PIN_INVALID_CHARACTER:
+ return getString(R.string.vm_change_pin_error_invalid);
+ case OmtpConstants.CHANGE_PIN_MISMATCH:
+ return getString(R.string.vm_change_pin_error_mismatch);
+ case OmtpConstants.CHANGE_PIN_SYSTEM_ERROR:
+ return getString(R.string.vm_change_pin_error_system_error);
+ default:
+ VvmLog.wtf(TAG, "Unexpected ChangePinResult " + result);
+ return null;
+ }
+ }
+
+ private void verifyOldPin() {
+ processPinChange(mOldPin, mOldPin);
+ }
+
+ private void setNextEnabled(boolean enabled) {
+ mNextButton.setEnabled(enabled);
+ }
+
+ private void showError(CharSequence message) {
+ showError(message, null);
+ }
+
+ private void showError(CharSequence message, @Nullable OnDismissListener callback) {
+ new AlertDialog.Builder(this)
+ .setMessage(message)
+ .setPositiveButton(android.R.string.ok, null)
+ .setOnDismissListener(callback)
+ .show();
+ }
+
+ /** Asynchronous call to change the PIN on the server. */
+ private void processPinChange(String oldPin, String newPin) {
+ mProgressDialog = new ProgressDialog(this);
+ mProgressDialog.setCancelable(false);
+ mProgressDialog.setMessage(getString(R.string.vm_change_pin_progress_message));
+ mProgressDialog.show();
+
+ ChangePinNetworkRequestCallback callback = new ChangePinNetworkRequestCallback(oldPin, newPin);
+ callback.requestNetwork();
+ }
+
+ private class ChangePinNetworkRequestCallback extends VvmNetworkRequestCallback {
+
+ private final String mOldPin;
+ private final String mNewPin;
+
+ public ChangePinNetworkRequestCallback(String oldPin, String newPin) {
+ super(
+ mConfig, mPhoneAccountHandle, VoicemailChangePinActivity.this.getVoicemailStatusEditor());
+ mOldPin = oldPin;
+ mNewPin = newPin;
+ }
+
+ @Override
+ public void onAvailable(Network network) {
+ super.onAvailable(network);
+ try (ImapHelper helper =
+ new ImapHelper(
+ VoicemailChangePinActivity.this,
+ mPhoneAccountHandle,
+ network,
+ getVoicemailStatusEditor())) {
+
+ @ChangePinResult int result = helper.changePin(mOldPin, mNewPin);
+ sendResult(result);
+ } catch (InitializingException | MessagingException e) {
+ VvmLog.e(TAG, "ChangePinNetworkRequestCallback: onAvailable: ", e);
+ sendResult(OmtpConstants.CHANGE_PIN_SYSTEM_ERROR);
+ }
+ }
+
+ @Override
+ public void onFailed(String reason) {
+ super.onFailed(reason);
+ sendResult(OmtpConstants.CHANGE_PIN_SYSTEM_ERROR);
+ }
+
+ private void sendResult(@ChangePinResult int result) {
+ VvmLog.i(TAG, "Change PIN result: " + result);
+ if (mProgressDialog.isShowing()
+ && !VoicemailChangePinActivity.this.isDestroyed()
+ && !VoicemailChangePinActivity.this.isFinishing()) {
+ mProgressDialog.dismiss();
+ } else {
+ VvmLog.i(TAG, "Dialog not visible, not dismissing");
+ }
+ mHandler.obtainMessage(MESSAGE_HANDLE_RESULT, result, 0).sendToTarget();
+ releaseNetwork();
+ }
+ }
+}
diff --git a/java/com/android/voicemail/impl/settings/VoicemailRingtonePreference.java b/java/com/android/voicemail/impl/settings/VoicemailRingtonePreference.java
new file mode 100644
index 000000000..22c729c60
--- /dev/null
+++ b/java/com/android/voicemail/impl/settings/VoicemailRingtonePreference.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.settings;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.os.Handler;
+import android.os.Message;
+import android.preference.RingtonePreference;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import android.util.AttributeSet;
+import com.android.dialer.common.Assert;
+import com.android.dialer.util.SettingsUtil;
+
+/**
+ * Looks up the voicemail ringtone's name asynchronously and updates the preference's summary when
+ * it is created or updated.
+ */
+@TargetApi(VERSION_CODES.O)
+public class VoicemailRingtonePreference extends RingtonePreference {
+
+ /** Callback when the ringtone name has been fetched. */
+ public interface VoicemailRingtoneNameChangeListener {
+ void onVoicemailRingtoneNameChanged(CharSequence name);
+ }
+
+ private static final int MSG_UPDATE_VOICEMAIL_RINGTONE_SUMMARY = 1;
+
+ private PhoneAccountHandle phoneAccountHandle;
+ private final TelephonyManager telephonyManager;
+
+ private VoicemailRingtoneNameChangeListener mVoicemailRingtoneNameChangeListener;
+ private Runnable mVoicemailRingtoneLookupRunnable;
+ private final Handler mVoicemailRingtoneLookupComplete =
+ new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_UPDATE_VOICEMAIL_RINGTONE_SUMMARY:
+ if (mVoicemailRingtoneNameChangeListener != null) {
+ mVoicemailRingtoneNameChangeListener.onVoicemailRingtoneNameChanged(
+ (CharSequence) msg.obj);
+ }
+ setSummary((CharSequence) msg.obj);
+ break;
+ default:
+ Assert.fail();
+ }
+ }
+ };
+
+ public VoicemailRingtonePreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ telephonyManager = context.getSystemService(TelephonyManager.class);
+ }
+
+ public void init(PhoneAccountHandle phoneAccountHandle, CharSequence oldRingtoneName) {
+ this.phoneAccountHandle = phoneAccountHandle;
+ setSummary(oldRingtoneName);
+ mVoicemailRingtoneLookupRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ SettingsUtil.getRingtoneName(
+ getContext(),
+ mVoicemailRingtoneLookupComplete,
+ telephonyManager.getVoicemailRingtoneUri(phoneAccountHandle),
+ MSG_UPDATE_VOICEMAIL_RINGTONE_SUMMARY);
+ }
+ };
+
+ updateRingtoneName();
+ }
+
+ public void setVoicemailRingtoneNameChangeListener(VoicemailRingtoneNameChangeListener l) {
+ mVoicemailRingtoneNameChangeListener = l;
+ }
+
+ @Override
+ protected Uri onRestoreRingtone() {
+ return telephonyManager.getVoicemailRingtoneUri(phoneAccountHandle);
+ }
+
+ @Override
+ protected void onSaveRingtone(Uri ringtoneUri) {
+ telephonyManager.setVoicemailRingtoneUri(phoneAccountHandle, ringtoneUri);
+ updateRingtoneName();
+ }
+
+ private void updateRingtoneName() {
+ new Thread(mVoicemailRingtoneLookupRunnable).start();
+ }
+}
diff --git a/java/com/android/voicemail/impl/settings/VoicemailSettingsFragment.java b/java/com/android/voicemail/impl/settings/VoicemailSettingsFragment.java
new file mode 100644
index 000000000..a5b94a75e
--- /dev/null
+++ b/java/com/android/voicemail/impl/settings/VoicemailSettingsFragment.java
@@ -0,0 +1,202 @@
+/**
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.settings;
+
+import android.annotation.TargetApi;
+import android.content.Intent;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceScreen;
+import android.preference.SwitchPreference;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telephony.TelephonyManager;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.R;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.sync.VvmAccountManager;
+
+/** Fragment for voicemail settings. */
+@TargetApi(VERSION_CODES.O)
+public class VoicemailSettingsFragment extends PreferenceFragment
+ implements Preference.OnPreferenceChangeListener,
+ VoicemailRingtonePreference.VoicemailRingtoneNameChangeListener {
+
+ private static final String TAG = "VmSettingsActivity";
+
+ private PhoneAccountHandle phoneAccountHandle;
+ private OmtpVvmCarrierConfigHelper omtpVvmCarrierConfigHelper;
+
+ private VoicemailRingtonePreference voicemailRingtonePreference;
+ private CheckBoxPreference voicemailVibration;
+ private SwitchPreference voicemailVisualVoicemail;
+ private SwitchPreference autoArchiveSwitchPreference;
+ private Preference voicemailChangePinPreference;
+ private PreferenceScreen advancedSettings;
+
+ // The ringtone name is retrieved with an async call. Cache the old name so there will be no jank
+ // during transition.
+ private CharSequence oldRingtoneName = "";
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ phoneAccountHandle =
+ getContext()
+ .getSystemService(TelecomManager.class)
+ .getDefaultOutgoingPhoneAccount(PhoneAccount.SCHEME_TEL);
+
+ omtpVvmCarrierConfigHelper = new OmtpVvmCarrierConfigHelper(getContext(), phoneAccountHandle);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ PreferenceScreen preferenceScreen = getPreferenceScreen();
+ if (preferenceScreen != null) {
+ preferenceScreen.removeAll();
+ }
+
+ addPreferencesFromResource(R.xml.voicemail_settings);
+
+ PreferenceScreen prefSet = getPreferenceScreen();
+
+ voicemailRingtonePreference =
+ (VoicemailRingtonePreference)
+ findPreference(getString(R.string.voicemail_notification_ringtone_key));
+ voicemailRingtonePreference.setVoicemailRingtoneNameChangeListener(this);
+ voicemailRingtonePreference.init(phoneAccountHandle, oldRingtoneName);
+
+ voicemailVibration =
+ (CheckBoxPreference) findPreference(getString(R.string.voicemail_notification_vibrate_key));
+ voicemailVibration.setOnPreferenceChangeListener(this);
+ voicemailVibration.setChecked(
+ getContext()
+ .getSystemService(TelephonyManager.class)
+ .isVoicemailVibrationEnabled(phoneAccountHandle));
+
+ voicemailVisualVoicemail =
+ (SwitchPreference) findPreference(getString(R.string.voicemail_visual_voicemail_key));
+
+ autoArchiveSwitchPreference =
+ (SwitchPreference)
+ findPreference(getString(R.string.voicemail_visual_voicemail_archive_key));
+ autoArchiveSwitchPreference.setOnPreferenceChangeListener(this);
+ autoArchiveSwitchPreference.setChecked(
+ VisualVoicemailSettingsUtil.isArchiveEnabled(getContext(), phoneAccountHandle));
+
+ if (!ConfigProviderBindings.get(getContext())
+ .getBoolean(VisualVoicemailSettingsUtil.ALLOW_VOICEMAIL_ARCHIVE, true)) {
+ getPreferenceScreen().removePreference(autoArchiveSwitchPreference);
+ }
+
+ voicemailChangePinPreference = findPreference(getString(R.string.voicemail_change_pin_key));
+ Intent changePinIntent = new Intent(new Intent(getContext(), VoicemailChangePinActivity.class));
+ changePinIntent.putExtra(
+ VoicemailChangePinActivity.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle);
+
+ voicemailChangePinPreference.setIntent(changePinIntent);
+ if (VoicemailChangePinActivity.isDefaultOldPinSet(getContext(), phoneAccountHandle)) {
+ voicemailChangePinPreference.setTitle(R.string.voicemail_set_pin_dialog_title);
+ } else {
+ voicemailChangePinPreference.setTitle(R.string.voicemail_change_pin_dialog_title);
+ }
+
+ if (omtpVvmCarrierConfigHelper.isValid()) {
+ voicemailVisualVoicemail.setOnPreferenceChangeListener(this);
+ voicemailVisualVoicemail.setChecked(
+ VisualVoicemailSettingsUtil.isEnabled(getContext(), phoneAccountHandle));
+ if (!isVisualVoicemailActivated()) {
+ prefSet.removePreference(voicemailChangePinPreference);
+ }
+ } else {
+ prefSet.removePreference(voicemailVisualVoicemail);
+ prefSet.removePreference(voicemailChangePinPreference);
+ }
+
+ advancedSettings =
+ (PreferenceScreen) findPreference(getString(R.string.voicemail_advanced_settings_key));
+ Intent advancedSettingsIntent = new Intent(TelephonyManager.ACTION_CONFIGURE_VOICEMAIL);
+ advancedSettingsIntent.putExtra(TelephonyManager.EXTRA_HIDE_PUBLIC_SETTINGS, true);
+ advancedSettings.setIntent(advancedSettingsIntent);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ }
+
+ /**
+ * Implemented to support onPreferenceChangeListener to look for preference changes.
+ *
+ * @param preference is the preference to be changed
+ * @param objValue should be the value of the selection, NOT its localized display value.
+ */
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object objValue) {
+ VvmLog.d(TAG, "onPreferenceChange: \"" + preference + "\" changed to \"" + objValue + "\"");
+ if (preference.getKey().equals(voicemailVisualVoicemail.getKey())) {
+ boolean isEnabled = (boolean) objValue;
+ VisualVoicemailSettingsUtil.setEnabled(getContext(), phoneAccountHandle, isEnabled);
+ PreferenceScreen prefSet = getPreferenceScreen();
+ if (isVisualVoicemailActivated()) {
+ prefSet.addPreference(voicemailChangePinPreference);
+ } else {
+ prefSet.removePreference(voicemailChangePinPreference);
+ }
+ } else if (preference.getKey().equals(autoArchiveSwitchPreference.getKey())) {
+ logArchiveToggle((boolean) objValue);
+ VisualVoicemailSettingsUtil.setArchiveEnabled(
+ getContext(), phoneAccountHandle, (boolean) objValue);
+ } else if (preference.getKey().equals(voicemailVibration.getKey())) {
+ getContext()
+ .getSystemService(TelephonyManager.class)
+ .setVoicemailVibrationEnabled(phoneAccountHandle, (boolean) objValue);
+ }
+
+ // Always let the preference setting proceed.
+ return true;
+ }
+
+ private void logArchiveToggle(boolean userTurnedOn) {
+ if (userTurnedOn) {
+ Logger.get(getContext())
+ .logImpression(DialerImpression.Type.VVM_USER_TURNED_ARCHIVE_ON_FROM_SETTINGS);
+ } else {
+ Logger.get(getContext())
+ .logImpression(DialerImpression.Type.VVM_USER_TURNED_ARCHIVE_OFF_FROM_SETTINGS);
+ }
+ }
+
+ @Override
+ public void onVoicemailRingtoneNameChanged(CharSequence name) {
+ oldRingtoneName = name;
+ }
+
+ private boolean isVisualVoicemailActivated() {
+ if (!VisualVoicemailSettingsUtil.isEnabled(getContext(), phoneAccountHandle)) {
+ return false;
+ }
+ return VvmAccountManager.isAccountActivated(getContext(), phoneAccountHandle);
+ }
+}
diff --git a/java/com/android/voicemail/impl/sms/LegacyModeSmsHandler.java b/java/com/android/voicemail/impl/sms/LegacyModeSmsHandler.java
new file mode 100644
index 000000000..1d1a639c5
--- /dev/null
+++ b/java/com/android/voicemail/impl/sms/LegacyModeSmsHandler.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2016 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.sms;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.VisualVoicemailSms;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.TelephonyManagerStub;
+import com.android.voicemail.impl.VvmLog;
+
+/**
+ * Class ot handle voicemail SMS under legacy mode
+ *
+ * @see OmtpVvmCarrierConfigHelper#isLegacyModeEnabled()
+ */
+public class LegacyModeSmsHandler {
+
+ private static final String TAG = "LegacyModeSmsHandler";
+
+ public static void handle(Context context, VisualVoicemailSms sms) {
+ VvmLog.v(TAG, "processing VVM SMS on legacy mode");
+ String eventType = sms.getPrefix();
+ Bundle data = sms.getFields();
+ PhoneAccountHandle handle = sms.getPhoneAccountHandle();
+
+ if (eventType.equals(OmtpConstants.SYNC_SMS_PREFIX)) {
+ SyncMessage message = new SyncMessage(data);
+ VvmLog.v(
+ TAG, "Received SYNC sms for " + handle + " with event " + message.getSyncTriggerEvent());
+
+ switch (message.getSyncTriggerEvent()) {
+ case OmtpConstants.NEW_MESSAGE:
+ case OmtpConstants.MAILBOX_UPDATE:
+ // The user has called into the voicemail and the new message count could
+ // change.
+ // For some carriers new message count could be set to 0 even if there are still
+ // unread messages, to clear the message waiting indicator.
+ VvmLog.v(TAG, "updating MWI");
+
+ // Setting voicemail message count to non-zero will show the telephony voicemail
+ // notification, and zero will clear it.
+ TelephonyManagerStub.showVoicemailNotification(message.getNewMessageCount());
+ break;
+ default:
+ break;
+ }
+ }
+ }
+}
diff --git a/java/com/android/voicemail/impl/sms/OmtpCvvmMessageSender.java b/java/com/android/voicemail/impl/sms/OmtpCvvmMessageSender.java
new file mode 100644
index 000000000..5fc5e7092
--- /dev/null
+++ b/java/com/android/voicemail/impl/sms/OmtpCvvmMessageSender.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.sms;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.OmtpConstants;
+
+/** An implementation of the OmtpMessageSender for T-Mobile. */
+public class OmtpCvvmMessageSender extends OmtpMessageSender {
+ public OmtpCvvmMessageSender(
+ Context context,
+ PhoneAccountHandle phoneAccountHandle,
+ short applicationPort,
+ String destinationNumber) {
+ super(context, phoneAccountHandle, applicationPort, destinationNumber);
+ }
+
+ @Override
+ public void requestVvmActivation(@Nullable PendingIntent sentIntent) {
+ sendCvvmMessage(OmtpConstants.ACTIVATE_REQUEST, sentIntent);
+ }
+
+ @Override
+ public void requestVvmDeactivation(@Nullable PendingIntent sentIntent) {
+ sendCvvmMessage(OmtpConstants.DEACTIVATE_REQUEST, sentIntent);
+ }
+
+ @Override
+ public void requestVvmStatus(@Nullable PendingIntent sentIntent) {
+ sendCvvmMessage(OmtpConstants.STATUS_REQUEST, sentIntent);
+ }
+
+ private void sendCvvmMessage(String request, PendingIntent sentIntent) {
+ StringBuilder sb = new StringBuilder().append(request);
+ sb.append(OmtpConstants.SMS_PREFIX_SEPARATOR);
+ appendField(sb, "dt", "15");
+ sendSms(sb.toString(), sentIntent);
+ }
+}
diff --git a/java/com/android/voicemail/impl/sms/OmtpMessageReceiver.java b/java/com/android/voicemail/impl/sms/OmtpMessageReceiver.java
new file mode 100644
index 000000000..ef0bf10e9
--- /dev/null
+++ b/java/com/android/voicemail/impl/sms/OmtpMessageReceiver.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.sms;
+
+import android.annotation.TargetApi;
+import android.content.BroadcastReceiver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.UserManager;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.VisualVoicemailSms;
+import com.android.voicemail.impl.ActivationTask;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.OmtpService;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.Voicemail;
+import com.android.voicemail.impl.Voicemail.Builder;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.protocol.VisualVoicemailProtocol;
+import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil;
+import com.android.voicemail.impl.sync.OmtpVvmSyncService;
+import com.android.voicemail.impl.sync.SyncOneTask;
+import com.android.voicemail.impl.sync.SyncTask;
+import com.android.voicemail.impl.sync.VoicemailsQueryHelper;
+import com.android.voicemail.impl.utils.VoicemailDatabaseUtil;
+
+/** Receive SMS messages and send for processing by the OMTP visual voicemail source. */
+@TargetApi(VERSION_CODES.O)
+public class OmtpMessageReceiver extends BroadcastReceiver {
+
+ private static final String TAG = "OmtpMessageReceiver";
+
+ private Context mContext;
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mContext = context;
+ VisualVoicemailSms sms = intent.getExtras().getParcelable(OmtpService.EXTRA_VOICEMAIL_SMS);
+ PhoneAccountHandle phone = sms.getPhoneAccountHandle();
+
+ if (phone == null) {
+ // This should never happen
+ VvmLog.i(TAG, "Received message for null phone account");
+ return;
+ }
+
+ if (!context.getSystemService(UserManager.class).isUserUnlocked()) {
+ VvmLog.i(TAG, "Received message on locked device");
+ // LegacyModeSmsHandler can handle new message notifications without storage access
+ LegacyModeSmsHandler.handle(context, sms);
+ // A full sync will happen after the device is unlocked, so nothing else need to be
+ // done.
+ return;
+ }
+
+ OmtpVvmCarrierConfigHelper helper = new OmtpVvmCarrierConfigHelper(mContext, phone);
+ if (!VisualVoicemailSettingsUtil.isEnabled(mContext, phone)) {
+ if (helper.isLegacyModeEnabled()) {
+ LegacyModeSmsHandler.handle(context, sms);
+ } else {
+ VvmLog.i(TAG, "Received vvm message for disabled vvm source.");
+ }
+ return;
+ }
+
+ String eventType = sms.getPrefix();
+ Bundle data = sms.getFields();
+
+ if (eventType == null || data == null) {
+ VvmLog.e(TAG, "Unparsable VVM SMS received, ignoring");
+ return;
+ }
+
+ if (eventType.equals(OmtpConstants.SYNC_SMS_PREFIX)) {
+ SyncMessage message = new SyncMessage(data);
+
+ VvmLog.v(
+ TAG, "Received SYNC sms for " + phone + " with event " + message.getSyncTriggerEvent());
+ processSync(phone, message);
+ } else if (eventType.equals(OmtpConstants.STATUS_SMS_PREFIX)) {
+ VvmLog.v(TAG, "Received Status sms for " + phone);
+ // If the STATUS SMS is initiated by ActivationTask the TaskSchedulerService will reject
+ // the follow request. Providing the data will also prevent ActivationTask from
+ // requesting another STATUS SMS. The following task will only run if the carrier
+ // spontaneous send a STATUS SMS, in that case, the VVM service should be reactivated.
+ ActivationTask.start(context, phone, data);
+ } else {
+ VvmLog.w(TAG, "Unknown prefix: " + eventType);
+ VisualVoicemailProtocol protocol = helper.getProtocol();
+ if (protocol == null) {
+ return;
+ }
+ Bundle statusData = helper.getProtocol().translateStatusSmsBundle(helper, eventType, data);
+ if (statusData != null) {
+ VvmLog.i(TAG, "Protocol recognized the SMS as STATUS, activating");
+ ActivationTask.start(context, phone, data);
+ }
+ }
+ }
+
+ /**
+ * A sync message has two purposes: to signal a new voicemail message, and to indicate the
+ * voicemails on the server have changed remotely (usually through the TUI). Save the new message
+ * to the voicemail provider if it is the former case and perform a full sync in the latter case.
+ *
+ * @param message The sync message to extract data from.
+ */
+ private void processSync(PhoneAccountHandle phone, SyncMessage message) {
+ switch (message.getSyncTriggerEvent()) {
+ case OmtpConstants.NEW_MESSAGE:
+ if (!OmtpConstants.VOICE.equals(message.getContentType())) {
+ VvmLog.i(
+ TAG,
+ "Non-voice message of type '" + message.getContentType() + "' received, ignoring");
+ return;
+ }
+
+ Builder builder =
+ Voicemail.createForInsertion(message.getTimestampMillis(), message.getSender())
+ .setPhoneAccount(phone)
+ .setSourceData(message.getId())
+ .setDuration(message.getLength())
+ .setSourcePackage(mContext.getPackageName());
+ Voicemail voicemail = builder.build();
+
+ VoicemailsQueryHelper queryHelper = new VoicemailsQueryHelper(mContext);
+ if (queryHelper.isVoicemailUnique(voicemail)) {
+ Uri uri = VoicemailDatabaseUtil.insert(mContext, voicemail);
+ voicemail = builder.setId(ContentUris.parseId(uri)).setUri(uri).build();
+ SyncOneTask.start(mContext, phone, voicemail);
+ }
+ break;
+ case OmtpConstants.MAILBOX_UPDATE:
+ SyncTask.start(mContext, phone, OmtpVvmSyncService.SYNC_DOWNLOAD_ONLY);
+ break;
+ case OmtpConstants.GREETINGS_UPDATE:
+ // Not implemented in V1
+ break;
+ default:
+ VvmLog.e(TAG, "Unrecognized sync trigger event: " + message.getSyncTriggerEvent());
+ break;
+ }
+ }
+}
diff --git a/java/com/android/voicemail/impl/sms/OmtpMessageSender.java b/java/com/android/voicemail/impl/sms/OmtpMessageSender.java
new file mode 100644
index 000000000..6c9333fb3
--- /dev/null
+++ b/java/com/android/voicemail/impl/sms/OmtpMessageSender.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.sms;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.SmsManager;
+import android.telephony.VisualVoicemailService;
+import com.android.voicemail.impl.OmtpConstants;
+
+/**
+ * Send client originated OMTP messages to the OMTP server.
+ *
+ * <p>Uses {@link PendingIntent} instead of a call back to notify when the message is sent. This is
+ * primarily to keep the implementation simple and reuse what the underlying {@link SmsManager}
+ * interface provides.
+ *
+ * <p>Provides simple APIs to send different types of mobile originated OMTP SMS to the VVM server.
+ */
+public abstract class OmtpMessageSender {
+ protected static final String TAG = "OmtpMessageSender";
+ protected final Context mContext;
+ protected final PhoneAccountHandle mPhoneAccountHandle;
+ protected final short mApplicationPort;
+ protected final String mDestinationNumber;
+
+ public OmtpMessageSender(
+ Context context,
+ PhoneAccountHandle phoneAccountHandle,
+ short applicationPort,
+ String destinationNumber) {
+ mContext = context;
+ mPhoneAccountHandle = phoneAccountHandle;
+ mApplicationPort = applicationPort;
+ mDestinationNumber = destinationNumber;
+ }
+
+ /**
+ * Sends a request to the VVM server to activate VVM for the current subscriber.
+ *
+ * @param sentIntent If not NULL this PendingIntent is broadcast when the message is successfully
+ * sent, or failed.
+ */
+ public void requestVvmActivation(@Nullable PendingIntent sentIntent) {}
+
+ /**
+ * Sends a request to the VVM server to deactivate VVM for the current subscriber.
+ *
+ * @param sentIntent If not NULL this PendingIntent is broadcast when the message is successfully
+ * sent, or failed.
+ */
+ public void requestVvmDeactivation(@Nullable PendingIntent sentIntent) {}
+
+ /**
+ * Send a request to the VVM server to get account status of the current subscriber.
+ *
+ * @param sentIntent If not NULL this PendingIntent is broadcast when the message is successfully
+ * sent, or failed.
+ */
+ public void requestVvmStatus(@Nullable PendingIntent sentIntent) {}
+
+ protected void sendSms(String text, PendingIntent sentIntent) {
+ VisualVoicemailService.sendVisualVoicemailSms(
+ mContext, mPhoneAccountHandle, mDestinationNumber, mApplicationPort, text, sentIntent);
+ }
+
+ protected void appendField(StringBuilder sb, String field, Object value) {
+ sb.append(field).append(OmtpConstants.SMS_KEY_VALUE_SEPARATOR).append(value);
+ }
+}
diff --git a/java/com/android/voicemail/impl/sms/OmtpStandardMessageSender.java b/java/com/android/voicemail/impl/sms/OmtpStandardMessageSender.java
new file mode 100644
index 000000000..7974699a0
--- /dev/null
+++ b/java/com/android/voicemail/impl/sms/OmtpStandardMessageSender.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.sms;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.text.TextUtils;
+import com.android.voicemail.impl.OmtpConstants;
+
+/** A implementation of the OmtpMessageSender using the standard OMTP sms protocol. */
+public class OmtpStandardMessageSender extends OmtpMessageSender {
+ private final String mClientType;
+ private final String mProtocolVersion;
+ private final String mClientPrefix;
+
+ /**
+ * Creates a new instance of OmtpStandardMessageSender.
+ *
+ * @param applicationPort If set to a value > 0 then a binary sms is sent to this port number.
+ * Otherwise, a standard text SMS is sent.
+ * @param destinationNumber Destination number to be used.
+ * @param clientType The "ct" field to be set in the MO message. This is the value used by the VVM
+ * server to identify the client. Certain VVM servers require a specific agreed value for this
+ * field.
+ * @param protocolVersion OMTP protocol version.
+ * @param clientPrefix The client prefix requested to be used by the server in its MT messages.
+ */
+ public OmtpStandardMessageSender(
+ Context context,
+ PhoneAccountHandle phoneAccountHandle,
+ short applicationPort,
+ String destinationNumber,
+ String clientType,
+ String protocolVersion,
+ String clientPrefix) {
+ super(context, phoneAccountHandle, applicationPort, destinationNumber);
+ mClientType = clientType;
+ mProtocolVersion = protocolVersion;
+ mClientPrefix = clientPrefix;
+ }
+
+ // Activate message:
+ // V1.1: Activate:pv=<value>;ct=<value>
+ // V1.2: Activate:pv=<value>;ct=<value>;pt=<value>;<Clientprefix>
+ // V1.3: Activate:pv=<value>;ct=<value>;pt=<value>;<Clientprefix>
+ @Override
+ public void requestVvmActivation(@Nullable PendingIntent sentIntent) {
+ StringBuilder sb = new StringBuilder().append(OmtpConstants.ACTIVATE_REQUEST);
+
+ appendProtocolVersionAndClientType(sb);
+ if (TextUtils.equals(mProtocolVersion, OmtpConstants.PROTOCOL_VERSION1_2)
+ || TextUtils.equals(mProtocolVersion, OmtpConstants.PROTOCOL_VERSION1_3)) {
+ appendApplicationPort(sb);
+ appendClientPrefix(sb);
+ }
+
+ sendSms(sb.toString(), sentIntent);
+ }
+
+ // Deactivate message:
+ // V1.1: Deactivate:pv=<value>;ct=<string>
+ // V1.2: Deactivate:pv=<value>;ct=<string>
+ // V1.3: Deactivate:pv=<value>;ct=<string>
+ @Override
+ public void requestVvmDeactivation(@Nullable PendingIntent sentIntent) {
+ StringBuilder sb = new StringBuilder().append(OmtpConstants.DEACTIVATE_REQUEST);
+ appendProtocolVersionAndClientType(sb);
+
+ sendSms(sb.toString(), sentIntent);
+ }
+
+ // Status message:
+ // V1.1: STATUS
+ // V1.2: STATUS
+ // V1.3: STATUS:pv=<value>;ct=<value>;pt=<value>;<Clientprefix>
+ @Override
+ public void requestVvmStatus(@Nullable PendingIntent sentIntent) {
+ StringBuilder sb = new StringBuilder().append(OmtpConstants.STATUS_REQUEST);
+
+ if (TextUtils.equals(mProtocolVersion, OmtpConstants.PROTOCOL_VERSION1_3)) {
+ appendProtocolVersionAndClientType(sb);
+ appendApplicationPort(sb);
+ appendClientPrefix(sb);
+ }
+
+ sendSms(sb.toString(), sentIntent);
+ }
+
+ private void appendProtocolVersionAndClientType(StringBuilder sb) {
+ sb.append(OmtpConstants.SMS_PREFIX_SEPARATOR);
+ appendField(sb, OmtpConstants.PROTOCOL_VERSION, mProtocolVersion);
+ sb.append(OmtpConstants.SMS_FIELD_SEPARATOR);
+ appendField(sb, OmtpConstants.CLIENT_TYPE, mClientType);
+ }
+
+ private void appendApplicationPort(StringBuilder sb) {
+ sb.append(OmtpConstants.SMS_FIELD_SEPARATOR);
+ appendField(sb, OmtpConstants.APPLICATION_PORT, mApplicationPort);
+ }
+
+ private void appendClientPrefix(StringBuilder sb) {
+ sb.append(OmtpConstants.SMS_FIELD_SEPARATOR);
+ sb.append(mClientPrefix);
+ }
+}
diff --git a/java/com/android/voicemail/impl/sms/StatusMessage.java b/java/com/android/voicemail/impl/sms/StatusMessage.java
new file mode 100644
index 000000000..a5766a61a
--- /dev/null
+++ b/java/com/android/voicemail/impl/sms/StatusMessage.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.sms;
+
+import android.os.Bundle;
+import com.android.voicemail.impl.NeededForTesting;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.VisualVoicemailPreferences;
+import com.android.voicemail.impl.VvmLog;
+
+/**
+ * Structured data representation of OMTP STATUS message.
+ *
+ * <p>The getters will return null if the field was not set in the message body or it could not be
+ * parsed.
+ */
+public class StatusMessage {
+ // NOTE: Following Status SMS fields are not yet parsed, as they do not seem
+ // to be useful for initial omtp source implementation.
+ // lang, g_len, vs_len, pw_len, pm, gm, vtc, vt
+
+ private final String mProvisioningStatus;
+ private final String mStatusReturnCode;
+ private final String mSubscriptionUrl;
+ private final String mServerAddress;
+ private final String mTuiAccessNumber;
+ private final String mClientSmsDestinationNumber;
+ private final String mImapPort;
+ private final String mImapUserName;
+ private final String mImapPassword;
+ private final String mSmtpPort;
+ private final String mSmtpUserName;
+ private final String mSmtpPassword;
+ private final String mTuiPasswordLength;
+
+ @Override
+ public String toString() {
+ return "StatusMessage [mProvisioningStatus="
+ + mProvisioningStatus
+ + ", mStatusReturnCode="
+ + mStatusReturnCode
+ + ", mSubscriptionUrl="
+ + mSubscriptionUrl
+ + ", mServerAddress="
+ + mServerAddress
+ + ", mTuiAccessNumber="
+ + mTuiAccessNumber
+ + ", mClientSmsDestinationNumber="
+ + mClientSmsDestinationNumber
+ + ", mImapPort="
+ + mImapPort
+ + ", mImapUserName="
+ + mImapUserName
+ + ", mImapPassword="
+ + VvmLog.pii(mImapPassword)
+ + ", mSmtpPort="
+ + mSmtpPort
+ + ", mSmtpUserName="
+ + mSmtpUserName
+ + ", mSmtpPassword="
+ + VvmLog.pii(mSmtpPassword)
+ + ", mTuiPasswordLength="
+ + mTuiPasswordLength
+ + "]";
+ }
+
+ public StatusMessage(Bundle wrappedData) {
+ mProvisioningStatus = unquote(getString(wrappedData, OmtpConstants.PROVISIONING_STATUS));
+ mStatusReturnCode = getString(wrappedData, OmtpConstants.RETURN_CODE);
+ mSubscriptionUrl = getString(wrappedData, OmtpConstants.SUBSCRIPTION_URL);
+ mServerAddress = getString(wrappedData, OmtpConstants.SERVER_ADDRESS);
+ mTuiAccessNumber = getString(wrappedData, OmtpConstants.TUI_ACCESS_NUMBER);
+ mClientSmsDestinationNumber =
+ getString(wrappedData, OmtpConstants.CLIENT_SMS_DESTINATION_NUMBER);
+ mImapPort = getString(wrappedData, OmtpConstants.IMAP_PORT);
+ mImapUserName = getString(wrappedData, OmtpConstants.IMAP_USER_NAME);
+ mImapPassword = getString(wrappedData, OmtpConstants.IMAP_PASSWORD);
+ mSmtpPort = getString(wrappedData, OmtpConstants.SMTP_PORT);
+ mSmtpUserName = getString(wrappedData, OmtpConstants.SMTP_USER_NAME);
+ mSmtpPassword = getString(wrappedData, OmtpConstants.SMTP_PASSWORD);
+ mTuiPasswordLength = getString(wrappedData, OmtpConstants.TUI_PASSWORD_LENGTH);
+ }
+
+ private static String unquote(String string) {
+ if (string.length() < 2) {
+ return string;
+ }
+ if (string.startsWith("\"") && string.endsWith("\"")) {
+ return string.substring(1, string.length() - 1);
+ }
+ return string;
+ }
+
+ /** @return the subscriber's VVM provisioning status. */
+ public String getProvisioningStatus() {
+ return mProvisioningStatus;
+ }
+
+ /** @return the return-code of the status SMS. */
+ public String getReturnCode() {
+ return mStatusReturnCode;
+ }
+
+ /**
+ * @return the URL of the voicemail server. This is the URL to send the users to for subscribing
+ * to the visual voicemail service.
+ */
+ @NeededForTesting
+ public String getSubscriptionUrl() {
+ return mSubscriptionUrl;
+ }
+
+ /**
+ * @return the voicemail server address. Either server IP address or fully qualified domain name.
+ */
+ public String getServerAddress() {
+ return mServerAddress;
+ }
+
+ /**
+ * @return the Telephony User Interface number to call to access voicemails directly from the IVR.
+ */
+ @NeededForTesting
+ public String getTuiAccessNumber() {
+ return mTuiAccessNumber;
+ }
+
+ /** @return the number to which client originated SMSes should be sent to. */
+ @NeededForTesting
+ public String getClientSmsDestinationNumber() {
+ return mClientSmsDestinationNumber;
+ }
+
+ /** @return the IMAP server port to talk to. */
+ public String getImapPort() {
+ return mImapPort;
+ }
+
+ /** @return the IMAP user name to be used for authentication. */
+ public String getImapUserName() {
+ return mImapUserName;
+ }
+
+ /** @return the IMAP password to be used for authentication. */
+ public String getImapPassword() {
+ return mImapPassword;
+ }
+
+ /** @return the SMTP server port to talk to. */
+ @NeededForTesting
+ public String getSmtpPort() {
+ return mSmtpPort;
+ }
+
+ /** @return the SMTP user name to be used for SMTP authentication. */
+ @NeededForTesting
+ public String getSmtpUserName() {
+ return mSmtpUserName;
+ }
+
+ /** @return the SMTP password to be used for SMTP authentication. */
+ @NeededForTesting
+ public String getSmtpPassword() {
+ return mSmtpPassword;
+ }
+
+ public String getTuiPasswordLength() {
+ return mTuiPasswordLength;
+ }
+
+ private static String getString(Bundle bundle, String key) {
+ String value = bundle.getString(key);
+ if (value == null) {
+ return "";
+ }
+ return value;
+ }
+
+ /** Saves a StatusMessage to the {@link VisualVoicemailPreferences}. Not all fields are saved. */
+ public VisualVoicemailPreferences.Editor putStatus(VisualVoicemailPreferences.Editor editor) {
+ return editor
+ .putString(OmtpConstants.IMAP_PORT, getImapPort())
+ .putString(OmtpConstants.SERVER_ADDRESS, getServerAddress())
+ .putString(OmtpConstants.IMAP_USER_NAME, getImapUserName())
+ .putString(OmtpConstants.IMAP_PASSWORD, getImapPassword())
+ .putString(OmtpConstants.TUI_PASSWORD_LENGTH, getTuiPasswordLength());
+ }
+}
diff --git a/java/com/android/voicemail/impl/sms/StatusSmsFetcher.java b/java/com/android/voicemail/impl/sms/StatusSmsFetcher.java
new file mode 100644
index 000000000..d178628c6
--- /dev/null
+++ b/java/com/android/voicemail/impl/sms/StatusSmsFetcher.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.sms;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
+import android.support.annotation.WorkerThread;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.SmsManager;
+import android.telephony.VisualVoicemailSms;
+import com.android.voicemail.impl.Assert;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.OmtpService;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.protocol.VisualVoicemailProtocol;
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/** Intercepts a incoming STATUS SMS with a blocking call. */
+@SuppressWarnings("AndroidApiChecker") /* CompletableFuture is java8*/
+@TargetApi(VERSION_CODES.O)
+public class StatusSmsFetcher extends BroadcastReceiver implements Closeable {
+
+ private static final String TAG = "VvmStatusSmsFetcher";
+
+ private static final long STATUS_SMS_TIMEOUT_MILLIS = 60_000;
+
+ private static final String ACTION_REQUEST_SENT_INTENT =
+ "com.android.voicemailomtp.sms.REQUEST_SENT";
+ private static final int ACTION_REQUEST_SENT_REQUEST_CODE = 0;
+
+ private CompletableFuture<Bundle> mFuture = new CompletableFuture<>();
+
+ private final Context mContext;
+ private final PhoneAccountHandle mPhoneAccountHandle;
+
+ public StatusSmsFetcher(Context context, PhoneAccountHandle phoneAccountHandle) {
+ mContext = context;
+ mPhoneAccountHandle = phoneAccountHandle;
+ IntentFilter filter = new IntentFilter(ACTION_REQUEST_SENT_INTENT);
+ filter.addAction(OmtpService.ACTION_SMS_RECEIVED);
+ context.registerReceiver(this, filter);
+ }
+
+ @Override
+ public void close() throws IOException {
+ mContext.unregisterReceiver(this);
+ }
+
+ @WorkerThread
+ @Nullable
+ public Bundle get()
+ throws InterruptedException, ExecutionException, TimeoutException, CancellationException {
+ Assert.isNotMainThread();
+ return mFuture.get(STATUS_SMS_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+ }
+
+ public PendingIntent getSentIntent() {
+ Intent intent = new Intent(ACTION_REQUEST_SENT_INTENT);
+ intent.setPackage(mContext.getPackageName());
+ // Because the receiver is registered dynamically, implicit intent must be used.
+ // There should only be a single status SMS request at a time.
+ return PendingIntent.getBroadcast(
+ mContext, ACTION_REQUEST_SENT_REQUEST_CODE, intent, PendingIntent.FLAG_CANCEL_CURRENT);
+ }
+
+ @Override
+ @MainThread
+ public void onReceive(Context context, Intent intent) {
+ Assert.isMainThread();
+ if (ACTION_REQUEST_SENT_INTENT.equals(intent.getAction())) {
+ int resultCode = getResultCode();
+
+ if (resultCode == Activity.RESULT_OK) {
+ VvmLog.d(TAG, "Request SMS successfully sent");
+ return;
+ }
+
+ VvmLog.e(TAG, "Request SMS send failed: " + sentSmsResultToString(resultCode));
+ mFuture.cancel(true);
+ return;
+ }
+
+ VisualVoicemailSms sms = intent.getExtras().getParcelable(OmtpService.EXTRA_VOICEMAIL_SMS);
+
+ if (!mPhoneAccountHandle.equals(sms.getPhoneAccountHandle())) {
+ return;
+ }
+ String eventType = sms.getPrefix();
+
+ if (eventType.equals(OmtpConstants.STATUS_SMS_PREFIX)) {
+ mFuture.complete(sms.getFields());
+ return;
+ }
+
+ if (eventType.equals(OmtpConstants.SYNC_SMS_PREFIX)) {
+ return;
+ }
+
+ VvmLog.i(
+ TAG,
+ "VVM SMS with event " + eventType + " received, attempting to translate to STATUS SMS");
+ OmtpVvmCarrierConfigHelper helper =
+ new OmtpVvmCarrierConfigHelper(context, mPhoneAccountHandle);
+ VisualVoicemailProtocol protocol = helper.getProtocol();
+ if (protocol == null) {
+ return;
+ }
+ Bundle translatedBundle = protocol.translateStatusSmsBundle(helper, eventType, sms.getFields());
+
+ if (translatedBundle != null) {
+ VvmLog.i(TAG, "Translated to STATUS SMS");
+ mFuture.complete(translatedBundle);
+ }
+ }
+
+ private static String sentSmsResultToString(int resultCode) {
+ switch (resultCode) {
+ case Activity.RESULT_OK:
+ return "OK";
+ case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
+ return "RESULT_ERROR_GENERIC_FAILURE";
+ case SmsManager.RESULT_ERROR_NO_SERVICE:
+ return "RESULT_ERROR_GENERIC_FAILURE";
+ case SmsManager.RESULT_ERROR_NULL_PDU:
+ return "RESULT_ERROR_GENERIC_FAILURE";
+ case SmsManager.RESULT_ERROR_RADIO_OFF:
+ return "RESULT_ERROR_GENERIC_FAILURE";
+ default:
+ return "UNKNOWN CODE: " + resultCode;
+ }
+ }
+}
diff --git a/java/com/android/voicemail/impl/sms/SyncMessage.java b/java/com/android/voicemail/impl/sms/SyncMessage.java
new file mode 100644
index 000000000..3cfa1a7b3
--- /dev/null
+++ b/java/com/android/voicemail/impl/sms/SyncMessage.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.sms;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import com.android.voicemail.impl.NeededForTesting;
+import com.android.voicemail.impl.OmtpConstants;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+
+/**
+ * Structured data representation of an OMTP SYNC message.
+ *
+ * <p>Getters will return null if the field was not set in the message body or it could not be
+ * parsed.
+ */
+public class SyncMessage {
+ // Sync event that triggered this message.
+ private final String mSyncTriggerEvent;
+ // Total number of new messages on the server.
+ private final int mNewMessageCount;
+ // UID of the new message.
+ private final String mMessageId;
+ // Length of the message.
+ private final int mMessageLength;
+ // Content type (voice, video, fax...) of the new message.
+ private final String mContentType;
+ // Sender of the new message.
+ private final String mSender;
+ // Timestamp (in millis) of the new message.
+ private final long mMsgTimeMillis;
+
+ @Override
+ public String toString() {
+ return "SyncMessage [mSyncTriggerEvent="
+ + mSyncTriggerEvent
+ + ", mNewMessageCount="
+ + mNewMessageCount
+ + ", mMessageId="
+ + mMessageId
+ + ", mMessageLength="
+ + mMessageLength
+ + ", mContentType="
+ + mContentType
+ + ", mSender="
+ + mSender
+ + ", mMsgTimeMillis="
+ + mMsgTimeMillis
+ + "]";
+ }
+
+ public SyncMessage(Bundle wrappedData) {
+ mSyncTriggerEvent = getString(wrappedData, OmtpConstants.SYNC_TRIGGER_EVENT);
+ mMessageId = getString(wrappedData, OmtpConstants.MESSAGE_UID);
+ mMessageLength = getInt(wrappedData, OmtpConstants.MESSAGE_LENGTH);
+ mContentType = getString(wrappedData, OmtpConstants.CONTENT_TYPE);
+ mSender = getString(wrappedData, OmtpConstants.SENDER);
+ mNewMessageCount = getInt(wrappedData, OmtpConstants.NUM_MESSAGE_COUNT);
+ mMsgTimeMillis = parseTime(wrappedData.getString(OmtpConstants.TIME));
+ }
+
+ private static long parseTime(@Nullable String value) {
+ if (value == null) {
+ return 0L;
+ }
+ try {
+ return new SimpleDateFormat(OmtpConstants.DATE_TIME_FORMAT, Locale.US).parse(value).getTime();
+ } catch (ParseException e) {
+ return 0L;
+ }
+ }
+ /**
+ * @return the event that triggered the sync message. This is a mandatory field and must always be
+ * set.
+ */
+ public String getSyncTriggerEvent() {
+ return mSyncTriggerEvent;
+ }
+
+ /** @return the number of new messages stored on the voicemail server. */
+ @NeededForTesting
+ public int getNewMessageCount() {
+ return mNewMessageCount;
+ }
+
+ /**
+ * @return the message ID of the new message.
+ * <p>Expected to be set only for {@link OmtpConstants#NEW_MESSAGE}
+ */
+ public String getId() {
+ return mMessageId;
+ }
+
+ /**
+ * @return the content type of the new message.
+ * <p>Expected to be set only for {@link OmtpConstants#NEW_MESSAGE}
+ */
+ @NeededForTesting
+ public String getContentType() {
+ return mContentType;
+ }
+
+ /**
+ * @return the message length of the new message.
+ * <p>Expected to be set only for {@link OmtpConstants#NEW_MESSAGE}
+ */
+ public int getLength() {
+ return mMessageLength;
+ }
+
+ /**
+ * @return the sender's phone number of the new message specified as MSISDN.
+ * <p>Expected to be set only for {@link OmtpConstants#NEW_MESSAGE}
+ */
+ public String getSender() {
+ return mSender;
+ }
+
+ /**
+ * @return the timestamp as milliseconds for the new message.
+ * <p>Expected to be set only for {@link OmtpConstants#NEW_MESSAGE}
+ */
+ public long getTimestampMillis() {
+ return mMsgTimeMillis;
+ }
+
+ private static int getInt(Bundle wrappedData, String key) {
+ String value = wrappedData.getString(key);
+ if (value == null) {
+ return 0;
+ }
+ try {
+ return Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ }
+
+ private static String getString(Bundle wrappedData, String key) {
+ String value = wrappedData.getString(key);
+ if (value == null) {
+ return "";
+ }
+ return value;
+ }
+}
diff --git a/java/com/android/voicemail/impl/sms/Vvm3MessageSender.java b/java/com/android/voicemail/impl/sms/Vvm3MessageSender.java
new file mode 100644
index 000000000..1f176925c
--- /dev/null
+++ b/java/com/android/voicemail/impl/sms/Vvm3MessageSender.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.sms;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+
+public class Vvm3MessageSender extends OmtpMessageSender {
+
+ /**
+ * Creates a new instance of Vvm3MessageSender.
+ *
+ * @param applicationPort If set to a value > 0 then a binary sms is sent to this port number.
+ * Otherwise, a standard text SMS is sent.
+ */
+ public Vvm3MessageSender(
+ Context context,
+ PhoneAccountHandle phoneAccountHandle,
+ short applicationPort,
+ String destinationNumber) {
+ super(context, phoneAccountHandle, applicationPort, destinationNumber);
+ }
+
+ @Override
+ public void requestVvmActivation(@Nullable PendingIntent sentIntent) {
+ // Activation not supported for VVM3, send a status request instead.
+ requestVvmStatus(sentIntent);
+ }
+
+ @Override
+ public void requestVvmDeactivation(@Nullable PendingIntent sentIntent) {
+ // Deactivation not supported for VVM3, do nothing
+ }
+
+ @Override
+ public void requestVvmStatus(@Nullable PendingIntent sentIntent) {
+ // Status message:
+ // STATUS
+ StringBuilder sb = new StringBuilder().append("STATUS");
+ sendSms(sb.toString(), sentIntent);
+ }
+}
diff --git a/java/com/android/voicemail/impl/sync/OmtpVvmSyncReceiver.java b/java/com/android/voicemail/impl/sync/OmtpVvmSyncReceiver.java
new file mode 100644
index 000000000..5a2fe146e
--- /dev/null
+++ b/java/com/android/voicemail/impl/sync/OmtpVvmSyncReceiver.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.sync;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.provider.VoicemailContract;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import com.android.voicemail.impl.ActivationTask;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil;
+import java.util.List;
+
+public class OmtpVvmSyncReceiver extends BroadcastReceiver {
+
+ private static final String TAG = "OmtpVvmSyncReceiver";
+
+ @Override
+ public void onReceive(final Context context, Intent intent) {
+ if (VoicemailContract.ACTION_SYNC_VOICEMAIL.equals(intent.getAction())) {
+ VvmLog.v(TAG, "Sync intent received");
+
+ List<PhoneAccountHandle> accounts =
+ context.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts();
+ for (PhoneAccountHandle phoneAccount : accounts) {
+ if (!VisualVoicemailSettingsUtil.isEnabled(context, phoneAccount)) {
+ continue;
+ }
+ if (!VvmAccountManager.isAccountActivated(context, phoneAccount)) {
+ VvmLog.i(TAG, "Unactivated account " + phoneAccount + " found, activating");
+ ActivationTask.start(context, phoneAccount, null);
+ } else {
+ SyncTask.start(context, phoneAccount, OmtpVvmSyncService.SYNC_FULL_SYNC);
+ }
+ }
+ }
+ }
+}
diff --git a/java/com/android/voicemail/impl/sync/OmtpVvmSyncService.java b/java/com/android/voicemail/impl/sync/OmtpVvmSyncService.java
new file mode 100644
index 000000000..c255019fc
--- /dev/null
+++ b/java/com/android/voicemail/impl/sync/OmtpVvmSyncService.java
@@ -0,0 +1,340 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.sync;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.net.Network;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.support.v4.os.BuildCompat;
+import android.telecom.PhoneAccountHandle;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.voicemail.impl.ActivationTask;
+import com.android.voicemail.impl.Assert;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.Voicemail;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.fetch.VoicemailFetchedCallback;
+import com.android.voicemail.impl.imap.ImapHelper;
+import com.android.voicemail.impl.imap.ImapHelper.InitializingException;
+import com.android.voicemail.impl.scheduling.BaseTask;
+import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil;
+import com.android.voicemail.impl.sync.VvmNetworkRequest.NetworkWrapper;
+import com.android.voicemail.impl.sync.VvmNetworkRequest.RequestFailedException;
+import com.android.voicemail.impl.utils.VoicemailDatabaseUtil;
+import java.util.List;
+import java.util.Map;
+
+/** Sync OMTP visual voicemail. */
+@TargetApi(VERSION_CODES.O)
+public class OmtpVvmSyncService {
+
+ private static final String TAG = OmtpVvmSyncService.class.getSimpleName();
+
+ /** Signifies a sync with both uploading to the server and downloading from the server. */
+ public static final String SYNC_FULL_SYNC = "full_sync";
+ /** Only upload to the server. */
+ public static final String SYNC_UPLOAD_ONLY = "upload_only";
+ /** Only download from the server. */
+ public static final String SYNC_DOWNLOAD_ONLY = "download_only";
+ /** Only download single voicemail transcription. */
+ public static final String SYNC_DOWNLOAD_ONE_TRANSCRIPTION = "download_one_transcription";
+ /** Threshold for whether we should archive and delete voicemails from the remote VM server. */
+ private static final float AUTO_DELETE_ARCHIVE_VM_THRESHOLD = 0.75f;
+
+ private final Context mContext;
+
+ private VoicemailsQueryHelper mQueryHelper;
+
+ public OmtpVvmSyncService(Context context) {
+ mContext = context;
+ mQueryHelper = new VoicemailsQueryHelper(mContext);
+ }
+
+ public void sync(
+ BaseTask task,
+ String action,
+ PhoneAccountHandle phoneAccount,
+ Voicemail voicemail,
+ VoicemailStatus.Editor status) {
+ Assert.isTrue(phoneAccount != null);
+ VvmLog.v(TAG, "Sync requested: " + action + " - for account: " + phoneAccount);
+ setupAndSendRequest(task, phoneAccount, voicemail, action, status);
+ }
+
+ private void setupAndSendRequest(
+ BaseTask task,
+ PhoneAccountHandle phoneAccount,
+ Voicemail voicemail,
+ String action,
+ VoicemailStatus.Editor status) {
+ if (!VisualVoicemailSettingsUtil.isEnabled(mContext, phoneAccount)) {
+ VvmLog.v(TAG, "Sync requested for disabled account");
+ return;
+ }
+ if (!VvmAccountManager.isAccountActivated(mContext, phoneAccount)) {
+ ActivationTask.start(mContext, phoneAccount, null);
+ return;
+ }
+
+ OmtpVvmCarrierConfigHelper config = new OmtpVvmCarrierConfigHelper(mContext, phoneAccount);
+ // DATA_IMAP_OPERATION_STARTED posting should not be deferred. This event clears all data
+ // channel errors, which should happen when the task starts, not when it ends. It is the
+ // "Sync in progress..." status.
+ config.handleEvent(
+ VoicemailStatus.edit(mContext, phoneAccount), OmtpEvents.DATA_IMAP_OPERATION_STARTED);
+ try (NetworkWrapper network = VvmNetworkRequest.getNetwork(config, phoneAccount, status)) {
+ if (network == null) {
+ VvmLog.e(TAG, "unable to acquire network");
+ task.fail();
+ return;
+ }
+ doSync(task, network.get(), phoneAccount, voicemail, action, status);
+ } catch (RequestFailedException e) {
+ config.handleEvent(status, OmtpEvents.DATA_NO_CONNECTION_CELLULAR_REQUIRED);
+ task.fail();
+ }
+ }
+
+ private void doSync(
+ BaseTask task,
+ Network network,
+ PhoneAccountHandle phoneAccount,
+ Voicemail voicemail,
+ String action,
+ VoicemailStatus.Editor status) {
+ try (ImapHelper imapHelper = new ImapHelper(mContext, phoneAccount, network, status)) {
+ boolean success;
+ if (voicemail == null) {
+ success = syncAll(action, imapHelper, phoneAccount);
+ } else {
+ success = syncOne(imapHelper, voicemail, phoneAccount);
+ }
+ if (success) {
+ // TODO: b/30569269 failure should interrupt all subsequent task via exceptions
+ imapHelper.updateQuota();
+ autoDeleteAndArchiveVM(imapHelper, phoneAccount);
+ imapHelper.handleEvent(OmtpEvents.DATA_IMAP_OPERATION_COMPLETED);
+ } else {
+ task.fail();
+ }
+ } catch (InitializingException e) {
+ VvmLog.w(TAG, "Can't retrieve Imap credentials.", e);
+ return;
+ }
+ }
+
+ /**
+ * If the VM quota exceeds {@value AUTO_DELETE_ARCHIVE_VM_THRESHOLD}, we should archive the VMs
+ * and delete them from the server to ensure new VMs can be received.
+ */
+ private void autoDeleteAndArchiveVM(
+ ImapHelper imapHelper, PhoneAccountHandle phoneAccountHandle) {
+
+ if (ConfigProviderBindings.get(mContext)
+ .getBoolean(VisualVoicemailSettingsUtil.ALLOW_VOICEMAIL_ARCHIVE, true)
+ && isArchiveEnabled(mContext, phoneAccountHandle)) {
+ if ((float) imapHelper.getOccuupiedQuota() / (float) imapHelper.getTotalQuota()
+ > AUTO_DELETE_ARCHIVE_VM_THRESHOLD) {
+ deleteAndArchiveVM(imapHelper);
+ imapHelper.updateQuota();
+ Logger.get(mContext)
+ .logImpression(DialerImpression.Type.VVM_ARCHIVE_AUTO_DELETED_VM_FROM_SERVER);
+ } else {
+ VvmLog.i(TAG, "no need to archive and auto delete VM, quota below threshold");
+ }
+ } else {
+ VvmLog.i(TAG, "autoDeleteAndArchiveVM is turned off");
+ Logger.get(mContext).logImpression(DialerImpression.Type.VVM_ARCHIVE_AUTO_DELETE_TURNED_OFF);
+ }
+ }
+
+ private static boolean isArchiveEnabled(Context context, PhoneAccountHandle phoneAccountHandle) {
+ return VisualVoicemailSettingsUtil.isArchiveEnabled(context, phoneAccountHandle)
+ && VisualVoicemailSettingsUtil.isEnabled(context, phoneAccountHandle);
+ }
+
+ private void deleteAndArchiveVM(ImapHelper imapHelper) {
+ // Archive column should only be used for 0 and above
+ Assert.isTrue(BuildCompat.isAtLeastO());
+ // The number of voicemails that exceed our threshold and should be deleted from the server
+ int numVoicemails =
+ imapHelper.getOccuupiedQuota()
+ - ((int) AUTO_DELETE_ARCHIVE_VM_THRESHOLD * imapHelper.getTotalQuota());
+ List<Voicemail> oldestVoicemails = mQueryHelper.oldestVoicemailsOnServer(numVoicemails);
+ if (!oldestVoicemails.isEmpty()) {
+ mQueryHelper.markArchivedInDatabase(oldestVoicemails);
+ imapHelper.markMessagesAsDeleted(oldestVoicemails);
+ VvmLog.i(
+ TAG,
+ String.format(
+ "successfully archived and deleted %d voicemails", oldestVoicemails.size()));
+ } else {
+ VvmLog.w(TAG, "remote voicemail server is empty");
+ }
+ }
+
+ private boolean syncAll(String action, ImapHelper imapHelper, PhoneAccountHandle account) {
+ boolean uploadSuccess = true;
+ boolean downloadSuccess = true;
+
+ if (SYNC_FULL_SYNC.equals(action) || SYNC_UPLOAD_ONLY.equals(action)) {
+ uploadSuccess = upload(imapHelper);
+ }
+ if (SYNC_FULL_SYNC.equals(action) || SYNC_DOWNLOAD_ONLY.equals(action)) {
+ downloadSuccess = download(imapHelper, account);
+ }
+
+ VvmLog.v(
+ TAG,
+ "upload succeeded: ["
+ + String.valueOf(uploadSuccess)
+ + "] download succeeded: ["
+ + String.valueOf(downloadSuccess)
+ + "]");
+
+ return uploadSuccess && downloadSuccess;
+ }
+
+ private boolean syncOne(ImapHelper imapHelper, Voicemail voicemail, PhoneAccountHandle account) {
+ if (shouldPerformPrefetch(account, imapHelper)) {
+ VoicemailFetchedCallback callback =
+ new VoicemailFetchedCallback(mContext, voicemail.getUri(), account);
+ imapHelper.fetchVoicemailPayload(callback, voicemail.getSourceData());
+ }
+
+ return imapHelper.fetchTranscription(
+ new TranscriptionFetchedCallback(mContext, voicemail), voicemail.getSourceData());
+ }
+
+ private boolean upload(ImapHelper imapHelper) {
+ List<Voicemail> readVoicemails = mQueryHelper.getReadVoicemails();
+ List<Voicemail> deletedVoicemails = mQueryHelper.getDeletedVoicemails();
+
+ boolean success = true;
+
+ if (deletedVoicemails.size() > 0) {
+ if (imapHelper.markMessagesAsDeleted(deletedVoicemails)) {
+ // We want to delete selectively instead of all the voicemails for this provider
+ // in case the state changed since the IMAP query was completed.
+ mQueryHelper.deleteFromDatabase(deletedVoicemails);
+ } else {
+ success = false;
+ }
+ }
+
+ if (readVoicemails.size() > 0) {
+ if (imapHelper.markMessagesAsRead(readVoicemails)) {
+ mQueryHelper.markCleanInDatabase(readVoicemails);
+ } else {
+ success = false;
+ }
+ }
+
+ return success;
+ }
+
+ private boolean download(ImapHelper imapHelper, PhoneAccountHandle account) {
+ List<Voicemail> serverVoicemails = imapHelper.fetchAllVoicemails();
+ List<Voicemail> localVoicemails = mQueryHelper.getAllVoicemails();
+
+ if (localVoicemails == null || serverVoicemails == null) {
+ // Null value means the query failed.
+ return false;
+ }
+
+ Map<String, Voicemail> remoteMap = buildMap(serverVoicemails);
+
+ // Go through all the local voicemails and check if they are on the server.
+ // They may be read or deleted on the server but not locally. Perform the
+ // appropriate local operation if the status differs from the server. Remove
+ // the messages that exist both locally and on the server to know which server
+ // messages to insert locally.
+ // Voicemails that were removed automatically from the server, are marked as
+ // archived and are stored locally. We do not delete them, as they were removed from the server
+ // by design (to make space).
+ for (int i = 0; i < localVoicemails.size(); i++) {
+ Voicemail localVoicemail = localVoicemails.get(i);
+ Voicemail remoteVoicemail = remoteMap.remove(localVoicemail.getSourceData());
+
+ // Do not delete voicemails that are archived marked as archived.
+ if (remoteVoicemail == null) {
+ mQueryHelper.deleteNonArchivedFromDatabase(localVoicemail);
+ } else {
+ if (remoteVoicemail.isRead() != localVoicemail.isRead()) {
+ mQueryHelper.markReadInDatabase(localVoicemail);
+ }
+
+ if (!TextUtils.isEmpty(remoteVoicemail.getTranscription())
+ && TextUtils.isEmpty(localVoicemail.getTranscription())) {
+ mQueryHelper.updateWithTranscription(localVoicemail, remoteVoicemail.getTranscription());
+ }
+ }
+ }
+
+ // The leftover messages are messages that exist on the server but not locally.
+ boolean prefetchEnabled = shouldPerformPrefetch(account, imapHelper);
+ for (Voicemail remoteVoicemail : remoteMap.values()) {
+ Uri uri = VoicemailDatabaseUtil.insert(mContext, remoteVoicemail);
+ if (prefetchEnabled) {
+ VoicemailFetchedCallback fetchedCallback =
+ new VoicemailFetchedCallback(mContext, uri, account);
+ imapHelper.fetchVoicemailPayload(fetchedCallback, remoteVoicemail.getSourceData());
+ }
+ }
+
+ return true;
+ }
+
+ private boolean shouldPerformPrefetch(PhoneAccountHandle account, ImapHelper imapHelper) {
+ OmtpVvmCarrierConfigHelper carrierConfigHelper =
+ new OmtpVvmCarrierConfigHelper(mContext, account);
+ return carrierConfigHelper.isPrefetchEnabled() && !imapHelper.isRoaming();
+ }
+
+ /** Builds a map from provider data to message for the given collection of voicemails. */
+ private Map<String, Voicemail> buildMap(List<Voicemail> messages) {
+ Map<String, Voicemail> map = new ArrayMap<String, Voicemail>();
+ for (Voicemail message : messages) {
+ map.put(message.getSourceData(), message);
+ }
+ return map;
+ }
+
+ /** Callback for {@link ImapHelper#fetchTranscription(TranscriptionFetchedCallback, String)} */
+ public static class TranscriptionFetchedCallback {
+
+ private Context mContext;
+ private Voicemail mVoicemail;
+
+ public TranscriptionFetchedCallback(Context context, Voicemail voicemail) {
+ mContext = context;
+ mVoicemail = voicemail;
+ }
+
+ public void setVoicemailTranscription(String transcription) {
+ VoicemailsQueryHelper queryHelper = new VoicemailsQueryHelper(mContext);
+ queryHelper.updateWithTranscription(mVoicemail, transcription);
+ }
+ }
+}
diff --git a/java/com/android/voicemail/impl/sync/SyncOneTask.java b/java/com/android/voicemail/impl/sync/SyncOneTask.java
new file mode 100644
index 000000000..f9701506d
--- /dev/null
+++ b/java/com/android/voicemail/impl/sync/SyncOneTask.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl.sync;
+
+import android.content.Context;
+import android.content.Intent;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.Voicemail;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.scheduling.BaseTask;
+import com.android.voicemail.impl.scheduling.RetryPolicy;
+
+/**
+ * Task to download a single voicemail from the server. This task is initiated by a SMS notifying
+ * the new voicemail arrival, and ignores the duplicated tasks constraint.
+ */
+public class SyncOneTask extends BaseTask {
+
+ private static final int RETRY_TIMES = 2;
+ private static final int RETRY_INTERVAL_MILLIS = 5_000;
+
+ private static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle";
+ private static final String EXTRA_SYNC_TYPE = "extra_sync_type";
+ private static final String EXTRA_VOICEMAIL = "extra_voicemail";
+
+ private PhoneAccountHandle mPhone;
+ private String mSyncType;
+ private Voicemail mVoicemail;
+
+ public static void start(Context context, PhoneAccountHandle phone, Voicemail voicemail) {
+ Intent intent = BaseTask.createIntent(context, SyncOneTask.class, phone);
+ intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, phone);
+ intent.putExtra(EXTRA_SYNC_TYPE, OmtpVvmSyncService.SYNC_DOWNLOAD_ONE_TRANSCRIPTION);
+ intent.putExtra(EXTRA_VOICEMAIL, voicemail);
+ context.startService(intent);
+ }
+
+ public SyncOneTask() {
+ super(TASK_ALLOW_DUPLICATES);
+ addPolicy(new RetryPolicy(RETRY_TIMES, RETRY_INTERVAL_MILLIS));
+ }
+
+ public void onCreate(Context context, Intent intent, int flags, int startId) {
+ super.onCreate(context, intent, flags, startId);
+ mPhone = intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE);
+ mSyncType = intent.getStringExtra(EXTRA_SYNC_TYPE);
+ mVoicemail = intent.getParcelableExtra(EXTRA_VOICEMAIL);
+ }
+
+ @Override
+ public void onExecuteInBackgroundThread() {
+ OmtpVvmSyncService service = new OmtpVvmSyncService(getContext());
+ service.sync(this, mSyncType, mPhone, mVoicemail, VoicemailStatus.edit(getContext(), mPhone));
+ }
+
+ @Override
+ public Intent createRestartIntent() {
+ Intent intent = super.createRestartIntent();
+ intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, mPhone);
+ intent.putExtra(EXTRA_SYNC_TYPE, mSyncType);
+ intent.putExtra(EXTRA_VOICEMAIL, mVoicemail);
+ return intent;
+ }
+}
diff --git a/java/com/android/voicemail/impl/sync/SyncTask.java b/java/com/android/voicemail/impl/sync/SyncTask.java
new file mode 100644
index 000000000..71c98412b
--- /dev/null
+++ b/java/com/android/voicemail/impl/sync/SyncTask.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.sync;
+
+import android.content.Context;
+import android.content.Intent;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.scheduling.BaseTask;
+import com.android.voicemail.impl.scheduling.MinimalIntervalPolicy;
+import com.android.voicemail.impl.scheduling.RetryPolicy;
+
+/** System initiated sync request. */
+public class SyncTask extends BaseTask {
+
+ // Try sync for a total of 5 times, should take around 5 minutes before finally giving up.
+ private static final int RETRY_TIMES = 4;
+ private static final int RETRY_INTERVAL_MILLIS = 5_000;
+ private static final int MINIMAL_INTERVAL_MILLIS = 60_000;
+
+ private static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle";
+ private static final String EXTRA_SYNC_TYPE = "extra_sync_type";
+
+ private final RetryPolicy mRetryPolicy;
+
+ private PhoneAccountHandle mPhone;
+ private String mSyncType;
+
+ public static void start(Context context, PhoneAccountHandle phone, String syncType) {
+ Intent intent = BaseTask.createIntent(context, SyncTask.class, phone);
+ intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, phone);
+ intent.putExtra(EXTRA_SYNC_TYPE, syncType);
+ context.startService(intent);
+ }
+
+ public SyncTask() {
+ super(TASK_SYNC);
+ mRetryPolicy = new RetryPolicy(RETRY_TIMES, RETRY_INTERVAL_MILLIS);
+ addPolicy(mRetryPolicy);
+ addPolicy(new MinimalIntervalPolicy(MINIMAL_INTERVAL_MILLIS));
+ }
+
+ public void onCreate(Context context, Intent intent, int flags, int startId) {
+ super.onCreate(context, intent, flags, startId);
+ mPhone = intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE);
+ mSyncType = intent.getStringExtra(EXTRA_SYNC_TYPE);
+ }
+
+ @Override
+ public void onExecuteInBackgroundThread() {
+ OmtpVvmSyncService service = new OmtpVvmSyncService(getContext());
+ service.sync(this, mSyncType, mPhone, null, mRetryPolicy.getVoicemailStatusEditor());
+ }
+
+ @Override
+ public Intent createRestartIntent() {
+ Intent intent = super.createRestartIntent();
+ intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, mPhone);
+ intent.putExtra(EXTRA_SYNC_TYPE, mSyncType);
+ return intent;
+ }
+}
diff --git a/java/com/android/voicemail/impl/sync/UploadTask.java b/java/com/android/voicemail/impl/sync/UploadTask.java
new file mode 100644
index 000000000..7d1a79756
--- /dev/null
+++ b/java/com/android/voicemail/impl/sync/UploadTask.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.sync;
+
+import android.content.Context;
+import android.content.Intent;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.scheduling.BaseTask;
+import com.android.voicemail.impl.scheduling.PostponePolicy;
+
+/**
+ * Upload task triggered by database changes. Will wait until the database has been stable for
+ * {@link #POSTPONE_MILLIS} to execute.
+ */
+public class UploadTask extends BaseTask {
+
+ private static final String TAG = "VvmUploadTask";
+
+ private static final int POSTPONE_MILLIS = 5_000;
+
+ public UploadTask() {
+ super(TASK_UPLOAD);
+ addPolicy(new PostponePolicy(POSTPONE_MILLIS));
+ }
+
+ public static void start(Context context, PhoneAccountHandle phoneAccountHandle) {
+ Intent intent = BaseTask.createIntent(context, UploadTask.class, phoneAccountHandle);
+ context.startService(intent);
+ }
+
+ @Override
+ public void onCreate(Context context, Intent intent, int flags, int startId) {
+ super.onCreate(context, intent, flags, startId);
+ }
+
+ @Override
+ public void onExecuteInBackgroundThread() {
+ OmtpVvmSyncService service = new OmtpVvmSyncService(getContext());
+
+ PhoneAccountHandle phoneAccountHandle = getPhoneAccountHandle();
+ if (phoneAccountHandle == null) {
+ // This should never happen
+ VvmLog.e(TAG, "null phone account for phoneAccountHandle " + getPhoneAccountHandle());
+ return;
+ }
+ service.sync(
+ this,
+ OmtpVvmSyncService.SYNC_UPLOAD_ONLY,
+ phoneAccountHandle,
+ null,
+ VoicemailStatus.edit(getContext(), phoneAccountHandle));
+ }
+}
diff --git a/java/com/android/voicemailomtp/sync/VoicemailProviderChangeReceiver.java b/java/com/android/voicemail/impl/sync/VoicemailProviderChangeReceiver.java
index ade9ef12d..eaca3c44b 100644
--- a/java/com/android/voicemailomtp/sync/VoicemailProviderChangeReceiver.java
+++ b/java/com/android/voicemail/impl/sync/VoicemailProviderChangeReceiver.java
@@ -13,29 +13,28 @@
* See the License for the specific language governing permissions and
* limitations under the License
*/
-package com.android.voicemailomtp.sync;
+package com.android.voicemail.impl.sync;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.provider.VoicemailContract;
import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil;
-/**
- * Receives changes to the voicemail provider so they can be sent to the voicemail server.
- */
+/** Receives changes to the voicemail provider so they can be sent to the voicemail server. */
public class VoicemailProviderChangeReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- boolean isSelfChanged = intent.getBooleanExtra(VoicemailContract.EXTRA_SELF_CHANGE, false);
- OmtpVvmSourceManager vvmSourceManager =
- OmtpVvmSourceManager.getInstance(context);
- if (vvmSourceManager.getOmtpVvmSources().size() > 0 && !isSelfChanged) {
- for (PhoneAccountHandle source : OmtpVvmSourceManager.getInstance(context)
- .getOmtpVvmSources()) {
- UploadTask.start(context, source);
- }
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ boolean isSelfChanged = intent.getBooleanExtra(VoicemailContract.EXTRA_SELF_CHANGE, false);
+ if (!isSelfChanged) {
+ for (PhoneAccountHandle phoneAccount : VvmAccountManager.getActiveAccounts(context)) {
+ if (!VisualVoicemailSettingsUtil.isEnabled(context, phoneAccount)) {
+ continue;
}
+ UploadTask.start(context, phoneAccount);
+ }
}
+ }
}
diff --git a/java/com/android/voicemail/impl/sync/VoicemailStatusQueryHelper.java b/java/com/android/voicemail/impl/sync/VoicemailStatusQueryHelper.java
new file mode 100644
index 000000000..4ef19daf6
--- /dev/null
+++ b/java/com/android/voicemail/impl/sync/VoicemailStatusQueryHelper.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.sync;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Status;
+import android.telecom.PhoneAccountHandle;
+
+/** Construct queries to interact with the voicemail status table. */
+public class VoicemailStatusQueryHelper {
+
+ static final String[] PROJECTION =
+ new String[] {
+ Status._ID, // 0
+ Status.CONFIGURATION_STATE, // 1
+ Status.NOTIFICATION_CHANNEL_STATE, // 2
+ Status.SOURCE_PACKAGE // 3
+ };
+
+ public static final int _ID = 0;
+ public static final int CONFIGURATION_STATE = 1;
+ public static final int NOTIFICATION_CHANNEL_STATE = 2;
+ public static final int SOURCE_PACKAGE = 3;
+
+ private Context mContext;
+ private ContentResolver mContentResolver;
+ private Uri mSourceUri;
+
+ public VoicemailStatusQueryHelper(Context context) {
+ mContext = context;
+ mContentResolver = context.getContentResolver();
+ mSourceUri = VoicemailContract.Status.buildSourceUri(mContext.getPackageName());
+ }
+
+ /**
+ * Check if the configuration state for the voicemail source is "ok", meaning that the source is
+ * set up.
+ *
+ * @param phoneAccount The phone account for the voicemail source to check.
+ * @return {@code true} if the voicemail source is configured, {@code} false otherwise, including
+ * if the voicemail source is not registered in the table.
+ */
+ public boolean isVoicemailSourceConfigured(PhoneAccountHandle phoneAccount) {
+ return isFieldEqualTo(phoneAccount, CONFIGURATION_STATE, Status.CONFIGURATION_STATE_OK);
+ }
+
+ /**
+ * Check if the notifications channel of a voicemail source is active. That is, when a new
+ * voicemail is available, if the server able to notify the device.
+ *
+ * @return {@code true} if notifications channel is active, {@code false} otherwise.
+ */
+ public boolean isNotificationsChannelActive(PhoneAccountHandle phoneAccount) {
+ return isFieldEqualTo(
+ phoneAccount, NOTIFICATION_CHANNEL_STATE, Status.NOTIFICATION_CHANNEL_STATE_OK);
+ }
+
+ /**
+ * Check if a field for an entry in the status table is equal to a specific value.
+ *
+ * @param phoneAccount The phone account of the voicemail source to query for.
+ * @param columnIndex The column index of the field in the returned query.
+ * @param value The value to compare against.
+ * @return {@code true} if the stored value is equal to the provided value. {@code false}
+ * otherwise.
+ */
+ private boolean isFieldEqualTo(PhoneAccountHandle phoneAccount, int columnIndex, int value) {
+ Cursor cursor = null;
+ if (phoneAccount != null) {
+ String phoneAccountComponentName = phoneAccount.getComponentName().flattenToString();
+ String phoneAccountId = phoneAccount.getId();
+ if (phoneAccountComponentName == null || phoneAccountId == null) {
+ return false;
+ }
+ try {
+ String whereClause =
+ Status.PHONE_ACCOUNT_COMPONENT_NAME
+ + "=? AND "
+ + Status.PHONE_ACCOUNT_ID
+ + "=? AND "
+ + Status.SOURCE_PACKAGE
+ + "=?";
+ String[] whereArgs = {phoneAccountComponentName, phoneAccountId, mContext.getPackageName()};
+ cursor = mContentResolver.query(mSourceUri, PROJECTION, whereClause, whereArgs, null);
+ if (cursor != null && cursor.moveToFirst()) {
+ return cursor.getInt(columnIndex) == value;
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/java/com/android/voicemail/impl/sync/VoicemailsQueryHelper.java b/java/com/android/voicemail/impl/sync/VoicemailsQueryHelper.java
new file mode 100644
index 000000000..d129406ff
--- /dev/null
+++ b/java/com/android/voicemail/impl/sync/VoicemailsQueryHelper.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.sync;
+
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Voicemails;
+import android.telecom.PhoneAccountHandle;
+import com.android.dialer.common.Assert;
+import com.android.voicemail.impl.Voicemail;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Construct queries to interact with the voicemails table. */
+public class VoicemailsQueryHelper {
+ static final String[] PROJECTION =
+ new String[] {
+ Voicemails._ID, // 0
+ Voicemails.SOURCE_DATA, // 1
+ Voicemails.IS_READ, // 2
+ Voicemails.DELETED, // 3
+ Voicemails.TRANSCRIPTION // 4
+ };
+
+ public static final int _ID = 0;
+ public static final int SOURCE_DATA = 1;
+ public static final int IS_READ = 2;
+ public static final int DELETED = 3;
+ public static final int TRANSCRIPTION = 4;
+
+ static final String READ_SELECTION =
+ Voicemails.DIRTY + "=1 AND " + Voicemails.DELETED + "!=1 AND " + Voicemails.IS_READ + "=1";
+ static final String DELETED_SELECTION = Voicemails.DELETED + "=1";
+ static final String ARCHIVED_SELECTION = Voicemails.ARCHIVED + "=0";
+
+ private Context mContext;
+ private ContentResolver mContentResolver;
+ private Uri mSourceUri;
+
+ public VoicemailsQueryHelper(Context context) {
+ mContext = context;
+ mContentResolver = context.getContentResolver();
+ mSourceUri = VoicemailContract.Voicemails.buildSourceUri(mContext.getPackageName());
+ }
+
+ /**
+ * Get all the local read voicemails that have not been synced to the server.
+ *
+ * @return A list of read voicemails.
+ */
+ public List<Voicemail> getReadVoicemails() {
+ return getLocalVoicemails(READ_SELECTION);
+ }
+
+ /**
+ * Get all the locally deleted voicemails that have not been synced to the server.
+ *
+ * @return A list of deleted voicemails.
+ */
+ public List<Voicemail> getDeletedVoicemails() {
+ return getLocalVoicemails(DELETED_SELECTION);
+ }
+
+ /**
+ * Get all voicemails locally stored.
+ *
+ * @return A list of all locally stored voicemails.
+ */
+ public List<Voicemail> getAllVoicemails() {
+ return getLocalVoicemails(null);
+ }
+
+ /**
+ * Utility method to make queries to the voicemail database.
+ *
+ * @param selection A filter declaring which rows to return. {@code null} returns all rows.
+ * @return A list of voicemails according to the selection statement.
+ */
+ private List<Voicemail> getLocalVoicemails(String selection) {
+ Cursor cursor = mContentResolver.query(mSourceUri, PROJECTION, selection, null, null);
+ if (cursor == null) {
+ return null;
+ }
+ try {
+ List<Voicemail> voicemails = new ArrayList<Voicemail>();
+ while (cursor.moveToNext()) {
+ final long id = cursor.getLong(_ID);
+ final String sourceData = cursor.getString(SOURCE_DATA);
+ final boolean isRead = cursor.getInt(IS_READ) == 1;
+ final String transcription = cursor.getString(TRANSCRIPTION);
+ Voicemail voicemail =
+ Voicemail.createForUpdate(id, sourceData)
+ .setIsRead(isRead)
+ .setTranscription(transcription)
+ .build();
+ voicemails.add(voicemail);
+ }
+ return voicemails;
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * Deletes a list of voicemails from the voicemail content provider.
+ *
+ * @param voicemails The list of voicemails to delete
+ * @return The number of voicemails deleted
+ */
+ public int deleteFromDatabase(List<Voicemail> voicemails) {
+ int count = voicemails.size();
+ if (count == 0) {
+ return 0;
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < count; i++) {
+ if (i > 0) {
+ sb.append(",");
+ }
+ sb.append(voicemails.get(i).getId());
+ }
+
+ String selectionStatement = String.format(Voicemails._ID + " IN (%s)", sb.toString());
+ return mContentResolver.delete(Voicemails.CONTENT_URI, selectionStatement, null);
+ }
+
+ /** Utility method to delete a single voicemail that is not archived. */
+ public void deleteNonArchivedFromDatabase(Voicemail voicemail) {
+ mContentResolver.delete(
+ Voicemails.CONTENT_URI,
+ Voicemails._ID + "=? AND " + Voicemails.ARCHIVED + "= 0",
+ new String[] {Long.toString(voicemail.getId())});
+ }
+
+ public int markReadInDatabase(List<Voicemail> voicemails) {
+ int count = voicemails.size();
+ for (int i = 0; i < count; i++) {
+ markReadInDatabase(voicemails.get(i));
+ }
+ return count;
+ }
+
+ /** Utility method to mark single message as read. */
+ public void markReadInDatabase(Voicemail voicemail) {
+ Uri uri = ContentUris.withAppendedId(mSourceUri, voicemail.getId());
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(Voicemails.IS_READ, "1");
+ mContentResolver.update(uri, contentValues, null, null);
+ }
+
+ /**
+ * Sends an update command to the voicemail content provider for a list of voicemails. From the
+ * view of the provider, since the updater is the owner of the entry, a blank "update" means that
+ * the voicemail source is indicating that the server has up-to-date information on the voicemail.
+ * This flips the "dirty" bit to "0".
+ *
+ * @param voicemails The list of voicemails to update
+ * @return The number of voicemails updated
+ */
+ public int markCleanInDatabase(List<Voicemail> voicemails) {
+ int count = voicemails.size();
+ for (int i = 0; i < count; i++) {
+ markCleanInDatabase(voicemails.get(i));
+ }
+ return count;
+ }
+
+ /** Utility method to mark single message as clean. */
+ public void markCleanInDatabase(Voicemail voicemail) {
+ Uri uri = ContentUris.withAppendedId(mSourceUri, voicemail.getId());
+ ContentValues contentValues = new ContentValues();
+ mContentResolver.update(uri, contentValues, null, null);
+ }
+
+ /** Utility method to add a transcription to the voicemail. */
+ public void updateWithTranscription(Voicemail voicemail, String transcription) {
+ Uri uri = ContentUris.withAppendedId(mSourceUri, voicemail.getId());
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(Voicemails.TRANSCRIPTION, transcription);
+ mContentResolver.update(uri, contentValues, null, null);
+ }
+
+ /**
+ * Voicemail is unique if the tuple of (phone account component name, phone account id, source
+ * data) is unique. If the phone account is missing, we also consider this unique since it's
+ * simply an "unknown" account.
+ *
+ * @param voicemail The voicemail to check if it is unique.
+ * @return {@code true} if the voicemail is unique, {@code false} otherwise.
+ */
+ public boolean isVoicemailUnique(Voicemail voicemail) {
+ Cursor cursor = null;
+ PhoneAccountHandle phoneAccount = voicemail.getPhoneAccount();
+ if (phoneAccount != null) {
+ String phoneAccountComponentName = phoneAccount.getComponentName().flattenToString();
+ String phoneAccountId = phoneAccount.getId();
+ String sourceData = voicemail.getSourceData();
+ if (phoneAccountComponentName == null || phoneAccountId == null || sourceData == null) {
+ return true;
+ }
+ try {
+ String whereClause =
+ Voicemails.PHONE_ACCOUNT_COMPONENT_NAME
+ + "=? AND "
+ + Voicemails.PHONE_ACCOUNT_ID
+ + "=? AND "
+ + Voicemails.SOURCE_DATA
+ + "=?";
+ String[] whereArgs = {phoneAccountComponentName, phoneAccountId, sourceData};
+ cursor = mContentResolver.query(mSourceUri, PROJECTION, whereClause, whereArgs, null);
+ if (cursor.getCount() == 0) {
+ return true;
+ } else {
+ return false;
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Marks voicemails in the local database as archived. This indicates that the voicemails from the
+ * server were removed automatically to make space for new voicemails, and are stored locally on
+ * the users devices, without a corresponding server copy.
+ */
+ public void markArchivedInDatabase(List<Voicemail> voicemails) {
+ for (Voicemail voicemail : voicemails) {
+ markArchiveInDatabase(voicemail);
+ }
+ }
+
+ /** Utility method to mark single voicemail as archived. */
+ public void markArchiveInDatabase(Voicemail voicemail) {
+ Uri uri = ContentUris.withAppendedId(mSourceUri, voicemail.getId());
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(Voicemails.ARCHIVED, "1");
+ mContentResolver.update(uri, contentValues, null, null);
+ }
+
+ /** Find the oldest voicemails that are on the device, and also on the server. */
+ @TargetApi(VERSION_CODES.M) // used for try with resources
+ public List<Voicemail> oldestVoicemailsOnServer(int numVoicemails) {
+ if (numVoicemails <= 0) {
+ Assert.fail("Query for remote voicemails cannot be <= 0");
+ }
+
+ String sortAndLimit = "date ASC limit " + numVoicemails;
+
+ try (Cursor cursor =
+ mContentResolver.query(mSourceUri, null, ARCHIVED_SELECTION, null, sortAndLimit)) {
+
+ Assert.isNotNull(cursor);
+
+ List<Voicemail> voicemails = new ArrayList<>();
+ while (cursor.moveToNext()) {
+ final String sourceData = cursor.getString(SOURCE_DATA);
+ Voicemail voicemail = Voicemail.createForUpdate(cursor.getLong(_ID), sourceData).build();
+ voicemails.add(voicemail);
+ }
+
+ if (voicemails.size() != numVoicemails) {
+ Assert.fail(
+ String.format(
+ "voicemail count (%d) doesn't matched expected (%d)",
+ voicemails.size(), numVoicemails));
+ }
+ return voicemails;
+ }
+ }
+}
diff --git a/java/com/android/voicemail/impl/sync/VvmAccountManager.java b/java/com/android/voicemail/impl/sync/VvmAccountManager.java
new file mode 100644
index 000000000..05f649450
--- /dev/null
+++ b/java/com/android/voicemail/impl/sync/VvmAccountManager.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.sync;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import com.android.dialer.common.Assert;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.VisualVoicemailPreferences;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.sms.StatusMessage;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tracks the activation state of a visual voicemail phone account. An account is considered
+ * activated if it has valid connection information from the {@link StatusMessage} stored on the
+ * device. Once activation/provisioning is completed, {@link #addAccount(Context,
+ * PhoneAccountHandle, StatusMessage)} should be called to store the connection information. When an
+ * account is removed or if the connection information is deemed invalid, {@link
+ * #removeAccount(Context, PhoneAccountHandle)} should be called to clear the connection information
+ * and allow reactivation.
+ */
+public class VvmAccountManager {
+ public static final String TAG = "VvmAccountManager";
+
+ private static final String IS_ACCOUNT_ACTIVATED = "is_account_activated";
+
+ public static void addAccount(
+ Context context, PhoneAccountHandle phoneAccountHandle, StatusMessage statusMessage) {
+ VisualVoicemailPreferences preferences =
+ new VisualVoicemailPreferences(context, phoneAccountHandle);
+ statusMessage.putStatus(preferences.edit()).putBoolean(IS_ACCOUNT_ACTIVATED, true).apply();
+ }
+
+ public static void removeAccount(Context context, PhoneAccountHandle phoneAccount) {
+ VoicemailStatus.disable(context, phoneAccount);
+ VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(context, phoneAccount);
+ preferences
+ .edit()
+ .putBoolean(IS_ACCOUNT_ACTIVATED, false)
+ .putString(OmtpConstants.IMAP_USER_NAME, null)
+ .putString(OmtpConstants.IMAP_PASSWORD, null)
+ .apply();
+ }
+
+ public static boolean isAccountActivated(Context context, PhoneAccountHandle phoneAccount) {
+ Assert.isNotNull(phoneAccount);
+ VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(context, phoneAccount);
+ return preferences.getBoolean(IS_ACCOUNT_ACTIVATED, false);
+ }
+
+ @NonNull
+ public static List<PhoneAccountHandle> getActiveAccounts(Context context) {
+ List<PhoneAccountHandle> results = new ArrayList<>();
+ for (PhoneAccountHandle phoneAccountHandle :
+ context.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts()) {
+ if (isAccountActivated(context, phoneAccountHandle)) {
+ results.add(phoneAccountHandle);
+ }
+ }
+ return results;
+ }
+}
diff --git a/java/com/android/voicemail/impl/sync/VvmNetworkRequest.java b/java/com/android/voicemail/impl/sync/VvmNetworkRequest.java
new file mode 100644
index 000000000..189dc8f2b
--- /dev/null
+++ b/java/com/android/voicemail/impl/sync/VvmNetworkRequest.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.sync;
+
+import android.annotation.TargetApi;
+import android.net.Network;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.NonNull;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VvmLog;
+import java.io.Closeable;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+/**
+ * Class to retrieve a {@link Network} synchronously. {@link #getNetwork(OmtpVvmCarrierConfigHelper,
+ * PhoneAccountHandle)} will block until a suitable network is retrieved or it has failed.
+ */
+@SuppressWarnings("AndroidApiChecker") /* CompletableFuture is java8*/
+@TargetApi(VERSION_CODES.O)
+public class VvmNetworkRequest {
+
+ private static final String TAG = "VvmNetworkRequest";
+
+ /**
+ * A wrapper around a Network returned by a {@link VvmNetworkRequestCallback}, which should be
+ * closed once not needed anymore.
+ */
+ public static class NetworkWrapper implements Closeable {
+
+ private final Network mNetwork;
+ private final VvmNetworkRequestCallback mCallback;
+
+ private NetworkWrapper(Network network, VvmNetworkRequestCallback callback) {
+ mNetwork = network;
+ mCallback = callback;
+ }
+
+ public Network get() {
+ return mNetwork;
+ }
+
+ @Override
+ public void close() {
+ mCallback.releaseNetwork();
+ }
+ }
+
+ public static class RequestFailedException extends Exception {
+
+ private RequestFailedException(Throwable cause) {
+ super(cause);
+ }
+ }
+
+ @NonNull
+ public static NetworkWrapper getNetwork(
+ OmtpVvmCarrierConfigHelper config, PhoneAccountHandle handle, VoicemailStatus.Editor status)
+ throws RequestFailedException {
+ FutureNetworkRequestCallback callback =
+ new FutureNetworkRequestCallback(config, handle, status);
+ callback.requestNetwork();
+ try {
+ return callback.getFuture().get();
+ } catch (InterruptedException | ExecutionException e) {
+ callback.releaseNetwork();
+ VvmLog.e(TAG, "can't get future network", e);
+ throw new RequestFailedException(e);
+ }
+ }
+
+ private static class FutureNetworkRequestCallback extends VvmNetworkRequestCallback {
+
+ /**
+ * {@link CompletableFuture#get()} will block until {@link CompletableFuture# complete(Object) }
+ * has been called on the other thread.
+ */
+ private final CompletableFuture<NetworkWrapper> mFuture = new CompletableFuture<>();
+
+ public FutureNetworkRequestCallback(
+ OmtpVvmCarrierConfigHelper config,
+ PhoneAccountHandle phoneAccount,
+ VoicemailStatus.Editor status) {
+ super(config, phoneAccount, status);
+ }
+
+ public Future<NetworkWrapper> getFuture() {
+ return mFuture;
+ }
+
+ @Override
+ public void onAvailable(Network network) {
+ super.onAvailable(network);
+ mFuture.complete(new NetworkWrapper(network, this));
+ }
+
+ @Override
+ public void onFailed(String reason) {
+ super.onFailed(reason);
+ mFuture.complete(null);
+ }
+ }
+}
diff --git a/java/com/android/voicemail/impl/sync/VvmNetworkRequestCallback.java b/java/com/android/voicemail/impl/sync/VvmNetworkRequestCallback.java
new file mode 100644
index 000000000..067eff803
--- /dev/null
+++ b/java/com/android/voicemail/impl/sync/VvmNetworkRequestCallback.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.sync;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.Build.VERSION_CODES;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.CallSuper;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import com.android.dialer.common.Assert;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VvmLog;
+
+/**
+ * Base class for network request call backs for visual voicemail syncing with the Imap server. This
+ * handles retries and network requests.
+ */
+@TargetApi(VERSION_CODES.O)
+public abstract class VvmNetworkRequestCallback extends ConnectivityManager.NetworkCallback {
+
+ private static final String TAG = "VvmNetworkRequest";
+
+ // Timeout used to call ConnectivityManager.requestNetwork
+ private static final int NETWORK_REQUEST_TIMEOUT_MILLIS = 60 * 1000;
+
+ public static final String NETWORK_REQUEST_FAILED_TIMEOUT = "timeout";
+ public static final String NETWORK_REQUEST_FAILED_LOST = "lost";
+
+ protected Context mContext;
+ protected PhoneAccountHandle mPhoneAccount;
+ protected NetworkRequest mNetworkRequest;
+ private ConnectivityManager mConnectivityManager;
+ private final OmtpVvmCarrierConfigHelper mCarrierConfigHelper;
+ private final VoicemailStatus.Editor mStatus;
+ private boolean mRequestSent = false;
+ private boolean mResultReceived = false;
+
+ public VvmNetworkRequestCallback(
+ Context context, PhoneAccountHandle phoneAccount, VoicemailStatus.Editor status) {
+ mContext = context;
+ mPhoneAccount = phoneAccount;
+ mStatus = status;
+ mCarrierConfigHelper = new OmtpVvmCarrierConfigHelper(context, mPhoneAccount);
+ mNetworkRequest = createNetworkRequest();
+ }
+
+ public VvmNetworkRequestCallback(
+ OmtpVvmCarrierConfigHelper config,
+ PhoneAccountHandle phoneAccount,
+ VoicemailStatus.Editor status) {
+ mContext = config.getContext();
+ mPhoneAccount = phoneAccount;
+ mStatus = status;
+ mCarrierConfigHelper = config;
+ mNetworkRequest = createNetworkRequest();
+ }
+
+ public VoicemailStatus.Editor getVoicemailStatusEditor() {
+ return mStatus;
+ }
+
+ /**
+ * @return NetworkRequest for a proper transport type. Use only cellular network if the carrier
+ * requires it. Otherwise use whatever available.
+ */
+ private NetworkRequest createNetworkRequest() {
+
+ NetworkRequest.Builder builder =
+ new NetworkRequest.Builder().addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+
+ TelephonyManager telephonyManager =
+ mContext
+ .getSystemService(TelephonyManager.class)
+ .createForPhoneAccountHandle(mPhoneAccount);
+ // At this point mPhoneAccount should always be valid and telephonyManager will never be null
+ Assert.isNotNull(telephonyManager);
+ if (mCarrierConfigHelper.isCellularDataRequired()) {
+ VvmLog.d(TAG, "Transport type: CELLULAR");
+ builder
+ .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+ .setNetworkSpecifier(telephonyManager.getNetworkSpecifier());
+ } else {
+ VvmLog.d(TAG, "Transport type: ANY");
+ }
+ return builder.build();
+ }
+
+ public NetworkRequest getNetworkRequest() {
+ return mNetworkRequest;
+ }
+
+ @Override
+ @CallSuper
+ public void onLost(Network network) {
+ VvmLog.d(TAG, "onLost");
+ mResultReceived = true;
+ onFailed(NETWORK_REQUEST_FAILED_LOST);
+ }
+
+ @Override
+ @CallSuper
+ public void onAvailable(Network network) {
+ super.onAvailable(network);
+ mResultReceived = true;
+ }
+
+ @CallSuper
+ public void onUnavailable() {
+ // TODO: b/32637799 this is hidden, do we really need this?
+ mResultReceived = true;
+ onFailed(NETWORK_REQUEST_FAILED_TIMEOUT);
+ }
+
+ public void requestNetwork() {
+ if (mRequestSent == true) {
+ VvmLog.e(TAG, "requestNetwork() called twice");
+ return;
+ }
+ mRequestSent = true;
+ getConnectivityManager().requestNetwork(getNetworkRequest(), this);
+ /**
+ * Somehow requestNetwork() with timeout doesn't work, and it's a hidden method. Implement our
+ * own timeout mechanism instead.
+ */
+ Handler handler = new Handler(Looper.getMainLooper());
+ handler.postDelayed(
+ new Runnable() {
+ @Override
+ public void run() {
+ if (mResultReceived == false) {
+ onFailed(NETWORK_REQUEST_FAILED_TIMEOUT);
+ }
+ }
+ },
+ NETWORK_REQUEST_TIMEOUT_MILLIS);
+ }
+
+ public void releaseNetwork() {
+ VvmLog.d(TAG, "releaseNetwork");
+ getConnectivityManager().unregisterNetworkCallback(this);
+ }
+
+ public ConnectivityManager getConnectivityManager() {
+ if (mConnectivityManager == null) {
+ mConnectivityManager =
+ (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+ }
+ return mConnectivityManager;
+ }
+
+ @CallSuper
+ public void onFailed(String reason) {
+ VvmLog.d(TAG, "onFailed: " + reason);
+ if (mCarrierConfigHelper.isCellularDataRequired()) {
+ mCarrierConfigHelper.handleEvent(mStatus, OmtpEvents.DATA_NO_CONNECTION_CELLULAR_REQUIRED);
+ } else {
+ mCarrierConfigHelper.handleEvent(mStatus, OmtpEvents.DATA_NO_CONNECTION);
+ }
+ releaseNetwork();
+ }
+}
diff --git a/java/com/android/voicemail/impl/utils/IndentingPrintWriter.java b/java/com/android/voicemail/impl/utils/IndentingPrintWriter.java
new file mode 100644
index 000000000..bbc1d6601
--- /dev/null
+++ b/java/com/android/voicemail/impl/utils/IndentingPrintWriter.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.utils;
+
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.util.Arrays;
+
+/**
+ * Lightweight wrapper around {@link PrintWriter} that automatically indents newlines based on
+ * internal state. It also automatically wraps long lines based on given line length.
+ *
+ * <p>Delays writing indent until first actual write on a newline, enabling indent modification
+ * after newline.
+ */
+public class IndentingPrintWriter extends PrintWriter {
+
+ private final String mSingleIndent;
+ private final int mWrapLength;
+
+ /** Mutable version of current indent */
+ private StringBuilder mIndentBuilder = new StringBuilder();
+ /** Cache of current {@link #mIndentBuilder} value */
+ private char[] mCurrentIndent;
+ /** Length of current line being built, excluding any indent */
+ private int mCurrentLength;
+
+ /**
+ * Flag indicating if we're currently sitting on an empty line, and that next write should be
+ * prefixed with the current indent.
+ */
+ private boolean mEmptyLine = true;
+
+ private char[] mSingleChar = new char[1];
+
+ public IndentingPrintWriter(Writer writer, String singleIndent) {
+ this(writer, singleIndent, -1);
+ }
+
+ public IndentingPrintWriter(Writer writer, String singleIndent, int wrapLength) {
+ super(writer);
+ mSingleIndent = singleIndent;
+ mWrapLength = wrapLength;
+ }
+
+ public void increaseIndent() {
+ mIndentBuilder.append(mSingleIndent);
+ mCurrentIndent = null;
+ }
+
+ public void decreaseIndent() {
+ mIndentBuilder.delete(0, mSingleIndent.length());
+ mCurrentIndent = null;
+ }
+
+ public void printPair(String key, Object value) {
+ print(key + "=" + String.valueOf(value) + " ");
+ }
+
+ public void printPair(String key, Object[] value) {
+ print(key + "=" + Arrays.toString(value) + " ");
+ }
+
+ public void printHexPair(String key, int value) {
+ print(key + "=0x" + Integer.toHexString(value) + " ");
+ }
+
+ @Override
+ public void println() {
+ write('\n');
+ }
+
+ @Override
+ public void write(int c) {
+ mSingleChar[0] = (char) c;
+ write(mSingleChar, 0, 1);
+ }
+
+ @Override
+ public void write(String s, int off, int len) {
+ final char[] buf = new char[len];
+ s.getChars(off, len - off, buf, 0);
+ write(buf, 0, len);
+ }
+
+ @Override
+ public void write(char[] buf, int offset, int count) {
+ final int indentLength = mIndentBuilder.length();
+ final int bufferEnd = offset + count;
+ int lineStart = offset;
+ int lineEnd = offset;
+
+ // March through incoming buffer looking for newlines
+ while (lineEnd < bufferEnd) {
+ char ch = buf[lineEnd++];
+ mCurrentLength++;
+ if (ch == '\n') {
+ maybeWriteIndent();
+ super.write(buf, lineStart, lineEnd - lineStart);
+ lineStart = lineEnd;
+ mEmptyLine = true;
+ mCurrentLength = 0;
+ }
+
+ // Wrap if we've pushed beyond line length
+ if (mWrapLength > 0 && mCurrentLength >= mWrapLength - indentLength) {
+ if (!mEmptyLine) {
+ // Give ourselves a fresh line to work with
+ super.write('\n');
+ mEmptyLine = true;
+ mCurrentLength = lineEnd - lineStart;
+ } else {
+ // We need more than a dedicated line, slice it hard
+ maybeWriteIndent();
+ super.write(buf, lineStart, lineEnd - lineStart);
+ super.write('\n');
+ mEmptyLine = true;
+ lineStart = lineEnd;
+ mCurrentLength = 0;
+ }
+ }
+ }
+
+ if (lineStart != lineEnd) {
+ maybeWriteIndent();
+ super.write(buf, lineStart, lineEnd - lineStart);
+ }
+ }
+
+ private void maybeWriteIndent() {
+ if (mEmptyLine) {
+ mEmptyLine = false;
+ if (mIndentBuilder.length() != 0) {
+ if (mCurrentIndent == null) {
+ mCurrentIndent = mIndentBuilder.toString().toCharArray();
+ }
+ super.write(mCurrentIndent, 0, mCurrentIndent.length);
+ }
+ }
+ }
+}
diff --git a/java/com/android/voicemail/impl/utils/VoicemailDatabaseUtil.java b/java/com/android/voicemail/impl/utils/VoicemailDatabaseUtil.java
new file mode 100644
index 000000000..711d6a8a4
--- /dev/null
+++ b/java/com/android/voicemail/impl/utils/VoicemailDatabaseUtil.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.utils;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.VoicemailContract.Voicemails;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.Voicemail;
+import java.util.List;
+
+public class VoicemailDatabaseUtil {
+
+ /**
+ * Inserts a new voicemail into the voicemail content provider.
+ *
+ * @param context The context of the app doing the inserting
+ * @param voicemail Data to be inserted
+ * @return {@link Uri} of the newly inserted {@link Voicemail}
+ * @hide
+ */
+ public static Uri insert(Context context, Voicemail voicemail) {
+ ContentResolver contentResolver = context.getContentResolver();
+ ContentValues contentValues = getContentValues(voicemail);
+ return contentResolver.insert(
+ Voicemails.buildSourceUri(context.getPackageName()), contentValues);
+ }
+
+ /**
+ * Inserts a list of voicemails into the voicemail content provider.
+ *
+ * @param context The context of the app doing the inserting
+ * @param voicemails Data to be inserted
+ * @return the number of voicemails inserted
+ * @hide
+ */
+ public static int insert(Context context, List<Voicemail> voicemails) {
+ for (Voicemail voicemail : voicemails) {
+ insert(context, voicemail);
+ }
+ return voicemails.size();
+ }
+
+ /** Maps structured {@link Voicemail} to {@link ContentValues} in content provider. */
+ private static ContentValues getContentValues(Voicemail voicemail) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(Voicemails.DATE, String.valueOf(voicemail.getTimestampMillis()));
+ contentValues.put(Voicemails.NUMBER, voicemail.getNumber());
+ contentValues.put(Voicemails.DURATION, String.valueOf(voicemail.getDuration()));
+ contentValues.put(Voicemails.SOURCE_PACKAGE, voicemail.getSourcePackage());
+ contentValues.put(Voicemails.SOURCE_DATA, voicemail.getSourceData());
+ contentValues.put(Voicemails.IS_READ, voicemail.isRead() ? 1 : 0);
+ contentValues.put(Voicemails.IS_OMTP_VOICEMAIL, 1);
+
+ PhoneAccountHandle phoneAccount = voicemail.getPhoneAccount();
+ if (phoneAccount != null) {
+ contentValues.put(
+ Voicemails.PHONE_ACCOUNT_COMPONENT_NAME,
+ phoneAccount.getComponentName().flattenToString());
+ contentValues.put(Voicemails.PHONE_ACCOUNT_ID, phoneAccount.getId());
+ }
+
+ if (voicemail.getTranscription() != null) {
+ contentValues.put(Voicemails.TRANSCRIPTION, voicemail.getTranscription());
+ }
+
+ return contentValues;
+ }
+}
diff --git a/java/com/android/voicemail/impl/utils/VvmDumpHandler.java b/java/com/android/voicemail/impl/utils/VvmDumpHandler.java
new file mode 100644
index 000000000..5290f2cbe
--- /dev/null
+++ b/java/com/android/voicemail/impl/utils/VvmDumpHandler.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.utils;
+
+import android.content.Context;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.VvmLog;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+public class VvmDumpHandler {
+
+ public static void dump(Context context, FileDescriptor fd, PrintWriter writer, String[] args) {
+ IndentingPrintWriter indentedWriter = new IndentingPrintWriter(writer, " ");
+ indentedWriter.println("******* OmtpVvm *******");
+ indentedWriter.println("======= Configs =======");
+ indentedWriter.increaseIndent();
+ for (PhoneAccountHandle handle :
+ context.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts()) {
+ OmtpVvmCarrierConfigHelper config = new OmtpVvmCarrierConfigHelper(context, handle);
+ indentedWriter.println(config.toString());
+ }
+ indentedWriter.decreaseIndent();
+ indentedWriter.println("======== Logs =========");
+ VvmLog.dump(fd, indentedWriter, args);
+ }
+}
diff --git a/java/com/android/voicemail/impl/utils/XmlUtils.java b/java/com/android/voicemail/impl/utils/XmlUtils.java
new file mode 100644
index 000000000..f5703f30f
--- /dev/null
+++ b/java/com/android/voicemail/impl/utils/XmlUtils.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl.utils;
+
+import android.util.ArrayMap;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+public class XmlUtils {
+
+ public static final ArrayMap<String, ?> readThisArrayMapXml(
+ XmlPullParser parser, String endTag, String[] name, ReadMapCallback callback)
+ throws XmlPullParserException, java.io.IOException {
+ ArrayMap<String, Object> map = new ArrayMap<>();
+
+ int eventType = parser.getEventType();
+ do {
+ if (eventType == XmlPullParser.START_TAG) {
+ Object val = readThisValueXml(parser, name, callback, true);
+ map.put(name[0], val);
+ } else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals(endTag)) {
+ return map;
+ }
+ throw new XmlPullParserException("Expected " + endTag + " end tag at: " + parser.getName());
+ }
+ eventType = parser.next();
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ throw new XmlPullParserException("Document ended before " + endTag + " end tag");
+ }
+
+ /**
+ * Read an ArrayList object from an XmlPullParser. The XML data could previously have been
+ * generated by writeListXml(). The XmlPullParser must be positioned <em>after</em> the tag that
+ * begins the list.
+ *
+ * @param parser The XmlPullParser from which to read the list data.
+ * @param endTag Name of the tag that will end the list, usually "list".
+ * @param name An array of one string, used to return the name attribute of the list's tag.
+ * @return HashMap The newly generated list.
+ */
+ public static final ArrayList readThisListXml(
+ XmlPullParser parser,
+ String endTag,
+ String[] name,
+ ReadMapCallback callback,
+ boolean arrayMap)
+ throws XmlPullParserException, java.io.IOException {
+ ArrayList list = new ArrayList();
+
+ int eventType = parser.getEventType();
+ do {
+ if (eventType == XmlPullParser.START_TAG) {
+ Object val = readThisValueXml(parser, name, callback, arrayMap);
+ list.add(val);
+ } else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals(endTag)) {
+ return list;
+ }
+ throw new XmlPullParserException("Expected " + endTag + " end tag at: " + parser.getName());
+ }
+ eventType = parser.next();
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ throw new XmlPullParserException("Document ended before " + endTag + " end tag");
+ }
+
+ /**
+ * Read a String[] object from an XmlPullParser. The XML data could previously have been generated
+ * by writeStringArrayXml(). The XmlPullParser must be positioned <em>after</em> the tag that
+ * begins the list.
+ *
+ * @param parser The XmlPullParser from which to read the list data.
+ * @param endTag Name of the tag that will end the list, usually "string-array".
+ * @param name An array of one string, used to return the name attribute of the list's tag.
+ * @return Returns a newly generated String[].
+ */
+ public static String[] readThisStringArrayXml(XmlPullParser parser, String endTag, String[] name)
+ throws XmlPullParserException, java.io.IOException {
+
+ parser.next();
+
+ List<String> array = new ArrayList<>();
+
+ int eventType = parser.getEventType();
+ do {
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("item")) {
+ try {
+ array.add(parser.getAttributeValue(null, "value"));
+ } catch (NullPointerException e) {
+ throw new XmlPullParserException("Need value attribute in item");
+ } catch (NumberFormatException e) {
+ throw new XmlPullParserException("Not a number in value attribute in item");
+ }
+ } else {
+ throw new XmlPullParserException("Expected item tag at: " + parser.getName());
+ }
+ } else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals(endTag)) {
+ return array.toArray(new String[0]);
+ } else if (parser.getName().equals("item")) {
+
+ } else {
+ throw new XmlPullParserException(
+ "Expected " + endTag + " end tag at: " + parser.getName());
+ }
+ }
+ eventType = parser.next();
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ throw new XmlPullParserException("Document ended before " + endTag + " end tag");
+ }
+
+ private static Object readThisValueXml(
+ XmlPullParser parser, String[] name, ReadMapCallback callback, boolean arrayMap)
+ throws XmlPullParserException, java.io.IOException {
+ final String valueName = parser.getAttributeValue(null, "name");
+ final String tagName = parser.getName();
+
+ Object res;
+
+ if (tagName.equals("null")) {
+ res = null;
+ } else if (tagName.equals("string")) {
+ String value = "";
+ int eventType;
+ while ((eventType = parser.next()) != XmlPullParser.END_DOCUMENT) {
+ if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("string")) {
+ name[0] = valueName;
+ return value;
+ }
+ throw new XmlPullParserException("Unexpected end tag in <string>: " + parser.getName());
+ } else if (eventType == XmlPullParser.TEXT) {
+ value += parser.getText();
+ } else if (eventType == XmlPullParser.START_TAG) {
+ throw new XmlPullParserException("Unexpected start tag in <string>: " + parser.getName());
+ }
+ }
+ throw new XmlPullParserException("Unexpected end of document in <string>");
+ } else if ((res = readThisPrimitiveValueXml(parser, tagName)) != null) {
+ // all work already done by readThisPrimitiveValueXml
+ } else if (tagName.equals("string-array")) {
+ res = readThisStringArrayXml(parser, "string-array", name);
+ name[0] = valueName;
+ return res;
+ } else if (tagName.equals("list")) {
+ parser.next();
+ res = readThisListXml(parser, "list", name, callback, arrayMap);
+ name[0] = valueName;
+ return res;
+ } else if (callback != null) {
+ res = callback.readThisUnknownObjectXml(parser, tagName);
+ name[0] = valueName;
+ return res;
+ } else {
+ throw new XmlPullParserException("Unknown tag: " + tagName);
+ }
+
+ // Skip through to end tag.
+ int eventType;
+ while ((eventType = parser.next()) != XmlPullParser.END_DOCUMENT) {
+ if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals(tagName)) {
+ name[0] = valueName;
+ return res;
+ }
+ throw new XmlPullParserException(
+ "Unexpected end tag in <" + tagName + ">: " + parser.getName());
+ } else if (eventType == XmlPullParser.TEXT) {
+ throw new XmlPullParserException(
+ "Unexpected text in <" + tagName + ">: " + parser.getName());
+ } else if (eventType == XmlPullParser.START_TAG) {
+ throw new XmlPullParserException(
+ "Unexpected start tag in <" + tagName + ">: " + parser.getName());
+ }
+ }
+ throw new XmlPullParserException("Unexpected end of document in <" + tagName + ">");
+ }
+
+ private static final Object readThisPrimitiveValueXml(XmlPullParser parser, String tagName)
+ throws XmlPullParserException, java.io.IOException {
+ try {
+ if (tagName.equals("int")) {
+ return Integer.parseInt(parser.getAttributeValue(null, "value"));
+ } else if (tagName.equals("long")) {
+ return Long.valueOf(parser.getAttributeValue(null, "value"));
+ } else if (tagName.equals("float")) {
+ return Float.valueOf(parser.getAttributeValue(null, "value"));
+ } else if (tagName.equals("double")) {
+ return Double.valueOf(parser.getAttributeValue(null, "value"));
+ } else if (tagName.equals("boolean")) {
+ return Boolean.valueOf(parser.getAttributeValue(null, "value"));
+ } else {
+ return null;
+ }
+ } catch (NullPointerException e) {
+ throw new XmlPullParserException("Need value attribute in <" + tagName + ">");
+ } catch (NumberFormatException e) {
+ throw new XmlPullParserException("Not a number in value attribute in <" + tagName + ">");
+ }
+ }
+
+ public interface ReadMapCallback {
+
+ /**
+ * Called from readThisMapXml when a START_TAG is not recognized. The input stream is positioned
+ * within the start tag so that attributes can be read using in.getAttribute.
+ *
+ * @param in the XML input stream
+ * @param tag the START_TAG that was not recognized.
+ * @return the Object parsed from the stream which will be put into the map.
+ * @throws XmlPullParserException if the START_TAG is not recognized.
+ * @throws IOException on XmlPullParser serialization errors.
+ */
+ Object readThisUnknownObjectXml(XmlPullParser in, String tag)
+ throws XmlPullParserException, IOException;
+ }
+}
diff --git a/java/com/android/voicemailomtp/permissions.xml b/java/com/android/voicemail/permissions.xml
index 9326d803a..adb4b6f54 100644
--- a/java/com/android/voicemailomtp/permissions.xml
+++ b/java/com/android/voicemail/permissions.xml
@@ -7,9 +7,9 @@
<!-- Applications using this module should merge these permissions using android_manifest_merge -->
- <uses-permission android:name="com.android.voicemail.permission.ADD_VOICEMAIL" />
- <uses-permission android:name="com.android.voicemail.permission.WRITE_VOICEMAIL" />
- <uses-permission android:name="com.android.voicemail.permission.READ_VOICEMAIL" />
+ <uses-permission android:name="com.android.voicemail.permission.ADD_VOICEMAIL"/>
+ <uses-permission android:name="com.android.voicemail.permission.WRITE_VOICEMAIL"/>
+ <uses-permission android:name="com.android.voicemail.permission.READ_VOICEMAIL"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.SEND_SMS"/>
diff --git a/java/com/android/voicemail/stub/StubVoicemailClient.java b/java/com/android/voicemail/stub/StubVoicemailClient.java
new file mode 100644
index 000000000..9481a0e1a
--- /dev/null
+++ b/java/com/android/voicemail/stub/StubVoicemailClient.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.stub;
+
+import android.content.Context;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.VoicemailClient;
+import java.util.List;
+import javax.inject.Inject;
+
+/**
+ * A no-op version of the voicemail module for build targets that don't support the new OTMP client.
+ */
+public final class StubVoicemailClient implements VoicemailClient {
+ @Inject
+ public StubVoicemailClient() {}
+
+ @Override
+ public void appendOmtpVoicemailSelectionClause(
+ Context context, StringBuilder where, List<String> selectionArgs) {}
+
+ @Override
+ public String getSettingsFragment() {
+ return null;
+ }
+
+ @Override
+ public boolean isVoicemailArchiveEnabled(Context context, PhoneAccountHandle phoneAccountHandle) {
+ return false;
+ }
+
+ @Override
+ public void setVoicemailArchiveEnabled(
+ Context context, PhoneAccountHandle phoneAccountHandle, boolean value) {}
+}
diff --git a/java/com/android/voicemail/stub/StubVoicemailModule.java b/java/com/android/voicemail/stub/StubVoicemailModule.java
new file mode 100644
index 000000000..6c1552c15
--- /dev/null
+++ b/java/com/android/voicemail/stub/StubVoicemailModule.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.stub;
+
+import com.android.voicemail.VoicemailClient;
+import dagger.Binds;
+import dagger.Module;
+import javax.inject.Singleton;
+
+/**
+ * A no-op version of the voicemail module for build targets that don't support the new OTMP client.
+ */
+@Module
+public abstract class StubVoicemailModule {
+
+ @Binds
+ @Singleton
+ public abstract VoicemailClient bindVoicemailClient(StubVoicemailClient voicemailClient);
+}
diff --git a/java/com/android/voicemail/testing/TestVoicemailModule.java b/java/com/android/voicemail/testing/TestVoicemailModule.java
new file mode 100644
index 000000000..8b7b34c62
--- /dev/null
+++ b/java/com/android/voicemail/testing/TestVoicemailModule.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.testing;
+
+import com.android.voicemail.VoicemailClient;
+import dagger.Module;
+import dagger.Provides;
+import javax.inject.Singleton;
+
+/** Used to set a mock voicemail client for unit tests. */
+@Module
+public final class TestVoicemailModule {
+ private static VoicemailClient voicemailClient;
+
+ public static void setVoicemailClient(VoicemailClient voicemailClient) {
+ TestVoicemailModule.voicemailClient = voicemailClient;
+ }
+
+ @Provides
+ @Singleton
+ public static VoicemailClient provideVoicemailClient() {
+ return voicemailClient;
+ }
+}
diff --git a/java/com/android/voicemailomtp/ActivationTask.java b/java/com/android/voicemailomtp/ActivationTask.java
deleted file mode 100644
index 7de81e685..000000000
--- a/java/com/android/voicemailomtp/ActivationTask.java
+++ /dev/null
@@ -1,305 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.content.Intent;
-import android.database.ContentObserver;
-import android.os.Build.VERSION_CODES;
-import android.os.Bundle;
-import android.provider.Settings;
-import android.provider.Settings.Global;
-import android.support.annotation.Nullable;
-import android.support.annotation.WorkerThread;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.ServiceState;
-import android.telephony.TelephonyManager;
-import com.android.voicemailomtp.protocol.VisualVoicemailProtocol;
-import com.android.voicemailomtp.scheduling.BaseTask;
-import com.android.voicemailomtp.scheduling.RetryPolicy;
-import com.android.voicemailomtp.sms.StatusMessage;
-import com.android.voicemailomtp.sms.StatusSmsFetcher;
-import com.android.voicemailomtp.sync.OmtpVvmSourceManager;
-import com.android.voicemailomtp.sync.OmtpVvmSyncService;
-import com.android.voicemailomtp.sync.SyncTask;
-import java.io.IOException;
-import java.util.HashSet;
-import java.util.Set;
-import java.util.concurrent.CancellationException;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeoutException;
-
-/**
- * Task to activate the visual voicemail service. A request to activate VVM will be sent to the
- * carrier, which will respond with a STATUS SMS. The credentials will be updated from the SMS. If
- * the user is not provisioned provisioning will be attempted. Activation happens when the phone
- * boots, the SIM is inserted, signal returned when VVM is not activated yet, and when the carrier
- * spontaneously sent a STATUS SMS.
- */
-@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
-public class ActivationTask extends BaseTask {
-
- private static final String TAG = "VvmActivationTask";
-
- private static final int RETRY_TIMES = 4;
- private static final int RETRY_INTERVAL_MILLIS = 5_000;
-
- private static final String EXTRA_MESSAGE_DATA_BUNDLE = "extra_message_data_bundle";
-
- @Nullable
- private static DeviceProvisionedObserver sDeviceProvisionedObserver;
-
- private final RetryPolicy mRetryPolicy;
-
- private Bundle mMessageData;
-
- public ActivationTask() {
- super(TASK_ACTIVATION);
- mRetryPolicy = new RetryPolicy(RETRY_TIMES, RETRY_INTERVAL_MILLIS);
- addPolicy(mRetryPolicy);
- }
-
- /**
- * Has the user gone through the setup wizard yet.
- */
- private static boolean isDeviceProvisioned(Context context) {
- return Settings.Global.getInt(
- context.getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 0) == 1;
- }
-
- /**
- * @param messageData The optional bundle from {@link android.provider.VoicemailContract#
- * EXTRA_VOICEMAIL_SMS_FIELDS}, if the task is initiated by a status SMS. If null the task will
- * request a status SMS itself.
- */
- public static void start(Context context, PhoneAccountHandle phoneAccountHandle,
- @Nullable Bundle messageData) {
- if (!isDeviceProvisioned(context)) {
- VvmLog.i(TAG, "Activation requested while device is not provisioned, postponing");
- // Activation might need information such as system language to be set, so wait until
- // the setup wizard is finished. The data bundle from the SMS will be re-requested upon
- // activation.
- queueActivationAfterProvisioned(context, phoneAccountHandle);
- return;
- }
-
- Intent intent = BaseTask.createIntent(context, ActivationTask.class, phoneAccountHandle);
- if (messageData != null) {
- intent.putExtra(EXTRA_MESSAGE_DATA_BUNDLE, messageData);
- }
- context.startService(intent);
- }
-
- public void onCreate(Context context, Intent intent, int flags, int startId) {
- super.onCreate(context, intent, flags, startId);
- mMessageData = intent.getParcelableExtra(EXTRA_MESSAGE_DATA_BUNDLE);
- }
-
- @Override
- public Intent createRestartIntent() {
- Intent intent = super.createRestartIntent();
- // mMessageData is discarded, request a fresh STATUS SMS for retries.
- return intent;
- }
-
- @Override
- @WorkerThread
- public void onExecuteInBackgroundThread() {
- Assert.isNotMainThread();
-
- PhoneAccountHandle phoneAccountHandle = getPhoneAccountHandle();
- if (phoneAccountHandle == null) {
- // This should never happen
- VvmLog.e(TAG, "null PhoneAccountHandle");
- return;
- }
-
- OmtpVvmCarrierConfigHelper helper =
- new OmtpVvmCarrierConfigHelper(getContext(), phoneAccountHandle);
- if (!helper.isValid()) {
- VvmLog.i(TAG, "VVM not supported on phoneAccountHandle " + phoneAccountHandle);
- VoicemailStatus.disable(getContext(), phoneAccountHandle);
- return;
- }
-
- // OmtpVvmCarrierConfigHelper can start the activation process; it will pass in a vvm
- // content provider URI which we will use. On some occasions, setting that URI will
- // fail, so we will perform a few attempts to ensure that the vvm content provider has
- // a good chance of being started up.
- if (!VoicemailStatus.edit(getContext(), phoneAccountHandle)
- .setType(helper.getVvmType())
- .apply()) {
- VvmLog.e(TAG, "Failed to configure content provider - " + helper.getVvmType());
- fail();
- }
- VvmLog.i(TAG, "VVM content provider configured - " + helper.getVvmType());
-
- if (!OmtpVvmSourceManager.getInstance(getContext())
- .isVvmSourceRegistered(phoneAccountHandle)) {
- // This account has not been activated before during the lifetime of this boot.
- VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(getContext(),
- phoneAccountHandle);
- if (preferences.getString(OmtpConstants.SERVER_ADDRESS, null) == null) {
- // Only show the "activating" message if activation has not been completed before.
- // Subsequent activations are more of a status check and usually does not
- // concern the user.
- helper.handleEvent(VoicemailStatus.edit(getContext(), phoneAccountHandle),
- OmtpEvents.CONFIG_ACTIVATING);
- } else {
- // The account has been activated on this device before. Pretend it is already
- // activated. If there are any activation error it will overwrite this status.
- helper.handleEvent(VoicemailStatus.edit(getContext(), phoneAccountHandle),
- OmtpEvents.CONFIG_ACTIVATING_SUBSEQUENT);
- }
-
- }
- if (!hasSignal(getContext(), phoneAccountHandle)) {
- VvmLog.i(TAG, "Service lost during activation, aborting");
- // Restore the "NO SIGNAL" state since it will be overwritten by the CONFIG_ACTIVATING
- // event.
- helper.handleEvent(VoicemailStatus.edit(getContext(), phoneAccountHandle),
- OmtpEvents.NOTIFICATION_SERVICE_LOST);
- // Don't retry, a new activation will be started after the signal returned.
- return;
- }
-
- helper.activateSmsFilter();
- VoicemailStatus.Editor status = mRetryPolicy.getVoicemailStatusEditor();
-
- VisualVoicemailProtocol protocol = helper.getProtocol();
-
- Bundle data;
- if (mMessageData != null) {
- // The content of STATUS SMS is provided to launch this task, no need to request it
- // again.
- data = mMessageData;
- } else {
- try (StatusSmsFetcher fetcher = new StatusSmsFetcher(getContext(),
- phoneAccountHandle)) {
- protocol.startActivation(helper, fetcher.getSentIntent());
- // Both the fetcher and OmtpMessageReceiver will be triggered, but
- // OmtpMessageReceiver will just route the SMS back to ActivationTask, which will be
- // rejected because the task is still running.
- data = fetcher.get();
- } catch (TimeoutException e) {
- // The carrier is expected to return an STATUS SMS within STATUS_SMS_TIMEOUT_MILLIS
- // handleEvent() will do the logging.
- helper.handleEvent(status, OmtpEvents.CONFIG_STATUS_SMS_TIME_OUT);
- fail();
- return;
- } catch (CancellationException e) {
- VvmLog.e(TAG, "Unable to send status request SMS");
- fail();
- return;
- } catch (InterruptedException | ExecutionException | IOException e) {
- VvmLog.e(TAG, "can't get future STATUS SMS", e);
- fail();
- return;
- }
- }
-
- StatusMessage message = new StatusMessage(data);
- VvmLog.d(TAG, "STATUS SMS received: st=" + message.getProvisioningStatus()
- + ", rc=" + message.getReturnCode());
-
- if (message.getProvisioningStatus().equals(OmtpConstants.SUBSCRIBER_READY)) {
- VvmLog.d(TAG, "subscriber ready, no activation required");
- updateSource(getContext(), phoneAccountHandle, status, message);
- } else {
- if (helper.supportsProvisioning()) {
- VvmLog.i(TAG, "Subscriber not ready, start provisioning");
- helper.startProvisioning(this, phoneAccountHandle, status, message, data);
-
- } else if (message.getProvisioningStatus().equals(OmtpConstants.SUBSCRIBER_NEW)) {
- VvmLog.i(TAG, "Subscriber new but provisioning is not supported");
- // Ignore the non-ready state and attempt to use the provided info as is.
- // This is probably caused by not completing the new user tutorial.
- updateSource(getContext(), phoneAccountHandle, status, message);
- } else {
- VvmLog.i(TAG, "Subscriber not ready but provisioning is not supported");
- helper.handleEvent(status, OmtpEvents.CONFIG_SERVICE_NOT_AVAILABLE);
- }
- }
- }
-
- public static void updateSource(Context context, PhoneAccountHandle phone,
- VoicemailStatus.Editor status, StatusMessage message) {
- OmtpVvmSourceManager vvmSourceManager =
- OmtpVvmSourceManager.getInstance(context);
-
- if (OmtpConstants.SUCCESS.equals(message.getReturnCode())) {
- OmtpVvmCarrierConfigHelper helper = new OmtpVvmCarrierConfigHelper(context, phone);
- helper.handleEvent(status, OmtpEvents.CONFIG_REQUEST_STATUS_SUCCESS);
-
- // Save the IMAP credentials in preferences so they are persistent and can be retrieved.
- VisualVoicemailPreferences prefs = new VisualVoicemailPreferences(context, phone);
- message.putStatus(prefs.edit()).apply();
-
- // Add the source to indicate that it is active.
- vvmSourceManager.addSource(phone);
-
- SyncTask.start(context, phone, OmtpVvmSyncService.SYNC_FULL_SYNC);
- } else {
- VvmLog.e(TAG, "Visual voicemail not available for subscriber.");
- }
- }
-
- private static boolean hasSignal(Context context, PhoneAccountHandle phoneAccountHandle) {
- TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class)
- .createForPhoneAccountHandle(phoneAccountHandle);
- return telephonyManager.getServiceState().getState() == ServiceState.STATE_IN_SERVICE;
- }
-
- private static void queueActivationAfterProvisioned(Context context,
- PhoneAccountHandle phoneAccountHandle) {
- if (sDeviceProvisionedObserver == null) {
- sDeviceProvisionedObserver = new DeviceProvisionedObserver(context);
- context.getContentResolver()
- .registerContentObserver(Settings.Global.getUriFor(Global.DEVICE_PROVISIONED),
- false, sDeviceProvisionedObserver);
- }
- sDeviceProvisionedObserver.addPhoneAcountHandle(phoneAccountHandle);
- }
-
- private static class DeviceProvisionedObserver extends ContentObserver {
-
- private final Context mContext;
- private final Set<PhoneAccountHandle> mPhoneAccountHandles = new HashSet<>();
-
- private DeviceProvisionedObserver(Context context) {
- super(null);
- mContext = context;
- }
-
- public void addPhoneAcountHandle(PhoneAccountHandle phoneAccountHandle) {
- mPhoneAccountHandles.add(phoneAccountHandle);
- }
-
- @Override
- public void onChange(boolean selfChange) {
- if (isDeviceProvisioned(mContext)) {
- VvmLog.i(TAG, "device provisioned, resuming activation");
- for (PhoneAccountHandle phoneAccountHandle : mPhoneAccountHandles) {
- start(mContext, phoneAccountHandle, null);
- }
- mContext.getContentResolver().unregisterContentObserver(sDeviceProvisionedObserver);
- sDeviceProvisionedObserver = null;
- }
- }
- }
-}
diff --git a/java/com/android/voicemailomtp/AndroidManifest.xml b/java/com/android/voicemailomtp/AndroidManifest.xml
deleted file mode 100644
index 282a923d2..000000000
--- a/java/com/android/voicemailomtp/AndroidManifest.xml
+++ /dev/null
@@ -1,105 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2007 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
- package="com.android.voicemailomtp"
->
-
- <uses-sdk
- android:minSdkVersion="23"
- android:targetSdkVersion="25" />
-
- <application
- android:allowBackup="false"
- android:supportsRtl="true"
- android:usesCleartextTraffic="true"
- android:defaultToDeviceProtectedStorage="true"
- android:directBootAware="true">
-
- <activity android:name="com.android.voicemailomtp.settings.VoicemailSettingsActivity"
- android:label="@string/voicemail_settings_label">
- <intent-filter >
- <!-- DO NOT RENAME. There are existing apps which use this string. -->
- <action android:name="com.android.voicemailomtp.CallFeaturesSetting.ADD_VOICEMAIL" />
- <category android:name="android.intent.category.DEFAULT" />
- </intent-filter>
- <intent-filter>
- <action android:name="android.telephony.action.CONFIGURE_VOICEMAIL" />
- <category android:name="android.intent.category.DEFAULT" />
- </intent-filter>
- </activity>
-
- <receiver android:name="com.android.voicemailomtp.sms.OmtpMessageReceiver"
- android:exported="false"
- androidprv:systemUserOnly="true">
- <intent-filter>
- <action android:name="com.android.vociemailomtp.sms.sms_received"/>
- </intent-filter>
- </receiver>
-
- <receiver
- android:name="com.android.voicemailomtp.fetch.FetchVoicemailReceiver"
- android:exported="true"
- android:permission="com.android.voicemail.permission.READ_VOICEMAIL"
- androidprv:systemUserOnly="true">
- <intent-filter>
- <action android:name="android.intent.action.FETCH_VOICEMAIL" />
- <data
- android:scheme="content"
- android:host="com.android.voicemail"
- android:mimeType="vnd.android.cursor.item/voicemail" />
- </intent-filter>
- </receiver>
- <receiver
- android:name="com.android.voicemailomtp.sync.OmtpVvmSyncReceiver"
- android:exported="true"
- android:permission="com.android.voicemail.permission.READ_VOICEMAIL"
- androidprv:systemUserOnly="true">
- <intent-filter>
- <action android:name="android.provider.action.SYNC_VOICEMAIL"/>
- </intent-filter>
- </receiver>
- <receiver
- android:name="com.android.voicemailomtp.sync.VoicemailProviderChangeReceiver"
- android:exported="true">
- <intent-filter>
- <action android:name="android.intent.action.PROVIDER_CHANGED" />
- <data
- android:scheme="content"
- android:host="com.android.voicemail"
- android:mimeType="vnd.android.cursor.dir/voicemails"/>
- </intent-filter>
- </receiver>
-
- <service
- android:name="com.android.voicemailomtp.scheduling.TaskSchedulerService"
- android:exported="false" />
-
- <service
- android:name="com.android.voicemailomtp.OmtpService"
- android:permission="android.permission.BIND_VISUAL_VOICEMAIL_SERVICE"
- android:exported="true">
- <intent-filter>
- <action android:name="android.telephony.VisualVoicemailService"/>
- </intent-filter>
- </service>
- <activity android:name=".settings.VoicemailChangePinActivity"
- android:exported="false"
- android:windowSoftInputMode="stateVisible|adjustResize">
- </activity>
- </application>
-</manifest>
diff --git a/java/com/android/voicemailomtp/Assert.java b/java/com/android/voicemailomtp/Assert.java
deleted file mode 100644
index 1d295bed1..000000000
--- a/java/com/android/voicemailomtp/Assert.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-
-package com.android.voicemailomtp;
-
-import android.os.Looper;
-
-/**
- * Assertions which will result in program termination.
- */
-public class Assert {
-
- private static Boolean sIsMainThreadForTest;
-
- public static void isTrue(boolean condition) {
- if (!condition) {
- throw new AssertionError("Expected condition to be true");
- }
- }
-
- public static void isMainThread() {
- if (sIsMainThreadForTest != null) {
- isTrue(sIsMainThreadForTest);
- return;
- }
- isTrue(Looper.getMainLooper().equals(Looper.myLooper()));
- }
-
- public static void isNotMainThread() {
- if (sIsMainThreadForTest != null) {
- isTrue(!sIsMainThreadForTest);
- return;
- }
- isTrue(!Looper.getMainLooper().equals(Looper.myLooper()));
- }
-
- public static void fail() {
- throw new AssertionError("Fail");
- }
-
- /**
- * Override the main thread status for tests. Set to null to revert to normal behavior
- */
- @NeededForTesting
- public static void setIsMainThreadForTesting(Boolean isMainThread) {
- sIsMainThreadForTest = isMainThread;
- }
-}
diff --git a/java/com/android/voicemailomtp/DefaultOmtpEventHandler.java b/java/com/android/voicemailomtp/DefaultOmtpEventHandler.java
deleted file mode 100644
index 6a4b5104a..000000000
--- a/java/com/android/voicemailomtp/DefaultOmtpEventHandler.java
+++ /dev/null
@@ -1,202 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp;
-
-import android.content.Context;
-import android.provider.VoicemailContract;
-import android.provider.VoicemailContract.Status;
-
-import com.android.voicemailomtp.VoicemailStatus;
-import com.android.voicemailomtp.OmtpEvents.Type;
-
-public class DefaultOmtpEventHandler {
-
- private static final String TAG = "DefErrorCodeHandler";
-
- public static void handleEvent(Context context, OmtpVvmCarrierConfigHelper config,
- VoicemailStatus.Editor status, OmtpEvents event) {
- switch (event.getType()) {
- case Type.CONFIGURATION:
- handleConfigurationEvent(context, status, event);
- break;
- case Type.DATA_CHANNEL:
- handleDataChannelEvent(context, status, event);
- break;
- case Type.NOTIFICATION_CHANNEL:
- handleNotificationChannelEvent(context, config, status, event);
- break;
- case Type.OTHER:
- handleOtherEvent(context, status, event);
- break;
- default:
- VvmLog.wtf(TAG, "invalid event type " + event.getType() + " for " + event);
- }
- }
-
- private static void handleConfigurationEvent(Context context, VoicemailStatus.Editor status,
- OmtpEvents event) {
- switch (event) {
- case CONFIG_DEFAULT_PIN_REPLACED:
- case CONFIG_REQUEST_STATUS_SUCCESS:
- case CONFIG_PIN_SET:
- status
- .setConfigurationState(VoicemailContract.Status.CONFIGURATION_STATE_OK)
- .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_OK)
- .apply();
- break;
- case CONFIG_ACTIVATING:
- // Wipe all errors from the last activation. All errors shown should be new errors
- // for this activation.
- status
- .setConfigurationState(Status.CONFIGURATION_STATE_CONFIGURING)
- .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_OK)
- .setDataChannelState(Status.DATA_CHANNEL_STATE_OK).apply();
- break;
- case CONFIG_ACTIVATING_SUBSEQUENT:
- status
- .setConfigurationState(Status.CONFIGURATION_STATE_OK)
- .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_OK)
- .setDataChannelState(Status.DATA_CHANNEL_STATE_OK).apply();
- break;
- case CONFIG_SERVICE_NOT_AVAILABLE:
- status
- .setConfigurationState(Status.CONFIGURATION_STATE_FAILED)
- .apply();
- break;
- case CONFIG_STATUS_SMS_TIME_OUT:
- status
- .setConfigurationState(Status.CONFIGURATION_STATE_FAILED)
- .apply();
- break;
- default:
- VvmLog.wtf(TAG, "invalid configuration event " + event);
- }
- }
-
- private static void handleDataChannelEvent(Context context, VoicemailStatus.Editor status,
- OmtpEvents event) {
- switch (event) {
- case DATA_IMAP_OPERATION_STARTED:
- case DATA_IMAP_OPERATION_COMPLETED:
- status
- .setDataChannelState(Status.DATA_CHANNEL_STATE_OK)
- .apply();
- break;
-
- case DATA_NO_CONNECTION:
- status
- .setDataChannelState(Status.DATA_CHANNEL_STATE_NO_CONNECTION)
- .apply();
- break;
-
- case DATA_NO_CONNECTION_CELLULAR_REQUIRED:
- status
- .setDataChannelState(
- Status.DATA_CHANNEL_STATE_NO_CONNECTION_CELLULAR_REQUIRED)
- .apply();
- break;
- case DATA_INVALID_PORT:
- status
- .setDataChannelState(
- VoicemailContract.Status.DATA_CHANNEL_STATE_BAD_CONFIGURATION)
- .apply();
- break;
- case DATA_CANNOT_RESOLVE_HOST_ON_NETWORK:
- status
- .setDataChannelState(
- VoicemailContract.Status.DATA_CHANNEL_STATE_SERVER_CONNECTION_ERROR)
- .apply();
- break;
- case DATA_SSL_INVALID_HOST_NAME:
- case DATA_CANNOT_ESTABLISH_SSL_SESSION:
- case DATA_IOE_ON_OPEN:
- case DATA_GENERIC_IMAP_IOE:
- status
- .setDataChannelState(
- VoicemailContract.Status.DATA_CHANNEL_STATE_COMMUNICATION_ERROR)
- .apply();
- break;
- case DATA_BAD_IMAP_CREDENTIAL:
- case DATA_AUTH_UNKNOWN_USER:
- case DATA_AUTH_UNKNOWN_DEVICE:
- case DATA_AUTH_INVALID_PASSWORD:
- case DATA_AUTH_MAILBOX_NOT_INITIALIZED:
- case DATA_AUTH_SERVICE_NOT_PROVISIONED:
- case DATA_AUTH_SERVICE_NOT_ACTIVATED:
- case DATA_AUTH_USER_IS_BLOCKED:
- status
- .setDataChannelState(
- VoicemailContract.Status.DATA_CHANNEL_STATE_BAD_CONFIGURATION)
- .apply();
- break;
-
- case DATA_REJECTED_SERVER_RESPONSE:
- case DATA_INVALID_INITIAL_SERVER_RESPONSE:
- case DATA_MAILBOX_OPEN_FAILED:
- case DATA_SSL_EXCEPTION:
- case DATA_ALL_SOCKET_CONNECTION_FAILED:
- status
- .setDataChannelState(
- VoicemailContract.Status.DATA_CHANNEL_STATE_SERVER_ERROR)
- .apply();
- break;
-
- default:
- VvmLog.wtf(TAG, "invalid data channel event " + event);
- }
- }
-
- private static void handleNotificationChannelEvent(Context context,
- OmtpVvmCarrierConfigHelper config, VoicemailStatus.Editor status, OmtpEvents event) {
- switch (event) {
- case NOTIFICATION_IN_SERVICE:
- status
- .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_OK)
- // Clear the error state. A sync should follow signal return so any error
- // will be reposted.
- .setDataChannelState(Status.DATA_CHANNEL_STATE_OK)
- .apply();
- break;
- case NOTIFICATION_SERVICE_LOST:
- status.setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION);
- if (config.isCellularDataRequired()) {
- status.setDataChannelState(
- Status.DATA_CHANNEL_STATE_NO_CONNECTION_CELLULAR_REQUIRED);
- }
- status.apply();
- break;
- default:
- VvmLog.wtf(TAG, "invalid notification channel event " + event);
- }
- }
-
- private static void handleOtherEvent(Context context, VoicemailStatus.Editor status,
- OmtpEvents event) {
- switch (event) {
- case OTHER_SOURCE_REMOVED:
- status
- .setConfigurationState(Status.CONFIGURATION_STATE_NOT_CONFIGURED)
- .setNotificationChannelState(
- Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION)
- .setDataChannelState(Status.DATA_CHANNEL_STATE_NO_CONNECTION)
- .apply();
- break;
- default:
- VvmLog.wtf(TAG, "invalid other event " + event);
- }
- }
-}
diff --git a/java/com/android/voicemailomtp/OmtpConstants.java b/java/com/android/voicemailomtp/OmtpConstants.java
deleted file mode 100644
index da2b998b6..000000000
--- a/java/com/android/voicemailomtp/OmtpConstants.java
+++ /dev/null
@@ -1,248 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp;
-
-import android.support.annotation.IntDef;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * Wrapper class to hold relevant OMTP constants as defined in the OMTP spec. <p> In essence this is
- * a programmatic representation of the relevant portions of OMTP spec.
- */
-public class OmtpConstants {
- public static final String SMS_FIELD_SEPARATOR = ";";
- public static final String SMS_KEY_VALUE_SEPARATOR = "=";
- public static final String SMS_PREFIX_SEPARATOR = ":";
-
- public static final String SYNC_SMS_PREFIX = "SYNC";
- public static final String STATUS_SMS_PREFIX = "STATUS";
-
- // This is the format designated by the OMTP spec.
- public static final String DATE_TIME_FORMAT = "dd/MM/yyyy HH:mm Z";
-
- /** OMTP protocol versions. */
- public static final String PROTOCOL_VERSION1_1 = "11";
- public static final String PROTOCOL_VERSION1_2 = "12";
- public static final String PROTOCOL_VERSION1_3 = "13";
-
- ///////////////////////// Client/Mobile originated SMS //////////////////////
-
- /** Mobile Originated requests */
- public static final String ACTIVATE_REQUEST = "Activate";
- public static final String DEACTIVATE_REQUEST = "Deactivate";
- public static final String STATUS_REQUEST = "Status";
-
- /** fields that can be present in a Mobile Originated OMTP SMS */
- public static final String CLIENT_TYPE = "ct";
- public static final String APPLICATION_PORT = "pt";
- public static final String PROTOCOL_VERSION = "pv";
-
-
- //////////////////////////////// Sync SMS fields ////////////////////////////
-
- /**
- * Sync SMS fields.
- * <p>
- * Each string constant is the field's key in the SMS body which is used by the parser to
- * identify the field's value, if present, in the SMS body.
- */
-
- /**
- * The event that triggered this SYNC SMS.
- * See {@link OmtpConstants#SYNC_TRIGGER_EVENT_VALUES}
- */
- public static final String SYNC_TRIGGER_EVENT = "ev";
- public static final String MESSAGE_UID = "id";
- public static final String MESSAGE_LENGTH = "l";
- public static final String NUM_MESSAGE_COUNT = "c";
- /** See {@link OmtpConstants#CONTENT_TYPE_VALUES} */
- public static final String CONTENT_TYPE = "t";
- public static final String SENDER = "s";
- public static final String TIME = "dt";
-
- /**
- * SYNC message trigger events.
- * <p>
- * These are the possible values of {@link OmtpConstants#SYNC_TRIGGER_EVENT}.
- */
- public static final String NEW_MESSAGE = "NM";
- public static final String MAILBOX_UPDATE = "MBU";
- public static final String GREETINGS_UPDATE = "GU";
-
- public static final String[] SYNC_TRIGGER_EVENT_VALUES = {
- NEW_MESSAGE,
- MAILBOX_UPDATE,
- GREETINGS_UPDATE
- };
-
- /**
- * Content types supported by OMTP VVM.
- * <p>
- * These are the possible values of {@link OmtpConstants#CONTENT_TYPE}.
- */
- public static final String VOICE = "v";
- public static final String VIDEO = "o";
- public static final String FAX = "f";
- /** Voice message deposited by an external application */
- public static final String INFOTAINMENT = "i";
- /** Empty Call Capture - i.e. voicemail with no voice message. */
- public static final String ECC = "e";
-
- public static final String[] CONTENT_TYPE_VALUES = {VOICE, VIDEO, FAX, INFOTAINMENT, ECC};
-
- ////////////////////////////// Status SMS fields ////////////////////////////
-
- /**
- * Status SMS fields.
- * <p>
- * Each string constant is the field's key in the SMS body which is used by the parser to
- * identify the field's value, if present, in the SMS body.
- */
- /** See {@link OmtpConstants#PROVISIONING_STATUS_VALUES} */
- public static final String PROVISIONING_STATUS = "st";
- /** See {@link OmtpConstants#RETURN_CODE_VALUES} */
- public static final String RETURN_CODE = "rc";
- /** URL to send users to for activation VVM */
- public static final String SUBSCRIPTION_URL = "rs";
- /** IMAP4/SMTP server IP address or fully qualified domain name */
- public static final String SERVER_ADDRESS = "srv";
- /** Phone number to access voicemails through Telephony User Interface */
- public static final String TUI_ACCESS_NUMBER = "tui";
- public static final String TUI_PASSWORD_LENGTH = "pw_len";
- /** Number to send client origination SMS */
- public static final String CLIENT_SMS_DESTINATION_NUMBER = "dn";
- public static final String IMAP_PORT = "ipt";
- public static final String IMAP_USER_NAME = "u";
- public static final String IMAP_PASSWORD = "pw";
- public static final String SMTP_PORT = "spt";
- public static final String SMTP_USER_NAME = "smtp_u";
- public static final String SMTP_PASSWORD = "smtp_pw";
-
- /**
- * User provisioning status values.
- * <p>
- * Referred by {@link OmtpConstants#PROVISIONING_STATUS}.
- */
- public static final String SUBSCRIBER_NEW = "N";
- public static final String SUBSCRIBER_READY = "R";
- public static final String SUBSCRIBER_PROVISIONED = "P";
- public static final String SUBSCRIBER_UNKNOWN = "U";
- public static final String SUBSCRIBER_BLOCKED = "B";
-
- public static final String[] PROVISIONING_STATUS_VALUES = {
- SUBSCRIBER_NEW,
- SUBSCRIBER_READY,
- SUBSCRIBER_PROVISIONED,
- SUBSCRIBER_UNKNOWN,
- SUBSCRIBER_BLOCKED
- };
-
- /**
- * The return code included in a status message.
- * <p>
- * These are the possible values of {@link OmtpConstants#RETURN_CODE}.
- */
- public static final String SUCCESS = "0";
- public static final String SYSTEM_ERROR = "1";
- public static final String SUBSCRIBER_ERROR = "2";
- public static final String MAILBOX_UNKNOWN = "3";
- public static final String VVM_NOT_ACTIVATED = "4";
- public static final String VVM_NOT_PROVISIONED = "5";
- public static final String VVM_CLIENT_UKNOWN = "6";
- public static final String VVM_MAILBOX_NOT_INITIALIZED = "7";
-
- public static final String[] RETURN_CODE_VALUES = {
- SUCCESS,
- SYSTEM_ERROR,
- SUBSCRIBER_ERROR,
- MAILBOX_UNKNOWN,
- VVM_NOT_ACTIVATED,
- VVM_NOT_PROVISIONED,
- VVM_CLIENT_UKNOWN,
- VVM_MAILBOX_NOT_INITIALIZED,
- };
-
- /**
- * A map of all the field keys to the possible values they can have.
- */
- public static final Map<String, String[]> possibleValuesMap = new HashMap<String, String[]>() {{
- put(SYNC_TRIGGER_EVENT, SYNC_TRIGGER_EVENT_VALUES);
- put(CONTENT_TYPE, CONTENT_TYPE_VALUES);
- put(PROVISIONING_STATUS, PROVISIONING_STATUS_VALUES);
- put(RETURN_CODE, RETURN_CODE_VALUES);
- }};
-
- /**
- * IMAP command extensions
- */
-
- /**
- * OMTP spec v1.3 2.3.1 Change password request syntax
- *
- * This changes the PIN to access the Telephone User Interface, the traditional voicemail
- * system.
- */
- public static final String IMAP_CHANGE_TUI_PWD_FORMAT = "XCHANGE_TUI_PWD PWD=%1$s OLD_PWD=%2$s";
-
- /**
- * OMTP spec v1.3 2.4.1 Change languate request syntax
- *
- * This changes the language in the Telephone User Interface.
- */
- public static final String IMAP_CHANGE_VM_LANG_FORMAT = "XCHANGE_VM_LANG LANG=%1$s";
-
- /**
- * OMTP spec v1.3 2.5.1 Close NUT Request syntax
- *
- * This disables the new user tutorial, the message played to new users calling in the Telephone
- * User Interface.
- */
- public static final String IMAP_CLOSE_NUT = "XCLOSE_NUT";
-
- /**
- * Possible NO responses for CHANGE_TUI_PWD
- */
-
- public static final String RESPONSE_CHANGE_PIN_TOO_SHORT = "password too short";
- public static final String RESPONSE_CHANGE_PIN_TOO_LONG = "password too long";
- public static final String RESPONSE_CHANGE_PIN_TOO_WEAK = "password too weak";
- public static final String RESPONSE_CHANGE_PIN_MISMATCH = "old password mismatch";
- public static final String RESPONSE_CHANGE_PIN_INVALID_CHARACTER =
- "password contains invalid characters";
-
- @Retention(RetentionPolicy.SOURCE)
- @IntDef(value = {CHANGE_PIN_SUCCESS, CHANGE_PIN_TOO_SHORT, CHANGE_PIN_TOO_LONG,
- CHANGE_PIN_TOO_WEAK, CHANGE_PIN_MISMATCH, CHANGE_PIN_INVALID_CHARACTER,
- CHANGE_PIN_SYSTEM_ERROR})
-
- public @interface ChangePinResult {
-
- }
-
- public static final int CHANGE_PIN_SUCCESS = 0;
- public static final int CHANGE_PIN_TOO_SHORT = 1;
- public static final int CHANGE_PIN_TOO_LONG = 2;
- public static final int CHANGE_PIN_TOO_WEAK = 3;
- public static final int CHANGE_PIN_MISMATCH = 4;
- public static final int CHANGE_PIN_INVALID_CHARACTER = 5;
- public static final int CHANGE_PIN_SYSTEM_ERROR = 6;
-
- /** Indicates the client is Google visual voicemail version 1.0. */
- public static final String CLIENT_TYPE_GOOGLE_10 = "google.vvm.10";
-}
diff --git a/java/com/android/voicemailomtp/OmtpEvents.java b/java/com/android/voicemailomtp/OmtpEvents.java
deleted file mode 100644
index d5c2a8b03..000000000
--- a/java/com/android/voicemailomtp/OmtpEvents.java
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp;
-
-import android.support.annotation.IntDef;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/**
- * Events internal to the OMTP client. These should be translated into {@link
- * android.provider.VoicemailContract.Status} error codes before writing into the voicemail status
- * table.
- */
-public enum OmtpEvents {
-
- // Configuration State
- CONFIG_REQUEST_STATUS_SUCCESS(Type.CONFIGURATION, true),
-
- CONFIG_PIN_SET(Type.CONFIGURATION, true),
- // The voicemail PIN is replaced with a generated PIN, user should change it.
- CONFIG_DEFAULT_PIN_REPLACED(Type.CONFIGURATION, true),
- CONFIG_ACTIVATING(Type.CONFIGURATION, true),
- // There are already activation records, this is only a book-keeping activation.
- CONFIG_ACTIVATING_SUBSEQUENT(Type.CONFIGURATION, true),
- CONFIG_STATUS_SMS_TIME_OUT(Type.CONFIGURATION),
- CONFIG_SERVICE_NOT_AVAILABLE(Type.CONFIGURATION),
-
- // Data channel State
-
- // A new sync has started, old errors in data channel should be cleared.
- DATA_IMAP_OPERATION_STARTED(Type.DATA_CHANNEL, true),
- // Successfully downloaded/uploaded data from the server, which means the data channel is clear.
- DATA_IMAP_OPERATION_COMPLETED(Type.DATA_CHANNEL, true),
- // The port provided in the STATUS SMS is invalid.
- DATA_INVALID_PORT(Type.DATA_CHANNEL),
- // No connection to the internet, and the carrier requires cellular data
- DATA_NO_CONNECTION_CELLULAR_REQUIRED(Type.DATA_CHANNEL),
- // No connection to the internet.
- DATA_NO_CONNECTION(Type.DATA_CHANNEL),
- // Address lookup for the server hostname failed. DNS error?
- DATA_CANNOT_RESOLVE_HOST_ON_NETWORK(Type.DATA_CHANNEL),
- // All destination address that resolves to the server hostname are rejected or timed out
- DATA_ALL_SOCKET_CONNECTION_FAILED(Type.DATA_CHANNEL),
- // Failed to establish SSL with the server, either with a direct SSL connection or by
- // STARTTLS command
- DATA_CANNOT_ESTABLISH_SSL_SESSION(Type.DATA_CHANNEL),
- // Identity of the server cannot be verified.
- DATA_SSL_INVALID_HOST_NAME(Type.DATA_CHANNEL),
- // The server rejected our username/password
- DATA_BAD_IMAP_CREDENTIAL(Type.DATA_CHANNEL),
-
- DATA_AUTH_UNKNOWN_USER(Type.DATA_CHANNEL),
- DATA_AUTH_UNKNOWN_DEVICE(Type.DATA_CHANNEL),
- DATA_AUTH_INVALID_PASSWORD(Type.DATA_CHANNEL),
- DATA_AUTH_MAILBOX_NOT_INITIALIZED(Type.DATA_CHANNEL),
- DATA_AUTH_SERVICE_NOT_PROVISIONED(Type.DATA_CHANNEL),
- DATA_AUTH_SERVICE_NOT_ACTIVATED(Type.DATA_CHANNEL),
- DATA_AUTH_USER_IS_BLOCKED(Type.DATA_CHANNEL),
-
- // A command to the server didn't result with an "OK" or continuation request
- DATA_REJECTED_SERVER_RESPONSE(Type.DATA_CHANNEL),
- // The server did not greet us with a "OK", possibly not a IMAP server.
- DATA_INVALID_INITIAL_SERVER_RESPONSE(Type.DATA_CHANNEL),
- // An IOException occurred while trying to open an ImapConnection
- // TODO: reduce scope
- DATA_IOE_ON_OPEN(Type.DATA_CHANNEL),
- // The SELECT command on a mailbox is rejected
- DATA_MAILBOX_OPEN_FAILED(Type.DATA_CHANNEL),
- // An IOException has occurred
- // TODO: reduce scope
- DATA_GENERIC_IMAP_IOE(Type.DATA_CHANNEL),
- // An SslException has occurred while opening an ImapConnection
- // TODO: reduce scope
- DATA_SSL_EXCEPTION(Type.DATA_CHANNEL),
-
- // Notification Channel
-
- // Cell signal restored, can received VVM SMSs
- NOTIFICATION_IN_SERVICE(Type.NOTIFICATION_CHANNEL, true),
- // Cell signal lost, cannot received VVM SMSs
- NOTIFICATION_SERVICE_LOST(Type.NOTIFICATION_CHANNEL, false),
-
-
- // Other
- OTHER_SOURCE_REMOVED(Type.OTHER, false),
-
- // VVM3
- VVM3_NEW_USER_SETUP_FAILED,
- // Table 4. client internal error handling
- VVM3_VMG_DNS_FAILURE,
- VVM3_SPG_DNS_FAILURE,
- VVM3_VMG_CONNECTION_FAILED,
- VVM3_SPG_CONNECTION_FAILED,
- VVM3_VMG_TIMEOUT,
- VVM3_STATUS_SMS_TIMEOUT,
-
- VVM3_SUBSCRIBER_PROVISIONED,
- VVM3_SUBSCRIBER_BLOCKED,
- VVM3_SUBSCRIBER_UNKNOWN;
-
- public static class Type {
-
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({CONFIGURATION, DATA_CHANNEL, NOTIFICATION_CHANNEL, OTHER})
- public @interface Values {
-
- }
-
- public static final int CONFIGURATION = 1;
- public static final int DATA_CHANNEL = 2;
- public static final int NOTIFICATION_CHANNEL = 3;
- public static final int OTHER = 4;
- }
-
- private final int mType;
- private final boolean mIsSuccess;
-
- OmtpEvents(int type, boolean isSuccess) {
- mType = type;
- mIsSuccess = isSuccess;
- }
-
- OmtpEvents(int type) {
- mType = type;
- mIsSuccess = false;
- }
-
- OmtpEvents() {
- mType = Type.OTHER;
- mIsSuccess = false;
- }
-
- @Type.Values
- public int getType() {
- return mType;
- }
-
- public boolean isSuccess() {
- return mIsSuccess;
- }
-
-}
diff --git a/java/com/android/voicemailomtp/OmtpService.java b/java/com/android/voicemailomtp/OmtpService.java
deleted file mode 100644
index 261a7cb32..000000000
--- a/java/com/android/voicemailomtp/OmtpService.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp;
-
-import android.content.Intent;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.VisualVoicemailService;
-import android.telephony.VisualVoicemailSms;
-
-import com.android.voicemailomtp.sync.OmtpVvmSourceManager;
-
-public class OmtpService extends VisualVoicemailService {
-
- private static String TAG = "VvmOmtpService";
-
- public static final String ACTION_SMS_RECEIVED = "com.android.vociemailomtp.sms.sms_received";
-
- public static final String EXTRA_VOICEMAIL_SMS = "extra_voicemail_sms";
-
- @Override
- public void onCellServiceConnected(VisualVoicemailTask task,
- final PhoneAccountHandle phoneAccountHandle) {
- VvmLog.i(TAG, "onCellServiceConnected");
- ActivationTask
- .start(OmtpService.this, phoneAccountHandle, null);
- task.finish();
- }
-
- @Override
- public void onSmsReceived(VisualVoicemailTask task, final VisualVoicemailSms sms) {
- VvmLog.i(TAG, "onSmsReceived");
- Intent intent = new Intent(ACTION_SMS_RECEIVED);
- intent.setPackage(getPackageName());
- intent.putExtra(EXTRA_VOICEMAIL_SMS, sms);
- sendBroadcast(intent);
- task.finish();
- }
-
- @Override
- public void onSimRemoved(final VisualVoicemailTask task,
- final PhoneAccountHandle phoneAccountHandle) {
- VvmLog.i(TAG, "onSimRemoved");
- OmtpVvmSourceManager.getInstance(OmtpService.this).removeSource(phoneAccountHandle);
- task.finish();
- }
-
- @Override
- public void onStopped(VisualVoicemailTask task) {
- VvmLog.i(TAG, "onStopped");
- }
-}
diff --git a/java/com/android/voicemailomtp/OmtpVvmCarrierConfigHelper.java b/java/com/android/voicemailomtp/OmtpVvmCarrierConfigHelper.java
deleted file mode 100644
index b3e72d215..000000000
--- a/java/com/android/voicemailomtp/OmtpVvmCarrierConfigHelper.java
+++ /dev/null
@@ -1,423 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp;
-
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.os.Bundle;
-import android.os.PersistableBundle;
-import android.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.CarrierConfigManager;
-import android.telephony.TelephonyManager;
-import android.telephony.VisualVoicemailService;
-import android.telephony.VisualVoicemailSmsFilterSettings;
-import android.text.TextUtils;
-import android.util.ArraySet;
-
-import com.android.voicemailomtp.protocol.VisualVoicemailProtocol;
-import com.android.voicemailomtp.protocol.VisualVoicemailProtocolFactory;
-import com.android.voicemailomtp.sms.StatusMessage;
-
-import java.util.Arrays;
-import java.util.Set;
-
-/**
- * Manages carrier dependent visual voicemail configuration values. The primary source is the value
- * retrieved from CarrierConfigManager. If CarrierConfigManager does not provide the config
- * (KEY_VVM_TYPE_STRING is empty, or "hidden" configs), then the value hardcoded in telephony will
- * be used (in res/xml/vvm_config.xml)
- *
- * Hidden configs are new configs that are planned for future APIs, or miscellaneous settings that
- * may clutter CarrierConfigManager too much.
- *
- * The current hidden configs are: {@link #getSslPort()} {@link #getDisabledCapabilities()}
- */
-public class OmtpVvmCarrierConfigHelper {
-
- private static final String TAG = "OmtpVvmCarrierCfgHlpr";
-
- static final String KEY_VVM_TYPE_STRING = CarrierConfigManager.KEY_VVM_TYPE_STRING;
- static final String KEY_VVM_DESTINATION_NUMBER_STRING =
- CarrierConfigManager.KEY_VVM_DESTINATION_NUMBER_STRING;
- static final String KEY_VVM_PORT_NUMBER_INT =
- CarrierConfigManager.KEY_VVM_PORT_NUMBER_INT;
- static final String KEY_CARRIER_VVM_PACKAGE_NAME_STRING =
- CarrierConfigManager.KEY_CARRIER_VVM_PACKAGE_NAME_STRING;
- static final String KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY =
- "carrier_vvm_package_name_string_array";
- static final String KEY_VVM_PREFETCH_BOOL =
- CarrierConfigManager.KEY_VVM_PREFETCH_BOOL;
- static final String KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL =
- CarrierConfigManager.KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL;
-
- /**
- * @see #getSslPort()
- */
- static final String KEY_VVM_SSL_PORT_NUMBER_INT =
- "vvm_ssl_port_number_int";
-
- /**
- * @see #isLegacyModeEnabled()
- */
- static final String KEY_VVM_LEGACY_MODE_ENABLED_BOOL =
- "vvm_legacy_mode_enabled_bool";
-
- /**
- * Ban a capability reported by the server from being used. The array of string should be a
- * subset of the capabilities returned IMAP CAPABILITY command.
- *
- * @see #getDisabledCapabilities()
- */
- static final String KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY =
- "vvm_disabled_capabilities_string_array";
- static final String KEY_VVM_CLIENT_PREFIX_STRING =
- "vvm_client_prefix_string";
-
- private final Context mContext;
- private final PersistableBundle mCarrierConfig;
- private final String mVvmType;
- private final VisualVoicemailProtocol mProtocol;
- private final PersistableBundle mTelephonyConfig;
-
- private PhoneAccountHandle mPhoneAccountHandle;
-
- public OmtpVvmCarrierConfigHelper(Context context, PhoneAccountHandle handle) {
- mContext = context;
- mPhoneAccountHandle = handle;
- mCarrierConfig = getCarrierConfig();
-
- TelephonyManager telephonyManager =
- (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
- mTelephonyConfig = new TelephonyVvmConfigManager(context.getResources())
- .getConfig(telephonyManager.createForPhoneAccountHandle(mPhoneAccountHandle)
- .getSimOperator());
-
- mVvmType = getVvmType();
- mProtocol = VisualVoicemailProtocolFactory.create(mContext.getResources(), mVvmType);
-
- }
-
- @VisibleForTesting
- OmtpVvmCarrierConfigHelper(Context context, PersistableBundle carrierConfig,
- PersistableBundle telephonyConfig) {
- mContext = context;
- mCarrierConfig = carrierConfig;
- mTelephonyConfig = telephonyConfig;
- mVvmType = getVvmType();
- mProtocol = VisualVoicemailProtocolFactory.create(mContext.getResources(), mVvmType);
- }
-
- public Context getContext() {
- return mContext;
- }
-
- @Nullable
- public PhoneAccountHandle getPhoneAccountHandle() {
- return mPhoneAccountHandle;
- }
-
- /**
- * return whether the carrier's visual voicemail is supported, with KEY_VVM_TYPE_STRING set as a
- * known protocol.
- */
- public boolean isValid() {
- return mProtocol != null;
- }
-
- @Nullable
- public String getVvmType() {
- return (String) getValue(KEY_VVM_TYPE_STRING);
- }
-
- @Nullable
- public VisualVoicemailProtocol getProtocol() {
- return mProtocol;
- }
-
- /**
- * @returns arbitrary String stored in the config file. Used for protocol specific values.
- */
- @Nullable
- public String getString(String key) {
- return (String) getValue(key);
- }
-
- @Nullable
- public Set<String> getCarrierVvmPackageNames() {
- Set<String> names = getCarrierVvmPackageNames(mCarrierConfig);
- if (names != null) {
- return names;
- }
- return getCarrierVvmPackageNames(mTelephonyConfig);
- }
-
- private static Set<String> getCarrierVvmPackageNames(@Nullable PersistableBundle bundle) {
- if (bundle == null) {
- return null;
- }
- Set<String> names = new ArraySet<>();
- if (bundle.containsKey(KEY_CARRIER_VVM_PACKAGE_NAME_STRING)) {
- names.add(bundle.getString(KEY_CARRIER_VVM_PACKAGE_NAME_STRING));
- }
- if (bundle.containsKey(KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY)) {
- names.addAll(Arrays.asList(
- bundle.getStringArray(KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY)));
- }
- if (names.isEmpty()) {
- return null;
- }
- return names;
- }
-
- /**
- * For checking upon sim insertion whether visual voicemail should be enabled. This method does
- * so by checking if the carrier's voicemail app is installed.
- */
- public boolean isEnabledByDefault() {
- if (!isValid()) {
- return false;
- }
-
- Set<String> carrierPackages = getCarrierVvmPackageNames();
- if (carrierPackages == null) {
- return true;
- }
- for (String packageName : carrierPackages) {
- try {
- mContext.getPackageManager().getPackageInfo(packageName, 0);
- return false;
- } catch (NameNotFoundException e) {
- // Do nothing.
- }
- }
- return true;
- }
-
- public boolean isCellularDataRequired() {
- return (boolean) getValue(KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL, false);
- }
-
- public boolean isPrefetchEnabled() {
- return (boolean) getValue(KEY_VVM_PREFETCH_BOOL, true);
- }
-
-
- public int getApplicationPort() {
- return (int) getValue(KEY_VVM_PORT_NUMBER_INT, 0);
- }
-
- @Nullable
- public String getDestinationNumber() {
- return (String) getValue(KEY_VVM_DESTINATION_NUMBER_STRING);
- }
-
- /**
- * Hidden config.
- *
- * @return Port to start a SSL IMAP connection directly.
- *
- * TODO: make config public and add to CarrierConfigManager
- */
- public int getSslPort() {
- return (int) getValue(KEY_VVM_SSL_PORT_NUMBER_INT, 0);
- }
-
- /**
- * Hidden Config.
- *
- * <p>Sometimes the server states it supports a certain feature but we found they have bug on
- * the server side. For example, in b/28717550 the server reported AUTH=DIGEST-MD5 capability
- * but using it to login will cause subsequent response to be erroneous.
- *
- * @return A set of capabilities that is reported by the IMAP CAPABILITY command, but determined
- * to have issues and should not be used.
- */
- @Nullable
- public Set<String> getDisabledCapabilities() {
- Set<String> disabledCapabilities = getDisabledCapabilities(mCarrierConfig);
- if (disabledCapabilities != null) {
- return disabledCapabilities;
- }
- return getDisabledCapabilities(mTelephonyConfig);
- }
-
- @Nullable
- private static Set<String> getDisabledCapabilities(@Nullable PersistableBundle bundle) {
- if (bundle == null) {
- return null;
- }
- if (!bundle.containsKey(KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY)) {
- return null;
- }
- ArraySet<String> result = new ArraySet<String>();
- result.addAll(
- Arrays.asList(bundle.getStringArray(KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY)));
- return result;
- }
-
- public String getClientPrefix() {
- String prefix = (String) getValue(KEY_VVM_CLIENT_PREFIX_STRING);
- if (prefix != null) {
- return prefix;
- }
- return "//VVM";
- }
-
- /**
- * Should legacy mode be used when the OMTP VVM client is disabled?
- *
- * <p>Legacy mode is a mode that on the carrier side visual voicemail is still activated, but on
- * the client side all network operations are disabled. SMSs are still monitored so a new
- * message SYNC SMS will be translated to show a message waiting indicator, like traditional
- * voicemails.
- *
- * <p>This is for carriers that does not support VVM deactivation so voicemail can continue to
- * function without the data cost.
- */
- public boolean isLegacyModeEnabled() {
- return (boolean) getValue(KEY_VVM_LEGACY_MODE_ENABLED_BOOL, false);
- }
-
- public void startActivation() {
- PhoneAccountHandle phoneAccountHandle = getPhoneAccountHandle();
- if (phoneAccountHandle == null) {
- // This should never happen
- // Error logged in getPhoneAccountHandle().
- return;
- }
-
- if (mVvmType == null || mVvmType.isEmpty()) {
- // The VVM type is invalid; we should never have gotten here in the first place since
- // this is loaded initially in the constructor, and callers should check isValid()
- // before trying to start activation anyways.
- VvmLog.e(TAG, "startActivation : vvmType is null or empty for account " +
- phoneAccountHandle);
- return;
- }
-
- if (mProtocol != null) {
- ActivationTask.start(mContext, mPhoneAccountHandle, null);
- }
- }
-
- public void activateSmsFilter() {
- VisualVoicemailService.setSmsFilterSettings(mContext, getPhoneAccountHandle(),
- new VisualVoicemailSmsFilterSettings.Builder()
- .setClientPrefix(getClientPrefix())
- .build());
- }
-
- public void startDeactivation() {
- if (!isLegacyModeEnabled()) {
- // SMS should still be filtered in legacy mode
- VisualVoicemailService.setSmsFilterSettings(mContext, getPhoneAccountHandle(), null);
- }
- if (mProtocol != null) {
- mProtocol.startDeactivation(this);
- }
- }
-
- public boolean supportsProvisioning() {
- if (mProtocol != null) {
- return mProtocol.supportsProvisioning();
- }
- return false;
- }
-
- public void startProvisioning(ActivationTask task, PhoneAccountHandle phone,
- VoicemailStatus.Editor status, StatusMessage message, Bundle data) {
- if (mProtocol != null) {
- mProtocol.startProvisioning(task, phone, this, status, message, data);
- }
- }
-
- public void requestStatus(@Nullable PendingIntent sentIntent) {
- if (mProtocol != null) {
- mProtocol.requestStatus(this, sentIntent);
- }
- }
-
- public void handleEvent(VoicemailStatus.Editor status, OmtpEvents event) {
- VvmLog.i(TAG, "OmtpEvent:" + event);
- if (mProtocol != null) {
- mProtocol.handleEvent(mContext, this, status, event);
- }
- }
-
- @Override
- public String toString() {
- StringBuilder builder = new StringBuilder("OmtpVvmCarrierConfigHelper [");
- builder.append("phoneAccountHandle: ").append(mPhoneAccountHandle)
- .append(", carrierConfig: ").append(mCarrierConfig != null)
- .append(", telephonyConfig: ").append(mTelephonyConfig != null)
- .append(", type: ").append(getVvmType())
- .append(", destinationNumber: ").append(getDestinationNumber())
- .append(", applicationPort: ").append(getApplicationPort())
- .append(", sslPort: ").append(getSslPort())
- .append(", isEnabledByDefault: ").append(isEnabledByDefault())
- .append(", isCellularDataRequired: ").append(isCellularDataRequired())
- .append(", isPrefetchEnabled: ").append(isPrefetchEnabled())
- .append(", isLegacyModeEnabled: ").append(isLegacyModeEnabled())
- .append("]");
- return builder.toString();
- }
-
- @Nullable
- private PersistableBundle getCarrierConfig() {
-
- CarrierConfigManager carrierConfigManager = (CarrierConfigManager)
- mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE);
- if (carrierConfigManager == null) {
- VvmLog.w(TAG, "No carrier config service found.");
- return null;
- }
-
- PersistableBundle config = TelephonyManagerStub
- .getCarrirConfigForPhoneAccountHandle(getContext(), mPhoneAccountHandle);
-
- if (TextUtils.isEmpty(config.getString(CarrierConfigManager.KEY_VVM_TYPE_STRING))) {
- return null;
- }
- return config;
- }
-
- @Nullable
- private Object getValue(String key) {
- return getValue(key, null);
- }
-
- @Nullable
- private Object getValue(String key, Object defaultValue) {
- Object result;
- if (mCarrierConfig != null) {
- result = mCarrierConfig.get(key);
- if (result != null) {
- return result;
- }
- }
- if (mTelephonyConfig != null) {
- result = mTelephonyConfig.get(key);
- if (result != null) {
- return result;
- }
- }
- return defaultValue;
- }
-
-} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/SubscriptionInfoHelper.java b/java/com/android/voicemailomtp/SubscriptionInfoHelper.java
deleted file mode 100644
index b916247ad..000000000
--- a/java/com/android/voicemailomtp/SubscriptionInfoHelper.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/**
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp;
-
-import android.app.ActionBar;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.text.TextUtils;
-
-/**
- * Helper for manipulating intents or components with subscription-related information.
- *
- * In settings, subscription ids and labels are passed along to indicate that settings
- * are being changed for particular subscriptions. This helper provides functions for
- * helping extract this info and perform common operations using this info.
- */
-public class SubscriptionInfoHelper {
- public static final int NO_SUB_ID = -1;
-
- // Extra on intent containing the id of a subscription.
- public static final String SUB_ID_EXTRA =
- "com.android.voicemailomtp.settings.SubscriptionInfoHelper.SubscriptionId";
- // Extra on intent containing the label of a subscription.
- private static final String SUB_LABEL_EXTRA =
- "com.android.voicemailomtp.settings.SubscriptionInfoHelper.SubscriptionLabel";
-
- private static Context mContext;
-
- private static int mSubId = NO_SUB_ID;
- private static String mSubLabel;
-
- /**
- * Instantiates the helper, by extracting the subscription id and label from the intent.
- */
- public SubscriptionInfoHelper(Context context, Intent intent) {
- mContext = context;
- mSubId = intent.getIntExtra(SUB_ID_EXTRA, NO_SUB_ID);
- mSubLabel = intent.getStringExtra(SUB_LABEL_EXTRA);
- }
-
- /**
- * Sets the action bar title to the string specified by the given resource id, formatting
- * it with the subscription label. This assumes the resource string is formattable with a
- * string-type specifier.
- *
- * If the subscription label does not exists, leave the existing title.
- */
- public void setActionBarTitle(ActionBar actionBar, Resources res, int resId) {
- if (actionBar == null || TextUtils.isEmpty(mSubLabel)) {
- return;
- }
-
- String title = String.format(res.getString(resId), mSubLabel);
- actionBar.setTitle(title);
- }
-
- public int getSubId() {
- return mSubId;
- }
-}
diff --git a/java/com/android/voicemailomtp/TelephonyManagerStub.java b/java/com/android/voicemailomtp/TelephonyManagerStub.java
deleted file mode 100644
index e2e5dacdb..000000000
--- a/java/com/android/voicemailomtp/TelephonyManagerStub.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.os.Build.VERSION_CODES;
-import android.os.PersistableBundle;
-import android.telecom.PhoneAccount;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.CarrierConfigManager;
-import android.telephony.SubscriptionManager;
-import android.telephony.TelephonyManager;
-import java.lang.reflect.Method;
-
-/**
- * Temporary stub for public APIs that should be added into telephony manager.
- *
- * <p>TODO(b/32637799) remove this.
- */
-@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
-public class TelephonyManagerStub {
-
- private static final String TAG = "TelephonyManagerStub";
-
- public static void showVoicemailNotification(int voicemailCount) {
-
- }
-
- /**
- * Dismisses the message waiting (voicemail) indicator.
- *
- * @param subId the subscription id we should dismiss the notification for.
- */
- public static void clearMwiIndicator(int subId) {
-
- }
-
- public static void setShouldCheckVisualVoicemailConfigurationForMwi(int subId,
- boolean enabled) {
-
- }
-
- public static int getSubIdForPhoneAccount(Context context, PhoneAccount phoneAccount) {
- // Hidden
- TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class);
- try {
- Method method = TelephonyManager.class
- .getMethod("getSubIdForPhoneAccount", PhoneAccount.class);
- return (int) method.invoke(telephonyManager, phoneAccount);
- } catch (Exception e) {
- VvmLog.e(TAG, "reflection call to getSubIdForPhoneAccount failed:", e);
- }
- return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
- }
-
- public static String getNetworkSpecifierForPhoneAccountHandle(Context context,
- PhoneAccountHandle phoneAccountHandle) {
- return String.valueOf(SubscriptionManager.getDefaultDataSubscriptionId());
- }
-
- public static PersistableBundle getCarrirConfigForPhoneAccountHandle(Context context,
- PhoneAccountHandle phoneAccountHandle) {
- return context.getSystemService(CarrierConfigManager.class).getConfig();
- }
-}
diff --git a/java/com/android/voicemailomtp/TelephonyVvmConfigManager.java b/java/com/android/voicemailomtp/TelephonyVvmConfigManager.java
deleted file mode 100644
index ab13d36ad..000000000
--- a/java/com/android/voicemailomtp/TelephonyVvmConfigManager.java
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp;
-
-import android.content.res.Resources;
-import android.os.PersistableBundle;
-import android.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
-import android.util.ArrayMap;
-import com.android.voicemailomtp.utils.XmlUtils;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Map;
-import java.util.Map.Entry;
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
-/**
- * Load and caches telephony vvm config from res/xml/vvm_config.xml
- */
-public class TelephonyVvmConfigManager {
-
- private static final String TAG = "TelephonyVvmCfgMgr";
-
- private static final boolean USE_DEBUG_CONFIG = false;
-
- private static final String TAG_PERSISTABLEMAP = "pbundle_as_map";
-
- static final String KEY_MCCMNC = "mccmnc";
-
- private static Map<String, PersistableBundle> sCachedConfigs;
-
- private final Map<String, PersistableBundle> mConfigs;
-
- public TelephonyVvmConfigManager(Resources resources) {
- if (sCachedConfigs == null) {
- sCachedConfigs = loadConfigs(resources.getXml(R.xml.vvm_config));
- }
- mConfigs = sCachedConfigs;
- }
-
- @VisibleForTesting
- TelephonyVvmConfigManager(XmlPullParser parser) {
- mConfigs = loadConfigs(parser);
- }
-
- @Nullable
- public PersistableBundle getConfig(String mccMnc) {
- if (USE_DEBUG_CONFIG) {
- return mConfigs.get("TEST");
- }
- return mConfigs.get(mccMnc);
- }
-
- private static Map<String, PersistableBundle> loadConfigs(XmlPullParser parser) {
- Map<String, PersistableBundle> configs = new ArrayMap<>();
- try {
- ArrayList list = readBundleList(parser);
- for (Object object : list) {
- if (!(object instanceof PersistableBundle)) {
- throw new IllegalArgumentException("PersistableBundle expected, got " + object);
- }
- PersistableBundle bundle = (PersistableBundle) object;
- String[] mccMncs = bundle.getStringArray(KEY_MCCMNC);
- if (mccMncs == null) {
- throw new IllegalArgumentException("MCCMNC is null");
- }
- for (String mccMnc : mccMncs) {
- configs.put(mccMnc, bundle);
- }
- }
- } catch (IOException | XmlPullParserException e) {
- throw new RuntimeException(e);
- }
- return configs;
- }
-
- @Nullable
- public static ArrayList readBundleList(XmlPullParser in) throws IOException,
- XmlPullParserException {
- final int outerDepth = in.getDepth();
- int event;
- while (((event = in.next()) != XmlPullParser.END_DOCUMENT) &&
- (event != XmlPullParser.END_TAG || in.getDepth() < outerDepth)) {
- if (event == XmlPullParser.START_TAG) {
- final String startTag = in.getName();
- final String[] tagName = new String[1];
- in.next();
- return XmlUtils.readThisListXml(in, startTag, tagName,
- new MyReadMapCallback(), false);
- }
- }
- return null;
- }
-
- public static PersistableBundle restoreFromXml(XmlPullParser in) throws IOException,
- XmlPullParserException {
- final int outerDepth = in.getDepth();
- final String startTag = in.getName();
- final String[] tagName = new String[1];
- int event;
- while (((event = in.next()) != XmlPullParser.END_DOCUMENT) &&
- (event != XmlPullParser.END_TAG || in.getDepth() < outerDepth)) {
- if (event == XmlPullParser.START_TAG) {
- ArrayMap<String, ?> map =
- XmlUtils.readThisArrayMapXml(in, startTag, tagName,
- new MyReadMapCallback());
- PersistableBundle result = new PersistableBundle();
- for (Entry<String, ?> entry : map.entrySet()) {
- Object value = entry.getValue();
- if (value instanceof Integer) {
- result.putInt(entry.getKey(), (int) value);
- } else if (value instanceof Boolean) {
- result.putBoolean(entry.getKey(), (boolean) value);
- } else if (value instanceof String) {
- result.putString(entry.getKey(), (String) value);
- } else if (value instanceof String[]) {
- result.putStringArray(entry.getKey(), (String[]) value);
- } else if (value instanceof PersistableBundle) {
- result.putPersistableBundle(entry.getKey(), (PersistableBundle) value);
- }
- }
- return result;
- }
- }
- return PersistableBundle.EMPTY;
- }
-
- static class MyReadMapCallback implements XmlUtils.ReadMapCallback {
-
- @Override
- public Object readThisUnknownObjectXml(XmlPullParser in, String tag)
- throws XmlPullParserException, IOException {
- if (TAG_PERSISTABLEMAP.equals(tag)) {
- return restoreFromXml(in);
- }
- throw new XmlPullParserException("Unknown tag=" + tag);
- }
- }
-}
diff --git a/java/com/android/voicemailomtp/VisualVoicemailPreferences.java b/java/com/android/voicemailomtp/VisualVoicemailPreferences.java
deleted file mode 100644
index 5bc2c6951..000000000
--- a/java/com/android/voicemailomtp/VisualVoicemailPreferences.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.preference.PreferenceManager;
-import android.support.annotation.Nullable;
-import android.telecom.PhoneAccountHandle;
-import java.util.Set;
-
-/**
- * Save visual voicemail values in shared preferences to be retrieved later. Because a voicemail
- * source is tied 1:1 to a phone account, the phone account handle is used in the key for each
- * voicemail source and the associated data.
- */
-public class VisualVoicemailPreferences {
-
- private static final String VISUAL_VOICEMAIL_SHARED_PREFS_KEY_PREFIX =
- "visual_voicemail_";
-
- private final SharedPreferences mPreferences;
- private final PhoneAccountHandle mPhoneAccountHandle;
-
- public VisualVoicemailPreferences(Context context, PhoneAccountHandle phoneAccountHandle) {
- mPreferences = PreferenceManager.getDefaultSharedPreferences(context);
- mPhoneAccountHandle = phoneAccountHandle;
- }
-
- public class Editor {
-
- private final SharedPreferences.Editor mEditor;
-
- private Editor() {
- mEditor = mPreferences.edit();
- }
-
- public void apply() {
- mEditor.apply();
- }
-
- public Editor putBoolean(String key, boolean value) {
- mEditor.putBoolean(getKey(key), value);
- return this;
- }
-
- @NeededForTesting
- public Editor putFloat(String key, float value) {
- mEditor.putFloat(getKey(key), value);
- return this;
- }
-
- public Editor putInt(String key, int value) {
- mEditor.putInt(getKey(key), value);
- return this;
- }
-
- @NeededForTesting
- public Editor putLong(String key, long value) {
- mEditor.putLong(getKey(key), value);
- return this;
- }
-
- public Editor putString(String key, String value) {
- mEditor.putString(getKey(key), value);
- return this;
- }
-
- @NeededForTesting
- public Editor putStringSet(String key, Set<String> value) {
- mEditor.putStringSet(getKey(key), value);
- return this;
- }
- }
-
- public Editor edit() {
- return new Editor();
- }
-
- public boolean getBoolean(String key, boolean defValue) {
- return getValue(key, defValue);
- }
-
- @NeededForTesting
- public float getFloat(String key, float defValue) {
- return getValue(key, defValue);
- }
-
- public int getInt(String key, int defValue) {
- return getValue(key, defValue);
- }
-
- @NeededForTesting
- public long getLong(String key, long defValue) {
- return getValue(key, defValue);
- }
-
- public String getString(String key, String defValue) {
- return getValue(key, defValue);
- }
-
- @Nullable
- public String getString(String key) {
- return getValue(key, null);
- }
-
- @NeededForTesting
- public Set<String> getStringSet(String key, Set<String> defValue) {
- return getValue(key, defValue);
- }
-
- public boolean contains(String key) {
- return mPreferences.contains(getKey(key));
- }
-
- private <T> T getValue(String key, T defValue) {
- if (!contains(key)) {
- return defValue;
- }
- Object object = mPreferences.getAll().get(getKey(key));
- if (object == null) {
- return defValue;
- }
- return (T) object;
- }
-
- private String getKey(String key) {
- return VISUAL_VOICEMAIL_SHARED_PREFS_KEY_PREFIX + key + "_" + mPhoneAccountHandle.getId();
- }
-}
diff --git a/java/com/android/voicemailomtp/Voicemail.java b/java/com/android/voicemailomtp/Voicemail.java
deleted file mode 100644
index 9d8395142..000000000
--- a/java/com/android/voicemailomtp/Voicemail.java
+++ /dev/null
@@ -1,330 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.voicemailomtp;
-
-import android.net.Uri;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.telecom.PhoneAccountHandle;
-import android.text.TextUtils;
-
-/**
- * Represents a single voicemail stored in the voicemail content provider.
- */
-public class Voicemail implements Parcelable {
-
- private final Long mTimestamp;
- private final String mNumber;
- private final PhoneAccountHandle mPhoneAccount;
- private final Long mId;
- private final Long mDuration;
- private final String mSource;
- private final String mProviderData;
- private final Uri mUri;
- private final Boolean mIsRead;
- private final Boolean mHasContent;
- private final String mTranscription;
-
- private Voicemail(Long timestamp, String number, PhoneAccountHandle phoneAccountHandle, Long id,
- Long duration, String source, String providerData, Uri uri, Boolean isRead,
- Boolean hasContent, String transcription) {
- mTimestamp = timestamp;
- mNumber = number;
- mPhoneAccount = phoneAccountHandle;
- mId = id;
- mDuration = duration;
- mSource = source;
- mProviderData = providerData;
- mUri = uri;
- mIsRead = isRead;
- mHasContent = hasContent;
- mTranscription = transcription;
- }
-
- /**
- * Create a {@link Builder} for a new {@link Voicemail} to be inserted. <p> The number and the
- * timestamp are mandatory for insertion.
- */
- public static Builder createForInsertion(long timestamp, String number) {
- return new Builder().setNumber(number).setTimestamp(timestamp);
- }
-
- /**
- * Create a {@link Builder} for a {@link Voicemail} to be updated (or deleted). <p> The id and
- * source data fields are mandatory for update - id is necessary for updating the database and
- * source data is necessary for updating the server.
- */
- public static Builder createForUpdate(long id, String sourceData) {
- return new Builder().setId(id).setSourceData(sourceData);
- }
-
- /**
- * Builder pattern for creating a {@link Voicemail}. The builder must be created with the {@link
- * #createForInsertion(long, String)} method. <p> This class is <b>not thread safe</b>
- */
- public static class Builder {
-
- private Long mBuilderTimestamp;
- private String mBuilderNumber;
- private PhoneAccountHandle mBuilderPhoneAccount;
- private Long mBuilderId;
- private Long mBuilderDuration;
- private String mBuilderSourcePackage;
- private String mBuilderSourceData;
- private Uri mBuilderUri;
- private Boolean mBuilderIsRead;
- private boolean mBuilderHasContent;
- private String mBuilderTranscription;
-
- /**
- * You should use the correct factory method to construct a builder.
- */
- private Builder() {
- }
-
- public Builder setNumber(String number) {
- mBuilderNumber = number;
- return this;
- }
-
- public Builder setTimestamp(long timestamp) {
- mBuilderTimestamp = timestamp;
- return this;
- }
-
- public Builder setPhoneAccount(PhoneAccountHandle phoneAccount) {
- mBuilderPhoneAccount = phoneAccount;
- return this;
- }
-
- public Builder setId(long id) {
- mBuilderId = id;
- return this;
- }
-
- public Builder setDuration(long duration) {
- mBuilderDuration = duration;
- return this;
- }
-
- public Builder setSourcePackage(String sourcePackage) {
- mBuilderSourcePackage = sourcePackage;
- return this;
- }
-
- public Builder setSourceData(String sourceData) {
- mBuilderSourceData = sourceData;
- return this;
- }
-
- public Builder setUri(Uri uri) {
- mBuilderUri = uri;
- return this;
- }
-
- public Builder setIsRead(boolean isRead) {
- mBuilderIsRead = isRead;
- return this;
- }
-
- public Builder setHasContent(boolean hasContent) {
- mBuilderHasContent = hasContent;
- return this;
- }
-
- public Builder setTranscription(String transcription) {
- mBuilderTranscription = transcription;
- return this;
- }
-
- public Voicemail build() {
- mBuilderId = mBuilderId == null ? -1 : mBuilderId;
- mBuilderTimestamp = mBuilderTimestamp == null ? 0 : mBuilderTimestamp;
- mBuilderDuration = mBuilderDuration == null ? 0 : mBuilderDuration;
- mBuilderIsRead = mBuilderIsRead == null ? false : mBuilderIsRead;
- return new Voicemail(mBuilderTimestamp, mBuilderNumber, mBuilderPhoneAccount,
- mBuilderId, mBuilderDuration, mBuilderSourcePackage, mBuilderSourceData,
- mBuilderUri, mBuilderIsRead, mBuilderHasContent, mBuilderTranscription);
- }
- }
-
- /**
- * The identifier of the voicemail in the content provider. <p> This may be missing in the case
- * of a new {@link Voicemail} that we plan to insert into the content provider, since until it
- * has been inserted we don't know what id it should have. If none is specified, we return -1.
- */
- public long getId() {
- return mId;
- }
-
- /**
- * The number of the person leaving the voicemail, empty string if unknown, null if not set.
- */
- public String getNumber() {
- return mNumber;
- }
-
- /**
- * The phone account associated with the voicemail, null if not set.
- */
- public PhoneAccountHandle getPhoneAccount() {
- return mPhoneAccount;
- }
-
- /**
- * The timestamp the voicemail was received, in millis since the epoch, zero if not set.
- */
- public long getTimestampMillis() {
- return mTimestamp;
- }
-
- /**
- * Gets the duration of the voicemail in millis, or zero if the field is not set.
- */
- public long getDuration() {
- return mDuration;
- }
-
- /**
- * Returns the package name of the source that added this voicemail, or null if this field is
- * not set.
- */
- public String getSourcePackage() {
- return mSource;
- }
-
- /**
- * Returns the application-specific data type stored with the voicemail, or null if this field
- * is not set. <p> Source data is typically used as an identifier to uniquely identify the
- * voicemail against the voicemail server. This is likely to be something like the IMAP UID, or
- * some other server-generated identifying string.
- */
- public String getSourceData() {
- return mProviderData;
- }
-
- /**
- * Gets the Uri that can be used to refer to this voicemail, and to make it play. <p> Returns
- * null if we don't know the Uri.
- */
- public Uri getUri() {
- return mUri;
- }
-
- /**
- * Tells us if the voicemail message has been marked as read. <p> Always returns false if this
- * field has not been set, i.e. if hasRead() returns false.
- */
- public boolean isRead() {
- return mIsRead;
- }
-
- /**
- * Tells us if there is content stored at the Uri.
- */
- public boolean hasContent() {
- return mHasContent;
- }
-
- /**
- * Returns the text transcription of this voicemail, or null if this field is not set.
- */
- public String getTranscription() {
- return mTranscription;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeLong(mTimestamp);
- writeCharSequence(dest, mNumber);
- if (mPhoneAccount == null) {
- dest.writeInt(0);
- } else {
- dest.writeInt(1);
- mPhoneAccount.writeToParcel(dest, flags);
- }
- dest.writeLong(mId);
- dest.writeLong(mDuration);
- writeCharSequence(dest, mSource);
- writeCharSequence(dest, mProviderData);
- if (mUri == null) {
- dest.writeInt(0);
- } else {
- dest.writeInt(1);
- mUri.writeToParcel(dest, flags);
- }
- if (mIsRead) {
- dest.writeInt(1);
- } else {
- dest.writeInt(0);
- }
- if (mHasContent) {
- dest.writeInt(1);
- } else {
- dest.writeInt(0);
- }
- writeCharSequence(dest, mTranscription);
- }
-
- public static final Creator<Voicemail> CREATOR
- = new Creator<Voicemail>() {
- @Override
- public Voicemail createFromParcel(Parcel in) {
- return new Voicemail(in);
- }
-
- @Override
- public Voicemail[] newArray(int size) {
- return new Voicemail[size];
- }
- };
-
- private Voicemail(Parcel in) {
- mTimestamp = in.readLong();
- mNumber = (String) readCharSequence(in);
- if (in.readInt() > 0) {
- mPhoneAccount = PhoneAccountHandle.CREATOR.createFromParcel(in);
- } else {
- mPhoneAccount = null;
- }
- mId = in.readLong();
- mDuration = in.readLong();
- mSource = (String) readCharSequence(in);
- mProviderData = (String) readCharSequence(in);
- if (in.readInt() > 0) {
- mUri = Uri.CREATOR.createFromParcel(in);
- } else {
- mUri = null;
- }
- mIsRead = in.readInt() > 0 ? true : false;
- mHasContent = in.readInt() > 0 ? true : false;
- mTranscription = (String) readCharSequence(in);
- }
-
- private static CharSequence readCharSequence(Parcel in) {
- return TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
- }
-
- public static void writeCharSequence(Parcel dest, CharSequence val) {
- TextUtils.writeToParcel(val, dest, 0);
- }
-}
diff --git a/java/com/android/voicemailomtp/VoicemailStatus.java b/java/com/android/voicemailomtp/VoicemailStatus.java
deleted file mode 100644
index 63007932e..000000000
--- a/java/com/android/voicemailomtp/VoicemailStatus.java
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp;
-
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.content.Context;
-import android.net.Uri;
-import android.provider.VoicemailContract;
-import android.provider.VoicemailContract.Status;
-import android.support.annotation.Nullable;
-import android.telecom.PhoneAccountHandle;
-
-public class VoicemailStatus {
-
- private static final String TAG = "VvmStatus";
-
- public static class Editor {
-
- private final Context mContext;
- @Nullable
- private final PhoneAccountHandle mPhoneAccountHandle;
-
- private ContentValues mValues = new ContentValues();
-
- private Editor(Context context, PhoneAccountHandle phoneAccountHandle) {
- mContext = context;
- mPhoneAccountHandle = phoneAccountHandle;
- if (mPhoneAccountHandle == null) {
- VvmLog.w(TAG, "VoicemailStatus.Editor created with null phone account, status will"
- + " not be written");
- }
- }
-
- @Nullable
- public PhoneAccountHandle getPhoneAccountHandle() {
- return mPhoneAccountHandle;
- }
-
- public Editor setType(String type) {
- mValues.put(Status.SOURCE_TYPE, type);
- return this;
- }
-
- public Editor setConfigurationState(int configurationState) {
- mValues.put(Status.CONFIGURATION_STATE, configurationState);
- return this;
- }
-
- public Editor setDataChannelState(int dataChannelState) {
- mValues.put(Status.DATA_CHANNEL_STATE, dataChannelState);
- return this;
- }
-
- public Editor setNotificationChannelState(int notificationChannelState) {
- mValues.put(Status.NOTIFICATION_CHANNEL_STATE, notificationChannelState);
- return this;
- }
-
- public Editor setQuota(int occupied, int total) {
- if (occupied == VoicemailContract.Status.QUOTA_UNAVAILABLE
- && total == VoicemailContract.Status.QUOTA_UNAVAILABLE) {
- return this;
- }
-
- mValues.put(Status.QUOTA_OCCUPIED, occupied);
- mValues.put(Status.QUOTA_TOTAL, total);
- return this;
- }
-
- /**
- * Apply the changes to the {@link VoicemailStatus} {@link #Editor}.
- *
- * @return {@code true} if the changes were successfully applied, {@code false} otherwise.
- */
- public boolean apply() {
- if (mPhoneAccountHandle == null) {
- return false;
- }
- mValues.put(Status.PHONE_ACCOUNT_COMPONENT_NAME,
- mPhoneAccountHandle.getComponentName().flattenToString());
- mValues.put(Status.PHONE_ACCOUNT_ID, mPhoneAccountHandle.getId());
- ContentResolver contentResolver = mContext.getContentResolver();
- Uri statusUri = VoicemailContract.Status.buildSourceUri(mContext.getPackageName());
- try {
- contentResolver.insert(statusUri, mValues);
- } catch (IllegalArgumentException iae) {
- VvmLog.e(TAG, "apply :: failed to insert content resolver ", iae);
- mValues.clear();
- return false;
- }
- mValues.clear();
- return true;
- }
-
- public ContentValues getValues() {
- return mValues;
- }
- }
-
- /**
- * A voicemail status editor that the decision of whether to actually write to the database can
- * be deferred. This object will be passed around as a usual {@link Editor}, but {@link
- * #apply()} doesn't do anything. If later the creator of this object decides any status changes
- * written to it should be committed, {@link #deferredApply()} should be called.
- */
- public static class DeferredEditor extends Editor {
-
- private DeferredEditor(Context context, PhoneAccountHandle phoneAccountHandle) {
- super(context, phoneAccountHandle);
- }
-
- @Override
- public boolean apply() {
- // Do nothing
- return true;
- }
-
- public void deferredApply() {
- super.apply();
- }
- }
-
- public static Editor edit(Context context, PhoneAccountHandle phoneAccountHandle) {
- return new Editor(context, phoneAccountHandle);
- }
-
- /**
- * Reset the status to the "disabled" state, which the UI should not show anything for this
- * phoneAccountHandle.
- */
- public static void disable(Context context, PhoneAccountHandle phoneAccountHandle) {
- edit(context, phoneAccountHandle)
- .setConfigurationState(Status.CONFIGURATION_STATE_NOT_CONFIGURED)
- .setDataChannelState(Status.DATA_CHANNEL_STATE_NO_CONNECTION)
- .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION)
- .apply();
- }
-
- public static DeferredEditor deferredEdit(Context context,
- PhoneAccountHandle phoneAccountHandle) {
- return new DeferredEditor(context, phoneAccountHandle);
- }
-}
diff --git a/java/com/android/voicemailomtp/VvmLog.java b/java/com/android/voicemailomtp/VvmLog.java
deleted file mode 100644
index 2add66a53..000000000
--- a/java/com/android/voicemailomtp/VvmLog.java
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp;
-
-import android.util.Log;
-import com.android.voicemailomtp.utils.IndentingPrintWriter;
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
-import java.util.ArrayDeque;
-import java.util.Calendar;
-import java.util.Deque;
-import java.util.Iterator;
-
-/**
- * Helper methods for adding to OMTP visual voicemail local logs.
- */
-public class VvmLog {
-
- private static final int MAX_OMTP_VVM_LOGS = 100;
-
- private static final LocalLog sLocalLog = new LocalLog(MAX_OMTP_VVM_LOGS);
-
- public static void log(String tag, String log) {
- sLocalLog.log(tag + ": " + log);
- }
-
- public static void dump(FileDescriptor fd, PrintWriter printwriter, String[] args) {
- IndentingPrintWriter indentingPrintWriter = new IndentingPrintWriter(printwriter, " ");
- indentingPrintWriter.increaseIndent();
- sLocalLog.dump(fd, indentingPrintWriter, args);
- indentingPrintWriter.decreaseIndent();
- }
-
- public static int e(String tag, String log) {
- log(tag, log);
- return Log.e(tag, log);
- }
-
- public static int e(String tag, String log, Throwable e) {
- log(tag, log + " " + e);
- return Log.e(tag, log, e);
- }
-
- public static int w(String tag, String log) {
- log(tag, log);
- return Log.w(tag, log);
- }
-
- public static int w(String tag, String log, Throwable e) {
- log(tag, log + " " + e);
- return Log.w(tag, log, e);
- }
-
- public static int i(String tag, String log) {
- log(tag, log);
- return Log.i(tag, log);
- }
-
- public static int i(String tag, String log, Throwable e) {
- log(tag, log + " " + e);
- return Log.i(tag, log, e);
- }
-
- public static int d(String tag, String log) {
- log(tag, log);
- return Log.d(tag, log);
- }
-
- public static int d(String tag, String log, Throwable e) {
- log(tag, log + " " + e);
- return Log.d(tag, log, e);
- }
-
- public static int v(String tag, String log) {
- log(tag, log);
- return Log.v(tag, log);
- }
-
- public static int v(String tag, String log, Throwable e) {
- log(tag, log + " " + e);
- return Log.v(tag, log, e);
- }
-
- public static int wtf(String tag, String log) {
- log(tag, log);
- return Log.wtf(tag, log);
- }
-
- public static int wtf(String tag, String log, Throwable e) {
- log(tag, log + " " + e);
- return Log.wtf(tag, log, e);
- }
-
- /**
- * Redact personally identifiable information for production users. If we are running in verbose
- * mode, return the original string, otherwise return a SHA-1 hash of the input string.
- */
- public static String pii(Object pii) {
- if (pii == null) {
- return String.valueOf(pii);
- }
- return "[PII]";
- }
-
- public static class LocalLog {
-
- private final Deque<String> mLog;
- private final int mMaxLines;
-
- public LocalLog(int maxLines) {
- mMaxLines = Math.max(0, maxLines);
- mLog = new ArrayDeque<>(mMaxLines);
- }
-
- public void log(String msg) {
- if (mMaxLines <= 0) {
- return;
- }
- Calendar c = Calendar.getInstance();
- c.setTimeInMillis(System.currentTimeMillis());
- append(String.format("%tm-%td %tH:%tM:%tS.%tL - %s", c, c, c, c, c, c, msg));
- }
-
- private synchronized void append(String logLine) {
- while (mLog.size() >= mMaxLines) {
- mLog.remove();
- }
- mLog.add(logLine);
- }
-
- public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
- Iterator<String> itr = mLog.iterator();
- while (itr.hasNext()) {
- pw.println(itr.next());
- }
- }
-
- public synchronized void reverseDump(FileDescriptor fd, PrintWriter pw, String[] args) {
- Iterator<String> itr = mLog.descendingIterator();
- while (itr.hasNext()) {
- pw.println(itr.next());
- }
- }
-
- public static class ReadOnlyLocalLog {
-
- private final LocalLog mLog;
-
- ReadOnlyLocalLog(LocalLog log) {
- mLog = log;
- }
-
- public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
- mLog.dump(fd, pw, args);
- }
-
- public void reverseDump(FileDescriptor fd, PrintWriter pw, String[] args) {
- mLog.reverseDump(fd, pw, args);
- }
- }
-
- public ReadOnlyLocalLog readOnlyLocalLog() {
- return new ReadOnlyLocalLog(this);
- }
- }
-}
diff --git a/java/com/android/voicemailomtp/VvmPackageInstallReceiver.java b/java/com/android/voicemailomtp/VvmPackageInstallReceiver.java
deleted file mode 100644
index 7d9eee9f8..000000000
--- a/java/com/android/voicemailomtp/VvmPackageInstallReceiver.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.telecom.PhoneAccountHandle;
-
-import com.android.voicemailomtp.settings.VisualVoicemailSettingsUtil;
-import com.android.voicemailomtp.sync.OmtpVvmSourceManager;
-
-import java.util.Set;
-
-/**
- * When a new package is installed, check if it matches any of the vvm carrier apps of the currently
- * enabled dialer vvm sources.
- */
-public class VvmPackageInstallReceiver extends BroadcastReceiver {
-
- private static final String TAG = "VvmPkgInstallReceiver";
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (intent.getData() == null) {
- return;
- }
-
- String packageName = intent.getData().getSchemeSpecificPart();
- if (packageName == null) {
- return;
- }
-
- OmtpVvmSourceManager vvmSourceManager = OmtpVvmSourceManager.getInstance(context);
- Set<PhoneAccountHandle> phoneAccounts = vvmSourceManager.getOmtpVvmSources();
- for (PhoneAccountHandle phoneAccount : phoneAccounts) {
- if (VisualVoicemailSettingsUtil.isEnabledUserSet(context, phoneAccount)) {
- // Skip the check if this voicemail source's setting is overridden by the user.
- continue;
- }
-
- OmtpVvmCarrierConfigHelper carrierConfigHelper = new OmtpVvmCarrierConfigHelper(
- context, phoneAccount);
- if (carrierConfigHelper.getCarrierVvmPackageNames() == null) {
- continue;
- }
- if (carrierConfigHelper.getCarrierVvmPackageNames().contains(packageName)) {
- // Force deactivate the client. The user can re-enable it in the settings.
- // There are no need to update the settings for deactivation. At this point, if the
- // default value is used it should be false because a carrier package is present.
- VvmLog.i(TAG, "Carrier VVM package installed, disabling system VVM client");
- OmtpVvmSourceManager.getInstance(context).removeSource(phoneAccount);
- carrierConfigHelper.startDeactivation();
- }
- }
- }
-}
diff --git a/java/com/android/voicemailomtp/VvmPhoneStateListener.java b/java/com/android/voicemailomtp/VvmPhoneStateListener.java
deleted file mode 100644
index 1a3013d1f..000000000
--- a/java/com/android/voicemailomtp/VvmPhoneStateListener.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp;
-
-import android.content.Context;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.PhoneStateListener;
-import android.telephony.ServiceState;
-
-import com.android.voicemailomtp.sync.OmtpVvmSourceManager;
-import com.android.voicemailomtp.sync.OmtpVvmSyncService;
-import com.android.voicemailomtp.sync.SyncTask;
-import com.android.voicemailomtp.sync.VoicemailStatusQueryHelper;
-
-/**
- * Check if service is lost and indicate this in the voicemail status.
- */
-public class VvmPhoneStateListener extends PhoneStateListener {
-
- private static final String TAG = "VvmPhoneStateListener";
-
- private PhoneAccountHandle mPhoneAccount;
- private Context mContext;
- private int mPreviousState = -1;
-
- public VvmPhoneStateListener(Context context, PhoneAccountHandle accountHandle) {
- // TODO: b/32637799 too much trouble to call super constructor through reflection,
- // just use non-phoneAccountHandle version for now.
- super();
- mContext = context;
- mPhoneAccount = accountHandle;
- }
-
- @Override
- public void onServiceStateChanged(ServiceState serviceState) {
- if (mPhoneAccount == null) {
- VvmLog.e(TAG, "onServiceStateChanged on phoneAccount " + mPhoneAccount
- + " with invalid phoneAccountHandle, ignoring");
- return;
- }
-
- int state = serviceState.getState();
- if (state == mPreviousState || (state != ServiceState.STATE_IN_SERVICE
- && mPreviousState != ServiceState.STATE_IN_SERVICE)) {
- // Only interested in state changes or transitioning into or out of "in service".
- // Otherwise just quit.
- mPreviousState = state;
- return;
- }
-
- OmtpVvmCarrierConfigHelper helper = new OmtpVvmCarrierConfigHelper(mContext, mPhoneAccount);
-
- if (state == ServiceState.STATE_IN_SERVICE) {
- VoicemailStatusQueryHelper voicemailStatusQueryHelper =
- new VoicemailStatusQueryHelper(mContext);
- if (voicemailStatusQueryHelper.isVoicemailSourceConfigured(mPhoneAccount)) {
- if (!voicemailStatusQueryHelper.isNotificationsChannelActive(mPhoneAccount)) {
- VvmLog
- .v(TAG, "Notifications channel is active for " + mPhoneAccount);
- helper.handleEvent(VoicemailStatus.edit(mContext, mPhoneAccount),
- OmtpEvents.NOTIFICATION_IN_SERVICE);
- }
- }
-
- if (OmtpVvmSourceManager.getInstance(mContext).isVvmSourceRegistered(mPhoneAccount)) {
- VvmLog
- .v(TAG, "Signal returned: requesting resync for " + mPhoneAccount);
- // If the source is already registered, run a full sync in case something was missed
- // while signal was down.
- SyncTask.start(mContext, mPhoneAccount, OmtpVvmSyncService.SYNC_FULL_SYNC);
- } else {
- VvmLog.v(TAG,
- "Signal returned: reattempting activation for " + mPhoneAccount);
- // Otherwise initiate an activation because this means that an OMTP source was
- // recognized but either the activation text was not successfully sent or a response
- // was not received.
- helper.startActivation();
- }
- } else {
- VvmLog.v(TAG, "Notifications channel is inactive for " + mPhoneAccount);
-
- if (!OmtpVvmSourceManager.getInstance(mContext).isVvmSourceRegistered(mPhoneAccount)) {
- return;
- }
- helper.handleEvent(VoicemailStatus.edit(mContext, mPhoneAccount),
- OmtpEvents.NOTIFICATION_SERVICE_LOST);
- }
- mPreviousState = state;
- }
-}
diff --git a/java/com/android/voicemailomtp/fetch/FetchVoicemailReceiver.java b/java/com/android/voicemailomtp/fetch/FetchVoicemailReceiver.java
deleted file mode 100644
index 85fea80d7..000000000
--- a/java/com/android/voicemailomtp/fetch/FetchVoicemailReceiver.java
+++ /dev/null
@@ -1,219 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.fetch;
-
-import android.annotation.TargetApi;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.database.Cursor;
-import android.net.Network;
-import android.net.Uri;
-import android.os.Build.VERSION_CODES;
-import android.provider.VoicemailContract;
-import android.provider.VoicemailContract.Voicemails;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.v4.os.BuildCompat;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.TelephonyManager;
-import android.text.TextUtils;
-import com.android.voicemailomtp.VoicemailStatus;
-import com.android.voicemailomtp.VvmLog;
-import com.android.voicemailomtp.imap.ImapHelper;
-import com.android.voicemailomtp.imap.ImapHelper.InitializingException;
-import com.android.voicemailomtp.sync.OmtpVvmSourceManager;
-import com.android.voicemailomtp.sync.VvmNetworkRequestCallback;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-
-@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
-public class FetchVoicemailReceiver extends BroadcastReceiver {
-
- private static final String TAG = "FetchVoicemailReceiver";
-
- final static String[] PROJECTION = new String[]{
- Voicemails.SOURCE_DATA, // 0
- Voicemails.PHONE_ACCOUNT_ID, // 1
- Voicemails.PHONE_ACCOUNT_COMPONENT_NAME, // 2
- };
-
- public static final int SOURCE_DATA = 0;
- public static final int PHONE_ACCOUNT_ID = 1;
- public static final int PHONE_ACCOUNT_COMPONENT_NAME = 2;
-
- // Number of retries
- private static final int NETWORK_RETRY_COUNT = 3;
-
- private ContentResolver mContentResolver;
- private Uri mUri;
- private VvmNetworkRequestCallback mNetworkCallback;
- private Context mContext;
- private String mUid;
- private PhoneAccountHandle mPhoneAccount;
- private int mRetryCount = NETWORK_RETRY_COUNT;
-
- @Override
- public void onReceive(final Context context, Intent intent) {
- if (VoicemailContract.ACTION_FETCH_VOICEMAIL.equals(intent.getAction())) {
- VvmLog.i(TAG, "ACTION_FETCH_VOICEMAIL received");
- mContext = context;
- mContentResolver = context.getContentResolver();
- mUri = intent.getData();
-
- if (mUri == null) {
- VvmLog.w(TAG,
- VoicemailContract.ACTION_FETCH_VOICEMAIL + " intent sent with no data");
- return;
- }
-
- if (!context.getPackageName().equals(
- mUri.getQueryParameter(VoicemailContract.PARAM_KEY_SOURCE_PACKAGE))) {
- // Ignore if the fetch request is for a voicemail not from this package.
- VvmLog.e(TAG,
- "ACTION_FETCH_VOICEMAIL from foreign pacakge " + context.getPackageName());
- return;
- }
-
- Cursor cursor = mContentResolver.query(mUri, PROJECTION, null, null, null);
- if (cursor == null) {
- VvmLog.i(TAG, "ACTION_FETCH_VOICEMAIL query returned null");
- return;
- }
- try {
- if (cursor.moveToFirst()) {
- mUid = cursor.getString(SOURCE_DATA);
- String accountId = cursor.getString(PHONE_ACCOUNT_ID);
- if (TextUtils.isEmpty(accountId)) {
- TelephonyManager telephonyManager = (TelephonyManager)
- context.getSystemService(Context.TELEPHONY_SERVICE);
- accountId = telephonyManager.getSimSerialNumber();
-
- if (TextUtils.isEmpty(accountId)) {
- VvmLog.e(TAG, "Account null and no default sim found.");
- return;
- }
- }
-
- mPhoneAccount = new PhoneAccountHandle(
- ComponentName.unflattenFromString(
- cursor.getString(PHONE_ACCOUNT_COMPONENT_NAME)),
- cursor.getString(PHONE_ACCOUNT_ID));
- if (!OmtpVvmSourceManager.getInstance(context)
- .isVvmSourceRegistered(mPhoneAccount)) {
- mPhoneAccount = getAccountFromMarshmallowAccount(context, mPhoneAccount);
- if (mPhoneAccount == null) {
- VvmLog.w(TAG, "Account not registered - cannot retrieve message.");
- return;
- }
- VvmLog.i(TAG, "Fetching voicemail with Marshmallow PhoneAccountHandle");
- }
- VvmLog.i(TAG, "Requesting network to fetch voicemail");
- mNetworkCallback = new fetchVoicemailNetworkRequestCallback(context,
- mPhoneAccount);
- mNetworkCallback.requestNetwork();
- }
- } finally {
- cursor.close();
- }
- }
- }
-
- /**
- * In ag/930496 the format of PhoneAccountHandle has changed between Marshmallow and Nougat.
- * This method attempts to search the account from the old database in registered sources using
- * the old format. There's a chance of M phone account collisions on multi-SIM devices, but
- * visual voicemail is not supported on M multi-SIM.
- */
- @Nullable
- private static PhoneAccountHandle getAccountFromMarshmallowAccount(Context context,
- PhoneAccountHandle oldAccount) {
- if (!BuildCompat.isAtLeastN()) {
- return null;
- }
- for (PhoneAccountHandle handle : OmtpVvmSourceManager.getInstance(context)
- .getOmtpVvmSources()) {
- if (getIccSerialNumberFromFullIccSerialNumber(handle.getId())
- .equals(oldAccount.getId())) {
- return handle;
- }
- }
- return null;
- }
-
- /**
- * getIccSerialNumber() is used for ID before N, and getFullIccSerialNumber() after.
- * getIccSerialNumber() stops at the first hex char.
- */
- @NonNull
- private static String getIccSerialNumberFromFullIccSerialNumber(@NonNull String id) {
- for(int i =0;i<id.length();i++){
- if(!Character.isDigit(id.charAt(i))){
- return id.substring(0,i);
- }
- }
- return id;
- }
-
- private class fetchVoicemailNetworkRequestCallback extends VvmNetworkRequestCallback {
-
- public fetchVoicemailNetworkRequestCallback(Context context,
- PhoneAccountHandle phoneAccount) {
- super(context, phoneAccount, VoicemailStatus.edit(context, phoneAccount));
- }
-
- @Override
- public void onAvailable(final Network network) {
- super.onAvailable(network);
- fetchVoicemail(network, getVoicemailStatusEditor());
- }
- }
-
- private void fetchVoicemail(final Network network, final VoicemailStatus.Editor status) {
- Executor executor = Executors.newCachedThreadPool();
- executor.execute(new Runnable() {
- @Override
- public void run() {
- try {
- while (mRetryCount > 0) {
- VvmLog.i(TAG, "fetching voicemail, retry count=" + mRetryCount);
- try (ImapHelper imapHelper = new ImapHelper(mContext, mPhoneAccount,
- network, status)) {
- boolean success = imapHelper.fetchVoicemailPayload(
- new VoicemailFetchedCallback(mContext, mUri, mPhoneAccount),
- mUid);
- if (!success && mRetryCount > 0) {
- VvmLog.i(TAG, "fetch voicemail failed, retrying");
- mRetryCount--;
- } else {
- return;
- }
- } catch (InitializingException e) {
- VvmLog.w(TAG, "Can't retrieve Imap credentials ", e);
- return;
- }
- }
- } finally {
- if (mNetworkCallback != null) {
- mNetworkCallback.releaseNetwork();
- }
- }
- }
- });
- }
-}
diff --git a/java/com/android/voicemailomtp/fetch/VoicemailFetchedCallback.java b/java/com/android/voicemailomtp/fetch/VoicemailFetchedCallback.java
deleted file mode 100644
index 7479c4c4e..000000000
--- a/java/com/android/voicemailomtp/fetch/VoicemailFetchedCallback.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.fetch;
-
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.content.Context;
-import android.net.Uri;
-import android.provider.VoicemailContract.Voicemails;
-import android.support.annotation.Nullable;
-import android.telecom.PhoneAccountHandle;
-import android.telecom.TelecomManager;
-import com.android.voicemailomtp.R;
-import com.android.voicemailomtp.VvmLog;
-import com.android.voicemailomtp.imap.VoicemailPayload;
-import java.io.IOException;
-import java.io.OutputStream;
-import org.apache.commons.io.IOUtils;
-
-/**
- * Callback for when a voicemail payload is fetched. It copies the returned stream to the data
- * file corresponding to the voicemail.
- */
-public class VoicemailFetchedCallback {
- private static final String TAG = "VoicemailFetchedCallback";
-
- private final Context mContext;
- private final ContentResolver mContentResolver;
- private final Uri mUri;
- private final PhoneAccountHandle mPhoneAccountHandle;
-
- public VoicemailFetchedCallback(Context context, Uri uri,
- PhoneAccountHandle phoneAccountHandle) {
- mContext = context;
- mContentResolver = context.getContentResolver();
- mUri = uri;
- mPhoneAccountHandle = phoneAccountHandle;
- }
-
- /**
- * Saves the voicemail payload data into the voicemail provider then sets the "has_content" bit
- * of the voicemail to "1".
- *
- * @param voicemailPayload The object containing the content data for the voicemail
- */
- public void setVoicemailContent(@Nullable VoicemailPayload voicemailPayload) {
- if (voicemailPayload == null) {
- VvmLog.i(TAG, "Payload not found, message has unsupported format");
- ContentValues values = new ContentValues();
- values.put(Voicemails.TRANSCRIPTION,
- mContext.getString(R.string.vvm_unsupported_message_format,
- mContext.getSystemService(TelecomManager.class)
- .getVoiceMailNumber(mPhoneAccountHandle)));
- updateVoicemail(values);
- return;
- }
-
- VvmLog.d(TAG, String.format("Writing new voicemail content: %s", mUri));
- OutputStream outputStream = null;
-
- try {
- outputStream = mContentResolver.openOutputStream(mUri);
- byte[] inputBytes = voicemailPayload.getBytes();
- if (inputBytes != null) {
- outputStream.write(inputBytes);
- }
- } catch (IOException e) {
- VvmLog.w(TAG, String.format("File not found for %s", mUri));
- return;
- } finally {
- IOUtils.closeQuietly(outputStream);
- }
-
- // Update mime_type & has_content after we are done with file update.
- ContentValues values = new ContentValues();
- values.put(Voicemails.MIME_TYPE, voicemailPayload.getMimeType());
- values.put(Voicemails.HAS_CONTENT, true);
- updateVoicemail(values);
- }
-
- private void updateVoicemail(ContentValues values) {
- int updatedCount = mContentResolver.update(mUri, values, null, null);
- if (updatedCount != 1) {
- VvmLog
- .e(TAG, "Updating voicemail should have updated 1 row, was: " + updatedCount);
- }
- }
-}
diff --git a/java/com/android/voicemailomtp/imap/ImapHelper.java b/java/com/android/voicemailomtp/imap/ImapHelper.java
deleted file mode 100644
index b2a40fb64..000000000
--- a/java/com/android/voicemailomtp/imap/ImapHelper.java
+++ /dev/null
@@ -1,711 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.imap;
-
-import android.content.Context;
-import android.net.ConnectivityManager;
-import android.net.Network;
-import android.net.NetworkInfo;
-import android.provider.VoicemailContract;
-import android.telecom.PhoneAccountHandle;
-import android.util.Base64;
-
-import com.android.voicemailomtp.OmtpConstants;
-import com.android.voicemailomtp.OmtpConstants.ChangePinResult;
-import com.android.voicemailomtp.OmtpEvents;
-import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
-import com.android.voicemailomtp.VisualVoicemailPreferences;
-import com.android.voicemailomtp.Voicemail;
-import com.android.voicemailomtp.VoicemailStatus;
-import com.android.voicemailomtp.VvmLog;
-import com.android.voicemailomtp.fetch.VoicemailFetchedCallback;
-import com.android.voicemailomtp.mail.Address;
-import com.android.voicemailomtp.mail.Body;
-import com.android.voicemailomtp.mail.BodyPart;
-import com.android.voicemailomtp.mail.FetchProfile;
-import com.android.voicemailomtp.mail.Flag;
-import com.android.voicemailomtp.mail.Message;
-import com.android.voicemailomtp.mail.MessagingException;
-import com.android.voicemailomtp.mail.Multipart;
-import com.android.voicemailomtp.mail.TempDirectory;
-import com.android.voicemailomtp.mail.internet.MimeMessage;
-import com.android.voicemailomtp.mail.store.ImapConnection;
-import com.android.voicemailomtp.mail.store.ImapFolder;
-import com.android.voicemailomtp.mail.store.ImapStore;
-import com.android.voicemailomtp.mail.store.imap.ImapConstants;
-import com.android.voicemailomtp.mail.store.imap.ImapResponse;
-import com.android.voicemailomtp.mail.utils.LogUtils;
-import com.android.voicemailomtp.sync.OmtpVvmSyncService.TranscriptionFetchedCallback;
-
-import java.io.BufferedOutputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.Closeable;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Locale;
-import org.apache.commons.io.IOUtils;
-
-/**
- * A helper interface to abstract commands sent across IMAP interface for a given account.
- */
-public class ImapHelper implements Closeable {
-
- private static final String TAG = "ImapHelper";
-
- private ImapFolder mFolder;
- private ImapStore mImapStore;
-
- private final Context mContext;
- private final PhoneAccountHandle mPhoneAccount;
- private final Network mNetwork;
- private final VoicemailStatus.Editor mStatus;
-
- VisualVoicemailPreferences mPrefs;
- private static final String PREF_KEY_QUOTA_OCCUPIED = "quota_occupied_";
- private static final String PREF_KEY_QUOTA_TOTAL = "quota_total_";
-
- private int mQuotaOccupied;
- private int mQuotaTotal;
-
- private final OmtpVvmCarrierConfigHelper mConfig;
-
- public class InitializingException extends Exception {
-
- public InitializingException(String message) {
- super(message);
- }
- }
-
- public ImapHelper(Context context, PhoneAccountHandle phoneAccount, Network network,
- VoicemailStatus.Editor status)
- throws InitializingException {
- this(context,
- new OmtpVvmCarrierConfigHelper(
- context,
- phoneAccount),
- phoneAccount,
- network,
- status);
- }
-
- public ImapHelper(Context context, OmtpVvmCarrierConfigHelper config,
- PhoneAccountHandle phoneAccount, Network network, VoicemailStatus.Editor status)
- throws InitializingException {
- mContext = context;
- mPhoneAccount = phoneAccount;
- mNetwork = network;
- mStatus = status;
- mConfig = config;
- mPrefs = new VisualVoicemailPreferences(context,
- phoneAccount);
-
- try {
- TempDirectory.setTempDirectory(context);
-
- String username = mPrefs.getString(OmtpConstants.IMAP_USER_NAME, null);
- String password = mPrefs.getString(OmtpConstants.IMAP_PASSWORD, null);
- String serverName = mPrefs.getString(OmtpConstants.SERVER_ADDRESS, null);
- int port = Integer.parseInt(
- mPrefs.getString(OmtpConstants.IMAP_PORT, null));
- int auth = ImapStore.FLAG_NONE;
-
- int sslPort = mConfig.getSslPort();
- if (sslPort != 0) {
- port = sslPort;
- auth = ImapStore.FLAG_SSL;
- }
-
- mImapStore = new ImapStore(
- context, this, username, password, port, serverName, auth, network);
- } catch (NumberFormatException e) {
- handleEvent(OmtpEvents.DATA_INVALID_PORT);
- LogUtils.w(TAG, "Could not parse port number");
- throw new InitializingException("cannot initialize ImapHelper:" + e.toString());
- }
-
- mQuotaOccupied = mPrefs
- .getInt(PREF_KEY_QUOTA_OCCUPIED, VoicemailContract.Status.QUOTA_UNAVAILABLE);
- mQuotaTotal = mPrefs
- .getInt(PREF_KEY_QUOTA_TOTAL, VoicemailContract.Status.QUOTA_UNAVAILABLE);
- }
-
- @Override
- public void close() {
- mImapStore.closeConnection();
- }
-
- public boolean isRoaming() {
- ConnectivityManager connectivityManager = (ConnectivityManager) mContext.getSystemService(
- Context.CONNECTIVITY_SERVICE);
- NetworkInfo info = connectivityManager.getNetworkInfo(mNetwork);
- if (info == null) {
- return false;
- }
- return info.isRoaming();
- }
-
- public OmtpVvmCarrierConfigHelper getConfig() {
- return mConfig;
- }
-
- public ImapConnection connect() {
- return mImapStore.getConnection();
- }
-
- /**
- * The caller thread will block until the method returns.
- */
- public boolean markMessagesAsRead(List<Voicemail> voicemails) {
- return setFlags(voicemails, Flag.SEEN);
- }
-
- /**
- * The caller thread will block until the method returns.
- */
- public boolean markMessagesAsDeleted(List<Voicemail> voicemails) {
- return setFlags(voicemails, Flag.DELETED);
- }
-
- public void handleEvent(OmtpEvents event) {
- mConfig.handleEvent(mStatus, event);
- }
-
- /**
- * Set flags on the server for a given set of voicemails.
- *
- * @param voicemails The voicemails to set flags for.
- * @param flags The flags to set on the voicemails.
- * @return {@code true} if the operation completes successfully, {@code false} otherwise.
- */
- private boolean setFlags(List<Voicemail> voicemails, String... flags) {
- if (voicemails.size() == 0) {
- return false;
- }
- try {
- mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
- if (mFolder != null) {
- mFolder.setFlags(convertToImapMessages(voicemails), flags, true);
- return true;
- }
- return false;
- } catch (MessagingException e) {
- LogUtils.e(TAG, e, "Messaging exception");
- return false;
- } finally {
- closeImapFolder();
- }
- }
-
- /**
- * Fetch a list of voicemails from the server.
- *
- * @return A list of voicemail objects containing data about voicemails stored on the server.
- */
- public List<Voicemail> fetchAllVoicemails() {
- List<Voicemail> result = new ArrayList<Voicemail>();
- Message[] messages;
- try {
- mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
- if (mFolder == null) {
- // This means we were unable to successfully open the folder.
- return null;
- }
-
- // This method retrieves lightweight messages containing only the uid of the message.
- messages = mFolder.getMessages(null);
-
- for (Message message : messages) {
- // Get the voicemail details (message structure).
- MessageStructureWrapper messageStructureWrapper = fetchMessageStructure(message);
- if (messageStructureWrapper != null) {
- result.add(getVoicemailFromMessageStructure(messageStructureWrapper));
- }
- }
- return result;
- } catch (MessagingException e) {
- LogUtils.e(TAG, e, "Messaging Exception");
- return null;
- } finally {
- closeImapFolder();
- }
- }
-
- /**
- * Extract voicemail details from the message structure. Also fetch transcription if a
- * transcription exists.
- */
- private Voicemail getVoicemailFromMessageStructure(
- MessageStructureWrapper messageStructureWrapper) throws MessagingException {
- Message messageDetails = messageStructureWrapper.messageStructure;
-
- TranscriptionFetchedListener listener = new TranscriptionFetchedListener();
- if (messageStructureWrapper.transcriptionBodyPart != null) {
- FetchProfile fetchProfile = new FetchProfile();
- fetchProfile.add(messageStructureWrapper.transcriptionBodyPart);
-
- mFolder.fetch(new Message[]{messageDetails}, fetchProfile, listener);
- }
-
- // Found an audio attachment, this is a valid voicemail.
- long time = messageDetails.getSentDate().getTime();
- String number = getNumber(messageDetails.getFrom());
- boolean isRead = Arrays.asList(messageDetails.getFlags()).contains(Flag.SEEN);
- return Voicemail.createForInsertion(time, number)
- .setPhoneAccount(mPhoneAccount)
- .setSourcePackage(mContext.getPackageName())
- .setSourceData(messageDetails.getUid())
- .setIsRead(isRead)
- .setTranscription(listener.getVoicemailTranscription())
- .build();
- }
-
- /**
- * The "from" field of a visual voicemail IMAP message is the number of the caller who left the
- * message. Extract this number from the list of "from" addresses.
- *
- * @param fromAddresses A list of addresses that comprise the "from" line.
- * @return The number of the voicemail sender.
- */
- private String getNumber(Address[] fromAddresses) {
- if (fromAddresses != null && fromAddresses.length > 0) {
- if (fromAddresses.length != 1) {
- LogUtils.w(TAG, "More than one from addresses found. Using the first one.");
- }
- String sender = fromAddresses[0].getAddress();
- int atPos = sender.indexOf('@');
- if (atPos != -1) {
- // Strip domain part of the address.
- sender = sender.substring(0, atPos);
- }
- return sender;
- }
- return null;
- }
-
- /**
- * Fetches the structure of the given message and returns a wrapper containing the message
- * structure and the transcription structure (if applicable).
- *
- * @throws MessagingException if fetching the structure of the message fails
- */
- private MessageStructureWrapper fetchMessageStructure(Message message)
- throws MessagingException {
- LogUtils.d(TAG, "Fetching message structure for " + message.getUid());
-
- MessageStructureFetchedListener listener = new MessageStructureFetchedListener();
-
- FetchProfile fetchProfile = new FetchProfile();
- fetchProfile.addAll(Arrays.asList(FetchProfile.Item.FLAGS, FetchProfile.Item.ENVELOPE,
- FetchProfile.Item.STRUCTURE));
-
- // The IMAP folder fetch method will call "messageRetrieved" on the listener when the
- // message is successfully retrieved.
- mFolder.fetch(new Message[]{message}, fetchProfile, listener);
- return listener.getMessageStructure();
- }
-
- public boolean fetchVoicemailPayload(VoicemailFetchedCallback callback, final String uid) {
- try {
- mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
- if (mFolder == null) {
- // This means we were unable to successfully open the folder.
- return false;
- }
- Message message = mFolder.getMessage(uid);
- if (message == null) {
- return false;
- }
- VoicemailPayload voicemailPayload = fetchVoicemailPayload(message);
- callback.setVoicemailContent(voicemailPayload);
- return true;
- } catch (MessagingException e) {
- } finally {
- closeImapFolder();
- }
- return false;
- }
-
- /**
- * Fetches the body of the given message and returns the parsed voicemail payload.
- *
- * @throws MessagingException if fetching the body of the message fails
- */
- private VoicemailPayload fetchVoicemailPayload(Message message)
- throws MessagingException {
- LogUtils.d(TAG, "Fetching message body for " + message.getUid());
-
- MessageBodyFetchedListener listener = new MessageBodyFetchedListener();
-
- FetchProfile fetchProfile = new FetchProfile();
- fetchProfile.add(FetchProfile.Item.BODY);
-
- mFolder.fetch(new Message[]{message}, fetchProfile, listener);
- return listener.getVoicemailPayload();
- }
-
- public boolean fetchTranscription(TranscriptionFetchedCallback callback, String uid) {
- try {
- mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
- if (mFolder == null) {
- // This means we were unable to successfully open the folder.
- return false;
- }
-
- Message message = mFolder.getMessage(uid);
- if (message == null) {
- return false;
- }
-
- MessageStructureWrapper messageStructureWrapper = fetchMessageStructure(message);
- if (messageStructureWrapper != null) {
- TranscriptionFetchedListener listener = new TranscriptionFetchedListener();
- if (messageStructureWrapper.transcriptionBodyPart != null) {
- FetchProfile fetchProfile = new FetchProfile();
- fetchProfile.add(messageStructureWrapper.transcriptionBodyPart);
-
- // This method is called synchronously so the transcription will be populated
- // in the listener once the next method is called.
- mFolder.fetch(new Message[]{message}, fetchProfile, listener);
- callback.setVoicemailTranscription(listener.getVoicemailTranscription());
- }
- }
- return true;
- } catch (MessagingException e) {
- LogUtils.e(TAG, e, "Messaging Exception");
- return false;
- } finally {
- closeImapFolder();
- }
- }
-
-
- @ChangePinResult
- public int changePin(String oldPin, String newPin)
- throws MessagingException {
- ImapConnection connection = mImapStore.getConnection();
- try {
- String command = getConfig().getProtocol()
- .getCommand(OmtpConstants.IMAP_CHANGE_TUI_PWD_FORMAT);
- connection.sendCommand(
- String.format(Locale.US, command, newPin, oldPin), true);
- return getChangePinResultFromImapResponse(connection.readResponse());
- } catch (IOException ioe) {
- VvmLog.e(TAG, "changePin: ", ioe);
- return OmtpConstants.CHANGE_PIN_SYSTEM_ERROR;
- } finally {
- connection.destroyResponses();
- }
- }
-
- public void changeVoicemailTuiLanguage(String languageCode)
- throws MessagingException {
- ImapConnection connection = mImapStore.getConnection();
- try {
- String command = getConfig().getProtocol()
- .getCommand(OmtpConstants.IMAP_CHANGE_VM_LANG_FORMAT);
- connection.sendCommand(
- String.format(Locale.US, command, languageCode), true);
- } catch (IOException ioe) {
- LogUtils.e(TAG, ioe.toString());
- } finally {
- connection.destroyResponses();
- }
- }
-
- public void closeNewUserTutorial() throws MessagingException {
- ImapConnection connection = mImapStore.getConnection();
- try {
- String command = getConfig().getProtocol()
- .getCommand(OmtpConstants.IMAP_CLOSE_NUT);
- connection.executeSimpleCommand(command, false);
- } catch (IOException ioe) {
- throw new MessagingException(MessagingException.SERVER_ERROR, ioe.toString());
- } finally {
- connection.destroyResponses();
- }
- }
-
- @ChangePinResult
- private static int getChangePinResultFromImapResponse(ImapResponse response)
- throws MessagingException {
- if (!response.isTagged()) {
- throw new MessagingException(MessagingException.SERVER_ERROR,
- "tagged response expected");
- }
- if (!response.isOk()) {
- String message = response.getStringOrEmpty(1).getString();
- LogUtils.d(TAG, "change PIN failed: " + message);
- if (OmtpConstants.RESPONSE_CHANGE_PIN_TOO_SHORT.equals(message)) {
- return OmtpConstants.CHANGE_PIN_TOO_SHORT;
- }
- if (OmtpConstants.RESPONSE_CHANGE_PIN_TOO_LONG.equals(message)) {
- return OmtpConstants.CHANGE_PIN_TOO_LONG;
- }
- if (OmtpConstants.RESPONSE_CHANGE_PIN_TOO_WEAK.equals(message)) {
- return OmtpConstants.CHANGE_PIN_TOO_WEAK;
- }
- if (OmtpConstants.RESPONSE_CHANGE_PIN_MISMATCH.equals(message)) {
- return OmtpConstants.CHANGE_PIN_MISMATCH;
- }
- if (OmtpConstants.RESPONSE_CHANGE_PIN_INVALID_CHARACTER.equals(message)) {
- return OmtpConstants.CHANGE_PIN_INVALID_CHARACTER;
- }
- return OmtpConstants.CHANGE_PIN_SYSTEM_ERROR;
- }
- LogUtils.d(TAG, "change PIN succeeded");
- return OmtpConstants.CHANGE_PIN_SUCCESS;
- }
-
- public void updateQuota() {
- try {
- mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
- if (mFolder == null) {
- // This means we were unable to successfully open the folder.
- return;
- }
- updateQuota(mFolder);
- } catch (MessagingException e) {
- LogUtils.e(TAG, e, "Messaging Exception");
- } finally {
- closeImapFolder();
- }
- }
-
- private void updateQuota(ImapFolder folder) throws MessagingException {
- setQuota(folder.getQuota());
- }
-
- private void setQuota(ImapFolder.Quota quota) {
- if (quota == null) {
- return;
- }
- if (quota.occupied == mQuotaOccupied && quota.total == mQuotaTotal) {
- VvmLog.v(TAG, "Quota hasn't changed");
- return;
- }
- mQuotaOccupied = quota.occupied;
- mQuotaTotal = quota.total;
- VoicemailStatus.edit(mContext, mPhoneAccount)
- .setQuota(mQuotaOccupied, mQuotaTotal)
- .apply();
- mPrefs.edit()
- .putInt(PREF_KEY_QUOTA_OCCUPIED, mQuotaOccupied)
- .putInt(PREF_KEY_QUOTA_TOTAL, mQuotaTotal)
- .apply();
- VvmLog.v(TAG, "Quota changed to " + mQuotaOccupied + "/" + mQuotaTotal);
- }
-
- /**
- * A wrapper to hold a message with its header details and the structure for transcriptions (so
- * they can be fetched in the future).
- */
- public class MessageStructureWrapper {
-
- public Message messageStructure;
- public BodyPart transcriptionBodyPart;
-
- public MessageStructureWrapper() {
- }
- }
-
- /**
- * Listener for the message structure being fetched.
- */
- private final class MessageStructureFetchedListener
- implements ImapFolder.MessageRetrievalListener {
-
- private MessageStructureWrapper mMessageStructure;
-
- public MessageStructureFetchedListener() {
- }
-
- public MessageStructureWrapper getMessageStructure() {
- return mMessageStructure;
- }
-
- @Override
- public void messageRetrieved(Message message) {
- LogUtils.d(TAG, "Fetched message structure for " + message.getUid());
- LogUtils.d(TAG, "Message retrieved: " + message);
- try {
- mMessageStructure = getMessageOrNull(message);
- if (mMessageStructure == null) {
- LogUtils.d(TAG, "This voicemail does not have an attachment...");
- return;
- }
- } catch (MessagingException e) {
- LogUtils.e(TAG, e, "Messaging Exception");
- closeImapFolder();
- }
- }
-
- /**
- * Check if this IMAP message is a valid voicemail and whether it contains a transcription.
- *
- * @param message The IMAP message.
- * @return The MessageStructureWrapper object corresponding to an IMAP message and
- * transcription.
- */
- private MessageStructureWrapper getMessageOrNull(Message message)
- throws MessagingException {
- if (!message.getMimeType().startsWith("multipart/")) {
- LogUtils.w(TAG, "Ignored non multi-part message");
- return null;
- }
-
- MessageStructureWrapper messageStructureWrapper = new MessageStructureWrapper();
-
- Multipart multipart = (Multipart) message.getBody();
- for (int i = 0; i < multipart.getCount(); ++i) {
- BodyPart bodyPart = multipart.getBodyPart(i);
- String bodyPartMimeType = bodyPart.getMimeType().toLowerCase();
- LogUtils.d(TAG, "bodyPart mime type: " + bodyPartMimeType);
-
- if (bodyPartMimeType.startsWith("audio/")) {
- messageStructureWrapper.messageStructure = message;
- } else if (bodyPartMimeType.startsWith("text/")) {
- messageStructureWrapper.transcriptionBodyPart = bodyPart;
- } else {
- VvmLog.v(TAG, "Unknown bodyPart MIME: " + bodyPartMimeType);
- }
- }
-
- if (messageStructureWrapper.messageStructure != null) {
- return messageStructureWrapper;
- }
-
- // No attachment found, this is not a voicemail.
- return null;
- }
- }
-
- /**
- * Listener for the message body being fetched.
- */
- private final class MessageBodyFetchedListener implements ImapFolder.MessageRetrievalListener {
-
- private VoicemailPayload mVoicemailPayload;
-
- /**
- * Returns the fetch voicemail payload.
- */
- public VoicemailPayload getVoicemailPayload() {
- return mVoicemailPayload;
- }
-
- @Override
- public void messageRetrieved(Message message) {
- LogUtils.d(TAG, "Fetched message body for " + message.getUid());
- LogUtils.d(TAG, "Message retrieved: " + message);
- try {
- mVoicemailPayload = getVoicemailPayloadFromMessage(message);
- } catch (MessagingException e) {
- LogUtils.e(TAG, "Messaging Exception:", e);
- } catch (IOException e) {
- LogUtils.e(TAG, "IO Exception:", e);
- }
- }
-
- private VoicemailPayload getVoicemailPayloadFromMessage(Message message)
- throws MessagingException, IOException {
- Multipart multipart = (Multipart) message.getBody();
- List<String> mimeTypes = new ArrayList<>();
- for (int i = 0; i < multipart.getCount(); ++i) {
- BodyPart bodyPart = multipart.getBodyPart(i);
- String bodyPartMimeType = bodyPart.getMimeType().toLowerCase();
- mimeTypes.add(bodyPartMimeType);
- if (bodyPartMimeType.startsWith("audio/")) {
- byte[] bytes = getDataFromBody(bodyPart.getBody());
- LogUtils.d(TAG, String.format("Fetched %s bytes of data", bytes.length));
- return new VoicemailPayload(bodyPartMimeType, bytes);
- }
- }
- LogUtils.e(TAG, "No audio attachment found on this voicemail, mimeTypes:" + mimeTypes);
- return null;
- }
- }
-
- /**
- * Listener for the transcription being fetched.
- */
- private final class TranscriptionFetchedListener implements
- ImapFolder.MessageRetrievalListener {
-
- private String mVoicemailTranscription;
-
- /**
- * Returns the fetched voicemail transcription.
- */
- public String getVoicemailTranscription() {
- return mVoicemailTranscription;
- }
-
- @Override
- public void messageRetrieved(Message message) {
- LogUtils.d(TAG, "Fetched transcription for " + message.getUid());
- try {
- mVoicemailTranscription = new String(getDataFromBody(message.getBody()));
- } catch (MessagingException e) {
- LogUtils.e(TAG, "Messaging Exception:", e);
- } catch (IOException e) {
- LogUtils.e(TAG, "IO Exception:", e);
- }
- }
- }
-
- private ImapFolder openImapFolder(String modeReadWrite) {
- try {
- if (mImapStore == null) {
- return null;
- }
- ImapFolder folder = new ImapFolder(mImapStore, ImapConstants.INBOX);
- folder.open(modeReadWrite);
- return folder;
- } catch (MessagingException e) {
- LogUtils.e(TAG, e, "Messaging Exception");
- }
- return null;
- }
-
- private Message[] convertToImapMessages(List<Voicemail> voicemails) {
- Message[] messages = new Message[voicemails.size()];
- for (int i = 0; i < voicemails.size(); ++i) {
- messages[i] = new MimeMessage();
- messages[i].setUid(voicemails.get(i).getSourceData());
- }
- return messages;
- }
-
- private void closeImapFolder() {
- if (mFolder != null) {
- mFolder.close(true);
- }
- }
-
- private byte[] getDataFromBody(Body body) throws IOException, MessagingException {
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- BufferedOutputStream bufferedOut = new BufferedOutputStream(out);
- try {
- body.writeTo(bufferedOut);
- return Base64.decode(out.toByteArray(), Base64.DEFAULT);
- } finally {
- IOUtils.closeQuietly(bufferedOut);
- IOUtils.closeQuietly(out);
- }
- }
-} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/Address.java b/java/com/android/voicemailomtp/mail/Address.java
deleted file mode 100644
index ed3f44c03..000000000
--- a/java/com/android/voicemailomtp/mail/Address.java
+++ /dev/null
@@ -1,541 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail;
-
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.support.annotation.VisibleForTesting;
-import android.text.Html;
-import android.text.TextUtils;
-import android.text.util.Rfc822Token;
-import android.text.util.Rfc822Tokenizer;
-import com.android.voicemailomtp.mail.utils.LogUtils;
-import java.util.ArrayList;
-import java.util.regex.Pattern;
-import org.apache.james.mime4j.codec.EncoderUtil;
-import org.apache.james.mime4j.decoder.DecoderUtil;
-
-/**
- * This class represent email address.
- *
- * RFC822 email address may have following format.
- * "name" <address> (comment)
- * "name" <address>
- * name <address>
- * address
- * Name and comment part should be MIME/base64 encoded in header if necessary.
- *
- */
-public class Address implements Parcelable {
- public static final String ADDRESS_DELIMETER = ",";
- /**
- * Address part, in the form local_part@domain_part. No surrounding angle brackets.
- */
- private String mAddress;
-
- /**
- * Name part. No surrounding double quote, and no MIME/base64 encoding.
- * This must be null if Address has no name part.
- */
- private String mPersonal;
-
- /**
- * When personal is set, it will return the first token of the personal
- * string. Otherwise, it will return the e-mail address up to the '@' sign.
- */
- private String mSimplifiedName;
-
- // Regex that matches address surrounded by '<>' optionally. '^<?([^>]+)>?$'
- private static final Pattern REMOVE_OPTIONAL_BRACKET = Pattern.compile("^<?([^>]+)>?$");
- // Regex that matches personal name surrounded by '""' optionally. '^"?([^"]+)"?$'
- private static final Pattern REMOVE_OPTIONAL_DQUOTE = Pattern.compile("^\"?([^\"]*)\"?$");
- // Regex that matches escaped character '\\([\\"])'
- private static final Pattern UNQUOTE = Pattern.compile("\\\\([\\\\\"])");
-
- // TODO: LOCAL_PART and DOMAIN_PART_PART are too permissive and can be improved.
- // TODO: Fix this to better constrain comments.
- /** Regex for the local part of an email address. */
- private static final String LOCAL_PART = "[^@]+";
- /** Regex for each part of the domain part, i.e. the thing between the dots. */
- private static final String DOMAIN_PART_PART = "[[\\w][\\d]\\-\\(\\)\\[\\]]+";
- /** Regex for the domain part, which is two or more {@link #DOMAIN_PART_PART} separated by . */
- private static final String DOMAIN_PART =
- "(" + DOMAIN_PART_PART + "\\.)+" + DOMAIN_PART_PART;
-
- /** Pattern to check if an email address is valid. */
- private static final Pattern EMAIL_ADDRESS =
- Pattern.compile("\\A" + LOCAL_PART + "@" + DOMAIN_PART + "\\z");
-
- private static final Address[] EMPTY_ADDRESS_ARRAY = new Address[0];
-
- // delimiters are chars that do not appear in an email address, used by fromHeader
- private static final char LIST_DELIMITER_EMAIL = '\1';
- private static final char LIST_DELIMITER_PERSONAL = '\2';
-
- private static final String LOG_TAG = "Email Address";
-
- @VisibleForTesting
- public Address(String address) {
- setAddress(address);
- }
-
- public Address(String address, String personal) {
- setPersonal(personal);
- setAddress(address);
- }
-
- /**
- * Returns a simplified string for this e-mail address.
- * When a name is known, it will return the first token of that name. Otherwise, it will
- * return the e-mail address up to the '@' sign.
- */
- public String getSimplifiedName() {
- if (mSimplifiedName == null) {
- if (TextUtils.isEmpty(mPersonal) && !TextUtils.isEmpty(mAddress)) {
- int atSign = mAddress.indexOf('@');
- mSimplifiedName = (atSign != -1) ? mAddress.substring(0, atSign) : "";
- } else if (!TextUtils.isEmpty(mPersonal)) {
-
- // TODO: use Contacts' NameSplitter for more reliable first-name extraction
-
- int end = mPersonal.indexOf(' ');
- while (end > 0 && mPersonal.charAt(end - 1) == ',') {
- end--;
- }
- mSimplifiedName = (end < 1) ? mPersonal : mPersonal.substring(0, end);
-
- } else {
- LogUtils.w(LOG_TAG, "Unable to get a simplified name");
- mSimplifiedName = "";
- }
- }
- return mSimplifiedName;
- }
-
- public static synchronized Address getEmailAddress(String rawAddress) {
- if (TextUtils.isEmpty(rawAddress)) {
- return null;
- }
- String name, address;
- final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(rawAddress);
- if (tokens.length > 0) {
- final String tokenizedName = tokens[0].getName();
- name = tokenizedName != null ? Html.fromHtml(tokenizedName.trim()).toString()
- : "";
- address = Html.fromHtml(tokens[0].getAddress()).toString();
- } else {
- name = "";
- address = rawAddress == null ?
- "" : Html.fromHtml(rawAddress).toString();
- }
- return new Address(address, name);
- }
-
- public String getAddress() {
- return mAddress;
- }
-
- public void setAddress(String address) {
- mAddress = REMOVE_OPTIONAL_BRACKET.matcher(address).replaceAll("$1");
- }
-
- /**
- * Get name part as UTF-16 string. No surrounding double quote, and no MIME/base64 encoding.
- *
- * @return Name part of email address. Returns null if it is omitted.
- */
- public String getPersonal() {
- return mPersonal;
- }
-
- /**
- * Set personal part from UTF-16 string. Optional surrounding double quote will be removed.
- * It will be also unquoted and MIME/base64 decoded.
- *
- * @param personal name part of email address as UTF-16 string. Null is acceptable.
- */
- public void setPersonal(String personal) {
- mPersonal = decodeAddressPersonal(personal);
- }
-
- /**
- * Decodes name from UTF-16 string. Optional surrounding double quote will be removed.
- * It will be also unquoted and MIME/base64 decoded.
- *
- * @param personal name part of email address as UTF-16 string. Null is acceptable.
- */
- public static String decodeAddressPersonal(String personal) {
- if (personal != null) {
- personal = REMOVE_OPTIONAL_DQUOTE.matcher(personal).replaceAll("$1");
- personal = UNQUOTE.matcher(personal).replaceAll("$1");
- personal = DecoderUtil.decodeEncodedWords(personal);
- if (personal.length() == 0) {
- personal = null;
- }
- }
- return personal;
- }
-
- /**
- * This method is used to check that all the addresses that the user
- * entered in a list (e.g. To:) are valid, so that none is dropped.
- */
- @VisibleForTesting
- public static boolean isAllValid(String addressList) {
- // This code mimics the parse() method below.
- // I don't know how to better avoid the code-duplication.
- if (addressList != null && addressList.length() > 0) {
- Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
- for (int i = 0, length = tokens.length; i < length; ++i) {
- Rfc822Token token = tokens[i];
- String address = token.getAddress();
- if (!TextUtils.isEmpty(address) && !isValidAddress(address)) {
- return false;
- }
- }
- }
- return true;
- }
-
- /**
- * Parse a comma-delimited list of addresses in RFC822 format and return an
- * array of Address objects.
- *
- * @param addressList Address list in comma-delimited string.
- * @return An array of 0 or more Addresses.
- */
- public static Address[] parse(String addressList) {
- if (addressList == null || addressList.length() == 0) {
- return EMPTY_ADDRESS_ARRAY;
- }
- Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
- ArrayList<Address> addresses = new ArrayList<Address>();
- for (int i = 0, length = tokens.length; i < length; ++i) {
- Rfc822Token token = tokens[i];
- String address = token.getAddress();
- if (!TextUtils.isEmpty(address)) {
- if (isValidAddress(address)) {
- String name = token.getName();
- if (TextUtils.isEmpty(name)) {
- name = null;
- }
- addresses.add(new Address(address, name));
- }
- }
- }
- return addresses.toArray(new Address[addresses.size()]);
- }
-
- /**
- * Checks whether a string email address is valid.
- * E.g. name@domain.com is valid.
- */
- @VisibleForTesting
- static boolean isValidAddress(final String address) {
- return EMAIL_ADDRESS.matcher(address).find();
- }
-
- @Override
- public boolean equals(Object o) {
- if (o instanceof Address) {
- // It seems that the spec says that the "user" part is case-sensitive,
- // while the domain part in case-insesitive.
- // So foo@yahoo.com and Foo@yahoo.com are different.
- // This may seem non-intuitive from the user POV, so we
- // may re-consider it if it creates UI trouble.
- // A problem case is "replyAll" sending to both
- // a@b.c and to A@b.c, which turn out to be the same on the server.
- // Leave unchanged for now (i.e. case-sensitive).
- return getAddress().equals(((Address) o).getAddress());
- }
- return super.equals(o);
- }
-
- @Override
- public int hashCode() {
- return getAddress().hashCode();
- }
-
- /**
- * Get human readable address string.
- * Do not use this for email header.
- *
- * @return Human readable address string. Not quoted and not encoded.
- */
- @Override
- public String toString() {
- if (mPersonal != null && !mPersonal.equals(mAddress)) {
- if (mPersonal.matches(".*[\\(\\)<>@,;:\\\\\".\\[\\]].*")) {
- return ensureQuotedString(mPersonal) + " <" + mAddress + ">";
- } else {
- return mPersonal + " <" + mAddress + ">";
- }
- } else {
- return mAddress;
- }
- }
-
- /**
- * Ensures that the given string starts and ends with the double quote character. The string is
- * not modified in any way except to add the double quote character to start and end if it's not
- * already there.
- *
- * sample -> "sample"
- * "sample" -> "sample"
- * ""sample"" -> "sample"
- * "sample"" -> "sample"
- * sa"mp"le -> "sa"mp"le"
- * "sa"mp"le" -> "sa"mp"le"
- * (empty string) -> ""
- * " -> ""
- */
- private static String ensureQuotedString(String s) {
- if (s == null) {
- return null;
- }
- if (!s.matches("^\".*\"$")) {
- return "\"" + s + "\"";
- } else {
- return s;
- }
- }
-
- /**
- * Get human readable comma-delimited address string.
- *
- * @param addresses Address array
- * @return Human readable comma-delimited address string.
- */
- @VisibleForTesting
- public static String toString(Address[] addresses) {
- return toString(addresses, ADDRESS_DELIMETER);
- }
-
- /**
- * Get human readable address strings joined with the specified separator.
- *
- * @param addresses Address array
- * @param separator Separator
- * @return Human readable comma-delimited address string.
- */
- public static String toString(Address[] addresses, String separator) {
- if (addresses == null || addresses.length == 0) {
- return null;
- }
- if (addresses.length == 1) {
- return addresses[0].toString();
- }
- StringBuilder sb = new StringBuilder(addresses[0].toString());
- for (int i = 1; i < addresses.length; i++) {
- sb.append(separator);
- // TODO: investigate why this .trim() is needed.
- sb.append(addresses[i].toString().trim());
- }
- return sb.toString();
- }
-
- /**
- * Get RFC822/MIME compatible address string.
- *
- * @return RFC822/MIME compatible address string.
- * It may be surrounded by double quote or quoted and MIME/base64 encoded if necessary.
- */
- public String toHeader() {
- if (mPersonal != null) {
- return EncoderUtil.encodeAddressDisplayName(mPersonal) + " <" + mAddress + ">";
- } else {
- return mAddress;
- }
- }
-
- /**
- * Get RFC822/MIME compatible comma-delimited address string.
- *
- * @param addresses Address array
- * @return RFC822/MIME compatible comma-delimited address string.
- * it may be surrounded by double quoted or quoted and MIME/base64 encoded if necessary.
- */
- public static String toHeader(Address[] addresses) {
- if (addresses == null || addresses.length == 0) {
- return null;
- }
- if (addresses.length == 1) {
- return addresses[0].toHeader();
- }
- StringBuilder sb = new StringBuilder(addresses[0].toHeader());
- for (int i = 1; i < addresses.length; i++) {
- // We need space character to be able to fold line.
- sb.append(", ");
- sb.append(addresses[i].toHeader());
- }
- return sb.toString();
- }
-
- /**
- * Get Human friendly address string.
- *
- * @return the personal part of this Address, or the address part if the
- * personal part is not available
- */
- @VisibleForTesting
- public String toFriendly() {
- if (mPersonal != null && mPersonal.length() > 0) {
- return mPersonal;
- } else {
- return mAddress;
- }
- }
-
- /**
- * Creates a comma-delimited list of addresses in the "friendly" format (see toFriendly() for
- * details on the per-address conversion).
- *
- * @param addresses Array of Address[] values
- * @return A comma-delimited string listing all of the addresses supplied. Null if source
- * was null or empty.
- */
- @VisibleForTesting
- public static String toFriendly(Address[] addresses) {
- if (addresses == null || addresses.length == 0) {
- return null;
- }
- if (addresses.length == 1) {
- return addresses[0].toFriendly();
- }
- StringBuilder sb = new StringBuilder(addresses[0].toFriendly());
- for (int i = 1; i < addresses.length; i++) {
- sb.append(", ");
- sb.append(addresses[i].toFriendly());
- }
- return sb.toString();
- }
-
- /**
- * Returns exactly the same result as Address.toString(Address.fromHeader(addressList)).
- */
- @VisibleForTesting
- public static String fromHeaderToString(String addressList) {
- return toString(fromHeader(addressList));
- }
-
- /**
- * Returns exactly the same result as Address.toHeader(Address.parse(addressList)).
- */
- @VisibleForTesting
- public static String parseToHeader(String addressList) {
- return Address.toHeader(Address.parse(addressList));
- }
-
- /**
- * Returns null if the addressList has 0 addresses, otherwise returns the first address.
- * The same as Address.fromHeader(addressList)[0] for non-empty list.
- * This is an utility method that offers some performance optimization opportunities.
- */
- @VisibleForTesting
- public static Address firstAddress(String addressList) {
- Address[] array = fromHeader(addressList);
- return array.length > 0 ? array[0] : null;
- }
-
- /**
- * This method exists to convert an address list formatted in a deprecated legacy format to the
- * standard RFC822 header format. {@link #fromHeader(String)} is capable of reading the legacy
- * format and the RFC822 format. {@link #toHeader()} always produces the RFC822 format.
- *
- * This implementation is brute-force, and could be replaced with a more efficient version
- * if desired.
- */
- public static String reformatToHeader(String addressList) {
- return toHeader(fromHeader(addressList));
- }
-
- /**
- * @param addressList a CSV of RFC822 addresses or the deprecated legacy string format
- * @return array of addresses parsed from <code>addressList</code>
- */
- @VisibleForTesting
- public static Address[] fromHeader(String addressList) {
- if (addressList == null || addressList.length() == 0) {
- return EMPTY_ADDRESS_ARRAY;
- }
- // IF we're CSV, just parse
- if ((addressList.indexOf(LIST_DELIMITER_PERSONAL) == -1) &&
- (addressList.indexOf(LIST_DELIMITER_EMAIL) == -1)) {
- return Address.parse(addressList);
- }
- // Otherwise, do backward-compatible unpack
- ArrayList<Address> addresses = new ArrayList<Address>();
- int length = addressList.length();
- int pairStartIndex = 0;
- int pairEndIndex;
-
- /* addressEndIndex is only re-scanned (indexOf()) when a LIST_DELIMITER_PERSONAL
- is used, not for every email address; i.e. not for every iteration of the while().
- This reduces the theoretical complexity from quadratic to linear,
- and provides some speed-up in practice by removing redundant scans of the string.
- */
- int addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL);
-
- while (pairStartIndex < length) {
- pairEndIndex = addressList.indexOf(LIST_DELIMITER_EMAIL, pairStartIndex);
- if (pairEndIndex == -1) {
- pairEndIndex = length;
- }
- Address address;
- if (addressEndIndex == -1 || pairEndIndex <= addressEndIndex) {
- // in this case the DELIMITER_PERSONAL is in a future pair,
- // so don't use personal, and don't update addressEndIndex
- address = new Address(addressList.substring(pairStartIndex, pairEndIndex), null);
- } else {
- address = new Address(addressList.substring(pairStartIndex, addressEndIndex),
- addressList.substring(addressEndIndex + 1, pairEndIndex));
- // only update addressEndIndex when we use the LIST_DELIMITER_PERSONAL
- addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL, pairEndIndex + 1);
- }
- addresses.add(address);
- pairStartIndex = pairEndIndex + 1;
- }
- return addresses.toArray(new Address[addresses.size()]);
- }
-
- public static final Creator<Address> CREATOR = new Creator<Address>() {
- @Override
- public Address createFromParcel(Parcel parcel) {
- return new Address(parcel);
- }
-
- @Override
- public Address[] newArray(int size) {
- return new Address[size];
- }
- };
-
- public Address(Parcel in) {
- setPersonal(in.readString());
- setAddress(in.readString());
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel out, int flags) {
- out.writeString(mPersonal);
- out.writeString(mAddress);
- }
-}
diff --git a/java/com/android/voicemailomtp/mail/Base64Body.java b/java/com/android/voicemailomtp/mail/Base64Body.java
deleted file mode 100644
index 6e1deff44..000000000
--- a/java/com/android/voicemailomtp/mail/Base64Body.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail;
-
-import android.util.Base64;
-import android.util.Base64OutputStream;
-
-import org.apache.commons.io.IOUtils;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-public class Base64Body implements Body {
- private final InputStream mSource;
- // Because we consume the input stream, we can only write out once
- private boolean mAlreadyWritten;
-
- public Base64Body(InputStream source) {
- mSource = source;
- }
-
- @Override
- public InputStream getInputStream() throws MessagingException {
- return mSource;
- }
-
- /**
- * This method consumes the input stream, so can only be called once
- * @param out Stream to write to
- * @throws IllegalStateException If called more than once
- * @throws IOException
- * @throws MessagingException
- */
- @Override
- public void writeTo(OutputStream out)
- throws IllegalStateException, IOException, MessagingException {
- if (mAlreadyWritten) {
- throw new IllegalStateException("Base64Body can only be written once");
- }
- mAlreadyWritten = true;
- try {
- final Base64OutputStream b64out = new Base64OutputStream(out, Base64.DEFAULT);
- IOUtils.copyLarge(mSource, b64out);
- } finally {
- mSource.close();
- }
- }
-}
diff --git a/java/com/android/voicemailomtp/mail/FetchProfile.java b/java/com/android/voicemailomtp/mail/FetchProfile.java
deleted file mode 100644
index d050692cc..000000000
--- a/java/com/android/voicemailomtp/mail/FetchProfile.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail;
-
-import java.util.ArrayList;
-
-/**
- * <pre>
- * A FetchProfile is a list of items that should be downloaded in bulk for a set of messages.
- * FetchProfile can contain the following objects:
- * FetchProfile.Item: Described below.
- * Message: Indicates that the body of the entire message should be fetched.
- * Synonymous with FetchProfile.Item.BODY.
- * Part: Indicates that the given Part should be fetched. The provider
- * is expected have previously created the given BodyPart and stored
- * any information it needs to download the content.
- * </pre>
- */
-public class FetchProfile extends ArrayList<Fetchable> {
- /**
- * Default items available for pre-fetching. It should be expected that any
- * item fetched by using these items could potentially include all of the
- * previous items.
- */
- public enum Item implements Fetchable {
- /**
- * Download the flags of the message.
- */
- FLAGS,
-
- /**
- * Download the envelope of the message. This should include at minimum
- * the size and the following headers: date, subject, from, content-type, to, cc
- */
- ENVELOPE,
-
- /**
- * Download the structure of the message. This maps directly to IMAP's BODYSTRUCTURE
- * and may map to other providers.
- * The provider should, if possible, fill in a properly formatted MIME structure in
- * the message without actually downloading any message data. If the provider is not
- * capable of this operation it should specifically set the body of the message to null
- * so that upper levels can detect that a full body download is needed.
- */
- STRUCTURE,
-
- /**
- * A sane portion of the entire message, cut off at a provider determined limit.
- * This should generally be around 50kB.
- */
- BODY_SANE,
-
- /**
- * The entire message.
- */
- BODY,
- }
-
- /**
- * @return the first {@link Part} in this collection, or null if it doesn't contain
- * {@link Part}.
- */
- public Part getFirstPart() {
- for (Fetchable o : this) {
- if (o instanceof Part) {
- return (Part) o;
- }
- }
- return null;
- }
-}
diff --git a/java/com/android/voicemailomtp/mail/FixedLengthInputStream.java b/java/com/android/voicemailomtp/mail/FixedLengthInputStream.java
deleted file mode 100644
index 65655efd5..000000000
--- a/java/com/android/voicemailomtp/mail/FixedLengthInputStream.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-/**
- * A filtering InputStream that stops allowing reads after the given length has been read. This
- * is used to allow a client to read directly from an underlying protocol stream without reading
- * past where the protocol handler intended the client to read.
- */
-public class FixedLengthInputStream extends InputStream {
- private final InputStream mIn;
- private final int mLength;
- private int mCount;
-
- public FixedLengthInputStream(InputStream in, int length) {
- this.mIn = in;
- this.mLength = length;
- }
-
- @Override
- public int available() throws IOException {
- return mLength - mCount;
- }
-
- @Override
- public int read() throws IOException {
- if (mCount < mLength) {
- mCount++;
- return mIn.read();
- } else {
- return -1;
- }
- }
-
- @Override
- public int read(byte[] b, int offset, int length) throws IOException {
- if (mCount < mLength) {
- int d = mIn.read(b, offset, Math.min(mLength - mCount, length));
- if (d == -1) {
- return -1;
- } else {
- mCount += d;
- return d;
- }
- } else {
- return -1;
- }
- }
-
- @Override
- public int read(byte[] b) throws IOException {
- return read(b, 0, b.length);
- }
-
- public int getLength() {
- return mLength;
- }
-
- @Override
- public String toString() {
- return String.format("FixedLengthInputStream(in=%s, length=%d)", mIn.toString(), mLength);
- }
-} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/MailTransport.java b/java/com/android/voicemailomtp/mail/MailTransport.java
deleted file mode 100644
index 3bf851fd8..000000000
--- a/java/com/android/voicemailomtp/mail/MailTransport.java
+++ /dev/null
@@ -1,344 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail;
-
-import android.content.Context;
-import android.net.Network;
-import android.support.annotation.VisibleForTesting;
-import com.android.voicemailomtp.OmtpEvents;
-import com.android.voicemailomtp.imap.ImapHelper;
-import com.android.voicemailomtp.mail.store.ImapStore;
-import com.android.voicemailomtp.mail.utils.LogUtils;
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.InetAddress;
-import java.net.InetSocketAddress;
-import java.net.Socket;
-import java.util.ArrayList;
-import java.util.List;
-import javax.net.ssl.HostnameVerifier;
-import javax.net.ssl.HttpsURLConnection;
-import javax.net.ssl.SSLException;
-import javax.net.ssl.SSLPeerUnverifiedException;
-import javax.net.ssl.SSLSession;
-import javax.net.ssl.SSLSocket;
-
-/**
- * Make connection and perform operations on mail server by reading and writing lines.
- */
-public class MailTransport {
- private static final String TAG = "MailTransport";
-
- // TODO protected eventually
- /*protected*/ public static final int SOCKET_CONNECT_TIMEOUT = 10000;
- /*protected*/ public static final int SOCKET_READ_TIMEOUT = 60000;
-
- private static final HostnameVerifier HOSTNAME_VERIFIER =
- HttpsURLConnection.getDefaultHostnameVerifier();
-
- private final Context mContext;
- private final ImapHelper mImapHelper;
- private final Network mNetwork;
- private final String mHost;
- private final int mPort;
- private Socket mSocket;
- private BufferedInputStream mIn;
- private BufferedOutputStream mOut;
- private final int mFlags;
- private SocketCreator mSocketCreator;
- private InetSocketAddress mAddress;
-
- public MailTransport(Context context, ImapHelper imapHelper, Network network, String address,
- int port, int flags) {
- mContext = context;
- mImapHelper = imapHelper;
- mNetwork = network;
- mHost = address;
- mPort = port;
- mFlags = flags;
- }
-
- /**
- * Returns a new transport, using the current transport as a model. The new transport is
- * configured identically, but not opened or connected in any way.
- */
- @Override
- public MailTransport clone() {
- return new MailTransport(mContext, mImapHelper, mNetwork, mHost, mPort, mFlags);
- }
-
- public boolean canTrySslSecurity() {
- return (mFlags & ImapStore.FLAG_SSL) != 0;
- }
-
- public boolean canTrustAllCertificates() {
- return (mFlags & ImapStore.FLAG_TRUST_ALL) != 0;
- }
-
- /**
- * Attempts to open a connection using the Uri supplied for connection parameters. Will attempt
- * an SSL connection if indicated.
- */
- public void open() throws MessagingException {
- LogUtils.d(TAG, "*** IMAP open " + mHost + ":" + String.valueOf(mPort));
-
- List<InetSocketAddress> socketAddresses = new ArrayList<InetSocketAddress>();
-
- if (mNetwork == null) {
- socketAddresses.add(new InetSocketAddress(mHost, mPort));
- } else {
- try {
- InetAddress[] inetAddresses = mNetwork.getAllByName(mHost);
- if (inetAddresses.length == 0) {
- throw new MessagingException(MessagingException.IOERROR,
- "Host name " + mHost + "cannot be resolved on designated network");
- }
- for (int i = 0; i < inetAddresses.length; i++) {
- socketAddresses.add(new InetSocketAddress(inetAddresses[i], mPort));
- }
- } catch (IOException ioe) {
- LogUtils.d(TAG, ioe.toString());
- mImapHelper.handleEvent(OmtpEvents.DATA_CANNOT_RESOLVE_HOST_ON_NETWORK);
- throw new MessagingException(MessagingException.IOERROR, ioe.toString());
- }
- }
-
- boolean success = false;
- while (socketAddresses.size() > 0) {
- mSocket = createSocket();
- try {
- mAddress = socketAddresses.remove(0);
- mSocket.connect(mAddress, SOCKET_CONNECT_TIMEOUT);
-
- if (canTrySslSecurity()) {
- /*
- SSLSocket cannot be created with a connection timeout, so instead of doing a
- direct SSL connection, we connect with a normal connection and upgrade it into
- SSL
- */
- reopenTls();
- } else {
- mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
- mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
- mSocket.setSoTimeout(SOCKET_READ_TIMEOUT);
- }
- success = true;
- return;
- } catch (IOException ioe) {
- LogUtils.d(TAG, ioe.toString());
- if (socketAddresses.size() == 0) {
- // Only throw an error when there are no more sockets to try.
- mImapHelper.handleEvent(OmtpEvents.DATA_ALL_SOCKET_CONNECTION_FAILED);
- throw new MessagingException(MessagingException.IOERROR, ioe.toString());
- }
- } finally {
- if (!success) {
- try {
- mSocket.close();
- mSocket = null;
- } catch (IOException ioe) {
- throw new MessagingException(MessagingException.IOERROR, ioe.toString());
- }
-
- }
- }
- }
- }
-
- // For testing. We need something that can replace the behavior of "new Socket()"
- @VisibleForTesting
- interface SocketCreator {
-
- Socket createSocket() throws MessagingException;
- }
-
- @VisibleForTesting
- void setSocketCreator(SocketCreator creator) {
- mSocketCreator = creator;
- }
-
- protected Socket createSocket() throws MessagingException {
- if (mSocketCreator != null) {
- return mSocketCreator.createSocket();
- }
-
- if (mNetwork == null) {
- LogUtils.v(TAG, "createSocket: network not specified");
- return new Socket();
- }
-
- try {
- LogUtils.v(TAG, "createSocket: network specified");
- return mNetwork.getSocketFactory().createSocket();
- } catch (IOException ioe) {
- LogUtils.d(TAG, ioe.toString());
- throw new MessagingException(MessagingException.IOERROR, ioe.toString());
- }
- }
-
- /**
- * Attempts to reopen a normal connection into a TLS connection.
- */
- public void reopenTls() throws MessagingException {
- try {
- LogUtils.d(TAG, "open: converting to TLS socket");
- mSocket = HttpsURLConnection.getDefaultSSLSocketFactory()
- .createSocket(mSocket, mAddress.getHostName(), mAddress.getPort(), true);
- // After the socket connects to an SSL server, confirm that the hostname is as
- // expected
- if (!canTrustAllCertificates()) {
- verifyHostname(mSocket, mHost);
- }
- mSocket.setSoTimeout(SOCKET_READ_TIMEOUT);
- mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
- mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
-
- } catch (SSLException e) {
- LogUtils.d(TAG, e.toString());
- throw new CertificateValidationException(e.getMessage(), e);
- } catch (IOException ioe) {
- LogUtils.d(TAG, ioe.toString());
- throw new MessagingException(MessagingException.IOERROR, ioe.toString());
- }
- }
-
- /**
- * Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this
- * service but is not in the public API.
- *
- * Verify the hostname of the certificate used by the other end of a
- * connected socket. It is harmless to call this method redundantly if the hostname has already
- * been verified.
- *
- * <p>Wildcard certificates are allowed to verify any matching hostname,
- * so "foo.bar.example.com" is verified if the peer has a certificate
- * for "*.example.com".
- *
- * @param socket An SSL socket which has been connected to a server
- * @param hostname The expected hostname of the remote server
- * @throws IOException if something goes wrong handshaking with the server
- * @throws SSLPeerUnverifiedException if the server cannot prove its identity
- */
- private void verifyHostname(Socket socket, String hostname) throws IOException {
- // The code at the start of OpenSSLSocketImpl.startHandshake()
- // ensures that the call is idempotent, so we can safely call it.
- SSLSocket ssl = (SSLSocket) socket;
- ssl.startHandshake();
-
- SSLSession session = ssl.getSession();
- if (session == null) {
- mImapHelper.handleEvent(OmtpEvents.DATA_CANNOT_ESTABLISH_SSL_SESSION);
- throw new SSLException("Cannot verify SSL socket without session");
- }
- // TODO: Instead of reporting the name of the server we think we're connecting to,
- // we should be reporting the bad name in the certificate. Unfortunately this is buried
- // in the verifier code and is not available in the verifier API, and extracting the
- // CN & alts is beyond the scope of this patch.
- if (!HOSTNAME_VERIFIER.verify(hostname, session)) {
- mImapHelper.handleEvent(OmtpEvents.DATA_SSL_INVALID_HOST_NAME);
- throw new SSLPeerUnverifiedException("Certificate hostname not useable for server: "
- + session.getPeerPrincipal());
- }
- }
-
- public boolean isOpen() {
- return (mIn != null && mOut != null &&
- mSocket != null && mSocket.isConnected() && !mSocket.isClosed());
- }
-
- /**
- * Close the connection. MUST NOT return any exceptions - must be "best effort" and safe.
- */
- public void close() {
- try {
- mIn.close();
- } catch (Exception e) {
- // May fail if the connection is already closed.
- }
- try {
- mOut.close();
- } catch (Exception e) {
- // May fail if the connection is already closed.
- }
- try {
- mSocket.close();
- } catch (Exception e) {
- // May fail if the connection is already closed.
- }
- mIn = null;
- mOut = null;
- mSocket = null;
- }
-
- public String getHost() {
- return mHost;
- }
-
- public InputStream getInputStream() {
- return mIn;
- }
-
- public OutputStream getOutputStream() {
- return mOut;
- }
-
- /**
- * Writes a single line to the server using \r\n termination.
- */
- public void writeLine(String s, String sensitiveReplacement) throws IOException {
- if (sensitiveReplacement != null) {
- LogUtils.d(TAG, ">>> " + sensitiveReplacement);
- } else {
- LogUtils.d(TAG, ">>> " + s);
- }
-
- OutputStream out = getOutputStream();
- out.write(s.getBytes());
- out.write('\r');
- out.write('\n');
- out.flush();
- }
-
- /**
- * Reads a single line from the server, using either \r\n or \n as the delimiter. The
- * delimiter char(s) are not included in the result.
- */
- public String readLine(boolean loggable) throws IOException {
- StringBuffer sb = new StringBuffer();
- InputStream in = getInputStream();
- int d;
- while ((d = in.read()) != -1) {
- if (((char)d) == '\r') {
- continue;
- } else if (((char)d) == '\n') {
- break;
- } else {
- sb.append((char)d);
- }
- }
- if (d == -1) {
- LogUtils.d(TAG, "End of stream reached while trying to read line.");
- }
- String ret = sb.toString();
- if (loggable) {
- LogUtils.d(TAG, "<<< " + ret);
- }
- return ret;
- }
-}
diff --git a/java/com/android/voicemailomtp/mail/Message.java b/java/com/android/voicemailomtp/mail/Message.java
deleted file mode 100644
index 41555690f..000000000
--- a/java/com/android/voicemailomtp/mail/Message.java
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail;
-
-import android.support.annotation.VisibleForTesting;
-import java.util.Date;
-import java.util.HashSet;
-
-public abstract class Message implements Part, Body {
- public static final Message[] EMPTY_ARRAY = new Message[0];
-
- public static final String RECIPIENT_TYPE_TO = "to";
- public static final String RECIPIENT_TYPE_CC = "cc";
- public static final String RECIPIENT_TYPE_BCC = "bcc";
- public enum RecipientType {
- TO, CC, BCC,
- }
-
- protected String mUid;
-
- private HashSet<String> mFlags = null;
-
- protected Date mInternalDate;
-
- public String getUid() {
- return mUid;
- }
-
- public void setUid(String uid) {
- this.mUid = uid;
- }
-
- public abstract String getSubject() throws MessagingException;
-
- public abstract void setSubject(String subject) throws MessagingException;
-
- public Date getInternalDate() {
- return mInternalDate;
- }
-
- public void setInternalDate(Date internalDate) {
- this.mInternalDate = internalDate;
- }
-
- public abstract Date getReceivedDate() throws MessagingException;
-
- public abstract Date getSentDate() throws MessagingException;
-
- public abstract void setSentDate(Date sentDate) throws MessagingException;
-
- public abstract Address[] getRecipients(String type) throws MessagingException;
-
- public abstract void setRecipients(String type, Address[] addresses)
- throws MessagingException;
-
- public void setRecipient(String type, Address address) throws MessagingException {
- setRecipients(type, new Address[] {
- address
- });
- }
-
- public abstract Address[] getFrom() throws MessagingException;
-
- public abstract void setFrom(Address from) throws MessagingException;
-
- public abstract Address[] getReplyTo() throws MessagingException;
-
- public abstract void setReplyTo(Address[] from) throws MessagingException;
-
- // Always use these instead of getHeader("Message-ID") or setHeader("Message-ID");
- public abstract void setMessageId(String messageId) throws MessagingException;
- public abstract String getMessageId() throws MessagingException;
-
- @Override
- public boolean isMimeType(String mimeType) throws MessagingException {
- return getContentType().startsWith(mimeType);
- }
-
- private HashSet<String> getFlagSet() {
- if (mFlags == null) {
- mFlags = new HashSet<String>();
- }
- return mFlags;
- }
-
- /*
- * TODO Refactor Flags at some point to be able to store user defined flags.
- */
- public String[] getFlags() {
- return getFlagSet().toArray(new String[] {});
- }
-
- /**
- * Set/clear a flag directly, without involving overrides of {@link #setFlag} in subclasses.
- * Only used for testing.
- */
- @VisibleForTesting
- private final void setFlagDirectlyForTest(String flag, boolean set) throws MessagingException {
- if (set) {
- getFlagSet().add(flag);
- } else {
- getFlagSet().remove(flag);
- }
- }
-
- public void setFlag(String flag, boolean set) throws MessagingException {
- setFlagDirectlyForTest(flag, set);
- }
-
- /**
- * This method calls setFlag(String, boolean)
- * @param flags
- * @param set
- */
- public void setFlags(String[] flags, boolean set) throws MessagingException {
- for (String flag : flags) {
- setFlag(flag, set);
- }
- }
-
- public boolean isSet(String flag) {
- return getFlagSet().contains(flag);
- }
-
- public abstract void saveChanges() throws MessagingException;
-
- @Override
- public String toString() {
- return getClass().getSimpleName() + ':' + mUid;
- }
-}
diff --git a/java/com/android/voicemailomtp/mail/MessagingException.java b/java/com/android/voicemailomtp/mail/MessagingException.java
deleted file mode 100644
index 28550527f..000000000
--- a/java/com/android/voicemailomtp/mail/MessagingException.java
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.voicemailomtp.mail;
-
-/**
- * This exception is used for most types of failures that occur during server interactions.
- *
- * Data passed through this exception should be considered non-localized. Any strings should
- * either be internal-only (for debugging) or server-generated.
- *
- * TO DO: Does it make sense to further collapse AuthenticationFailedException and
- * CertificateValidationException and any others into this?
- */
-public class MessagingException extends Exception {
- public static final long serialVersionUID = -1;
-
- public static final int NO_ERROR = -1;
- /** Any exception that does not specify a specific issue */
- public static final int UNSPECIFIED_EXCEPTION = 0;
- /** Connection or IO errors */
- public static final int IOERROR = 1;
- /** The configuration requested TLS but the server did not support it. */
- public static final int TLS_REQUIRED = 2;
- /** Authentication is required but the server did not support it. */
- public static final int AUTH_REQUIRED = 3;
- /** General security failures */
- public static final int GENERAL_SECURITY = 4;
- /** Authentication failed */
- public static final int AUTHENTICATION_FAILED = 5;
- /** Attempt to create duplicate account */
- public static final int DUPLICATE_ACCOUNT = 6;
- /** Required security policies reported - advisory only */
- public static final int SECURITY_POLICIES_REQUIRED = 7;
- /** Required security policies not supported */
- public static final int SECURITY_POLICIES_UNSUPPORTED = 8;
- /** The protocol (or protocol version) isn't supported */
- public static final int PROTOCOL_VERSION_UNSUPPORTED = 9;
- /** The server's SSL certificate couldn't be validated */
- public static final int CERTIFICATE_VALIDATION_ERROR = 10;
- /** Authentication failed during autodiscover */
- public static final int AUTODISCOVER_AUTHENTICATION_FAILED = 11;
- /** Autodiscover completed with a result (non-error) */
- public static final int AUTODISCOVER_AUTHENTICATION_RESULT = 12;
- /** Ambiguous failure; server error or bad credentials */
- public static final int AUTHENTICATION_FAILED_OR_SERVER_ERROR = 13;
- /** The server refused access */
- public static final int ACCESS_DENIED = 14;
- /** The server refused access */
- public static final int ATTACHMENT_NOT_FOUND = 15;
- /** A client SSL certificate is required for connections to the server */
- public static final int CLIENT_CERTIFICATE_REQUIRED = 16;
- /** The client SSL certificate specified is invalid */
- public static final int CLIENT_CERTIFICATE_ERROR = 17;
- /** The server indicates it does not support OAuth authentication */
- public static final int OAUTH_NOT_SUPPORTED = 18;
- /** The server indicates it experienced an internal error */
- public static final int SERVER_ERROR = 19;
-
- protected int mExceptionType;
- // Exception type-specific data
- protected Object mExceptionData;
-
- public MessagingException(String message, Throwable throwable) {
- this(UNSPECIFIED_EXCEPTION, message, throwable);
- }
-
- public MessagingException(int exceptionType, String message, Throwable throwable) {
- super(message, throwable);
- mExceptionType = exceptionType;
- mExceptionData = null;
- }
-
- /**
- * Constructs a MessagingException with an exceptionType and a null message.
- * @param exceptionType The exception type to set for this exception.
- */
- public MessagingException(int exceptionType) {
- this(exceptionType, null, null);
- }
-
- /**
- * Constructs a MessagingException with a message.
- * @param message the message for this exception
- */
- public MessagingException(String message) {
- this(UNSPECIFIED_EXCEPTION, message, null);
- }
-
- /**
- * Constructs a MessagingException with an exceptionType and a message.
- * @param exceptionType The exception type to set for this exception.
- */
- public MessagingException(int exceptionType, String message) {
- this(exceptionType, message, null);
- }
-
- /**
- * Constructs a MessagingException with an exceptionType, a message, and data
- * @param exceptionType The exception type to set for this exception.
- * @param message the message for the exception (or null)
- * @param data exception-type specific data for the exception (or null)
- */
- public MessagingException(int exceptionType, String message, Object data) {
- super(message);
- mExceptionType = exceptionType;
- mExceptionData = data;
- }
-
- /**
- * Return the exception type. Will be OTHER_EXCEPTION if not explicitly set.
- *
- * @return Returns the exception type.
- */
- public int getExceptionType() {
- return mExceptionType;
- }
- /**
- * Return the exception data. Will be null if not explicitly set.
- *
- * @return Returns the exception data.
- */
- public Object getExceptionData() {
- return mExceptionData;
- }
-} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/Multipart.java b/java/com/android/voicemailomtp/mail/Multipart.java
deleted file mode 100644
index b45ebab3d..000000000
--- a/java/com/android/voicemailomtp/mail/Multipart.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail;
-
-import java.util.ArrayList;
-
-public abstract class Multipart implements Body {
- protected Part mParent;
-
- protected ArrayList<BodyPart> mParts = new ArrayList<BodyPart>();
-
- protected String mContentType;
-
- public void addBodyPart(BodyPart part) throws MessagingException {
- mParts.add(part);
- }
-
- public void addBodyPart(BodyPart part, int index) throws MessagingException {
- mParts.add(index, part);
- }
-
- public BodyPart getBodyPart(int index) throws MessagingException {
- return mParts.get(index);
- }
-
- public String getContentType() throws MessagingException {
- return mContentType;
- }
-
- public int getCount() throws MessagingException {
- return mParts.size();
- }
-
- public boolean removeBodyPart(BodyPart part) throws MessagingException {
- return mParts.remove(part);
- }
-
- public void removeBodyPart(int index) throws MessagingException {
- mParts.remove(index);
- }
-
- public Part getParent() throws MessagingException {
- return mParent;
- }
-
- public void setParent(Part parent) throws MessagingException {
- this.mParent = parent;
- }
-}
diff --git a/java/com/android/voicemailomtp/mail/PackedString.java b/java/com/android/voicemailomtp/mail/PackedString.java
deleted file mode 100644
index 585759611..000000000
--- a/java/com/android/voicemailomtp/mail/PackedString.java
+++ /dev/null
@@ -1,175 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * A utility class for creating and modifying Strings that are tagged and packed together.
- *
- * Uses non-printable (control chars) for internal delimiters; Intended for regular displayable
- * strings only, so please use base64 or other encoding if you need to hide any binary data here.
- *
- * Binary compatible with Address.pack() format, which should migrate to use this code.
- */
-public class PackedString {
-
- /**
- * Packing format is:
- * element : [ value ] or [ value TAG-DELIMITER tag ]
- * packed-string : [ element ] [ ELEMENT-DELIMITER [ element ] ]*
- */
- private static final char DELIMITER_ELEMENT = '\1';
- private static final char DELIMITER_TAG = '\2';
-
- private String mString;
- private HashMap<String, String> mExploded;
- private static final HashMap<String, String> EMPTY_MAP = new HashMap<String, String>();
-
- /**
- * Create a packed string using an already-packed string (e.g. from database)
- * @param string packed string
- */
- public PackedString(String string) {
- mString = string;
- mExploded = null;
- }
-
- /**
- * Get the value referred to by a given tag. If the tag does not exist, return null.
- * @param tag identifier of string of interest
- * @return returns value, or null if no string is found
- */
- public String get(String tag) {
- if (mExploded == null) {
- mExploded = explode(mString);
- }
- return mExploded.get(tag);
- }
-
- /**
- * Return a map of all of the values referred to by a given tag. This is a shallow
- * copy, don't edit the values.
- * @return a map of the values in the packed string
- */
- public Map<String, String> unpack() {
- if (mExploded == null) {
- mExploded = explode(mString);
- }
- return new HashMap<String,String>(mExploded);
- }
-
- /**
- * Read out all values into a map.
- */
- private static HashMap<String, String> explode(String packed) {
- if (packed == null || packed.length() == 0) {
- return EMPTY_MAP;
- }
- HashMap<String, String> map = new HashMap<String, String>();
-
- int length = packed.length();
- int elementStartIndex = 0;
- int elementEndIndex = 0;
- int tagEndIndex = packed.indexOf(DELIMITER_TAG);
-
- while (elementStartIndex < length) {
- elementEndIndex = packed.indexOf(DELIMITER_ELEMENT, elementStartIndex);
- if (elementEndIndex == -1) {
- elementEndIndex = length;
- }
- String tag;
- String value;
- if (tagEndIndex == -1 || elementEndIndex <= tagEndIndex) {
- // in this case the DELIMITER_PERSONAL is in a future pair (or not found)
- // so synthesize a positional tag for the value, and don't update tagEndIndex
- value = packed.substring(elementStartIndex, elementEndIndex);
- tag = Integer.toString(map.size());
- } else {
- value = packed.substring(elementStartIndex, tagEndIndex);
- tag = packed.substring(tagEndIndex + 1, elementEndIndex);
- // scan forward for next tag, if any
- tagEndIndex = packed.indexOf(DELIMITER_TAG, elementEndIndex + 1);
- }
- map.put(tag, value);
- elementStartIndex = elementEndIndex + 1;
- }
-
- return map;
- }
-
- /**
- * Builder class for creating PackedString values. Can also be used for editing existing
- * PackedString representations.
- */
- static public class Builder {
- HashMap<String, String> mMap;
-
- /**
- * Create a builder that's empty (for filling)
- */
- public Builder() {
- mMap = new HashMap<String, String>();
- }
-
- /**
- * Create a builder using the values of an existing PackedString (for editing).
- */
- public Builder(String packed) {
- mMap = explode(packed);
- }
-
- /**
- * Add a tagged value
- * @param tag identifier of string of interest
- * @param value the value to record in this position. null to delete entry.
- */
- public void put(String tag, String value) {
- if (value == null) {
- mMap.remove(tag);
- } else {
- mMap.put(tag, value);
- }
- }
-
- /**
- * Get the value referred to by a given tag. If the tag does not exist, return null.
- * @param tag identifier of string of interest
- * @return returns value, or null if no string is found
- */
- public String get(String tag) {
- return mMap.get(tag);
- }
-
- /**
- * Pack the values and return a single, encoded string
- */
- @Override
- public String toString() {
- StringBuilder sb = new StringBuilder();
- for (Map.Entry<String,String> entry : mMap.entrySet()) {
- if (sb.length() > 0) {
- sb.append(DELIMITER_ELEMENT);
- }
- sb.append(entry.getValue());
- sb.append(DELIMITER_TAG);
- sb.append(entry.getKey());
- }
- return sb.toString();
- }
- }
-}
diff --git a/java/com/android/voicemailomtp/mail/Part.java b/java/com/android/voicemailomtp/mail/Part.java
deleted file mode 100644
index 51f8a4c38..000000000
--- a/java/com/android/voicemailomtp/mail/Part.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail;
-
-import java.io.IOException;
-import java.io.OutputStream;
-
-public interface Part extends Fetchable {
- public void addHeader(String name, String value) throws MessagingException;
-
- public void removeHeader(String name) throws MessagingException;
-
- public void setHeader(String name, String value) throws MessagingException;
-
- public Body getBody() throws MessagingException;
-
- public String getContentType() throws MessagingException;
-
- public String getDisposition() throws MessagingException;
-
- public String getContentId() throws MessagingException;
-
- public String[] getHeader(String name) throws MessagingException;
-
- public void setExtendedHeader(String name, String value) throws MessagingException;
-
- public String getExtendedHeader(String name) throws MessagingException;
-
- public int getSize() throws MessagingException;
-
- public boolean isMimeType(String mimeType) throws MessagingException;
-
- public String getMimeType() throws MessagingException;
-
- public void setBody(Body body) throws MessagingException;
-
- public void writeTo(OutputStream out) throws IOException, MessagingException;
-}
diff --git a/java/com/android/voicemailomtp/mail/PeekableInputStream.java b/java/com/android/voicemailomtp/mail/PeekableInputStream.java
deleted file mode 100644
index c1181d189..000000000
--- a/java/com/android/voicemailomtp/mail/PeekableInputStream.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.voicemailomtp.mail;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-/**
- * A filtering InputStream that allows single byte "peeks" without consuming the byte. The
- * client of this stream can call peek() to see the next available byte in the stream
- * and a subsequent read will still return the peeked byte.
- */
-public class PeekableInputStream extends InputStream {
- private final InputStream mIn;
- private boolean mPeeked;
- private int mPeekedByte;
-
- public PeekableInputStream(InputStream in) {
- this.mIn = in;
- }
-
- @Override
- public int read() throws IOException {
- if (!mPeeked) {
- return mIn.read();
- } else {
- mPeeked = false;
- return mPeekedByte;
- }
- }
-
- public int peek() throws IOException {
- if (!mPeeked) {
- mPeekedByte = read();
- mPeeked = true;
- }
- return mPeekedByte;
- }
-
- @Override
- public int read(byte[] b, int offset, int length) throws IOException {
- if (!mPeeked) {
- return mIn.read(b, offset, length);
- } else {
- b[0] = (byte)mPeekedByte;
- mPeeked = false;
- int r = mIn.read(b, offset + 1, length - 1);
- if (r == -1) {
- return 1;
- } else {
- return r + 1;
- }
- }
- }
-
- @Override
- public int read(byte[] b) throws IOException {
- return read(b, 0, b.length);
- }
-
- @Override
- public String toString() {
- return String.format("PeekableInputStream(in=%s, peeked=%b, peekedByte=%d)",
- mIn.toString(), mPeeked, mPeekedByte);
- }
-} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/internet/BinaryTempFileBody.java b/java/com/android/voicemailomtp/mail/internet/BinaryTempFileBody.java
deleted file mode 100644
index 52c43de16..000000000
--- a/java/com/android/voicemailomtp/mail/internet/BinaryTempFileBody.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail.internet;
-
-import com.android.voicemailomtp.mail.Body;
-import com.android.voicemailomtp.mail.MessagingException;
-import com.android.voicemailomtp.mail.TempDirectory;
-
-import org.apache.commons.io.IOUtils;
-
-import android.util.Base64;
-import android.util.Base64OutputStream;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.FilterInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-/**
- * A Body that is backed by a temp file. The Body exposes a getOutputStream method that allows
- * the user to write to the temp file. After the write the body is available via getInputStream
- * and writeTo one time. After writeTo is called, or the InputStream returned from
- * getInputStream is closed the file is deleted and the Body should be considered disposed of.
- */
-public class BinaryTempFileBody implements Body {
- private File mFile;
-
- /**
- * An alternate way to put data into a BinaryTempFileBody is to simply supply an already-
- * created file. Note that this file will be deleted after it is read.
- * @param filePath The file containing the data to be stored on disk temporarily
- */
- public void setFile(String filePath) {
- mFile = new File(filePath);
- }
-
- public OutputStream getOutputStream() throws IOException {
- mFile = File.createTempFile("body", null, TempDirectory.getTempDirectory());
- mFile.deleteOnExit();
- return new FileOutputStream(mFile);
- }
-
- @Override
- public InputStream getInputStream() throws MessagingException {
- try {
- return new BinaryTempFileBodyInputStream(new FileInputStream(mFile));
- }
- catch (IOException ioe) {
- throw new MessagingException("Unable to open body", ioe);
- }
- }
-
- @Override
- public void writeTo(OutputStream out) throws IOException, MessagingException {
- InputStream in = getInputStream();
- Base64OutputStream base64Out = new Base64OutputStream(
- out, Base64.CRLF | Base64.NO_CLOSE);
- IOUtils.copy(in, base64Out);
- base64Out.close();
- mFile.delete();
- in.close();
- }
-
- class BinaryTempFileBodyInputStream extends FilterInputStream {
- public BinaryTempFileBodyInputStream(InputStream in) {
- super(in);
- }
-
- @Override
- public void close() throws IOException {
- super.close();
- mFile.delete();
- }
- }
-}
diff --git a/java/com/android/voicemailomtp/mail/internet/MimeBodyPart.java b/java/com/android/voicemailomtp/mail/internet/MimeBodyPart.java
deleted file mode 100644
index 8a9c45cf9..000000000
--- a/java/com/android/voicemailomtp/mail/internet/MimeBodyPart.java
+++ /dev/null
@@ -1,207 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail.internet;
-
-import com.android.voicemailomtp.mail.Body;
-import com.android.voicemailomtp.mail.BodyPart;
-import com.android.voicemailomtp.mail.MessagingException;
-
-import java.io.BufferedWriter;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.util.regex.Pattern;
-
-/**
- * TODO this is a close approximation of Message, need to update along with
- * Message.
- */
-public class MimeBodyPart extends BodyPart {
- protected MimeHeader mHeader = new MimeHeader();
- protected MimeHeader mExtendedHeader;
- protected Body mBody;
- protected int mSize;
-
- // regex that matches content id surrounded by "<>" optionally.
- private static final Pattern REMOVE_OPTIONAL_BRACKETS = Pattern.compile("^<?([^>]+)>?$");
- // regex that matches end of line.
- private static final Pattern END_OF_LINE = Pattern.compile("\r?\n");
-
- public MimeBodyPart() throws MessagingException {
- this(null);
- }
-
- public MimeBodyPart(Body body) throws MessagingException {
- this(body, null);
- }
-
- public MimeBodyPart(Body body, String mimeType) throws MessagingException {
- if (mimeType != null) {
- setHeader(MimeHeader.HEADER_CONTENT_TYPE, mimeType);
- }
- setBody(body);
- }
-
- protected String getFirstHeader(String name) throws MessagingException {
- return mHeader.getFirstHeader(name);
- }
-
- @Override
- public void addHeader(String name, String value) throws MessagingException {
- mHeader.addHeader(name, value);
- }
-
- @Override
- public void setHeader(String name, String value) throws MessagingException {
- mHeader.setHeader(name, value);
- }
-
- @Override
- public String[] getHeader(String name) throws MessagingException {
- return mHeader.getHeader(name);
- }
-
- @Override
- public void removeHeader(String name) throws MessagingException {
- mHeader.removeHeader(name);
- }
-
- @Override
- public Body getBody() throws MessagingException {
- return mBody;
- }
-
- @Override
- public void setBody(Body body) throws MessagingException {
- this.mBody = body;
- if (body instanceof com.android.voicemailomtp.mail.Multipart) {
- com.android.voicemailomtp.mail.Multipart multipart =
- ((com.android.voicemailomtp.mail.Multipart)body);
- multipart.setParent(this);
- setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType());
- }
- else if (body instanceof TextBody) {
- String contentType = String.format("%s;\n charset=utf-8", getMimeType());
- String name = MimeUtility.getHeaderParameter(getContentType(), "name");
- if (name != null) {
- contentType += String.format(";\n name=\"%s\"", name);
- }
- setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType);
- setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
- }
- }
-
- @Override
- public String getContentType() throws MessagingException {
- String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
- if (contentType == null) {
- return "text/plain";
- } else {
- return contentType;
- }
- }
-
- @Override
- public String getDisposition() throws MessagingException {
- String contentDisposition = getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
- if (contentDisposition == null) {
- return null;
- } else {
- return contentDisposition;
- }
- }
-
- @Override
- public String getContentId() throws MessagingException {
- String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID);
- if (contentId == null) {
- return null;
- } else {
- // remove optionally surrounding brackets.
- return REMOVE_OPTIONAL_BRACKETS.matcher(contentId).replaceAll("$1");
- }
- }
-
- @Override
- public String getMimeType() throws MessagingException {
- return MimeUtility.getHeaderParameter(getContentType(), null);
- }
-
- @Override
- public boolean isMimeType(String mimeType) throws MessagingException {
- return getMimeType().equals(mimeType);
- }
-
- public void setSize(int size) {
- this.mSize = size;
- }
-
- @Override
- public int getSize() throws MessagingException {
- return mSize;
- }
-
- /**
- * Set extended header
- *
- * @param name Extended header name
- * @param value header value - flattened by removing CR-NL if any
- * remove header if value is null
- * @throws MessagingException
- */
- @Override
- public void setExtendedHeader(String name, String value) throws MessagingException {
- if (value == null) {
- if (mExtendedHeader != null) {
- mExtendedHeader.removeHeader(name);
- }
- return;
- }
- if (mExtendedHeader == null) {
- mExtendedHeader = new MimeHeader();
- }
- mExtendedHeader.setHeader(name, END_OF_LINE.matcher(value).replaceAll(""));
- }
-
- /**
- * Get extended header
- *
- * @param name Extended header name
- * @return header value - null if header does not exist
- * @throws MessagingException
- */
- @Override
- public String getExtendedHeader(String name) throws MessagingException {
- if (mExtendedHeader == null) {
- return null;
- }
- return mExtendedHeader.getFirstHeader(name);
- }
-
- /**
- * Write the MimeMessage out in MIME format.
- */
- @Override
- public void writeTo(OutputStream out) throws IOException, MessagingException {
- BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
- mHeader.writeTo(out);
- writer.write("\r\n");
- writer.flush();
- if (mBody != null) {
- mBody.writeTo(out);
- }
- }
-}
diff --git a/java/com/android/voicemailomtp/mail/internet/MimeHeader.java b/java/com/android/voicemailomtp/mail/internet/MimeHeader.java
deleted file mode 100644
index 4b0aea749..000000000
--- a/java/com/android/voicemailomtp/mail/internet/MimeHeader.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail.internet;
-
-import com.android.voicemailomtp.mail.MessagingException;
-
-import java.io.BufferedWriter;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.util.ArrayList;
-
-public class MimeHeader {
- /**
- * Application specific header that contains Store specific information about an attachment.
- * In IMAP this contains the IMAP BODYSTRUCTURE part id so that the ImapStore can later
- * retrieve the attachment at will from the server.
- * The info is recorded from this header on LocalStore.appendMessage and is put back
- * into the MIME data by LocalStore.fetch.
- */
- public static final String HEADER_ANDROID_ATTACHMENT_STORE_DATA = "X-Android-Attachment-StoreData";
-
- public static final String HEADER_CONTENT_TYPE = "Content-Type";
- public static final String HEADER_CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding";
- public static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition";
- public static final String HEADER_CONTENT_ID = "Content-ID";
-
- /**
- * Fields that should be omitted when writing the header using writeTo()
- */
- private static final String[] WRITE_OMIT_FIELDS = {
-// HEADER_ANDROID_ATTACHMENT_DOWNLOADED,
-// HEADER_ANDROID_ATTACHMENT_ID,
- HEADER_ANDROID_ATTACHMENT_STORE_DATA
- };
-
- protected final ArrayList<Field> mFields = new ArrayList<Field>();
-
- public void clear() {
- mFields.clear();
- }
-
- public String getFirstHeader(String name) throws MessagingException {
- String[] header = getHeader(name);
- if (header == null) {
- return null;
- }
- return header[0];
- }
-
- public void addHeader(String name, String value) throws MessagingException {
- mFields.add(new Field(name, value));
- }
-
- public void setHeader(String name, String value) throws MessagingException {
- if (name == null || value == null) {
- return;
- }
- removeHeader(name);
- addHeader(name, value);
- }
-
- public String[] getHeader(String name) throws MessagingException {
- ArrayList<String> values = new ArrayList<String>();
- for (Field field : mFields) {
- if (field.name.equalsIgnoreCase(name)) {
- values.add(field.value);
- }
- }
- if (values.size() == 0) {
- return null;
- }
- return values.toArray(new String[] {});
- }
-
- public void removeHeader(String name) throws MessagingException {
- ArrayList<Field> removeFields = new ArrayList<Field>();
- for (Field field : mFields) {
- if (field.name.equalsIgnoreCase(name)) {
- removeFields.add(field);
- }
- }
- mFields.removeAll(removeFields);
- }
-
- /**
- * Write header into String
- *
- * @return CR-NL separated header string except the headers in writeOmitFields
- * null if header is empty
- */
- public String writeToString() {
- if (mFields.size() == 0) {
- return null;
- }
- StringBuilder builder = new StringBuilder();
- for (Field field : mFields) {
- if (!arrayContains(WRITE_OMIT_FIELDS, field.name)) {
- builder.append(field.name + ": " + field.value + "\r\n");
- }
- }
- return builder.toString();
- }
-
- public void writeTo(OutputStream out) throws IOException, MessagingException {
- BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
- for (Field field : mFields) {
- if (!arrayContains(WRITE_OMIT_FIELDS, field.name)) {
- writer.write(field.name + ": " + field.value + "\r\n");
- }
- }
- writer.flush();
- }
-
- private static class Field {
- final String name;
- final String value;
-
- public Field(String name, String value) {
- this.name = name;
- this.value = value;
- }
-
- @Override
- public String toString() {
- return name + "=" + value;
- }
- }
-
- @Override
- public String toString() {
- return (mFields == null) ? null : mFields.toString();
- }
-
- public final static boolean arrayContains(Object[] a, Object o) {
- int index = arrayIndex(a, o);
- return (index >= 0);
- }
-
- public final static int arrayIndex(Object[] a, Object o) {
- for (int i = 0, count = a.length; i < count; i++) {
- if (a[i].equals(o)) {
- return i;
- }
- }
- return -1;
- }
-}
diff --git a/java/com/android/voicemailomtp/mail/internet/MimeMessage.java b/java/com/android/voicemailomtp/mail/internet/MimeMessage.java
deleted file mode 100644
index a11cd6d83..000000000
--- a/java/com/android/voicemailomtp/mail/internet/MimeMessage.java
+++ /dev/null
@@ -1,675 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail.internet;
-
-import com.android.voicemailomtp.mail.Address;
-import com.android.voicemailomtp.mail.Body;
-import com.android.voicemailomtp.mail.BodyPart;
-import com.android.voicemailomtp.mail.Message;
-import com.android.voicemailomtp.mail.MessagingException;
-import com.android.voicemailomtp.mail.Multipart;
-import com.android.voicemailomtp.mail.Part;
-import com.android.voicemailomtp.mail.utils.LogUtils;
-
-import org.apache.james.mime4j.BodyDescriptor;
-import org.apache.james.mime4j.ContentHandler;
-import org.apache.james.mime4j.EOLConvertingInputStream;
-import org.apache.james.mime4j.MimeStreamParser;
-import org.apache.james.mime4j.field.DateTimeField;
-import org.apache.james.mime4j.field.Field;
-
-import android.text.TextUtils;
-
-import java.io.BufferedWriter;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.Locale;
-import java.util.Stack;
-import java.util.regex.Pattern;
-
-/**
- * An implementation of Message that stores all of its metadata in RFC 822 and
- * RFC 2045 style headers.
- *
- * NOTE: Automatic generation of a local message-id is becoming unwieldy and should be removed.
- * It would be better to simply do it explicitly on local creation of new outgoing messages.
- */
-public class MimeMessage extends Message {
- private MimeHeader mHeader;
- private MimeHeader mExtendedHeader;
-
- // NOTE: The fields here are transcribed out of headers, and values stored here will supersede
- // the values found in the headers. Use caution to prevent any out-of-phase errors. In
- // particular, any adds/changes/deletes here must be echoed by changes in the parse() function.
- private Address[] mFrom;
- private Address[] mTo;
- private Address[] mCc;
- private Address[] mBcc;
- private Address[] mReplyTo;
- private Date mSentDate;
- private Body mBody;
- protected int mSize;
- private boolean mInhibitLocalMessageId = false;
- private boolean mComplete = true;
-
- // Shared random source for generating local message-id values
- private static final java.util.Random sRandom = new java.util.Random();
-
- // In MIME, en_US-like date format should be used. In other words "MMM" should be encoded to
- // "Jan", not the other localized format like "Ene" (meaning January in locale es).
- // This conversion is used when generating outgoing MIME messages. Incoming MIME date
- // headers are parsed by org.apache.james.mime4j.field.DateTimeField which does not have any
- // localization code.
- private static final SimpleDateFormat DATE_FORMAT =
- new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
-
- // regex that matches content id surrounded by "<>" optionally.
- private static final Pattern REMOVE_OPTIONAL_BRACKETS = Pattern.compile("^<?([^>]+)>?$");
- // regex that matches end of line.
- private static final Pattern END_OF_LINE = Pattern.compile("\r?\n");
-
- public MimeMessage() {
- mHeader = null;
- }
-
- /**
- * Generate a local message id. This is only used when none has been assigned, and is
- * installed lazily. Any remote (typically server-assigned) message id takes precedence.
- * @return a long, locally-generated message-ID value
- */
- private static String generateMessageId() {
- final StringBuilder sb = new StringBuilder();
- sb.append("<");
- for (int i = 0; i < 24; i++) {
- // We'll use a 5-bit range (0..31)
- final int value = sRandom.nextInt() & 31;
- final char c = "0123456789abcdefghijklmnopqrstuv".charAt(value);
- sb.append(c);
- }
- sb.append(".");
- sb.append(Long.toString(System.currentTimeMillis()));
- sb.append("@email.android.com>");
- return sb.toString();
- }
-
- /**
- * Parse the given InputStream using Apache Mime4J to build a MimeMessage.
- *
- * @param in InputStream providing message content
- * @throws IOException
- * @throws MessagingException
- */
- public MimeMessage(InputStream in) throws IOException, MessagingException {
- parse(in);
- }
-
- private MimeStreamParser init() {
- // Before parsing the input stream, clear all local fields that may be superceded by
- // the new incoming message.
- getMimeHeaders().clear();
- mInhibitLocalMessageId = true;
- mFrom = null;
- mTo = null;
- mCc = null;
- mBcc = null;
- mReplyTo = null;
- mSentDate = null;
- mBody = null;
-
- final MimeStreamParser parser = new MimeStreamParser();
- parser.setContentHandler(new MimeMessageBuilder());
- return parser;
- }
-
- protected void parse(InputStream in) throws IOException, MessagingException {
- final MimeStreamParser parser = init();
- parser.parse(new EOLConvertingInputStream(in));
- mComplete = !parser.getPrematureEof();
- }
-
- public void parse(InputStream in, EOLConvertingInputStream.Callback callback)
- throws IOException, MessagingException {
- final MimeStreamParser parser = init();
- parser.parse(new EOLConvertingInputStream(in, getSize(), callback));
- mComplete = !parser.getPrematureEof();
- }
-
- /**
- * Return the internal mHeader value, with very lazy initialization.
- * The goal is to save memory by not creating the headers until needed.
- */
- private MimeHeader getMimeHeaders() {
- if (mHeader == null) {
- mHeader = new MimeHeader();
- }
- return mHeader;
- }
-
- @Override
- public Date getReceivedDate() throws MessagingException {
- return null;
- }
-
- @Override
- public Date getSentDate() throws MessagingException {
- if (mSentDate == null) {
- try {
- DateTimeField field = (DateTimeField)Field.parse("Date: "
- + MimeUtility.unfoldAndDecode(getFirstHeader("Date")));
- mSentDate = field.getDate();
- // TODO: We should make it more clear what exceptions can be thrown here,
- // and whether they reflect a normal or error condition.
- } catch (Exception e) {
- LogUtils.v(LogUtils.TAG, "Message missing Date header");
- }
- }
- if (mSentDate == null) {
- // If we still don't have a date, fall back to "Delivery-date"
- try {
- DateTimeField field = (DateTimeField)Field.parse("Date: "
- + MimeUtility.unfoldAndDecode(getFirstHeader("Delivery-date")));
- mSentDate = field.getDate();
- // TODO: We should make it more clear what exceptions can be thrown here,
- // and whether they reflect a normal or error condition.
- } catch (Exception e) {
- LogUtils.v(LogUtils.TAG, "Message also missing Delivery-Date header");
- }
- }
- return mSentDate;
- }
-
- @Override
- public void setSentDate(Date sentDate) throws MessagingException {
- setHeader("Date", DATE_FORMAT.format(sentDate));
- this.mSentDate = sentDate;
- }
-
- @Override
- public String getContentType() throws MessagingException {
- final String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
- if (contentType == null) {
- return "text/plain";
- } else {
- return contentType;
- }
- }
-
- @Override
- public String getDisposition() throws MessagingException {
- return getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
- }
-
- @Override
- public String getContentId() throws MessagingException {
- final String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID);
- if (contentId == null) {
- return null;
- } else {
- // remove optionally surrounding brackets.
- return REMOVE_OPTIONAL_BRACKETS.matcher(contentId).replaceAll("$1");
- }
- }
-
- public boolean isComplete() {
- return mComplete;
- }
-
- @Override
- public String getMimeType() throws MessagingException {
- return MimeUtility.getHeaderParameter(getContentType(), null);
- }
-
- @Override
- public int getSize() throws MessagingException {
- return mSize;
- }
-
- /**
- * Returns a list of the given recipient type from this message. If no addresses are
- * found the method returns an empty array.
- */
- @Override
- public Address[] getRecipients(String type) throws MessagingException {
- if (type == RECIPIENT_TYPE_TO) {
- if (mTo == null) {
- mTo = Address.parse(MimeUtility.unfold(getFirstHeader("To")));
- }
- return mTo;
- } else if (type == RECIPIENT_TYPE_CC) {
- if (mCc == null) {
- mCc = Address.parse(MimeUtility.unfold(getFirstHeader("CC")));
- }
- return mCc;
- } else if (type == RECIPIENT_TYPE_BCC) {
- if (mBcc == null) {
- mBcc = Address.parse(MimeUtility.unfold(getFirstHeader("BCC")));
- }
- return mBcc;
- } else {
- throw new MessagingException("Unrecognized recipient type.");
- }
- }
-
- @Override
- public void setRecipients(String type, Address[] addresses) throws MessagingException {
- final int TO_LENGTH = 4; // "To: "
- final int CC_LENGTH = 4; // "Cc: "
- final int BCC_LENGTH = 5; // "Bcc: "
- if (type == RECIPIENT_TYPE_TO) {
- if (addresses == null || addresses.length == 0) {
- removeHeader("To");
- this.mTo = null;
- } else {
- setHeader("To", MimeUtility.fold(Address.toHeader(addresses), TO_LENGTH));
- this.mTo = addresses;
- }
- } else if (type == RECIPIENT_TYPE_CC) {
- if (addresses == null || addresses.length == 0) {
- removeHeader("CC");
- this.mCc = null;
- } else {
- setHeader("CC", MimeUtility.fold(Address.toHeader(addresses), CC_LENGTH));
- this.mCc = addresses;
- }
- } else if (type == RECIPIENT_TYPE_BCC) {
- if (addresses == null || addresses.length == 0) {
- removeHeader("BCC");
- this.mBcc = null;
- } else {
- setHeader("BCC", MimeUtility.fold(Address.toHeader(addresses), BCC_LENGTH));
- this.mBcc = addresses;
- }
- } else {
- throw new MessagingException("Unrecognized recipient type.");
- }
- }
-
- /**
- * Returns the unfolded, decoded value of the Subject header.
- */
- @Override
- public String getSubject() throws MessagingException {
- return MimeUtility.unfoldAndDecode(getFirstHeader("Subject"));
- }
-
- @Override
- public void setSubject(String subject) throws MessagingException {
- final int HEADER_NAME_LENGTH = 9; // "Subject: "
- setHeader("Subject", MimeUtility.foldAndEncode2(subject, HEADER_NAME_LENGTH));
- }
-
- @Override
- public Address[] getFrom() throws MessagingException {
- if (mFrom == null) {
- String list = MimeUtility.unfold(getFirstHeader("From"));
- if (list == null || list.length() == 0) {
- list = MimeUtility.unfold(getFirstHeader("Sender"));
- }
- mFrom = Address.parse(list);
- }
- return mFrom;
- }
-
- @Override
- public void setFrom(Address from) throws MessagingException {
- final int FROM_LENGTH = 6; // "From: "
- if (from != null) {
- setHeader("From", MimeUtility.fold(from.toHeader(), FROM_LENGTH));
- this.mFrom = new Address[] {
- from
- };
- } else {
- this.mFrom = null;
- }
- }
-
- @Override
- public Address[] getReplyTo() throws MessagingException {
- if (mReplyTo == null) {
- mReplyTo = Address.parse(MimeUtility.unfold(getFirstHeader("Reply-to")));
- }
- return mReplyTo;
- }
-
- @Override
- public void setReplyTo(Address[] replyTo) throws MessagingException {
- final int REPLY_TO_LENGTH = 10; // "Reply-to: "
- if (replyTo == null || replyTo.length == 0) {
- removeHeader("Reply-to");
- mReplyTo = null;
- } else {
- setHeader("Reply-to", MimeUtility.fold(Address.toHeader(replyTo), REPLY_TO_LENGTH));
- mReplyTo = replyTo;
- }
- }
-
- /**
- * Set the mime "Message-ID" header
- * @param messageId the new Message-ID value
- * @throws MessagingException
- */
- @Override
- public void setMessageId(String messageId) throws MessagingException {
- setHeader("Message-ID", messageId);
- }
-
- /**
- * Get the mime "Message-ID" header. This value will be preloaded with a locally-generated
- * random ID, if the value has not previously been set. Local generation can be inhibited/
- * overridden by explicitly clearing the headers, removing the message-id header, etc.
- * @return the Message-ID header string, or null if explicitly has been set to null
- */
- @Override
- public String getMessageId() throws MessagingException {
- String messageId = getFirstHeader("Message-ID");
- if (messageId == null && !mInhibitLocalMessageId) {
- messageId = generateMessageId();
- setMessageId(messageId);
- }
- return messageId;
- }
-
- @Override
- public void saveChanges() throws MessagingException {
- throw new MessagingException("saveChanges not yet implemented");
- }
-
- @Override
- public Body getBody() throws MessagingException {
- return mBody;
- }
-
- @Override
- public void setBody(Body body) throws MessagingException {
- this.mBody = body;
- if (body instanceof Multipart) {
- final Multipart multipart = ((Multipart)body);
- multipart.setParent(this);
- setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType());
- setHeader("MIME-Version", "1.0");
- }
- else if (body instanceof TextBody) {
- setHeader(MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\n charset=utf-8",
- getMimeType()));
- setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
- }
- }
-
- protected String getFirstHeader(String name) throws MessagingException {
- return getMimeHeaders().getFirstHeader(name);
- }
-
- @Override
- public void addHeader(String name, String value) throws MessagingException {
- getMimeHeaders().addHeader(name, value);
- }
-
- @Override
- public void setHeader(String name, String value) throws MessagingException {
- getMimeHeaders().setHeader(name, value);
- }
-
- @Override
- public String[] getHeader(String name) throws MessagingException {
- return getMimeHeaders().getHeader(name);
- }
-
- @Override
- public void removeHeader(String name) throws MessagingException {
- getMimeHeaders().removeHeader(name);
- if ("Message-ID".equalsIgnoreCase(name)) {
- mInhibitLocalMessageId = true;
- }
- }
-
- /**
- * Set extended header
- *
- * @param name Extended header name
- * @param value header value - flattened by removing CR-NL if any
- * remove header if value is null
- * @throws MessagingException
- */
- @Override
- public void setExtendedHeader(String name, String value) throws MessagingException {
- if (value == null) {
- if (mExtendedHeader != null) {
- mExtendedHeader.removeHeader(name);
- }
- return;
- }
- if (mExtendedHeader == null) {
- mExtendedHeader = new MimeHeader();
- }
- mExtendedHeader.setHeader(name, END_OF_LINE.matcher(value).replaceAll(""));
- }
-
- /**
- * Get extended header
- *
- * @param name Extended header name
- * @return header value - null if header does not exist
- * @throws MessagingException
- */
- @Override
- public String getExtendedHeader(String name) throws MessagingException {
- if (mExtendedHeader == null) {
- return null;
- }
- return mExtendedHeader.getFirstHeader(name);
- }
-
- /**
- * Set entire extended headers from String
- *
- * @param headers Extended header and its value - "CR-NL-separated pairs
- * if null or empty, remove entire extended headers
- * @throws MessagingException
- */
- public void setExtendedHeaders(String headers) throws MessagingException {
- if (TextUtils.isEmpty(headers)) {
- mExtendedHeader = null;
- } else {
- mExtendedHeader = new MimeHeader();
- for (final String header : END_OF_LINE.split(headers)) {
- final String[] tokens = header.split(":", 2);
- if (tokens.length != 2) {
- throw new MessagingException("Illegal extended headers: " + headers);
- }
- mExtendedHeader.setHeader(tokens[0].trim(), tokens[1].trim());
- }
- }
- }
-
- /**
- * Get entire extended headers as String
- *
- * @return "CR-NL-separated extended headers - null if extended header does not exist
- */
- public String getExtendedHeaders() {
- if (mExtendedHeader != null) {
- return mExtendedHeader.writeToString();
- }
- return null;
- }
-
- /**
- * Write message header and body to output stream
- *
- * @param out Output steam to write message header and body.
- */
- @Override
- public void writeTo(OutputStream out) throws IOException, MessagingException {
- final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
- // Force creation of local message-id
- getMessageId();
- getMimeHeaders().writeTo(out);
- // mExtendedHeader will not be write out to external output stream,
- // because it is intended to internal use.
- writer.write("\r\n");
- writer.flush();
- if (mBody != null) {
- mBody.writeTo(out);
- }
- }
-
- @Override
- public InputStream getInputStream() throws MessagingException {
- return null;
- }
-
- class MimeMessageBuilder implements ContentHandler {
- private final Stack<Object> stack = new Stack<Object>();
-
- public MimeMessageBuilder() {
- }
-
- private void expect(Class<?> c) {
- if (!c.isInstance(stack.peek())) {
- throw new IllegalStateException("Internal stack error: " + "Expected '"
- + c.getName() + "' found '" + stack.peek().getClass().getName() + "'");
- }
- }
-
- @Override
- public void startMessage() {
- if (stack.isEmpty()) {
- stack.push(MimeMessage.this);
- } else {
- expect(Part.class);
- try {
- final MimeMessage m = new MimeMessage();
- ((Part)stack.peek()).setBody(m);
- stack.push(m);
- } catch (MessagingException me) {
- throw new Error(me);
- }
- }
- }
-
- @Override
- public void endMessage() {
- expect(MimeMessage.class);
- stack.pop();
- }
-
- @Override
- public void startHeader() {
- expect(Part.class);
- }
-
- @Override
- public void field(String fieldData) {
- expect(Part.class);
- try {
- final String[] tokens = fieldData.split(":", 2);
- ((Part)stack.peek()).addHeader(tokens[0], tokens[1].trim());
- } catch (MessagingException me) {
- throw new Error(me);
- }
- }
-
- @Override
- public void endHeader() {
- expect(Part.class);
- }
-
- @Override
- public void startMultipart(BodyDescriptor bd) {
- expect(Part.class);
-
- final Part e = (Part)stack.peek();
- try {
- final MimeMultipart multiPart = new MimeMultipart(e.getContentType());
- e.setBody(multiPart);
- stack.push(multiPart);
- } catch (MessagingException me) {
- throw new Error(me);
- }
- }
-
- @Override
- public void body(BodyDescriptor bd, InputStream in) throws IOException {
- expect(Part.class);
- final Body body = MimeUtility.decodeBody(in, bd.getTransferEncoding());
- try {
- ((Part)stack.peek()).setBody(body);
- } catch (MessagingException me) {
- throw new Error(me);
- }
- }
-
- @Override
- public void endMultipart() {
- stack.pop();
- }
-
- @Override
- public void startBodyPart() {
- expect(MimeMultipart.class);
-
- try {
- final MimeBodyPart bodyPart = new MimeBodyPart();
- ((MimeMultipart)stack.peek()).addBodyPart(bodyPart);
- stack.push(bodyPart);
- } catch (MessagingException me) {
- throw new Error(me);
- }
- }
-
- @Override
- public void endBodyPart() {
- expect(BodyPart.class);
- stack.pop();
- }
-
- @Override
- public void epilogue(InputStream is) throws IOException {
- expect(MimeMultipart.class);
- final StringBuilder sb = new StringBuilder();
- int b;
- while ((b = is.read()) != -1) {
- sb.append((char)b);
- }
- // TODO: why is this commented out?
- // ((Multipart) stack.peek()).setEpilogue(sb.toString());
- }
-
- @Override
- public void preamble(InputStream is) throws IOException {
- expect(MimeMultipart.class);
- final StringBuilder sb = new StringBuilder();
- int b;
- while ((b = is.read()) != -1) {
- sb.append((char)b);
- }
- try {
- ((MimeMultipart)stack.peek()).setPreamble(sb.toString());
- } catch (MessagingException me) {
- throw new Error(me);
- }
- }
-
- @Override
- public void raw(InputStream is) throws IOException {
- throw new UnsupportedOperationException("Not supported");
- }
- }
-}
diff --git a/java/com/android/voicemailomtp/mail/internet/MimeMultipart.java b/java/com/android/voicemailomtp/mail/internet/MimeMultipart.java
deleted file mode 100644
index 111924336..000000000
--- a/java/com/android/voicemailomtp/mail/internet/MimeMultipart.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail.internet;
-
-import com.android.voicemailomtp.mail.BodyPart;
-import com.android.voicemailomtp.mail.MessagingException;
-import com.android.voicemailomtp.mail.Multipart;
-
-import java.io.BufferedWriter;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-
-public class MimeMultipart extends Multipart {
- protected String mPreamble;
-
- protected String mContentType;
-
- protected String mBoundary;
-
- protected String mSubType;
-
- public MimeMultipart() throws MessagingException {
- mBoundary = generateBoundary();
- setSubType("mixed");
- }
-
- public MimeMultipart(String contentType) throws MessagingException {
- this.mContentType = contentType;
- try {
- mSubType = MimeUtility.getHeaderParameter(contentType, null).split("/")[1];
- mBoundary = MimeUtility.getHeaderParameter(contentType, "boundary");
- if (mBoundary == null) {
- throw new MessagingException("MultiPart does not contain boundary: " + contentType);
- }
- } catch (Exception e) {
- throw new MessagingException(
- "Invalid MultiPart Content-Type; must contain subtype and boundary. ("
- + contentType + ")", e);
- }
- }
-
- public String generateBoundary() {
- StringBuffer sb = new StringBuffer();
- sb.append("----");
- for (int i = 0; i < 30; i++) {
- sb.append(Integer.toString((int)(Math.random() * 35), 36));
- }
- return sb.toString().toUpperCase();
- }
-
- public String getPreamble() throws MessagingException {
- return mPreamble;
- }
-
- public void setPreamble(String preamble) throws MessagingException {
- this.mPreamble = preamble;
- }
-
- @Override
- public String getContentType() throws MessagingException {
- return mContentType;
- }
-
- public void setSubType(String subType) throws MessagingException {
- this.mSubType = subType;
- mContentType = String.format("multipart/%s; boundary=\"%s\"", subType, mBoundary);
- }
-
- @Override
- public void writeTo(OutputStream out) throws IOException, MessagingException {
- BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
-
- if (mPreamble != null) {
- writer.write(mPreamble + "\r\n");
- }
-
- for (int i = 0, count = mParts.size(); i < count; i++) {
- BodyPart bodyPart = mParts.get(i);
- writer.write("--" + mBoundary + "\r\n");
- writer.flush();
- bodyPart.writeTo(out);
- writer.write("\r\n");
- }
-
- writer.write("--" + mBoundary + "--\r\n");
- writer.flush();
- }
-
- @Override
- public InputStream getInputStream() throws MessagingException {
- return null;
- }
-
- public String getSubTypeForTest() {
- return mSubType;
- }
-}
diff --git a/java/com/android/voicemailomtp/mail/internet/MimeUtility.java b/java/com/android/voicemailomtp/mail/internet/MimeUtility.java
deleted file mode 100644
index 4d310b0f5..000000000
--- a/java/com/android/voicemailomtp/mail/internet/MimeUtility.java
+++ /dev/null
@@ -1,416 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail.internet;
-
-import android.text.TextUtils;
-import android.util.Base64;
-import android.util.Base64DataException;
-import android.util.Base64InputStream;
-
-import com.android.voicemailomtp.mail.Body;
-import com.android.voicemailomtp.mail.BodyPart;
-import com.android.voicemailomtp.mail.Message;
-import com.android.voicemailomtp.mail.MessagingException;
-import com.android.voicemailomtp.mail.Multipart;
-import com.android.voicemailomtp.mail.Part;
-import com.android.voicemailomtp.VvmLog;
-
-import org.apache.commons.io.IOUtils;
-import org.apache.james.mime4j.codec.EncoderUtil;
-import org.apache.james.mime4j.decoder.DecoderUtil;
-import org.apache.james.mime4j.decoder.QuotedPrintableInputStream;
-import org.apache.james.mime4j.util.CharsetUtil;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-public class MimeUtility {
- private static final String LOG_TAG = "Email";
-
- public static final String MIME_TYPE_RFC822 = "message/rfc822";
- private final static Pattern PATTERN_CR_OR_LF = Pattern.compile("\r|\n");
-
- /**
- * Replace sequences of CRLF+WSP with WSP. Tries to preserve original string
- * object whenever possible.
- */
- public static String unfold(String s) {
- if (s == null) {
- return null;
- }
- Matcher patternMatcher = PATTERN_CR_OR_LF.matcher(s);
- if (patternMatcher.find()) {
- patternMatcher.reset();
- s = patternMatcher.replaceAll("");
- }
- return s;
- }
-
- public static String decode(String s) {
- if (s == null) {
- return null;
- }
- return DecoderUtil.decodeEncodedWords(s);
- }
-
- public static String unfoldAndDecode(String s) {
- return decode(unfold(s));
- }
-
- // TODO implement proper foldAndEncode
- // NOTE: When this really works, we *must* remove all calls to foldAndEncode2() to prevent
- // duplication of encoding.
- public static String foldAndEncode(String s) {
- return s;
- }
-
- /**
- * INTERIM version of foldAndEncode that will be used only by Subject: headers.
- * This is safer than implementing foldAndEncode() (see above) and risking unknown damage
- * to other headers.
- *
- * TODO: Copy this code to foldAndEncode(), get rid of this function, confirm all working OK.
- *
- * @param s original string to encode and fold
- * @param usedCharacters number of characters already used up by header name
-
- * @return the String ready to be transmitted
- */
- public static String foldAndEncode2(String s, int usedCharacters) {
- // james.mime4j.codec.EncoderUtil.java
- // encode: encodeIfNecessary(text, usage, numUsedInHeaderName)
- // Usage.TEXT_TOKENlooks like the right thing for subjects
- // use WORD_ENTITY for address/names
-
- String encoded = EncoderUtil.encodeIfNecessary(s, EncoderUtil.Usage.TEXT_TOKEN,
- usedCharacters);
-
- return fold(encoded, usedCharacters);
- }
-
- /**
- * INTERIM: From newer version of org.apache.james (but we don't want to import
- * the entire MimeUtil class).
- *
- * Splits the specified string into a multiple-line representation with
- * lines no longer than 76 characters (because the line might contain
- * encoded words; see <a href='http://www.faqs.org/rfcs/rfc2047.html'>RFC
- * 2047</a> section 2). If the string contains non-whitespace sequences
- * longer than 76 characters a line break is inserted at the whitespace
- * character following the sequence resulting in a line longer than 76
- * characters.
- *
- * @param s
- * string to split.
- * @param usedCharacters
- * number of characters already used up. Usually the number of
- * characters for header field name plus colon and one space.
- * @return a multiple-line representation of the given string.
- */
- public static String fold(String s, int usedCharacters) {
- final int maxCharacters = 76;
-
- final int length = s.length();
- if (usedCharacters + length <= maxCharacters)
- return s;
-
- StringBuilder sb = new StringBuilder();
-
- int lastLineBreak = -usedCharacters;
- int wspIdx = indexOfWsp(s, 0);
- while (true) {
- if (wspIdx == length) {
- sb.append(s.substring(Math.max(0, lastLineBreak)));
- return sb.toString();
- }
-
- int nextWspIdx = indexOfWsp(s, wspIdx + 1);
-
- if (nextWspIdx - lastLineBreak > maxCharacters) {
- sb.append(s.substring(Math.max(0, lastLineBreak), wspIdx));
- sb.append("\r\n");
- lastLineBreak = wspIdx;
- }
-
- wspIdx = nextWspIdx;
- }
- }
-
- /**
- * INTERIM: From newer version of org.apache.james (but we don't want to import
- * the entire MimeUtil class).
- *
- * Search for whitespace.
- */
- private static int indexOfWsp(String s, int fromIndex) {
- final int len = s.length();
- for (int index = fromIndex; index < len; index++) {
- char c = s.charAt(index);
- if (c == ' ' || c == '\t')
- return index;
- }
- return len;
- }
-
- /**
- * Returns the named parameter of a header field. If name is null the first
- * parameter is returned, or if there are no additional parameters in the
- * field the entire field is returned. Otherwise the named parameter is
- * searched for in a case insensitive fashion and returned. If the parameter
- * cannot be found the method returns null.
- *
- * TODO: quite inefficient with the inner trimming & splitting.
- * TODO: Also has a latent bug: uses "startsWith" to match the name, which can false-positive.
- * TODO: The doc says that for a null name you get the first param, but you get the header.
- * Should probably just fix the doc, but if other code assumes that behavior, fix the code.
- * TODO: Need to decode %-escaped strings, as in: filename="ab%22d".
- * ('+' -> ' ' conversion too? check RFC)
- *
- * @param header
- * @param name
- * @return the entire header (if name=null), the found parameter, or null
- */
- public static String getHeaderParameter(String header, String name) {
- if (header == null) {
- return null;
- }
- String[] parts = unfold(header).split(";");
- if (name == null) {
- return parts[0].trim();
- }
- String lowerCaseName = name.toLowerCase();
- for (String part : parts) {
- if (part.trim().toLowerCase().startsWith(lowerCaseName)) {
- String[] parameterParts = part.split("=", 2);
- if (parameterParts.length < 2) {
- return null;
- }
- String parameter = parameterParts[1].trim();
- if (parameter.startsWith("\"") && parameter.endsWith("\"")) {
- return parameter.substring(1, parameter.length() - 1);
- } else {
- return parameter;
- }
- }
- }
- return null;
- }
-
- /**
- * Reads the Part's body and returns a String based on any charset conversion that needed
- * to be done.
- * @param part The part containing a body
- * @return a String containing the converted text in the body, or null if there was no text
- * or an error during conversion.
- */
- public static String getTextFromPart(Part part) {
- try {
- if (part != null && part.getBody() != null) {
- InputStream in = part.getBody().getInputStream();
- String mimeType = part.getMimeType();
- if (mimeType != null && MimeUtility.mimeTypeMatches(mimeType, "text/*")) {
- /*
- * Now we read the part into a buffer for further processing. Because
- * the stream is now wrapped we'll remove any transfer encoding at this point.
- */
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- IOUtils.copy(in, out);
- in.close();
- in = null; // we want all of our memory back, and close might not release
-
- /*
- * We've got a text part, so let's see if it needs to be processed further.
- */
- String charset = getHeaderParameter(part.getContentType(), "charset");
- if (charset != null) {
- /*
- * See if there is conversion from the MIME charset to the Java one.
- */
- charset = CharsetUtil.toJavaCharset(charset);
- }
- /*
- * No encoding, so use us-ascii, which is the standard.
- */
- if (charset == null) {
- charset = "ASCII";
- }
- /*
- * Convert and return as new String
- */
- String result = out.toString(charset);
- out.close();
- return result;
- }
- }
-
- }
- catch (OutOfMemoryError oom) {
- /*
- * If we are not able to process the body there's nothing we can do about it. Return
- * null and let the upper layers handle the missing content.
- */
- VvmLog.e(LOG_TAG, "Unable to getTextFromPart " + oom.toString());
- }
- catch (Exception e) {
- /*
- * If we are not able to process the body there's nothing we can do about it. Return
- * null and let the upper layers handle the missing content.
- */
- VvmLog.e(LOG_TAG, "Unable to getTextFromPart " + e.toString());
- }
- return null;
- }
-
- /**
- * Returns true if the given mimeType matches the matchAgainst specification. The comparison
- * ignores case and the matchAgainst string may include "*" for a wildcard (e.g. "image/*").
- *
- * @param mimeType A MIME type to check.
- * @param matchAgainst A MIME type to check against. May include wildcards.
- * @return true if the mimeType matches
- */
- public static boolean mimeTypeMatches(String mimeType, String matchAgainst) {
- Pattern p = Pattern.compile(matchAgainst.replaceAll("\\*", "\\.\\*"),
- Pattern.CASE_INSENSITIVE);
- return p.matcher(mimeType).matches();
- }
-
- /**
- * Returns true if the given mimeType matches any of the matchAgainst specifications. The
- * comparison ignores case and the matchAgainst strings may include "*" for a wildcard
- * (e.g. "image/*").
- *
- * @param mimeType A MIME type to check.
- * @param matchAgainst An array of MIME types to check against. May include wildcards.
- * @return true if the mimeType matches any of the matchAgainst strings
- */
- public static boolean mimeTypeMatches(String mimeType, String[] matchAgainst) {
- for (String matchType : matchAgainst) {
- if (mimeTypeMatches(mimeType, matchType)) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Given an input stream and a transfer encoding, return a wrapped input stream for that
- * encoding (or the original if none is required)
- * @param in the input stream
- * @param contentTransferEncoding the content transfer encoding
- * @return a properly wrapped stream
- */
- public static InputStream getInputStreamForContentTransferEncoding(InputStream in,
- String contentTransferEncoding) {
- if (contentTransferEncoding != null) {
- contentTransferEncoding =
- MimeUtility.getHeaderParameter(contentTransferEncoding, null);
- if ("quoted-printable".equalsIgnoreCase(contentTransferEncoding)) {
- in = new QuotedPrintableInputStream(in);
- }
- else if ("base64".equalsIgnoreCase(contentTransferEncoding)) {
- in = new Base64InputStream(in, Base64.DEFAULT);
- }
- }
- return in;
- }
-
- /**
- * Removes any content transfer encoding from the stream and returns a Body.
- */
- public static Body decodeBody(InputStream in, String contentTransferEncoding)
- throws IOException {
- /*
- * We'll remove any transfer encoding by wrapping the stream.
- */
- in = getInputStreamForContentTransferEncoding(in, contentTransferEncoding);
- BinaryTempFileBody tempBody = new BinaryTempFileBody();
- OutputStream out = tempBody.getOutputStream();
- try {
- IOUtils.copy(in, out);
- } catch (Base64DataException bde) {
- // TODO Need to fix this somehow
- //String warning = "\n\n" + Email.getMessageDecodeErrorString();
- //out.write(warning.getBytes());
- } finally {
- out.close();
- }
- return tempBody;
- }
-
- /**
- * Recursively scan a Part (usually a Message) and sort out which of its children will be
- * "viewable" and which will be attachments.
- *
- * @param part The part to be broken down
- * @param viewables This arraylist will be populated with all parts that appear to be
- * the "message" (e.g. text/plain & text/html)
- * @param attachments This arraylist will be populated with all parts that appear to be
- * attachments (including inlines)
- * @throws MessagingException
- */
- public static void collectParts(Part part, ArrayList<Part> viewables,
- ArrayList<Part> attachments) throws MessagingException {
- String disposition = part.getDisposition();
- String dispositionType = MimeUtility.getHeaderParameter(disposition, null);
- // If a disposition is not specified, default to "inline"
- boolean inline =
- TextUtils.isEmpty(dispositionType) || "inline".equalsIgnoreCase(dispositionType);
- // The lower-case mime type
- String mimeType = part.getMimeType().toLowerCase();
-
- if (part.getBody() instanceof Multipart) {
- // If the part is Multipart but not alternative it's either mixed or
- // something we don't know about, which means we treat it as mixed
- // per the spec. We just process its pieces recursively.
- MimeMultipart mp = (MimeMultipart)part.getBody();
- boolean foundHtml = false;
- if (mp.getSubTypeForTest().equals("alternative")) {
- for (int i = 0; i < mp.getCount(); i++) {
- if (mp.getBodyPart(i).isMimeType("text/html")) {
- foundHtml = true;
- break;
- }
- }
- }
- for (int i = 0; i < mp.getCount(); i++) {
- // See if we have text and html
- BodyPart bp = mp.getBodyPart(i);
- // If there's html, don't bother loading text
- if (foundHtml && bp.isMimeType("text/plain")) {
- continue;
- }
- collectParts(bp, viewables, attachments);
- }
- } else if (part.getBody() instanceof Message) {
- // If the part is an embedded message we just continue to process
- // it, pulling any viewables or attachments into the running list.
- Message message = (Message)part.getBody();
- collectParts(message, viewables, attachments);
- } else if (inline && (mimeType.startsWith("text") || (mimeType.startsWith("image")))) {
- // We'll treat text and images as viewables
- viewables.add(part);
- } else {
- // Everything else is an attachment.
- attachments.add(part);
- }
- }
-}
diff --git a/java/com/android/voicemailomtp/mail/internet/TextBody.java b/java/com/android/voicemailomtp/mail/internet/TextBody.java
deleted file mode 100644
index 578193eff..000000000
--- a/java/com/android/voicemailomtp/mail/internet/TextBody.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail.internet;
-
-import android.util.Base64;
-
-import com.android.voicemailomtp.mail.Body;
-import com.android.voicemailomtp.mail.MessagingException;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.UnsupportedEncodingException;
-
-public class TextBody implements Body {
- String mBody;
-
- public TextBody(String body) {
- this.mBody = body;
- }
-
- @Override
- public void writeTo(OutputStream out) throws IOException, MessagingException {
- byte[] bytes = mBody.getBytes("UTF-8");
- out.write(Base64.encode(bytes, Base64.CRLF));
- }
-
- /**
- * Get the text of the body in it's unencoded format.
- * @return
- */
- public String getText() {
- return mBody;
- }
-
- /**
- * Returns an InputStream that reads this body's text in UTF-8 format.
- */
- @Override
- public InputStream getInputStream() throws MessagingException {
- try {
- byte[] b = mBody.getBytes("UTF-8");
- return new ByteArrayInputStream(b);
- }
- catch (UnsupportedEncodingException usee) {
- return null;
- }
- }
-}
diff --git a/java/com/android/voicemailomtp/mail/store/ImapConnection.java b/java/com/android/voicemailomtp/mail/store/ImapConnection.java
deleted file mode 100644
index 61dcf1281..000000000
--- a/java/com/android/voicemailomtp/mail/store/ImapConnection.java
+++ /dev/null
@@ -1,413 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail.store;
-
-import android.util.ArraySet;
-import android.util.Base64;
-import com.android.voicemailomtp.mail.AuthenticationFailedException;
-import com.android.voicemailomtp.mail.CertificateValidationException;
-import com.android.voicemailomtp.mail.MailTransport;
-import com.android.voicemailomtp.mail.MessagingException;
-import com.android.voicemailomtp.mail.store.ImapStore.ImapException;
-import com.android.voicemailomtp.mail.store.imap.DigestMd5Utils;
-import com.android.voicemailomtp.mail.store.imap.ImapConstants;
-import com.android.voicemailomtp.mail.store.imap.ImapResponse;
-import com.android.voicemailomtp.mail.store.imap.ImapResponseParser;
-import com.android.voicemailomtp.mail.store.imap.ImapUtility;
-import com.android.voicemailomtp.mail.utils.LogUtils;
-import com.android.voicemailomtp.OmtpEvents;
-import com.android.voicemailomtp.VvmLog;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicInteger;
-import javax.net.ssl.SSLException;
-
-/**
- * A cacheable class that stores the details for a single IMAP connection.
- */
-public class ImapConnection {
- private final String TAG = "ImapConnection";
-
- private String mLoginPhrase;
- private ImapStore mImapStore;
- private MailTransport mTransport;
- private ImapResponseParser mParser;
- private Set<String> mCapabilities = new ArraySet<>();
-
- static final String IMAP_REDACTED_LOG = "[IMAP command redacted]";
-
- /**
- * Next tag to use. All connections associated to the same ImapStore instance share the same
- * counter to make tests simpler.
- * (Some of the tests involve multiple connections but only have a single counter to track the
- * tag.)
- */
- private final AtomicInteger mNextCommandTag = new AtomicInteger(0);
-
- ImapConnection(ImapStore store) {
- setStore(store);
- }
-
- void setStore(ImapStore store) {
- // TODO: maybe we should throw an exception if the connection is not closed here,
- // if it's not currently closed, then we won't reopen it, so if the credentials have
- // changed, the connection will not be reestablished.
- mImapStore = store;
- mLoginPhrase = null;
- }
-
- /**
- * Generates and returns the phrase to be used for authentication. This will be a LOGIN with
- * username and password.
- *
- * @return the login command string to sent to the IMAP server
- */
- String getLoginPhrase() {
- if (mLoginPhrase == null) {
- if (mImapStore.getUsername() != null && mImapStore.getPassword() != null) {
- // build the LOGIN string once (instead of over-and-over again.)
- // apply the quoting here around the built-up password
- mLoginPhrase = ImapConstants.LOGIN + " " + mImapStore.getUsername() + " "
- + ImapUtility.imapQuoted(mImapStore.getPassword());
- }
- }
- return mLoginPhrase;
- }
-
- public void open() throws IOException, MessagingException {
- if (mTransport != null && mTransport.isOpen()) {
- return;
- }
-
- try {
- // copy configuration into a clean transport, if necessary
- if (mTransport == null) {
- mTransport = mImapStore.cloneTransport();
- }
-
- mTransport.open();
-
- createParser();
-
- // The server should greet us with something like
- // * OK IMAP4rev1 Server
- // consume the response before doing anything else.
- ImapResponse response = mParser.readResponse(false);
- if (!response.isOk()) {
- mImapStore.getImapHelper()
- .handleEvent(OmtpEvents.DATA_INVALID_INITIAL_SERVER_RESPONSE);
- throw new MessagingException(
- MessagingException.AUTHENTICATION_FAILED_OR_SERVER_ERROR,
- "Invalid server initial response");
- }
-
- queryCapability();
-
- maybeDoStartTls();
-
- // LOGIN
- doLogin();
- } catch (SSLException e) {
- LogUtils.d(TAG, "SSLException ", e);
- mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_SSL_EXCEPTION);
- throw new CertificateValidationException(e.getMessage(), e);
- } catch (IOException ioe) {
- LogUtils.d(TAG, "IOException", ioe);
- mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_IOE_ON_OPEN);
- throw ioe;
- } finally {
- destroyResponses();
- }
- }
-
- void logout() {
- try {
- sendCommand(ImapConstants.LOGOUT, false);
- if (!mParser.readResponse(true).is(0, ImapConstants.BYE)) {
- VvmLog.e(TAG, "Server did not respond LOGOUT with BYE");
- }
- if (!mParser.readResponse(false).isOk()) {
- VvmLog.e(TAG, "Server did not respond OK after LOGOUT");
- }
- } catch (IOException | MessagingException e) {
- VvmLog.e(TAG, "Error while logging out:" + e);
- }
- }
-
- /**
- * Closes the connection and releases all resources. This connection can not be used again
- * until {@link #setStore(ImapStore)} is called.
- */
- void close() {
- if (mTransport != null) {
- logout();
- mTransport.close();
- mTransport = null;
- }
- destroyResponses();
- mParser = null;
- mImapStore = null;
- }
-
- /**
- * Attempts to convert the connection into secure connection.
- */
- private void maybeDoStartTls() throws IOException, MessagingException {
- // STARTTLS is required in the OMTP standard but not every implementation support it.
- // Make sure the server does have this capability
- if (hasCapability(ImapConstants.CAPABILITY_STARTTLS)) {
- executeSimpleCommand(ImapConstants.STARTTLS);
- mTransport.reopenTls();
- createParser();
- // The cached capabilities should be refreshed after TLS is established.
- queryCapability();
- }
- }
-
- /**
- * Logs into the IMAP server
- */
- private void doLogin() throws IOException, MessagingException, AuthenticationFailedException {
- try {
- if (mCapabilities.contains(ImapConstants.CAPABILITY_AUTH_DIGEST_MD5)) {
- doDigestMd5Auth();
- } else {
- executeSimpleCommand(getLoginPhrase(), true);
- }
- } catch (ImapException ie) {
- LogUtils.d(TAG, "ImapException", ie);
- String status = ie.getStatus();
- String statusMessage = ie.getStatusMessage();
- String alertText = ie.getAlertText();
-
- if (ImapConstants.NO.equals(status)) {
- switch (statusMessage) {
- case ImapConstants.NO_UNKNOWN_USER:
- mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_UNKNOWN_USER);
- break;
- case ImapConstants.NO_UNKNOWN_CLIENT:
- mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_UNKNOWN_DEVICE);
- break;
- case ImapConstants.NO_INVALID_PASSWORD:
- mImapStore.getImapHelper()
- .handleEvent(OmtpEvents.DATA_AUTH_INVALID_PASSWORD);
- break;
- case ImapConstants.NO_MAILBOX_NOT_INITIALIZED:
- mImapStore.getImapHelper()
- .handleEvent(OmtpEvents.DATA_AUTH_MAILBOX_NOT_INITIALIZED);
- break;
- case ImapConstants.NO_SERVICE_IS_NOT_PROVISIONED:
- mImapStore.getImapHelper()
- .handleEvent(OmtpEvents.DATA_AUTH_SERVICE_NOT_PROVISIONED);
- break;
- case ImapConstants.NO_SERVICE_IS_NOT_ACTIVATED:
- mImapStore.getImapHelper()
- .handleEvent(OmtpEvents.DATA_AUTH_SERVICE_NOT_ACTIVATED);
- break;
- case ImapConstants.NO_USER_IS_BLOCKED:
- mImapStore.getImapHelper()
- .handleEvent(OmtpEvents.DATA_AUTH_USER_IS_BLOCKED);
- break;
- case ImapConstants.NO_APPLICATION_ERROR:
- mImapStore.getImapHelper()
- .handleEvent(OmtpEvents.DATA_REJECTED_SERVER_RESPONSE);
- default:
- mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_BAD_IMAP_CREDENTIAL);
- }
- throw new AuthenticationFailedException(alertText, ie);
- }
-
- mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_REJECTED_SERVER_RESPONSE);
- throw new MessagingException(alertText, ie);
- }
- }
-
- private void doDigestMd5Auth() throws IOException, MessagingException {
-
- // Initiate the authentication.
- // The server will issue us a challenge, asking to run MD5 on the nonce with our password
- // and other data, including the cnonce we randomly generated.
- //
- // C: a AUTHENTICATE DIGEST-MD5
- // S: (BASE64) realm="elwood.innosoft.com",nonce="OA6MG9tEQGm2hh",qop="auth",
- // algorithm=md5-sess,charset=utf-8
- List<ImapResponse> responses = executeSimpleCommand(
- ImapConstants.AUTHENTICATE + " " + ImapConstants.AUTH_DIGEST_MD5);
- String decodedChallenge = decodeBase64(responses.get(0).getStringOrEmpty(0).getString());
-
- Map<String, String> challenge = DigestMd5Utils.parseDigestMessage(decodedChallenge);
- DigestMd5Utils.Data data = new DigestMd5Utils.Data(mImapStore, mTransport, challenge);
-
- String response = data.createResponse();
- // Respond to the challenge. If the server accepts it, it will reply a response-auth which
- // is the MD5 of our password and the cnonce we've provided, to prove the server does know
- // the password.
- //
- // C: (BASE64) charset=utf-8,username="chris",realm="elwood.innosoft.com",
- // nonce="OA6MG9tEQGm2hh",nc=00000001,cnonce="OA6MHXh6VqTrRk",
- // digest-uri="imap/elwood.innosoft.com",
- // response=d388dad90d4bbd760a152321f2143af7,qop=auth
- // S: (BASE64) rspauth=ea40f60335c427b5527b84dbabcdfffd
-
- responses = executeContinuationResponse(encodeBase64(response), true);
-
- // Verify response-auth.
- // If failed verifyResponseAuth() will throw a MessagingException, terminating the
- // connection
- String decodedResponseAuth = decodeBase64(responses.get(0).getStringOrEmpty(0).getString());
- data.verifyResponseAuth(decodedResponseAuth);
-
- // Send a empty response to indicate we've accepted the response-auth
- //
- // C: (empty)
- // S: a OK User logged in
- executeContinuationResponse("", false);
-
- }
-
- private static String decodeBase64(String string) {
- return new String(Base64.decode(string, Base64.DEFAULT));
- }
-
- private static String encodeBase64(String string) {
- return Base64.encodeToString(string.getBytes(), Base64.NO_WRAP);
- }
-
- private void queryCapability() throws IOException, MessagingException {
- List<ImapResponse> responses = executeSimpleCommand(ImapConstants.CAPABILITY);
- mCapabilities.clear();
- Set<String> disabledCapabilities = mImapStore.getImapHelper().getConfig()
- .getDisabledCapabilities();
- for (ImapResponse response : responses) {
- if (response.isTagged()) {
- continue;
- }
- for (int i = 0; i < response.size(); i++) {
- String capability = response.getStringOrEmpty(i).getString();
- if (disabledCapabilities != null) {
- if (!disabledCapabilities.contains(capability)) {
- mCapabilities.add(capability);
- }
- } else {
- mCapabilities.add(capability);
- }
- }
- }
-
- LogUtils.d(TAG, "Capabilities: " + mCapabilities.toString());
- }
-
- private boolean hasCapability(String capability) {
- return mCapabilities.contains(capability);
- }
- /**
- * Create an {@link ImapResponseParser} from {@code mTransport.getInputStream()} and
- * set it to {@link #mParser}.
- *
- * If we already have an {@link ImapResponseParser}, we
- * {@link #destroyResponses()} and throw it away.
- */
- private void createParser() {
- destroyResponses();
- mParser = new ImapResponseParser(mTransport.getInputStream());
- }
-
-
- public void destroyResponses() {
- if (mParser != null) {
- mParser.destroyResponses();
- }
- }
-
- public ImapResponse readResponse() throws IOException, MessagingException {
- return mParser.readResponse(false);
- }
-
- public List<ImapResponse> executeSimpleCommand(String command)
- throws IOException, MessagingException{
- return executeSimpleCommand(command, false);
- }
-
- /**
- * Send a single command to the server. The command will be preceded by an IMAP command
- * tag and followed by \r\n (caller need not supply them).
- * Execute a simple command at the server, a simple command being one that is sent in a single
- * line of text
- *
- * @param command the command to send to the server
- * @param sensitive whether the command should be redacted in logs (used for login)
- * @return a list of ImapResponses
- * @throws IOException
- * @throws MessagingException
- */
- public List<ImapResponse> executeSimpleCommand(String command, boolean sensitive)
- throws IOException, MessagingException {
- // TODO: It may be nice to catch IOExceptions and close the connection here.
- // Currently, we expect callers to do that, but if they fail to we'll be in a broken state.
- sendCommand(command, sensitive);
- return getCommandResponses();
- }
-
- public String sendCommand(String command, boolean sensitive)
- throws IOException, MessagingException {
- open();
-
- if (mTransport == null) {
- throw new IOException("Null transport");
- }
- String tag = Integer.toString(mNextCommandTag.incrementAndGet());
- String commandToSend = tag + " " + command;
- mTransport.writeLine(commandToSend, (sensitive ? IMAP_REDACTED_LOG : command));
- return tag;
- }
-
- List<ImapResponse> executeContinuationResponse(String response, boolean sensitive)
- throws IOException, MessagingException {
- mTransport.writeLine(response, (sensitive ? IMAP_REDACTED_LOG : response));
- return getCommandResponses();
- }
-
- /**
- * Read and return all of the responses from the most recent command sent to the server
- *
- * @return a list of ImapResponses
- * @throws IOException
- * @throws MessagingException
- */
- List<ImapResponse> getCommandResponses()
- throws IOException, MessagingException {
- final List<ImapResponse> responses = new ArrayList<ImapResponse>();
- ImapResponse response;
- do {
- response = mParser.readResponse(false);
- responses.add(response);
- } while (!(response.isTagged() || response.isContinuationRequest()));
-
- if (!(response.isOk() || response.isContinuationRequest())) {
- final String toString = response.toString();
- final String status = response.getStatusOrEmpty().getString();
- final String statusMessage = response.getStatusResponseTextOrEmpty().getString();
- final String alert = response.getAlertTextOrEmpty().getString();
- final String responseCode = response.getResponseCodeOrEmpty().getString();
- destroyResponses();
- throw new ImapException(toString, status, statusMessage, alert, responseCode);
- }
- return responses;
- }
-}
diff --git a/java/com/android/voicemailomtp/mail/store/ImapFolder.java b/java/com/android/voicemailomtp/mail/store/ImapFolder.java
deleted file mode 100644
index eca349876..000000000
--- a/java/com/android/voicemailomtp/mail/store/ImapFolder.java
+++ /dev/null
@@ -1,784 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail.store;
-
-import android.content.Context;
-import android.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
-import android.text.TextUtils;
-import android.util.Base64DataException;
-import com.android.voicemailomtp.OmtpEvents;
-import com.android.voicemailomtp.mail.AuthenticationFailedException;
-import com.android.voicemailomtp.mail.Body;
-import com.android.voicemailomtp.mail.FetchProfile;
-import com.android.voicemailomtp.mail.Flag;
-import com.android.voicemailomtp.mail.Message;
-import com.android.voicemailomtp.mail.MessagingException;
-import com.android.voicemailomtp.mail.Part;
-import com.android.voicemailomtp.mail.internet.BinaryTempFileBody;
-import com.android.voicemailomtp.mail.internet.MimeBodyPart;
-import com.android.voicemailomtp.mail.internet.MimeHeader;
-import com.android.voicemailomtp.mail.internet.MimeMultipart;
-import com.android.voicemailomtp.mail.internet.MimeUtility;
-import com.android.voicemailomtp.mail.store.ImapStore.ImapException;
-import com.android.voicemailomtp.mail.store.ImapStore.ImapMessage;
-import com.android.voicemailomtp.mail.store.imap.ImapConstants;
-import com.android.voicemailomtp.mail.store.imap.ImapElement;
-import com.android.voicemailomtp.mail.store.imap.ImapList;
-import com.android.voicemailomtp.mail.store.imap.ImapResponse;
-import com.android.voicemailomtp.mail.store.imap.ImapString;
-import com.android.voicemailomtp.mail.utils.LogUtils;
-import com.android.voicemailomtp.mail.utils.Utility;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Locale;
-
-public class ImapFolder {
- private static final String TAG = "ImapFolder";
- private final static String[] PERMANENT_FLAGS =
- { Flag.DELETED, Flag.SEEN, Flag.FLAGGED, Flag.ANSWERED };
- private static final int COPY_BUFFER_SIZE = 16*1024;
-
- private final ImapStore mStore;
- private final String mName;
- private int mMessageCount = -1;
- private ImapConnection mConnection;
- private String mMode;
- private boolean mExists;
- /** A set of hashes that can be used to track dirtiness */
- Object mHash[];
-
- public static final String MODE_READ_ONLY = "mode_read_only";
- public static final String MODE_READ_WRITE = "mode_read_write";
-
- public ImapFolder(ImapStore store, String name) {
- mStore = store;
- mName = name;
- }
-
- /**
- * Callback for each message retrieval.
- */
- public interface MessageRetrievalListener {
- public void messageRetrieved(Message message);
- }
-
- private void destroyResponses() {
- if (mConnection != null) {
- mConnection.destroyResponses();
- }
- }
-
- public void open(String mode) throws MessagingException {
- try {
- if (isOpen()) {
- throw new AssertionError("Duplicated open on ImapFolder");
- }
- synchronized (this) {
- mConnection = mStore.getConnection();
- }
- // * FLAGS (\Answered \Flagged \Deleted \Seen \Draft NonJunk
- // $MDNSent)
- // * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft
- // NonJunk $MDNSent \*)] Flags permitted.
- // * 23 EXISTS
- // * 0 RECENT
- // * OK [UIDVALIDITY 1125022061] UIDs valid
- // * OK [UIDNEXT 57576] Predicted next UID
- // 2 OK [READ-WRITE] Select completed.
- try {
- doSelect();
- } catch (IOException ioe) {
- throw ioExceptionHandler(mConnection, ioe);
- } finally {
- destroyResponses();
- }
- } catch (AuthenticationFailedException e) {
- // Don't cache this connection, so we're forced to try connecting/login again
- mConnection = null;
- close(false);
- throw e;
- } catch (MessagingException e) {
- mExists = false;
- close(false);
- throw e;
- }
- }
-
- public boolean isOpen() {
- return mExists && mConnection != null;
- }
-
- public String getMode() {
- return mMode;
- }
-
- public void close(boolean expunge) {
- if (expunge) {
- try {
- expunge();
- } catch (MessagingException e) {
- LogUtils.e(TAG, e, "Messaging Exception");
- }
- }
- mMessageCount = -1;
- synchronized (this) {
- mConnection = null;
- }
- }
-
- public int getMessageCount() {
- return mMessageCount;
- }
-
- String[] getSearchUids(List<ImapResponse> responses) {
- // S: * SEARCH 2 3 6
- final ArrayList<String> uids = new ArrayList<String>();
- for (ImapResponse response : responses) {
- if (!response.isDataResponse(0, ImapConstants.SEARCH)) {
- continue;
- }
- // Found SEARCH response data
- for (int i = 1; i < response.size(); i++) {
- ImapString s = response.getStringOrEmpty(i);
- if (s.isString()) {
- uids.add(s.getString());
- }
- }
- }
- return uids.toArray(Utility.EMPTY_STRINGS);
- }
-
- @VisibleForTesting
- String[] searchForUids(String searchCriteria) throws MessagingException {
- checkOpen();
- try {
- try {
- final String command = ImapConstants.UID_SEARCH + " " + searchCriteria;
- final String[] result = getSearchUids(mConnection.executeSimpleCommand(command));
- LogUtils.d(TAG, "searchForUids '" + searchCriteria + "' results: " +
- result.length);
- return result;
- } catch (ImapException me) {
- LogUtils.d(TAG, "ImapException in search: " + searchCriteria, me);
- return Utility.EMPTY_STRINGS; // Not found
- } catch (IOException ioe) {
- LogUtils.d(TAG, "IOException in search: " + searchCriteria, ioe);
- mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
- throw ioExceptionHandler(mConnection, ioe);
- }
- } finally {
- destroyResponses();
- }
- }
-
- @Nullable
- public Message getMessage(String uid) throws MessagingException {
- checkOpen();
-
- final String[] uids = searchForUids(ImapConstants.UID + " " + uid);
- for (int i = 0; i < uids.length; i++) {
- if (uids[i].equals(uid)) {
- return new ImapMessage(uid, this);
- }
- }
- LogUtils.e(TAG, "UID " + uid + " not found on server");
- return null;
- }
-
- @VisibleForTesting
- protected static boolean isAsciiString(String str) {
- int len = str.length();
- for (int i = 0; i < len; i++) {
- char c = str.charAt(i);
- if (c >= 128) return false;
- }
- return true;
- }
-
- public Message[] getMessages(String[] uids) throws MessagingException {
- if (uids == null) {
- uids = searchForUids("1:* NOT DELETED");
- }
- return getMessagesInternal(uids);
- }
-
- public Message[] getMessagesInternal(String[] uids) {
- final ArrayList<Message> messages = new ArrayList<Message>(uids.length);
- for (int i = 0; i < uids.length; i++) {
- final String uid = uids[i];
- final ImapMessage message = new ImapMessage(uid, this);
- messages.add(message);
- }
- return messages.toArray(Message.EMPTY_ARRAY);
- }
-
- public void fetch(Message[] messages, FetchProfile fp,
- MessageRetrievalListener listener) throws MessagingException {
- try {
- fetchInternal(messages, fp, listener);
- } catch (RuntimeException e) { // Probably a parser error.
- LogUtils.w(TAG, "Exception detected: " + e.getMessage());
- throw e;
- }
- }
-
- public void fetchInternal(Message[] messages, FetchProfile fp,
- MessageRetrievalListener listener) throws MessagingException {
- if (messages.length == 0) {
- return;
- }
- checkOpen();
- HashMap<String, Message> messageMap = new HashMap<String, Message>();
- for (Message m : messages) {
- messageMap.put(m.getUid(), m);
- }
-
- /*
- * Figure out what command we are going to run:
- * FLAGS - UID FETCH (FLAGS)
- * ENVELOPE - UID FETCH (INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[
- * HEADER.FIELDS (date subject from content-type to cc)])
- * STRUCTURE - UID FETCH (BODYSTRUCTURE)
- * BODY_SANE - UID FETCH (BODY.PEEK[]<0.N>) where N = max bytes returned
- * BODY - UID FETCH (BODY.PEEK[])
- * Part - UID FETCH (BODY.PEEK[ID]) where ID = mime part ID
- */
-
- final LinkedHashSet<String> fetchFields = new LinkedHashSet<String>();
-
- fetchFields.add(ImapConstants.UID);
- if (fp.contains(FetchProfile.Item.FLAGS)) {
- fetchFields.add(ImapConstants.FLAGS);
- }
- if (fp.contains(FetchProfile.Item.ENVELOPE)) {
- fetchFields.add(ImapConstants.INTERNALDATE);
- fetchFields.add(ImapConstants.RFC822_SIZE);
- fetchFields.add(ImapConstants.FETCH_FIELD_HEADERS);
- }
- if (fp.contains(FetchProfile.Item.STRUCTURE)) {
- fetchFields.add(ImapConstants.BODYSTRUCTURE);
- }
-
- if (fp.contains(FetchProfile.Item.BODY_SANE)) {
- fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_SANE);
- }
- if (fp.contains(FetchProfile.Item.BODY)) {
- fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK);
- }
-
- // TODO Why are we only fetching the first part given?
- final Part fetchPart = fp.getFirstPart();
- if (fetchPart != null) {
- final String[] partIds =
- fetchPart.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA);
- // TODO Why can a single part have more than one Id? And why should we only fetch
- // the first id if there are more than one?
- if (partIds != null) {
- fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_BARE
- + "[" + partIds[0] + "]");
- }
- }
-
- try {
- mConnection.sendCommand(String.format(Locale.US,
- ImapConstants.UID_FETCH + " %s (%s)", ImapStore.joinMessageUids(messages),
- Utility.combine(fetchFields.toArray(new String[fetchFields.size()]), ' ')
- ), false);
- ImapResponse response;
- do {
- response = null;
- try {
- response = mConnection.readResponse();
-
- if (!response.isDataResponse(1, ImapConstants.FETCH)) {
- continue; // Ignore
- }
- final ImapList fetchList = response.getListOrEmpty(2);
- final String uid = fetchList.getKeyedStringOrEmpty(ImapConstants.UID)
- .getString();
- if (TextUtils.isEmpty(uid)) continue;
-
- ImapMessage message = (ImapMessage) messageMap.get(uid);
- if (message == null) continue;
-
- if (fp.contains(FetchProfile.Item.FLAGS)) {
- final ImapList flags =
- fetchList.getKeyedListOrEmpty(ImapConstants.FLAGS);
- for (int i = 0, count = flags.size(); i < count; i++) {
- final ImapString flag = flags.getStringOrEmpty(i);
- if (flag.is(ImapConstants.FLAG_DELETED)) {
- message.setFlagInternal(Flag.DELETED, true);
- } else if (flag.is(ImapConstants.FLAG_ANSWERED)) {
- message.setFlagInternal(Flag.ANSWERED, true);
- } else if (flag.is(ImapConstants.FLAG_SEEN)) {
- message.setFlagInternal(Flag.SEEN, true);
- } else if (flag.is(ImapConstants.FLAG_FLAGGED)) {
- message.setFlagInternal(Flag.FLAGGED, true);
- }
- }
- }
- if (fp.contains(FetchProfile.Item.ENVELOPE)) {
- final Date internalDate = fetchList.getKeyedStringOrEmpty(
- ImapConstants.INTERNALDATE).getDateOrNull();
- final int size = fetchList.getKeyedStringOrEmpty(
- ImapConstants.RFC822_SIZE).getNumberOrZero();
- final String header = fetchList.getKeyedStringOrEmpty(
- ImapConstants.BODY_BRACKET_HEADER, true).getString();
-
- message.setInternalDate(internalDate);
- message.setSize(size);
- message.parse(Utility.streamFromAsciiString(header));
- }
- if (fp.contains(FetchProfile.Item.STRUCTURE)) {
- ImapList bs = fetchList.getKeyedListOrEmpty(
- ImapConstants.BODYSTRUCTURE);
- if (!bs.isEmpty()) {
- try {
- parseBodyStructure(bs, message, ImapConstants.TEXT);
- } catch (MessagingException e) {
- LogUtils.v(TAG, e, "Error handling message");
- message.setBody(null);
- }
- }
- }
- if (fp.contains(FetchProfile.Item.BODY)
- || fp.contains(FetchProfile.Item.BODY_SANE)) {
- // Body is keyed by "BODY[]...".
- // Previously used "BODY[..." but this can be confused with "BODY[HEADER..."
- // TODO Should we accept "RFC822" as well??
- ImapString body = fetchList.getKeyedStringOrEmpty("BODY[]", true);
- InputStream bodyStream = body.getAsStream();
- message.parse(bodyStream);
- }
- if (fetchPart != null) {
- InputStream bodyStream =
- fetchList.getKeyedStringOrEmpty("BODY[", true).getAsStream();
- String encodings[] = fetchPart.getHeader(
- MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING);
-
- String contentTransferEncoding = null;
- if (encodings != null && encodings.length > 0) {
- contentTransferEncoding = encodings[0];
- } else {
- // According to http://tools.ietf.org/html/rfc2045#section-6.1
- // "7bit" is the default.
- contentTransferEncoding = "7bit";
- }
-
- try {
- // TODO Don't create 2 temp files.
- // decodeBody creates BinaryTempFileBody, but we could avoid this
- // if we implement ImapStringBody.
- // (We'll need to share a temp file. Protect it with a ref-count.)
- message.setBody(decodeBody(mStore.getContext(), bodyStream,
- contentTransferEncoding, fetchPart.getSize(), listener));
- } catch(Exception e) {
- // TODO: Figure out what kinds of exceptions might actually be thrown
- // from here. This blanket catch-all is because we're not sure what to
- // do if we don't have a contentTransferEncoding, and we don't have
- // time to figure out what exceptions might be thrown.
- LogUtils.e(TAG, "Error fetching body %s", e);
- }
- }
-
- if (listener != null) {
- listener.messageRetrieved(message);
- }
- } finally {
- destroyResponses();
- }
- } while (!response.isTagged());
- } catch (IOException ioe) {
- mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
- throw ioExceptionHandler(mConnection, ioe);
- }
- }
-
- /**
- * Removes any content transfer encoding from the stream and returns a Body.
- * This code is taken/condensed from MimeUtility.decodeBody
- */
- private static Body decodeBody(Context context,InputStream in, String contentTransferEncoding,
- int size, MessageRetrievalListener listener) throws IOException {
- // Get a properly wrapped input stream
- in = MimeUtility.getInputStreamForContentTransferEncoding(in, contentTransferEncoding);
- BinaryTempFileBody tempBody = new BinaryTempFileBody();
- OutputStream out = tempBody.getOutputStream();
- try {
- byte[] buffer = new byte[COPY_BUFFER_SIZE];
- int n = 0;
- int count = 0;
- while (-1 != (n = in.read(buffer))) {
- out.write(buffer, 0, n);
- count += n;
- }
- } catch (Base64DataException bde) {
- String warning = "\n\nThere was an error while decoding the message.";
- out.write(warning.getBytes());
- } finally {
- out.close();
- }
- return tempBody;
- }
-
- public String[] getPermanentFlags() {
- return PERMANENT_FLAGS;
- }
-
- /**
- * Handle any untagged responses that the caller doesn't care to handle themselves.
- * @param responses
- */
- private void handleUntaggedResponses(List<ImapResponse> responses) {
- for (ImapResponse response : responses) {
- handleUntaggedResponse(response);
- }
- }
-
- /**
- * Handle an untagged response that the caller doesn't care to handle themselves.
- * @param response
- */
- private void handleUntaggedResponse(ImapResponse response) {
- if (response.isDataResponse(1, ImapConstants.EXISTS)) {
- mMessageCount = response.getStringOrEmpty(0).getNumberOrZero();
- }
- }
-
- private static void parseBodyStructure(ImapList bs, Part part, String id)
- throws MessagingException {
- if (bs.getElementOrNone(0).isList()) {
- /*
- * This is a multipart/*
- */
- MimeMultipart mp = new MimeMultipart();
- for (int i = 0, count = bs.size(); i < count; i++) {
- ImapElement e = bs.getElementOrNone(i);
- if (e.isList()) {
- /*
- * For each part in the message we're going to add a new BodyPart and parse
- * into it.
- */
- MimeBodyPart bp = new MimeBodyPart();
- if (id.equals(ImapConstants.TEXT)) {
- parseBodyStructure(bs.getListOrEmpty(i), bp, Integer.toString(i + 1));
-
- } else {
- parseBodyStructure(bs.getListOrEmpty(i), bp, id + "." + (i + 1));
- }
- mp.addBodyPart(bp);
-
- } else {
- if (e.isString()) {
- mp.setSubType(bs.getStringOrEmpty(i).getString().toLowerCase(Locale.US));
- }
- break; // Ignore the rest of the list.
- }
- }
- part.setBody(mp);
- } else {
- /*
- * This is a body. We need to add as much information as we can find out about
- * it to the Part.
- */
-
- /*
- body type
- body subtype
- body parameter parenthesized list
- body id
- body description
- body encoding
- body size
- */
-
- final ImapString type = bs.getStringOrEmpty(0);
- final ImapString subType = bs.getStringOrEmpty(1);
- final String mimeType =
- (type.getString() + "/" + subType.getString()).toLowerCase(Locale.US);
-
- final ImapList bodyParams = bs.getListOrEmpty(2);
- final ImapString cid = bs.getStringOrEmpty(3);
- final ImapString encoding = bs.getStringOrEmpty(5);
- final int size = bs.getStringOrEmpty(6).getNumberOrZero();
-
- if (MimeUtility.mimeTypeMatches(mimeType, MimeUtility.MIME_TYPE_RFC822)) {
- // A body type of type MESSAGE and subtype RFC822
- // contains, immediately after the basic fields, the
- // envelope structure, body structure, and size in
- // text lines of the encapsulated message.
- // [MESSAGE, RFC822, [NAME, filename.eml], NIL, NIL, 7BIT, 5974, NIL,
- // [INLINE, [FILENAME*0, Fwd: Xxx..., FILENAME*1, filename.eml]], NIL]
- /*
- * This will be caught by fetch and handled appropriately.
- */
- throw new MessagingException("BODYSTRUCTURE " + MimeUtility.MIME_TYPE_RFC822
- + " not yet supported.");
- }
-
- /*
- * Set the content type with as much information as we know right now.
- */
- final StringBuilder contentType = new StringBuilder(mimeType);
-
- /*
- * If there are body params we might be able to get some more information out
- * of them.
- */
- for (int i = 1, count = bodyParams.size(); i < count; i += 2) {
-
- // TODO We need to convert " into %22, but
- // because MimeUtility.getHeaderParameter doesn't recognize it,
- // we can't fix it for now.
- contentType.append(String.format(";\n %s=\"%s\"",
- bodyParams.getStringOrEmpty(i - 1).getString(),
- bodyParams.getStringOrEmpty(i).getString()));
- }
-
- part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType.toString());
-
- // Extension items
- final ImapList bodyDisposition;
-
- if (type.is(ImapConstants.TEXT) && bs.getElementOrNone(9).isList()) {
- // If media-type is TEXT, 9th element might be: [body-fld-lines] := number
- // So, if it's not a list, use 10th element.
- // (Couldn't find evidence in the RFC if it's ALWAYS 10th element.)
- bodyDisposition = bs.getListOrEmpty(9);
- } else {
- bodyDisposition = bs.getListOrEmpty(8);
- }
-
- final StringBuilder contentDisposition = new StringBuilder();
-
- if (bodyDisposition.size() > 0) {
- final String bodyDisposition0Str =
- bodyDisposition.getStringOrEmpty(0).getString().toLowerCase(Locale.US);
- if (!TextUtils.isEmpty(bodyDisposition0Str)) {
- contentDisposition.append(bodyDisposition0Str);
- }
-
- final ImapList bodyDispositionParams = bodyDisposition.getListOrEmpty(1);
- if (!bodyDispositionParams.isEmpty()) {
- /*
- * If there is body disposition information we can pull some more
- * information about the attachment out.
- */
- for (int i = 1, count = bodyDispositionParams.size(); i < count; i += 2) {
-
- // TODO We need to convert " into %22. See above.
- contentDisposition.append(String.format(Locale.US, ";\n %s=\"%s\"",
- bodyDispositionParams.getStringOrEmpty(i - 1)
- .getString().toLowerCase(Locale.US),
- bodyDispositionParams.getStringOrEmpty(i).getString()));
- }
- }
- }
-
- if ((size > 0)
- && (MimeUtility.getHeaderParameter(contentDisposition.toString(), "size")
- == null)) {
- contentDisposition.append(String.format(Locale.US, ";\n size=%d", size));
- }
-
- if (contentDisposition.length() > 0) {
- /*
- * Set the content disposition containing at least the size. Attachment
- * handling code will use this down the road.
- */
- part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION,
- contentDisposition.toString());
- }
-
- /*
- * Set the Content-Transfer-Encoding header. Attachment code will use this
- * to parse the body.
- */
- if (!encoding.isEmpty()) {
- part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING,
- encoding.getString());
- }
-
- /*
- * Set the Content-ID header.
- */
- if (!cid.isEmpty()) {
- part.setHeader(MimeHeader.HEADER_CONTENT_ID, cid.getString());
- }
-
- if (size > 0) {
- if (part instanceof ImapMessage) {
- ((ImapMessage) part).setSize(size);
- } else if (part instanceof MimeBodyPart) {
- ((MimeBodyPart) part).setSize(size);
- } else {
- throw new MessagingException("Unknown part type " + part.toString());
- }
- }
- part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id);
- }
-
- }
-
- public Message[] expunge() throws MessagingException {
- checkOpen();
- try {
- handleUntaggedResponses(mConnection.executeSimpleCommand(ImapConstants.EXPUNGE));
- } catch (IOException ioe) {
- mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
- throw ioExceptionHandler(mConnection, ioe);
- } finally {
- destroyResponses();
- }
- return null;
- }
-
- public void setFlags(Message[] messages, String[] flags, boolean value)
- throws MessagingException {
- checkOpen();
-
- String allFlags = "";
- if (flags.length > 0) {
- StringBuilder flagList = new StringBuilder();
- for (int i = 0, count = flags.length; i < count; i++) {
- String flag = flags[i];
- if (flag == Flag.SEEN) {
- flagList.append(" " + ImapConstants.FLAG_SEEN);
- } else if (flag == Flag.DELETED) {
- flagList.append(" " + ImapConstants.FLAG_DELETED);
- } else if (flag == Flag.FLAGGED) {
- flagList.append(" " + ImapConstants.FLAG_FLAGGED);
- } else if (flag == Flag.ANSWERED) {
- flagList.append(" " + ImapConstants.FLAG_ANSWERED);
- }
- }
- allFlags = flagList.substring(1);
- }
- try {
- mConnection.executeSimpleCommand(String.format(Locale.US,
- ImapConstants.UID_STORE + " %s %s" + ImapConstants.FLAGS_SILENT + " (%s)",
- ImapStore.joinMessageUids(messages),
- value ? "+" : "-",
- allFlags));
-
- } catch (IOException ioe) {
- mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
- throw ioExceptionHandler(mConnection, ioe);
- } finally {
- destroyResponses();
- }
- }
-
- /**
- * Selects the folder for use. Before performing any operations on this folder, it
- * must be selected.
- */
- private void doSelect() throws IOException, MessagingException {
- final List<ImapResponse> responses = mConnection.executeSimpleCommand(
- String.format(Locale.US, ImapConstants.SELECT + " \"%s\"", mName));
-
- // Assume the folder is opened read-write; unless we are notified otherwise
- mMode = MODE_READ_WRITE;
- int messageCount = -1;
- for (ImapResponse response : responses) {
- if (response.isDataResponse(1, ImapConstants.EXISTS)) {
- messageCount = response.getStringOrEmpty(0).getNumberOrZero();
- } else if (response.isOk()) {
- final ImapString responseCode = response.getResponseCodeOrEmpty();
- if (responseCode.is(ImapConstants.READ_ONLY)) {
- mMode = MODE_READ_ONLY;
- } else if (responseCode.is(ImapConstants.READ_WRITE)) {
- mMode = MODE_READ_WRITE;
- }
- } else if (response.isTagged()) { // Not OK
- mStore.getImapHelper().handleEvent(OmtpEvents.DATA_MAILBOX_OPEN_FAILED);
- throw new MessagingException("Can't open mailbox: "
- + response.getStatusResponseTextOrEmpty());
- }
- }
- if (messageCount == -1) {
- throw new MessagingException("Did not find message count during select");
- }
- mMessageCount = messageCount;
- mExists = true;
- }
-
- public class Quota {
-
- public final int occupied;
- public final int total;
-
- public Quota(int occupied, int total) {
- this.occupied = occupied;
- this.total = total;
- }
- }
-
- public Quota getQuota() throws MessagingException {
- try {
- final List<ImapResponse> responses = mConnection.executeSimpleCommand(
- String.format(Locale.US, ImapConstants.GETQUOTAROOT + " \"%s\"", mName));
-
- for (ImapResponse response : responses) {
- if (!response.isDataResponse(0, ImapConstants.QUOTA)) {
- continue;
- }
- ImapList list = response.getListOrEmpty(2);
- for (int i = 0; i < list.size(); i += 3) {
- if (!list.getStringOrEmpty(i).is("voice")) {
- continue;
- }
- return new Quota(
- list.getStringOrEmpty(i + 1).getNumber(-1),
- list.getStringOrEmpty(i + 2).getNumber(-1));
- }
- }
- } catch (IOException ioe) {
- mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
- throw ioExceptionHandler(mConnection, ioe);
- } finally {
- destroyResponses();
- }
- return null;
- }
-
- private void checkOpen() throws MessagingException {
- if (!isOpen()) {
- throw new MessagingException("Folder " + mName + " is not open.");
- }
- }
-
- private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) {
- LogUtils.d(TAG, "IO Exception detected: ", ioe);
- connection.close();
- if (connection == mConnection) {
- mConnection = null; // To prevent close() from returning the connection to the pool.
- close(false);
- }
- return new MessagingException(MessagingException.IOERROR, "IO Error", ioe);
- }
-
- public Message createMessage(String uid) {
- return new ImapMessage(uid, this);
- }
-} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/ImapStore.java b/java/com/android/voicemailomtp/mail/store/ImapStore.java
deleted file mode 100644
index f3e0c098e..000000000
--- a/java/com/android/voicemailomtp/mail/store/ImapStore.java
+++ /dev/null
@@ -1,176 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.voicemailomtp.mail.store;
-
-import android.content.Context;
-import android.net.Network;
-
-import com.android.voicemailomtp.mail.MailTransport;
-import com.android.voicemailomtp.mail.Message;
-import com.android.voicemailomtp.mail.MessagingException;
-import com.android.voicemailomtp.mail.internet.MimeMessage;
-import com.android.voicemailomtp.imap.ImapHelper;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-public class ImapStore {
- /**
- * A global suggestion to Store implementors on how much of the body
- * should be returned on FetchProfile.Item.BODY_SANE requests. We'll use 125k now.
- */
- public static final int FETCH_BODY_SANE_SUGGESTED_SIZE = (125 * 1024);
- private final Context mContext;
- private final ImapHelper mHelper;
- private final String mUsername;
- private final String mPassword;
- private final MailTransport mTransport;
- private ImapConnection mConnection;
-
- public static final int FLAG_NONE = 0x00; // No flags
- public static final int FLAG_SSL = 0x01; // Use SSL
- public static final int FLAG_TLS = 0x02; // Use TLS
- public static final int FLAG_AUTHENTICATE = 0x04; // Use name/password for authentication
- public static final int FLAG_TRUST_ALL = 0x08; // Trust all certificates
- public static final int FLAG_OAUTH = 0x10; // Use OAuth for authentication
-
- /**
- * Contains all the information necessary to log into an imap server
- */
- public ImapStore(Context context, ImapHelper helper, String username, String password, int port,
- String serverName, int flags, Network network) {
- mContext = context;
- mHelper = helper;
- mUsername = username;
- mPassword = password;
- mTransport = new MailTransport(context, this.getImapHelper(),
- network, serverName, port, flags);
- }
-
- public Context getContext() {
- return mContext;
- }
-
- public ImapHelper getImapHelper() {
- return mHelper;
- }
-
- public String getUsername() {
- return mUsername;
- }
-
- public String getPassword() {
- return mPassword;
- }
-
- /** Returns a clone of the transport associated with this store. */
- MailTransport cloneTransport() {
- return mTransport.clone();
- }
-
- /**
- * Returns UIDs of Messages joined with "," as the separator.
- */
- static String joinMessageUids(Message[] messages) {
- StringBuilder sb = new StringBuilder();
- boolean notFirst = false;
- for (Message m : messages) {
- if (notFirst) {
- sb.append(',');
- }
- sb.append(m.getUid());
- notFirst = true;
- }
- return sb.toString();
- }
-
- static class ImapMessage extends MimeMessage {
- private ImapFolder mFolder;
-
- ImapMessage(String uid, ImapFolder folder) {
- mUid = uid;
- mFolder = folder;
- }
-
- public void setSize(int size) {
- mSize = size;
- }
-
- @Override
- public void parse(InputStream in) throws IOException, MessagingException {
- super.parse(in);
- }
-
- public void setFlagInternal(String flag, boolean set) throws MessagingException {
- super.setFlag(flag, set);
- }
-
- @Override
- public void setFlag(String flag, boolean set) throws MessagingException {
- super.setFlag(flag, set);
- mFolder.setFlags(new Message[] { this }, new String[] { flag }, set);
- }
- }
-
- static class ImapException extends MessagingException {
- private static final long serialVersionUID = 1L;
-
- private final String mStatus;
- private final String mStatusMessage;
- private final String mAlertText;
- private final String mResponseCode;
-
- public ImapException(String message, String status, String statusMessage, String alertText,
- String responseCode) {
- super(message);
- mStatus = status;
- mStatusMessage = statusMessage;
- mAlertText = alertText;
- mResponseCode = responseCode;
- }
-
- public String getStatus() {
- return mStatus;
- }
-
- public String getStatusMessage() {
- return mStatusMessage;
- }
-
- public String getAlertText() {
- return mAlertText;
- }
-
- public String getResponseCode() {
- return mResponseCode;
- }
- }
-
- public void closeConnection() {
- if (mConnection != null) {
- mConnection.close();
- mConnection = null;
- }
- }
-
- public ImapConnection getConnection() {
- if (mConnection == null) {
- mConnection = new ImapConnection(this);
- }
- return mConnection;
- }
-} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/imap/DigestMd5Utils.java b/java/com/android/voicemailomtp/mail/store/imap/DigestMd5Utils.java
deleted file mode 100644
index b78f55293..000000000
--- a/java/com/android/voicemailomtp/mail/store/imap/DigestMd5Utils.java
+++ /dev/null
@@ -1,335 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.voicemailomtp.mail.store.imap;
-
-import android.annotation.TargetApi;
-import android.os.Build.VERSION_CODES;
-import android.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
-import android.util.ArrayMap;
-import android.util.Base64;
-import com.android.voicemailomtp.VvmLog;
-import com.android.voicemailomtp.mail.MailTransport;
-import com.android.voicemailomtp.mail.MessagingException;
-import com.android.voicemailomtp.mail.store.ImapStore;
-import java.nio.charset.StandardCharsets;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.util.Map;
-
-@SuppressWarnings("AndroidApiChecker") // Map.getOrDefault() is java8
-@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
-public class DigestMd5Utils {
-
- private static final String TAG = "DigestMd5Utils";
-
- private static final String DIGEST_CHARSET = "CHARSET";
- private static final String DIGEST_USERNAME = "username";
- private static final String DIGEST_REALM = "realm";
- private static final String DIGEST_NONCE = "nonce";
- private static final String DIGEST_NC = "nc";
- private static final String DIGEST_CNONCE = "cnonce";
- private static final String DIGEST_URI = "digest-uri";
- private static final String DIGEST_RESPONSE = "response";
- private static final String DIGEST_QOP = "qop";
-
- private static final String RESPONSE_AUTH_HEADER = "rspauth=";
- private static final String HEX_CHARS = "0123456789abcdef";
-
- /**
- * Represents the set of data we need to generate the DIGEST-MD5 response.
- */
- public static class Data {
-
- private static final String CHARSET = "utf-8";
-
- public String username;
- public String password;
- public String realm;
- public String nonce;
- public String nc;
- public String cnonce;
- public String digestUri;
- public String qop;
-
- @VisibleForTesting
- Data() {
- // Do nothing
- }
-
- public Data(ImapStore imapStore, MailTransport transport, Map<String, String> challenge) {
- username = imapStore.getUsername();
- password = imapStore.getPassword();
- realm = challenge.getOrDefault(DIGEST_REALM, "");
- nonce = challenge.get(DIGEST_NONCE);
- cnonce = createCnonce();
- nc = "00000001"; // Subsequent Authentication not supported, nounce count always 1.
- qop = "auth"; // Other config not supported
- digestUri = "imap/" + transport.getHost();
- }
-
- private static String createCnonce() {
- SecureRandom generator = new SecureRandom();
-
- // At least 64 bits of entropy is required
- byte[] rawBytes = new byte[8];
- generator.nextBytes(rawBytes);
-
- return Base64.encodeToString(rawBytes, Base64.NO_WRAP);
- }
-
- /**
- * Verify the response-auth returned by the server is correct.
- */
- public void verifyResponseAuth(String response)
- throws MessagingException {
- if (!response.startsWith(RESPONSE_AUTH_HEADER)) {
- throw new MessagingException("response-auth expected");
- }
- if (!response.substring(RESPONSE_AUTH_HEADER.length())
- .equals(DigestMd5Utils.getResponse(this, true))) {
- throw new MessagingException("invalid response-auth return from the server.");
- }
- }
-
- public String createResponse() {
- String response = getResponse(this, false);
- ResponseBuilder builder = new ResponseBuilder();
- builder
- .append(DIGEST_CHARSET, CHARSET)
- .appendQuoted(DIGEST_USERNAME, username)
- .appendQuoted(DIGEST_REALM, realm)
- .appendQuoted(DIGEST_NONCE, nonce)
- .append(DIGEST_NC, nc)
- .appendQuoted(DIGEST_CNONCE, cnonce)
- .appendQuoted(DIGEST_URI, digestUri)
- .append(DIGEST_RESPONSE, response)
- .append(DIGEST_QOP, qop);
- return builder.toString();
- }
-
- private static class ResponseBuilder {
-
- private StringBuilder mBuilder = new StringBuilder();
-
- public ResponseBuilder appendQuoted(String key, String value) {
- if (mBuilder.length() != 0) {
- mBuilder.append(",");
- }
- mBuilder.append(key).append("=\"").append(value).append("\"");
- return this;
- }
-
- public ResponseBuilder append(String key, String value) {
- if (mBuilder.length() != 0) {
- mBuilder.append(",");
- }
- mBuilder.append(key).append("=").append(value);
- return this;
- }
-
- @Override
- public String toString() {
- return mBuilder.toString();
- }
- }
- }
-
- /*
- response-value =
- toHex( getKeyDigest ( toHex(getMd5(a1)),
- { nonce-value, ":" nc-value, ":",
- cnonce-value, ":", qop-value, ":", toHex(getMd5(a2)) }))
- * @param isResponseAuth is the response the one the server is returning us. response-auth has
- * different a2 format.
- */
- @VisibleForTesting
- static String getResponse(Data data, boolean isResponseAuth) {
- StringBuilder a1 = new StringBuilder();
- a1.append(new String(
- getMd5(data.username + ":" + data.realm + ":" + data.password),
- StandardCharsets.ISO_8859_1));
- a1.append(":").append(data.nonce).append(":").append(data.cnonce);
-
- StringBuilder a2 = new StringBuilder();
- if (!isResponseAuth) {
- a2.append("AUTHENTICATE");
- }
- a2.append(":").append(data.digestUri);
-
- return toHex(getKeyDigest(
- toHex(getMd5(a1.toString())),
- data.nonce + ":" + data.nc + ":" + data.cnonce + ":" + data.qop + ":" + toHex(
- getMd5(a2.toString()))
- ));
- }
-
- /**
- * Let getMd5(s) be the 16 octet MD5 hash [RFC 1321] of the octet string s.
- */
- private static byte[] getMd5(String s) {
- try {
- MessageDigest digester = MessageDigest.getInstance("MD5");
- digester.update(s.getBytes(StandardCharsets.ISO_8859_1));
- return digester.digest();
- } catch (NoSuchAlgorithmException e) {
- throw new AssertionError(e);
- }
- }
-
- /**
- * Let getKeyDigest(k, s) be getMd5({k, ":", s}), i.e., the 16 octet hash of the string k, a colon and the
- * string s.
- */
- private static byte[] getKeyDigest(String k, String s) {
- StringBuilder builder = new StringBuilder(k).append(":").append(s);
- return getMd5(builder.toString());
- }
-
- /**
- * Let toHex(n) be the representation of the 16 octet MD5 hash n as a string of 32 hex digits
- * (with alphabetic characters always in lower case, since MD5 is case sensitive).
- */
- private static String toHex(byte[] n) {
- StringBuilder result = new StringBuilder();
- for (byte b : n) {
- int unsignedByte = b & 0xFF;
- result.append(HEX_CHARS.charAt(unsignedByte / 16))
- .append(HEX_CHARS.charAt(unsignedByte % 16));
- }
- return result.toString();
- }
-
- public static Map<String, String> parseDigestMessage(String message) throws MessagingException {
- Map<String, String> result = new DigestMessageParser(message).parse();
- if (!result.containsKey(DIGEST_NONCE)) {
- throw new MessagingException("nonce missing from server DIGEST-MD5 challenge");
- }
- return result;
- }
-
- /**
- * Parse the key-value pair returned by the server.
- */
- private static class DigestMessageParser {
-
- private final String mMessage;
- private int mPosition = 0;
- private Map<String, String> mResult = new ArrayMap<>();
-
- public DigestMessageParser(String message) {
- mMessage = message;
- }
-
- @Nullable
- public Map<String, String> parse() {
- try {
- while (mPosition < mMessage.length()) {
- parsePair();
- if (mPosition != mMessage.length()) {
- expect(',');
- }
- }
- } catch (IndexOutOfBoundsException e) {
- VvmLog.e(TAG, e.toString());
- return null;
- }
- return mResult;
- }
-
- private void parsePair() {
- String key = parseKey();
- expect('=');
- String value = parseValue();
- mResult.put(key, value);
- }
-
- private void expect(char c) {
- if (pop() != c) {
- throw new IllegalStateException(
- "unexpected character " + mMessage.charAt(mPosition));
- }
- }
-
- private char pop() {
- char result = peek();
- mPosition++;
- return result;
- }
-
- private char peek() {
- return mMessage.charAt(mPosition);
- }
-
- private void goToNext(char c) {
- while (peek() != c) {
- mPosition++;
- }
- }
-
- private String parseKey() {
- int start = mPosition;
- goToNext('=');
- return mMessage.substring(start, mPosition);
- }
-
- private String parseValue() {
- if (peek() == '"') {
- return parseQuotedValue();
- } else {
- return parseUnquotedValue();
- }
- }
-
- private String parseQuotedValue() {
- expect('"');
- StringBuilder result = new StringBuilder();
- while (true) {
- char c = pop();
- if (c == '\\') {
- result.append(pop());
- } else if (c == '"') {
- break;
- } else {
- result.append(c);
- }
- }
- return result.toString();
- }
-
- private String parseUnquotedValue() {
- StringBuilder result = new StringBuilder();
- while (true) {
- char c = pop();
- if (c == '\\') {
- result.append(pop());
- } else if (c == ',') {
- mPosition--;
- break;
- } else {
- result.append(c);
- }
-
- if (mPosition == mMessage.length()) {
- break;
- }
- }
- return result.toString();
- }
- }
-}
diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapConstants.java b/java/com/android/voicemailomtp/mail/store/imap/ImapConstants.java
deleted file mode 100644
index d8e75752f..000000000
--- a/java/com/android/voicemailomtp/mail/store/imap/ImapConstants.java
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.voicemailomtp.mail.store.imap;
-
-import com.android.voicemailomtp.mail.store.ImapStore;
-
-import java.util.Locale;
-
-public final class ImapConstants {
- private ImapConstants() {}
-
- public static final String FETCH_FIELD_BODY_PEEK_BARE = "BODY.PEEK";
- public static final String FETCH_FIELD_BODY_PEEK = FETCH_FIELD_BODY_PEEK_BARE + "[]";
- public static final String FETCH_FIELD_BODY_PEEK_SANE = String.format(
- Locale.US, "BODY.PEEK[]<0.%d>", ImapStore.FETCH_BODY_SANE_SUGGESTED_SIZE);
- public static final String FETCH_FIELD_HEADERS =
- "BODY.PEEK[HEADER.FIELDS (date subject from content-type to cc message-id)]";
-
- public static final String ALERT = "ALERT";
- public static final String APPEND = "APPEND";
- public static final String AUTHENTICATE = "AUTHENTICATE";
- public static final String BAD = "BAD";
- public static final String BADCHARSET = "BADCHARSET";
- public static final String BODY = "BODY";
- public static final String BODY_BRACKET_HEADER = "BODY[HEADER";
- public static final String BODYSTRUCTURE = "BODYSTRUCTURE";
- public static final String BYE = "BYE";
- public static final String CAPABILITY = "CAPABILITY";
- public static final String CHECK = "CHECK";
- public static final String CLOSE = "CLOSE";
- public static final String COPY = "COPY";
- public static final String COPYUID = "COPYUID";
- public static final String CREATE = "CREATE";
- public static final String DELETE = "DELETE";
- public static final String EXAMINE = "EXAMINE";
- public static final String EXISTS = "EXISTS";
- public static final String EXPUNGE = "EXPUNGE";
- public static final String FETCH = "FETCH";
- public static final String FLAG_ANSWERED = "\\ANSWERED";
- public static final String FLAG_DELETED = "\\DELETED";
- public static final String FLAG_FLAGGED = "\\FLAGGED";
- public static final String FLAG_NO_SELECT = "\\NOSELECT";
- public static final String FLAG_SEEN = "\\SEEN";
- public static final String FLAGS = "FLAGS";
- public static final String FLAGS_SILENT = "FLAGS.SILENT";
- public static final String ID = "ID";
- public static final String INBOX = "INBOX";
- public static final String INTERNALDATE = "INTERNALDATE";
- public static final String LIST = "LIST";
- public static final String LOGIN = "LOGIN";
- public static final String LOGOUT = "LOGOUT";
- public static final String LSUB = "LSUB";
- public static final String NAMESPACE = "NAMESPACE";
- public static final String NO = "NO";
- public static final String NOOP = "NOOP";
- public static final String OK = "OK";
- public static final String PARSE = "PARSE";
- public static final String PERMANENTFLAGS = "PERMANENTFLAGS";
- public static final String PREAUTH = "PREAUTH";
- public static final String READ_ONLY = "READ-ONLY";
- public static final String READ_WRITE = "READ-WRITE";
- public static final String RENAME = "RENAME";
- public static final String RFC822_SIZE = "RFC822.SIZE";
- public static final String SEARCH = "SEARCH";
- public static final String SELECT = "SELECT";
- public static final String STARTTLS = "STARTTLS";
- public static final String STATUS = "STATUS";
- public static final String STORE = "STORE";
- public static final String SUBSCRIBE = "SUBSCRIBE";
- public static final String TEXT = "TEXT";
- public static final String TRYCREATE = "TRYCREATE";
- public static final String UID = "UID";
- public static final String UID_COPY = "UID COPY";
- public static final String UID_FETCH = "UID FETCH";
- public static final String UID_SEARCH = "UID SEARCH";
- public static final String UID_STORE = "UID STORE";
- public static final String UIDNEXT = "UIDNEXT";
- public static final String UIDPLUS = "UIDPLUS";
- public static final String UIDVALIDITY = "UIDVALIDITY";
- public static final String UNSEEN = "UNSEEN";
- public static final String UNSUBSCRIBE = "UNSUBSCRIBE";
- public static final String XOAUTH2 = "XOAUTH2";
- public static final String APPENDUID = "APPENDUID";
- public static final String NIL = "NIL";
-
- /**
- * NO responses
- */
- public static final String NO_COMMAND_NOT_ALLOWED = "command not allowed";
- public static final String NO_RESERVATION_FAILED = "reservation failed";
- public static final String NO_APPLICATION_ERROR = "application error";
- public static final String NO_INVALID_PARAMETER = "invalid parameter";
- public static final String NO_INVALID_COMMAND = "invalid command";
- public static final String NO_UNKNOWN_COMMAND = "unknown command";
- // AUTHENTICATE
- // The subscriber can not be located in the system.
- public static final String NO_UNKNOWN_USER = "unknown user";
- // The Client Type or Protocol Version is unknown.
- public static final String NO_UNKNOWN_CLIENT = "unknown client";
- // The password received from the client does not match the password defined in the subscriber's profile.
- public static final String NO_INVALID_PASSWORD = "invalid password";
- // The subscriber's mailbox has not yet been initialised via the TUI
- public static final String NO_MAILBOX_NOT_INITIALIZED = "mailbox not initialized";
- // The subscriber has not been provisioned for the VVM service.
- public static final String NO_SERVICE_IS_NOT_PROVISIONED =
- "service is not provisioned";
- // The subscriber is provisioned for the VVM service but the VVM service is currently not active
- public static final String NO_SERVICE_IS_NOT_ACTIVATED = "service is not activated";
- // The Voice Mail Blocked flag in the subscriber's profile is set to YES.
- public static final String NO_USER_IS_BLOCKED = "user is blocked";
-
- /**
- * extensions
- */
- public static final String GETQUOTA = "GETQUOTA";
- public static final String GETQUOTAROOT = "GETQUOTAROOT";
- public static final String QUOTAROOT = "QUOTAROOT";
- public static final String QUOTA = "QUOTA";
-
- /**
- * capabilities
- */
- public static final String CAPABILITY_AUTH_DIGEST_MD5 = "AUTH=DIGEST-MD5";
- public static final String CAPABILITY_STARTTLS = "STARTTLS";
-
- /**
- * authentication
- */
- public static final String AUTH_DIGEST_MD5 = "DIGEST-MD5";
-} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapElement.java b/java/com/android/voicemailomtp/mail/store/imap/ImapElement.java
deleted file mode 100644
index 9f272e31c..000000000
--- a/java/com/android/voicemailomtp/mail/store/imap/ImapElement.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.voicemailomtp.mail.store.imap;
-
-/**
- * Class representing "element"s in IMAP responses.
- *
- * <p>Class hierarchy:
- * <pre>
- * ImapElement
- * |
- * |-- ImapElement.NONE (for 'index out of range')
- * |
- * |-- ImapList (isList() == true)
- * | |
- * | |-- ImapList.EMPTY
- * | |
- * | --- ImapResponse
- * |
- * --- ImapString (isString() == true)
- * |
- * |-- ImapString.EMPTY
- * |
- * |-- ImapSimpleString
- * |
- * |-- ImapMemoryLiteral
- * |
- * --- ImapTempFileLiteral
- * </pre>
- */
-public abstract class ImapElement {
- /**
- * An element that is returned by {@link ImapList#getElementOrNone} to indicate an index
- * is out of range.
- */
- public static final ImapElement NONE = new ImapElement() {
- @Override public void destroy() {
- // Don't call super.destroy().
- // It's a shared object. We don't want the mDestroyed to be set on this.
- }
-
- @Override public boolean isList() {
- return false;
- }
-
- @Override public boolean isString() {
- return false;
- }
-
- @Override public String toString() {
- return "[NO ELEMENT]";
- }
-
- @Override
- public boolean equalsForTest(ImapElement that) {
- return super.equalsForTest(that);
- }
- };
-
- private boolean mDestroyed = false;
-
- public abstract boolean isList();
-
- public abstract boolean isString();
-
- protected boolean isDestroyed() {
- return mDestroyed;
- }
-
- /**
- * Clean up the resources used by the instance.
- * It's for removing a temp file used by {@link ImapTempFileLiteral}.
- */
- public void destroy() {
- mDestroyed = true;
- }
-
- /**
- * Throws {@link RuntimeException} if it's already destroyed.
- */
- protected final void checkNotDestroyed() {
- if (mDestroyed) {
- throw new RuntimeException("Already destroyed");
- }
- }
-
- /**
- * Return a string that represents this object; it's purely for the debug purpose. Don't
- * mistake it for {@link ImapString#getString}.
- *
- * Abstract to force subclasses to implement it.
- */
- @Override
- public abstract String toString();
-
- /**
- * The equals implementation that is intended to be used only for unit testing.
- * (Because it may be heavy and has a special sense of "equal" for testing.)
- */
- public boolean equalsForTest(ImapElement that) {
- if (that == null) {
- return false;
- }
- return this.getClass() == that.getClass(); // Has to be the same class.
- }
-} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapList.java b/java/com/android/voicemailomtp/mail/store/imap/ImapList.java
deleted file mode 100644
index 970423cbd..000000000
--- a/java/com/android/voicemailomtp/mail/store/imap/ImapList.java
+++ /dev/null
@@ -1,235 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.voicemailomtp.mail.store.imap;
-
-import java.util.ArrayList;
-
-/**
- * Class represents an IMAP list.
- */
-public class ImapList extends ImapElement {
- /**
- * {@link ImapList} representing an empty list.
- */
- public static final ImapList EMPTY = new ImapList() {
- @Override public void destroy() {
- // Don't call super.destroy().
- // It's a shared object. We don't want the mDestroyed to be set on this.
- }
-
- @Override void add(ImapElement e) {
- throw new RuntimeException();
- }
- };
-
- private ArrayList<ImapElement> mList = new ArrayList<ImapElement>();
-
- /* package */ void add(ImapElement e) {
- if (e == null) {
- throw new RuntimeException("Can't add null");
- }
- mList.add(e);
- }
-
- @Override
- public final boolean isString() {
- return false;
- }
-
- @Override
- public final boolean isList() {
- return true;
- }
-
- public final int size() {
- return mList.size();
- }
-
- public final boolean isEmpty() {
- return size() == 0;
- }
-
- /**
- * Return true if the element at {@code index} exists, is string, and equals to {@code s}.
- * (case insensitive)
- */
- public final boolean is(int index, String s) {
- return is(index, s, false);
- }
-
- /**
- * Same as {@link #is(int, String)}, but does the prefix match if {@code prefixMatch}.
- */
- public final boolean is(int index, String s, boolean prefixMatch) {
- if (!prefixMatch) {
- return getStringOrEmpty(index).is(s);
- } else {
- return getStringOrEmpty(index).startsWith(s);
- }
- }
-
- /**
- * Return the element at {@code index}.
- * If {@code index} is out of range, returns {@link ImapElement#NONE}.
- */
- public final ImapElement getElementOrNone(int index) {
- return (index >= mList.size()) ? ImapElement.NONE : mList.get(index);
- }
-
- /**
- * Return the element at {@code index} if it's a list.
- * If {@code index} is out of range or not a list, returns {@link ImapList#EMPTY}.
- */
- public final ImapList getListOrEmpty(int index) {
- ImapElement el = getElementOrNone(index);
- return el.isList() ? (ImapList) el : EMPTY;
- }
-
- /**
- * Return the element at {@code index} if it's a string.
- * If {@code index} is out of range or not a string, returns {@link ImapString#EMPTY}.
- */
- public final ImapString getStringOrEmpty(int index) {
- ImapElement el = getElementOrNone(index);
- return el.isString() ? (ImapString) el : ImapString.EMPTY;
- }
-
- /**
- * Return an element keyed by {@code key}. Return null if not found. {@code key} has to be
- * at an even index.
- */
- /* package */ final ImapElement getKeyedElementOrNull(String key, boolean prefixMatch) {
- for (int i = 1; i < size(); i += 2) {
- if (is(i-1, key, prefixMatch)) {
- return mList.get(i);
- }
- }
- return null;
- }
-
- /**
- * Return an {@link ImapList} keyed by {@code key}.
- * Return {@link ImapList#EMPTY} if not found.
- */
- public final ImapList getKeyedListOrEmpty(String key) {
- return getKeyedListOrEmpty(key, false);
- }
-
- /**
- * Return an {@link ImapList} keyed by {@code key}.
- * Return {@link ImapList#EMPTY} if not found.
- */
- public final ImapList getKeyedListOrEmpty(String key, boolean prefixMatch) {
- ImapElement e = getKeyedElementOrNull(key, prefixMatch);
- return (e != null) ? ((ImapList) e) : ImapList.EMPTY;
- }
-
- /**
- * Return an {@link ImapString} keyed by {@code key}.
- * Return {@link ImapString#EMPTY} if not found.
- */
- public final ImapString getKeyedStringOrEmpty(String key) {
- return getKeyedStringOrEmpty(key, false);
- }
-
- /**
- * Return an {@link ImapString} keyed by {@code key}.
- * Return {@link ImapString#EMPTY} if not found.
- */
- public final ImapString getKeyedStringOrEmpty(String key, boolean prefixMatch) {
- ImapElement e = getKeyedElementOrNull(key, prefixMatch);
- return (e != null) ? ((ImapString) e) : ImapString.EMPTY;
- }
-
- /**
- * Return true if it contains {@code s}.
- */
- public final boolean contains(String s) {
- for (int i = 0; i < size(); i++) {
- if (getStringOrEmpty(i).is(s)) {
- return true;
- }
- }
- return false;
- }
-
- @Override
- public void destroy() {
- if (mList != null) {
- for (ImapElement e : mList) {
- e.destroy();
- }
- mList = null;
- }
- super.destroy();
- }
-
- @Override
- public String toString() {
- return mList.toString();
- }
-
- /**
- * Return the text representations of the contents concatenated with ",".
- */
- public final String flatten() {
- return flatten(new StringBuilder()).toString();
- }
-
- /**
- * Returns text representations (i.e. getString()) of contents joined together with
- * "," as the separator.
- *
- * Only used for building the capability string passed to vendor policies.
- *
- * We can't use toString(), because it's for debugging (meaning the format may change any time),
- * and it won't expand literals.
- */
- private final StringBuilder flatten(StringBuilder sb) {
- sb.append('[');
- for (int i = 0; i < mList.size(); i++) {
- if (i > 0) {
- sb.append(',');
- }
- final ImapElement e = getElementOrNone(i);
- if (e.isList()) {
- getListOrEmpty(i).flatten(sb);
- } else if (e.isString()) {
- sb.append(getStringOrEmpty(i).getString());
- }
- }
- sb.append(']');
- return sb;
- }
-
- @Override
- public boolean equalsForTest(ImapElement that) {
- if (!super.equalsForTest(that)) {
- return false;
- }
- ImapList thatList = (ImapList) that;
- if (size() != thatList.size()) {
- return false;
- }
- for (int i = 0; i < size(); i++) {
- if (!mList.get(i).equalsForTest(thatList.getElementOrNone(i))) {
- return false;
- }
- }
- return true;
- }
-} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapMemoryLiteral.java b/java/com/android/voicemailomtp/mail/store/imap/ImapMemoryLiteral.java
deleted file mode 100644
index ad60ca7a4..000000000
--- a/java/com/android/voicemailomtp/mail/store/imap/ImapMemoryLiteral.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.voicemailomtp.mail.store.imap;
-
-import com.android.voicemailomtp.mail.FixedLengthInputStream;
-import com.android.voicemailomtp.VvmLog;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
-
-/**
- * Subclass of {@link ImapString} used for literals backed by an in-memory byte array.
- */
-public class ImapMemoryLiteral extends ImapString {
- private final String TAG = "ImapMemoryLiteral";
- private byte[] mData;
-
- /* package */ ImapMemoryLiteral(FixedLengthInputStream in) throws IOException {
- // We could use ByteArrayOutputStream and IOUtils.copy, but it'd perform an unnecessary
- // copy....
- mData = new byte[in.getLength()];
- int pos = 0;
- while (pos < mData.length) {
- int read = in.read(mData, pos, mData.length - pos);
- if (read < 0) {
- break;
- }
- pos += read;
- }
- if (pos != mData.length) {
- VvmLog.w(TAG, "length mismatch");
- }
- }
-
- @Override
- public void destroy() {
- mData = null;
- super.destroy();
- }
-
- @Override
- public String getString() {
- try {
- return new String(mData, "US-ASCII");
- } catch (UnsupportedEncodingException e) {
- VvmLog.e(TAG, "Unsupported encoding: ", e);
- }
- return null;
- }
-
- @Override
- public InputStream getAsStream() {
- return new ByteArrayInputStream(mData);
- }
-
- @Override
- public String toString() {
- return String.format("{%d byte literal(memory)}", mData.length);
- }
-} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapResponse.java b/java/com/android/voicemailomtp/mail/store/imap/ImapResponse.java
deleted file mode 100644
index 412f16d8a..000000000
--- a/java/com/android/voicemailomtp/mail/store/imap/ImapResponse.java
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.voicemailomtp.mail.store.imap;
-
-/**
- * Class represents an IMAP response.
- */
-public class ImapResponse extends ImapList {
- private final String mTag;
- private final boolean mIsContinuationRequest;
-
- /* package */ ImapResponse(String tag, boolean isContinuationRequest) {
- mTag = tag;
- mIsContinuationRequest = isContinuationRequest;
- }
-
- /* package */ static boolean isStatusResponse(String symbol) {
- return ImapConstants.OK.equalsIgnoreCase(symbol)
- || ImapConstants.NO.equalsIgnoreCase(symbol)
- || ImapConstants.BAD.equalsIgnoreCase(symbol)
- || ImapConstants.PREAUTH.equalsIgnoreCase(symbol)
- || ImapConstants.BYE.equalsIgnoreCase(symbol);
- }
-
- /**
- * @return whether it's a tagged response.
- */
- public boolean isTagged() {
- return mTag != null;
- }
-
- /**
- * @return whether it's a continuation request.
- */
- public boolean isContinuationRequest() {
- return mIsContinuationRequest;
- }
-
- public boolean isStatusResponse() {
- return isStatusResponse(getStringOrEmpty(0).getString());
- }
-
- /**
- * @return whether it's an OK response.
- */
- public boolean isOk() {
- return is(0, ImapConstants.OK);
- }
-
- /**
- * @return whether it's an BAD response.
- */
- public boolean isBad() {
- return is(0, ImapConstants.BAD);
- }
-
- /**
- * @return whether it's an NO response.
- */
- public boolean isNo() {
- return is(0, ImapConstants.NO);
- }
-
- /**
- * @return whether it's an {@code responseType} data response. (i.e. not tagged).
- * @param index where {@code responseType} should appear. e.g. 1 for "FETCH"
- * @param responseType e.g. "FETCH"
- */
- public final boolean isDataResponse(int index, String responseType) {
- return !isTagged() && getStringOrEmpty(index).is(responseType);
- }
-
- /**
- * @return Response code (RFC 3501 7.1) if it's a status response.
- *
- * e.g. "ALERT" for "* OK [ALERT] System shutdown in 10 minutes"
- */
- public ImapString getResponseCodeOrEmpty() {
- if (!isStatusResponse()) {
- return ImapString.EMPTY; // Not a status response.
- }
- return getListOrEmpty(1).getStringOrEmpty(0);
- }
-
- /**
- * @return Alert message it it has ALERT response code.
- *
- * e.g. "System shutdown in 10 minutes" for "* OK [ALERT] System shutdown in 10 minutes"
- */
- public ImapString getAlertTextOrEmpty() {
- if (!getResponseCodeOrEmpty().is(ImapConstants.ALERT)) {
- return ImapString.EMPTY; // Not an ALERT
- }
- // The 3rd element contains all the rest of line.
- return getStringOrEmpty(2);
- }
-
- /**
- * @return Response text in a status response.
- */
- public ImapString getStatusResponseTextOrEmpty() {
- if (!isStatusResponse()) {
- return ImapString.EMPTY;
- }
- return getStringOrEmpty(getElementOrNone(1).isList() ? 2 : 1);
- }
-
- public ImapString getStatusOrEmpty() {
- if (!isStatusResponse()) {
- return ImapString.EMPTY;
- }
- return getStringOrEmpty(0);
- }
-
- @Override
- public String toString() {
- String tag = mTag;
- if (isContinuationRequest()) {
- tag = "+";
- }
- return "#" + tag + "# " + super.toString();
- }
-
- @Override
- public boolean equalsForTest(ImapElement that) {
- if (!super.equalsForTest(that)) {
- return false;
- }
- final ImapResponse thatResponse = (ImapResponse) that;
- if (mTag == null) {
- if (thatResponse.mTag != null) {
- return false;
- }
- } else {
- if (!mTag.equals(thatResponse.mTag)) {
- return false;
- }
- }
- if (mIsContinuationRequest != thatResponse.mIsContinuationRequest) {
- return false;
- }
- return true;
- }
-} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapResponseParser.java b/java/com/android/voicemailomtp/mail/store/imap/ImapResponseParser.java
deleted file mode 100644
index 692596f14..000000000
--- a/java/com/android/voicemailomtp/mail/store/imap/ImapResponseParser.java
+++ /dev/null
@@ -1,432 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.voicemailomtp.mail.store.imap;
-
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.android.voicemailomtp.mail.FixedLengthInputStream;
-import com.android.voicemailomtp.mail.MessagingException;
-import com.android.voicemailomtp.mail.PeekableInputStream;
-import com.android.voicemailomtp.VvmLog;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-
-/**
- * IMAP response parser.
- */
-public class ImapResponseParser {
- private static final String TAG = "ImapResponseParser";
-
- /**
- * Literal larger than this will be stored in temp file.
- */
- public static final int LITERAL_KEEP_IN_MEMORY_THRESHOLD = 2 * 1024 * 1024;
-
- /** Input stream */
- private final PeekableInputStream mIn;
-
- private final int mLiteralKeepInMemoryThreshold;
-
- /** StringBuilder used by readUntil() */
- private final StringBuilder mBufferReadUntil = new StringBuilder();
-
- /** StringBuilder used by parseBareString() */
- private final StringBuilder mParseBareString = new StringBuilder();
-
- /**
- * We store all {@link ImapResponse} in it. {@link #destroyResponses()} must be called from
- * time to time to destroy them and clear it.
- */
- private final ArrayList<ImapResponse> mResponsesToDestroy = new ArrayList<ImapResponse>();
-
- /**
- * Exception thrown when we receive BYE. It derives from IOException, so it'll be treated
- * in the same way EOF does.
- */
- public static class ByeException extends IOException {
- public static final String MESSAGE = "Received BYE";
- public ByeException() {
- super(MESSAGE);
- }
- }
-
- /**
- * Public constructor for normal use.
- */
- public ImapResponseParser(InputStream in) {
- this(in, LITERAL_KEEP_IN_MEMORY_THRESHOLD);
- }
-
- /**
- * Constructor for testing to override the literal size threshold.
- */
- /* package for test */ ImapResponseParser(InputStream in, int literalKeepInMemoryThreshold) {
- mIn = new PeekableInputStream(in);
- mLiteralKeepInMemoryThreshold = literalKeepInMemoryThreshold;
- }
-
- private static IOException newEOSException() {
- final String message = "End of stream reached";
- VvmLog.d(TAG, message);
- return new IOException(message);
- }
-
- /**
- * Peek next one byte.
- *
- * Throws IOException() if reaches EOF. As long as logical response lines end with \r\n,
- * we shouldn't see EOF during parsing.
- */
- private int peek() throws IOException {
- final int next = mIn.peek();
- if (next == -1) {
- throw newEOSException();
- }
- return next;
- }
-
- /**
- * Read and return one byte from {@link #mIn}, and put it in {@link #mDiscourseLogger}.
- *
- * Throws IOException() if reaches EOF. As long as logical response lines end with \r\n,
- * we shouldn't see EOF during parsing.
- */
- private int readByte() throws IOException {
- int next = mIn.read();
- if (next == -1) {
- throw newEOSException();
- }
- return next;
- }
-
- /**
- * Destroy all the {@link ImapResponse}s stored in the internal storage and clear it.
- *
- * @see #readResponse()
- */
- public void destroyResponses() {
- for (ImapResponse r : mResponsesToDestroy) {
- r.destroy();
- }
- mResponsesToDestroy.clear();
- }
-
- /**
- * Reads the next response available on the stream and returns an
- * {@link ImapResponse} object that represents it.
- *
- * <p>When this method successfully returns an {@link ImapResponse}, the {@link ImapResponse}
- * is stored in the internal storage. When the {@link ImapResponse} is no longer used
- * {@link #destroyResponses} should be called to destroy all the responses in the array.
- *
- * @param byeExpected is a untagged BYE response expected? If not proper cleanup will be done
- * and {@link ByeException} will be thrown.
- * @return the parsed {@link ImapResponse} object.
- * @exception ByeException when detects BYE and <code>byeExpected</code> is false.
- */
- public ImapResponse readResponse(boolean byeExpected) throws IOException, MessagingException {
- ImapResponse response = null;
- try {
- response = parseResponse();
- } catch (RuntimeException e) {
- // Parser crash -- log network activities.
- onParseError(e);
- throw e;
- } catch (IOException e) {
- // Network error, or received an unexpected char.
- onParseError(e);
- throw e;
- }
-
- // Handle this outside of try-catch. We don't have to dump protocol log when getting BYE.
- if (!byeExpected && response.is(0, ImapConstants.BYE)) {
- Log.w(TAG, ByeException.MESSAGE);
- response.destroy();
- throw new ByeException();
- }
- mResponsesToDestroy.add(response);
- return response;
- }
-
- private void onParseError(Exception e) {
- // Read a few more bytes, so that the log will contain some more context, even if the parser
- // crashes in the middle of a response.
- // This also makes sure the byte in question will be logged, no matter where it crashes.
- // e.g. when parseAtom() peeks and finds at an unexpected char, it throws an exception
- // before actually reading it.
- // However, we don't want to read too much, because then it may get into an email message.
- try {
- for (int i = 0; i < 4; i++) {
- int b = readByte();
- if (b == -1 || b == '\n') {
- break;
- }
- }
- } catch (IOException ignore) {
- }
- VvmLog.w(TAG, "Exception detected: " + e.getMessage());
- }
-
- /**
- * Read next byte from stream and throw it away. If the byte is different from {@code expected}
- * throw {@link MessagingException}.
- */
- /* package for test */ void expect(char expected) throws IOException {
- final int next = readByte();
- if (expected != next) {
- throw new IOException(String.format("Expected %04x (%c) but got %04x (%c)",
- (int) expected, expected, next, (char) next));
- }
- }
-
- /**
- * Read bytes until we find {@code end}, and return all as string.
- * The {@code end} will be read (rather than peeked) and won't be included in the result.
- */
- /* package for test */ String readUntil(char end) throws IOException {
- mBufferReadUntil.setLength(0);
- for (;;) {
- final int ch = readByte();
- if (ch != end) {
- mBufferReadUntil.append((char) ch);
- } else {
- return mBufferReadUntil.toString();
- }
- }
- }
-
- /**
- * Read all bytes until \r\n.
- */
- /* package */ String readUntilEol() throws IOException {
- String ret = readUntil('\r');
- expect('\n'); // TODO Should this really be error?
- return ret;
- }
-
- /**
- * Parse and return the response line.
- */
- private ImapResponse parseResponse() throws IOException, MessagingException {
- // We need to destroy the response if we get an exception.
- // So, we first store the response that's being built in responseToDestroy, until it's
- // completely built, at which point we copy it into responseToReturn and null out
- // responseToDestroyt.
- // If responseToDestroy is not null in finally, we destroy it because that means
- // we got an exception somewhere.
- ImapResponse responseToDestroy = null;
- final ImapResponse responseToReturn;
-
- try {
- final int ch = peek();
- if (ch == '+') { // Continuation request
- readByte(); // skip +
- expect(' ');
- responseToDestroy = new ImapResponse(null, true);
-
- // If it's continuation request, we don't really care what's in it.
- responseToDestroy.add(new ImapSimpleString(readUntilEol()));
-
- // Response has successfully been built. Let's return it.
- responseToReturn = responseToDestroy;
- responseToDestroy = null;
- } else {
- // Status response or response data
- final String tag;
- if (ch == '*') {
- tag = null;
- readByte(); // skip *
- expect(' ');
- } else {
- tag = readUntil(' ');
- }
- responseToDestroy = new ImapResponse(tag, false);
-
- final ImapString firstString = parseBareString();
- responseToDestroy.add(firstString);
-
- // parseBareString won't eat a space after the string, so we need to skip it,
- // if exists.
- // If the next char is not ' ', it should be EOL.
- if (peek() == ' ') {
- readByte(); // skip ' '
-
- if (responseToDestroy.isStatusResponse()) { // It's a status response
-
- // Is there a response code?
- final int next = peek();
- if (next == '[') {
- responseToDestroy.add(parseList('[', ']'));
- if (peek() == ' ') { // Skip following space
- readByte();
- }
- }
-
- String rest = readUntilEol();
- if (!TextUtils.isEmpty(rest)) {
- // The rest is free-form text.
- responseToDestroy.add(new ImapSimpleString(rest));
- }
- } else { // It's a response data.
- parseElements(responseToDestroy, '\0');
- }
- } else {
- expect('\r');
- expect('\n');
- }
-
- // Response has successfully been built. Let's return it.
- responseToReturn = responseToDestroy;
- responseToDestroy = null;
- }
- } finally {
- if (responseToDestroy != null) {
- // We get an exception.
- responseToDestroy.destroy();
- }
- }
-
- return responseToReturn;
- }
-
- private ImapElement parseElement() throws IOException, MessagingException {
- final int next = peek();
- switch (next) {
- case '(':
- return parseList('(', ')');
- case '[':
- return parseList('[', ']');
- case '"':
- readByte(); // Skip "
- return new ImapSimpleString(readUntil('"'));
- case '{':
- return parseLiteral();
- case '\r': // CR
- readByte(); // Consume \r
- expect('\n'); // Should be followed by LF.
- return null;
- case '\n': // LF // There shouldn't be a bare LF, but just in case.
- readByte(); // Consume \n
- return null;
- default:
- return parseBareString();
- }
- }
-
- /**
- * Parses an atom.
- *
- * Special case: If an atom contains '[', everything until the next ']' will be considered
- * a part of the atom.
- * (e.g. "BODY[HEADER.FIELDS ("DATE" ...)]" will become a single ImapString)
- *
- * If the value is "NIL", returns an empty string.
- */
- private ImapString parseBareString() throws IOException, MessagingException {
- mParseBareString.setLength(0);
- for (;;) {
- final int ch = peek();
-
- // TODO Can we clean this up? (This condition is from the old parser.)
- if (ch == '(' || ch == ')' || ch == '{' || ch == ' ' ||
- // ']' is not part of atom (it's in resp-specials)
- ch == ']' ||
- // docs claim that flags are \ atom but atom isn't supposed to
- // contain
- // * and some flags contain *
- // ch == '%' || ch == '*' ||
- ch == '%' ||
- // TODO probably should not allow \ and should recognize
- // it as a flag instead
- // ch == '"' || ch == '\' ||
- ch == '"' || (0x00 <= ch && ch <= 0x1f) || ch == 0x7f) {
- if (mParseBareString.length() == 0) {
- throw new MessagingException("Expected string, none found.");
- }
- String s = mParseBareString.toString();
-
- // NIL will be always converted into the empty string.
- if (ImapConstants.NIL.equalsIgnoreCase(s)) {
- return ImapString.EMPTY;
- }
- return new ImapSimpleString(s);
- } else if (ch == '[') {
- // Eat all until next ']'
- mParseBareString.append((char) readByte());
- mParseBareString.append(readUntil(']'));
- mParseBareString.append(']'); // readUntil won't include the end char.
- } else {
- mParseBareString.append((char) readByte());
- }
- }
- }
-
- private void parseElements(ImapList list, char end)
- throws IOException, MessagingException {
- for (;;) {
- for (;;) {
- final int next = peek();
- if (next == end) {
- return;
- }
- if (next != ' ') {
- break;
- }
- // Skip space
- readByte();
- }
- final ImapElement el = parseElement();
- if (el == null) { // EOL
- return;
- }
- list.add(el);
- }
- }
-
- private ImapList parseList(char opening, char closing)
- throws IOException, MessagingException {
- expect(opening);
- final ImapList list = new ImapList();
- parseElements(list, closing);
- expect(closing);
- return list;
- }
-
- private ImapString parseLiteral() throws IOException, MessagingException {
- expect('{');
- final int size;
- try {
- size = Integer.parseInt(readUntil('}'));
- } catch (NumberFormatException nfe) {
- throw new MessagingException("Invalid length in literal");
- }
- if (size < 0) {
- throw new MessagingException("Invalid negative length in literal");
- }
- expect('\r');
- expect('\n');
- FixedLengthInputStream in = new FixedLengthInputStream(mIn, size);
- if (size > mLiteralKeepInMemoryThreshold) {
- return new ImapTempFileLiteral(in);
- } else {
- return new ImapMemoryLiteral(in);
- }
- }
-} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapSimpleString.java b/java/com/android/voicemailomtp/mail/store/imap/ImapSimpleString.java
deleted file mode 100644
index 22d8141a0..000000000
--- a/java/com/android/voicemailomtp/mail/store/imap/ImapSimpleString.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.voicemailomtp.mail.store.imap;
-
-import com.android.voicemailomtp.VvmLog;
-
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
-
-/**
- * Subclass of {@link ImapString} used for non literals.
- */
-public class ImapSimpleString extends ImapString {
- private final String TAG = "ImapSimpleString";
- private String mString;
-
- /* package */ ImapSimpleString(String string) {
- mString = (string != null) ? string : "";
- }
-
- @Override
- public void destroy() {
- mString = null;
- super.destroy();
- }
-
- @Override
- public String getString() {
- return mString;
- }
-
- @Override
- public InputStream getAsStream() {
- try {
- return new ByteArrayInputStream(mString.getBytes("US-ASCII"));
- } catch (UnsupportedEncodingException e) {
- VvmLog.e(TAG, "Unsupported encoding: ", e);
- }
- return null;
- }
-
- @Override
- public String toString() {
- // Purposefully not return just mString, in order to prevent using it instead of getString.
- return "\"" + mString + "\"";
- }
-} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapString.java b/java/com/android/voicemailomtp/mail/store/imap/ImapString.java
deleted file mode 100644
index 83efb6479..000000000
--- a/java/com/android/voicemailomtp/mail/store/imap/ImapString.java
+++ /dev/null
@@ -1,192 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.voicemailomtp.mail.store.imap;
-
-import com.android.voicemailomtp.VvmLog;
-
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.Locale;
-
-/**
- * Class represents an IMAP "element" that is not a list.
- *
- * An atom, quoted string, literal, are all represented by this. Values like OK, STATUS are too.
- * Also, this class class may contain more arbitrary value like "BODY[HEADER.FIELDS ("DATE")]".
- * See {@link ImapResponseParser}.
- */
-public abstract class ImapString extends ImapElement {
- private static final byte[] EMPTY_BYTES = new byte[0];
-
- public static final ImapString EMPTY = new ImapString() {
- @Override public void destroy() {
- // Don't call super.destroy().
- // It's a shared object. We don't want the mDestroyed to be set on this.
- }
-
- @Override public String getString() {
- return "";
- }
-
- @Override public InputStream getAsStream() {
- return new ByteArrayInputStream(EMPTY_BYTES);
- }
-
- @Override public String toString() {
- return "";
- }
- };
-
- // This is used only for parsing IMAP's FETCH ENVELOPE command, in which
- // en_US-like date format is used like "01-Jan-2009 11:20:39 -0800", so this should be
- // handled by Locale.US
- private final static SimpleDateFormat DATE_TIME_FORMAT =
- new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss Z", Locale.US);
-
- private boolean mIsInteger;
- private int mParsedInteger;
- private Date mParsedDate;
-
- @Override
- public final boolean isList() {
- return false;
- }
-
- @Override
- public final boolean isString() {
- return true;
- }
-
- /**
- * @return true if and only if the length of the string is larger than 0.
- *
- * Note: IMAP NIL is considered an empty string. See {@link ImapResponseParser
- * #parseBareString}.
- * On the other hand, a quoted/literal string with value NIL (i.e. "NIL" and {3}\r\nNIL) is
- * treated literally.
- */
- public final boolean isEmpty() {
- return getString().length() == 0;
- }
-
- public abstract String getString();
-
- public abstract InputStream getAsStream();
-
- /**
- * @return whether it can be parsed as a number.
- */
- public final boolean isNumber() {
- if (mIsInteger) {
- return true;
- }
- try {
- mParsedInteger = Integer.parseInt(getString());
- mIsInteger = true;
- return true;
- } catch (NumberFormatException e) {
- return false;
- }
- }
-
- /**
- * @return value parsed as a number, or 0 if the string is not a number.
- */
- public final int getNumberOrZero() {
- return getNumber(0);
- }
-
- /**
- * @return value parsed as a number, or {@code defaultValue} if the string is not a number.
- */
- public final int getNumber(int defaultValue) {
- if (!isNumber()) {
- return defaultValue;
- }
- return mParsedInteger;
- }
-
- /**
- * @return whether it can be parsed as a date using {@link #DATE_TIME_FORMAT}.
- */
- public final boolean isDate() {
- if (mParsedDate != null) {
- return true;
- }
- if (isEmpty()) {
- return false;
- }
- try {
- mParsedDate = DATE_TIME_FORMAT.parse(getString());
- return true;
- } catch (ParseException e) {
- VvmLog.w("ImapString", getString() + " can't be parsed as a date.");
- return false;
- }
- }
-
- /**
- * @return value it can be parsed as a {@link Date}, or null otherwise.
- */
- public final Date getDateOrNull() {
- if (!isDate()) {
- return null;
- }
- return mParsedDate;
- }
-
- /**
- * @return whether the value case-insensitively equals to {@code s}.
- */
- public final boolean is(String s) {
- if (s == null) {
- return false;
- }
- return getString().equalsIgnoreCase(s);
- }
-
-
- /**
- * @return whether the value case-insensitively starts with {@code s}.
- */
- public final boolean startsWith(String prefix) {
- if (prefix == null) {
- return false;
- }
- final String me = this.getString();
- if (me.length() < prefix.length()) {
- return false;
- }
- return me.substring(0, prefix.length()).equalsIgnoreCase(prefix);
- }
-
- // To force subclasses to implement it.
- @Override
- public abstract String toString();
-
- @Override
- public final boolean equalsForTest(ImapElement that) {
- if (!super.equalsForTest(that)) {
- return false;
- }
- ImapString thatString = (ImapString) that;
- return getString().equals(thatString.getString());
- }
-} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapTempFileLiteral.java b/java/com/android/voicemailomtp/mail/store/imap/ImapTempFileLiteral.java
deleted file mode 100644
index efe5c3848..000000000
--- a/java/com/android/voicemailomtp/mail/store/imap/ImapTempFileLiteral.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail.store.imap;
-
-import com.android.voicemailomtp.mail.FixedLengthInputStream;
-import com.android.voicemailomtp.mail.TempDirectory;
-import com.android.voicemailomtp.mail.utils.Utility;
-import com.android.voicemailomtp.mail.utils.LogUtils;
-
-import org.apache.commons.io.IOUtils;
-
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-/**
- * Subclass of {@link ImapString} used for literals backed by a temp file.
- */
-public class ImapTempFileLiteral extends ImapString {
- private final String TAG = "ImapTempFileLiteral";
-
- /* package for test */ final File mFile;
-
- /** Size is purely for toString() */
- private final int mSize;
-
- /* package */ ImapTempFileLiteral(FixedLengthInputStream stream) throws IOException {
- mSize = stream.getLength();
- mFile = File.createTempFile("imap", ".tmp", TempDirectory.getTempDirectory());
-
- // Unfortunately, we can't really use deleteOnExit(), because temp filenames are random
- // so it'd simply cause a memory leak.
- // deleteOnExit() simply adds filenames to a static list and the list will never shrink.
- // mFile.deleteOnExit();
- OutputStream out = new FileOutputStream(mFile);
- IOUtils.copy(stream, out);
- out.close();
- }
-
- /**
- * Make sure we delete the temp file.
- *
- * We should always be calling {@link ImapResponse#destroy()}, but it's here as a last resort.
- */
- @Override
- protected void finalize() throws Throwable {
- try {
- destroy();
- } finally {
- super.finalize();
- }
- }
-
- @Override
- public InputStream getAsStream() {
- checkNotDestroyed();
- try {
- return new FileInputStream(mFile);
- } catch (FileNotFoundException e) {
- // It's probably possible if we're low on storage and the system clears the cache dir.
- LogUtils.w(TAG, "ImapTempFileLiteral: Temp file not found");
-
- // Return 0 byte stream as a dummy...
- return new ByteArrayInputStream(new byte[0]);
- }
- }
-
- @Override
- public String getString() {
- checkNotDestroyed();
- try {
- byte[] bytes = IOUtils.toByteArray(getAsStream());
- // Prevent crash from OOM; we've seen this, but only rarely and not reproducibly
- if (bytes.length > ImapResponseParser.LITERAL_KEEP_IN_MEMORY_THRESHOLD) {
- throw new IOException();
- }
- return Utility.fromAscii(bytes);
- } catch (IOException e) {
- LogUtils.w(TAG, "ImapTempFileLiteral: Error while reading temp file", e);
- return "";
- }
- }
-
- @Override
- public void destroy() {
- try {
- if (!isDestroyed() && mFile.exists()) {
- mFile.delete();
- }
- } catch (RuntimeException re) {
- // Just log and ignore.
- LogUtils.w(TAG, "Failed to remove temp file: " + re.getMessage());
- }
- super.destroy();
- }
-
- @Override
- public String toString() {
- return String.format("{%d byte literal(file)}", mSize);
- }
-
- public boolean tempFileExistsForTest() {
- return mFile.exists();
- }
-} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapUtility.java b/java/com/android/voicemailomtp/mail/store/imap/ImapUtility.java
deleted file mode 100644
index b045eb32f..000000000
--- a/java/com/android/voicemailomtp/mail/store/imap/ImapUtility.java
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail.store.imap;
-
-import com.android.voicemailomtp.mail.utils.LogUtils;
-
-import java.util.ArrayList;
-
-/**
- * Utility methods for use with IMAP.
- */
-public class ImapUtility {
- public static final String TAG = "ImapUtility";
- /**
- * Apply quoting rules per IMAP RFC,
- * quoted = DQUOTE *QUOTED-CHAR DQUOTE
- * QUOTED-CHAR = <any TEXT-CHAR except quoted-specials> / "\" quoted-specials
- * quoted-specials = DQUOTE / "\"
- *
- * This is used primarily for IMAP login, but might be useful elsewhere.
- *
- * NOTE: Not very efficient - you may wish to preflight this, or perhaps it should check
- * for trouble chars before calling the replace functions.
- *
- * @param s The string to be quoted.
- * @return A copy of the string, having undergone quoting as described above
- */
- public static String imapQuoted(String s) {
-
- // First, quote any backslashes by replacing \ with \\
- // regex Pattern: \\ (Java string const = \\\\)
- // Substitute: \\\\ (Java string const = \\\\\\\\)
- String result = s.replaceAll("\\\\", "\\\\\\\\");
-
- // Then, quote any double-quotes by replacing " with \"
- // regex Pattern: " (Java string const = \")
- // Substitute: \\" (Java string const = \\\\\")
- result = result.replaceAll("\"", "\\\\\"");
-
- // return string with quotes around it
- return "\"" + result + "\"";
- }
-
- /**
- * Gets all of the values in a sequence set per RFC 3501. Any ranges are expanded into a
- * list of individual numbers. If the set is invalid, an empty array is returned.
- * <pre>
- * sequence-number = nz-number / "*"
- * sequence-range = sequence-number ":" sequence-number
- * sequence-set = (sequence-number / sequence-range) *("," sequence-set)
- * </pre>
- */
- public static String[] getImapSequenceValues(String set) {
- ArrayList<String> list = new ArrayList<String>();
- if (set != null) {
- String[] setItems = set.split(",");
- for (String item : setItems) {
- if (item.indexOf(':') == -1) {
- // simple item
- try {
- Integer.parseInt(item); // Don't need the value; just ensure it's valid
- list.add(item);
- } catch (NumberFormatException e) {
- LogUtils.d(TAG, "Invalid UID value", e);
- }
- } else {
- // range
- for (String rangeItem : getImapRangeValues(item)) {
- list.add(rangeItem);
- }
- }
- }
- }
- String[] stringList = new String[list.size()];
- return list.toArray(stringList);
- }
-
- /**
- * Expand the given number range into a list of individual numbers. If the range is not valid,
- * an empty array is returned.
- * <pre>
- * sequence-number = nz-number / "*"
- * sequence-range = sequence-number ":" sequence-number
- * sequence-set = (sequence-number / sequence-range) *("," sequence-set)
- * </pre>
- */
- public static String[] getImapRangeValues(String range) {
- ArrayList<String> list = new ArrayList<String>();
- try {
- if (range != null) {
- int colonPos = range.indexOf(':');
- if (colonPos > 0) {
- int first = Integer.parseInt(range.substring(0, colonPos));
- int second = Integer.parseInt(range.substring(colonPos + 1));
- if (first < second) {
- for (int i = first; i <= second; i++) {
- list.add(Integer.toString(i));
- }
- } else {
- for (int i = first; i >= second; i--) {
- list.add(Integer.toString(i));
- }
- }
- }
- }
- } catch (NumberFormatException e) {
- LogUtils.d(TAG, "Invalid range value", e);
- }
- String[] stringList = new String[list.size()];
- return list.toArray(stringList);
- }
-} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/utils/LogUtils.java b/java/com/android/voicemailomtp/mail/utils/LogUtils.java
deleted file mode 100644
index a213a835e..000000000
--- a/java/com/android/voicemailomtp/mail/utils/LogUtils.java
+++ /dev/null
@@ -1,413 +0,0 @@
-/**
- * Copyright (c) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail.utils;
-
-import android.net.Uri;
-import android.support.annotation.VisibleForTesting;
-import android.text.TextUtils;
-import android.util.Log;
-import com.android.voicemailomtp.VvmLog;
-import java.util.List;
-import java.util.regex.Pattern;
-
-public class LogUtils {
- public static final String TAG = "Email Log";
-
- // "GMT" + "+" or "-" + 4 digits
- private static final Pattern DATE_CLEANUP_PATTERN_WRONG_TIMEZONE =
- Pattern.compile("GMT([-+]\\d{4})$");
-
- private static final String ACCOUNT_PREFIX = "account:";
-
- /**
- * Priority constant for the println method; use LogUtils.v.
- */
- public static final int VERBOSE = Log.VERBOSE;
-
- /**
- * Priority constant for the println method; use LogUtils.d.
- */
- public static final int DEBUG = Log.DEBUG;
-
- /**
- * Priority constant for the println method; use LogUtils.i.
- */
- public static final int INFO = Log.INFO;
-
- /**
- * Priority constant for the println method; use LogUtils.w.
- */
- public static final int WARN = Log.WARN;
-
- /**
- * Priority constant for the println method; use LogUtils.e.
- */
- public static final int ERROR = Log.ERROR;
-
- /**
- * Used to enable/disable logging that we don't want included in production releases. This should
- * be set to DEBUG for production releases, and VERBOSE for internal builds.
- */
- private static final int MAX_ENABLED_LOG_LEVEL = DEBUG;
-
- private static Boolean sDebugLoggingEnabledForTests = null;
-
- /**
- * Enable debug logging for unit tests.
- */
- @VisibleForTesting
- public static void setDebugLoggingEnabledForTests(boolean enabled) {
- setDebugLoggingEnabledForTestsInternal(enabled);
- }
-
- protected static void setDebugLoggingEnabledForTestsInternal(boolean enabled) {
- sDebugLoggingEnabledForTests = Boolean.valueOf(enabled);
- }
-
- /**
- * Returns true if the build configuration prevents debug logging.
- */
- @VisibleForTesting
- public static boolean buildPreventsDebugLogging() {
- return MAX_ENABLED_LOG_LEVEL > VERBOSE;
- }
-
- /**
- * Returns a boolean indicating whether debug logging is enabled.
- */
- protected static boolean isDebugLoggingEnabled(String tag) {
- if (buildPreventsDebugLogging()) {
- return false;
- }
- if (sDebugLoggingEnabledForTests != null) {
- return sDebugLoggingEnabledForTests.booleanValue();
- }
- return Log.isLoggable(tag, Log.DEBUG) || Log.isLoggable(TAG, Log.DEBUG);
- }
-
- /**
- * Returns a String for the specified content provider uri. This will do
- * sanitation of the uri to remove PII if debug logging is not enabled.
- */
- public static String contentUriToString(final Uri uri) {
- return contentUriToString(TAG, uri);
- }
-
- /**
- * Returns a String for the specified content provider uri. This will do
- * sanitation of the uri to remove PII if debug logging is not enabled.
- */
- public static String contentUriToString(String tag, Uri uri) {
- if (isDebugLoggingEnabled(tag)) {
- // Debug logging has been enabled, so log the uri as is
- return uri.toString();
- } else {
- // Debug logging is not enabled, we want to remove the email address from the uri.
- List<String> pathSegments = uri.getPathSegments();
-
- Uri.Builder builder = new Uri.Builder()
- .scheme(uri.getScheme())
- .authority(uri.getAuthority())
- .query(uri.getQuery())
- .fragment(uri.getFragment());
-
- // This assumes that the first path segment is the account
- final String account = pathSegments.get(0);
-
- builder = builder.appendPath(sanitizeAccountName(account));
- for (int i = 1; i < pathSegments.size(); i++) {
- builder.appendPath(pathSegments.get(i));
- }
- return builder.toString();
- }
- }
-
- /**
- * Sanitizes an account name. If debug logging is not enabled, a sanitized name
- * is returned.
- */
- public static String sanitizeAccountName(String accountName) {
- if (TextUtils.isEmpty(accountName)) {
- return "";
- }
-
- return ACCOUNT_PREFIX + sanitizeName(TAG, accountName);
- }
-
- public static String sanitizeName(final String tag, final String name) {
- if (TextUtils.isEmpty(name)) {
- return "";
- }
-
- if (isDebugLoggingEnabled(tag)) {
- return name;
- }
-
- return String.valueOf(name.hashCode());
- }
-
- /**
- * Checks to see whether or not a log for the specified tag is loggable at the specified level.
- */
- public static boolean isLoggable(String tag, int level) {
- if (MAX_ENABLED_LOG_LEVEL > level) {
- return false;
- }
- return Log.isLoggable(tag, level) || Log.isLoggable(TAG, level);
- }
-
- /**
- * Send a {@link #VERBOSE} log message.
- * @param tag Used to identify the source of a log message. It usually identifies
- * the class or activity where the log call occurs.
- * @param format the format string (see {@link java.util.Formatter#format})
- * @param args
- * the list of arguments passed to the formatter. If there are
- * more arguments than required by {@code format},
- * additional arguments are ignored.
- */
- public static int v(String tag, String format, Object... args) {
- if (isLoggable(tag, VERBOSE)) {
- return VvmLog.v(tag, String.format(format, args));
- }
- return 0;
- }
-
- /**
- * Send a {@link #VERBOSE} log message.
- * @param tag Used to identify the source of a log message. It usually identifies
- * the class or activity where the log call occurs.
- * @param tr An exception to log
- * @param format the format string (see {@link java.util.Formatter#format})
- * @param args
- * the list of arguments passed to the formatter. If there are
- * more arguments than required by {@code format},
- * additional arguments are ignored.
- */
- public static int v(String tag, Throwable tr, String format, Object... args) {
- if (isLoggable(tag, VERBOSE)) {
- return VvmLog.v(tag, String.format(format, args), tr);
- }
- return 0;
- }
-
- /**
- * Send a {@link #DEBUG} log message.
- * @param tag Used to identify the source of a log message. It usually identifies
- * the class or activity where the log call occurs.
- * @param format the format string (see {@link java.util.Formatter#format})
- * @param args
- * the list of arguments passed to the formatter. If there are
- * more arguments than required by {@code format},
- * additional arguments are ignored.
- */
- public static int d(String tag, String format, Object... args) {
- if (isLoggable(tag, DEBUG)) {
- return VvmLog.d(tag, String.format(format, args));
- }
- return 0;
- }
-
- /**
- * Send a {@link #DEBUG} log message.
- * @param tag Used to identify the source of a log message. It usually identifies
- * the class or activity where the log call occurs.
- * @param tr An exception to log
- * @param format the format string (see {@link java.util.Formatter#format})
- * @param args
- * the list of arguments passed to the formatter. If there are
- * more arguments than required by {@code format},
- * additional arguments are ignored.
- */
- public static int d(String tag, Throwable tr, String format, Object... args) {
- if (isLoggable(tag, DEBUG)) {
- return VvmLog.d(tag, String.format(format, args), tr);
- }
- return 0;
- }
-
- /**
- * Send a {@link #INFO} log message.
- * @param tag Used to identify the source of a log message. It usually identifies
- * the class or activity where the log call occurs.
- * @param format the format string (see {@link java.util.Formatter#format})
- * @param args
- * the list of arguments passed to the formatter. If there are
- * more arguments than required by {@code format},
- * additional arguments are ignored.
- */
- public static int i(String tag, String format, Object... args) {
- if (isLoggable(tag, INFO)) {
- return VvmLog.i(tag, String.format(format, args));
- }
- return 0;
- }
-
- /**
- * Send a {@link #INFO} log message.
- * @param tag Used to identify the source of a log message. It usually identifies
- * the class or activity where the log call occurs.
- * @param tr An exception to log
- * @param format the format string (see {@link java.util.Formatter#format})
- * @param args
- * the list of arguments passed to the formatter. If there are
- * more arguments than required by {@code format},
- * additional arguments are ignored.
- */
- public static int i(String tag, Throwable tr, String format, Object... args) {
- if (isLoggable(tag, INFO)) {
- return VvmLog.i(tag, String.format(format, args), tr);
- }
- return 0;
- }
-
- /**
- * Send a {@link #WARN} log message.
- * @param tag Used to identify the source of a log message. It usually identifies
- * the class or activity where the log call occurs.
- * @param format the format string (see {@link java.util.Formatter#format})
- * @param args
- * the list of arguments passed to the formatter. If there are
- * more arguments than required by {@code format},
- * additional arguments are ignored.
- */
- public static int w(String tag, String format, Object... args) {
- if (isLoggable(tag, WARN)) {
- return VvmLog.w(tag, String.format(format, args));
- }
- return 0;
- }
-
- /**
- * Send a {@link #WARN} log message.
- * @param tag Used to identify the source of a log message. It usually identifies
- * the class or activity where the log call occurs.
- * @param tr An exception to log
- * @param format the format string (see {@link java.util.Formatter#format})
- * @param args
- * the list of arguments passed to the formatter. If there are
- * more arguments than required by {@code format},
- * additional arguments are ignored.
- */
- public static int w(String tag, Throwable tr, String format, Object... args) {
- if (isLoggable(tag, WARN)) {
- return VvmLog.w(tag, String.format(format, args), tr);
- }
- return 0;
- }
-
- /**
- * Send a {@link #ERROR} log message.
- * @param tag Used to identify the source of a log message. It usually identifies
- * the class or activity where the log call occurs.
- * @param format the format string (see {@link java.util.Formatter#format})
- * @param args
- * the list of arguments passed to the formatter. If there are
- * more arguments than required by {@code format},
- * additional arguments are ignored.
- */
- public static int e(String tag, String format, Object... args) {
- if (isLoggable(tag, ERROR)) {
- return VvmLog.e(tag, String.format(format, args));
- }
- return 0;
- }
-
- /**
- * Send a {@link #ERROR} log message.
- * @param tag Used to identify the source of a log message. It usually identifies
- * the class or activity where the log call occurs.
- * @param tr An exception to log
- * @param format the format string (see {@link java.util.Formatter#format})
- * @param args
- * the list of arguments passed to the formatter. If there are
- * more arguments than required by {@code format},
- * additional arguments are ignored.
- */
- public static int e(String tag, Throwable tr, String format, Object... args) {
- if (isLoggable(tag, ERROR)) {
- return VvmLog.e(tag, String.format(format, args), tr);
- }
- return 0;
- }
-
- /**
- * What a Terrible Failure: Report a condition that should never happen.
- * The error will always be logged at level ASSERT with the call stack.
- * Depending on system configuration, a report may be added to the
- * {@link android.os.DropBoxManager} and/or the process may be terminated
- * immediately with an error dialog.
- * @param tag Used to identify the source of a log message. It usually identifies
- * the class or activity where the log call occurs.
- * @param format the format string (see {@link java.util.Formatter#format})
- * @param args
- * the list of arguments passed to the formatter. If there are
- * more arguments than required by {@code format},
- * additional arguments are ignored.
- */
- public static int wtf(String tag, String format, Object... args) {
- return VvmLog.wtf(tag, String.format(format, args), new Error());
- }
-
- /**
- * What a Terrible Failure: Report a condition that should never happen.
- * The error will always be logged at level ASSERT with the call stack.
- * Depending on system configuration, a report may be added to the
- * {@link android.os.DropBoxManager} and/or the process may be terminated
- * immediately with an error dialog.
- * @param tag Used to identify the source of a log message. It usually identifies
- * the class or activity where the log call occurs.
- * @param tr An exception to log
- * @param format the format string (see {@link java.util.Formatter#format})
- * @param args
- * the list of arguments passed to the formatter. If there are
- * more arguments than required by {@code format},
- * additional arguments are ignored.
- */
- public static int wtf(String tag, Throwable tr, String format, Object... args) {
- return VvmLog.wtf(tag, String.format(format, args), tr);
- }
-
-
- /**
- * Try to make a date MIME(RFC 2822/5322)-compliant.
- *
- * It fixes:
- * - "Thu, 10 Dec 09 15:08:08 GMT-0700" to "Thu, 10 Dec 09 15:08:08 -0700"
- * (4 digit zone value can't be preceded by "GMT")
- * We got a report saying eBay sends a date in this format
- */
- public static String cleanUpMimeDate(String date) {
- if (TextUtils.isEmpty(date)) {
- return date;
- }
- date = DATE_CLEANUP_PATTERN_WRONG_TIMEZONE.matcher(date).replaceFirst("$1");
- return date;
- }
-
-
- public static String byteToHex(int b) {
- return byteToHex(new StringBuilder(), b).toString();
- }
-
- public static StringBuilder byteToHex(StringBuilder sb, int b) {
- b &= 0xFF;
- sb.append("0123456789ABCDEF".charAt(b >> 4));
- sb.append("0123456789ABCDEF".charAt(b & 0xF));
- return sb;
- }
-
-}
diff --git a/java/com/android/voicemailomtp/mail/utils/Utility.java b/java/com/android/voicemailomtp/mail/utils/Utility.java
deleted file mode 100644
index c7286fa64..000000000
--- a/java/com/android/voicemailomtp/mail/utils/Utility.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/**
- * Copyright (c) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail.utils;
-
-import java.io.ByteArrayInputStream;
-import java.nio.ByteBuffer;
-import java.nio.CharBuffer;
-import java.nio.charset.Charset;
-
-/**
- * Simple utility methods used in email functions.
- */
-public class Utility {
- public static final Charset ASCII = Charset.forName("US-ASCII");
-
- public static final String[] EMPTY_STRINGS = new String[0];
-
- /**
- * Returns a concatenated string containing the output of every Object's
- * toString() method, each separated by the given separator character.
- */
- public static String combine(Object[] parts, char separator) {
- if (parts == null) {
- return null;
- }
- StringBuilder sb = new StringBuilder();
- for (int i = 0; i < parts.length; i++) {
- sb.append(parts[i].toString());
- if (i < parts.length - 1) {
- sb.append(separator);
- }
- }
- return sb.toString();
- }
-
- /** Converts a String to ASCII bytes */
- public static byte[] toAscii(String s) {
- return encode(ASCII, s);
- }
-
- /** Builds a String from ASCII bytes */
- public static String fromAscii(byte[] b) {
- return decode(ASCII, b);
- }
-
- private static byte[] encode(Charset charset, String s) {
- if (s == null) {
- return null;
- }
- final ByteBuffer buffer = charset.encode(CharBuffer.wrap(s));
- final byte[] bytes = new byte[buffer.limit()];
- buffer.get(bytes);
- return bytes;
- }
-
- private static String decode(Charset charset, byte[] b) {
- if (b == null) {
- return null;
- }
- final CharBuffer cb = charset.decode(ByteBuffer.wrap(b));
- return new String(cb.array(), 0, cb.length());
- }
-
- public static ByteArrayInputStream streamFromAsciiString(String ascii) {
- return new ByteArrayInputStream(toAscii(ascii));
- }
-}
diff --git a/java/com/android/voicemailomtp/protocol/CvvmProtocol.java b/java/com/android/voicemailomtp/protocol/CvvmProtocol.java
deleted file mode 100644
index 48ed99709..000000000
--- a/java/com/android/voicemailomtp/protocol/CvvmProtocol.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.protocol;
-
-import android.content.Context;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.SmsManager;
-
-import com.android.voicemailomtp.OmtpConstants;
-import com.android.voicemailomtp.sms.OmtpCvvmMessageSender;
-import com.android.voicemailomtp.sms.OmtpMessageSender;
-
-/**
- * A flavor of OMTP protocol with a different mobile originated (MO) format
- *
- * Used by carriers such as T-Mobile
- */
-public class CvvmProtocol extends VisualVoicemailProtocol {
-
- private static String IMAP_CHANGE_TUI_PWD_FORMAT = "CHANGE_TUI_PWD PWD=%1$s OLD_PWD=%2$s";
- private static String IMAP_CHANGE_VM_LANG_FORMAT = "CHANGE_VM_LANG Lang=%1$s";
- private static String IMAP_CLOSE_NUT = "CLOSE_NUT";
-
- @Override
- public OmtpMessageSender createMessageSender(Context context,
- PhoneAccountHandle phoneAccountHandle, short applicationPort,
- String destinationNumber) {
- return new OmtpCvvmMessageSender(context, phoneAccountHandle, applicationPort,
- destinationNumber);
- }
-
- @Override
- public String getCommand(String command) {
- if (command == OmtpConstants.IMAP_CHANGE_TUI_PWD_FORMAT) {
- return IMAP_CHANGE_TUI_PWD_FORMAT;
- }
- if (command == OmtpConstants.IMAP_CLOSE_NUT) {
- return IMAP_CLOSE_NUT;
- }
- if (command == OmtpConstants.IMAP_CHANGE_VM_LANG_FORMAT) {
- return IMAP_CHANGE_VM_LANG_FORMAT;
- }
- return super.getCommand(command);
- }
-}
diff --git a/java/com/android/voicemailomtp/protocol/ProtocolHelper.java b/java/com/android/voicemailomtp/protocol/ProtocolHelper.java
deleted file mode 100644
index 4fca199bf..000000000
--- a/java/com/android/voicemailomtp/protocol/ProtocolHelper.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.protocol;
-
-import android.telephony.SmsManager;
-import android.text.TextUtils;
-
-import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
-import com.android.voicemailomtp.VvmLog;
-import com.android.voicemailomtp.sms.OmtpMessageSender;
-
-public class ProtocolHelper {
-
- private static final String TAG = "ProtocolHelper";
-
- public static OmtpMessageSender getMessageSender(VisualVoicemailProtocol protocol,
- OmtpVvmCarrierConfigHelper config) {
-
- int applicationPort = config.getApplicationPort();
- String destinationNumber = config.getDestinationNumber();
- if (TextUtils.isEmpty(destinationNumber)) {
- VvmLog.w(TAG, "No destination number for this carrier.");
- return null;
- }
-
- return protocol.createMessageSender(config.getContext(), config.getPhoneAccountHandle(),
- (short) applicationPort, destinationNumber);
- }
-}
diff --git a/java/com/android/voicemailomtp/protocol/VisualVoicemailProtocol.java b/java/com/android/voicemailomtp/protocol/VisualVoicemailProtocol.java
deleted file mode 100644
index 9ff2ed167..000000000
--- a/java/com/android/voicemailomtp/protocol/VisualVoicemailProtocol.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.protocol;
-
-import android.app.PendingIntent;
-import android.content.Context;
-import android.os.Bundle;
-import android.support.annotation.Nullable;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.SmsManager;
-import com.android.voicemailomtp.ActivationTask;
-import com.android.voicemailomtp.DefaultOmtpEventHandler;
-import com.android.voicemailomtp.OmtpEvents;
-import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
-import com.android.voicemailomtp.VoicemailStatus;
-import com.android.voicemailomtp.sms.OmtpMessageSender;
-import com.android.voicemailomtp.sms.StatusMessage;
-
-public abstract class VisualVoicemailProtocol {
-
- /**
- * Activation should cause the carrier to respond with a STATUS SMS.
- */
- public void startActivation(OmtpVvmCarrierConfigHelper config, PendingIntent sentIntent) {
- OmtpMessageSender messageSender = ProtocolHelper.getMessageSender(this, config);
- if (messageSender != null) {
- messageSender.requestVvmActivation(sentIntent);
- }
- }
-
- public void startDeactivation(OmtpVvmCarrierConfigHelper config) {
- OmtpMessageSender messageSender = ProtocolHelper.getMessageSender(this, config);
- if (messageSender != null) {
- messageSender.requestVvmDeactivation(null);
- }
- }
-
- public boolean supportsProvisioning() {
- return false;
- }
-
- public void startProvisioning(ActivationTask task, PhoneAccountHandle handle,
- OmtpVvmCarrierConfigHelper config, VoicemailStatus.Editor editor, StatusMessage message,
- Bundle data) {
- // Do nothing
- }
-
- public void requestStatus(OmtpVvmCarrierConfigHelper config,
- @Nullable PendingIntent sentIntent) {
- OmtpMessageSender messageSender = ProtocolHelper.getMessageSender(this, config);
- if (messageSender != null) {
- messageSender.requestVvmStatus(sentIntent);
- }
- }
-
- public abstract OmtpMessageSender createMessageSender(Context context,
- PhoneAccountHandle phoneAccountHandle,
- short applicationPort, String destinationNumber);
-
- /**
- * Translate an OMTP IMAP command to the protocol specific one. For example, changing the TUI
- * password on OMTP is XCHANGE_TUI_PWD, but on CVVM and VVM3 it is CHANGE_TUI_PWD.
- *
- * @param command A String command in {@link com.android.voicemailomtp.OmtpConstants}, the exact
- * instance should be used instead of its' value.
- * @returns Translated command, or {@code null} if not available in this protocol
- */
- public String getCommand(String command) {
- return command;
- }
-
- public void handleEvent(Context context, OmtpVvmCarrierConfigHelper config,
- VoicemailStatus.Editor status, OmtpEvents event) {
- DefaultOmtpEventHandler.handleEvent(context, config, status, event);
- }
-
- /**
- * Given an VVM SMS with an unknown {@code event}, let the protocol attempt to translate it into
- * an equivalent STATUS SMS. Returns {@code null} if it cannot be translated.
- */
- @Nullable
- public Bundle translateStatusSmsBundle(OmtpVvmCarrierConfigHelper config, String event,
- Bundle data) {
- return null;
- }
-}
diff --git a/java/com/android/voicemailomtp/protocol/Vvm3EventHandler.java b/java/com/android/voicemailomtp/protocol/Vvm3EventHandler.java
deleted file mode 100644
index 72646386c..000000000
--- a/java/com/android/voicemailomtp/protocol/Vvm3EventHandler.java
+++ /dev/null
@@ -1,271 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.protocol;
-
-import android.content.Context;
-import android.support.annotation.IntDef;
-import android.util.Log;
-import com.android.voicemailomtp.DefaultOmtpEventHandler;
-import com.android.voicemailomtp.OmtpEvents;
-import com.android.voicemailomtp.OmtpEvents.Type;
-import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
-import com.android.voicemailomtp.VoicemailStatus;
-import com.android.voicemailomtp.settings.VoicemailChangePinActivity;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/**
- * Handles {@link OmtpEvents} when {@link Vvm3Protocol} is being used. This handler writes custom
- * error codes into the voicemail status table so support on the dialer side is required.
- *
- * TODO(b/29577838) disable VVM3 by default so support on system dialer can be ensured.
- */
-public class Vvm3EventHandler {
-
- private static final String TAG = "Vvm3EventHandler";
-
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({VMS_DNS_FAILURE, VMG_DNS_FAILURE, SPG_DNS_FAILURE, VMS_NO_CELLULAR, VMG_NO_CELLULAR,
- SPG_NO_CELLULAR, VMS_TIMEOUT, VMG_TIMEOUT, STATUS_SMS_TIMEOUT, SUBSCRIBER_BLOCKED,
- UNKNOWN_USER, UNKNOWN_DEVICE, INVALID_PASSWORD, MAILBOX_NOT_INITIALIZED,
- SERVICE_NOT_PROVISIONED, SERVICE_NOT_ACTIVATED, USER_BLOCKED, IMAP_GETQUOTA_ERROR,
- IMAP_SELECT_ERROR, IMAP_ERROR, VMG_INTERNAL_ERROR, VMG_DB_ERROR,
- VMG_COMMUNICATION_ERROR, SPG_URL_NOT_FOUND, VMG_UNKNOWN_ERROR, PIN_NOT_SET})
- public @interface ErrorCode {
-
- }
-
- public static final int VMS_DNS_FAILURE = -9001;
- public static final int VMG_DNS_FAILURE = -9002;
- public static final int SPG_DNS_FAILURE = -9003;
- public static final int VMS_NO_CELLULAR = -9004;
- public static final int VMG_NO_CELLULAR = -9005;
- public static final int SPG_NO_CELLULAR = -9006;
- public static final int VMS_TIMEOUT = -9007;
- public static final int VMG_TIMEOUT = -9008;
- public static final int STATUS_SMS_TIMEOUT = -9009;
-
- public static final int SUBSCRIBER_BLOCKED = -9990;
- public static final int UNKNOWN_USER = -9991;
- public static final int UNKNOWN_DEVICE = -9992;
- public static final int INVALID_PASSWORD = -9993;
- public static final int MAILBOX_NOT_INITIALIZED = -9994;
- public static final int SERVICE_NOT_PROVISIONED = -9995;
- public static final int SERVICE_NOT_ACTIVATED = -9996;
- public static final int USER_BLOCKED = -9998;
- public static final int IMAP_GETQUOTA_ERROR = -9997;
- public static final int IMAP_SELECT_ERROR = -9989;
- public static final int IMAP_ERROR = -9999;
-
- public static final int VMG_INTERNAL_ERROR = -101;
- public static final int VMG_DB_ERROR = -102;
- public static final int VMG_COMMUNICATION_ERROR = -103;
- public static final int SPG_URL_NOT_FOUND = -301;
-
- // Non VVM3 codes:
- public static final int VMG_UNKNOWN_ERROR = -1;
- public static final int PIN_NOT_SET = -100;
- // STATUS SMS returned st=U and rc!=2. The user cannot be provisioned and must contact customer
- // support.
- public static final int SUBSCRIBER_UNKNOWN = -99;
-
-
- public static void handleEvent(Context context, OmtpVvmCarrierConfigHelper config,
- VoicemailStatus.Editor status, OmtpEvents event) {
- boolean handled = false;
- switch (event.getType()) {
- case Type.CONFIGURATION:
- handled = handleConfigurationEvent(context, status, event);
- break;
- case Type.DATA_CHANNEL:
- handled = handleDataChannelEvent(status, event);
- break;
- case Type.NOTIFICATION_CHANNEL:
- handled = handleNotificationChannelEvent(status, event);
- break;
- case Type.OTHER:
- handled = handleOtherEvent(status, event);
- break;
- default:
- Log.wtf(TAG, "invalid event type " + event.getType() + " for " + event);
- }
- if (!handled) {
- DefaultOmtpEventHandler.handleEvent(context, config, status, event);
- }
- }
-
- private static boolean handleConfigurationEvent(Context context, VoicemailStatus.Editor status,
- OmtpEvents event) {
- switch (event) {
- case CONFIG_REQUEST_STATUS_SUCCESS:
- if (status.getPhoneAccountHandle() == null) {
- // This should never happen.
- Log.e(TAG, "status editor has null phone account handle");
- return true;
- }
-
- if (!VoicemailChangePinActivity
- .isDefaultOldPinSet(context, status.getPhoneAccountHandle())) {
- return false;
- } else {
- postError(status, PIN_NOT_SET);
- }
- break;
- case CONFIG_DEFAULT_PIN_REPLACED:
- postError(status, PIN_NOT_SET);
- break;
- case CONFIG_STATUS_SMS_TIME_OUT:
- postError(status, STATUS_SMS_TIMEOUT);
- break;
- default:
- return false;
- }
- return true;
- }
-
- private static boolean handleDataChannelEvent(VoicemailStatus.Editor status, OmtpEvents event) {
- switch (event) {
- case DATA_NO_CONNECTION:
- case DATA_NO_CONNECTION_CELLULAR_REQUIRED:
- case DATA_ALL_SOCKET_CONNECTION_FAILED:
- postError(status, VMS_NO_CELLULAR);
- break;
- case DATA_SSL_INVALID_HOST_NAME:
- case DATA_CANNOT_ESTABLISH_SSL_SESSION:
- case DATA_IOE_ON_OPEN:
- postError(status, VMS_TIMEOUT);
- break;
- case DATA_CANNOT_RESOLVE_HOST_ON_NETWORK:
- postError(status, VMS_DNS_FAILURE);
- break;
- case DATA_BAD_IMAP_CREDENTIAL:
- postError(status, IMAP_ERROR);
- break;
- case DATA_AUTH_UNKNOWN_USER:
- postError(status, UNKNOWN_USER);
- break;
- case DATA_AUTH_UNKNOWN_DEVICE:
- postError(status, UNKNOWN_DEVICE);
- break;
- case DATA_AUTH_INVALID_PASSWORD:
- postError(status, INVALID_PASSWORD);
- break;
- case DATA_AUTH_MAILBOX_NOT_INITIALIZED:
- postError(status, MAILBOX_NOT_INITIALIZED);
- break;
- case DATA_AUTH_SERVICE_NOT_PROVISIONED:
- postError(status, SERVICE_NOT_PROVISIONED);
- break;
- case DATA_AUTH_SERVICE_NOT_ACTIVATED:
- postError(status, SERVICE_NOT_ACTIVATED);
- break;
- case DATA_AUTH_USER_IS_BLOCKED:
- postError(status, USER_BLOCKED);
- break;
- case DATA_REJECTED_SERVER_RESPONSE:
- case DATA_INVALID_INITIAL_SERVER_RESPONSE:
- case DATA_SSL_EXCEPTION:
- postError(status, IMAP_ERROR);
- break;
- default:
- return false;
- }
- return true;
- }
-
- private static boolean handleNotificationChannelEvent(VoicemailStatus.Editor status,
- OmtpEvents event) {
- return false;
- }
-
- private static boolean handleOtherEvent(VoicemailStatus.Editor status,
- OmtpEvents event) {
- switch (event) {
- case VVM3_NEW_USER_SETUP_FAILED:
- postError(status, MAILBOX_NOT_INITIALIZED);
- break;
- case VVM3_VMG_DNS_FAILURE:
- postError(status, VMG_DNS_FAILURE);
- break;
- case VVM3_SPG_DNS_FAILURE:
- postError(status, SPG_DNS_FAILURE);
- break;
- case VVM3_VMG_CONNECTION_FAILED:
- postError(status, VMG_NO_CELLULAR);
- break;
- case VVM3_SPG_CONNECTION_FAILED:
- postError(status, SPG_NO_CELLULAR);
- break;
- case VVM3_VMG_TIMEOUT:
- postError(status, VMG_TIMEOUT);
- break;
- case VVM3_SUBSCRIBER_PROVISIONED:
- postError(status, SERVICE_NOT_ACTIVATED);
- break;
- case VVM3_SUBSCRIBER_BLOCKED:
- postError(status, SUBSCRIBER_BLOCKED);
- break;
- case VVM3_SUBSCRIBER_UNKNOWN:
- postError(status, SUBSCRIBER_UNKNOWN);
- break;
- default:
- return false;
- }
- return true;
- }
-
- private static void postError(VoicemailStatus.Editor editor, @ErrorCode int errorCode) {
- switch (errorCode) {
- case VMG_DNS_FAILURE:
- case SPG_DNS_FAILURE:
- case VMG_NO_CELLULAR:
- case SPG_NO_CELLULAR:
- case VMG_TIMEOUT:
- case SUBSCRIBER_BLOCKED:
- case UNKNOWN_USER:
- case UNKNOWN_DEVICE:
- case INVALID_PASSWORD:
- case MAILBOX_NOT_INITIALIZED:
- case SERVICE_NOT_PROVISIONED:
- case SERVICE_NOT_ACTIVATED:
- case USER_BLOCKED:
- case VMG_UNKNOWN_ERROR:
- case SPG_URL_NOT_FOUND:
- case VMG_INTERNAL_ERROR:
- case VMG_DB_ERROR:
- case VMG_COMMUNICATION_ERROR:
- case PIN_NOT_SET:
- case SUBSCRIBER_UNKNOWN:
- editor.setConfigurationState(errorCode);
- break;
- case VMS_NO_CELLULAR:
- case VMS_DNS_FAILURE:
- case VMS_TIMEOUT:
- case IMAP_GETQUOTA_ERROR:
- case IMAP_SELECT_ERROR:
- case IMAP_ERROR:
- editor.setDataChannelState(errorCode);
- break;
- case STATUS_SMS_TIMEOUT:
- editor.setNotificationChannelState(errorCode);
- break;
- default:
- Log.wtf(TAG, "unknown error code: " + errorCode);
- }
- editor.apply();
- }
-}
diff --git a/java/com/android/voicemailomtp/protocol/Vvm3Protocol.java b/java/com/android/voicemailomtp/protocol/Vvm3Protocol.java
deleted file mode 100644
index 652d1010a..000000000
--- a/java/com/android/voicemailomtp/protocol/Vvm3Protocol.java
+++ /dev/null
@@ -1,301 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.protocol;
-
-import android.annotation.TargetApi;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.net.Network;
-import android.os.Build.VERSION_CODES;
-import android.os.Bundle;
-import android.support.annotation.Nullable;
-import android.telecom.PhoneAccountHandle;
-import android.text.TextUtils;
-import com.android.voicemailomtp.ActivationTask;
-import com.android.voicemailomtp.OmtpConstants;
-import com.android.voicemailomtp.OmtpEvents;
-import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
-import com.android.voicemailomtp.VisualVoicemailPreferences;
-import com.android.voicemailomtp.VoicemailStatus;
-import com.android.voicemailomtp.VvmLog;
-import com.android.voicemailomtp.imap.ImapHelper;
-import com.android.voicemailomtp.imap.ImapHelper.InitializingException;
-import com.android.voicemailomtp.mail.MessagingException;
-import com.android.voicemailomtp.settings.VisualVoicemailSettingsUtil;
-import com.android.voicemailomtp.settings.VoicemailChangePinActivity;
-import com.android.voicemailomtp.sms.OmtpMessageSender;
-import com.android.voicemailomtp.sms.StatusMessage;
-import com.android.voicemailomtp.sms.Vvm3MessageSender;
-import com.android.voicemailomtp.sync.VvmNetworkRequest;
-import com.android.voicemailomtp.sync.VvmNetworkRequest.NetworkWrapper;
-import com.android.voicemailomtp.sync.VvmNetworkRequest.RequestFailedException;
-import java.io.IOException;
-import java.security.SecureRandom;
-import java.util.Locale;
-
-/**
- * A flavor of OMTP protocol with a different provisioning process
- *
- * <p>Used by carriers such as Verizon Wireless
- */
-@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
-public class Vvm3Protocol extends VisualVoicemailProtocol {
-
- private static final String TAG = "Vvm3Protocol";
-
- private static final String SMS_EVENT_UNRECOGNIZED = "UNRECOGNIZED";
- private static final String SMS_EVENT_UNRECOGNIZED_CMD = "cmd";
- private static final String SMS_EVENT_UNRECOGNIZED_STATUS = "STATUS";
- private static final String DEFAULT_VMG_URL_KEY = "default_vmg_url";
-
- private static final String IMAP_CHANGE_TUI_PWD_FORMAT = "CHANGE_TUI_PWD PWD=%1$s OLD_PWD=%2$s";
- private static final String IMAP_CHANGE_VM_LANG_FORMAT = "CHANGE_VM_LANG Lang=%1$s";
- private static final String IMAP_CLOSE_NUT = "CLOSE_NUT";
-
- private static final String ISO639_Spanish = "es";
-
- /**
- * For VVM3, if the STATUS SMS returns {@link StatusMessage#getProvisioningStatus()} of {@link
- * OmtpConstants#SUBSCRIBER_UNKNOWN} and {@link StatusMessage#getReturnCode()} of this value,
- * the user can self-provision visual voicemail service. For other response codes, the user must
- * contact customer support to resolve the issue.
- */
- private static final String VVM3_UNKNOWN_SUBSCRIBER_CAN_SUBSCRIBE_RESPONSE_CODE = "2";
-
- // Default prompt level when using the telephone user interface.
- // Standard prompt when the user call into the voicemail, and no prompts when someone else is
- // leaving a voicemail.
- private static final String VVM3_VM_LANGUAGE_ENGLISH_STANDARD_NO_GUEST_PROMPTS = "5";
- private static final String VVM3_VM_LANGUAGE_SPANISH_STANDARD_NO_GUEST_PROMPTS = "6";
-
- private static final int DEFAULT_PIN_LENGTH = 6;
-
- @Override
- public void startActivation(OmtpVvmCarrierConfigHelper config,
- @Nullable PendingIntent sentIntent) {
- // VVM3 does not support activation SMS.
- // Send a status request which will start the provisioning process if the user is not
- // provisioned.
- VvmLog.i(TAG, "Activating");
- config.requestStatus(sentIntent);
- }
-
- @Override
- public void startDeactivation(OmtpVvmCarrierConfigHelper config) {
- // VVM3 does not support deactivation.
- // do nothing.
- }
-
- @Override
- public boolean supportsProvisioning() {
- return true;
- }
-
- @Override
- public void startProvisioning(ActivationTask task, PhoneAccountHandle phoneAccountHandle,
- OmtpVvmCarrierConfigHelper config, VoicemailStatus.Editor status, StatusMessage message,
- Bundle data) {
- VvmLog.i(TAG, "start vvm3 provisioning");
- if (OmtpConstants.SUBSCRIBER_UNKNOWN.equals(message.getProvisioningStatus())) {
- VvmLog.i(TAG, "Provisioning status: Unknown");
- if (VVM3_UNKNOWN_SUBSCRIBER_CAN_SUBSCRIBE_RESPONSE_CODE
- .equals(message.getReturnCode())) {
- VvmLog.i(TAG, "Self provisioning available, subscribing");
- new Vvm3Subscriber(task, phoneAccountHandle, config, status, data).subscribe();
- } else {
- config.handleEvent(status, OmtpEvents.VVM3_SUBSCRIBER_UNKNOWN);
- }
- } else if (OmtpConstants.SUBSCRIBER_NEW.equals(message.getProvisioningStatus())) {
- VvmLog.i(TAG, "setting up new user");
- // Save the IMAP credentials in preferences so they are persistent and can be retrieved.
- VisualVoicemailPreferences prefs =
- new VisualVoicemailPreferences(config.getContext(), phoneAccountHandle);
- message.putStatus(prefs.edit()).apply();
-
- startProvisionNewUser(task, phoneAccountHandle, config, status, message);
- } else if (OmtpConstants.SUBSCRIBER_PROVISIONED.equals(message.getProvisioningStatus())) {
- VvmLog.i(TAG, "User provisioned but not activated, disabling VVM");
- VisualVoicemailSettingsUtil
- .setEnabled(config.getContext(), phoneAccountHandle, false);
- } else if (OmtpConstants.SUBSCRIBER_BLOCKED.equals(message.getProvisioningStatus())) {
- VvmLog.i(TAG, "User blocked");
- config.handleEvent(status, OmtpEvents.VVM3_SUBSCRIBER_BLOCKED);
- }
- }
-
- @Override
- public OmtpMessageSender createMessageSender(Context context,
- PhoneAccountHandle phoneAccountHandle, short applicationPort,
- String destinationNumber) {
- return new Vvm3MessageSender(context, phoneAccountHandle, applicationPort,
- destinationNumber);
- }
-
- @Override
- public void handleEvent(Context context, OmtpVvmCarrierConfigHelper config,
- VoicemailStatus.Editor status, OmtpEvents event) {
- Vvm3EventHandler.handleEvent(context, config, status, event);
- }
-
- @Override
- public String getCommand(String command) {
- if (command == OmtpConstants.IMAP_CHANGE_TUI_PWD_FORMAT) {
- return IMAP_CHANGE_TUI_PWD_FORMAT;
- }
- if (command == OmtpConstants.IMAP_CLOSE_NUT) {
- return IMAP_CLOSE_NUT;
- }
- if (command == OmtpConstants.IMAP_CHANGE_VM_LANG_FORMAT) {
- return IMAP_CHANGE_VM_LANG_FORMAT;
- }
- return super.getCommand(command);
- }
-
- @Override
- public Bundle translateStatusSmsBundle(OmtpVvmCarrierConfigHelper config, String event,
- Bundle data) {
- // UNRECOGNIZED?cmd=STATUS is the response of a STATUS request when the user is provisioned
- // with iPhone visual voicemail without VoLTE. Translate it into an unprovisioned status
- // so provisioning can be done.
- if (!SMS_EVENT_UNRECOGNIZED.equals(event)) {
- return null;
- }
- if (!SMS_EVENT_UNRECOGNIZED_STATUS.equals(data.getString(SMS_EVENT_UNRECOGNIZED_CMD))) {
- return null;
- }
- Bundle bundle = new Bundle();
- bundle.putString(OmtpConstants.PROVISIONING_STATUS, OmtpConstants.SUBSCRIBER_UNKNOWN);
- bundle.putString(OmtpConstants.RETURN_CODE,
- VVM3_UNKNOWN_SUBSCRIBER_CAN_SUBSCRIBE_RESPONSE_CODE);
- String vmgUrl = config.getString(DEFAULT_VMG_URL_KEY);
- if (TextUtils.isEmpty(vmgUrl)) {
- VvmLog.e(TAG, "Unable to translate STATUS SMS: VMG URL is not set in config");
- return null;
- }
- bundle.putString(Vvm3Subscriber.VMG_URL_KEY, vmgUrl);
- VvmLog.i(TAG, "UNRECOGNIZED?cmd=STATUS translated into unprovisioned STATUS SMS");
- return bundle;
- }
-
- private void startProvisionNewUser(ActivationTask task, PhoneAccountHandle phoneAccountHandle,
- OmtpVvmCarrierConfigHelper config, VoicemailStatus.Editor status,
- StatusMessage message) {
- try (NetworkWrapper wrapper = VvmNetworkRequest
- .getNetwork(config, phoneAccountHandle, status)) {
- Network network = wrapper.get();
-
- VvmLog.i(TAG, "new user: network available");
- try (ImapHelper helper = new ImapHelper(config.getContext(), phoneAccountHandle,
- network, status)) {
- // VVM3 has inconsistent error language code to OMTP. Just issue a raw command
- // here.
- // TODO(b/29082671): use LocaleList
- if (Locale.getDefault().getLanguage()
- .equals(new Locale(ISO639_Spanish).getLanguage())) {
- // Spanish
- helper.changeVoicemailTuiLanguage(
- VVM3_VM_LANGUAGE_SPANISH_STANDARD_NO_GUEST_PROMPTS);
- } else {
- // English
- helper.changeVoicemailTuiLanguage(
- VVM3_VM_LANGUAGE_ENGLISH_STANDARD_NO_GUEST_PROMPTS);
- }
- VvmLog.i(TAG, "new user: language set");
-
- if (setPin(config.getContext(), phoneAccountHandle, helper, message)) {
- // Only close new user tutorial if the PIN has been changed.
- helper.closeNewUserTutorial();
- VvmLog.i(TAG, "new user: NUT closed");
-
- config.requestStatus(null);
- }
- } catch (InitializingException | MessagingException | IOException e) {
- config.handleEvent(status, OmtpEvents.VVM3_NEW_USER_SETUP_FAILED);
- task.fail();
- VvmLog.e(TAG, e.toString());
- }
- } catch (RequestFailedException e) {
- config.handleEvent(status, OmtpEvents.DATA_NO_CONNECTION_CELLULAR_REQUIRED);
- task.fail();
- }
-
- }
-
-
- private static boolean setPin(Context context, PhoneAccountHandle phoneAccountHandle,
- ImapHelper helper, StatusMessage message)
- throws IOException, MessagingException {
- String defaultPin = getDefaultPin(message);
- if (defaultPin == null) {
- VvmLog.i(TAG, "cannot generate default PIN");
- return false;
- }
-
- if (VoicemailChangePinActivity.isDefaultOldPinSet(context, phoneAccountHandle)) {
- // The pin was already set
- VvmLog.i(TAG, "PIN already set");
- return true;
- }
- String newPin = generatePin(getMinimumPinLength(context, phoneAccountHandle));
- if (helper.changePin(defaultPin, newPin) == OmtpConstants.CHANGE_PIN_SUCCESS) {
- VoicemailChangePinActivity.setDefaultOldPIN(context, phoneAccountHandle, newPin);
- helper.handleEvent(OmtpEvents.CONFIG_DEFAULT_PIN_REPLACED);
- }
- VvmLog.i(TAG, "new user: PIN set");
- return true;
- }
-
- @Nullable
- private static String getDefaultPin(StatusMessage message) {
- // The IMAP username is [phone number]@example.com
- String username = message.getImapUserName();
- try {
- String number = username.substring(0, username.indexOf('@'));
- if (number.length() < 4) {
- VvmLog.e(TAG, "unable to extract number from IMAP username");
- return null;
- }
- return "1" + number.substring(number.length() - 4);
- } catch (StringIndexOutOfBoundsException e) {
- VvmLog.e(TAG, "unable to extract number from IMAP username");
- return null;
- }
-
- }
-
- private static int getMinimumPinLength(Context context, PhoneAccountHandle phoneAccountHandle) {
- VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(context,
- phoneAccountHandle);
- // The OMTP pin length format is {min}-{max}
- String[] lengths = preferences.getString(OmtpConstants.TUI_PASSWORD_LENGTH, "").split("-");
- if (lengths.length == 2) {
- try {
- return Integer.parseInt(lengths[0]);
- } catch (NumberFormatException e) {
- return DEFAULT_PIN_LENGTH;
- }
- }
- return DEFAULT_PIN_LENGTH;
- }
-
- private static String generatePin(int length) {
- SecureRandom random = new SecureRandom();
- return String.format(Locale.US, "%010d", Math.abs(random.nextLong()))
- .substring(0, length);
-
- }
-}
diff --git a/java/com/android/voicemailomtp/protocol/Vvm3Subscriber.java b/java/com/android/voicemailomtp/protocol/Vvm3Subscriber.java
deleted file mode 100644
index 0a4d792b2..000000000
--- a/java/com/android/voicemailomtp/protocol/Vvm3Subscriber.java
+++ /dev/null
@@ -1,326 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.protocol;
-
-import android.annotation.TargetApi;
-import android.net.Network;
-import android.os.Build;
-import android.os.Build.VERSION_CODES;
-import android.os.Bundle;
-import android.support.annotation.WorkerThread;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.TelephonyManager;
-import android.text.Html;
-import android.text.Spanned;
-import android.text.style.URLSpan;
-import android.util.ArrayMap;
-import com.android.voicemailomtp.ActivationTask;
-import com.android.voicemailomtp.Assert;
-import com.android.voicemailomtp.OmtpEvents;
-import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
-import com.android.voicemailomtp.VoicemailStatus;
-import com.android.voicemailomtp.VvmLog;
-import com.android.voicemailomtp.sync.VvmNetworkRequest;
-import com.android.voicemailomtp.sync.VvmNetworkRequest.NetworkWrapper;
-import com.android.voicemailomtp.sync.VvmNetworkRequest.RequestFailedException;
-import com.android.volley.AuthFailureError;
-import com.android.volley.Request;
-import com.android.volley.RequestQueue;
-import com.android.volley.toolbox.HurlStack;
-import com.android.volley.toolbox.RequestFuture;
-import com.android.volley.toolbox.StringRequest;
-import com.android.volley.toolbox.Volley;
-import java.io.IOException;
-import java.net.CookieHandler;
-import java.net.CookieManager;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Random;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * Class to subscribe to basic VVM3 visual voicemail, for example, Verizon. Subscription is required
- * when the user is unprovisioned. This could happen when the user is on a legacy service, or
- * switched over from devices that used other type of visual voicemail.
- *
- * <p>The STATUS SMS will come with a URL to the voicemail management gateway. From it we can find
- * the self provisioning gateway URL that we can modify voicemail services.
- *
- * <p>A request to the self provisioning gateway to activate basic visual voicemail will return us
- * with a web page. If the user hasn't subscribe to it yet it will contain a link to confirm the
- * subscription. This link should be clicked through cellular network, and have cookies enabled.
- *
- * <p>After the process is completed, the carrier should send us another STATUS SMS with a new or
- * ready user.
- */
-@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
-public class Vvm3Subscriber {
-
- private static final String TAG = "Vvm3Subscriber";
-
- private static final String OPERATION_GET_SPG_URL = "retrieveSPGURL";
- private static final String SPG_URL_TAG = "spgurl";
- private static final String TRANSACTION_ID_TAG = "transactionid";
- //language=XML
- private static final String VMG_XML_REQUEST_FORMAT = ""
- + "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
- + "<VMGVVMRequest>"
- + " <MessageHeader>"
- + " <transactionid>%1$s</transactionid>"
- + " </MessageHeader>"
- + " <MessageBody>"
- + " <mdn>%2$s</mdn>"
- + " <operation>%3$s</operation>"
- + " <source>Device</source>"
- + " <devicemodel>%4$s</devicemodel>"
- + " </MessageBody>"
- + "</VMGVVMRequest>";
-
- static final String VMG_URL_KEY = "vmg_url";
-
- // Self provisioning POST key/values. VVM3 API 2.1.0 12.3
- private static final String SPG_VZW_MDN_PARAM = "VZW_MDN";
- private static final String SPG_VZW_SERVICE_PARAM = "VZW_SERVICE";
- private static final String SPG_VZW_SERVICE_BASIC = "BVVM";
- private static final String SPG_DEVICE_MODEL_PARAM = "DEVICE_MODEL";
- // Value for all android device
- private static final String SPG_DEVICE_MODEL_ANDROID = "DROID_4G";
- private static final String SPG_APP_TOKEN_PARAM = "APP_TOKEN";
- private static final String SPG_APP_TOKEN = "q8e3t5u2o1";
- private static final String SPG_LANGUAGE_PARAM = "SPG_LANGUAGE_PARAM";
- private static final String SPG_LANGUAGE_EN = "ENGLISH";
-
- private static final String BASIC_SUBSCRIBE_LINK_TEXT = "Subscribe to Basic Visual Voice Mail";
-
- private static final int REQUEST_TIMEOUT_SECONDS = 30;
-
- private final ActivationTask mTask;
- private final PhoneAccountHandle mHandle;
- private final OmtpVvmCarrierConfigHelper mHelper;
- private final VoicemailStatus.Editor mStatus;
- private final Bundle mData;
-
- private final String mNumber;
-
- private RequestQueue mRequestQueue;
-
- private static class ProvisioningException extends Exception {
-
- public ProvisioningException(String message) {
- super(message);
- }
- }
-
- static {
- // Set the default cookie handler to retain session data for the self provisioning gateway.
- // Note; this is not ideal as it is application-wide, and can easily get clobbered.
- // But it seems to be the preferred way to manage cookie for HttpURLConnection, and manually
- // managing cookies will greatly increase complexity.
- CookieManager cookieManager = new CookieManager();
- CookieHandler.setDefault(cookieManager);
- }
-
- @WorkerThread
- public Vvm3Subscriber(ActivationTask task, PhoneAccountHandle handle,
- OmtpVvmCarrierConfigHelper helper, VoicemailStatus.Editor status, Bundle data) {
- Assert.isNotMainThread();
- mTask = task;
- mHandle = handle;
- mHelper = helper;
- mStatus = status;
- mData = data;
-
- // Assuming getLine1Number() will work with VVM3. For unprovisioned users the IMAP username
- // is not included in the status SMS, thus no other way to get the current phone number.
- mNumber = mHelper.getContext().getSystemService(TelephonyManager.class)
- .createForPhoneAccountHandle(mHandle).getLine1Number();
- }
-
- @WorkerThread
- public void subscribe() {
- Assert.isNotMainThread();
- // Cellular data is required to subscribe.
- // processSubscription() is called after network is available.
- VvmLog.i(TAG, "Subscribing");
-
- try (NetworkWrapper wrapper = VvmNetworkRequest.getNetwork(mHelper, mHandle, mStatus)) {
- Network network = wrapper.get();
- VvmLog.d(TAG, "provisioning: network available");
- mRequestQueue = Volley
- .newRequestQueue(mHelper.getContext(), new NetworkSpecifiedHurlStack(network));
- processSubscription();
- } catch (RequestFailedException e) {
- mHelper.handleEvent(mStatus, OmtpEvents.VVM3_VMG_CONNECTION_FAILED);
- mTask.fail();
- }
- }
-
- private void processSubscription() {
- try {
- String gatewayUrl = getSelfProvisioningGateway();
- String selfProvisionResponse = getSelfProvisionResponse(gatewayUrl);
- String subscribeLink = findSubscribeLink(selfProvisionResponse);
- clickSubscribeLink(subscribeLink);
- } catch (ProvisioningException e) {
- VvmLog.e(TAG, e.toString());
- mTask.fail();
- }
- }
-
- /**
- * Get the URL to perform self-provisioning from the voicemail management gateway.
- */
- private String getSelfProvisioningGateway() throws ProvisioningException {
- VvmLog.i(TAG, "retrieving SPG URL");
- String response = vvm3XmlRequest(OPERATION_GET_SPG_URL);
- return extractText(response, SPG_URL_TAG);
- }
-
- /**
- * Sent a request to the self-provisioning gateway, which will return us with a webpage. The
- * page might contain a "Subscribe to Basic Visual Voice Mail" link to complete the
- * subscription. The cookie from this response and cellular data is required to click the link.
- */
- private String getSelfProvisionResponse(String url) throws ProvisioningException {
- VvmLog.i(TAG, "Retrieving self provisioning response");
-
- RequestFuture<String> future = RequestFuture.newFuture();
-
- StringRequest stringRequest = new StringRequest(Request.Method.POST, url, future, future) {
- @Override
- protected Map<String, String> getParams() {
- Map<String, String> params = new ArrayMap<>();
- params.put(SPG_VZW_MDN_PARAM, mNumber);
- params.put(SPG_VZW_SERVICE_PARAM, SPG_VZW_SERVICE_BASIC);
- params.put(SPG_DEVICE_MODEL_PARAM, SPG_DEVICE_MODEL_ANDROID);
- params.put(SPG_APP_TOKEN_PARAM, SPG_APP_TOKEN);
- // Language to display the subscription page. The page is never shown to the user
- // so just use English.
- params.put(SPG_LANGUAGE_PARAM, SPG_LANGUAGE_EN);
- return params;
- }
- };
-
- mRequestQueue.add(stringRequest);
- try {
- return future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
- } catch (InterruptedException | ExecutionException | TimeoutException e) {
- mHelper.handleEvent(mStatus, OmtpEvents.VVM3_SPG_CONNECTION_FAILED);
- throw new ProvisioningException(e.toString());
- }
- }
-
- private void clickSubscribeLink(String subscribeLink) throws ProvisioningException {
- VvmLog.i(TAG, "Clicking subscribe link");
- RequestFuture<String> future = RequestFuture.newFuture();
-
- StringRequest stringRequest = new StringRequest(Request.Method.POST,
- subscribeLink, future, future);
- mRequestQueue.add(stringRequest);
- try {
- // A new STATUS SMS will be sent after this request.
- future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
- } catch (TimeoutException | ExecutionException | InterruptedException e) {
- mHelper.handleEvent(mStatus, OmtpEvents.VVM3_SPG_CONNECTION_FAILED);
- throw new ProvisioningException(e.toString());
- }
- // It could take very long for the STATUS SMS to return. Waiting for it is unreliable.
- // Just leave the CONFIG STATUS as CONFIGURING and end the task. The user can always
- // manually retry if it took too long.
- }
-
- private String vvm3XmlRequest(String operation) throws ProvisioningException {
- VvmLog.d(TAG, "Sending vvm3XmlRequest for " + operation);
- String voicemailManagementGateway = mData.getString(VMG_URL_KEY);
- if (voicemailManagementGateway == null) {
- VvmLog.e(TAG, "voicemailManagementGateway url unknown");
- return null;
- }
- String transactionId = createTransactionId();
- String body = String.format(Locale.US, VMG_XML_REQUEST_FORMAT,
- transactionId, mNumber, operation, Build.MODEL);
-
- RequestFuture<String> future = RequestFuture.newFuture();
- StringRequest stringRequest = new StringRequest(Request.Method.POST,
- voicemailManagementGateway, future, future) {
- @Override
- public byte[] getBody() throws AuthFailureError {
- return body.getBytes();
- }
- };
- mRequestQueue.add(stringRequest);
-
- try {
- String response = future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
- if (!transactionId.equals(extractText(response, TRANSACTION_ID_TAG))) {
- throw new ProvisioningException("transactionId mismatch");
- }
- return response;
- } catch (InterruptedException | ExecutionException | TimeoutException e) {
- mHelper.handleEvent(mStatus, OmtpEvents.VVM3_VMG_CONNECTION_FAILED);
- throw new ProvisioningException(e.toString());
- }
- }
-
- private String findSubscribeLink(String response) throws ProvisioningException {
- Spanned doc = Html.fromHtml(response, Html.FROM_HTML_MODE_LEGACY);
- URLSpan[] spans = doc.getSpans(0, doc.length(), URLSpan.class);
- StringBuilder fulltext = new StringBuilder();
- for (URLSpan span : spans) {
- String text = doc.subSequence(doc.getSpanStart(span), doc.getSpanEnd(span)).toString();
- if (BASIC_SUBSCRIBE_LINK_TEXT.equals(text)) {
- return span.getURL();
- }
- fulltext.append(text);
- }
- throw new ProvisioningException("Subscribe link not found: " + fulltext);
- }
-
- private String createTransactionId() {
- return String.valueOf(Math.abs(new Random().nextLong()));
- }
-
- private String extractText(String xml, String tag) throws ProvisioningException {
- Pattern pattern = Pattern.compile("<" + tag + ">(.*)<\\/" + tag + ">");
- Matcher matcher = pattern.matcher(xml);
- if (matcher.find()) {
- return matcher.group(1);
- }
- throw new ProvisioningException("Tag " + tag + " not found in xml response");
- }
-
- private static class NetworkSpecifiedHurlStack extends HurlStack {
-
- private final Network mNetwork;
-
- public NetworkSpecifiedHurlStack(Network network) {
- mNetwork = network;
- }
-
- @Override
- protected HttpURLConnection createConnection(URL url) throws IOException {
- return (HttpURLConnection) mNetwork.openConnection(url);
- }
-
- }
-}
diff --git a/java/com/android/voicemailomtp/res/values/strings.xml b/java/com/android/voicemailomtp/res/values/strings.xml
deleted file mode 100644
index 7a1407371..000000000
--- a/java/com/android/voicemailomtp/res/values/strings.xml
+++ /dev/null
@@ -1,86 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2007 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-
-<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-
- <!-- Title of the "Voicemail" settings screen, with a text label identifying which SIM the settings are for. -->
- <string translatable="false" name="voicemail_settings_with_label">Voicemail (<xliff:g id="subscriptionlabel" example="Mock Carrier">%s</xliff:g>)</string>
-
- <!-- Call settings screen, setting option name -->
- <string translatable="false" name="voicemail_settings_label">Voicemail</string>
-
- <!-- DO NOT TRANSLATE. Internal key for a visual voicemail preference. -->
- <string translatable="false" name="voicemail_visual_voicemail_key">
- voicemail_visual_voicemail_key
- </string>
- <!-- DO NOT TRANSLATE. Internal key for a voicemail change pin preference. -->
- <string translatable="false" name="voicemail_change_pin_key">voicemail_change_pin_key</string>
-
- <!-- Visual voicemail on/off title [CHAR LIMIT=40] -->
- <string translatable="false" name="voicemail_visual_voicemail_switch_title">Visual Voicemail</string>
-
- <!-- Voicemail change PIN dialog title [CHAR LIMIT=40] -->
- <string translatable="false" name="voicemail_set_pin_dialog_title">Set PIN</string>
- <!-- Voicemail change PIN dialog title [CHAR LIMIT=40] -->
- <string translatable="false" name="voicemail_change_pin_dialog_title">Change PIN</string>
-
- <!-- Hint for the old PIN field in the change vociemail PIN dialog -->
- <string translatable="false" name="vm_change_pin_old_pin">Old PIN</string>
- <!-- Hint for the new PIN field in the change vociemail PIN dialog -->
- <string translatable="false" name="vm_change_pin_new_pin">New PIN</string>
-
- <!-- Message on the dialog when PIN changing is in progress -->
- <string translatable="false" name="vm_change_pin_progress_message">Please wait.</string>
- <!-- Error message for the voicemail PIN change if the PIN is too short -->
- <string translatable="false" name="vm_change_pin_error_too_short">The new PIN is too short.</string>
- <!-- Error message for the voicemail PIN change if the PIN is too long -->
- <string translatable="false" name="vm_change_pin_error_too_long">The new PIN is too long.</string>
- <!-- Error message for the voicemail PIN change if the PIN is too weak -->
- <string translatable="false" name="vm_change_pin_error_too_weak">The new PIN is too weak. A strong password should not have continuous sequence or repeated digits.</string>
- <!-- Error message for the voicemail PIN change if the old PIN entered doesn't match -->
- <string translatable="false" name="vm_change_pin_error_mismatch">The old PIN does not match.</string>
- <!-- Error message for the voicemail PIN change if the new PIN contains invalid character -->
- <string translatable="false" name="vm_change_pin_error_invalid">The new PIN contains invalid characters.</string>
- <!-- Error message for the voicemail PIN change if operation has failed -->
- <string translatable="false" name="vm_change_pin_error_system_error">Unable to change PIN</string>
- <!-- Message to replace the transcription if a visual voicemail message is not supported-->
- <string translatable="false" name="vvm_unsupported_message_format">Unsupported message type, call <xliff:g id="number" example="*86">%s</xliff:g> to listen.</string>
-
- <!-- The title for the change voicemail PIN activity -->
- <string translatable="false" name="change_pin_title">Change Voicemail PIN</string>
- <!-- The label for the continue button in change voicemail PIN activity -->
- <string translatable="false" name="change_pin_continue_label">Continue</string>
- <!-- The label for the cancel button in change voicemail PIN activity -->
- <string translatable="false" name="change_pin_cancel_label">Cancel</string>
- <!-- The label for the ok button in change voicemail PIN activity -->
- <string translatable="false" name="change_pin_ok_label">Ok</string>
- <!-- The title for the enter old pin step in change voicemail PIN activity -->
- <string translatable="false" name="change_pin_enter_old_pin_header">Confirm your old PIN</string>
- <!-- The hint for the enter old pin step in change voicemail PIN activity -->
- <string translatable="false" name="change_pin_enter_old_pin_hint">Enter your voicemail PIN to continue.</string>
- <!-- The title for the enter new pin step in change voicemail PIN activity -->
- <string translatable="false" name="change_pin_enter_new_pin_header">Set a new PIN</string>
- <!-- The hint for the enter new pin step in change voicemail PIN activity -->
- <string translatable="false" name="change_pin_enter_new_pin_hint">PIN must be <xliff:g id="min" example="4">%1$d</xliff:g>-<xliff:g id="max" example="7">%2$d</xliff:g> digits.</string>
- <!-- The title for the confirm new pin step in change voicemail PIN activity -->
- <string translatable="false" name="change_pin_confirm_pin_header">Confirm your PIN</string>
- <!-- The error message for th confirm new pin step in change voicemail PIN activity, if the pin doen't match the one previously entered -->
- <string translatable="false" name="change_pin_confirm_pins_dont_match">PINs don\'t match</string>
- <!-- The toast to show after the voicemail PIN has been successfully changed -->
- <string translatable="false" name="change_pin_succeeded">Voicemail PIN updated</string>
- <!-- The error message to show if the server reported an error while attempting to change the voicemail PIN -->
- <string translatable="false" name="change_pin_system_error">Unable to set PIN</string>
-</resources>
diff --git a/java/com/android/voicemailomtp/scheduling/BaseTask.java b/java/com/android/voicemailomtp/scheduling/BaseTask.java
deleted file mode 100644
index 8097bb4dc..000000000
--- a/java/com/android/voicemailomtp/scheduling/BaseTask.java
+++ /dev/null
@@ -1,206 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.scheduling;
-
-import android.content.Context;
-import android.content.Intent;
-import android.os.SystemClock;
-import android.support.annotation.CallSuper;
-import android.support.annotation.MainThread;
-import android.support.annotation.NonNull;
-import android.support.annotation.WorkerThread;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.SubscriptionManager;
-import com.android.voicemailomtp.Assert;
-import com.android.voicemailomtp.NeededForTesting;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Provides common utilities for task implementations, such as execution time and managing {@link
- * Policy}
- */
-public abstract class BaseTask implements Task {
-
- private static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle";
-
- private Context mContext;
-
- private int mId;
- private PhoneAccountHandle mPhoneAccountHandle;
-
- private boolean mHasStarted;
- private volatile boolean mHasFailed;
-
- @NonNull
- private final List<Policy> mPolicies = new ArrayList<>();
-
- private long mExecutionTime;
-
- private static Clock sClock = new Clock();
-
- protected BaseTask(int id) {
- mId = id;
- mExecutionTime = getTimeMillis();
- }
-
- /**
- * Modify the task ID to prevent arbitrary task from executing. Can only be called before {@link
- * #onCreate(Context, Intent, int, int)} returns.
- */
- @MainThread
- public void setId(int id) {
- Assert.isMainThread();
- mId = id;
- }
-
- @MainThread
- public boolean hasStarted() {
- Assert.isMainThread();
- return mHasStarted;
- }
-
- @MainThread
- public boolean hasFailed() {
- Assert.isMainThread();
- return mHasFailed;
- }
-
- public Context getContext() {
- return mContext;
- }
-
- public PhoneAccountHandle getPhoneAccountHandle() {
- return mPhoneAccountHandle;
- }
- /**
- * Should be call in the constructor or {@link Policy#onCreate(BaseTask, Intent, int, int)} will
- * be missed.
- */
- @MainThread
- public BaseTask addPolicy(Policy policy) {
- Assert.isMainThread();
- mPolicies.add(policy);
- return this;
- }
-
- /**
- * Indicate the task has failed. {@link Policy#onFail()} will be triggered once the execution
- * ends. This mechanism is used by policies for actions such as determining whether to schedule
- * a retry. Must be call inside {@link #onExecuteInBackgroundThread()}
- */
- @WorkerThread
- public void fail() {
- Assert.isNotMainThread();
- mHasFailed = true;
- }
-
- @MainThread
- public void setExecutionTime(long timeMillis) {
- Assert.isMainThread();
- mExecutionTime = timeMillis;
- }
-
- public long getTimeMillis() {
- return sClock.getTimeMillis();
- }
-
- /**
- * Creates an intent that can be used to restart the current task. Derived class should build
- * their intent upon this.
- */
- public Intent createRestartIntent() {
- return createIntent(getContext(), this.getClass(), mPhoneAccountHandle);
- }
-
- /**
- * Creates an intent that can be used to start the {@link TaskSchedulerService}. Derived class
- * should build their intent upon this.
- */
- public static Intent createIntent(Context context, Class<? extends BaseTask> task,
- PhoneAccountHandle phoneAccountHandle) {
- Intent intent = TaskSchedulerService.createIntent(context, task);
- intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle);
- return intent;
- }
-
- @Override
- public TaskId getId() {
- return new TaskId(mId, mPhoneAccountHandle);
- }
-
- @Override
- @CallSuper
- public void onCreate(Context context, Intent intent, int flags, int startId) {
- mContext = context;
- mPhoneAccountHandle = intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE);
- for (Policy policy : mPolicies) {
- policy.onCreate(this, intent, flags, startId);
- }
- }
-
- @Override
- public long getReadyInMilliSeconds() {
- return mExecutionTime - getTimeMillis();
- }
-
- @Override
- @CallSuper
- public void onBeforeExecute() {
- for (Policy policy : mPolicies) {
- policy.onBeforeExecute();
- }
- mHasStarted = true;
- }
-
- @Override
- @CallSuper
- public void onCompleted() {
- if (mHasFailed) {
- for (Policy policy : mPolicies) {
- policy.onFail();
- }
- }
-
- for (Policy policy : mPolicies) {
- policy.onCompleted();
- }
- }
-
- @Override
- public void onDuplicatedTaskAdded(Task task) {
- for (Policy policy : mPolicies) {
- policy.onDuplicatedTaskAdded();
- }
- }
-
- @NeededForTesting
- static class Clock {
-
- public long getTimeMillis() {
- return SystemClock.elapsedRealtime();
- }
- }
-
- /**
- * Used to replace the clock with an deterministic clock
- */
- @NeededForTesting
- static void setClockForTesting(Clock clock) {
- sClock = clock;
- }
-}
diff --git a/java/com/android/voicemailomtp/scheduling/BlockerTask.java b/java/com/android/voicemailomtp/scheduling/BlockerTask.java
deleted file mode 100644
index 55ad9a7fd..000000000
--- a/java/com/android/voicemailomtp/scheduling/BlockerTask.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.scheduling;
-
-import android.content.Context;
-import android.content.Intent;
-
-import com.android.voicemailomtp.VvmLog;
-
-/**
- * Task to block another task of the same ID from being queued for a certain amount of time.
- */
-public class BlockerTask extends BaseTask {
-
- private static final String TAG = "BlockerTask";
-
- public static final String EXTRA_TASK_ID = "extra_task_id";
- public static final String EXTRA_BLOCK_FOR_MILLIS = "extra_block_for_millis";
-
- public BlockerTask() {
- super(TASK_INVALID);
- }
-
- @Override
- public void onCreate(Context context, Intent intent, int flags, int startId) {
- super.onCreate(context, intent, flags, startId);
- setId(intent.getIntExtra(EXTRA_TASK_ID, TASK_INVALID));
- setExecutionTime(getTimeMillis() + intent.getIntExtra(EXTRA_BLOCK_FOR_MILLIS, 0));
- }
-
- @Override
- public void onExecuteInBackgroundThread() {
- // Do nothing.
- }
-
- @Override
- public void onDuplicatedTaskAdded(Task task) {
- VvmLog
- .v(TAG, task.toString() + "blocked, " + getReadyInMilliSeconds() + "millis remaining");
- }
-}
diff --git a/java/com/android/voicemailomtp/scheduling/MinimalIntervalPolicy.java b/java/com/android/voicemailomtp/scheduling/MinimalIntervalPolicy.java
deleted file mode 100644
index bef449b30..000000000
--- a/java/com/android/voicemailomtp/scheduling/MinimalIntervalPolicy.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.scheduling;
-
-import android.content.Intent;
-
-import com.android.voicemailomtp.scheduling.Task.TaskId;
-
-/**
- * If a task with this policy succeeds, a {@link BlockerTask} with the same {@link TaskId} of the
- * task will be queued immediately, preventing the same task from running for a certain amount of
- * time.
- */
-public class MinimalIntervalPolicy implements Policy {
-
- BaseTask mTask;
- TaskId mId;
- int mBlockForMillis;
-
- public MinimalIntervalPolicy(int blockForMillis) {
- mBlockForMillis = blockForMillis;
- }
-
- @Override
- public void onCreate(BaseTask task, Intent intent, int flags, int startId) {
- mTask = task;
- mId = mTask.getId();
- }
-
- @Override
- public void onBeforeExecute() {
-
- }
-
- @Override
- public void onCompleted() {
- if (!mTask.hasFailed()) {
- Intent intent = mTask
- .createIntent(mTask.getContext(), BlockerTask.class, mId.phoneAccountHandle);
- intent.putExtra(BlockerTask.EXTRA_TASK_ID, mId.id);
- intent.putExtra(BlockerTask.EXTRA_BLOCK_FOR_MILLIS, mBlockForMillis);
- mTask.getContext().startService(intent);
- }
- }
-
- @Override
- public void onFail() {
-
- }
-
- @Override
- public void onDuplicatedTaskAdded() {
-
- }
-}
diff --git a/java/com/android/voicemailomtp/scheduling/RetryPolicy.java b/java/com/android/voicemailomtp/scheduling/RetryPolicy.java
deleted file mode 100644
index 463657483..000000000
--- a/java/com/android/voicemailomtp/scheduling/RetryPolicy.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.scheduling;
-
-import android.content.Intent;
-import android.telecom.PhoneAccountHandle;
-
-import com.android.voicemailomtp.VoicemailStatus;
-import com.android.voicemailomtp.VvmLog;
-
-/**
- * A task with this policy will automatically re-queue itself if {@link BaseTask#fail()} has been
- * called during {@link BaseTask#onExecuteInBackgroundThread()}. A task will be retried at most
- * <code>retryLimit</code> times and with a <code>retryDelayMillis</code> interval in between.
- */
-public class RetryPolicy implements Policy {
-
- private static final String TAG = "RetryPolicy";
- private static final String EXTRA_RETRY_COUNT = "extra_retry_count";
-
- private final int mRetryLimit;
- private final int mRetryDelayMillis;
-
- private BaseTask mTask;
-
- private int mRetryCount;
- private boolean mFailed;
-
- private VoicemailStatus.DeferredEditor mVoicemailStatusEditor;
-
- public RetryPolicy(int retryLimit, int retryDelayMillis) {
- mRetryLimit = retryLimit;
- mRetryDelayMillis = retryDelayMillis;
- }
-
- private boolean hasMoreRetries() {
- return mRetryCount < mRetryLimit;
- }
-
- /**
- * Error status should only be set if retries has exhausted or the task is successful. Status
- * writes to this editor will be deferred until the task has ended, and will only be committed
- * if the task is successful or there are no retries left.
- */
- public VoicemailStatus.Editor getVoicemailStatusEditor() {
- return mVoicemailStatusEditor;
- }
-
- @Override
- public void onCreate(BaseTask task, Intent intent, int flags, int startId) {
- mTask = task;
- mRetryCount = intent.getIntExtra(EXTRA_RETRY_COUNT, 0);
- if (mRetryCount > 0) {
- VvmLog.d(TAG, "retry #" + mRetryCount + " for " + mTask + " queued, executing in "
- + mRetryDelayMillis);
- mTask.setExecutionTime(mTask.getTimeMillis() + mRetryDelayMillis);
- }
- PhoneAccountHandle phoneAccountHandle = task.getPhoneAccountHandle();
- if (phoneAccountHandle == null) {
- VvmLog.e(TAG,
- "null phone account for phoneAccountHandle " + task.getPhoneAccountHandle());
- // This should never happen, but continue on if it does. The status write will be
- // discarded.
- }
- mVoicemailStatusEditor = VoicemailStatus
- .deferredEdit(task.getContext(), phoneAccountHandle);
- }
-
- @Override
- public void onBeforeExecute() {
-
- }
-
- @Override
- public void onCompleted() {
- if (!mFailed || !hasMoreRetries()) {
- if (!mFailed) {
- VvmLog.d(TAG, mTask.toString() + " completed successfully");
- }
- if (!hasMoreRetries()) {
- VvmLog.d(TAG, "Retry limit for " + mTask + " reached");
- }
- VvmLog.i(TAG, "committing deferred status: " + mVoicemailStatusEditor.getValues());
- mVoicemailStatusEditor.deferredApply();
- return;
- }
- VvmLog.i(TAG, "discarding deferred status: " + mVoicemailStatusEditor.getValues());
- Intent intent = mTask.createRestartIntent();
- intent.putExtra(EXTRA_RETRY_COUNT, mRetryCount + 1);
-
- mTask.getContext().startService(intent);
- }
-
- @Override
- public void onFail() {
- mFailed = true;
- }
-
- @Override
- public void onDuplicatedTaskAdded() {
-
- }
-}
diff --git a/java/com/android/voicemailomtp/scheduling/Task.java b/java/com/android/voicemailomtp/scheduling/Task.java
deleted file mode 100644
index 61c35396b..000000000
--- a/java/com/android/voicemailomtp/scheduling/Task.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.scheduling;
-
-import android.content.Context;
-import android.content.Intent;
-import android.support.annotation.MainThread;
-import android.support.annotation.WorkerThread;
-import android.telecom.PhoneAccountHandle;
-
-import java.util.Objects;
-
-/**
- * A task for {@link TaskSchedulerService} to execute. Since the task is sent through a intent to
- * the scheduler, The task must be constructable with the intent. Specifically, It must have a
- * constructor with zero arguments, and have all relevant data packed inside the intent. Use {@link
- * TaskSchedulerService#createIntent(Context, Class)} to create a intent that will construct the
- * Task.
- *
- * <p>Only {@link #onExecuteInBackgroundThread()} is run on the worker thread.
- */
-public interface Task {
-
- /**
- * TaskId to indicate it has not be set. If a task does not provide a default TaskId it should
- * be set before {@link Task#onCreate(Context, Intent, int, int) returns}
- */
- int TASK_INVALID = -1;
-
- /**
- * TaskId to indicate it should always be queued regardless of duplicates. {@link
- * Task#onDuplicatedTaskAdded(Task)} will never be called on tasks with this TaskId.
- */
- int TASK_ALLOW_DUPLICATES = -2;
-
- int TASK_UPLOAD = 1;
- int TASK_SYNC = 2;
- int TASK_ACTIVATION = 3;
-
- /**
- * Used to differentiate between types of tasks. If a task with the same TaskId is already in
- * the queue the new task will be rejected.
- */
- class TaskId {
-
- /**
- * Indicates the operation type of the task.
- */
- public final int id;
- /**
- * Same operation for a different phoneAccountHandle is allowed. phoneAccountHandle is used
- * to differentiate phone accounts in multi-SIM scenario. For example, each SIM can queue a
- * sync task for their own.
- */
- public final PhoneAccountHandle phoneAccountHandle;
-
- public TaskId(int id, PhoneAccountHandle phoneAccountHandle) {
- this.id = id;
- this.phoneAccountHandle = phoneAccountHandle;
- }
-
- @Override
- public boolean equals(Object object) {
- if (!(object instanceof TaskId)) {
- return false;
- }
- TaskId other = (TaskId) object;
- return id == other.id && phoneAccountHandle.equals(other.phoneAccountHandle);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(id, phoneAccountHandle);
- }
- }
-
- TaskId getId();
-
- @MainThread
- void onCreate(Context context, Intent intent, int flags, int startId);
-
- /**
- * @return number of milliSeconds the scheduler should wait before running this task. A value
- * less than {@link TaskSchedulerService#READY_TOLERANCE_MILLISECONDS} will be considered ready.
- * If no tasks are ready, the scheduler will sleep for this amount of time before doing another
- * check (it will still wake if a new task is added). The first task in the queue that is ready
- * will be executed.
- */
- @MainThread
- long getReadyInMilliSeconds();
-
- /**
- * Called on the main thread when the scheduler is about to send the task into the worker
- * thread, calling {@link #onExecuteInBackgroundThread()}
- */
- @MainThread
- void onBeforeExecute();
-
- /**
- * The actual payload of the task, executed on the worker thread.
- */
- @WorkerThread
- void onExecuteInBackgroundThread();
-
- /**
- * Called on the main thread when {@link #onExecuteInBackgroundThread()} has finished or thrown
- * an uncaught exception. The task is already removed from the queue at this point, and a same
- * task can be queued again.
- */
- @MainThread
- void onCompleted();
-
- /**
- * Another task with the same TaskId has been added. Necessary data can be retrieved from the
- * other task, and after this returns the task will be discarded.
- */
- @MainThread
- void onDuplicatedTaskAdded(Task task);
-}
diff --git a/java/com/android/voicemailomtp/scheduling/TaskSchedulerService.java b/java/com/android/voicemailomtp/scheduling/TaskSchedulerService.java
deleted file mode 100644
index 90b50e913..000000000
--- a/java/com/android/voicemailomtp/scheduling/TaskSchedulerService.java
+++ /dev/null
@@ -1,392 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.scheduling;
-
-import android.app.AlarmManager;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Binder;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.IBinder;
-import android.os.Looper;
-import android.os.Message;
-import android.os.PowerManager;
-import android.os.PowerManager.WakeLock;
-import android.os.SystemClock;
-import android.support.annotation.MainThread;
-import android.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
-import android.support.annotation.WorkerThread;
-import com.android.voicemailomtp.Assert;
-import com.android.voicemailomtp.NeededForTesting;
-import com.android.voicemailomtp.VvmLog;
-import com.android.voicemailomtp.scheduling.Task.TaskId;
-import java.util.ArrayDeque;
-import java.util.Queue;
-
-/**
- * A service to queue and run {@link Task} on a worker thread. Only one task will be ran at a time,
- * and same task cannot exist in the queue at the same time. The service will be started when a
- * intent is received, and stopped when there are no more tasks in the queue.
- */
-public class TaskSchedulerService extends Service {
-
- private static final String TAG = "VvmTaskScheduler";
-
- private static final String ACTION_WAKEUP = "action_wakeup";
-
- private static final int READY_TOLERANCE_MILLISECONDS = 100;
-
- /**
- * Threshold to determine whether to do a short or long sleep when a task is scheduled in the
- * future.
- *
- * <p>A short sleep will continue to held the wake lock and use {@link
- * Handler#postDelayed(Runnable, long)} to wait for the next task.
- *
- * <p>A long sleep will release the wake lock and set a {@link AlarmManager} alarm. The alarm is
- * exact and will wake up the device. Note: as this service is run in the telephony process it
- * does not seem to be restricted by doze or sleep, it will fire exactly at the moment. The
- * unbundled version should take doze into account.
- */
- private static final int SHORT_SLEEP_THRESHOLD_MILLISECONDS = 60_000;
- /**
- * When there are no more tasks to be run the service should be stopped. But when all tasks has
- * finished there might still be more tasks in the message queue waiting to be processed,
- * especially the ones submitted in {@link Task#onCompleted()}. Wait for a while before stopping
- * the service to make sure there are no pending messages.
- */
- private static final int STOP_DELAY_MILLISECONDS = 5_000;
- private static final String EXTRA_CLASS_NAME = "extra_class_name";
-
- private static final String WAKE_LOCK_TAG = "TaskSchedulerService_wakelock";
-
- // The thread to run tasks on
- private volatile WorkerThreadHandler mWorkerThreadHandler;
-
- private Context mContext = this;
- /**
- * Used by tests to turn task handling into a single threaded process by calling {@link
- * Handler#handleMessage(Message)} directly
- */
- private MessageSender mMessageSender = new MessageSender();
-
- private MainThreadHandler mMainThreadHandler;
-
- private WakeLock mWakeLock;
-
- /**
- * Main thread only, access through {@link #getTasks()}
- */
- private final Queue<Task> mTasks = new ArrayDeque<>();
- private boolean mWorkerThreadIsBusy = false;
-
- private final Runnable mStopServiceWithDelay = new Runnable() {
- @Override
- public void run() {
- VvmLog.d(TAG, "Stopping service");
- stopSelf();
- }
- };
- /**
- * Should attempt to run the next task when a task has finished or been added.
- */
- private boolean mTaskAutoRunDisabledForTesting = false;
-
- @VisibleForTesting
- final class WorkerThreadHandler extends Handler {
-
- public WorkerThreadHandler(Looper looper) {
- super(looper);
- }
-
- @Override
- @WorkerThread
- public void handleMessage(Message msg) {
- Assert.isNotMainThread();
- Task task = (Task) msg.obj;
- try {
- VvmLog.v(TAG, "executing task " + task);
- task.onExecuteInBackgroundThread();
- } catch (Throwable throwable) {
- VvmLog.e(TAG, "Exception while executing task " + task + ":", throwable);
- }
-
- Message schedulerMessage = mMainThreadHandler.obtainMessage();
- schedulerMessage.obj = task;
- mMessageSender.send(schedulerMessage);
- }
- }
-
- @VisibleForTesting
- final class MainThreadHandler extends Handler {
-
- public MainThreadHandler(Looper looper) {
- super(looper);
- }
-
- @Override
- @MainThread
- public void handleMessage(Message msg) {
- Assert.isMainThread();
- Task task = (Task) msg.obj;
- getTasks().remove(task);
- task.onCompleted();
- mWorkerThreadIsBusy = false;
- maybeRunNextTask();
- }
- }
-
- @Override
- @MainThread
- public void onCreate() {
- super.onCreate();
- mWakeLock = getSystemService(PowerManager.class)
- .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG);
- mWakeLock.setReferenceCounted(false);
- HandlerThread thread = new HandlerThread("VvmTaskSchedulerService");
- thread.start();
-
- mWorkerThreadHandler = new WorkerThreadHandler(thread.getLooper());
- mMainThreadHandler = new MainThreadHandler(Looper.getMainLooper());
- }
-
- @Override
- public void onDestroy() {
- mWorkerThreadHandler.getLooper().quit();
- mWakeLock.release();
- }
-
- @Override
- @MainThread
- public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
- Assert.isMainThread();
- // maybeRunNextTask() will release the wakelock either by entering a long sleep or stopping
- // the service.
- mWakeLock.acquire();
- if (ACTION_WAKEUP.equals(intent.getAction())) {
- VvmLog.d(TAG, "woke up by AlarmManager");
- } else {
- Task task = createTask(intent, flags, startId);
- if (task == null) {
- VvmLog.e(TAG, "cannot create task form intent");
- } else {
- addTask(task);
- }
- }
- maybeRunNextTask();
- // STICKY means the service will be automatically restarted will the last intent if it is
- // killed.
- return START_NOT_STICKY;
- }
-
- @MainThread
- @VisibleForTesting
- void addTask(Task task) {
- Assert.isMainThread();
- if (task.getId().id == Task.TASK_INVALID) {
- throw new AssertionError("Task id was not set to a valid value before adding.");
- }
- if (task.getId().id != Task.TASK_ALLOW_DUPLICATES) {
- Task oldTask = getTask(task.getId());
- if (oldTask != null) {
- oldTask.onDuplicatedTaskAdded(task);
- return;
- }
- }
- mMainThreadHandler.removeCallbacks(mStopServiceWithDelay);
- getTasks().add(task);
- maybeRunNextTask();
- }
-
- @MainThread
- @Nullable
- private Task getTask(TaskId taskId) {
- Assert.isMainThread();
- for (Task task : getTasks()) {
- if (task.getId().equals(taskId)) {
- return task;
- }
- }
- return null;
- }
-
- @MainThread
- private Queue<Task> getTasks() {
- Assert.isMainThread();
- return mTasks;
- }
-
- /**
- * Create an intent that will queue the <code>task</code>
- */
- public static Intent createIntent(Context context, Class<? extends Task> task) {
- Intent intent = new Intent(context, TaskSchedulerService.class);
- intent.putExtra(EXTRA_CLASS_NAME, task.getName());
- return intent;
- }
-
- @VisibleForTesting
- @MainThread
- @Nullable
- Task createTask(@Nullable Intent intent, int flags, int startId) {
- Assert.isMainThread();
- if (intent == null) {
- return null;
- }
- String className = intent.getStringExtra(EXTRA_CLASS_NAME);
- VvmLog.d(TAG, "create task:" + className);
- if (className == null) {
- throw new IllegalArgumentException("EXTRA_CLASS_NAME expected");
- }
- try {
- Task task = (Task) Class.forName(className).newInstance();
- task.onCreate(mContext, intent, flags, startId);
- return task;
- } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
- throw new IllegalArgumentException(e);
- }
- }
-
- @MainThread
- private void maybeRunNextTask() {
- Assert.isMainThread();
- if (mWorkerThreadIsBusy) {
- return;
- }
- if (mTaskAutoRunDisabledForTesting) {
- // If mTaskAutoRunDisabledForTesting is true, runNextTask() must be explicitly called
- // to run the next task.
- return;
- }
-
- runNextTask();
- }
-
- @VisibleForTesting
- @MainThread
- void runNextTask() {
- Assert.isMainThread();
- // The current alarm is no longer valid, a new one will be set up if required.
- getSystemService(AlarmManager.class).cancel(getWakeupIntent());
- if (getTasks().isEmpty()) {
- prepareStop();
- return;
- }
- Long minimalWaitTime = null;
- for (Task task : getTasks()) {
- long waitTime = task.getReadyInMilliSeconds();
- if (waitTime < READY_TOLERANCE_MILLISECONDS) {
- task.onBeforeExecute();
- Message message = mWorkerThreadHandler.obtainMessage();
- message.obj = task;
- mWorkerThreadIsBusy = true;
- mMessageSender.send(message);
- return;
- } else {
- if (minimalWaitTime == null || waitTime < minimalWaitTime) {
- minimalWaitTime = waitTime;
- }
- }
- }
- VvmLog.d(TAG, "minimal wait time:" + minimalWaitTime);
- if (!mTaskAutoRunDisabledForTesting && minimalWaitTime != null) {
- // No tasks are currently ready. Sleep until the next one should be.
- // If a new task is added during the sleep the service will wake immediately.
- sleep(minimalWaitTime);
- }
- }
-
- private void sleep(long timeMillis) {
- if (timeMillis < SHORT_SLEEP_THRESHOLD_MILLISECONDS) {
- mMainThreadHandler.postDelayed(new Runnable() {
- @Override
- public void run() {
- maybeRunNextTask();
- }
- }, timeMillis);
- return;
- }
-
- // Tasks does not have a strict timing requirement, use AlarmManager.set() so the OS could
- // optimize the battery usage. As this service currently run in the telephony process the
- // OS give it privileges to behave the same as setExact(), but set() is the targeted
- // behavior once this is unbundled.
- getSystemService(AlarmManager.class).set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
- SystemClock.elapsedRealtime() + timeMillis,
- getWakeupIntent());
- mWakeLock.release();
- VvmLog.d(TAG, "Long sleep for " + timeMillis + " millis");
- }
-
- private PendingIntent getWakeupIntent() {
- Intent intent = new Intent(ACTION_WAKEUP, null, this, getClass());
- return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
- }
-
- private void prepareStop() {
- VvmLog.d(TAG,
- "No more tasks, stopping service if no task are added in "
- + STOP_DELAY_MILLISECONDS + " millis");
- mMainThreadHandler.postDelayed(mStopServiceWithDelay, STOP_DELAY_MILLISECONDS);
- }
-
- static class MessageSender {
-
- public void send(Message message) {
- message.sendToTarget();
- }
- }
-
- @NeededForTesting
- void setContextForTest(Context context) {
- mContext = context;
- }
-
- @NeededForTesting
- void setTaskAutoRunDisabledForTest(boolean value) {
- mTaskAutoRunDisabledForTesting = value;
- }
-
- @NeededForTesting
- void setMessageSenderForTest(MessageSender sender) {
- mMessageSender = sender;
- }
-
- @NeededForTesting
- void clearTasksForTest() {
- mTasks.clear();
- }
-
- @Override
- @Nullable
- public IBinder onBind(Intent intent) {
- return new LocalBinder();
- }
-
- @NeededForTesting
- class LocalBinder extends Binder {
-
- @NeededForTesting
- public TaskSchedulerService getService() {
- return TaskSchedulerService.this;
- }
- }
-}
diff --git a/java/com/android/voicemailomtp/settings/VisualVoicemailSettingsUtil.java b/java/com/android/voicemailomtp/settings/VisualVoicemailSettingsUtil.java
deleted file mode 100644
index 5cec52842..000000000
--- a/java/com/android/voicemailomtp/settings/VisualVoicemailSettingsUtil.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.settings;
-
-import android.content.Context;
-import android.telecom.PhoneAccountHandle;
-
-import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
-import com.android.voicemailomtp.VisualVoicemailPreferences;
-import com.android.voicemailomtp.sync.OmtpVvmSourceManager;
-
-/**
- * Save whether or not a particular account is enabled in shared to be retrieved later.
- */
-public class VisualVoicemailSettingsUtil {
-
- private static final String IS_ENABLED_KEY = "is_enabled";
-
-
- public static void setEnabled(Context context, PhoneAccountHandle phoneAccount,
- boolean isEnabled) {
- new VisualVoicemailPreferences(context, phoneAccount).edit()
- .putBoolean(IS_ENABLED_KEY, isEnabled)
- .apply();
- OmtpVvmCarrierConfigHelper config = new OmtpVvmCarrierConfigHelper(context, phoneAccount);
- if (isEnabled) {
- OmtpVvmSourceManager.getInstance(context).addPhoneStateListener(phoneAccount);
- config.startActivation();
- } else {
- OmtpVvmSourceManager.getInstance(context).removeSource(phoneAccount);
- config.startDeactivation();
- }
- }
-
- public static boolean isEnabled(Context context,
- PhoneAccountHandle phoneAccount) {
- if (phoneAccount == null) {
- return false;
- }
-
- VisualVoicemailPreferences prefs = new VisualVoicemailPreferences(context, phoneAccount);
- if (prefs.contains(IS_ENABLED_KEY)) {
- // isEnableByDefault is a bit expensive, so don't use it as default value of
- // getBoolean(). The "false" here should never be actually used.
- return prefs.getBoolean(IS_ENABLED_KEY, false);
- }
- return new OmtpVvmCarrierConfigHelper(context, phoneAccount).isEnabledByDefault();
- }
-
- /**
- * Whether the client enabled status is explicitly set by user or by default(Whether carrier VVM
- * app is installed). This is used to determine whether to disable the client when the carrier
- * VVM app is installed. If the carrier VVM app is installed the client should give priority to
- * it if the settings are not touched.
- */
- public static boolean isEnabledUserSet(Context context,
- PhoneAccountHandle phoneAccount) {
- if (phoneAccount == null) {
- return false;
- }
- VisualVoicemailPreferences prefs = new VisualVoicemailPreferences(context, phoneAccount);
- return prefs.contains(IS_ENABLED_KEY);
- }
-}
diff --git a/java/com/android/voicemailomtp/settings/VoicemailChangePinActivity.java b/java/com/android/voicemailomtp/settings/VoicemailChangePinActivity.java
deleted file mode 100644
index e679e9970..000000000
--- a/java/com/android/voicemailomtp/settings/VoicemailChangePinActivity.java
+++ /dev/null
@@ -1,634 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.settings;
-
-import android.annotation.TargetApi;
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.app.ProgressDialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.DialogInterface.OnDismissListener;
-import android.net.Network;
-import android.os.Build.VERSION_CODES;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Message;
-import android.support.annotation.Nullable;
-import android.telecom.PhoneAccountHandle;
-import android.text.Editable;
-import android.text.InputFilter;
-import android.text.InputFilter.LengthFilter;
-import android.text.TextWatcher;
-import android.view.KeyEvent;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.view.WindowManager;
-import android.view.inputmethod.EditorInfo;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.TextView;
-import android.widget.TextView.OnEditorActionListener;
-import android.widget.Toast;
-import com.android.voicemailomtp.OmtpConstants;
-import com.android.voicemailomtp.OmtpConstants.ChangePinResult;
-import com.android.voicemailomtp.OmtpEvents;
-import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
-import com.android.voicemailomtp.R;
-import com.android.voicemailomtp.VisualVoicemailPreferences;
-import com.android.voicemailomtp.VoicemailStatus;
-import com.android.voicemailomtp.VvmLog;
-import com.android.voicemailomtp.imap.ImapHelper;
-import com.android.voicemailomtp.imap.ImapHelper.InitializingException;
-import com.android.voicemailomtp.mail.MessagingException;
-import com.android.voicemailomtp.sync.VvmNetworkRequestCallback;
-
-/**
- * Dialog to change the voicemail PIN. The TUI (Telephony User Interface) PIN is used when accessing
- * traditional voicemail through phone call. The intent to launch this activity must contain {@link
- * #EXTRA_PHONE_ACCOUNT_HANDLE}
- */
-@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
-public class VoicemailChangePinActivity extends Activity
- implements OnClickListener, OnEditorActionListener, TextWatcher {
-
- private static final String TAG = "VmChangePinActivity";
-
- public static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle";
-
- private static final String KEY_DEFAULT_OLD_PIN = "default_old_pin";
-
- private static final int MESSAGE_HANDLE_RESULT = 1;
-
- private PhoneAccountHandle mPhoneAccountHandle;
- private OmtpVvmCarrierConfigHelper mConfig;
-
- private int mPinMinLength;
- private int mPinMaxLength;
-
- private State mUiState = State.Initial;
- private String mOldPin;
- private String mFirstPin;
-
- private ProgressDialog mProgressDialog;
-
- private TextView mHeaderText;
- private TextView mHintText;
- private TextView mErrorText;
- private EditText mPinEntry;
- private Button mCancelButton;
- private Button mNextButton;
-
- private Handler mHandler = new Handler() {
- @Override
- public void handleMessage(Message message) {
- if (message.what == MESSAGE_HANDLE_RESULT) {
- mUiState.handleResult(VoicemailChangePinActivity.this, message.arg1);
- }
- }
- };
-
- private enum State {
- /**
- * Empty state to handle initial state transition. Will immediately switch into {@link
- * #VerifyOldPin} if a default PIN has been set by the OMTP client, or {@link #EnterOldPin}
- * if not.
- */
- Initial,
- /**
- * Prompt the user to enter old PIN. The PIN will be verified with the server before
- * proceeding to {@link #EnterNewPin}.
- */
- EnterOldPin {
- @Override
- public void onEnter(VoicemailChangePinActivity activity) {
- activity.setHeader(R.string.change_pin_enter_old_pin_header);
- activity.mHintText.setText(R.string.change_pin_enter_old_pin_hint);
- activity.mNextButton.setText(R.string.change_pin_continue_label);
- activity.mErrorText.setText(null);
- }
-
- @Override
- public void onInputChanged(VoicemailChangePinActivity activity) {
- activity.setNextEnabled(activity.getCurrentPasswordInput().length() > 0);
- }
-
-
- @Override
- public void handleNext(VoicemailChangePinActivity activity) {
- activity.mOldPin = activity.getCurrentPasswordInput();
- activity.verifyOldPin();
- }
-
- @Override
- public void handleResult(VoicemailChangePinActivity activity,
- @ChangePinResult int result) {
- if (result == OmtpConstants.CHANGE_PIN_SUCCESS) {
- activity.updateState(State.EnterNewPin);
- } else {
- CharSequence message = activity.getChangePinResultMessage(result);
- activity.showError(message);
- activity.mPinEntry.setText("");
- }
- }
- },
- /**
- * The default old PIN is found. Show a blank screen while verifying with the server to make
- * sure the PIN is still valid. If the PIN is still valid, proceed to {@link #EnterNewPin}.
- * If not, the user probably changed the PIN through other means, proceed to {@link
- * #EnterOldPin}. If any other issue caused the verifying to fail, show an error and exit.
- */
- VerifyOldPin {
- @Override
- public void onEnter(VoicemailChangePinActivity activity) {
- activity.findViewById(android.R.id.content).setVisibility(View.INVISIBLE);
- activity.verifyOldPin();
- }
-
- @Override
- public void handleResult(final VoicemailChangePinActivity activity,
- @ChangePinResult int result) {
- if (result == OmtpConstants.CHANGE_PIN_SUCCESS) {
- activity.updateState(State.EnterNewPin);
- } else if (result == OmtpConstants.CHANGE_PIN_SYSTEM_ERROR) {
- activity.getWindow().setSoftInputMode(
- WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
- activity.showError(activity.getString(R.string.change_pin_system_error),
- new OnDismissListener() {
- @Override
- public void onDismiss(DialogInterface dialog) {
- activity.finish();
- }
- });
- } else {
- VvmLog.e(TAG, "invalid default old PIN: " + activity
- .getChangePinResultMessage(result));
- // If the default old PIN is rejected by the server, the PIN is probably changed
- // through other means, or the generated pin is invalid
- // Wipe the default old PIN so the old PIN input box will be shown to the user
- // on the next time.
- setDefaultOldPIN(activity, activity.mPhoneAccountHandle, null);
- activity.handleOmtpEvent(OmtpEvents.CONFIG_PIN_SET);
- activity.updateState(State.EnterOldPin);
- }
- }
-
- @Override
- public void onLeave(VoicemailChangePinActivity activity) {
- activity.findViewById(android.R.id.content).setVisibility(View.VISIBLE);
- }
- },
- /**
- * Let the user enter the new PIN and validate the format. Only length is enforced, PIN
- * strength check relies on the server. After a valid PIN is entered, proceed to {@link
- * #ConfirmNewPin}
- */
- EnterNewPin {
- @Override
- public void onEnter(VoicemailChangePinActivity activity) {
- activity.mHeaderText.setText(R.string.change_pin_enter_new_pin_header);
- activity.mNextButton.setText(R.string.change_pin_continue_label);
- activity.mHintText.setText(
- activity.getString(R.string.change_pin_enter_new_pin_hint,
- activity.mPinMinLength, activity.mPinMaxLength));
- }
-
- @Override
- public void onInputChanged(VoicemailChangePinActivity activity) {
- String password = activity.getCurrentPasswordInput();
- if (password.length() == 0) {
- activity.setNextEnabled(false);
- return;
- }
- CharSequence error = activity.validatePassword(password);
- if (error != null) {
- activity.mErrorText.setText(error);
- activity.setNextEnabled(false);
- } else {
- activity.mErrorText.setText(null);
- activity.setNextEnabled(true);
- }
- }
-
- @Override
- public void handleNext(VoicemailChangePinActivity activity) {
- CharSequence errorMsg;
- errorMsg = activity.validatePassword(activity.getCurrentPasswordInput());
- if (errorMsg != null) {
- activity.showError(errorMsg);
- return;
- }
- activity.mFirstPin = activity.getCurrentPasswordInput();
- activity.updateState(State.ConfirmNewPin);
- }
- },
- /**
- * Let the user type in the same PIN again to avoid typos. If the PIN matches then perform a
- * PIN change to the server. Finish the activity if succeeded. Return to {@link
- * #EnterOldPin} if the old PIN is rejected, {@link #EnterNewPin} for other failure.
- */
- ConfirmNewPin {
- @Override
- public void onEnter(VoicemailChangePinActivity activity) {
- activity.mHeaderText.setText(R.string.change_pin_confirm_pin_header);
- activity.mHintText.setText(null);
- activity.mNextButton.setText(R.string.change_pin_ok_label);
- }
-
- @Override
- public void onInputChanged(VoicemailChangePinActivity activity) {
- if (activity.getCurrentPasswordInput().length() == 0) {
- activity.setNextEnabled(false);
- return;
- }
- if (activity.getCurrentPasswordInput().equals(activity.mFirstPin)) {
- activity.setNextEnabled(true);
- activity.mErrorText.setText(null);
- } else {
- activity.setNextEnabled(false);
- activity.mErrorText.setText(R.string.change_pin_confirm_pins_dont_match);
- }
- }
-
- @Override
- public void handleResult(VoicemailChangePinActivity activity,
- @ChangePinResult int result) {
- if (result == OmtpConstants.CHANGE_PIN_SUCCESS) {
- // If the PIN change succeeded we no longer know what the old (current) PIN is.
- // Wipe the default old PIN so the old PIN input box will be shown to the user
- // on the next time.
- setDefaultOldPIN(activity, activity.mPhoneAccountHandle, null);
- activity.handleOmtpEvent(OmtpEvents.CONFIG_PIN_SET);
-
- activity.finish();
-
- Toast.makeText(activity, activity.getString(R.string.change_pin_succeeded),
- Toast.LENGTH_SHORT).show();
- } else {
- CharSequence message = activity.getChangePinResultMessage(result);
- VvmLog.i(TAG, "Change PIN failed: " + message);
- activity.showError(message);
- if (result == OmtpConstants.CHANGE_PIN_MISMATCH) {
- // Somehow the PIN has changed, prompt to enter the old PIN again.
- activity.updateState(State.EnterOldPin);
- } else {
- // The new PIN failed to fulfil other restrictions imposed by the server.
- activity.updateState(State.EnterNewPin);
- }
-
- }
-
- }
-
- @Override
- public void handleNext(VoicemailChangePinActivity activity) {
- activity.processPinChange(activity.mOldPin, activity.mFirstPin);
- }
- };
-
- /**
- * The activity has switched from another state to this one.
- */
- public void onEnter(VoicemailChangePinActivity activity) {
- // Do nothing
- }
-
- /**
- * The user has typed something into the PIN input field. Also called after {@link
- * #onEnter(VoicemailChangePinActivity)}
- */
- public void onInputChanged(VoicemailChangePinActivity activity) {
- // Do nothing
- }
-
- /**
- * The asynchronous call to change the PIN on the server has returned.
- */
- public void handleResult(VoicemailChangePinActivity activity, @ChangePinResult int result) {
- // Do nothing
- }
-
- /**
- * The user has pressed the "next" button.
- */
- public void handleNext(VoicemailChangePinActivity activity) {
- // Do nothing
- }
-
- /**
- * The activity has switched from this state to another one.
- */
- public void onLeave(VoicemailChangePinActivity activity) {
- // Do nothing
- }
-
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- mPhoneAccountHandle = getIntent().getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE);
- mConfig = new OmtpVvmCarrierConfigHelper(this, mPhoneAccountHandle);
- setContentView(R.layout.voicemail_change_pin);
- setTitle(R.string.change_pin_title);
-
- readPinLength();
-
- View view = findViewById(android.R.id.content);
-
- mCancelButton = (Button) view.findViewById(R.id.cancel_button);
- mCancelButton.setOnClickListener(this);
- mNextButton = (Button) view.findViewById(R.id.next_button);
- mNextButton.setOnClickListener(this);
-
- mPinEntry = (EditText) view.findViewById(R.id.pin_entry);
- mPinEntry.setOnEditorActionListener(this);
- mPinEntry.addTextChangedListener(this);
- if (mPinMaxLength != 0) {
- mPinEntry.setFilters(new InputFilter[]{new LengthFilter(mPinMaxLength)});
- }
-
-
- mHeaderText = (TextView) view.findViewById(R.id.headerText);
- mHintText = (TextView) view.findViewById(R.id.hintText);
- mErrorText = (TextView) view.findViewById(R.id.errorText);
-
- if (isDefaultOldPinSet(this, mPhoneAccountHandle)) {
- mOldPin = getDefaultOldPin(this, mPhoneAccountHandle);
- updateState(State.VerifyOldPin);
- } else {
- updateState(State.EnterOldPin);
- }
- }
-
- private void handleOmtpEvent(OmtpEvents event) {
- mConfig.handleEvent(getVoicemailStatusEditor(), event);
- }
-
- private VoicemailStatus.Editor getVoicemailStatusEditor() {
- // This activity does not have any automatic retry mechanism, errors should be written right
- // away.
- return VoicemailStatus.edit(this, mPhoneAccountHandle);
- }
-
- /**
- * Extracts the pin length requirement sent by the server with a STATUS SMS.
- */
- private void readPinLength() {
- VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(this,
- mPhoneAccountHandle);
- // The OMTP pin length format is {min}-{max}
- String[] lengths = preferences.getString(OmtpConstants.TUI_PASSWORD_LENGTH, "").split("-");
- if (lengths.length == 2) {
- try {
- mPinMinLength = Integer.parseInt(lengths[0]);
- mPinMaxLength = Integer.parseInt(lengths[1]);
- } catch (NumberFormatException e) {
- mPinMinLength = 0;
- mPinMaxLength = 0;
- }
- } else {
- mPinMinLength = 0;
- mPinMaxLength = 0;
- }
- }
-
- @Override
- public void onResume() {
- super.onResume();
- updateState(mUiState);
-
- }
-
- public void handleNext() {
- if (mPinEntry.length() == 0) {
- return;
- }
- mUiState.handleNext(this);
- }
-
- public void onClick(View v) {
- if (v.getId() == R.id.next_button) {
- handleNext();
- } else if (v.getId() == R.id.cancel_button) {
- finish();
- }
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- if (item.getItemId() == android.R.id.home) {
- onBackPressed();
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
-
- public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
- if (!mNextButton.isEnabled()) {
- return true;
- }
- // Check if this was the result of hitting the enter or "done" key
- if (actionId == EditorInfo.IME_NULL
- || actionId == EditorInfo.IME_ACTION_DONE
- || actionId == EditorInfo.IME_ACTION_NEXT) {
- handleNext();
- return true;
- }
- return false;
- }
-
- public void afterTextChanged(Editable s) {
- mUiState.onInputChanged(this);
- }
-
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
- // Do nothing
- }
-
- public void onTextChanged(CharSequence s, int start, int before, int count) {
- // Do nothing
- }
-
- /**
- * After replacing the default PIN with a random PIN, call this to store the random PIN. The
- * stored PIN will be automatically entered when the user attempts to change the PIN.
- */
- public static void setDefaultOldPIN(Context context, PhoneAccountHandle phoneAccountHandle,
- String pin) {
- new VisualVoicemailPreferences(context, phoneAccountHandle)
- .edit().putString(KEY_DEFAULT_OLD_PIN, pin).apply();
- }
-
- public static boolean isDefaultOldPinSet(Context context,
- PhoneAccountHandle phoneAccountHandle) {
- return getDefaultOldPin(context, phoneAccountHandle) != null;
- }
-
- private static String getDefaultOldPin(Context context, PhoneAccountHandle phoneAccountHandle) {
- return new VisualVoicemailPreferences(context, phoneAccountHandle)
- .getString(KEY_DEFAULT_OLD_PIN);
- }
-
- private String getCurrentPasswordInput() {
- return mPinEntry.getText().toString();
- }
-
- private void updateState(State state) {
- State previousState = mUiState;
- mUiState = state;
- if (previousState != state) {
- previousState.onLeave(this);
- mPinEntry.setText("");
- mUiState.onEnter(this);
- }
- mUiState.onInputChanged(this);
- }
-
- /**
- * Validates PIN and returns a message to display if PIN fails test.
- *
- * @param password the raw password the user typed in
- * @return error message to show to user or null if password is OK
- */
- private CharSequence validatePassword(String password) {
- if (mPinMinLength == 0 && mPinMaxLength == 0) {
- // Invalid length requirement is sent by the server, just accept anything and let the
- // server decide.
- return null;
- }
-
- if (password.length() < mPinMinLength) {
- return getString(R.string.vm_change_pin_error_too_short);
- }
- return null;
- }
-
- private void setHeader(int text) {
- mHeaderText.setText(text);
- mPinEntry.setContentDescription(mHeaderText.getText());
- }
-
- /**
- * Get the corresponding message for the {@link ChangePinResult}.<code>result</code> must not
- * {@link OmtpConstants#CHANGE_PIN_SUCCESS}
- */
- private CharSequence getChangePinResultMessage(@ChangePinResult int result) {
- switch (result) {
- case OmtpConstants.CHANGE_PIN_TOO_SHORT:
- return getString(R.string.vm_change_pin_error_too_short);
- case OmtpConstants.CHANGE_PIN_TOO_LONG:
- return getString(R.string.vm_change_pin_error_too_long);
- case OmtpConstants.CHANGE_PIN_TOO_WEAK:
- return getString(R.string.vm_change_pin_error_too_weak);
- case OmtpConstants.CHANGE_PIN_INVALID_CHARACTER:
- return getString(R.string.vm_change_pin_error_invalid);
- case OmtpConstants.CHANGE_PIN_MISMATCH:
- return getString(R.string.vm_change_pin_error_mismatch);
- case OmtpConstants.CHANGE_PIN_SYSTEM_ERROR:
- return getString(R.string.vm_change_pin_error_system_error);
- default:
- VvmLog.wtf(TAG, "Unexpected ChangePinResult " + result);
- return null;
- }
- }
-
- private void verifyOldPin() {
- processPinChange(mOldPin, mOldPin);
- }
-
- private void setNextEnabled(boolean enabled) {
- mNextButton.setEnabled(enabled);
- }
-
-
- private void showError(CharSequence message) {
- showError(message, null);
- }
-
- private void showError(CharSequence message, @Nullable OnDismissListener callback) {
- new AlertDialog.Builder(this)
- .setMessage(message)
- .setPositiveButton(android.R.string.ok, null)
- .setOnDismissListener(callback)
- .show();
- }
-
- /**
- * Asynchronous call to change the PIN on the server.
- */
- private void processPinChange(String oldPin, String newPin) {
- mProgressDialog = new ProgressDialog(this);
- mProgressDialog.setCancelable(false);
- mProgressDialog.setMessage(getString(R.string.vm_change_pin_progress_message));
- mProgressDialog.show();
-
- ChangePinNetworkRequestCallback callback = new ChangePinNetworkRequestCallback(oldPin,
- newPin);
- callback.requestNetwork();
- }
-
- private class ChangePinNetworkRequestCallback extends VvmNetworkRequestCallback {
-
- private final String mOldPin;
- private final String mNewPin;
-
- public ChangePinNetworkRequestCallback(String oldPin, String newPin) {
- super(mConfig, mPhoneAccountHandle,
- VoicemailChangePinActivity.this.getVoicemailStatusEditor());
- mOldPin = oldPin;
- mNewPin = newPin;
- }
-
- @Override
- public void onAvailable(Network network) {
- super.onAvailable(network);
- try (ImapHelper helper =
- new ImapHelper(VoicemailChangePinActivity.this, mPhoneAccountHandle, network,
- getVoicemailStatusEditor())) {
-
- @ChangePinResult int result =
- helper.changePin(mOldPin, mNewPin);
- sendResult(result);
- } catch (InitializingException | MessagingException e) {
- VvmLog.e(TAG, "ChangePinNetworkRequestCallback: onAvailable: ", e);
- sendResult(OmtpConstants.CHANGE_PIN_SYSTEM_ERROR);
- }
- }
-
- @Override
- public void onFailed(String reason) {
- super.onFailed(reason);
- sendResult(OmtpConstants.CHANGE_PIN_SYSTEM_ERROR);
- }
-
- private void sendResult(@ChangePinResult int result) {
- VvmLog.i(TAG, "Change PIN result: " + result);
- if (mProgressDialog.isShowing() && !VoicemailChangePinActivity.this.isDestroyed() &&
- !VoicemailChangePinActivity.this.isFinishing()) {
- mProgressDialog.dismiss();
- } else {
- VvmLog.i(TAG, "Dialog not visible, not dismissing");
- }
- mHandler.obtainMessage(MESSAGE_HANDLE_RESULT, result, 0).sendToTarget();
- releaseNetwork();
- }
- }
-
-}
diff --git a/java/com/android/voicemailomtp/settings/VoicemailSettingsActivity.java b/java/com/android/voicemailomtp/settings/VoicemailSettingsActivity.java
deleted file mode 100644
index ac0df6fab..000000000
--- a/java/com/android/voicemailomtp/settings/VoicemailSettingsActivity.java
+++ /dev/null
@@ -1,222 +0,0 @@
-/**
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.settings;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.preference.Preference;
-import android.preference.PreferenceActivity;
-import android.preference.PreferenceScreen;
-import android.preference.SwitchPreference;
-import android.telecom.PhoneAccountHandle;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.MenuItem;
-
-import com.android.voicemailomtp.OmtpConstants;
-import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
-import com.android.voicemailomtp.R;
-import com.android.voicemailomtp.SubscriptionInfoHelper;
-import com.android.voicemailomtp.VisualVoicemailPreferences;
-
-public class VoicemailSettingsActivity extends PreferenceActivity implements
- Preference.OnPreferenceChangeListener {
- private static final String LOG_TAG = VoicemailSettingsActivity.class.getSimpleName();
- private static final boolean DBG = true;
-
- /**
- * Intent action to bring up Voicemail Provider settings
- * DO NOT RENAME. There are existing apps which use this intent value.
- */
- public static final String ACTION_ADD_VOICEMAIL =
- "com.android.voicemailomtp.CallFeaturesSetting.ADD_VOICEMAIL";
-
- /**
- * Intent action to bring up the {@code VoicemailSettingsActivity}.
- * DO NOT RENAME. There are existing apps which use this intent value.
- */
- public static final String ACTION_CONFIGURE_VOICEMAIL =
- "com.android.voicemailomtp.CallFeaturesSetting.CONFIGURE_VOICEMAIL";
-
- // Extra put in the return from VM provider config containing voicemail number to set
- public static final String VM_NUMBER_EXTRA = "com.android.voicemailomtp.VoicemailNumber";
- // Extra put in the return from VM provider config containing call forwarding number to set
- public static final String FWD_NUMBER_EXTRA = "com.android.voicemailomtp.ForwardingNumber";
- // Extra put in the return from VM provider config containing call forwarding number to set
- public static final String FWD_NUMBER_TIME_EXTRA = "com.android.voicemailomtp.ForwardingNumberTime";
- // If the VM provider returns non null value in this extra we will force the user to
- // choose another VM provider
- public static final String SIGNOUT_EXTRA = "com.android.voicemailomtp.Signout";
-
- /**
- * String Extra put into ACTION_ADD_VOICEMAIL call to indicate which provider should be hidden
- * in the list of providers presented to the user. This allows a provider which is being
- * disabled (e.g. GV user logging out) to force the user to pick some other provider.
- */
- public static final String IGNORE_PROVIDER_EXTRA = "com.android.voicemailomtp.ProviderToIgnore";
-
- /**
- * String Extra put into ACTION_ADD_VOICEMAIL to indicate that the voicemail setup screen should
- * be opened.
- */
- public static final String SETUP_VOICEMAIL_EXTRA = "com.android.voicemailomtp.SetupVoicemail";
-
- /** Event for Async voicemail change call */
- private static final int EVENT_VOICEMAIL_CHANGED = 500;
- private static final int EVENT_FORWARDING_CHANGED = 501;
- private static final int EVENT_FORWARDING_GET_COMPLETED = 502;
-
- /** Handle to voicemail pref */
- private static final int VOICEMAIL_PREF_ID = 1;
- private static final int VOICEMAIL_PROVIDER_CFG_ID = 2;
-
- /**
- * Used to indicate that the voicemail preference should be shown.
- */
- private boolean mShowVoicemailPreference = false;
-
- private int mSubId;
- private PhoneAccountHandle mPhoneAccountHandle;
- private SubscriptionInfoHelper mSubscriptionInfoHelper;
- private OmtpVvmCarrierConfigHelper mOmtpVvmCarrierConfigHelper;
-
- private SwitchPreference mVoicemailVisualVoicemail;
- private Preference mVoicemailChangePinPreference;
-
- //*********************************************************************************************
- // Preference Activity Methods
- //*********************************************************************************************
-
- @Override
- protected void onCreate(Bundle icicle) {
- super.onCreate(icicle);
-
- // Show the voicemail preference in onResume if the calling intent specifies the
- // ACTION_ADD_VOICEMAIL action.
- mShowVoicemailPreference = (icicle == null) &&
- TextUtils.equals(getIntent().getAction(), ACTION_ADD_VOICEMAIL);
-
- mSubscriptionInfoHelper = new SubscriptionInfoHelper(this, getIntent());
- mSubscriptionInfoHelper.setActionBarTitle(
- getActionBar(), getResources(), R.string.voicemail_settings_with_label);
- mSubId = mSubscriptionInfoHelper.getSubId();
- // TODO: scrap this activity.
- /*
- mPhoneAccountHandle = PhoneAccountHandleConverter
- .fromSubId(this, mSubId);
-
- mOmtpVvmCarrierConfigHelper = new OmtpVvmCarrierConfigHelper(
- this, mSubId);
- */
- }
-
- @Override
- protected void onResume() {
- super.onResume();
-
- PreferenceScreen preferenceScreen = getPreferenceScreen();
- if (preferenceScreen != null) {
- preferenceScreen.removeAll();
- }
-
- addPreferencesFromResource(R.xml.voicemail_settings);
-
- PreferenceScreen prefSet = getPreferenceScreen();
-
- mVoicemailVisualVoicemail = (SwitchPreference) findPreference(
- getResources().getString(R.string.voicemail_visual_voicemail_key));
-
- mVoicemailChangePinPreference = findPreference(
- getResources().getString(R.string.voicemail_change_pin_key));
- Intent changePinIntent = new Intent(new Intent(this, VoicemailChangePinActivity.class));
- changePinIntent.putExtra(VoicemailChangePinActivity.EXTRA_PHONE_ACCOUNT_HANDLE,
- mPhoneAccountHandle);
-
- mVoicemailChangePinPreference.setIntent(changePinIntent);
- if (VoicemailChangePinActivity.isDefaultOldPinSet(this, mPhoneAccountHandle)) {
- mVoicemailChangePinPreference.setTitle(R.string.voicemail_set_pin_dialog_title);
- } else {
- mVoicemailChangePinPreference.setTitle(R.string.voicemail_change_pin_dialog_title);
- }
-
- if (mOmtpVvmCarrierConfigHelper.isValid()) {
- mVoicemailVisualVoicemail.setOnPreferenceChangeListener(this);
- mVoicemailVisualVoicemail.setChecked(
- VisualVoicemailSettingsUtil.isEnabled(this, mPhoneAccountHandle));
- if (!isVisualVoicemailActivated()) {
- prefSet.removePreference(mVoicemailChangePinPreference);
- }
- } else {
- prefSet.removePreference(mVoicemailVisualVoicemail);
- prefSet.removePreference(mVoicemailChangePinPreference);
- }
- }
-
- @Override
- public void onPause() {
- super.onPause();
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- if (item.getItemId() == android.R.id.home) {
- onBackPressed();
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
-
- /**
- * Implemented to support onPreferenceChangeListener to look for preference changes.
- *
- * @param preference is the preference to be changed
- * @param objValue should be the value of the selection, NOT its localized
- * display value.
- */
- @Override
- public boolean onPreferenceChange(Preference preference, Object objValue) {
- if (DBG) log("onPreferenceChange: \"" + preference + "\" changed to \"" + objValue + "\"");
- if (preference.getKey().equals(mVoicemailVisualVoicemail.getKey())) {
- boolean isEnabled = (boolean) objValue;
- VisualVoicemailSettingsUtil
- .setEnabled(this, mPhoneAccountHandle, isEnabled);
- PreferenceScreen prefSet = getPreferenceScreen();
- if (isVisualVoicemailActivated()) {
- prefSet.addPreference(mVoicemailChangePinPreference);
- } else {
- prefSet.removePreference(mVoicemailChangePinPreference);
- }
- }
-
- // Always let the preference setting proceed.
- return true;
- }
-
- private boolean isVisualVoicemailActivated() {
- if (!VisualVoicemailSettingsUtil.isEnabled(this, mPhoneAccountHandle)) {
- return false;
- }
- VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(this,
- mPhoneAccountHandle);
- return preferences.getString(OmtpConstants.SERVER_ADDRESS, null) != null;
-
- }
-
- private static void log(String msg) {
- Log.d(LOG_TAG, msg);
- }
-}
diff --git a/java/com/android/voicemailomtp/sms/LegacyModeSmsHandler.java b/java/com/android/voicemailomtp/sms/LegacyModeSmsHandler.java
deleted file mode 100644
index bb722bffc..000000000
--- a/java/com/android/voicemailomtp/sms/LegacyModeSmsHandler.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2016 Google Inc. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.sms;
-
-import android.content.Context;
-import android.os.Bundle;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.VisualVoicemailSms;
-
-import com.android.voicemailomtp.OmtpConstants;
-import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
-import com.android.voicemailomtp.TelephonyManagerStub;
-import com.android.voicemailomtp.VvmLog;
-
-/**
- * Class ot handle voicemail SMS under legacy mode
- *
- * @see OmtpVvmCarrierConfigHelper#isLegacyModeEnabled()
- */
-public class LegacyModeSmsHandler {
-
- private static final String TAG = "LegacyModeSmsHandler";
-
- public static void handle(Context context, VisualVoicemailSms sms) {
- VvmLog.v(TAG, "processing VVM SMS on legacy mode");
- String eventType = sms.getPrefix();
- Bundle data = sms.getFields();
- PhoneAccountHandle handle = sms.getPhoneAccountHandle();
-
- if (eventType.equals(OmtpConstants.SYNC_SMS_PREFIX)) {
- SyncMessage message = new SyncMessage(data);
- VvmLog.v(TAG, "Received SYNC sms for " + handle +
- " with event " + message.getSyncTriggerEvent());
-
- switch (message.getSyncTriggerEvent()) {
- case OmtpConstants.NEW_MESSAGE:
- case OmtpConstants.MAILBOX_UPDATE:
- // The user has called into the voicemail and the new message count could
- // change.
- // For some carriers new message count could be set to 0 even if there are still
- // unread messages, to clear the message waiting indicator.
- VvmLog.v(TAG, "updating MWI");
-
- // Setting voicemail message count to non-zero will show the telephony voicemail
- // notification, and zero will clear it.
- TelephonyManagerStub.showVoicemailNotification(message.getNewMessageCount());
- break;
- default:
- break;
- }
- }
- }
-}
diff --git a/java/com/android/voicemailomtp/sms/OmtpCvvmMessageSender.java b/java/com/android/voicemailomtp/sms/OmtpCvvmMessageSender.java
deleted file mode 100644
index 63af2c13d..000000000
--- a/java/com/android/voicemailomtp/sms/OmtpCvvmMessageSender.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (C) 2015 Google Inc. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.sms;
-
-import android.app.PendingIntent;
-import android.content.Context;
-import android.support.annotation.Nullable;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.SmsManager;
-import com.android.voicemailomtp.OmtpConstants;
-
-/**
- * An implementation of the OmtpMessageSender for T-Mobile.
- */
-public class OmtpCvvmMessageSender extends OmtpMessageSender {
- public OmtpCvvmMessageSender(Context context, PhoneAccountHandle phoneAccountHandle,
- short applicationPort, String destinationNumber) {
- super(context, phoneAccountHandle, applicationPort, destinationNumber);
- }
-
- @Override
- public void requestVvmActivation(@Nullable PendingIntent sentIntent) {
- sendCvvmMessage(OmtpConstants.ACTIVATE_REQUEST, sentIntent);
- }
-
- @Override
- public void requestVvmDeactivation(@Nullable PendingIntent sentIntent) {
- sendCvvmMessage(OmtpConstants.DEACTIVATE_REQUEST, sentIntent);
- }
-
- @Override
- public void requestVvmStatus(@Nullable PendingIntent sentIntent) {
- sendCvvmMessage(OmtpConstants.STATUS_REQUEST, sentIntent);
- }
-
- private void sendCvvmMessage(String request, PendingIntent sentIntent) {
- StringBuilder sb = new StringBuilder().append(request);
- sb.append(OmtpConstants.SMS_PREFIX_SEPARATOR);
- appendField(sb, "dt", "15");
- sendSms(sb.toString(), sentIntent);
- }
-}
diff --git a/java/com/android/voicemailomtp/sms/OmtpMessageReceiver.java b/java/com/android/voicemailomtp/sms/OmtpMessageReceiver.java
deleted file mode 100644
index c4ad2085f..000000000
--- a/java/com/android/voicemailomtp/sms/OmtpMessageReceiver.java
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.sms;
-
-import android.annotation.TargetApi;
-import android.content.BroadcastReceiver;
-import android.content.ContentUris;
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Build.VERSION_CODES;
-import android.os.Bundle;
-import android.os.UserManager;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.VisualVoicemailSms;
-import com.android.voicemailomtp.ActivationTask;
-import com.android.voicemailomtp.OmtpConstants;
-import com.android.voicemailomtp.OmtpService;
-import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
-import com.android.voicemailomtp.Voicemail;
-import com.android.voicemailomtp.VvmLog;
-import com.android.voicemailomtp.protocol.VisualVoicemailProtocol;
-import com.android.voicemailomtp.settings.VisualVoicemailSettingsUtil;
-import com.android.voicemailomtp.sync.OmtpVvmSyncService;
-import com.android.voicemailomtp.sync.SyncOneTask;
-import com.android.voicemailomtp.sync.SyncTask;
-import com.android.voicemailomtp.sync.VoicemailsQueryHelper;
-import com.android.voicemailomtp.utils.VoicemailDatabaseUtil;
-
-/** Receive SMS messages and send for processing by the OMTP visual voicemail source. */
-@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
-public class OmtpMessageReceiver extends BroadcastReceiver {
-
- private static final String TAG = "OmtpMessageReceiver";
-
- private Context mContext;
-
- @Override
- public void onReceive(Context context, Intent intent) {
- mContext = context;
- VisualVoicemailSms sms = intent.getExtras().getParcelable(OmtpService.EXTRA_VOICEMAIL_SMS);
- PhoneAccountHandle phone = sms.getPhoneAccountHandle();
-
- if (phone == null) {
- // This should never happen
- VvmLog.i(TAG, "Received message for null phone account");
- return;
- }
-
- if (!context.getSystemService(UserManager.class).isUserUnlocked()) {
- VvmLog.i(TAG, "Received message on locked device");
- // LegacyModeSmsHandler can handle new message notifications without storage access
- LegacyModeSmsHandler.handle(context, sms);
- // A full sync will happen after the device is unlocked, so nothing else need to be
- // done.
- return;
- }
-
- OmtpVvmCarrierConfigHelper helper = new OmtpVvmCarrierConfigHelper(mContext, phone);
- if (!VisualVoicemailSettingsUtil.isEnabled(mContext, phone)) {
- if (helper.isLegacyModeEnabled()) {
- LegacyModeSmsHandler.handle(context, sms);
- } else {
- VvmLog.i(TAG, "Received vvm message for disabled vvm source.");
- }
- return;
- }
-
- String eventType = sms.getPrefix();
- Bundle data = sms.getFields();
-
- if (eventType == null || data == null) {
- VvmLog.e(TAG, "Unparsable VVM SMS received, ignoring");
- return;
- }
-
- if (eventType.equals(OmtpConstants.SYNC_SMS_PREFIX)) {
- SyncMessage message = new SyncMessage(data);
-
- VvmLog.v(TAG, "Received SYNC sms for " + phone +
- " with event " + message.getSyncTriggerEvent());
- processSync(phone, message);
- } else if (eventType.equals(OmtpConstants.STATUS_SMS_PREFIX)) {
- VvmLog.v(TAG, "Received Status sms for " + phone);
- // If the STATUS SMS is initiated by ActivationTask the TaskSchedulerService will reject
- // the follow request. Providing the data will also prevent ActivationTask from
- // requesting another STATUS SMS. The following task will only run if the carrier
- // spontaneous send a STATUS SMS, in that case, the VVM service should be reactivated.
- ActivationTask.start(context, phone, data);
- } else {
- VvmLog.w(TAG, "Unknown prefix: " + eventType);
- VisualVoicemailProtocol protocol = helper.getProtocol();
- if (protocol == null) {
- return;
- }
- Bundle statusData = helper.getProtocol()
- .translateStatusSmsBundle(helper, eventType, data);
- if (statusData != null) {
- VvmLog.i(TAG, "Protocol recognized the SMS as STATUS, activating");
- ActivationTask.start(context, phone, data);
- }
- }
- }
-
- /**
- * A sync message has two purposes: to signal a new voicemail message, and to indicate the
- * voicemails on the server have changed remotely (usually through the TUI). Save the new
- * message to the voicemail provider if it is the former case and perform a full sync in the
- * latter case.
- *
- * @param message The sync message to extract data from.
- */
- private void processSync(PhoneAccountHandle phone, SyncMessage message) {
- switch (message.getSyncTriggerEvent()) {
- case OmtpConstants.NEW_MESSAGE:
- if (!OmtpConstants.VOICE.equals(message.getContentType())) {
- VvmLog.i(TAG, "Non-voice message of type '" + message.getContentType()
- + "' received, ignoring");
- return;
- }
-
- Voicemail.Builder builder = Voicemail.createForInsertion(
- message.getTimestampMillis(), message.getSender())
- .setPhoneAccount(phone)
- .setSourceData(message.getId())
- .setDuration(message.getLength())
- .setSourcePackage(mContext.getPackageName());
- Voicemail voicemail = builder.build();
-
- VoicemailsQueryHelper queryHelper = new VoicemailsQueryHelper(mContext);
- if (queryHelper.isVoicemailUnique(voicemail)) {
- Uri uri = VoicemailDatabaseUtil.insert(mContext, voicemail);
- voicemail = builder.setId(ContentUris.parseId(uri)).setUri(uri).build();
- SyncOneTask.start(mContext, phone, voicemail);
- }
- break;
- case OmtpConstants.MAILBOX_UPDATE:
- SyncTask.start(mContext, phone, OmtpVvmSyncService.SYNC_DOWNLOAD_ONLY);
- break;
- case OmtpConstants.GREETINGS_UPDATE:
- // Not implemented in V1
- break;
- default:
- VvmLog.e(TAG,
- "Unrecognized sync trigger event: " + message.getSyncTriggerEvent());
- break;
- }
- }
-}
diff --git a/java/com/android/voicemailomtp/sms/OmtpMessageSender.java b/java/com/android/voicemailomtp/sms/OmtpMessageSender.java
deleted file mode 100644
index 2323e4bcf..000000000
--- a/java/com/android/voicemailomtp/sms/OmtpMessageSender.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright (C) 2015 Google Inc. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.sms;
-
-import android.app.PendingIntent;
-import android.content.Context;
-import android.support.annotation.Nullable;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.SmsManager;
-import android.telephony.VisualVoicemailService;
-import com.android.voicemailomtp.OmtpConstants;
-import com.android.voicemailomtp.TelephonyManagerStub;
-import com.android.voicemailomtp.VvmLog;
-import java.io.UnsupportedEncodingException;
-import java.util.Locale;
-
-/**
- * Send client originated OMTP messages to the OMTP server.
- * <p>
- * Uses {@link PendingIntent} instead of a call back to notify when the message is
- * sent. This is primarily to keep the implementation simple and reuse what the underlying
- * {@link SmsManager} interface provides.
- * <p>
- * Provides simple APIs to send different types of mobile originated OMTP SMS to the VVM server.
- */
-public abstract class OmtpMessageSender {
- protected static final String TAG = "OmtpMessageSender";
- protected final Context mContext;
- protected final PhoneAccountHandle mPhoneAccountHandle;
- protected final short mApplicationPort;
- protected final String mDestinationNumber;
-
-
- public OmtpMessageSender(Context context, PhoneAccountHandle phoneAccountHandle,
- short applicationPort,
- String destinationNumber) {
- mContext = context;
- mPhoneAccountHandle = phoneAccountHandle;
- mApplicationPort = applicationPort;
- mDestinationNumber = destinationNumber;
- }
-
- /**
- * Sends a request to the VVM server to activate VVM for the current subscriber.
- *
- * @param sentIntent If not NULL this PendingIntent is broadcast when the message is
- * successfully sent, or failed.
- */
- public void requestVvmActivation(@Nullable PendingIntent sentIntent) {}
-
- /**
- * Sends a request to the VVM server to deactivate VVM for the current subscriber.
- *
- * @param sentIntent If not NULL this PendingIntent is broadcast when the message is
- * successfully sent, or failed.
- */
- public void requestVvmDeactivation(@Nullable PendingIntent sentIntent) {}
-
- /**
- * Send a request to the VVM server to get account status of the current subscriber.
- *
- * @param sentIntent If not NULL this PendingIntent is broadcast when the message is
- * successfully sent, or failed.
- */
- public void requestVvmStatus(@Nullable PendingIntent sentIntent) {}
-
- protected void sendSms(String text, PendingIntent sentIntent) {
- VisualVoicemailService
- .sendVisualVoicemailSms(mContext, mPhoneAccountHandle, mDestinationNumber,
- mApplicationPort, text, sentIntent);
- }
-
- protected void appendField(StringBuilder sb, String field, Object value) {
- sb.append(field).append(OmtpConstants.SMS_KEY_VALUE_SEPARATOR).append(value);
- }
-}
diff --git a/java/com/android/voicemailomtp/sms/OmtpStandardMessageSender.java b/java/com/android/voicemailomtp/sms/OmtpStandardMessageSender.java
deleted file mode 100644
index aa8374781..000000000
--- a/java/com/android/voicemailomtp/sms/OmtpStandardMessageSender.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright (C) 2015 Google Inc. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.sms;
-
-import android.app.PendingIntent;
-import android.content.Context;
-import android.support.annotation.Nullable;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.SmsManager;
-import android.text.TextUtils;
-import com.android.voicemailomtp.OmtpConstants;
-
-/**
- * A implementation of the OmtpMessageSender using the standard OMTP sms protocol.
- */
-public class OmtpStandardMessageSender extends OmtpMessageSender {
- private final String mClientType;
- private final String mProtocolVersion;
- private final String mClientPrefix;
-
- /**
- * Creates a new instance of OmtpStandardMessageSender.
- *
- * @param applicationPort If set to a value > 0 then a binary sms is sent to this port number.
- * Otherwise, a standard text SMS is sent.
- * @param destinationNumber Destination number to be used.
- * @param clientType The "ct" field to be set in the MO message. This is the value used by the
- * VVM server to identify the client. Certain VVM servers require a specific agreed
- * value for this field.
- * @param protocolVersion OMTP protocol version.
- * @param clientPrefix The client prefix requested to be used by the server in its MT messages.
- */
- public OmtpStandardMessageSender(Context context, PhoneAccountHandle phoneAccountHandle,
- short applicationPort,
- String destinationNumber, String clientType, String protocolVersion,
- String clientPrefix) {
- super(context, phoneAccountHandle, applicationPort, destinationNumber);
- mClientType = clientType;
- mProtocolVersion = protocolVersion;
- mClientPrefix = clientPrefix;
- }
-
- // Activate message:
- // V1.1: Activate:pv=<value>;ct=<value>
- // V1.2: Activate:pv=<value>;ct=<value>;pt=<value>;<Clientprefix>
- // V1.3: Activate:pv=<value>;ct=<value>;pt=<value>;<Clientprefix>
- @Override
- public void requestVvmActivation(@Nullable PendingIntent sentIntent) {
- StringBuilder sb = new StringBuilder().append(OmtpConstants.ACTIVATE_REQUEST);
-
- appendProtocolVersionAndClientType(sb);
- if (TextUtils.equals(mProtocolVersion, OmtpConstants.PROTOCOL_VERSION1_2) ||
- TextUtils.equals(mProtocolVersion, OmtpConstants.PROTOCOL_VERSION1_3)) {
- appendApplicationPort(sb);
- appendClientPrefix(sb);
- }
-
- sendSms(sb.toString(), sentIntent);
- }
-
- // Deactivate message:
- // V1.1: Deactivate:pv=<value>;ct=<string>
- // V1.2: Deactivate:pv=<value>;ct=<string>
- // V1.3: Deactivate:pv=<value>;ct=<string>
- @Override
- public void requestVvmDeactivation(@Nullable PendingIntent sentIntent) {
- StringBuilder sb = new StringBuilder().append(OmtpConstants.DEACTIVATE_REQUEST);
- appendProtocolVersionAndClientType(sb);
-
- sendSms(sb.toString(), sentIntent);
- }
-
- // Status message:
- // V1.1: STATUS
- // V1.2: STATUS
- // V1.3: STATUS:pv=<value>;ct=<value>;pt=<value>;<Clientprefix>
- @Override
- public void requestVvmStatus(@Nullable PendingIntent sentIntent) {
- StringBuilder sb = new StringBuilder().append(OmtpConstants.STATUS_REQUEST);
-
- if (TextUtils.equals(mProtocolVersion, OmtpConstants.PROTOCOL_VERSION1_3)) {
- appendProtocolVersionAndClientType(sb);
- appendApplicationPort(sb);
- appendClientPrefix(sb);
- }
-
- sendSms(sb.toString(), sentIntent);
- }
-
- private void appendProtocolVersionAndClientType(StringBuilder sb) {
- sb.append(OmtpConstants.SMS_PREFIX_SEPARATOR);
- appendField(sb, OmtpConstants.PROTOCOL_VERSION, mProtocolVersion);
- sb.append(OmtpConstants.SMS_FIELD_SEPARATOR);
- appendField(sb, OmtpConstants.CLIENT_TYPE, mClientType);
- }
-
- private void appendApplicationPort(StringBuilder sb) {
- sb.append(OmtpConstants.SMS_FIELD_SEPARATOR);
- appendField(sb, OmtpConstants.APPLICATION_PORT, mApplicationPort);
- }
-
- private void appendClientPrefix(StringBuilder sb) {
- sb.append(OmtpConstants.SMS_FIELD_SEPARATOR);
- sb.append(mClientPrefix);
- }
-}
diff --git a/java/com/android/voicemailomtp/sms/StatusMessage.java b/java/com/android/voicemailomtp/sms/StatusMessage.java
deleted file mode 100644
index 3dfd4973e..000000000
--- a/java/com/android/voicemailomtp/sms/StatusMessage.java
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.sms;
-
-import android.os.Bundle;
-import com.android.voicemailomtp.NeededForTesting;
-import com.android.voicemailomtp.OmtpConstants;
-import com.android.voicemailomtp.VisualVoicemailPreferences;
-import com.android.voicemailomtp.VvmLog;
-
-/**
- * Structured data representation of OMTP STATUS message.
- *
- * The getters will return null if the field was not set in the message body or it could not be
- * parsed.
- */
-public class StatusMessage {
- // NOTE: Following Status SMS fields are not yet parsed, as they do not seem
- // to be useful for initial omtp source implementation.
- // lang, g_len, vs_len, pw_len, pm, gm, vtc, vt
-
- private final String mProvisioningStatus;
- private final String mStatusReturnCode;
- private final String mSubscriptionUrl;
- private final String mServerAddress;
- private final String mTuiAccessNumber;
- private final String mClientSmsDestinationNumber;
- private final String mImapPort;
- private final String mImapUserName;
- private final String mImapPassword;
- private final String mSmtpPort;
- private final String mSmtpUserName;
- private final String mSmtpPassword;
- private final String mTuiPasswordLength;
-
- @Override
- public String toString() {
- return "StatusMessage [mProvisioningStatus=" + mProvisioningStatus
- + ", mStatusReturnCode=" + mStatusReturnCode
- + ", mSubscriptionUrl=" + mSubscriptionUrl
- + ", mServerAddress=" + mServerAddress
- + ", mTuiAccessNumber=" + mTuiAccessNumber
- + ", mClientSmsDestinationNumber=" + mClientSmsDestinationNumber
- + ", mImapPort=" + mImapPort
- + ", mImapUserName=" + mImapUserName
- + ", mImapPassword=" + VvmLog.pii(mImapPassword)
- + ", mSmtpPort=" + mSmtpPort
- + ", mSmtpUserName=" + mSmtpUserName
- + ", mSmtpPassword=" + VvmLog.pii(mSmtpPassword)
- + ", mTuiPasswordLength=" + mTuiPasswordLength + "]";
- }
-
- public StatusMessage(Bundle wrappedData) {
- mProvisioningStatus = unquote(getString(wrappedData, OmtpConstants.PROVISIONING_STATUS));
- mStatusReturnCode = getString(wrappedData, OmtpConstants.RETURN_CODE);
- mSubscriptionUrl = getString(wrappedData, OmtpConstants.SUBSCRIPTION_URL);
- mServerAddress = getString(wrappedData, OmtpConstants.SERVER_ADDRESS);
- mTuiAccessNumber = getString(wrappedData, OmtpConstants.TUI_ACCESS_NUMBER);
- mClientSmsDestinationNumber = getString(wrappedData,
- OmtpConstants.CLIENT_SMS_DESTINATION_NUMBER);
- mImapPort = getString(wrappedData, OmtpConstants.IMAP_PORT);
- mImapUserName = getString(wrappedData, OmtpConstants.IMAP_USER_NAME);
- mImapPassword = getString(wrappedData, OmtpConstants.IMAP_PASSWORD);
- mSmtpPort = getString(wrappedData, OmtpConstants.SMTP_PORT);
- mSmtpUserName = getString(wrappedData, OmtpConstants.SMTP_USER_NAME);
- mSmtpPassword = getString(wrappedData, OmtpConstants.SMTP_PASSWORD);
- mTuiPasswordLength = getString(wrappedData, OmtpConstants.TUI_PASSWORD_LENGTH);
- }
-
- private static String unquote(String string) {
- if (string.length() < 2) {
- return string;
- }
- if (string.startsWith("\"") && string.endsWith("\"")) {
- return string.substring(1, string.length() - 1);
- }
- return string;
- }
-
- /**
- * @return the subscriber's VVM provisioning status.
- */
- public String getProvisioningStatus() {
- return mProvisioningStatus;
- }
-
- /**
- * @return the return-code of the status SMS.
- */
- public String getReturnCode() {
- return mStatusReturnCode;
- }
-
- /**
- * @return the URL of the voicemail server. This is the URL to send the users to for subscribing
- * to the visual voicemail service.
- */
- @NeededForTesting
- public String getSubscriptionUrl() {
- return mSubscriptionUrl;
- }
-
- /**
- * @return the voicemail server address. Either server IP address or fully qualified domain
- * name.
- */
- public String getServerAddress() {
- return mServerAddress;
- }
-
- /**
- * @return the Telephony User Interface number to call to access voicemails directly from the
- * IVR.
- */
- @NeededForTesting
- public String getTuiAccessNumber() {
- return mTuiAccessNumber;
- }
-
- /**
- * @return the number to which client originated SMSes should be sent to.
- */
- @NeededForTesting
- public String getClientSmsDestinationNumber() {
- return mClientSmsDestinationNumber;
- }
-
- /**
- * @return the IMAP server port to talk to.
- */
- public String getImapPort() {
- return mImapPort;
- }
-
- /**
- * @return the IMAP user name to be used for authentication.
- */
- public String getImapUserName() {
- return mImapUserName;
- }
-
- /**
- * @return the IMAP password to be used for authentication.
- */
- public String getImapPassword() {
- return mImapPassword;
- }
-
- /**
- * @return the SMTP server port to talk to.
- */
- @NeededForTesting
- public String getSmtpPort() {
- return mSmtpPort;
- }
-
- /**
- * @return the SMTP user name to be used for SMTP authentication.
- */
- @NeededForTesting
- public String getSmtpUserName() {
- return mSmtpUserName;
- }
-
- /**
- * @return the SMTP password to be used for SMTP authentication.
- */
- @NeededForTesting
- public String getSmtpPassword() {
- return mSmtpPassword;
- }
-
- public String getTuiPasswordLength() {
- return mTuiPasswordLength;
- }
-
- private static String getString(Bundle bundle, String key) {
- String value = bundle.getString(key);
- if (value == null) {
- return "";
- }
- return value;
- }
-
- /**
- * Saves a StatusMessage to the {@link VisualVoicemailPreferences}. Not all fields are saved.
- */
- public VisualVoicemailPreferences.Editor putStatus(VisualVoicemailPreferences.Editor editor) {
- return editor
- .putString(OmtpConstants.IMAP_PORT, getImapPort())
- .putString(OmtpConstants.SERVER_ADDRESS, getServerAddress())
- .putString(OmtpConstants.IMAP_USER_NAME, getImapUserName())
- .putString(OmtpConstants.IMAP_PASSWORD, getImapPassword())
- .putString(OmtpConstants.TUI_PASSWORD_LENGTH, getTuiPasswordLength());
- }
-} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/sms/StatusSmsFetcher.java b/java/com/android/voicemailomtp/sms/StatusSmsFetcher.java
deleted file mode 100644
index 4e10c0e43..000000000
--- a/java/com/android/voicemailomtp/sms/StatusSmsFetcher.java
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.sms;
-
-import android.annotation.TargetApi;
-import android.app.Activity;
-import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.Build.VERSION_CODES;
-import android.os.Bundle;
-import android.support.annotation.MainThread;
-import android.support.annotation.Nullable;
-import android.support.annotation.WorkerThread;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.SmsManager;
-import android.telephony.VisualVoicemailSms;
-import com.android.voicemailomtp.Assert;
-import com.android.voicemailomtp.OmtpConstants;
-import com.android.voicemailomtp.OmtpService;
-import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
-import com.android.voicemailomtp.VvmLog;
-import com.android.voicemailomtp.protocol.VisualVoicemailProtocol;
-import java.io.Closeable;
-import java.io.IOException;
-import java.util.concurrent.CancellationException;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-/** Intercepts a incoming STATUS SMS with a blocking call. */
-@SuppressWarnings("AndroidApiChecker") /* CompletableFuture is java8*/
-@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
-public class StatusSmsFetcher extends BroadcastReceiver implements Closeable {
-
- private static final String TAG = "VvmStatusSmsFetcher";
-
- private static final long STATUS_SMS_TIMEOUT_MILLIS = 60_000;
-
- private static final String ACTION_REQUEST_SENT_INTENT
- = "com.android.voicemailomtp.sms.REQUEST_SENT";
- private static final int ACTION_REQUEST_SENT_REQUEST_CODE = 0;
-
- private CompletableFuture<Bundle> mFuture = new CompletableFuture<>();
-
- private final Context mContext;
- private final PhoneAccountHandle mPhoneAccountHandle;
-
- public StatusSmsFetcher(Context context, PhoneAccountHandle phoneAccountHandle) {
- mContext = context;
- mPhoneAccountHandle = phoneAccountHandle;
- IntentFilter filter = new IntentFilter(ACTION_REQUEST_SENT_INTENT);
- filter.addAction(OmtpService.ACTION_SMS_RECEIVED);
- context.registerReceiver(this, filter);
- }
-
- @Override
- public void close() throws IOException {
- mContext.unregisterReceiver(this);
- }
-
- @WorkerThread
- @Nullable
- public Bundle get() throws InterruptedException, ExecutionException, TimeoutException,
- CancellationException {
- Assert.isNotMainThread();
- return mFuture.get(STATUS_SMS_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
- }
-
- public PendingIntent getSentIntent() {
- Intent intent = new Intent(ACTION_REQUEST_SENT_INTENT);
- intent.setPackage(mContext.getPackageName());
- // Because the receiver is registered dynamically, implicit intent must be used.
- // There should only be a single status SMS request at a time.
- return PendingIntent.getBroadcast(mContext, ACTION_REQUEST_SENT_REQUEST_CODE, intent,
- PendingIntent.FLAG_CANCEL_CURRENT);
- }
-
- @Override
- @MainThread
- public void onReceive(Context context, Intent intent) {
- Assert.isMainThread();
- if (ACTION_REQUEST_SENT_INTENT.equals(intent.getAction())) {
- int resultCode = getResultCode();
-
- if (resultCode == Activity.RESULT_OK) {
- VvmLog.d(TAG, "Request SMS successfully sent");
- return;
- }
-
- VvmLog.e(TAG, "Request SMS send failed: " + sentSmsResultToString(resultCode));
- mFuture.cancel(true);
- return;
- }
-
- VisualVoicemailSms sms = intent.getExtras().getParcelable(OmtpService.EXTRA_VOICEMAIL_SMS);
-
- if (!mPhoneAccountHandle.equals(sms.getPhoneAccountHandle())) {
- return;
- }
- String eventType = sms.getPrefix();
-
- if (eventType.equals(OmtpConstants.STATUS_SMS_PREFIX)) {
- mFuture.complete(sms.getFields());
- return;
- }
-
- if (eventType.equals(OmtpConstants.SYNC_SMS_PREFIX)) {
- return;
- }
-
- VvmLog.i(TAG, "VVM SMS with event " + eventType
- + " received, attempting to translate to STATUS SMS");
- OmtpVvmCarrierConfigHelper helper = new OmtpVvmCarrierConfigHelper(context,
- mPhoneAccountHandle);
- VisualVoicemailProtocol protocol = helper.getProtocol();
- if (protocol == null) {
- return;
- }
- Bundle translatedBundle = protocol.translateStatusSmsBundle(helper, eventType,
- sms.getFields());
-
- if (translatedBundle != null) {
- VvmLog.i(TAG, "Translated to STATUS SMS");
- mFuture.complete(translatedBundle);
- }
- }
-
- private static String sentSmsResultToString(int resultCode) {
- switch (resultCode) {
- case Activity.RESULT_OK:
- return "OK";
- case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
- return "RESULT_ERROR_GENERIC_FAILURE";
- case SmsManager.RESULT_ERROR_NO_SERVICE:
- return "RESULT_ERROR_GENERIC_FAILURE";
- case SmsManager.RESULT_ERROR_NULL_PDU:
- return "RESULT_ERROR_GENERIC_FAILURE";
- case SmsManager.RESULT_ERROR_RADIO_OFF:
- return "RESULT_ERROR_GENERIC_FAILURE";
- default:
- return "UNKNOWN CODE: " + resultCode;
- }
- }
-}
diff --git a/java/com/android/voicemailomtp/sms/SyncMessage.java b/java/com/android/voicemailomtp/sms/SyncMessage.java
deleted file mode 100644
index 89cfc0f19..000000000
--- a/java/com/android/voicemailomtp/sms/SyncMessage.java
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.sms;
-
-import android.os.Bundle;
-import android.support.annotation.Nullable;
-import com.android.voicemailomtp.NeededForTesting;
-import com.android.voicemailomtp.OmtpConstants;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.Locale;
-
-/**
- * Structured data representation of an OMTP SYNC message.
- *
- * Getters will return null if the field was not set in the message body or it could not be parsed.
- */
-public class SyncMessage {
- // Sync event that triggered this message.
- private final String mSyncTriggerEvent;
- // Total number of new messages on the server.
- private final int mNewMessageCount;
- // UID of the new message.
- private final String mMessageId;
- // Length of the message.
- private final int mMessageLength;
- // Content type (voice, video, fax...) of the new message.
- private final String mContentType;
- // Sender of the new message.
- private final String mSender;
- // Timestamp (in millis) of the new message.
- private final long mMsgTimeMillis;
-
- @Override
- public String toString() {
- return "SyncMessage [mSyncTriggerEvent=" + mSyncTriggerEvent
- + ", mNewMessageCount=" + mNewMessageCount
- + ", mMessageId=" + mMessageId
- + ", mMessageLength=" + mMessageLength
- + ", mContentType=" + mContentType
- + ", mSender=" + mSender
- + ", mMsgTimeMillis=" + mMsgTimeMillis + "]";
- }
-
- public SyncMessage(Bundle wrappedData) {
- mSyncTriggerEvent = getString(wrappedData, OmtpConstants.SYNC_TRIGGER_EVENT);
- mMessageId = getString(wrappedData, OmtpConstants.MESSAGE_UID);
- mMessageLength = getInt(wrappedData, OmtpConstants.MESSAGE_LENGTH);
- mContentType = getString(wrappedData, OmtpConstants.CONTENT_TYPE);
- mSender = getString(wrappedData, OmtpConstants.SENDER);
- mNewMessageCount = getInt(wrappedData, OmtpConstants.NUM_MESSAGE_COUNT);
- mMsgTimeMillis = parseTime(wrappedData.getString(OmtpConstants.TIME));
- }
-
- private static long parseTime(@Nullable String value) {
- if (value == null) {
- return 0L;
- }
- try {
- return new SimpleDateFormat(
- OmtpConstants.DATE_TIME_FORMAT, Locale.US)
- .parse(value).getTime();
- } catch (ParseException e) {
- return 0L;
- }
- }
- /**
- * @return the event that triggered the sync message. This is a mandatory field and must always
- * be set.
- */
- public String getSyncTriggerEvent() {
- return mSyncTriggerEvent;
- }
-
- /**
- * @return the number of new messages stored on the voicemail server.
- */
- @NeededForTesting
- public int getNewMessageCount() {
- return mNewMessageCount;
- }
-
- /**
- * @return the message ID of the new message.
- * <p>
- * Expected to be set only for
- * {@link com.android.voicemailomtp.OmtpConstants#NEW_MESSAGE}
- */
- public String getId() {
- return mMessageId;
- }
-
- /**
- * @return the content type of the new message.
- * <p>
- * Expected to be set only for
- * {@link com.android.voicemailomtp.OmtpConstants#NEW_MESSAGE}
- */
- @NeededForTesting
- public String getContentType() {
- return mContentType;
- }
-
- /**
- * @return the message length of the new message.
- * <p>
- * Expected to be set only for
- * {@link com.android.voicemailomtp.OmtpConstants#NEW_MESSAGE}
- */
- public int getLength() {
- return mMessageLength;
- }
-
- /**
- * @return the sender's phone number of the new message specified as MSISDN.
- * <p>
- * Expected to be set only for
- * {@link com.android.voicemailomtp.OmtpConstants#NEW_MESSAGE}
- */
- public String getSender() {
- return mSender;
- }
-
- /**
- * @return the timestamp as milliseconds for the new message.
- * <p>
- * Expected to be set only for
- * {@link com.android.voicemailomtp.OmtpConstants#NEW_MESSAGE}
- */
- public long getTimestampMillis() {
- return mMsgTimeMillis;
- }
-
- private static int getInt(Bundle wrappedData, String key) {
- String value = wrappedData.getString(key);
- if (value == null) {
- return 0;
- }
- try {
- return Integer.parseInt(value);
- } catch (NumberFormatException e) {
- return 0;
- }
- }
-
- private static String getString(Bundle wrappedData, String key) {
- String value = wrappedData.getString(key);
- if (value == null) {
- return "";
- }
- return value;
- }
-} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/sms/Vvm3MessageSender.java b/java/com/android/voicemailomtp/sms/Vvm3MessageSender.java
deleted file mode 100644
index 02e465967..000000000
--- a/java/com/android/voicemailomtp/sms/Vvm3MessageSender.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.sms;
-
-import android.app.PendingIntent;
-import android.content.Context;
-import android.support.annotation.Nullable;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.SmsManager;
-
-public class Vvm3MessageSender extends OmtpMessageSender {
-
- /**
- * Creates a new instance of Vvm3MessageSender.
- *
- * @param applicationPort If set to a value > 0 then a binary sms is sent to this port number.
- * Otherwise, a standard text SMS is sent.
- */
- public Vvm3MessageSender(Context context, PhoneAccountHandle phoneAccountHandle,
- short applicationPort, String destinationNumber) {
- super(context, phoneAccountHandle, applicationPort, destinationNumber);
- }
-
- @Override
- public void requestVvmActivation(@Nullable PendingIntent sentIntent) {
- // Activation not supported for VVM3, send a status request instead.
- requestVvmStatus(sentIntent);
- }
-
- @Override
- public void requestVvmDeactivation(@Nullable PendingIntent sentIntent) {
- // Deactivation not supported for VVM3, do nothing
- }
-
-
- @Override
- public void requestVvmStatus(@Nullable PendingIntent sentIntent) {
- // Status message:
- // STATUS
- StringBuilder sb = new StringBuilder().append("STATUS");
- sendSms(sb.toString(), sentIntent);
- }
-}
diff --git a/java/com/android/voicemailomtp/sync/OmtpVvmSourceManager.java b/java/com/android/voicemailomtp/sync/OmtpVvmSourceManager.java
deleted file mode 100644
index ad3c025cf..000000000
--- a/java/com/android/voicemailomtp/sync/OmtpVvmSourceManager.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.sync;
-
-import android.content.Context;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.PhoneStateListener;
-import android.telephony.TelephonyManager;
-
-import com.android.voicemailomtp.VoicemailStatus;
-import com.android.voicemailomtp.VvmPhoneStateListener;
-
-import java.util.Collections;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-
-/**
- * A singleton class designed to remember the active OMTP visual voicemail sources. Because a
- * voicemail source is tied 1:1 to a phone account, the phone account handle is used as the key
- * for each voicemail source and the associated data.
- */
-public class OmtpVvmSourceManager {
- public static final String TAG = "OmtpVvmSourceManager";
-
- private static OmtpVvmSourceManager sInstance = new OmtpVvmSourceManager();
-
- private Context mContext;
- private TelephonyManager mTelephonyManager;
- // Each phone account is associated with a phone state listener for updates to whether the
- // device is able to sync.
- private Set<PhoneAccountHandle> mActiveVvmSources;
- private Map<PhoneAccountHandle, PhoneStateListener> mPhoneStateListenerMap;
-
- /**
- * Private constructor. Instance should only be acquired through getInstance().
- */
- private OmtpVvmSourceManager() {}
-
- public static OmtpVvmSourceManager getInstance(Context context) {
- sInstance.setup(context);
- return sInstance;
- }
-
- /**
- * Set the context and system services so they do not need to be retrieved every time.
- * @param context The context to get the subscription and telephony manager for.
- */
- private void setup(Context context) {
- if (mContext == null) {
- mContext = context;
- mTelephonyManager = (TelephonyManager)
- mContext.getSystemService(Context.TELEPHONY_SERVICE);
- mActiveVvmSources = Collections.newSetFromMap(
- new ConcurrentHashMap<PhoneAccountHandle, Boolean>(8, 0.9f, 1));
- mPhoneStateListenerMap =
- new ConcurrentHashMap<PhoneAccountHandle, PhoneStateListener>(8, 0.9f, 1);
- }
- }
-
- public void addSource(PhoneAccountHandle phoneAccount) {
- mActiveVvmSources.add(phoneAccount);
- }
-
- public void removeSource(PhoneAccountHandle phoneAccount) {
- // TODO: should use OmtpVvmCarrierConfigHelper to handle the event. But currently it
- // couldn't handle events on removed SIMs
- VoicemailStatus.disable(mContext, phoneAccount);
- removePhoneStateListener(phoneAccount);
- mActiveVvmSources.remove(phoneAccount);
- }
-
- public void addPhoneStateListener(PhoneAccountHandle phoneAccount) {
- if (!mPhoneStateListenerMap.containsKey(phoneAccount)) {
- VvmPhoneStateListener phoneStateListener = new VvmPhoneStateListener(mContext,
- phoneAccount);
- mPhoneStateListenerMap.put(phoneAccount, phoneStateListener);
- mTelephonyManager.createForPhoneAccountHandle(phoneAccount)
- .listen(phoneStateListener, PhoneStateListener.LISTEN_SERVICE_STATE);
- }
- }
-
- public void removePhoneStateListener(PhoneAccountHandle phoneAccount) {
- PhoneStateListener phoneStateListener =
- mPhoneStateListenerMap.remove(phoneAccount);
- mTelephonyManager.createForPhoneAccountHandle(phoneAccount).listen(phoneStateListener, 0);
- }
-
- public Set<PhoneAccountHandle> getOmtpVvmSources() {
- return mActiveVvmSources;
- }
-
- /**
- * Check if a certain account is registered.
- *
- * @param phoneAccount The account to look for.
- * @return {@code true} if the account is in the list of registered OMTP voicemail sources.
- * {@code false} otherwise.
- */
- public boolean isVvmSourceRegistered(PhoneAccountHandle phoneAccount) {
- if (phoneAccount == null) {
- return false;
- }
-
- return mActiveVvmSources.contains(phoneAccount);
- }
-} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/sync/OmtpVvmSyncReceiver.java b/java/com/android/voicemailomtp/sync/OmtpVvmSyncReceiver.java
deleted file mode 100644
index 971a1c5a8..000000000
--- a/java/com/android/voicemailomtp/sync/OmtpVvmSyncReceiver.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.sync;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.provider.VoicemailContract;
-import android.telecom.PhoneAccountHandle;
-import android.telecom.TelecomManager;
-
-import com.android.voicemailomtp.ActivationTask;
-import com.android.voicemailomtp.VvmLog;
-import com.android.voicemailomtp.settings.VisualVoicemailSettingsUtil;
-
-import java.util.List;
-
-public class OmtpVvmSyncReceiver extends BroadcastReceiver {
-
- private static final String TAG = "OmtpVvmSyncReceiver";
-
- @Override
- public void onReceive(final Context context, Intent intent) {
- if (VoicemailContract.ACTION_SYNC_VOICEMAIL.equals(intent.getAction())) {
- VvmLog.v(TAG, "Sync intent received");
- for (PhoneAccountHandle source : OmtpVvmSourceManager.getInstance(context)
- .getOmtpVvmSources()) {
- SyncTask.start(context, source, OmtpVvmSyncService.SYNC_FULL_SYNC);
- }
- activateUnactivatedAccounts(context);
- }
- }
-
- private static void activateUnactivatedAccounts(Context context) {
- List<PhoneAccountHandle> accounts =
- context.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts();
- for (PhoneAccountHandle phoneAccount : accounts) {
- if (!VisualVoicemailSettingsUtil.isEnabled(context, phoneAccount)) {
- continue;
- }
- if (!OmtpVvmSourceManager.getInstance(context).isVvmSourceRegistered(phoneAccount)) {
- VvmLog.i(TAG, "Unactivated account " + phoneAccount + " found, activating");
- ActivationTask.start(context, phoneAccount, null);
- }
- }
- }
-}
diff --git a/java/com/android/voicemailomtp/sync/OmtpVvmSyncService.java b/java/com/android/voicemailomtp/sync/OmtpVvmSyncService.java
deleted file mode 100644
index a3418cc28..000000000
--- a/java/com/android/voicemailomtp/sync/OmtpVvmSyncService.java
+++ /dev/null
@@ -1,278 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.sync;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.net.Network;
-import android.net.Uri;
-import android.os.Build.VERSION_CODES;
-import android.telecom.PhoneAccountHandle;
-import android.text.TextUtils;
-import com.android.voicemailomtp.ActivationTask;
-import com.android.voicemailomtp.Assert;
-import com.android.voicemailomtp.OmtpEvents;
-import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
-import com.android.voicemailomtp.Voicemail;
-import com.android.voicemailomtp.VoicemailStatus;
-import com.android.voicemailomtp.VvmLog;
-import com.android.voicemailomtp.fetch.VoicemailFetchedCallback;
-import com.android.voicemailomtp.imap.ImapHelper;
-import com.android.voicemailomtp.imap.ImapHelper.InitializingException;
-import com.android.voicemailomtp.scheduling.BaseTask;
-import com.android.voicemailomtp.settings.VisualVoicemailSettingsUtil;
-import com.android.voicemailomtp.sync.VvmNetworkRequest.NetworkWrapper;
-import com.android.voicemailomtp.sync.VvmNetworkRequest.RequestFailedException;
-import com.android.voicemailomtp.utils.VoicemailDatabaseUtil;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/** Sync OMTP visual voicemail. */
-@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
-public class OmtpVvmSyncService {
-
- private static final String TAG = OmtpVvmSyncService.class.getSimpleName();
-
- /**
- * Signifies a sync with both uploading to the server and downloading from the server.
- */
- public static final String SYNC_FULL_SYNC = "full_sync";
- /**
- * Only upload to the server.
- */
- public static final String SYNC_UPLOAD_ONLY = "upload_only";
- /**
- * Only download from the server.
- */
- public static final String SYNC_DOWNLOAD_ONLY = "download_only";
- /**
- * Only download single voicemail transcription.
- */
- public static final String SYNC_DOWNLOAD_ONE_TRANSCRIPTION =
- "download_one_transcription";
-
- private final Context mContext;
-
- // Record the timestamp of the last full sync so that duplicate syncs can be reduced.
- private static final String LAST_FULL_SYNC_TIMESTAMP = "last_full_sync_timestamp";
- // Constant indicating that there has never been a full sync.
- public static final long NO_PRIOR_FULL_SYNC = -1;
-
- private VoicemailsQueryHelper mQueryHelper;
-
- public OmtpVvmSyncService(Context context) {
- mContext = context;
- mQueryHelper = new VoicemailsQueryHelper(mContext);
- }
-
- public void sync(BaseTask task, String action, PhoneAccountHandle phoneAccount,
- Voicemail voicemail, VoicemailStatus.Editor status) {
- Assert.isTrue(phoneAccount != null);
- VvmLog.v(TAG, "Sync requested: " + action + " - for account: " + phoneAccount);
- setupAndSendRequest(task, phoneAccount, voicemail, action, status);
- }
-
- private void setupAndSendRequest(BaseTask task, PhoneAccountHandle phoneAccount,
- Voicemail voicemail, String action, VoicemailStatus.Editor status) {
- if (!VisualVoicemailSettingsUtil.isEnabled(mContext, phoneAccount)) {
- VvmLog.v(TAG, "Sync requested for disabled account");
- return;
- }
- if (!OmtpVvmSourceManager.getInstance(mContext).isVvmSourceRegistered(phoneAccount)) {
- ActivationTask.start(mContext, phoneAccount, null);
- return;
- }
-
- OmtpVvmCarrierConfigHelper config = new OmtpVvmCarrierConfigHelper(mContext, phoneAccount);
- // DATA_IMAP_OPERATION_STARTED posting should not be deferred. This event clears all data
- // channel errors, which should happen when the task starts, not when it ends. It is the
- // "Sync in progress..." status.
- config.handleEvent(VoicemailStatus.edit(mContext, phoneAccount),
- OmtpEvents.DATA_IMAP_OPERATION_STARTED);
- try (NetworkWrapper network = VvmNetworkRequest.getNetwork(config, phoneAccount, status)) {
- if (network == null) {
- VvmLog.e(TAG, "unable to acquire network");
- task.fail();
- return;
- }
- doSync(task, network.get(), phoneAccount, voicemail, action, status);
- } catch (RequestFailedException e) {
- config.handleEvent(status, OmtpEvents.DATA_NO_CONNECTION_CELLULAR_REQUIRED);
- task.fail();
- }
- }
-
- private void doSync(BaseTask task, Network network, PhoneAccountHandle phoneAccount,
- Voicemail voicemail, String action, VoicemailStatus.Editor status) {
- try (ImapHelper imapHelper = new ImapHelper(mContext, phoneAccount, network, status)) {
- boolean success;
- if (voicemail == null) {
- success = syncAll(action, imapHelper, phoneAccount);
- } else {
- success = syncOne(imapHelper, voicemail, phoneAccount);
- }
- if (success) {
- // TODO: b/30569269 failure should interrupt all subsequent task via exceptions
- imapHelper.updateQuota();
- imapHelper.handleEvent(OmtpEvents.DATA_IMAP_OPERATION_COMPLETED);
- } else {
- task.fail();
- }
- } catch (InitializingException e) {
- VvmLog.w(TAG, "Can't retrieve Imap credentials.", e);
- return;
- }
- }
-
- private boolean syncAll(String action, ImapHelper imapHelper, PhoneAccountHandle account) {
- boolean uploadSuccess = true;
- boolean downloadSuccess = true;
-
- if (SYNC_FULL_SYNC.equals(action) || SYNC_UPLOAD_ONLY.equals(action)) {
- uploadSuccess = upload(imapHelper);
- }
- if (SYNC_FULL_SYNC.equals(action) || SYNC_DOWNLOAD_ONLY.equals(action)) {
- downloadSuccess = download(imapHelper, account);
- }
-
- VvmLog.v(TAG, "upload succeeded: [" + String.valueOf(uploadSuccess)
- + "] download succeeded: [" + String.valueOf(downloadSuccess) + "]");
-
- return uploadSuccess && downloadSuccess;
- }
-
- private boolean syncOne(ImapHelper imapHelper, Voicemail voicemail,
- PhoneAccountHandle account) {
- if (shouldPerformPrefetch(account, imapHelper)) {
- VoicemailFetchedCallback callback = new VoicemailFetchedCallback(mContext,
- voicemail.getUri(), account);
- imapHelper.fetchVoicemailPayload(callback, voicemail.getSourceData());
- }
-
- return imapHelper.fetchTranscription(
- new TranscriptionFetchedCallback(mContext, voicemail),
- voicemail.getSourceData());
- }
-
- private boolean upload(ImapHelper imapHelper) {
- List<Voicemail> readVoicemails = mQueryHelper.getReadVoicemails();
- List<Voicemail> deletedVoicemails = mQueryHelper.getDeletedVoicemails();
-
- boolean success = true;
-
- if (deletedVoicemails.size() > 0) {
- if (imapHelper.markMessagesAsDeleted(deletedVoicemails)) {
- // We want to delete selectively instead of all the voicemails for this provider
- // in case the state changed since the IMAP query was completed.
- mQueryHelper.deleteFromDatabase(deletedVoicemails);
- } else {
- success = false;
- }
- }
-
- if (readVoicemails.size() > 0) {
- if (imapHelper.markMessagesAsRead(readVoicemails)) {
- mQueryHelper.markCleanInDatabase(readVoicemails);
- } else {
- success = false;
- }
- }
-
- return success;
- }
-
- private boolean download(ImapHelper imapHelper, PhoneAccountHandle account) {
- List<Voicemail> serverVoicemails = imapHelper.fetchAllVoicemails();
- List<Voicemail> localVoicemails = mQueryHelper.getAllVoicemails();
-
- if (localVoicemails == null || serverVoicemails == null) {
- // Null value means the query failed.
- return false;
- }
-
- Map<String, Voicemail> remoteMap = buildMap(serverVoicemails);
-
- // Go through all the local voicemails and check if they are on the server.
- // They may be read or deleted on the server but not locally. Perform the
- // appropriate local operation if the status differs from the server. Remove
- // the messages that exist both locally and on the server to know which server
- // messages to insert locally.
- for (int i = 0; i < localVoicemails.size(); i++) {
- Voicemail localVoicemail = localVoicemails.get(i);
- Voicemail remoteVoicemail = remoteMap.remove(localVoicemail.getSourceData());
- if (remoteVoicemail == null) {
- mQueryHelper.deleteFromDatabase(localVoicemail);
- } else {
- if (remoteVoicemail.isRead() != localVoicemail.isRead()) {
- mQueryHelper.markReadInDatabase(localVoicemail);
- }
-
- if (!TextUtils.isEmpty(remoteVoicemail.getTranscription()) &&
- TextUtils.isEmpty(localVoicemail.getTranscription())) {
- mQueryHelper.updateWithTranscription(localVoicemail,
- remoteVoicemail.getTranscription());
- }
- }
- }
-
- // The leftover messages are messages that exist on the server but not locally.
- boolean prefetchEnabled = shouldPerformPrefetch(account, imapHelper);
- for (Voicemail remoteVoicemail : remoteMap.values()) {
- Uri uri = VoicemailDatabaseUtil.insert(mContext, remoteVoicemail);
- if (prefetchEnabled) {
- VoicemailFetchedCallback fetchedCallback =
- new VoicemailFetchedCallback(mContext, uri, account);
- imapHelper.fetchVoicemailPayload(fetchedCallback, remoteVoicemail.getSourceData());
- }
- }
-
- return true;
- }
-
- private boolean shouldPerformPrefetch(PhoneAccountHandle account, ImapHelper imapHelper) {
- OmtpVvmCarrierConfigHelper carrierConfigHelper =
- new OmtpVvmCarrierConfigHelper(mContext, account);
- return carrierConfigHelper.isPrefetchEnabled() && !imapHelper.isRoaming();
- }
-
- /**
- * Builds a map from provider data to message for the given collection of voicemails.
- */
- private Map<String, Voicemail> buildMap(List<Voicemail> messages) {
- Map<String, Voicemail> map = new HashMap<String, Voicemail>();
- for (Voicemail message : messages) {
- map.put(message.getSourceData(), message);
- }
- return map;
- }
-
- public class TranscriptionFetchedCallback {
-
- private Context mContext;
- private Voicemail mVoicemail;
-
- public TranscriptionFetchedCallback(Context context, Voicemail voicemail) {
- mContext = context;
- mVoicemail = voicemail;
- }
-
- public void setVoicemailTranscription(String transcription) {
- VoicemailsQueryHelper queryHelper = new VoicemailsQueryHelper(mContext);
- queryHelper.updateWithTranscription(mVoicemail, transcription);
- }
- }
-}
diff --git a/java/com/android/voicemailomtp/sync/SyncOneTask.java b/java/com/android/voicemailomtp/sync/SyncOneTask.java
deleted file mode 100644
index 9264e6c08..000000000
--- a/java/com/android/voicemailomtp/sync/SyncOneTask.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.voicemailomtp.sync;
-
-import android.content.Context;
-import android.content.Intent;
-import android.telecom.PhoneAccountHandle;
-
-import com.android.voicemailomtp.Voicemail;
-import com.android.voicemailomtp.VoicemailStatus;
-import com.android.voicemailomtp.scheduling.BaseTask;
-import com.android.voicemailomtp.scheduling.RetryPolicy;
-
-/**
- * Task to download a single voicemail from the server. This task is initiated by a SMS notifying
- * the new voicemail arrival, and ignores the duplicated tasks constraint.
- */
-public class SyncOneTask extends BaseTask {
-
- private static final int RETRY_TIMES = 2;
- private static final int RETRY_INTERVAL_MILLIS = 5_000;
-
- private static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle";
- private static final String EXTRA_SYNC_TYPE = "extra_sync_type";
- private static final String EXTRA_VOICEMAIL = "extra_voicemail";
-
- private PhoneAccountHandle mPhone;
- private String mSyncType;
- private Voicemail mVoicemail;
-
- public static void start(Context context, PhoneAccountHandle phone, Voicemail voicemail) {
- Intent intent = BaseTask
- .createIntent(context, SyncOneTask.class, phone);
- intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, phone);
- intent.putExtra(EXTRA_SYNC_TYPE, OmtpVvmSyncService.SYNC_DOWNLOAD_ONE_TRANSCRIPTION);
- intent.putExtra(EXTRA_VOICEMAIL, voicemail);
- context.startService(intent);
- }
-
- public SyncOneTask() {
- super(TASK_ALLOW_DUPLICATES);
- addPolicy(new RetryPolicy(RETRY_TIMES, RETRY_INTERVAL_MILLIS));
- }
-
- public void onCreate(Context context, Intent intent, int flags, int startId) {
- super.onCreate(context, intent, flags, startId);
- mPhone = intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE);
- mSyncType = intent.getStringExtra(EXTRA_SYNC_TYPE);
- mVoicemail = intent.getParcelableExtra(EXTRA_VOICEMAIL);
- }
-
- @Override
- public void onExecuteInBackgroundThread() {
- OmtpVvmSyncService service = new OmtpVvmSyncService(getContext());
- service.sync(this, mSyncType, mPhone, mVoicemail,
- VoicemailStatus.edit(getContext(), mPhone));
- }
-
- @Override
- public Intent createRestartIntent() {
- Intent intent = super.createRestartIntent();
- intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, mPhone);
- intent.putExtra(EXTRA_SYNC_TYPE, mSyncType);
- intent.putExtra(EXTRA_VOICEMAIL, mVoicemail);
- return intent;
- }
-
-}
diff --git a/java/com/android/voicemailomtp/sync/SyncTask.java b/java/com/android/voicemailomtp/sync/SyncTask.java
deleted file mode 100644
index 41b22f22c..000000000
--- a/java/com/android/voicemailomtp/sync/SyncTask.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.sync;
-
-import android.content.Context;
-import android.content.Intent;
-import android.telecom.PhoneAccountHandle;
-
-import com.android.voicemailomtp.scheduling.BaseTask;
-import com.android.voicemailomtp.scheduling.MinimalIntervalPolicy;
-import com.android.voicemailomtp.scheduling.RetryPolicy;
-
-/**
- * System initiated sync request.
- */
-public class SyncTask extends BaseTask {
-
- // Try sync for a total of 5 times, should take around 5 minutes before finally giving up.
- private static final int RETRY_TIMES = 4;
- private static final int RETRY_INTERVAL_MILLIS = 5_000;
- private static final int MINIMAL_INTERVAL_MILLIS = 60_000;
-
- private static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle";
- private static final String EXTRA_SYNC_TYPE = "extra_sync_type";
-
- private final RetryPolicy mRetryPolicy;
-
- private PhoneAccountHandle mPhone;
- private String mSyncType;
-
- public static void start(Context context, PhoneAccountHandle phone, String syncType) {
- Intent intent = BaseTask
- .createIntent(context, SyncTask.class, phone);
- intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, phone);
- intent.putExtra(EXTRA_SYNC_TYPE, syncType);
- context.startService(intent);
- }
-
- public SyncTask() {
- super(TASK_SYNC);
- mRetryPolicy = new RetryPolicy(RETRY_TIMES, RETRY_INTERVAL_MILLIS);
- addPolicy(mRetryPolicy);
- addPolicy(new MinimalIntervalPolicy(MINIMAL_INTERVAL_MILLIS));
- }
-
- public void onCreate(Context context, Intent intent, int flags, int startId) {
- super.onCreate(context, intent, flags, startId);
- mPhone = intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE);
- mSyncType = intent.getStringExtra(EXTRA_SYNC_TYPE);
- }
-
- @Override
- public void onExecuteInBackgroundThread() {
- OmtpVvmSyncService service = new OmtpVvmSyncService(getContext());
- service.sync(this, mSyncType, mPhone, null, mRetryPolicy.getVoicemailStatusEditor());
- }
-
- @Override
- public Intent createRestartIntent() {
- Intent intent = super.createRestartIntent();
- intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, mPhone);
- intent.putExtra(EXTRA_SYNC_TYPE, mSyncType);
- return intent;
- }
-}
diff --git a/java/com/android/voicemailomtp/sync/UploadTask.java b/java/com/android/voicemailomtp/sync/UploadTask.java
deleted file mode 100644
index 30a16812b..000000000
--- a/java/com/android/voicemailomtp/sync/UploadTask.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.sync;
-
-import android.content.Context;
-import android.content.Intent;
-import android.telecom.PhoneAccountHandle;
-
-import com.android.voicemailomtp.VoicemailStatus;
-import com.android.voicemailomtp.VvmLog;
-import com.android.voicemailomtp.scheduling.BaseTask;
-import com.android.voicemailomtp.scheduling.PostponePolicy;
-
-/**
- * Upload task triggered by database changes. Will wait until the database has been stable for
- * {@link #POSTPONE_MILLIS} to execute.
- */
-public class UploadTask extends BaseTask {
-
- private static final String TAG = "VvmUploadTask";
-
- private static final int POSTPONE_MILLIS = 5_000;
-
- public UploadTask() {
- super(TASK_UPLOAD);
- addPolicy(new PostponePolicy(POSTPONE_MILLIS));
- }
-
- public static void start(Context context, PhoneAccountHandle phoneAccountHandle) {
- Intent intent = BaseTask
- .createIntent(context, UploadTask.class, phoneAccountHandle);
- context.startService(intent);
- }
-
- @Override
- public void onCreate(Context context, Intent intent, int flags, int startId) {
- super.onCreate(context, intent, flags, startId);
- }
-
- @Override
- public void onExecuteInBackgroundThread() {
- OmtpVvmSyncService service = new OmtpVvmSyncService(getContext());
-
- PhoneAccountHandle phoneAccountHandle = getPhoneAccountHandle();
- if (phoneAccountHandle == null) {
- // This should never happen
- VvmLog.e(TAG, "null phone account for phoneAccountHandle " + getPhoneAccountHandle());
- return;
- }
- service.sync(this, OmtpVvmSyncService.SYNC_UPLOAD_ONLY,
- phoneAccountHandle, null,
- VoicemailStatus.edit(getContext(), phoneAccountHandle));
- }
-}
diff --git a/java/com/android/voicemailomtp/sync/VoicemailStatusQueryHelper.java b/java/com/android/voicemailomtp/sync/VoicemailStatusQueryHelper.java
deleted file mode 100644
index 89ba0b494..000000000
--- a/java/com/android/voicemailomtp/sync/VoicemailStatusQueryHelper.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.sync;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.database.Cursor;
-import android.net.Uri;
-import android.provider.VoicemailContract;
-import android.provider.VoicemailContract.Status;
-import android.telecom.PhoneAccountHandle;
-
-/**
- * Construct queries to interact with the voicemail status table.
- */
-public class VoicemailStatusQueryHelper {
-
- final static String[] PROJECTION = new String[] {
- Status._ID, // 0
- Status.CONFIGURATION_STATE, // 1
- Status.NOTIFICATION_CHANNEL_STATE, // 2
- Status.SOURCE_PACKAGE // 3
- };
-
- public static final int _ID = 0;
- public static final int CONFIGURATION_STATE = 1;
- public static final int NOTIFICATION_CHANNEL_STATE = 2;
- public static final int SOURCE_PACKAGE = 3;
-
- private Context mContext;
- private ContentResolver mContentResolver;
- private Uri mSourceUri;
-
- public VoicemailStatusQueryHelper(Context context) {
- mContext = context;
- mContentResolver = context.getContentResolver();
- mSourceUri = VoicemailContract.Status.buildSourceUri(mContext.getPackageName());
- }
-
- /**
- * Check if the configuration state for the voicemail source is "ok", meaning that the
- * source is set up.
- *
- * @param phoneAccount The phone account for the voicemail source to check.
- * @return {@code true} if the voicemail source is configured, {@code} false otherwise,
- * including if the voicemail source is not registered in the table.
- */
- public boolean isVoicemailSourceConfigured(PhoneAccountHandle phoneAccount) {
- return isFieldEqualTo(phoneAccount, CONFIGURATION_STATE, Status.CONFIGURATION_STATE_OK);
- }
-
- /**
- * Check if the notifications channel of a voicemail source is active. That is, when a new
- * voicemail is available, if the server able to notify the device.
- *
- * @return {@code true} if notifications channel is active, {@code false} otherwise.
- */
- public boolean isNotificationsChannelActive(PhoneAccountHandle phoneAccount) {
- return isFieldEqualTo(phoneAccount, NOTIFICATION_CHANNEL_STATE,
- Status.NOTIFICATION_CHANNEL_STATE_OK);
- }
-
- /**
- * Check if a field for an entry in the status table is equal to a specific value.
- *
- * @param phoneAccount The phone account of the voicemail source to query for.
- * @param columnIndex The column index of the field in the returned query.
- * @param value The value to compare against.
- * @return {@code true} if the stored value is equal to the provided value. {@code false}
- * otherwise.
- */
- private boolean isFieldEqualTo(PhoneAccountHandle phoneAccount, int columnIndex, int value) {
- Cursor cursor = null;
- if (phoneAccount != null) {
- String phoneAccountComponentName = phoneAccount.getComponentName().flattenToString();
- String phoneAccountId = phoneAccount.getId();
- if (phoneAccountComponentName == null || phoneAccountId == null) {
- return false;
- }
- try {
- String whereClause =
- Status.PHONE_ACCOUNT_COMPONENT_NAME + "=? AND " +
- Status.PHONE_ACCOUNT_ID + "=? AND " + Status.SOURCE_PACKAGE + "=?";
- String[] whereArgs = { phoneAccountComponentName, phoneAccountId,
- mContext.getPackageName()};
- cursor = mContentResolver.query(
- mSourceUri, PROJECTION, whereClause, whereArgs, null);
- if (cursor != null && cursor.moveToFirst()) {
- return cursor.getInt(columnIndex) == value;
- }
- }
- finally {
- if (cursor != null) {
- cursor.close();
- }
- }
- }
- return false;
- }
-}
diff --git a/java/com/android/voicemailomtp/sync/VoicemailsQueryHelper.java b/java/com/android/voicemailomtp/sync/VoicemailsQueryHelper.java
deleted file mode 100644
index 1450e3d1b..000000000
--- a/java/com/android/voicemailomtp/sync/VoicemailsQueryHelper.java
+++ /dev/null
@@ -1,244 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.sync;
-
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.database.Cursor;
-import android.net.Uri;
-import android.provider.VoicemailContract;
-import android.provider.VoicemailContract.Voicemails;
-import android.telecom.PhoneAccountHandle;
-import com.android.voicemailomtp.Voicemail;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Construct queries to interact with the voicemails table.
- */
-public class VoicemailsQueryHelper {
- final static String[] PROJECTION = new String[] {
- Voicemails._ID, // 0
- Voicemails.SOURCE_DATA, // 1
- Voicemails.IS_READ, // 2
- Voicemails.DELETED, // 3
- Voicemails.TRANSCRIPTION // 4
- };
-
- public static final int _ID = 0;
- public static final int SOURCE_DATA = 1;
- public static final int IS_READ = 2;
- public static final int DELETED = 3;
- public static final int TRANSCRIPTION = 4;
-
- final static String READ_SELECTION = Voicemails.DIRTY + "=1 AND "
- + Voicemails.DELETED + "!=1 AND " + Voicemails.IS_READ + "=1";
- final static String DELETED_SELECTION = Voicemails.DELETED + "=1";
-
- private Context mContext;
- private ContentResolver mContentResolver;
- private Uri mSourceUri;
-
- public VoicemailsQueryHelper(Context context) {
- mContext = context;
- mContentResolver = context.getContentResolver();
- mSourceUri = VoicemailContract.Voicemails.buildSourceUri(mContext.getPackageName());
- }
-
- /**
- * Get all the local read voicemails that have not been synced to the server.
- *
- * @return A list of read voicemails.
- */
- public List<Voicemail> getReadVoicemails() {
- return getLocalVoicemails(READ_SELECTION);
- }
-
- /**
- * Get all the locally deleted voicemails that have not been synced to the server.
- *
- * @return A list of deleted voicemails.
- */
- public List<Voicemail> getDeletedVoicemails() {
- return getLocalVoicemails(DELETED_SELECTION);
- }
-
- /**
- * Get all voicemails locally stored.
- *
- * @return A list of all locally stored voicemails.
- */
- public List<Voicemail> getAllVoicemails() {
- return getLocalVoicemails(null);
- }
-
- /**
- * Utility method to make queries to the voicemail database.
- *
- * @param selection A filter declaring which rows to return. {@code null} returns all rows.
- * @return A list of voicemails according to the selection statement.
- */
- private List<Voicemail> getLocalVoicemails(String selection) {
- Cursor cursor = mContentResolver.query(mSourceUri, PROJECTION, selection, null, null);
- if (cursor == null) {
- return null;
- }
- try {
- List<Voicemail> voicemails = new ArrayList<Voicemail>();
- while (cursor.moveToNext()) {
- final long id = cursor.getLong(_ID);
- final String sourceData = cursor.getString(SOURCE_DATA);
- final boolean isRead = cursor.getInt(IS_READ) == 1;
- final String transcription = cursor.getString(TRANSCRIPTION);
- Voicemail voicemail = Voicemail
- .createForUpdate(id, sourceData)
- .setIsRead(isRead)
- .setTranscription(transcription).build();
- voicemails.add(voicemail);
- }
- return voicemails;
- } finally {
- cursor.close();
- }
- }
-
- /**
- * Deletes a list of voicemails from the voicemail content provider.
- *
- * @param voicemails The list of voicemails to delete
- * @return The number of voicemails deleted
- */
- public int deleteFromDatabase(List<Voicemail> voicemails) {
- int count = voicemails.size();
- if (count == 0) {
- return 0;
- }
-
- StringBuilder sb = new StringBuilder();
- for (int i = 0; i < count; i++) {
- if (i > 0) {
- sb.append(",");
- }
- sb.append(voicemails.get(i).getId());
- }
-
- String selectionStatement = String.format(Voicemails._ID + " IN (%s)", sb.toString());
- return mContentResolver.delete(Voicemails.CONTENT_URI, selectionStatement, null);
- }
-
- /**
- * Utility method to delete a single voicemail.
- */
- public void deleteFromDatabase(Voicemail voicemail) {
- mContentResolver.delete(Voicemails.CONTENT_URI, Voicemails._ID + "=?",
- new String[] { Long.toString(voicemail.getId()) });
- }
-
- public int markReadInDatabase(List<Voicemail> voicemails) {
- int count = voicemails.size();
- for (int i = 0; i < count; i++) {
- markReadInDatabase(voicemails.get(i));
- }
- return count;
- }
-
- /**
- * Utility method to mark single message as read.
- */
- public void markReadInDatabase(Voicemail voicemail) {
- Uri uri = ContentUris.withAppendedId(mSourceUri, voicemail.getId());
- ContentValues contentValues = new ContentValues();
- contentValues.put(Voicemails.IS_READ, "1");
- mContentResolver.update(uri, contentValues, null, null);
- }
-
- /**
- * Sends an update command to the voicemail content provider for a list of voicemails. From the
- * view of the provider, since the updater is the owner of the entry, a blank "update" means
- * that the voicemail source is indicating that the server has up-to-date information on the
- * voicemail. This flips the "dirty" bit to "0".
- *
- * @param voicemails The list of voicemails to update
- * @return The number of voicemails updated
- */
- public int markCleanInDatabase(List<Voicemail> voicemails) {
- int count = voicemails.size();
- for (int i = 0; i < count; i++) {
- markCleanInDatabase(voicemails.get(i));
- }
- return count;
- }
-
- /**
- * Utility method to mark single message as clean.
- */
- public void markCleanInDatabase(Voicemail voicemail) {
- Uri uri = ContentUris.withAppendedId(mSourceUri, voicemail.getId());
- ContentValues contentValues = new ContentValues();
- mContentResolver.update(uri, contentValues, null, null);
- }
-
- /**
- * Utility method to add a transcription to the voicemail.
- */
- public void updateWithTranscription(Voicemail voicemail, String transcription) {
- Uri uri = ContentUris.withAppendedId(mSourceUri, voicemail.getId());
- ContentValues contentValues = new ContentValues();
- contentValues.put(Voicemails.TRANSCRIPTION, transcription);
- mContentResolver.update(uri, contentValues, null, null);
- }
-
- /**
- * Voicemail is unique if the tuple of (phone account component name, phone account id, source
- * data) is unique. If the phone account is missing, we also consider this unique since it's
- * simply an "unknown" account.
- * @param voicemail The voicemail to check if it is unique.
- * @return {@code true} if the voicemail is unique, {@code false} otherwise.
- */
- public boolean isVoicemailUnique(Voicemail voicemail) {
- Cursor cursor = null;
- PhoneAccountHandle phoneAccount = voicemail.getPhoneAccount();
- if (phoneAccount != null) {
- String phoneAccountComponentName = phoneAccount.getComponentName().flattenToString();
- String phoneAccountId = phoneAccount.getId();
- String sourceData = voicemail.getSourceData();
- if (phoneAccountComponentName == null || phoneAccountId == null || sourceData == null) {
- return true;
- }
- try {
- String whereClause =
- Voicemails.PHONE_ACCOUNT_COMPONENT_NAME + "=? AND " +
- Voicemails.PHONE_ACCOUNT_ID + "=? AND " + Voicemails.SOURCE_DATA + "=?";
- String[] whereArgs = { phoneAccountComponentName, phoneAccountId, sourceData };
- cursor = mContentResolver.query(
- mSourceUri, PROJECTION, whereClause, whereArgs, null);
- if (cursor.getCount() == 0) {
- return true;
- } else {
- return false;
- }
- }
- finally {
- if (cursor != null) {
- cursor.close();
- }
- }
- }
- return true;
- }
-}
diff --git a/java/com/android/voicemailomtp/sync/VvmNetworkRequest.java b/java/com/android/voicemailomtp/sync/VvmNetworkRequest.java
deleted file mode 100644
index 966b940c2..000000000
--- a/java/com/android/voicemailomtp/sync/VvmNetworkRequest.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.sync;
-
-import android.annotation.TargetApi;
-import android.net.Network;
-import android.os.Build.VERSION_CODES;
-import android.support.annotation.NonNull;
-import android.telecom.PhoneAccountHandle;
-import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
-import com.android.voicemailomtp.VoicemailStatus;
-import com.android.voicemailomtp.VvmLog;
-import java.io.Closeable;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
-
-/**
- * Class to retrieve a {@link Network} synchronously. {@link #getNetwork(OmtpVvmCarrierConfigHelper,
- * PhoneAccountHandle)} will block until a suitable network is retrieved or it has failed.
- */
-@SuppressWarnings("AndroidApiChecker") /* CompletableFuture is java8*/
-@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
-public class VvmNetworkRequest {
-
- private static final String TAG = "VvmNetworkRequest";
-
- /**
- * A wrapper around a Network returned by a {@link VvmNetworkRequestCallback}, which should be
- * closed once not needed anymore.
- */
- public static class NetworkWrapper implements Closeable {
-
- private final Network mNetwork;
- private final VvmNetworkRequestCallback mCallback;
-
- private NetworkWrapper(Network network, VvmNetworkRequestCallback callback) {
- mNetwork = network;
- mCallback = callback;
- }
-
- public Network get() {
- return mNetwork;
- }
-
- @Override
- public void close() {
- mCallback.releaseNetwork();
- }
- }
-
- public static class RequestFailedException extends Exception {
-
- private RequestFailedException(Throwable cause) {
- super(cause);
- }
- }
-
- @NonNull
- public static NetworkWrapper getNetwork(OmtpVvmCarrierConfigHelper config,
- PhoneAccountHandle handle, VoicemailStatus.Editor status) throws RequestFailedException {
- FutureNetworkRequestCallback callback = new FutureNetworkRequestCallback(config, handle,
- status);
- callback.requestNetwork();
- try {
- return callback.getFuture().get();
- } catch (InterruptedException | ExecutionException e) {
- callback.releaseNetwork();
- VvmLog.e(TAG, "can't get future network", e);
- throw new RequestFailedException(e);
- }
- }
-
- private static class FutureNetworkRequestCallback extends VvmNetworkRequestCallback {
-
- /**
- * {@link CompletableFuture#get()} will block until {@link CompletableFuture#
- * complete(Object) } has been called on the other thread.
- */
- private final CompletableFuture<NetworkWrapper> mFuture = new CompletableFuture<>();
-
- public FutureNetworkRequestCallback(OmtpVvmCarrierConfigHelper config,
- PhoneAccountHandle phoneAccount, VoicemailStatus.Editor status) {
- super(config, phoneAccount, status);
- }
-
- public Future<NetworkWrapper> getFuture() {
- return mFuture;
- }
-
- @Override
- public void onAvailable(Network network) {
- super.onAvailable(network);
- mFuture.complete(new NetworkWrapper(network, this));
- }
-
- @Override
- public void onFailed(String reason) {
- super.onFailed(reason);
- mFuture.complete(null);
- }
-
- }
-}
diff --git a/java/com/android/voicemailomtp/sync/VvmNetworkRequestCallback.java b/java/com/android/voicemailomtp/sync/VvmNetworkRequestCallback.java
deleted file mode 100644
index 8481a9d16..000000000
--- a/java/com/android/voicemailomtp/sync/VvmNetworkRequestCallback.java
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.sync;
-
-import android.content.Context;
-import android.net.ConnectivityManager;
-import android.net.Network;
-import android.net.NetworkCapabilities;
-import android.net.NetworkRequest;
-import android.os.Handler;
-import android.os.Looper;
-import android.support.annotation.CallSuper;
-import android.telecom.PhoneAccountHandle;
-
-import com.android.voicemailomtp.OmtpEvents;
-import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
-import com.android.voicemailomtp.TelephonyManagerStub;
-import com.android.voicemailomtp.VoicemailStatus;
-import com.android.voicemailomtp.VvmLog;
-
-/**
- * Base class for network request call backs for visual voicemail syncing with the Imap server. This
- * handles retries and network requests.
- */
-public abstract class VvmNetworkRequestCallback extends ConnectivityManager.NetworkCallback {
-
- private static final String TAG = "VvmNetworkRequest";
-
- // Timeout used to call ConnectivityManager.requestNetwork
- private static final int NETWORK_REQUEST_TIMEOUT_MILLIS = 60 * 1000;
-
- public static final String NETWORK_REQUEST_FAILED_TIMEOUT = "timeout";
- public static final String NETWORK_REQUEST_FAILED_LOST = "lost";
-
- protected Context mContext;
- protected PhoneAccountHandle mPhoneAccount;
- protected NetworkRequest mNetworkRequest;
- private ConnectivityManager mConnectivityManager;
- private final OmtpVvmCarrierConfigHelper mCarrierConfigHelper;
- private final VoicemailStatus.Editor mStatus;
- private boolean mRequestSent = false;
- private boolean mResultReceived = false;
-
- public VvmNetworkRequestCallback(Context context, PhoneAccountHandle phoneAccount,
- VoicemailStatus.Editor status) {
- mContext = context;
- mPhoneAccount = phoneAccount;
- mStatus = status;
- mCarrierConfigHelper = new OmtpVvmCarrierConfigHelper(context, mPhoneAccount);
- mNetworkRequest = createNetworkRequest();
- }
-
- public VvmNetworkRequestCallback(OmtpVvmCarrierConfigHelper config,
- PhoneAccountHandle phoneAccount, VoicemailStatus.Editor status) {
- mContext = config.getContext();
- mPhoneAccount = phoneAccount;
- mStatus = status;
- mCarrierConfigHelper = config;
- mNetworkRequest = createNetworkRequest();
- }
-
- public VoicemailStatus.Editor getVoicemailStatusEditor() {
- return mStatus;
- }
-
- /**
- * @return NetworkRequest for a proper transport type. Use only cellular network if the carrier
- * requires it. Otherwise use whatever available.
- */
- private NetworkRequest createNetworkRequest() {
-
- NetworkRequest.Builder builder = new NetworkRequest.Builder()
- .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
-
- if (mCarrierConfigHelper.isCellularDataRequired()) {
- VvmLog.d(TAG, "Transport type: CELLULAR");
- builder.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
- .setNetworkSpecifier(TelephonyManagerStub
- .getNetworkSpecifierForPhoneAccountHandle(mContext, mPhoneAccount));
- } else {
- VvmLog.d(TAG, "Transport type: ANY");
- }
- return builder.build();
- }
-
- public NetworkRequest getNetworkRequest() {
- return mNetworkRequest;
- }
-
- @Override
- @CallSuper
- public void onLost(Network network) {
- VvmLog.d(TAG, "onLost");
- mResultReceived = true;
- onFailed(NETWORK_REQUEST_FAILED_LOST);
- }
-
- @Override
- @CallSuper
- public void onAvailable(Network network) {
- super.onAvailable(network);
- mResultReceived = true;
- }
-
- @CallSuper
- public void onUnavailable() {
- // TODO: b/32637799 this is hidden, do we really need this?
- mResultReceived = true;
- onFailed(NETWORK_REQUEST_FAILED_TIMEOUT);
- }
-
- public void requestNetwork() {
- if (mRequestSent == true) {
- VvmLog.e(TAG, "requestNetwork() called twice");
- return;
- }
- mRequestSent = true;
- getConnectivityManager().requestNetwork(getNetworkRequest(), this);
- /**
- * Somehow requestNetwork() with timeout doesn't work, and it's a hidden method.
- * Implement our own timeout mechanism instead.
- */
- Handler handler = new Handler(Looper.getMainLooper());
- handler.postDelayed(new Runnable() {
- @Override
- public void run() {
- if (mResultReceived == false) {
- onFailed(NETWORK_REQUEST_FAILED_TIMEOUT);
- }
- }
- }, NETWORK_REQUEST_TIMEOUT_MILLIS);
- }
-
- public void releaseNetwork() {
- VvmLog.d(TAG, "releaseNetwork");
- getConnectivityManager().unregisterNetworkCallback(this);
- }
-
- public ConnectivityManager getConnectivityManager() {
- if (mConnectivityManager == null) {
- mConnectivityManager = (ConnectivityManager) mContext.getSystemService(
- Context.CONNECTIVITY_SERVICE);
- }
- return mConnectivityManager;
- }
-
- @CallSuper
- public void onFailed(String reason) {
- VvmLog.d(TAG, "onFailed: " + reason);
- if (mCarrierConfigHelper.isCellularDataRequired()) {
- mCarrierConfigHelper
- .handleEvent(mStatus, OmtpEvents.DATA_NO_CONNECTION_CELLULAR_REQUIRED);
- } else {
- mCarrierConfigHelper.handleEvent(mStatus, OmtpEvents.DATA_NO_CONNECTION);
- }
- releaseNetwork();
- }
-}
diff --git a/java/com/android/voicemailomtp/utils/IndentingPrintWriter.java b/java/com/android/voicemailomtp/utils/IndentingPrintWriter.java
deleted file mode 100644
index eda7c4ee3..000000000
--- a/java/com/android/voicemailomtp/utils/IndentingPrintWriter.java
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.utils;
-
-import java.io.PrintWriter;
-import java.io.Writer;
-import java.util.Arrays;
-
-/**
- * Lightweight wrapper around {@link PrintWriter} that automatically indents newlines based on
- * internal state. It also automatically wraps long lines based on given line length. <p> Delays
- * writing indent until first actual write on a newline, enabling indent modification after
- * newline.
- */
-public class IndentingPrintWriter extends PrintWriter {
-
- private final String mSingleIndent;
- private final int mWrapLength;
-
- /**
- * Mutable version of current indent
- */
- private StringBuilder mIndentBuilder = new StringBuilder();
- /**
- * Cache of current {@link #mIndentBuilder} value
- */
- private char[] mCurrentIndent;
- /**
- * Length of current line being built, excluding any indent
- */
- private int mCurrentLength;
-
- /**
- * Flag indicating if we're currently sitting on an empty line, and that next write should be
- * prefixed with the current indent.
- */
- private boolean mEmptyLine = true;
-
- private char[] mSingleChar = new char[1];
-
- public IndentingPrintWriter(Writer writer, String singleIndent) {
- this(writer, singleIndent, -1);
- }
-
- public IndentingPrintWriter(Writer writer, String singleIndent, int wrapLength) {
- super(writer);
- mSingleIndent = singleIndent;
- mWrapLength = wrapLength;
- }
-
- public void increaseIndent() {
- mIndentBuilder.append(mSingleIndent);
- mCurrentIndent = null;
- }
-
- public void decreaseIndent() {
- mIndentBuilder.delete(0, mSingleIndent.length());
- mCurrentIndent = null;
- }
-
- public void printPair(String key, Object value) {
- print(key + "=" + String.valueOf(value) + " ");
- }
-
- public void printPair(String key, Object[] value) {
- print(key + "=" + Arrays.toString(value) + " ");
- }
-
- public void printHexPair(String key, int value) {
- print(key + "=0x" + Integer.toHexString(value) + " ");
- }
-
- @Override
- public void println() {
- write('\n');
- }
-
- @Override
- public void write(int c) {
- mSingleChar[0] = (char) c;
- write(mSingleChar, 0, 1);
- }
-
- @Override
- public void write(String s, int off, int len) {
- final char[] buf = new char[len];
- s.getChars(off, len - off, buf, 0);
- write(buf, 0, len);
- }
-
- @Override
- public void write(char[] buf, int offset, int count) {
- final int indentLength = mIndentBuilder.length();
- final int bufferEnd = offset + count;
- int lineStart = offset;
- int lineEnd = offset;
-
- // March through incoming buffer looking for newlines
- while (lineEnd < bufferEnd) {
- char ch = buf[lineEnd++];
- mCurrentLength++;
- if (ch == '\n') {
- maybeWriteIndent();
- super.write(buf, lineStart, lineEnd - lineStart);
- lineStart = lineEnd;
- mEmptyLine = true;
- mCurrentLength = 0;
- }
-
- // Wrap if we've pushed beyond line length
- if (mWrapLength > 0 && mCurrentLength >= mWrapLength - indentLength) {
- if (!mEmptyLine) {
- // Give ourselves a fresh line to work with
- super.write('\n');
- mEmptyLine = true;
- mCurrentLength = lineEnd - lineStart;
- } else {
- // We need more than a dedicated line, slice it hard
- maybeWriteIndent();
- super.write(buf, lineStart, lineEnd - lineStart);
- super.write('\n');
- mEmptyLine = true;
- lineStart = lineEnd;
- mCurrentLength = 0;
- }
- }
- }
-
- if (lineStart != lineEnd) {
- maybeWriteIndent();
- super.write(buf, lineStart, lineEnd - lineStart);
- }
- }
-
- private void maybeWriteIndent() {
- if (mEmptyLine) {
- mEmptyLine = false;
- if (mIndentBuilder.length() != 0) {
- if (mCurrentIndent == null) {
- mCurrentIndent = mIndentBuilder.toString().toCharArray();
- }
- super.write(mCurrentIndent, 0, mCurrentIndent.length);
- }
- }
- }
-} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/utils/VoicemailDatabaseUtil.java b/java/com/android/voicemailomtp/utils/VoicemailDatabaseUtil.java
deleted file mode 100644
index f94070ecd..000000000
--- a/java/com/android/voicemailomtp/utils/VoicemailDatabaseUtil.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.utils;
-
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.content.Context;
-import android.net.Uri;
-import android.provider.VoicemailContract.Voicemails;
-import android.telecom.PhoneAccountHandle;
-import com.android.voicemailomtp.Voicemail;
-import java.util.List;
-
-public class VoicemailDatabaseUtil {
-
- /**
- * Inserts a new voicemail into the voicemail content provider.
- *
- * @param context The context of the app doing the inserting
- * @param voicemail Data to be inserted
- * @return {@link Uri} of the newly inserted {@link Voicemail}
- * @hide
- */
- public static Uri insert(Context context, Voicemail voicemail) {
- ContentResolver contentResolver = context.getContentResolver();
- ContentValues contentValues = getContentValues(voicemail);
- return contentResolver
- .insert(Voicemails.buildSourceUri(context.getPackageName()), contentValues);
- }
-
- /**
- * Inserts a list of voicemails into the voicemail content provider.
- *
- * @param context The context of the app doing the inserting
- * @param voicemails Data to be inserted
- * @return the number of voicemails inserted
- * @hide
- */
- public static int insert(Context context, List<Voicemail> voicemails) {
- ContentResolver contentResolver = context.getContentResolver();
- int count = voicemails.size();
- for (int i = 0; i < count; i++) {
- ContentValues contentValues = getContentValues(voicemails.get(i));
- contentResolver
- .insert(Voicemails.buildSourceUri(context.getPackageName()), contentValues);
- }
- return count;
- }
-
-
- /**
- * Maps structured {@link Voicemail} to {@link ContentValues} in content provider.
- */
- private static ContentValues getContentValues(Voicemail voicemail) {
- ContentValues contentValues = new ContentValues();
- contentValues.put(Voicemails.DATE, String.valueOf(voicemail.getTimestampMillis()));
- contentValues.put(Voicemails.NUMBER, voicemail.getNumber());
- contentValues.put(Voicemails.DURATION, String.valueOf(voicemail.getDuration()));
- contentValues.put(Voicemails.SOURCE_PACKAGE, voicemail.getSourcePackage());
- contentValues.put(Voicemails.SOURCE_DATA, voicemail.getSourceData());
- contentValues.put(Voicemails.IS_READ, voicemail.isRead() ? 1 : 0);
-
- PhoneAccountHandle phoneAccount = voicemail.getPhoneAccount();
- if (phoneAccount != null) {
- contentValues.put(Voicemails.PHONE_ACCOUNT_COMPONENT_NAME,
- phoneAccount.getComponentName().flattenToString());
- contentValues.put(Voicemails.PHONE_ACCOUNT_ID, phoneAccount.getId());
- }
-
- if (voicemail.getTranscription() != null) {
- contentValues.put(Voicemails.TRANSCRIPTION, voicemail.getTranscription());
- }
-
- return contentValues;
- }
-}
diff --git a/java/com/android/voicemailomtp/utils/VvmDumpHandler.java b/java/com/android/voicemailomtp/utils/VvmDumpHandler.java
deleted file mode 100644
index 5768a9c19..000000000
--- a/java/com/android/voicemailomtp/utils/VvmDumpHandler.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.utils;
-
-import android.content.Context;
-import android.telecom.PhoneAccountHandle;
-import android.telecom.TelecomManager;
-
-import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
-import com.android.voicemailomtp.VvmLog;
-
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
-
-public class VvmDumpHandler {
-
- public static void dump(Context context, FileDescriptor fd, PrintWriter writer,
- String[] args) {
- IndentingPrintWriter indentedWriter = new IndentingPrintWriter(writer, " ");
- indentedWriter.println("******* OmtpVvm *******");
- indentedWriter.println("======= Configs =======");
- indentedWriter.increaseIndent();
- for (PhoneAccountHandle handle : context.getSystemService(TelecomManager.class)
- .getCallCapablePhoneAccounts()) {
- OmtpVvmCarrierConfigHelper config = new OmtpVvmCarrierConfigHelper(context, handle);
- indentedWriter.println(config.toString());
- }
- indentedWriter.decreaseIndent();
- indentedWriter.println("======== Logs =========");
- VvmLog.dump(fd, indentedWriter, args);
- }
-}
diff --git a/java/com/android/voicemailomtp/utils/XmlUtils.java b/java/com/android/voicemailomtp/utils/XmlUtils.java
deleted file mode 100644
index 768247e27..000000000
--- a/java/com/android/voicemailomtp/utils/XmlUtils.java
+++ /dev/null
@@ -1,245 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.voicemailomtp.utils;
-
-import android.util.ArrayMap;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
-public class XmlUtils {
-
- public static final ArrayMap<String, ?> readThisArrayMapXml(XmlPullParser parser, String endTag,
- String[] name, ReadMapCallback callback)
- throws XmlPullParserException, java.io.IOException {
- ArrayMap<String, Object> map = new ArrayMap<>();
-
- int eventType = parser.getEventType();
- do {
- if (eventType == XmlPullParser.START_TAG) {
- Object val = readThisValueXml(parser, name, callback, true);
- map.put(name[0], val);
- } else if (eventType == XmlPullParser.END_TAG) {
- if (parser.getName().equals(endTag)) {
- return map;
- }
- throw new XmlPullParserException(
- "Expected " + endTag + " end tag at: " + parser.getName());
- }
- eventType = parser.next();
- } while (eventType != XmlPullParser.END_DOCUMENT);
-
- throw new XmlPullParserException(
- "Document ended before " + endTag + " end tag");
- }
-
- /**
- * Read an ArrayList object from an XmlPullParser. The XML data could previously have been
- * generated by writeListXml(). The XmlPullParser must be positioned <em>after</em> the tag
- * that begins the list.
- *
- * @param parser The XmlPullParser from which to read the list data.
- * @param endTag Name of the tag that will end the list, usually "list".
- * @param name An array of one string, used to return the name attribute of the list's tag.
- * @return HashMap The newly generated list.
- */
- public static final ArrayList readThisListXml(XmlPullParser parser, String endTag,
- String[] name, ReadMapCallback callback, boolean arrayMap)
- throws XmlPullParserException, java.io.IOException {
- ArrayList list = new ArrayList();
-
- int eventType = parser.getEventType();
- do {
- if (eventType == XmlPullParser.START_TAG) {
- Object val = readThisValueXml(parser, name, callback, arrayMap);
- list.add(val);
- } else if (eventType == XmlPullParser.END_TAG) {
- if (parser.getName().equals(endTag)) {
- return list;
- }
- throw new XmlPullParserException(
- "Expected " + endTag + " end tag at: " + parser.getName());
- }
- eventType = parser.next();
- } while (eventType != XmlPullParser.END_DOCUMENT);
-
- throw new XmlPullParserException(
- "Document ended before " + endTag + " end tag");
- }
-
- /**
- * Read a String[] object from an XmlPullParser. The XML data could previously have been
- * generated by writeStringArrayXml(). The XmlPullParser must be positioned <em>after</em> the
- * tag that begins the list.
- *
- * @param parser The XmlPullParser from which to read the list data.
- * @param endTag Name of the tag that will end the list, usually "string-array".
- * @param name An array of one string, used to return the name attribute of the list's tag.
- * @return Returns a newly generated String[].
- */
- public static String[] readThisStringArrayXml(XmlPullParser parser, String endTag,
- String[] name) throws XmlPullParserException, java.io.IOException {
-
- parser.next();
-
- List<String> array = new ArrayList<>();
-
- int eventType = parser.getEventType();
- do {
- if (eventType == XmlPullParser.START_TAG) {
- if (parser.getName().equals("item")) {
- try {
- array.add(parser.getAttributeValue(null, "value"));
- } catch (NullPointerException e) {
- throw new XmlPullParserException("Need value attribute in item");
- } catch (NumberFormatException e) {
- throw new XmlPullParserException("Not a number in value attribute in item");
- }
- } else {
- throw new XmlPullParserException("Expected item tag at: " + parser.getName());
- }
- } else if (eventType == XmlPullParser.END_TAG) {
- if (parser.getName().equals(endTag)) {
- return array.toArray(new String[0]);
- } else if (parser.getName().equals("item")) {
-
- } else {
- throw new XmlPullParserException("Expected " + endTag + " end tag at: " +
- parser.getName());
- }
- }
- eventType = parser.next();
- } while (eventType != XmlPullParser.END_DOCUMENT);
-
- throw new XmlPullParserException("Document ended before " + endTag + " end tag");
- }
-
- private static Object readThisValueXml(XmlPullParser parser, String[] name,
- ReadMapCallback callback, boolean arrayMap)
- throws XmlPullParserException, java.io.IOException {
- final String valueName = parser.getAttributeValue(null, "name");
- final String tagName = parser.getName();
-
- Object res;
-
- if (tagName.equals("null")) {
- res = null;
- } else if (tagName.equals("string")) {
- String value = "";
- int eventType;
- while ((eventType = parser.next()) != XmlPullParser.END_DOCUMENT) {
- if (eventType == XmlPullParser.END_TAG) {
- if (parser.getName().equals("string")) {
- name[0] = valueName;
- return value;
- }
- throw new XmlPullParserException(
- "Unexpected end tag in <string>: " + parser.getName());
- } else if (eventType == XmlPullParser.TEXT) {
- value += parser.getText();
- } else if (eventType == XmlPullParser.START_TAG) {
- throw new XmlPullParserException(
- "Unexpected start tag in <string>: " + parser.getName());
- }
- }
- throw new XmlPullParserException(
- "Unexpected end of document in <string>");
- } else if ((res = readThisPrimitiveValueXml(parser, tagName)) != null) {
- // all work already done by readThisPrimitiveValueXml
- } else if (tagName.equals("string-array")) {
- res = readThisStringArrayXml(parser, "string-array", name);
- name[0] = valueName;
- return res;
- } else if (tagName.equals("list")) {
- parser.next();
- res = readThisListXml(parser, "list", name, callback, arrayMap);
- name[0] = valueName;
- return res;
- } else if (callback != null) {
- res = callback.readThisUnknownObjectXml(parser, tagName);
- name[0] = valueName;
- return res;
- } else {
- throw new XmlPullParserException("Unknown tag: " + tagName);
- }
-
- // Skip through to end tag.
- int eventType;
- while ((eventType = parser.next()) != XmlPullParser.END_DOCUMENT) {
- if (eventType == XmlPullParser.END_TAG) {
- if (parser.getName().equals(tagName)) {
- name[0] = valueName;
- return res;
- }
- throw new XmlPullParserException(
- "Unexpected end tag in <" + tagName + ">: " + parser.getName());
- } else if (eventType == XmlPullParser.TEXT) {
- throw new XmlPullParserException(
- "Unexpected text in <" + tagName + ">: " + parser.getName());
- } else if (eventType == XmlPullParser.START_TAG) {
- throw new XmlPullParserException(
- "Unexpected start tag in <" + tagName + ">: " + parser.getName());
- }
- }
- throw new XmlPullParserException(
- "Unexpected end of document in <" + tagName + ">");
- }
-
- private static final Object readThisPrimitiveValueXml(XmlPullParser parser, String tagName)
- throws XmlPullParserException, java.io.IOException {
- try {
- if (tagName.equals("int")) {
- return Integer.parseInt(parser.getAttributeValue(null, "value"));
- } else if (tagName.equals("long")) {
- return Long.valueOf(parser.getAttributeValue(null, "value"));
- } else if (tagName.equals("float")) {
- return Float.valueOf(parser.getAttributeValue(null, "value"));
- } else if (tagName.equals("double")) {
- return Double.valueOf(parser.getAttributeValue(null, "value"));
- } else if (tagName.equals("boolean")) {
- return Boolean.valueOf(parser.getAttributeValue(null, "value"));
- } else {
- return null;
- }
- } catch (NullPointerException e) {
- throw new XmlPullParserException("Need value attribute in <" + tagName + ">");
- } catch (NumberFormatException e) {
- throw new XmlPullParserException(
- "Not a number in value attribute in <" + tagName + ">");
- }
- }
-
- public interface ReadMapCallback {
-
- /**
- * Called from readThisMapXml when a START_TAG is not recognized. The input stream is
- * positioned within the start tag so that attributes can be read using in.getAttribute.
- *
- * @param in the XML input stream
- * @param tag the START_TAG that was not recognized.
- * @return the Object parsed from the stream which will be put into the map.
- * @throws XmlPullParserException if the START_TAG is not recognized.
- * @throws IOException on XmlPullParser serialization errors.
- */
- Object readThisUnknownObjectXml(XmlPullParser in, String tag)
- throws XmlPullParserException, IOException;
- }
-} \ No newline at end of file