From d5e47f6da5b08b13ecdfa7f1edc7e12aeb83fab9 Mon Sep 17 00:00:00 2001 From: Eric Erfanian Date: Wed, 15 Mar 2017 14:41:07 -0700 Subject: Update Dialer source from latest green build. * Refactor voicemail component * Add new enriched calling components Test: treehugger, manual aosp testing Change-Id: I521a0f86327d4b42e14d93927c7d613044ed5942 --- .../android/incallui/AnswerScreenPresenter.java | 17 +- .../incallui/AnswerScreenPresenterStub.java | 2 +- java/com/android/incallui/CallButtonPresenter.java | 85 ++--- java/com/android/incallui/CallCardPresenter.java | 138 +++++--- .../com/android/incallui/CallerInfoAsyncQuery.java | 23 +- java/com/android/incallui/CallerInfoUtils.java | 18 +- java/com/android/incallui/ContactInfoCache.java | 332 ++++++++++++------ .../com/android/incallui/ExternalCallNotifier.java | 31 +- java/com/android/incallui/InCallActivity.java | 106 +++--- .../com/android/incallui/InCallActivityCommon.java | 27 +- java/com/android/incallui/InCallPresenter.java | 89 ++--- .../incallui/NotificationBroadcastReceiver.java | 10 +- java/com/android/incallui/ProximitySensor.java | 5 +- java/com/android/incallui/StatusBarNotifier.java | 278 ++++++++++----- java/com/android/incallui/VideoCallPresenter.java | 247 +++++++------- .../com/android/incallui/VideoPauseController.java | 236 +++++-------- .../incallui/answer/bindings/AnswerBindings.java | 4 +- .../incallui/answer/impl/AnswerFragment.java | 78 ++--- .../answer/impl/AnswerVideoCallScreen.java | 19 +- .../answer/impl/affordance/SwipeButtonHelper.java | 2 +- .../impl/answermethod/AnswerMethodHolder.java | 2 + .../impl/answermethod/FlingUpDownMethod.java | 15 +- .../incallui/answer/impl/hint/AndroidManifest.xml | 2 +- .../answer/impl/hint/AnswerHintFactory.java | 13 +- .../incallui/answer/impl/hint/EventAnswerHint.java | 235 ------------- .../answer/impl/hint/EventPayloadLoader.java | 30 -- .../answer/impl/hint/EventPayloadLoaderImpl.java | 118 ------- .../answer/impl/hint/EventSecretCodeListener.java | 67 ---- .../incallui/answer/impl/hint/PawAnswerHint.java | 236 +++++++++++++ .../incallui/answer/impl/hint/PawImageLoader.java | 28 ++ .../answer/impl/hint/PawImageLoaderImpl.java | 48 +++ .../answer/impl/hint/PawSecretCodeListener.java | 81 +++++ .../impl/hint/res/drawable-xxhdpi/cat_paw.webp | Bin 0 -> 68172 bytes .../impl/hint/res/drawable-xxhdpi/dog_paw.webp | Bin 0 -> 22704 bytes .../answer/impl/hint/res/layout/event_hint.xml | 36 -- .../answer/impl/hint/res/layout/paw_hint.xml | 37 ++ .../android/incallui/answer/impl/proguard.flags | 5 + .../incallui/answer/impl/res/values/dimens.xml | 1 + .../incallui/answer/protocol/AnswerScreen.java | 2 +- .../answer/protocol/AnswerScreenDelegate.java | 2 +- .../AnswerProximitySensor.java | 3 +- java/com/android/incallui/call/CallList.java | 50 ++- java/com/android/incallui/call/DialerCall.java | 377 ++++++++------------- .../android/incallui/call/DialerCallListener.java | 4 +- .../incallui/call/InCallVideoCallCallback.java | 197 ----------- .../call/InCallVideoCallCallbackNotifier.java | 165 +-------- java/com/android/incallui/call/VideoUtils.java | 103 +----- .../incallui/calllocation/CallLocation.java | 32 ++ .../calllocation/CallLocationComponent.java | 46 +++ .../incallui/calllocation/impl/AndroidManifest.xml | 26 ++ .../incallui/calllocation/impl/AuthException.java | 25 ++ .../calllocation/impl/CallLocationImpl.java | 67 ++++ .../calllocation/impl/CallLocationModule.java | 29 ++ .../calllocation/impl/DownloadMapImageTask.java | 77 +++++ .../impl/GoogleLocationSettingHelper.java | 123 +++++++ .../incallui/calllocation/impl/HttpFetcher.java | 289 ++++++++++++++++ .../calllocation/impl/LocationFragment.java | 197 +++++++++++ .../incallui/calllocation/impl/LocationHelper.java | 219 ++++++++++++ .../calllocation/impl/LocationPresenter.java | 98 ++++++ .../calllocation/impl/LocationUrlBuilder.java | 177 ++++++++++ .../calllocation/impl/ReverseGeocodeTask.java | 144 ++++++++ .../calllocation/impl/TrafficStatsTags.java | 29 ++ .../impl/res/layout/location_fragment.xml | 134 ++++++++ .../calllocation/impl/res/values/dimens.xml | 6 + .../calllocation/impl/res/values/strings.xml | 15 + .../calllocation/impl/res/values/styles.xml | 28 ++ .../calllocation/stub/StubCallLocationModule.java | 54 +++ .../incallui/commontheme/res/anim/blinking.xml | 10 + .../android/incallui/contactgrid/BottomRow.java | 13 +- .../incallui/contactgrid/ContactGridManager.java | 53 ++- java/com/android/incallui/contactgrid/TopRow.java | 20 +- .../res/layout/incall_contactgrid_bottom_row.xml | 2 +- .../hold/res/layout/incall_on_hold_banner.xml | 1 - .../AutoValue_MappedButtonConfig_MappingInfo.java | 135 -------- .../incallui/incall/impl/FakeDragAnimation.java | 62 ++++ .../incallui/incall/impl/InCallFragment.java | 57 ++-- .../incallui/incall/impl/InCallPagerAdapter.java | 25 +- .../incallui/incall/impl/MappedButtonConfig.java | 6 +- .../incallui/incall/protocol/PrimaryCallState.java | 26 +- .../incallui/incall/protocol/PrimaryInfo.java | 8 +- java/com/android/incallui/maps/Maps.java | 33 ++ java/com/android/incallui/maps/MapsComponent.java | 49 +++ .../android/incallui/maps/StaticMapBinding.java | 51 --- .../android/incallui/maps/StaticMapFactory.java | 28 -- .../android/incallui/maps/impl/AndroidManifest.xml | 26 ++ java/com/android/incallui/maps/impl/MapsImpl.java | 40 +++ .../com/android/incallui/maps/impl/MapsModule.java | 31 ++ .../incallui/maps/impl/StaticMapFragment.java | 76 +++++ .../maps/impl/res/layout/static_map_fragment.xml | 29 ++ .../android/incallui/maps/stub/StubMapsModule.java | 52 +++ .../incallui/maps/testing/TestMapsModule.java | 40 +++ java/com/android/incallui/res/values/strings.xml | 31 -- .../incallui/sessiondata/MultimediaFragment.java | 18 +- .../res/layout/fragment_composer_image.xml | 3 +- .../res/layout/fragment_composer_image_frag.xml | 8 + .../res/layout/fragment_composer_text_image.xml | 8 + .../layout/fragment_composer_text_image_frag.xml | 8 + .../incallui/spam/SpamCallListListener.java | 25 +- .../incallui/video/bindings/VideoBindings.java | 4 +- .../incallui/video/impl/VideoCallFragment.java | 43 ++- .../video/impl/res/layout/frag_videocall.xml | 11 +- .../incallui/video/impl/res/values/colors.xml | 20 ++ .../incallui/video/protocol/VideoCallScreen.java | 6 + java/com/android/incallui/videotech/VideoTech.java | 96 ++++++ .../incallui/videotech/empty/EmptyVideoTech.java | 76 +++++ .../videotech/ims/ImsVideoCallCallback.java | 201 +++++++++++ .../incallui/videotech/ims/ImsVideoTech.java | 212 ++++++++++++ .../incallui/videotech/rcs/RcsVideoShare.java | 195 +++++++++++ 108 files changed, 4931 insertions(+), 2326 deletions(-) delete mode 100644 java/com/android/incallui/answer/impl/hint/EventAnswerHint.java delete mode 100644 java/com/android/incallui/answer/impl/hint/EventPayloadLoader.java delete mode 100644 java/com/android/incallui/answer/impl/hint/EventPayloadLoaderImpl.java delete mode 100644 java/com/android/incallui/answer/impl/hint/EventSecretCodeListener.java create mode 100644 java/com/android/incallui/answer/impl/hint/PawAnswerHint.java create mode 100644 java/com/android/incallui/answer/impl/hint/PawImageLoader.java create mode 100644 java/com/android/incallui/answer/impl/hint/PawImageLoaderImpl.java create mode 100644 java/com/android/incallui/answer/impl/hint/PawSecretCodeListener.java create mode 100644 java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/cat_paw.webp create mode 100644 java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/dog_paw.webp delete mode 100644 java/com/android/incallui/answer/impl/hint/res/layout/event_hint.xml create mode 100644 java/com/android/incallui/answer/impl/hint/res/layout/paw_hint.xml create mode 100644 java/com/android/incallui/answer/impl/proguard.flags delete mode 100644 java/com/android/incallui/call/InCallVideoCallCallback.java create mode 100644 java/com/android/incallui/calllocation/CallLocation.java create mode 100644 java/com/android/incallui/calllocation/CallLocationComponent.java create mode 100644 java/com/android/incallui/calllocation/impl/AndroidManifest.xml create mode 100644 java/com/android/incallui/calllocation/impl/AuthException.java create mode 100644 java/com/android/incallui/calllocation/impl/CallLocationImpl.java create mode 100644 java/com/android/incallui/calllocation/impl/CallLocationModule.java create mode 100644 java/com/android/incallui/calllocation/impl/DownloadMapImageTask.java create mode 100644 java/com/android/incallui/calllocation/impl/GoogleLocationSettingHelper.java create mode 100644 java/com/android/incallui/calllocation/impl/HttpFetcher.java create mode 100644 java/com/android/incallui/calllocation/impl/LocationFragment.java create mode 100644 java/com/android/incallui/calllocation/impl/LocationHelper.java create mode 100644 java/com/android/incallui/calllocation/impl/LocationPresenter.java create mode 100644 java/com/android/incallui/calllocation/impl/LocationUrlBuilder.java create mode 100644 java/com/android/incallui/calllocation/impl/ReverseGeocodeTask.java create mode 100644 java/com/android/incallui/calllocation/impl/TrafficStatsTags.java create mode 100644 java/com/android/incallui/calllocation/impl/res/layout/location_fragment.xml create mode 100644 java/com/android/incallui/calllocation/impl/res/values/dimens.xml create mode 100644 java/com/android/incallui/calllocation/impl/res/values/strings.xml create mode 100644 java/com/android/incallui/calllocation/impl/res/values/styles.xml create mode 100644 java/com/android/incallui/calllocation/stub/StubCallLocationModule.java create mode 100644 java/com/android/incallui/commontheme/res/anim/blinking.xml delete mode 100644 java/com/android/incallui/incall/impl/AutoValue_MappedButtonConfig_MappingInfo.java create mode 100644 java/com/android/incallui/incall/impl/FakeDragAnimation.java create mode 100644 java/com/android/incallui/maps/Maps.java create mode 100644 java/com/android/incallui/maps/MapsComponent.java delete mode 100644 java/com/android/incallui/maps/StaticMapBinding.java delete mode 100644 java/com/android/incallui/maps/StaticMapFactory.java create mode 100644 java/com/android/incallui/maps/impl/AndroidManifest.xml create mode 100644 java/com/android/incallui/maps/impl/MapsImpl.java create mode 100644 java/com/android/incallui/maps/impl/MapsModule.java create mode 100644 java/com/android/incallui/maps/impl/StaticMapFragment.java create mode 100644 java/com/android/incallui/maps/impl/res/layout/static_map_fragment.xml create mode 100644 java/com/android/incallui/maps/stub/StubMapsModule.java create mode 100644 java/com/android/incallui/maps/testing/TestMapsModule.java create mode 100644 java/com/android/incallui/video/impl/res/values/colors.xml create mode 100644 java/com/android/incallui/videotech/VideoTech.java create mode 100644 java/com/android/incallui/videotech/empty/EmptyVideoTech.java create mode 100644 java/com/android/incallui/videotech/ims/ImsVideoCallCallback.java create mode 100644 java/com/android/incallui/videotech/ims/ImsVideoTech.java create mode 100644 java/com/android/incallui/videotech/rcs/RcsVideoShare.java (limited to 'java/com/android/incallui') 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 mInfoMap = new ConcurrentHashMap<>(); + private final ConcurrentHashMap mInfoMap = new ConcurrentHashMap<>(); private final Map> 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 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 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 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 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 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 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); @@ -917,20 +893,6 @@ public class VideoCallPresenter updateFullscreenAndGreenScreenMode(callState, sessionModificationState); } - /** - * 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. * @@ -958,17 +920,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 @@ -1023,42 +974,6 @@ public class VideoCallPresenter mVideoCallScreen.onLocalVideoDimensionsChanged(); } - /** - * 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. * @@ -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. + * + *

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(); @@ -305,11 +305,6 @@ public class AnswerFragment extends Fragment return Assert.isNotNull(getArguments().getString(ARG_CALL_ID)); } - @Override - public int getVideoState() { - return getArguments().getInt(ARG_VIDEO_STATE); - } - @Override public boolean isVideoUpgradeRequest() { return getArguments().getBoolean(ARG_IS_VIDEO_UPGRADE_REQUEST); @@ -317,7 +312,7 @@ public class AnswerFragment extends Fragment @Override public void setTextResponses(List 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"> - + 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/EventAnswerHint.java b/java/com/android/incallui/answer/impl/hint/EventAnswerHint.java deleted file mode 100644 index 7ee327d50..000000000 --- a/java/com/android/incallui/answer/impl/hint/EventAnswerHint.java +++ /dev/null @@ -1,235 +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.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.support.annotation.DimenRes; -import android.support.annotation.NonNull; -import android.support.v4.view.animation.FastOutSlowInInterpolator; -import android.util.TypedValue; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.Interpolator; -import android.view.animation.LinearInterpolator; -import android.widget.ImageView; -import android.widget.TextView; -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 { - - private static final long FADE_IN_DELAY_SCALE_MILLIS = 380; - private static final long FADE_IN_DURATION_SCALE_MILLIS = 200; - private static final long FADE_IN_DELAY_ALPHA_MILLIS = 340; - private static final long FADE_IN_DURATION_ALPHA_MILLIS = 50; - - private static final long SWIPE_UP_DURATION_ALPHA_MILLIS = 500; - - private static final long FADE_OUT_DELAY_SCALE_SMALL_MILLIS = 90; - private static final long FADE_OUT_DURATION_SCALE_MILLIS = 100; - 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 final Context context; - private final Drawable payload; - private final long puckUpDurationMillis; - private final long puckUpDelayMillis; - - private View puck; - private View payloadView; - private View answerHintContainer; - private AnimatorSet answerGestureHintAnim; - - public EventAnswerHint( - @NonNull Context context, - @NonNull Drawable payload, - long puckUpDurationMillis, - long puckUpDelayMillis) { - this.context = Assert.isNotNull(context); - this.payload = Assert.isNotNull(payload); - this.puckUpDurationMillis = puckUpDurationMillis; - this.puckUpDelayMillis = puckUpDelayMillis; - } - - @Override - public void onCreateView( - LayoutInflater inflater, ViewGroup container, View puck, TextView hintText) { - this.puck = puck; - View view = inflater.inflate(R.layout.event_hint, container, true); - answerHintContainer = view.findViewById(R.id.answer_hint_container); - payloadView = view.findViewById(R.id.payload); - hintText.setTextSize( - TypedValue.COMPLEX_UNIT_PX, context.getResources().getDimension(R.dimen.hint_text_size)); - ((ImageView) payloadView).setImageDrawable(payload); - } - - @Override - public void onBounceStart() { - if (answerGestureHintAnim == null) { - - answerGestureHintAnim = new AnimatorSet(); - answerHintContainer.setY(puck.getY() + getDimension(R.dimen.hint_initial_offset)); - - Animator fadeIn = createFadeIn(); - - Animator swipeUp = - ObjectAnimator.ofFloat( - answerHintContainer, - View.TRANSLATION_Y, - puck.getY() - getDimension(R.dimen.hint_offset)); - swipeUp.setInterpolator(new FastOutSlowInInterpolator()); - swipeUp.setDuration(SWIPE_UP_DURATION_ALPHA_MILLIS); - - Animator fadeOut = createFadeOut(); - - answerGestureHintAnim.play(fadeIn).after(puckUpDelayMillis); - answerGestureHintAnim.play(swipeUp).after(fadeIn); - // The fade out should start fading the alpha just as the puck is dropping. Scaling will start - // a bit earlier. - answerGestureHintAnim - .play(fadeOut) - .after(puckUpDelayMillis + puckUpDurationMillis - FADE_OUT_DELAY_ALPHA_MILLIS); - - fadeIn.addListener( - new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - super.onAnimationStart(animation); - payloadView.setAlpha(0); - payloadView.setScaleX(1); - payloadView.setScaleY(1); - answerHintContainer.setY(puck.getY() + getDimension(R.dimen.hint_initial_offset)); - answerHintContainer.setVisibility(View.VISIBLE); - } - }); - } - - answerGestureHintAnim.start(); - } - - private Animator createFadeIn() { - AnimatorSet set = new AnimatorSet(); - set.play(createFadeInScaleAndAlpha(payloadView)); - return set; - } - - private static Animator createFadeInScaleAndAlpha(View target) { - Animator scale = - createUniformScaleAnimator( - target, - FADE_SCALE, - 1.0f, - FADE_IN_DURATION_SCALE_MILLIS, - FADE_IN_DELAY_SCALE_MILLIS, - new LinearInterpolator()); - Animator alpha = - createAlphaAnimator( - target, - 0f, - 1.0f, - FADE_IN_DURATION_ALPHA_MILLIS, - FADE_IN_DELAY_ALPHA_MILLIS, - new LinearInterpolator()); - AnimatorSet set = new AnimatorSet(); - set.play(scale).with(alpha); - return set; - } - - private Animator createFadeOut() { - AnimatorSet set = new AnimatorSet(); - set.play(createFadeOutScaleAndAlpha(payloadView, FADE_OUT_DELAY_SCALE_SMALL_MILLIS)); - return set; - } - - private static Animator createFadeOutScaleAndAlpha(View target, long scaleDelay) { - Animator scale = - createUniformScaleAnimator( - target, - 1.0f, - FADE_SCALE, - FADE_OUT_DURATION_SCALE_MILLIS, - scaleDelay, - new LinearInterpolator()); - Animator alpha = - createAlphaAnimator( - target, - 01.0f, - 0.0f, - FADE_OUT_DURATION_ALPHA_MILLIS, - FADE_OUT_DELAY_ALPHA_MILLIS, - new LinearInterpolator()); - AnimatorSet set = new AnimatorSet(); - set.play(scale).with(alpha); - return set; - } - - @Override - public void onBounceEnd() { - if (answerGestureHintAnim != null) { - answerGestureHintAnim.end(); - answerGestureHintAnim = null; - answerHintContainer.setVisibility(View.GONE); - } - } - - @Override - public void onAnswered() { - // Do nothing - } - - private float getDimension(@DimenRes int id) { - return context.getResources().getDimension(id); - } - - private static Animator createUniformScaleAnimator( - View target, - float scaleBegin, - float scaleEnd, - long duration, - long delay, - Interpolator interpolator) { - Animator scaleX = ObjectAnimator.ofFloat(target, View.SCALE_X, scaleBegin, scaleEnd); - Animator scaleY = ObjectAnimator.ofFloat(target, View.SCALE_Y, scaleBegin, scaleEnd); - scaleX.setDuration(duration); - scaleY.setDuration(duration); - scaleX.setInterpolator(interpolator); - scaleY.setInterpolator(interpolator); - AnimatorSet set = new AnimatorSet(); - set.play(scaleX).with(scaleY).after(delay); - return set; - } - - private static Animator createAlphaAnimator( - View target, float begin, float end, long duration, long delay, Interpolator interpolator) { - Animator alpha = ObjectAnimator.ofFloat(target, View.ALPHA, begin, end); - alpha.setDuration(duration); - alpha.setInterpolator(interpolator); - alpha.setStartDelay(delay); - return alpha; - } -} diff --git a/java/com/android/incallui/answer/impl/hint/EventPayloadLoader.java b/java/com/android/incallui/answer/impl/hint/EventPayloadLoader.java deleted file mode 100644 index 09e3bedf2..000000000 --- a/java/com/android/incallui/answer/impl/hint/EventPayloadLoader.java +++ /dev/null @@ -1,30 +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.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 { - @Nullable - Drawable loadPayload( - @NonNull Context context, long currentTimeUtcMillis, @NonNull TimeZone timeZone); -} 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 -out -pass 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/EventSecretCodeListener.java b/java/com/android/incallui/answer/impl/hint/EventSecretCodeListener.java deleted file mode 100644 index 7cf4054a9..000000000 --- a/java/com/android/incallui/answer/impl/hint/EventSecretCodeListener.java +++ /dev/null @@ -1,67 +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.answer.impl.hint; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.support.annotation.VisibleForTesting; -import android.text.TextUtils; -import android.widget.Toast; -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; - -/** - * Listen to the broadcast when the user dials "*#*#[number]#*#*" to toggle the event answer hint. - */ -public class EventSecretCodeListener extends BroadcastReceiver { - - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - static final String CONFIG_EVENT_SECRET_CODE = "event_secret_code"; - - public static final String EVENT_ENABLED_WITH_SECRET_CODE_KEY = "event_enabled_with_secret_code"; - - @Override - public void onReceive(Context context, Intent intent) { - String host = intent.getData().getHost(); - String secretCode = - ConfigProviderBindings.get(context).getString(CONFIG_EVENT_SECRET_CODE, null); - if (secretCode == null) { - return; - } - if (!TextUtils.equals(secretCode, host)) { - return; - } - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); - boolean wasEnabled = preferences.getBoolean(EVENT_ENABLED_WITH_SECRET_CODE_KEY, false); - if (wasEnabled) { - preferences.edit().putBoolean(EVENT_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"); - } else { - preferences.edit().putBoolean(EVENT_ENABLED_WITH_SECRET_CODE_KEY, true).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"); - } - } -} diff --git a/java/com/android/incallui/answer/impl/hint/PawAnswerHint.java b/java/com/android/incallui/answer/impl/hint/PawAnswerHint.java new file mode 100644 index 000000000..36b761f57 --- /dev/null +++ b/java/com/android/incallui/answer/impl/hint/PawAnswerHint.java @@ -0,0 +1,236 @@ +/* + * 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.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.support.annotation.DimenRes; +import android.support.annotation.NonNull; +import android.support.v4.view.animation.FastOutSlowInInterpolator; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Interpolator; +import android.view.animation.LinearInterpolator; +import android.widget.ImageView; +import android.widget.TextView; +import com.android.dialer.common.Assert; + +/** + * An Answer hint that animates a {@link Drawable} payload with animation similar to {@link + * DotAnswerHint}. + */ +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; + private static final long FADE_IN_DELAY_ALPHA_MILLIS = 340; + private static final long FADE_IN_DURATION_ALPHA_MILLIS = 50; + + private static final long SWIPE_UP_DURATION_ALPHA_MILLIS = 500; + + private static final long FADE_OUT_DELAY_SCALE_SMALL_MILLIS = 90; + private static final long FADE_OUT_DURATION_SCALE_MILLIS = 100; + private static final long FADE_OUT_DELAY_ALPHA_MILLIS = 130; + private static final long FADE_OUT_DURATION_ALPHA_MILLIS = 170; + + private static final float IMAGE_SCALE = 1.5f; + private static final float FADE_SCALE = 2.0f; + + private final Context context; + private final Drawable payload; + private final long puckUpDurationMillis; + private final long puckUpDelayMillis; + + private View puck; + private View payloadView; + private View answerHintContainer; + private AnimatorSet answerGestureHintAnim; + + public PawAnswerHint( + @NonNull Context context, + @NonNull Drawable payload, + long puckUpDurationMillis, + long puckUpDelayMillis) { + this.context = Assert.isNotNull(context); + this.payload = Assert.isNotNull(payload); + this.puckUpDurationMillis = puckUpDurationMillis; + this.puckUpDelayMillis = puckUpDelayMillis; + } + + @Override + public void onCreateView( + LayoutInflater inflater, ViewGroup container, View puck, TextView hintText) { + this.puck = puck; + View view = inflater.inflate(R.layout.paw_hint, container, true); + answerHintContainer = view.findViewById(R.id.answer_hint_container); + 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); + } + + @Override + public void onBounceStart() { + if (answerGestureHintAnim == null) { + + answerGestureHintAnim = new AnimatorSet(); + answerHintContainer.setY(puck.getY() + getDimension(R.dimen.hint_initial_offset)); + + Animator fadeIn = createFadeIn(); + + Animator swipeUp = + ObjectAnimator.ofFloat( + answerHintContainer, + View.TRANSLATION_Y, + puck.getY() - getDimension(R.dimen.hint_offset)); + swipeUp.setInterpolator(new FastOutSlowInInterpolator()); + swipeUp.setDuration(SWIPE_UP_DURATION_ALPHA_MILLIS); + + Animator fadeOut = createFadeOut(); + + answerGestureHintAnim.play(fadeIn).after(puckUpDelayMillis); + answerGestureHintAnim.play(swipeUp).after(fadeIn); + // The fade out should start fading the alpha just as the puck is dropping. Scaling will start + // a bit earlier. + answerGestureHintAnim + .play(fadeOut) + .after(puckUpDelayMillis + puckUpDurationMillis - FADE_OUT_DELAY_ALPHA_MILLIS); + + fadeIn.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + payloadView.setAlpha(0); + payloadView.setScaleX(1); + payloadView.setScaleY(1); + answerHintContainer.setY(puck.getY() + getDimension(R.dimen.hint_initial_offset)); + answerHintContainer.setVisibility(View.VISIBLE); + } + }); + } + + answerGestureHintAnim.start(); + } + + private Animator createFadeIn() { + AnimatorSet set = new AnimatorSet(); + set.play(createFadeInScaleAndAlpha(payloadView)); + return set; + } + + private static Animator createFadeInScaleAndAlpha(View target) { + Animator scale = + createUniformScaleAnimator( + target, + FADE_SCALE, + IMAGE_SCALE, + FADE_IN_DURATION_SCALE_MILLIS, + FADE_IN_DELAY_SCALE_MILLIS, + new LinearInterpolator()); + Animator alpha = + createAlphaAnimator( + target, + 0f, + 1.0f, + FADE_IN_DURATION_ALPHA_MILLIS, + FADE_IN_DELAY_ALPHA_MILLIS, + new LinearInterpolator()); + AnimatorSet set = new AnimatorSet(); + set.play(scale).with(alpha); + return set; + } + + private Animator createFadeOut() { + AnimatorSet set = new AnimatorSet(); + set.play(createFadeOutScaleAndAlpha(payloadView, FADE_OUT_DELAY_SCALE_SMALL_MILLIS)); + return set; + } + + private static Animator createFadeOutScaleAndAlpha(View target, long scaleDelay) { + Animator scale = + createUniformScaleAnimator( + target, + IMAGE_SCALE, + FADE_SCALE, + FADE_OUT_DURATION_SCALE_MILLIS, + scaleDelay, + new LinearInterpolator()); + Animator alpha = + createAlphaAnimator( + target, + 1.0f, + 0.0f, + FADE_OUT_DURATION_ALPHA_MILLIS, + FADE_OUT_DELAY_ALPHA_MILLIS, + new LinearInterpolator()); + AnimatorSet set = new AnimatorSet(); + set.play(scale).with(alpha); + return set; + } + + @Override + public void onBounceEnd() { + if (answerGestureHintAnim != null) { + answerGestureHintAnim.end(); + answerGestureHintAnim = null; + answerHintContainer.setVisibility(View.GONE); + } + } + + @Override + public void onAnswered() { + // Do nothing + } + + private float getDimension(@DimenRes int id) { + return context.getResources().getDimension(id); + } + + private static Animator createUniformScaleAnimator( + View target, + float scaleBegin, + float scaleEnd, + long duration, + long delay, + Interpolator interpolator) { + Animator scaleX = ObjectAnimator.ofFloat(target, View.SCALE_X, scaleBegin, scaleEnd); + Animator scaleY = ObjectAnimator.ofFloat(target, View.SCALE_Y, scaleBegin, scaleEnd); + scaleX.setDuration(duration); + scaleY.setDuration(duration); + scaleX.setInterpolator(interpolator); + scaleY.setInterpolator(interpolator); + AnimatorSet set = new AnimatorSet(); + set.play(scaleX).with(scaleY).after(delay); + return set; + } + + private static Animator createAlphaAnimator( + View target, float begin, float end, long duration, long delay, Interpolator interpolator) { + Animator alpha = ObjectAnimator.ofFloat(target, View.ALPHA, begin, end); + alpha.setDuration(duration); + alpha.setInterpolator(interpolator); + alpha.setStartDelay(delay); + return alpha; + } +} diff --git a/java/com/android/incallui/answer/impl/hint/PawImageLoader.java b/java/com/android/incallui/answer/impl/hint/PawImageLoader.java new file mode 100644 index 000000000..09e700fe0 --- /dev/null +++ b/java/com/android/incallui/answer/impl/hint/PawImageLoader.java @@ -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 + */ + +package com.android.incallui.answer.impl.hint; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +/** Loads a {@link Drawable} payload for the {@link PawAnswerHint} if it should be displayed. */ +public interface PawImageLoader { + @Nullable + 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/PawSecretCodeListener.java b/java/com/android/incallui/answer/impl/hint/PawSecretCodeListener.java new file mode 100644 index 000000000..b4fc19c0d --- /dev/null +++ b/java/com/android/incallui/answer/impl/hint/PawSecretCodeListener.java @@ -0,0 +1,81 @@ +/* + * 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.answer.impl.hint; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +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.Type; +import java.util.Random; + +/** + * Listen to the broadcast when the user dials "*#*#[number]#*#*" to toggle the event answer hint. + */ +public class PawSecretCodeListener extends BroadcastReceiver { + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + static final String CONFIG_PAW_SECRET_CODE = "paw_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_PAW_SECRET_CODE, "729"); + if (secretCode == null) { + return; + } + if (!TextUtils.equals(secretCode, host)) { + return; + } + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + boolean wasEnabled = preferences.getBoolean(PAW_ENABLED_WITH_SECRET_CODE_KEY, false); + if (wasEnabled) { + 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(Type.EVENT_ANSWER_HINT_DEACTIVATED); + LogUtil.i("PawSecretCodeListener.onReceive", "PawAnswerHint disabled"); + } else { + 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(Type.EVENT_ANSWER_HINT_ACTIVATED); + LogUtil.i("PawSecretCodeListener.onReceive", "PawAnswerHint enabled"); + } + } +} diff --git a/java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/cat_paw.webp b/java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/cat_paw.webp new file mode 100644 index 000000000..f7ff6eb54 Binary files /dev/null and b/java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/cat_paw.webp differ diff --git a/java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/dog_paw.webp b/java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/dog_paw.webp new file mode 100644 index 000000000..3a232542c Binary files /dev/null and b/java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/dog_paw.webp differ diff --git a/java/com/android/incallui/answer/impl/hint/res/layout/event_hint.xml b/java/com/android/incallui/answer/impl/hint/res/layout/event_hint.xml deleted file mode 100644 index d505014c1..000000000 --- a/java/com/android/incallui/answer/impl/hint/res/layout/event_hint.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - \ No newline at end of file diff --git a/java/com/android/incallui/answer/impl/hint/res/layout/paw_hint.xml b/java/com/android/incallui/answer/impl/hint/res/layout/paw_hint.xml new file mode 100644 index 000000000..c3b12a01d --- /dev/null +++ b/java/com/android/incallui/answer/impl/hint/res/layout/paw_hint.xml @@ -0,0 +1,37 @@ + + + + + \ No newline at end of file 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 (android.content.Context, android.util.AttributeSet); + public (); +} \ 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 @@ 0dp 0dp false + 1000 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 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 mListeners = new CopyOnWriteArrayList<>(); private final List 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 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() { @@ -861,24 +769,6 @@ public class DialerCall { && hasProperty(CallCompat.Details.PROPERTY_IS_EXTERNAL_CALL); } - /** - * Determines if the external call is pullable. - * - *

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. - * - *

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. * @@ -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 mSessionModificationListeners = - Collections.newSetFromMap( - new ConcurrentHashMap(8, 0.9f, 1)); - - private final Set mVideoEventListeners = - Collections.newSetFromMap(new ConcurrentHashMap(8, 0.9f, 1)); private final Set mSurfaceChangeListeners = Collections.newSetFromMap(new ConcurrentHashMap(8, 0.9f, 1)); @@ -54,48 +44,6 @@ public class InCallVideoCallCallbackNotifier { return sInstance; } - /** - * 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}. * @@ -117,56 +65,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. * @@ -193,67 +91,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. */ 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 @@ + + + + + + + + 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 { + + private static final String STATIC_MAP_SRC_NAME = "src"; + + private final WeakReference mUiReference; + + public DownloadMapImageTask(WeakReference 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> 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> 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> headers) + throws ProtocolException { + conn.setRequestMethod(requestMethod); + if (headers != null) { + for (Pair 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 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> 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. + * + *

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. + * + *

If location data is inaccurate, stale, or unavailable, this should not be shown. + */ +public class LocationFragment extends BaseFragment + 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 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() { + @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}. + * + *

Performs lookup for the address and map image to show. + */ +public class LocationPresenter extends Presenter + 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. + * + *

This image has the following characteristics: + * + *

- 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. + * + *

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. + * + *

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 { + + // 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 mUiReference; + + public ReverseGeocodeTask(WeakReference 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + 92dp + 92dp + 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 @@ + + + + + Emergency Location Map + + + You are here + + %f, %f + + + Finding your location + + 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 @@ + + + + + + + + + + + + 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 @@ + + + + \ 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: * *

    - *
  • Mobile +1 (650) 253-0000 - *
  • [HD icon] 00:15 - *
  • Call ended - *
  • Hanging up + *
  • Mobile +1 (650) 253-0000 + *
  • [HD attempting icon]/[HD icon] 00:15 + *
  • Call ended + *
  • Hanging up *
*/ 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(); @@ -226,6 +229,24 @@ public class ContactGridManager { } } + /** + * Returns the appropriate LetterTileDrawable.TYPE_ based on a given call state. + * + *

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: * @@ -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: * *

    *
  • Mobile +1 (650) 253-0000 - *
  • [HD icon] 00:15 + *
  • [HD attempting icon]/[HD icon] 00:15 *
  • Call ended *
  • Hanging up *
@@ -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"> 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/StaticMapFactory.java b/java/com/android/incallui/maps/StaticMapFactory.java deleted file mode 100644 index a35013886..000000000 --- a/java/com/android/incallui/maps/StaticMapFactory.java +++ /dev/null @@ -1,28 +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.location.Location; -import android.support.annotation.NonNull; -import android.support.v4.app.Fragment; - -/** A Factory that can create Fragments for showing a static map */ -public interface StaticMapFactory { - - @NonNull - Fragment getStaticMap(@NonNull Location location); -} 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 @@ + + + + + + + + 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 @@ + + + + + + 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 @@ ABSENTNUMBER - - Service - - - Setup - - - <Not set> - Other call settings @@ -242,26 +231,6 @@ select contact - - Vibrate - - Vibrate - - - Sound - - - never - - - - always - silent - never - - 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"/> 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"/> + + + + 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"/> + + + android:background="@color/videocall_overlay_background_color" + tools:visibility="gone"/> + android:background="@color/videocall_overlay_background_color" + tools:visibility="gone"/> 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 @@ + + + + + #89ffffff + 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; + } +} -- cgit v1.2.3