diff options
663 files changed, 29574 insertions, 23441 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 Binary files differnew file mode 100644 index 000000000..2ccc89d24 --- /dev/null +++ b/assets/quantum/res/drawable-hdpi/quantum_ic_block_white_24.png 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 Binary files differnew file mode 100644 index 000000000..ea6a8ab5f --- /dev/null +++ b/assets/quantum/res/drawable-hdpi/quantum_ic_call_made_white_24.png 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 Binary files differnew file mode 100644 index 000000000..f188eb9aa --- /dev/null +++ b/assets/quantum/res/drawable-hdpi/quantum_ic_call_missed_white_24.png 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 Binary files differnew file mode 100644 index 000000000..ca2ae411a --- /dev/null +++ b/assets/quantum/res/drawable-hdpi/quantum_ic_call_received_white_24.png 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 Binary files differnew file mode 100644 index 000000000..6acef1745 --- /dev/null +++ b/assets/quantum/res/drawable-hdpi/quantum_ic_content_copy_grey600_24.png 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 Binary files differnew file mode 100644 index 000000000..8444f3138 --- /dev/null +++ b/assets/quantum/res/drawable-hdpi/quantum_ic_delete_white_24.png 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 Binary files differnew file mode 100644 index 000000000..4a27b4696 --- /dev/null +++ b/assets/quantum/res/drawable-hdpi/quantum_ic_edit_grey600_24.png 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 Binary files differnew file mode 100644 index 000000000..90bf872ac --- /dev/null +++ b/assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_content_copy_grey600_24.png 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 Binary files differnew file mode 100644 index 000000000..01b869a60 --- /dev/null +++ b/assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_content_copy_grey600_24.png 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 Binary files differnew file mode 100644 index 000000000..831b5249c --- /dev/null +++ b/assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_content_copy_grey600_24.png 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 Binary files differnew file mode 100644 index 000000000..71f3bd683 --- /dev/null +++ b/assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_content_copy_grey600_24.png 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 Binary files differnew file mode 100644 index 000000000..3b2aed29b --- /dev/null +++ b/assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_content_copy_grey600_24.png 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 Binary files differnew file mode 100644 index 000000000..ec1b33f0e --- /dev/null +++ b/assets/quantum/res/drawable-mdpi/quantum_ic_block_white_24.png 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 Binary files differnew file mode 100644 index 000000000..9b3cd4380 --- /dev/null +++ b/assets/quantum/res/drawable-mdpi/quantum_ic_call_made_white_24.png 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 Binary files differnew file mode 100644 index 000000000..42c360b8a --- /dev/null +++ b/assets/quantum/res/drawable-mdpi/quantum_ic_call_missed_white_24.png 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 Binary files differnew file mode 100644 index 000000000..fbc1e86e2 --- /dev/null +++ b/assets/quantum/res/drawable-mdpi/quantum_ic_call_received_white_24.png 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 Binary files differnew file mode 100644 index 000000000..8ac80b083 --- /dev/null +++ b/assets/quantum/res/drawable-mdpi/quantum_ic_content_copy_grey600_24.png 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 Binary files differnew file mode 100644 index 000000000..e2268c9be --- /dev/null +++ b/assets/quantum/res/drawable-mdpi/quantum_ic_delete_white_24.png 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 Binary files differnew file mode 100644 index 000000000..f003bc9d3 --- /dev/null +++ b/assets/quantum/res/drawable-mdpi/quantum_ic_edit_grey600_24.png 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 Binary files differnew file mode 100644 index 000000000..7aba97b65 --- /dev/null +++ b/assets/quantum/res/drawable-xhdpi/quantum_ic_block_white_24.png 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 Binary files differnew file mode 100644 index 000000000..7fe694105 --- /dev/null +++ b/assets/quantum/res/drawable-xhdpi/quantum_ic_call_made_white_24.png 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 Binary files differnew file mode 100644 index 000000000..dd64489aa --- /dev/null +++ b/assets/quantum/res/drawable-xhdpi/quantum_ic_call_missed_white_24.png 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 Binary files differnew file mode 100644 index 000000000..807308d9d --- /dev/null +++ b/assets/quantum/res/drawable-xhdpi/quantum_ic_call_received_white_24.png 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 Binary files differnew file mode 100644 index 000000000..ca6259859 --- /dev/null +++ b/assets/quantum/res/drawable-xhdpi/quantum_ic_content_copy_grey600_24.png 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 Binary files differnew file mode 100644 index 000000000..484260a97 --- /dev/null +++ b/assets/quantum/res/drawable-xhdpi/quantum_ic_delete_white_24.png 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 Binary files differnew file mode 100644 index 000000000..b5b3a243c --- /dev/null +++ b/assets/quantum/res/drawable-xhdpi/quantum_ic_edit_grey600_24.png 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 Binary files differnew file mode 100644 index 000000000..fddfa54b8 --- /dev/null +++ b/assets/quantum/res/drawable-xxhdpi/quantum_ic_block_white_24.png 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 Binary files differnew file mode 100644 index 000000000..ae471c9fc --- /dev/null +++ b/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_made_white_24.png 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 Binary files differnew file mode 100644 index 000000000..2374dc5a1 --- /dev/null +++ b/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_missed_white_24.png 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 Binary files differnew file mode 100644 index 000000000..58421114f --- /dev/null +++ b/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_received_white_24.png 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 Binary files differnew file mode 100644 index 000000000..c480ba78f --- /dev/null +++ b/assets/quantum/res/drawable-xxhdpi/quantum_ic_content_copy_grey600_24.png 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 Binary files differnew file mode 100644 index 000000000..603f28cbd --- /dev/null +++ b/assets/quantum/res/drawable-xxhdpi/quantum_ic_delete_white_24.png 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 Binary files differnew file mode 100644 index 000000000..f1f9ffce8 --- /dev/null +++ b/assets/quantum/res/drawable-xxhdpi/quantum_ic_edit_grey600_24.png 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 Binary files differnew file mode 100644 index 000000000..0378d1bed --- /dev/null +++ b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_block_white_24.png 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 Binary files differnew file mode 100644 index 000000000..844ef86a0 --- /dev/null +++ b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_made_white_24.png 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 Binary files differnew file mode 100644 index 000000000..b1321a9ae --- /dev/null +++ b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_missed_white_24.png 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 Binary files differnew file mode 100644 index 000000000..417999c85 --- /dev/null +++ b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_received_white_24.png 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 Binary files differnew file mode 100644 index 000000000..f0ea085c9 --- /dev/null +++ b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_content_copy_grey600_24.png 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 Binary files differnew file mode 100644 index 000000000..c582dc2a4 --- /dev/null +++ b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_delete_white_24.png 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 Binary files differnew file mode 100644 index 000000000..a61298dbe --- /dev/null +++ b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_edit_grey600_24.png 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 Binary files differnew file mode 100644 index 000000000..ff55620d0 --- /dev/null +++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/search_shadow.9.png 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/dialer/app/res/menu/call_log_options.xml b/java/com/android/dialer/app/res/menu/call_log_options.xml new file mode 100644 index 000000000..e78b72e3c --- /dev/null +++ b/java/com/android/dialer/app/res/menu/call_log_options.xml @@ -0,0 +1,22 @@ +<?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. +--> +<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/HasRootComponent.java b/java/com/android/dialer/inject/HasRootComponent.java new file mode 100644 index 000000000..0802b806a --- /dev/null +++ b/java/com/android/dialer/inject/HasRootComponent.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.dialer.inject; + +/** + * 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 Binary files differnew file mode 100644 index 000000000..2ccc89d24 --- /dev/null +++ b/java/com/android/dialer/theme/res/drawable-hdpi/ic_block_24dp.png 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 Binary files differindex 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 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 Binary files differnew file mode 100644 index 000000000..ec1b33f0e --- /dev/null +++ b/java/com/android/dialer/theme/res/drawable-mdpi/ic_block_24dp.png 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 Binary files differindex 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 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 Binary files differnew file mode 100644 index 000000000..7aba97b65 --- /dev/null +++ b/java/com/android/dialer/theme/res/drawable-xhdpi/ic_block_24dp.png 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 Binary files differindex 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 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 Binary files differnew file mode 100644 index 000000000..fddfa54b8 --- /dev/null +++ b/java/com/android/dialer/theme/res/drawable-xxhdpi/ic_block_24dp.png 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 Binary files differindex 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 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 Binary files differnew file mode 100644 index 000000000..0378d1bed --- /dev/null +++ b/java/com/android/dialer/theme/res/drawable-xxxhdpi/ic_block_24dp.png 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 Binary files differindex 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 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 Binary files differnew file mode 100644 index 000000000..f7ff6eb54 --- /dev/null +++ b/java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/cat_paw.webp 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 Binary files differnew file mode 100644 index 000000000..3a232542c --- /dev/null +++ b/java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/dog_paw.webp 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"><Not set></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/dialer/inject/DialerAppComponent.java b/java/com/android/voicemail/stub/StubVoicemailModule.java index 9832ce804..6c1552c15 100644 --- a/java/com/android/dialer/inject/DialerAppComponent.java +++ b/java/com/android/voicemail/stub/StubVoicemailModule.java @@ -14,16 +14,20 @@ * limitations under the License */ -package com.android.dialer.inject; +package com.android.voicemail.stub; -import com.android.dialer.enrichedcall.EnrichedCallManager; -import com.android.dialer.enrichedcall.StubEnrichedCallModule; -import dagger.Component; +import com.android.voicemail.VoicemailClient; +import dagger.Binds; +import dagger.Module; 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(); +/** + * 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/res/xml/voicemail_settings.xml b/java/com/android/voicemailomtp/res/xml/voicemail_settings.xml deleted file mode 100644 index 03bc34efc..000000000 --- a/java/com/android/voicemailomtp/res/xml/voicemail_settings.xml +++ /dev/null @@ -1,27 +0,0 @@ -<?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_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> 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 |