summaryrefslogtreecommitdiff
path: root/java/com/android/incallui
diff options
context:
space:
mode:
authorEric Erfanian <erfanian@google.com>2017-03-15 14:41:07 -0700
committerEric Erfanian <erfanian@google.com>2017-03-15 16:24:23 -0700
commitd5e47f6da5b08b13ecdfa7f1edc7e12aeb83fab9 (patch)
treeb54abbb51fb7d66e7755a1fbb5db023ff601090b /java/com/android/incallui
parent30436e7e6d3f2c8755a91b2b6222b74d465a9e87 (diff)
Update Dialer source from latest green build.
* Refactor voicemail component * Add new enriched calling components Test: treehugger, manual aosp testing Change-Id: I521a0f86327d4b42e14d93927c7d613044ed5942
Diffstat (limited to 'java/com/android/incallui')
-rw-r--r--java/com/android/incallui/AnswerScreenPresenter.java17
-rw-r--r--java/com/android/incallui/AnswerScreenPresenterStub.java2
-rw-r--r--java/com/android/incallui/CallButtonPresenter.java85
-rw-r--r--java/com/android/incallui/CallCardPresenter.java138
-rw-r--r--java/com/android/incallui/CallerInfoAsyncQuery.java23
-rw-r--r--java/com/android/incallui/CallerInfoUtils.java18
-rw-r--r--java/com/android/incallui/ContactInfoCache.java332
-rw-r--r--java/com/android/incallui/ExternalCallNotifier.java31
-rw-r--r--java/com/android/incallui/InCallActivity.java106
-rw-r--r--java/com/android/incallui/InCallActivityCommon.java27
-rw-r--r--java/com/android/incallui/InCallPresenter.java89
-rw-r--r--java/com/android/incallui/NotificationBroadcastReceiver.java10
-rw-r--r--java/com/android/incallui/ProximitySensor.java5
-rw-r--r--java/com/android/incallui/StatusBarNotifier.java278
-rw-r--r--java/com/android/incallui/VideoCallPresenter.java247
-rw-r--r--java/com/android/incallui/VideoPauseController.java236
-rw-r--r--java/com/android/incallui/answer/bindings/AnswerBindings.java4
-rw-r--r--java/com/android/incallui/answer/impl/AnswerFragment.java78
-rw-r--r--java/com/android/incallui/answer/impl/AnswerVideoCallScreen.java19
-rw-r--r--java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java2
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/AnswerMethodHolder.java2
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java15
-rw-r--r--java/com/android/incallui/answer/impl/hint/AndroidManifest.xml2
-rw-r--r--java/com/android/incallui/answer/impl/hint/AnswerHintFactory.java13
-rw-r--r--java/com/android/incallui/answer/impl/hint/EventPayloadLoaderImpl.java118
-rw-r--r--java/com/android/incallui/answer/impl/hint/PawAnswerHint.java (renamed from java/com/android/incallui/answer/impl/hint/EventAnswerHint.java)17
-rw-r--r--java/com/android/incallui/answer/impl/hint/PawImageLoader.java (renamed from java/com/android/incallui/answer/impl/hint/EventPayloadLoader.java)8
-rw-r--r--java/com/android/incallui/answer/impl/hint/PawImageLoaderImpl.java48
-rw-r--r--java/com/android/incallui/answer/impl/hint/PawSecretCodeListener.java (renamed from java/com/android/incallui/answer/impl/hint/EventSecretCodeListener.java)38
-rw-r--r--java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/cat_paw.webpbin0 -> 68172 bytes
-rw-r--r--java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/dog_paw.webpbin0 -> 22704 bytes
-rw-r--r--java/com/android/incallui/answer/impl/hint/res/layout/paw_hint.xml (renamed from java/com/android/incallui/answer/impl/hint/res/layout/event_hint.xml)7
-rw-r--r--java/com/android/incallui/answer/impl/proguard.flags5
-rw-r--r--java/com/android/incallui/answer/impl/res/values/dimens.xml1
-rw-r--r--java/com/android/incallui/answer/protocol/AnswerScreen.java2
-rw-r--r--java/com/android/incallui/answer/protocol/AnswerScreenDelegate.java2
-rw-r--r--java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java3
-rw-r--r--java/com/android/incallui/call/CallList.java50
-rw-r--r--java/com/android/incallui/call/DialerCall.java377
-rw-r--r--java/com/android/incallui/call/DialerCallListener.java4
-rw-r--r--java/com/android/incallui/call/InCallVideoCallCallback.java197
-rw-r--r--java/com/android/incallui/call/InCallVideoCallCallbackNotifier.java165
-rw-r--r--java/com/android/incallui/call/VideoUtils.java103
-rw-r--r--java/com/android/incallui/calllocation/CallLocation.java (renamed from java/com/android/incallui/maps/StaticMapFactory.java)16
-rw-r--r--java/com/android/incallui/calllocation/CallLocationComponent.java46
-rw-r--r--java/com/android/incallui/calllocation/impl/AndroidManifest.xml26
-rw-r--r--java/com/android/incallui/calllocation/impl/AuthException.java25
-rw-r--r--java/com/android/incallui/calllocation/impl/CallLocationImpl.java67
-rw-r--r--java/com/android/incallui/calllocation/impl/CallLocationModule.java29
-rw-r--r--java/com/android/incallui/calllocation/impl/DownloadMapImageTask.java77
-rw-r--r--java/com/android/incallui/calllocation/impl/GoogleLocationSettingHelper.java123
-rw-r--r--java/com/android/incallui/calllocation/impl/HttpFetcher.java289
-rw-r--r--java/com/android/incallui/calllocation/impl/LocationFragment.java197
-rw-r--r--java/com/android/incallui/calllocation/impl/LocationHelper.java219
-rw-r--r--java/com/android/incallui/calllocation/impl/LocationPresenter.java98
-rw-r--r--java/com/android/incallui/calllocation/impl/LocationUrlBuilder.java177
-rw-r--r--java/com/android/incallui/calllocation/impl/ReverseGeocodeTask.java144
-rw-r--r--java/com/android/incallui/calllocation/impl/TrafficStatsTags.java29
-rw-r--r--java/com/android/incallui/calllocation/impl/res/layout/location_fragment.xml134
-rw-r--r--java/com/android/incallui/calllocation/impl/res/values/dimens.xml6
-rw-r--r--java/com/android/incallui/calllocation/impl/res/values/strings.xml15
-rw-r--r--java/com/android/incallui/calllocation/impl/res/values/styles.xml28
-rw-r--r--java/com/android/incallui/calllocation/stub/StubCallLocationModule.java54
-rw-r--r--java/com/android/incallui/commontheme/res/anim/blinking.xml10
-rw-r--r--java/com/android/incallui/contactgrid/BottomRow.java13
-rw-r--r--java/com/android/incallui/contactgrid/ContactGridManager.java53
-rw-r--r--java/com/android/incallui/contactgrid/TopRow.java20
-rw-r--r--java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_bottom_row.xml2
-rw-r--r--java/com/android/incallui/hold/res/layout/incall_on_hold_banner.xml1
-rw-r--r--java/com/android/incallui/incall/impl/AutoValue_MappedButtonConfig_MappingInfo.java135
-rw-r--r--java/com/android/incallui/incall/impl/FakeDragAnimation.java62
-rw-r--r--java/com/android/incallui/incall/impl/InCallFragment.java57
-rw-r--r--java/com/android/incallui/incall/impl/InCallPagerAdapter.java25
-rw-r--r--java/com/android/incallui/incall/impl/MappedButtonConfig.java6
-rw-r--r--java/com/android/incallui/incall/protocol/PrimaryCallState.java26
-rw-r--r--java/com/android/incallui/incall/protocol/PrimaryInfo.java8
-rw-r--r--java/com/android/incallui/maps/Maps.java33
-rw-r--r--java/com/android/incallui/maps/MapsComponent.java49
-rw-r--r--java/com/android/incallui/maps/StaticMapBinding.java51
-rw-r--r--java/com/android/incallui/maps/impl/AndroidManifest.xml26
-rw-r--r--java/com/android/incallui/maps/impl/MapsImpl.java40
-rw-r--r--java/com/android/incallui/maps/impl/MapsModule.java31
-rw-r--r--java/com/android/incallui/maps/impl/StaticMapFragment.java76
-rw-r--r--java/com/android/incallui/maps/impl/res/layout/static_map_fragment.xml29
-rw-r--r--java/com/android/incallui/maps/stub/StubMapsModule.java52
-rw-r--r--java/com/android/incallui/maps/testing/TestMapsModule.java40
-rw-r--r--java/com/android/incallui/res/values/strings.xml31
-rw-r--r--java/com/android/incallui/sessiondata/MultimediaFragment.java18
-rw-r--r--java/com/android/incallui/sessiondata/res/layout/fragment_composer_image.xml3
-rw-r--r--java/com/android/incallui/sessiondata/res/layout/fragment_composer_image_frag.xml8
-rw-r--r--java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image.xml8
-rw-r--r--java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image_frag.xml8
-rw-r--r--java/com/android/incallui/spam/SpamCallListListener.java25
-rw-r--r--java/com/android/incallui/video/bindings/VideoBindings.java4
-rw-r--r--java/com/android/incallui/video/impl/VideoCallFragment.java43
-rw-r--r--java/com/android/incallui/video/impl/res/layout/frag_videocall.xml11
-rw-r--r--java/com/android/incallui/video/impl/res/values/colors.xml20
-rw-r--r--java/com/android/incallui/video/protocol/VideoCallScreen.java6
-rw-r--r--java/com/android/incallui/videotech/VideoTech.java96
-rw-r--r--java/com/android/incallui/videotech/empty/EmptyVideoTech.java76
-rw-r--r--java/com/android/incallui/videotech/ims/ImsVideoCallCallback.java201
-rw-r--r--java/com/android/incallui/videotech/ims/ImsVideoTech.java212
-rw-r--r--java/com/android/incallui/videotech/rcs/RcsVideoShare.java195
103 files changed, 4569 insertions, 1964 deletions
diff --git a/java/com/android/incallui/AnswerScreenPresenter.java b/java/com/android/incallui/AnswerScreenPresenter.java
index a21876b2b..442ad260f 100644
--- a/java/com/android/incallui/AnswerScreenPresenter.java
+++ b/java/com/android/incallui/AnswerScreenPresenter.java
@@ -20,6 +20,7 @@ import android.content.Context;
import android.support.annotation.FloatRange;
import android.support.annotation.NonNull;
import android.support.v4.os.UserManagerCompat;
+import android.telecom.VideoProfile;
import com.android.dialer.common.Assert;
import com.android.dialer.common.LogUtil;
import com.android.incallui.answer.protocol.AnswerScreen;
@@ -71,18 +72,26 @@ public class AnswerScreenPresenter
}
@Override
- public void onAnswer(int videoState) {
+ public void onAnswer(boolean answerVideoAsAudio) {
if (answerScreen.isVideoUpgradeRequest()) {
- call.acceptUpgradeRequest(videoState);
+ if (answerVideoAsAudio) {
+ call.getVideoTech().acceptVideoRequestAsAudio();
+ } else {
+ call.getVideoTech().acceptVideoRequest();
+ }
} else {
- call.answer(videoState);
+ if (answerVideoAsAudio) {
+ call.answer(VideoProfile.STATE_AUDIO_ONLY);
+ } else {
+ call.answer();
+ }
}
}
@Override
public void onReject() {
if (answerScreen.isVideoUpgradeRequest()) {
- call.declineUpgradeRequest();
+ call.getVideoTech().declineVideoRequest();
} else {
call.reject(false /* rejectWithMessage */, null);
}
diff --git a/java/com/android/incallui/AnswerScreenPresenterStub.java b/java/com/android/incallui/AnswerScreenPresenterStub.java
index fc47bf5b0..fc4e7df65 100644
--- a/java/com/android/incallui/AnswerScreenPresenterStub.java
+++ b/java/com/android/incallui/AnswerScreenPresenterStub.java
@@ -34,7 +34,7 @@ public class AnswerScreenPresenterStub implements AnswerScreenDelegate {
public void onRejectCallWithMessage(String message) {}
@Override
- public void onAnswer(int videoState) {}
+ public void onAnswer(boolean answerVideoAsAudio) {}
@Override
public void onReject() {}
diff --git a/java/com/android/incallui/CallButtonPresenter.java b/java/com/android/incallui/CallButtonPresenter.java
index d6f4cddc9..c5c43f7aa 100644
--- a/java/com/android/incallui/CallButtonPresenter.java
+++ b/java/com/android/incallui/CallButtonPresenter.java
@@ -17,17 +17,13 @@
package com.android.incallui;
import android.content.Context;
-import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.os.UserManagerCompat;
import android.telecom.CallAudioState;
-import android.telecom.InCallService.VideoCall;
-import android.telecom.VideoProfile;
import com.android.contacts.common.compat.CallCompat;
import com.android.dialer.common.Assert;
import com.android.dialer.common.LogUtil;
-import com.android.dialer.compat.SdkVersionOverride;
import com.android.dialer.logging.Logger;
import com.android.dialer.logging.nano.DialerImpression;
import com.android.incallui.AudioModeProvider.AudioModeListener;
@@ -39,6 +35,7 @@ import com.android.incallui.InCallPresenter.InCallStateListener;
import com.android.incallui.InCallPresenter.IncomingCallListener;
import com.android.incallui.call.CallList;
import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.DialerCall.CameraDirection;
import com.android.incallui.call.TelecomAdapter;
import com.android.incallui.call.VideoUtils;
import com.android.incallui.incall.protocol.InCallButtonIds;
@@ -212,6 +209,13 @@ public class CallButtonPresenter
@Override
public void muteClicked(boolean checked) {
LogUtil.v("CallButtonPresenter", "turning on mute: " + checked);
+ Logger.get(mContext)
+ .logCallImpression(
+ checked
+ ? DialerImpression.Type.IN_CALL_SCREEN_TURN_ON_MUTE
+ : DialerImpression.Type.IN_CALL_SCREEN_TURN_OFF_MUTE,
+ mCall.getUniqueCallId(),
+ mCall.getTimeAddedMs());
TelecomAdapter.getInstance().mute(checked);
}
@@ -262,18 +266,8 @@ public class CallButtonPresenter
@Override
public void changeToVideoClicked() {
- VideoCall videoCall = mCall.getVideoCall();
- if (videoCall == null) {
- return;
- }
- int currVideoState = mCall.getVideoState();
- int currUnpausedVideoState = VideoUtils.getUnPausedVideoState(currVideoState);
- currUnpausedVideoState |= VideoProfile.STATE_BIDIRECTIONAL;
-
- VideoProfile videoProfile = new VideoProfile(currUnpausedVideoState);
- videoCall.sendSessionModifyRequest(videoProfile);
- mCall.setSessionModificationState(
- DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE);
+ LogUtil.enterBlock("CallButtonPresenter.changeToVideoClicked");
+ mCall.getVideoTech().upgradeToVideo();
}
@Override
@@ -300,26 +294,25 @@ public class CallButtonPresenter
InCallCameraManager cameraManager = InCallPresenter.getInstance().getInCallCameraManager();
cameraManager.setUseFrontFacingCamera(useFrontFacingCamera);
- VideoCall videoCall = mCall.getVideoCall();
- if (videoCall == null) {
- return;
- }
-
String cameraId = cameraManager.getActiveCameraId();
if (cameraId != null) {
final int cameraDir =
cameraManager.isUsingFrontFacingCamera()
- ? DialerCall.VideoSettings.CAMERA_DIRECTION_FRONT_FACING
- : DialerCall.VideoSettings.CAMERA_DIRECTION_BACK_FACING;
- mCall.getVideoSettings().setCameraDir(cameraDir);
- videoCall.setCamera(cameraId);
- videoCall.requestCameraCapabilities();
+ ? CameraDirection.CAMERA_DIRECTION_FRONT_FACING
+ : CameraDirection.CAMERA_DIRECTION_BACK_FACING;
+ mCall.setCameraDir(cameraDir);
+ mCall.getVideoTech().setCamera(cameraId);
}
}
@Override
public void toggleCameraClicked() {
LogUtil.i("CallButtonPresenter.toggleCameraClicked", "");
+ Logger.get(mContext)
+ .logCallImpression(
+ DialerImpression.Type.IN_CALL_SCREEN_SWAP_CAMERA,
+ mCall.getUniqueCallId(),
+ mCall.getTimeAddedMs());
switchCameraClicked(
!InCallPresenter.getInstance().getInCallCameraManager().isUsingFrontFacingCamera());
}
@@ -333,24 +326,19 @@ public class CallButtonPresenter
@Override
public void pauseVideoClicked(boolean pause) {
LogUtil.i("CallButtonPresenter.pauseVideoClicked", "%s", pause ? "pause" : "unpause");
- VideoCall videoCall = mCall.getVideoCall();
- if (videoCall == null) {
- return;
- }
- int currUnpausedVideoState = VideoUtils.getUnPausedVideoState(mCall.getVideoState());
+ Logger.get(mContext)
+ .logCallImpression(
+ pause
+ ? DialerImpression.Type.IN_CALL_SCREEN_TURN_OFF_VIDEO
+ : DialerImpression.Type.IN_CALL_SCREEN_TURN_ON_VIDEO,
+ mCall.getUniqueCallId(),
+ mCall.getTimeAddedMs());
+
if (pause) {
- videoCall.setCamera(null);
- VideoProfile videoProfile =
- new VideoProfile(currUnpausedVideoState & ~VideoProfile.STATE_TX_ENABLED);
- videoCall.sendSessionModifyRequest(videoProfile);
+ mCall.getVideoTech().stopTransmission();
} else {
- InCallCameraManager cameraManager = InCallPresenter.getInstance().getInCallCameraManager();
- videoCall.setCamera(cameraManager.getActiveCameraId());
- VideoProfile videoProfile =
- new VideoProfile(currUnpausedVideoState | VideoProfile.STATE_TX_ENABLED);
- videoCall.sendSessionModifyRequest(videoProfile);
- mCall.setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE);
+ mCall.getVideoTech().resumeTransmission();
}
mInCallButtonUi.setVideoPaused(pause);
@@ -386,7 +374,7 @@ public class CallButtonPresenter
*/
private void updateButtonsState(DialerCall call) {
LogUtil.v("CallButtonPresenter.updateButtonsState", "");
- final boolean isVideo = VideoUtils.isVideoCall(call);
+ final boolean isVideo = call.isVideoCall();
// Common functionality (audio, hold, etc).
// Show either HOLD or SWAP, but not both. If neither HOLD or SWAP is available:
@@ -402,7 +390,7 @@ public class CallButtonPresenter
final boolean showAddCall =
TelecomAdapter.getInstance().canAddCall() && UserManagerCompat.isUserUnlocked(mContext);
final boolean showMerge = call.can(android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE);
- final boolean showUpgradeToVideo = !isVideo && hasVideoCallCapabilities(call);
+ final boolean showUpgradeToVideo = !isVideo && (hasVideoCallCapabilities(call));
final boolean showDowngradeToAudio = isVideo && isDowngradeToAudioSupported(call);
final boolean showMute = call.can(android.telecom.Call.Details.CAPABILITY_MUTE);
@@ -427,8 +415,7 @@ public class CallButtonPresenter
InCallButtonIds.BUTTON_SWITCH_CAMERA, isVideo && hasCameraPermission);
mInCallButtonUi.showButton(InCallButtonIds.BUTTON_PAUSE_VIDEO, showPauseVideo);
if (isVideo) {
- mInCallButtonUi.setVideoPaused(
- !VideoUtils.isTransmissionEnabled(call) || !hasCameraPermission);
+ mInCallButtonUi.setVideoPaused(!call.getVideoTech().isTransmitting() || !hasCameraPermission);
}
mInCallButtonUi.showButton(InCallButtonIds.BUTTON_DIALPAD, true);
mInCallButtonUi.showButton(InCallButtonIds.BUTTON_MERGE, showMerge);
@@ -437,12 +424,7 @@ public class CallButtonPresenter
}
private boolean hasVideoCallCapabilities(DialerCall call) {
- if (SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.M) >= Build.VERSION_CODES.M) {
- return call.can(android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_TX)
- && call.can(android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_RX);
- }
- // In L, this single flag represents both video transmitting and receiving capabilities
- return call.can(android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_TX);
+ return call.getVideoTech().isAvailable();
}
/**
@@ -454,6 +436,7 @@ public class CallButtonPresenter
* @return {@code true} if downgrading to an audio-only call from a video call is supported.
*/
private boolean isDowngradeToAudioSupported(DialerCall call) {
+ // TODO(b/33676907): If there is an RCS video share session, return true here
return !call.can(CallCompat.Details.CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO);
}
diff --git a/java/com/android/incallui/CallCardPresenter.java b/java/com/android/incallui/CallCardPresenter.java
index 930775772..668692d71 100644
--- a/java/com/android/incallui/CallCardPresenter.java
+++ b/java/com/android/incallui/CallCardPresenter.java
@@ -19,7 +19,6 @@ package com.android.incallui;
import static com.android.contacts.common.compat.CallCompat.Details.PROPERTY_ENTERPRISE_CALL;
import android.Manifest;
-import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
@@ -29,6 +28,7 @@ import android.graphics.drawable.Drawable;
import android.hardware.display.DisplayManager;
import android.os.BatteryManager;
import android.os.Handler;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;
@@ -46,9 +46,12 @@ import com.android.contacts.common.util.ContactDisplayUtils;
import com.android.dialer.common.Assert;
import com.android.dialer.common.ConfigProviderBindings;
import com.android.dialer.common.LogUtil;
+import com.android.dialer.compat.ActivityCompat;
+import com.android.dialer.enrichedcall.EnrichedCallComponent;
import com.android.dialer.enrichedcall.EnrichedCallManager;
import com.android.dialer.enrichedcall.Session;
import com.android.dialer.multimedia.MultimediaData;
+import com.android.dialer.oem.MotorolaUtils;
import com.android.incallui.ContactInfoCache.ContactCacheEntry;
import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback;
import com.android.incallui.InCallPresenter.InCallDetailsListener;
@@ -58,14 +61,16 @@ import com.android.incallui.InCallPresenter.InCallStateListener;
import com.android.incallui.InCallPresenter.IncomingCallListener;
import com.android.incallui.call.CallList;
import com.android.incallui.call.DialerCall;
-import com.android.incallui.call.DialerCall.SessionModificationState;
import com.android.incallui.call.DialerCallListener;
+import com.android.incallui.calllocation.CallLocation;
+import com.android.incallui.calllocation.CallLocationComponent;
import com.android.incallui.incall.protocol.ContactPhotoType;
import com.android.incallui.incall.protocol.InCallScreen;
import com.android.incallui.incall.protocol.InCallScreenDelegate;
import com.android.incallui.incall.protocol.PrimaryCallState;
import com.android.incallui.incall.protocol.PrimaryInfo;
import com.android.incallui.incall.protocol.SecondaryInfo;
+import com.android.incallui.videotech.VideoTech;
import java.lang.ref.WeakReference;
/**
@@ -116,7 +121,8 @@ public class CallCardPresenter
private InCallScreen mInCallScreen;
private boolean isInCallScreenReady;
private boolean shouldSendAccessibilityEvent;
- private final String locationModule = null;
+
+ @NonNull private final CallLocation callLocation;
private final Runnable sendAccessibilityEventRunnable =
new Runnable() {
@Override
@@ -135,6 +141,7 @@ public class CallCardPresenter
public CallCardPresenter(Context context) {
LogUtil.i("CallCardController.constructor", null);
mContext = Assert.isNotNull(context).getApplicationContext();
+ callLocation = CallLocationComponent.get(mContext).getCallLocation();
}
private static boolean hasCallSubject(DialerCall call) {
@@ -175,8 +182,7 @@ public class CallCardPresenter
mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY);
}
- EnrichedCallManager.Accessor.getInstance(((Application) mContext))
- .registerStateChangedListener(this);
+ EnrichedCallComponent.get(mContext).getEnrichedCallManager().registerStateChangedListener(this);
// Contact search may have completed before ui is ready.
if (mPrimaryContactInfo != null) {
@@ -189,6 +195,11 @@ public class CallCardPresenter
InCallPresenter.getInstance().addDetailsListener(this);
InCallPresenter.getInstance().addInCallEventListener(this);
isInCallScreenReady = true;
+
+ // Showing the location may have been skipped if the UI wasn't ready during previous layout.
+ if (shouldShowLocation()) {
+ updatePrimaryDisplayInfo();
+ }
}
@Override
@@ -196,7 +207,8 @@ public class CallCardPresenter
LogUtil.i("CallCardController.onInCallScreenUnready", null);
Assert.checkState(isInCallScreenReady);
- EnrichedCallManager.Accessor.getInstance(((Application) mContext))
+ EnrichedCallComponent.get(mContext)
+ .getEnrichedCallManager()
.unregisterStateChangedListener(this);
// stop getting call state changes
InCallPresenter.getInstance().removeListener(this);
@@ -207,6 +219,8 @@ public class CallCardPresenter
mPrimary.removeListener(this);
}
+ callLocation.close();
+
mPrimary = null;
mPrimaryContactInfo = null;
mSecondaryContactInfo = null;
@@ -282,7 +296,6 @@ public class CallCardPresenter
mContext, mPrimary, mPrimary.getState() == DialerCall.State.INCOMING);
updatePrimaryDisplayInfo();
maybeStartSearch(mPrimary, true);
- maybeClearSessionModificationState(mPrimary);
}
if (previousPrimary != null && mPrimary == null) {
@@ -300,7 +313,6 @@ public class CallCardPresenter
mContext, mSecondary, mSecondary.getState() == DialerCall.State.INCOMING);
updateSecondaryDisplayInfo();
maybeStartSearch(mSecondary, false);
- maybeClearSessionModificationState(mSecondary);
}
// Set the call state
@@ -373,25 +385,18 @@ public class CallCardPresenter
@Override
public void onDialerCallUpgradeToVideo() {}
- /**
- * Handles a change to the session modification state for a call.
- *
- * @param sessionModificationState The new session modification state.
- */
+ /** Handles a change to the session modification state for a call. */
@Override
- public void onDialerCallSessionModificationStateChange(
- @SessionModificationState int sessionModificationState) {
- LogUtil.v(
- "CallCardPresenter.onDialerCallSessionModificationStateChange",
- "state: " + sessionModificationState);
+ public void onDialerCallSessionModificationStateChange() {
+ LogUtil.enterBlock("CallCardPresenter.onDialerCallSessionModificationStateChange");
if (mPrimary == null) {
return;
}
getUi()
.setEndCallButtonEnabled(
- sessionModificationState
- != DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST,
+ mPrimary.getVideoTech().getSessionModificationState()
+ != VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST,
true /* shouldAnimate */);
updatePrimaryCallState();
}
@@ -418,6 +423,13 @@ public class CallCardPresenter
&& mPrimaryContactInfo.userType == ContactsUtils.USER_TYPE_WORK);
boolean isHdAudioCall =
isPrimaryCallActive() && mPrimary.hasProperty(Details.PROPERTY_HIGH_DEF_AUDIO);
+ boolean isAttemptingHdAudioCall =
+ !isHdAudioCall
+ && !mPrimary.hasProperty(DialerCall.PROPERTY_CODEC_KNOWN)
+ && MotorolaUtils.shouldBlinkHdIconWhenConnectingCall(mContext);
+
+ boolean isBusiness = mPrimaryContactInfo != null && mPrimaryContactInfo.isBusiness;
+
// Check for video state change and update the visibility of the contact photo. The contact
// photo is hidden when the incoming video surface is shown.
// The contact photo visibility can also change in setPrimary().
@@ -427,8 +439,8 @@ public class CallCardPresenter
.setCallState(
new PrimaryCallState(
mPrimary.getState(),
- mPrimary.getVideoState(),
- mPrimary.getSessionModificationState(),
+ mPrimary.isVideoCall(),
+ mPrimary.getVideoTech().getSessionModificationState(),
mPrimary.getDisconnectCause(),
getConnectionLabel(),
getCallStateIcon(),
@@ -438,12 +450,14 @@ public class CallCardPresenter
mPrimary.hasProperty(Details.PROPERTY_WIFI),
mPrimary.isConferenceCall(),
isWorkCall,
+ isAttemptingHdAudioCall,
isHdAudioCall,
!TextUtils.isEmpty(mPrimary.getLastForwardedNumber()),
shouldShowContactPhoto,
mPrimary.getConnectTimeMillis(),
CallerInfoUtils.isVoiceMailNumber(mContext, mPrimary),
- mPrimary.isRemotelyHeld()));
+ mPrimary.isRemotelyHeld(),
+ isBusiness));
InCallActivity activity =
(InCallActivity) (mInCallScreen.getInCallScreenFragment().getActivity());
@@ -508,15 +522,6 @@ public class CallCardPresenter
}
}
- private void maybeClearSessionModificationState(DialerCall call) {
- @SessionModificationState int state = call.getSessionModificationState();
- if (state != DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST
- && state != DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
- LogUtil.i("CallCardPresenter.maybeClearSessionModificationState", "clearing state");
- call.setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
- }
- }
-
/** Starts a query for more contact data for the save primary and secondary calls. */
private void startContactInfoSearch(
final DialerCall call, final boolean isPrimary, boolean isIncoming) {
@@ -642,13 +647,17 @@ public class CallCardPresenter
// DialerCall placed through a work phone account.
boolean hasWorkCallProperty = mPrimary.hasProperty(PROPERTY_ENTERPRISE_CALL);
- Session enrichedCallSession =
- mPrimary.getNumber() == null
- ? null
- : EnrichedCallManager.Accessor.getInstance(((Application) mContext))
- .getSession(mPrimary.getNumber());
- MultimediaData enrichedCallMultimediaData =
- enrichedCallSession == null ? null : enrichedCallSession.getMultimediaData();
+ MultimediaData multimediaData = null;
+ if (mPrimary.getNumber() != null) {
+ Session enrichedCallSession =
+ EnrichedCallComponent.get(mContext)
+ .getEnrichedCallManager()
+ .getSession(mPrimary.getUniqueCallId(), mPrimary.getNumber());
+ if (enrichedCallSession != null) {
+ enrichedCallSession.setUniqueDialerCallId(mPrimary.getUniqueCallId());
+ multimediaData = enrichedCallSession.getMultimediaData();
+ }
+ }
if (mPrimary.isConferenceCall()) {
LogUtil.v(
@@ -671,7 +680,8 @@ public class CallCardPresenter
false /* answeringDisconnectsOngoingCall */,
shouldShowLocation(),
null /* contactInfoLookupKey */,
- null /* enrichedCallMultimediaData */));
+ null /* enrichedCallMultimediaData */,
+ mPrimary.getNumberPresentation()));
} else if (mPrimaryContactInfo != null) {
LogUtil.v(
"CallCardPresenter.updatePrimaryDisplayInfo",
@@ -696,6 +706,7 @@ public class CallCardPresenter
}
boolean nameIsNumber = name != null && name.equals(mPrimaryContactInfo.number);
+
// DialerCall with caller that is a work contact.
boolean isWorkContact = (mPrimaryContactInfo.userType == ContactsUtils.USER_TYPE_WORK);
mInCallScreen.setPrimary(
@@ -714,13 +725,52 @@ public class CallCardPresenter
mPrimary.answeringDisconnectsForegroundVideoCall(),
shouldShowLocation(),
mPrimaryContactInfo.lookupKey,
- enrichedCallMultimediaData));
+ multimediaData,
+ mPrimary.getNumberPresentation()));
} else {
// Clear the primary display info.
mInCallScreen.setPrimary(PrimaryInfo.createEmptyPrimaryInfo());
}
- mInCallScreen.showLocationUi(null);
+ if (isInCallScreenReady) {
+ mInCallScreen.showLocationUi(getLocationFragment());
+ } else {
+ LogUtil.i("CallCardPresenter.updatePrimaryDisplayInfo", "UI not ready, not showing location");
+ }
+ }
+
+ private Fragment getLocationFragment() {
+ if (!ConfigProviderBindings.get(mContext)
+ .getBoolean(CONFIG_ENABLE_EMERGENCY_LOCATION, CONFIG_ENABLE_EMERGENCY_LOCATION_DEFAULT)) {
+ LogUtil.i("CallCardPresenter.getLocationFragment", "disabled by config.");
+ return null;
+ }
+ if (!shouldShowLocation()) {
+ LogUtil.i("CallCardPresenter.getLocationFragment", "shouldn't show location");
+ return null;
+ }
+ if (!hasLocationPermission()) {
+ LogUtil.i("CallCardPresenter.getLocationFragment", "no location permission.");
+ return null;
+ }
+ if (isBatteryTooLowForEmergencyLocation()) {
+ LogUtil.i("CallCardPresenter.getLocationFragment", "low battery.");
+ return null;
+ }
+ if (ActivityCompat.isInMultiWindowMode(mInCallScreen.getInCallScreenFragment().getActivity())) {
+ LogUtil.i("CallCardPresenter.getLocationFragment", "in multi-window mode");
+ return null;
+ }
+ if (mPrimary.isVideoCall()) {
+ LogUtil.i("CallCardPresenter.getLocationFragment", "emergency video calls not supported");
+ return null;
+ }
+ if (!callLocation.canGetLocation(mContext)) {
+ LogUtil.i("CallCardPresenter.getLocationFragment", "can't get current location");
+ return null;
+ }
+ LogUtil.i("CallCardPresenter.getLocationFragment", "returning location fragment");
+ return callLocation.getLocationFragment(mContext);
}
private boolean shouldShowLocation() {
@@ -972,8 +1022,8 @@ public class CallCardPresenter
|| callState == DialerCall.State.INCOMING) {
return false;
}
- if (mPrimary.getSessionModificationState()
- == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+ if (mPrimary.getVideoTech().getSessionModificationState()
+ == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
return false;
}
return true;
diff --git a/java/com/android/incallui/CallerInfoAsyncQuery.java b/java/com/android/incallui/CallerInfoAsyncQuery.java
index f8d7ac65a..d620d4705 100644
--- a/java/com/android/incallui/CallerInfoAsyncQuery.java
+++ b/java/com/android/incallui/CallerInfoAsyncQuery.java
@@ -55,7 +55,7 @@ import java.util.Arrays;
public class CallerInfoAsyncQuery {
/** Interface for a CallerInfoAsyncQueryHandler result return. */
- public interface OnQueryCompleteListener {
+ interface OnQueryCompleteListener {
/** Called when the query is complete. */
@MainThread
@@ -85,7 +85,7 @@ public class CallerInfoAsyncQuery {
private CallerInfoAsyncQuery() {}
@RequiresPermission(Manifest.permission.READ_CONTACTS)
- public static void startQuery(
+ static void startQuery(
final int token,
final Context context,
final CallerInfo info,
@@ -99,7 +99,7 @@ public class CallerInfoAsyncQuery {
new OnQueryCompleteListener() {
@Override
public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
- Log.d(LOG_TAG, "contactsProviderQueryCompleteListener done");
+ Log.d(LOG_TAG, "contactsProviderQueryCompleteListener onQueryComplete");
// If there are no other directory queries, make sure that the listener is
// notified of this result. see b/27621628
if ((ci != null && ci.contactExists)
@@ -112,6 +112,7 @@ public class CallerInfoAsyncQuery {
@Override
public void onDataLoaded(int token, Object cookie, CallerInfo ci) {
+ Log.d(LOG_TAG, "contactsProviderQueryCompleteListener onDataLoaded");
listener.onDataLoaded(token, cookie, ci);
}
};
@@ -270,9 +271,9 @@ public class CallerInfoAsyncQuery {
/* Directory lookup related code - END */
/** Simple exception used to communicate problems with the query pool. */
- public static class QueryPoolException extends SQLException {
+ private static class QueryPoolException extends SQLException {
- public QueryPoolException(String error) {
+ QueryPoolException(String error) {
super(error);
}
}
@@ -337,7 +338,7 @@ public class CallerInfoAsyncQuery {
}
}
- public OnQueryCompleteListener newListener(long directoryId) {
+ OnQueryCompleteListener newListener(long directoryId) {
return new DirectoryQueryCompleteListener(directoryId);
}
@@ -351,11 +352,13 @@ public class CallerInfoAsyncQuery {
@Override
public void onDataLoaded(int token, Object cookie, CallerInfo ci) {
+ Log.d(LOG_TAG, "DirectoryQueryCompleteListener.onDataLoaded");
mListener.onDataLoaded(token, cookie, ci);
}
@Override
public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
+ Log.d(LOG_TAG, "DirectoryQueryCompleteListener.onQueryComplete");
onDirectoryQueryComplete(token, cookie, ci, mDirectoryId);
}
}
@@ -446,7 +449,7 @@ public class CallerInfoAsyncQuery {
mCallerInfo = null;
}
- protected void updateData(int token, Object cookie, Cursor cursor) {
+ void updateData(int token, Object cookie, Cursor cursor) {
try {
Log.d(this, "##### updateData() ##### for token: " + token);
@@ -549,9 +552,9 @@ public class CallerInfoAsyncQuery {
* times before the query is complete. All accesses (listeners) must be queued up and informed
* in order when the query is complete.
*/
- protected class CallerInfoWorkerHandler extends WorkerHandler {
+ class CallerInfoWorkerHandler extends WorkerHandler {
- public CallerInfoWorkerHandler(Looper looper) {
+ CallerInfoWorkerHandler(Looper looper) {
super(looper);
}
@@ -624,7 +627,7 @@ public class CallerInfoAsyncQuery {
case EVENT_ADD_LISTENER:
updateData(msg.arg1, cw, (Cursor) args.result);
break;
- default:
+ default: // fall out
}
Message reply = args.handler.obtainMessage(msg.what);
reply.obj = args;
diff --git a/java/com/android/incallui/CallerInfoUtils.java b/java/com/android/incallui/CallerInfoUtils.java
index 9f57fba65..7c14533bb 100644
--- a/java/com/android/incallui/CallerInfoUtils.java
+++ b/java/com/android/incallui/CallerInfoUtils.java
@@ -22,6 +22,7 @@ import android.content.Loader;
import android.content.Loader.OnLoadCompleteListener;
import android.content.pm.PackageManager;
import android.net.Uri;
+import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.telecom.PhoneAccount;
import android.telecom.TelecomManager;
@@ -53,7 +54,7 @@ public class CallerInfoUtils {
* OnQueryCompleteListener (which contains information about the phone number label, user's name,
* etc).
*/
- public static CallerInfo getCallerInfoForCall(
+ static CallerInfo getCallerInfoForCall(
Context context,
DialerCall call,
Object cookie,
@@ -81,7 +82,7 @@ public class CallerInfoUtils {
return info;
}
- public static CallerInfo buildCallerInfo(Context context, DialerCall call) {
+ static CallerInfo buildCallerInfo(Context context, DialerCall call) {
CallerInfo info = new CallerInfo();
// Store CNAP information retrieved from the Connection (we want to do this
@@ -91,6 +92,7 @@ public class CallerInfoUtils {
info.numberPresentation = call.getNumberPresentation();
info.namePresentation = call.getCnapNamePresentation();
info.callSubject = call.getCallSubject();
+ info.contactExists = false;
String number = call.getNumber();
if (!TextUtils.isEmpty(number)) {
@@ -109,9 +111,7 @@ public class CallerInfoUtils {
// Because the InCallUI is immediately launched before the call is connected, occasionally
// a voicemail call will be passed to InCallUI as a "voicemail:" URI without a number.
// This call should still be handled as a voicemail call.
- if ((call.getHandle() != null
- && PhoneAccount.SCHEME_VOICEMAIL.equals(call.getHandle().getScheme()))
- || isVoiceMailNumber(context, call)) {
+ if (isVoiceMailNumber(context, call)) {
info.markAsVoiceMail(context);
}
@@ -145,11 +145,17 @@ public class CallerInfoUtils {
return cacheInfo;
}
- public static boolean isVoiceMailNumber(Context context, DialerCall call) {
+ public static boolean isVoiceMailNumber(Context context, @NonNull DialerCall call) {
+ if (call.getHandle() != null
+ && PhoneAccount.SCHEME_VOICEMAIL.equals(call.getHandle().getScheme())) {
+ return true;
+ }
+
if (ContextCompat.checkSelfPermission(context, permission.READ_PHONE_STATE)
!= PackageManager.PERMISSION_GRANTED) {
return false;
}
+
return TelecomUtil.isVoicemailNumber(context, call.getAccountHandle(), call.getNumber());
}
diff --git a/java/com/android/incallui/ContactInfoCache.java b/java/com/android/incallui/ContactInfoCache.java
index 4d4d94a17..c4e25e700 100644
--- a/java/com/android/incallui/ContactInfoCache.java
+++ b/java/com/android/incallui/ContactInfoCache.java
@@ -35,6 +35,7 @@ import android.support.annotation.NonNull;
import android.support.annotation.WorkerThread;
import android.support.v4.os.UserManagerCompat;
import android.telecom.TelecomManager;
+import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -74,10 +75,11 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
private final PhoneNumberService mPhoneNumberService;
// Cache info map needs to be thread-safe since it could be modified by both main thread and
// worker thread.
- private final Map<String, ContactCacheEntry> mInfoMap = new ConcurrentHashMap<>();
+ private final ConcurrentHashMap<String, ContactCacheEntry> mInfoMap = new ConcurrentHashMap<>();
private final Map<String, Set<ContactInfoCacheCallback>> mCallBacks = new ArrayMap<>();
private Drawable mDefaultContactPhotoDrawable;
private Drawable mConferencePhotoDrawable;
+ private int mQueryId;
private ContactInfoCache(Context context) {
mContext = context;
@@ -91,7 +93,7 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
return sCache;
}
- public static ContactCacheEntry buildCacheEntryFromCall(
+ static ContactCacheEntry buildCacheEntryFromCall(
Context context, DialerCall call, boolean isIncoming) {
final ContactCacheEntry entry = new ContactCacheEntry();
@@ -103,7 +105,7 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
}
/** Populate a cache entry from a call (which got converted into a caller info). */
- public static void populateCacheEntry(
+ private static void populateCacheEntry(
@NonNull Context context,
@NonNull CallerInfo info,
@NonNull ContactCacheEntry cce,
@@ -153,7 +155,7 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
// (Typically, we promote the phone number up to the "name" slot
// onscreen, and possibly display a descriptive string in the
// "number" slot.)
- if (TextUtils.isEmpty(number)) {
+ if (TextUtils.isEmpty(number) && TextUtils.isEmpty(info.cnapName)) {
// No name *or* number! Display a generic "unknown" string
// (or potentially some other default based on the presentation.)
displayName = getPresentationString(context, presentation, info.callSubject);
@@ -236,6 +238,7 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
cce.label = label;
cce.isSipCall = isSipCall;
cce.userType = info.userType;
+ cce.originalPhoneNumber = info.phoneNumber;
if (info.contactExists) {
cce.contactLookupResult = ContactLookupResult.Type.LOCAL_CONTACT;
@@ -261,11 +264,11 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
return name;
}
- public ContactCacheEntry getInfo(String callId) {
+ ContactCacheEntry getInfo(String callId) {
return mInfoMap.get(callId);
}
- public void maybeInsertCnapInformationIntoCache(
+ void maybeInsertCnapInformationIntoCache(
Context context, final DialerCall call, final CallerInfo info) {
final CachedNumberLookupService cachedNumberLookupService =
PhoneNumberCache.get(context).getCachedNumberLookupService();
@@ -331,8 +334,13 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
final ContactCacheEntry cacheEntry = mInfoMap.get(callId);
Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
- // If we have a previously obtained intermediate result return that now
- if (cacheEntry != null) {
+ // We need to force a new query if phone number has changed.
+ boolean forceQuery = needForceQuery(call, cacheEntry);
+ Log.d(TAG, "findInfo: callId = " + callId + "; forceQuery = " + forceQuery);
+
+ // If we have a previously obtained intermediate result return that now except needs
+ // force query.
+ if (cacheEntry != null && !forceQuery) {
Log.d(
TAG,
"Contact lookup. In memory cache hit; lookup "
@@ -346,14 +354,19 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
// If the entry already exists, add callback
if (callBacks != null) {
+ Log.d(TAG, "Another query is in progress, add callback only.");
callBacks.add(callback);
- return;
+ if (!forceQuery) {
+ Log.d(TAG, "No need to query again, just return and wait for existing query to finish");
+ return;
+ }
+ } else {
+ Log.d(TAG, "Contact lookup. In memory cache miss; searching provider.");
+ // New lookup
+ callBacks = new ArraySet<>();
+ callBacks.add(callback);
+ mCallBacks.put(callId, callBacks);
}
- Log.d(TAG, "Contact lookup. In memory cache miss; searching provider.");
- // New lookup
- callBacks = new ArraySet<>();
- callBacks.add(callback);
- mCallBacks.put(callId, callBacks);
/**
* Performs a query for caller information. Save any immediate data we get from the query. An
@@ -361,25 +374,47 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
* such as those for voicemail and emergency call information, will not perform an additional
* asynchronous query.
*/
+ final CallerInfoQueryToken queryToken = new CallerInfoQueryToken(mQueryId, callId);
+ mQueryId++;
final CallerInfo callerInfo =
CallerInfoUtils.getCallerInfoForCall(
mContext,
call,
new DialerCallCookieWrapper(callId, call.getNumberPresentation()),
- new FindInfoCallback(isIncoming));
+ new FindInfoCallback(isIncoming, queryToken));
- updateCallerInfoInCacheOnAnyThread(
- callId, call.getNumberPresentation(), callerInfo, isIncoming, false);
- sendInfoNotifications(callId, mInfoMap.get(callId));
+ if (cacheEntry != null) {
+ // We should not override the old cache item until the new query is
+ // back. We should only update the queryId. Otherwise, we may see
+ // flicker of the name and image (old cache -> new cache before query
+ // -> new cache after query)
+ cacheEntry.queryId = queryToken.mQueryId;
+ Log.d(TAG, "There is an existing cache. Do not override until new query is back");
+ } else {
+ ContactCacheEntry initialCacheEntry =
+ updateCallerInfoInCacheOnAnyThread(
+ callId, call.getNumberPresentation(), callerInfo, isIncoming, false, queryToken);
+ sendInfoNotifications(callId, initialCacheEntry);
+ }
}
@AnyThread
- private void updateCallerInfoInCacheOnAnyThread(
+ private ContactCacheEntry updateCallerInfoInCacheOnAnyThread(
String callId,
int numberPresentation,
CallerInfo callerInfo,
boolean isIncoming,
- boolean didLocalLookup) {
+ boolean didLocalLookup,
+ CallerInfoQueryToken queryToken) {
+ Log.d(
+ TAG,
+ "updateCallerInfoInCacheOnAnyThread: callId = "
+ + callId
+ + "; queryId = "
+ + queryToken.mQueryId
+ + "; didLocalLookup = "
+ + didLocalLookup);
+
int presentationMode = numberPresentation;
if (callerInfo.contactExists
|| callerInfo.isEmergencyNumber()
@@ -387,38 +422,57 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
presentationMode = TelecomManager.PRESENTATION_ALLOWED;
}
- synchronized (mInfoMap) {
- ContactCacheEntry cacheEntry = mInfoMap.get(callId);
- // Ensure we always have a cacheEntry. Replace the existing entry if
- // it has no name or if we found a local contact.
- if (cacheEntry == null
- || TextUtils.isEmpty(cacheEntry.namePrimary)
- || callerInfo.contactExists) {
- cacheEntry = buildEntry(mContext, callerInfo, presentationMode, isIncoming);
- mInfoMap.put(callId, cacheEntry);
- }
- if (didLocalLookup) {
- // Before issuing a request for more data from other services, we only check that the
- // contact wasn't found in the local DB. We don't check the if the cache entry already
- // has a name because we allow overriding cnap data with data from other services.
- if (!callerInfo.contactExists && mPhoneNumberService != null) {
- Log.d(TAG, "Contact lookup. Local contacts miss, checking remote");
- final PhoneNumberServiceListener listener = new PhoneNumberServiceListener(callId);
- mPhoneNumberService.getPhoneNumberInfo(cacheEntry.number, listener, listener, isIncoming);
- } else if (cacheEntry.displayPhotoUri != null) {
- Log.d(TAG, "Contact lookup. Local contact found, starting image load");
- // Load the image with a callback to update the image state.
- // When the load is finished, onImageLoadComplete() will be called.
- cacheEntry.hasPhotoToLoad = true;
- ContactsAsyncHelper.startObtainPhotoAsync(
- TOKEN_UPDATE_PHOTO_FOR_CALL_STATE,
- mContext,
- cacheEntry.displayPhotoUri,
- ContactInfoCache.this,
- callId);
+ // We always replace the entry. The only exception is the same photo case.
+ ContactCacheEntry cacheEntry = buildEntry(mContext, callerInfo, presentationMode, isIncoming);
+ cacheEntry.queryId = queryToken.mQueryId;
+
+ ContactCacheEntry existingCacheEntry = mInfoMap.get(callId);
+ Log.d(TAG, "Existing cacheEntry in hashMap " + existingCacheEntry);
+
+ if (didLocalLookup) {
+ // Before issuing a request for more data from other services, we only check that the
+ // contact wasn't found in the local DB. We don't check the if the cache entry already
+ // has a name because we allow overriding cnap data with data from other services.
+ if (!callerInfo.contactExists && mPhoneNumberService != null) {
+ Log.d(TAG, "Contact lookup. Local contacts miss, checking remote");
+ final PhoneNumberServiceListener listener =
+ new PhoneNumberServiceListener(callId, queryToken.mQueryId);
+ cacheEntry.hasPendingQuery = true;
+ mPhoneNumberService.getPhoneNumberInfo(cacheEntry.number, listener, listener, isIncoming);
+ } else if (cacheEntry.displayPhotoUri != null) {
+ // When the difference between 2 numbers is only the prefix (e.g. + or IDD),
+ // we will still trigger force query so that the number can be updated on
+ // the calling screen. We need not query the image again if the previous
+ // query already has the image to avoid flickering.
+ if (existingCacheEntry != null
+ && existingCacheEntry.displayPhotoUri != null
+ && existingCacheEntry.displayPhotoUri.equals(cacheEntry.displayPhotoUri)
+ && existingCacheEntry.photo != null) {
+ Log.d(TAG, "Same picture. Do not need start image load.");
+ cacheEntry.photo = existingCacheEntry.photo;
+ cacheEntry.photoType = existingCacheEntry.photoType;
+ return cacheEntry;
}
+
+ Log.d(TAG, "Contact lookup. Local contact found, starting image load");
+ // Load the image with a callback to update the image state.
+ // When the load is finished, onImageLoadComplete() will be called.
+ cacheEntry.hasPendingQuery = true;
+ ContactsAsyncHelper.startObtainPhotoAsync(
+ TOKEN_UPDATE_PHOTO_FOR_CALL_STATE,
+ mContext,
+ cacheEntry.displayPhotoUri,
+ ContactInfoCache.this,
+ queryToken);
}
+ Log.d(TAG, "put entry into map: " + cacheEntry);
+ mInfoMap.put(callId, cacheEntry);
+ } else {
+ // Don't overwrite if there is existing cache.
+ Log.d(TAG, "put entry into map if not exists: " + cacheEntry);
+ mInfoMap.putIfAbsent(callId, cacheEntry);
}
+ return cacheEntry;
}
/**
@@ -429,35 +483,42 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
@Override
public void onImageLoaded(int token, Drawable photo, Bitmap photoIcon, Object cookie) {
Assert.isWorkerThread();
+ CallerInfoQueryToken myCookie = (CallerInfoQueryToken) cookie;
+ final String callId = myCookie.mCallId;
+ final int queryId = myCookie.mQueryId;
+ if (!isWaitingForThisQuery(callId, queryId)) {
+ return;
+ }
loadImage(photo, photoIcon, cookie);
}
private void loadImage(Drawable photo, Bitmap photoIcon, Object cookie) {
- Log.d(this, "Image load complete with context: ", mContext);
+ Log.d(TAG, "Image load complete with context: ", mContext);
// TODO: may be nice to update the image view again once the newer one
// is available on contacts database.
- String callId = (String) cookie;
+ CallerInfoQueryToken myCookie = (CallerInfoQueryToken) cookie;
+ final String callId = myCookie.mCallId;
ContactCacheEntry entry = mInfoMap.get(callId);
if (entry == null) {
- Log.e(this, "Image Load received for empty search entry.");
+ Log.e(TAG, "Image Load received for empty search entry.");
clearCallbacks(callId);
return;
}
- Log.d(this, "setting photo for entry: ", entry);
+ Log.d(TAG, "setting photo for entry: ", entry);
// Conference call icons are being handled in CallCardPresenter.
if (photo != null) {
- Log.v(this, "direct drawable: ", photo);
+ Log.v(TAG, "direct drawable: ", photo);
entry.photo = photo;
entry.photoType = ContactPhotoType.CONTACT;
} else if (photoIcon != null) {
- Log.v(this, "photo icon: ", photoIcon);
+ Log.v(TAG, "photo icon: ", photoIcon);
entry.photo = new BitmapDrawable(mContext.getResources(), photoIcon);
entry.photoType = ContactPhotoType.CONTACT;
} else {
- Log.v(this, "unknown photo");
+ Log.v(TAG, "unknown photo");
entry.photo = null;
entry.photoType = ContactPhotoType.DEFAULT_PLACEHOLDER;
}
@@ -471,9 +532,13 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
@Override
public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie) {
Assert.isMainThread();
- String callId = (String) cookie;
- ContactCacheEntry entry = mInfoMap.get(callId);
- sendImageNotifications(callId, entry);
+ CallerInfoQueryToken myCookie = (CallerInfoQueryToken) cookie;
+ final String callId = myCookie.mCallId;
+ final int queryId = myCookie.mQueryId;
+ if (!isWaitingForThisQuery(callId, queryId)) {
+ return;
+ }
+ sendImageNotifications(callId, mInfoMap.get(callId));
clearCallbacks(callId);
}
@@ -482,6 +547,7 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
public void clearCache() {
mInfoMap.clear();
mCallBacks.clear();
+ mQueryId = 0;
}
private ContactCacheEntry buildEntry(
@@ -500,9 +566,6 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
cce.photo = getDefaultContactPhotoDrawable();
cce.photoType = ContactPhotoType.DEFAULT_PLACEHOLDER;
}
- } else if (info.contactDisplayPhotoUri == null) {
- cce.photo = getDefaultContactPhotoDrawable();
- cce.photoType = ContactPhotoType.DEFAULT_PLACEHOLDER;
} else {
cce.displayPhotoUri = info.contactDisplayPhotoUri;
cce.photo = null;
@@ -528,7 +591,9 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
}
/** Sends the updated information to call the callbacks for the entry. */
+ @MainThread
private void sendInfoNotifications(String callId, ContactCacheEntry entry) {
+ Assert.isMainThread();
final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
if (callBacks != null) {
for (ContactInfoCacheCallback callBack : callBacks) {
@@ -537,7 +602,9 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
}
}
+ @MainThread
private void sendImageNotifications(String callId, ContactCacheEntry entry) {
+ Assert.isMainThread();
final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
if (callBacks != null && entry.photo != null) {
for (ContactInfoCacheCallback callBack : callBacks) {
@@ -583,21 +650,26 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
public String location;
public String label;
public Drawable photo;
- @ContactPhotoType public int photoType;
- public boolean isSipCall;
+ @ContactPhotoType int photoType;
+ boolean isSipCall;
// Note in cache entry whether this is a pending async loading action to know whether to
// wait for its callback or not.
- public boolean hasPhotoToLoad;
+ boolean hasPendingQuery;
/** This will be used for the "view" notification. */
public Uri contactUri;
/** Either a display photo or a thumbnail URI. */
- public Uri displayPhotoUri;
+ Uri displayPhotoUri;
public Uri lookupUri; // Sent to NotificationMananger
public String lookupKey;
public int contactLookupResult = ContactLookupResult.Type.NOT_FOUND;
public long userType = ContactsUtils.USER_TYPE_CURRENT;
- public Uri contactRingtoneUri;
+ Uri contactRingtoneUri;
+ /** Query id to identify the query session. */
+ int queryId;
+ /** The phone number without any changes to display to the user (ex: cnap...) */
+ String originalPhoneNumber;
+ boolean isBusiness;
@Override
public String toString() {
@@ -631,6 +703,10 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
+ userType
+ ", contactRingtoneUri="
+ contactRingtoneUri
+ + ", queryId="
+ + queryId
+ + ", originalPhoneNumber="
+ + originalPhoneNumber
+ '}';
}
}
@@ -648,16 +724,22 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
private class FindInfoCallback implements OnQueryCompleteListener {
private final boolean mIsIncoming;
+ private final CallerInfoQueryToken mQueryToken;
- public FindInfoCallback(boolean isIncoming) {
+ public FindInfoCallback(boolean isIncoming, CallerInfoQueryToken queryToken) {
mIsIncoming = isIncoming;
+ mQueryToken = queryToken;
}
@Override
public void onDataLoaded(int token, Object cookie, CallerInfo ci) {
Assert.isWorkerThread();
DialerCallCookieWrapper cw = (DialerCallCookieWrapper) cookie;
- updateCallerInfoInCacheOnAnyThread(cw.callId, cw.numberPresentation, ci, mIsIncoming, true);
+ if (!isWaitingForThisQuery(cw.callId, mQueryToken.mQueryId)) {
+ return;
+ }
+ updateCallerInfoInCacheOnAnyThread(
+ cw.callId, cw.numberPresentation, ci, mIsIncoming, true, mQueryToken);
}
@Override
@@ -665,6 +747,9 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
Assert.isMainThread();
DialerCallCookieWrapper cw = (DialerCallCookieWrapper) cookie;
String callId = cw.callId;
+ if (!isWaitingForThisQuery(cw.callId, mQueryToken.mQueryId)) {
+ return;
+ }
ContactCacheEntry cacheEntry = mInfoMap.get(callId);
// This may happen only when InCallPresenter attempt to cleanup.
if (cacheEntry == null) {
@@ -673,7 +758,7 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
return;
}
sendInfoNotifications(callId, cacheEntry);
- if (!cacheEntry.hasPhotoToLoad) {
+ if (!cacheEntry.hasPendingQuery) {
if (callerInfo.contactExists) {
Log.d(TAG, "Contact lookup done. Local contact found, no image.");
} else {
@@ -691,13 +776,20 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
implements PhoneNumberService.NumberLookupListener, PhoneNumberService.ImageLookupListener {
private final String mCallId;
+ private final int mQueryIdOfRemoteLookup;
- PhoneNumberServiceListener(String callId) {
+ PhoneNumberServiceListener(String callId, int queryId) {
mCallId = callId;
+ mQueryIdOfRemoteLookup = queryId;
}
@Override
public void onPhoneNumberInfoComplete(final PhoneNumberService.PhoneNumberInfo info) {
+ Log.d(TAG, "PhoneNumberServiceListener.onPhoneNumberInfoComplete");
+ if (!isWaitingForThisQuery(mCallId, mQueryIdOfRemoteLookup)) {
+ return;
+ }
+
// If we got a miss, this is the end of the lookup pipeline,
// so clear the callbacks and return.
if (info == null) {
@@ -705,11 +797,11 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
clearCallbacks(mCallId);
return;
}
-
ContactCacheEntry entry = new ContactCacheEntry();
entry.namePrimary = info.getDisplayName();
entry.number = info.getNumber();
entry.contactLookupResult = info.getLookupSource();
+ entry.isBusiness = info.isBusiness();
final int type = info.getPhoneType();
final String label = info.getPhoneLabel();
if (type == Phone.TYPE_CUSTOM) {
@@ -718,33 +810,32 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
final CharSequence typeStr = Phone.getTypeLabel(mContext.getResources(), type, label);
entry.label = typeStr == null ? null : typeStr.toString();
}
- synchronized (mInfoMap) {
- final ContactCacheEntry oldEntry = mInfoMap.get(mCallId);
- if (oldEntry != null) {
- // Location is only obtained from local lookup so persist
- // the value for remote lookups. Once we have a name this
- // field is no longer used; it is persisted here in case
- // the UI is ever changed to use it.
- entry.location = oldEntry.location;
- // Contact specific ringtone is obtained from local lookup.
- entry.contactRingtoneUri = oldEntry.contactRingtoneUri;
- }
-
- // If no image and it's a business, switch to using the default business avatar.
- if (info.getImageUrl() == null && info.isBusiness()) {
- Log.d(TAG, "Business has no image. Using default.");
- entry.photo = mContext.getResources().getDrawable(R.drawable.img_business);
- entry.photoType = ContactPhotoType.BUSINESS;
- }
+ final ContactCacheEntry oldEntry = mInfoMap.get(mCallId);
+ if (oldEntry != null) {
+ // Location is only obtained from local lookup so persist
+ // the value for remote lookups. Once we have a name this
+ // field is no longer used; it is persisted here in case
+ // the UI is ever changed to use it.
+ entry.location = oldEntry.location;
+ // Contact specific ringtone is obtained from local lookup.
+ entry.contactRingtoneUri = oldEntry.contactRingtoneUri;
+ }
- mInfoMap.put(mCallId, entry);
+ // If no image and it's a business, switch to using the default business avatar.
+ if (info.getImageUrl() == null && info.isBusiness()) {
+ Log.d(TAG, "Business has no image. Using default.");
+ entry.photo = mContext.getResources().getDrawable(R.drawable.img_business);
+ entry.photoType = ContactPhotoType.BUSINESS;
}
+
+ Log.d(TAG, "put entry into map: " + entry);
+ mInfoMap.put(mCallId, entry);
sendInfoNotifications(mCallId, entry);
- entry.hasPhotoToLoad = info.getImageUrl() != null;
+ entry.hasPendingQuery = info.getImageUrl() != null;
// If there is no image then we should not expect another callback.
- if (!entry.hasPhotoToLoad) {
+ if (!entry.hasPendingQuery) {
// We're done, so clear callbacks
clearCallbacks(mCallId);
}
@@ -752,8 +843,59 @@ public class ContactInfoCache implements OnImageLoadCompleteListener {
@Override
public void onImageFetchComplete(Bitmap bitmap) {
- loadImage(null, bitmap, mCallId);
- onImageLoadComplete(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE, null, bitmap, mCallId);
+ Log.d(TAG, "PhoneNumberServiceListener.onImageFetchComplete");
+ if (!isWaitingForThisQuery(mCallId, mQueryIdOfRemoteLookup)) {
+ return;
+ }
+ CallerInfoQueryToken queryToken = new CallerInfoQueryToken(mQueryIdOfRemoteLookup, mCallId);
+ loadImage(null, bitmap, queryToken);
+ onImageLoadComplete(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE, null, bitmap, queryToken);
+ }
+ }
+
+ private boolean needForceQuery(DialerCall call, ContactCacheEntry cacheEntry) {
+ if (call == null || call.isConferenceCall()) {
+ return false;
+ }
+
+ String newPhoneNumber = PhoneNumberUtils.stripSeparators(call.getNumber());
+ if (cacheEntry == null) {
+ // No info in the map yet so it is the 1st query
+ Log.d(TAG, "needForceQuery: first query");
+ return true;
+ }
+ String oldPhoneNumber = PhoneNumberUtils.stripSeparators(cacheEntry.originalPhoneNumber);
+
+ if (!TextUtils.equals(oldPhoneNumber, newPhoneNumber)) {
+ Log.d(TAG, "phone number has changed: " + oldPhoneNumber + " -> " + newPhoneNumber);
+ return true;
+ }
+
+ return false;
+ }
+
+ private static final class CallerInfoQueryToken {
+ final int mQueryId;
+ final String mCallId;
+
+ CallerInfoQueryToken(int queryId, String callId) {
+ mQueryId = queryId;
+ mCallId = callId;
+ }
+ }
+
+ /** Check if the queryId in the cached map is the same as the one from query result. */
+ private boolean isWaitingForThisQuery(String callId, int queryId) {
+ final ContactCacheEntry existingCacheEntry = mInfoMap.get(callId);
+ if (existingCacheEntry == null) {
+ // This might happen if lookup on background thread comes back before the initial entry is
+ // created.
+ Log.d(TAG, "Cached entry is null.");
+ return true;
+ } else {
+ int waitingQueryId = existingCacheEntry.queryId;
+ Log.d(TAG, "waitingQueryId = " + waitingQueryId + "; queryId = " + queryId);
+ return waitingQueryId == queryId;
}
}
}
diff --git a/java/com/android/incallui/ExternalCallNotifier.java b/java/com/android/incallui/ExternalCallNotifier.java
index 466e12a6d..6ec94a631 100644
--- a/java/com/android/incallui/ExternalCallNotifier.java
+++ b/java/com/android/incallui/ExternalCallNotifier.java
@@ -41,6 +41,8 @@ import com.android.contacts.common.compat.CallCompat;
import com.android.contacts.common.preference.ContactsPreferences;
import com.android.contacts.common.util.BitmapUtil;
import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.dialer.notification.NotificationChannelManager;
+import com.android.dialer.notification.NotificationChannelManager.Channel;
import com.android.incallui.call.DialerCall;
import com.android.incallui.call.DialerCallDelegate;
import com.android.incallui.call.ExternalCallList;
@@ -57,9 +59,9 @@ import java.util.Map;
public class ExternalCallNotifier implements ExternalCallList.ExternalCallListener {
/** Tag used with the notification manager to uniquely identify external call notifications. */
- private static final String NOTIFICATION_TAG = "EXTERNAL_CALL";
+ private static final int NOTIFICATION_ID = R.id.notification_external_call;
- private static final int SUMMARY_ID = -1;
+ private static final String NOTIFICATION_GROUP = "ExternalCallNotifier";
private final Context mContext;
private final ContactInfoCache mContactInfoCache;
private Map<Call, NotificationInfo> mNotifications = new ArrayMap<>();
@@ -186,14 +188,15 @@ public class ExternalCallNotifier implements ExternalCallList.ExternalCallListen
NotificationManager notificationManager =
(NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
- notificationManager.cancel(NOTIFICATION_TAG, mNotifications.get(call).getNotificationId());
+ notificationManager.cancel(
+ String.valueOf(mNotifications.get(call).getNotificationId()), NOTIFICATION_ID);
mNotifications.remove(call);
if (mShowingSummary && mNotifications.size() <= 1) {
// Where a summary notification is showing and there is now not enough notifications to
// necessitate a summary, cancel the summary.
- notificationManager.cancel(NOTIFICATION_TAG, SUMMARY_ID);
+ notificationManager.cancel(NOTIFICATION_GROUP, NOTIFICATION_ID);
mShowingSummary = false;
// If there is still a single call requiring a notification, re-post the notification as a
@@ -234,7 +237,7 @@ public class ExternalCallNotifier implements ExternalCallList.ExternalCallListen
builder.setOngoing(true);
// Make the notification prioritized over the other normal notifications.
builder.setPriority(Notification.PRIORITY_HIGH);
- builder.setGroup(NOTIFICATION_TAG);
+ builder.setGroup(NOTIFICATION_GROUP);
boolean isVideoCall = VideoProfile.isVideo(info.getCall().getDetails().getVideoState());
// Set the content ("Ongoing call on another device")
@@ -249,6 +252,9 @@ public class ExternalCallNotifier implements ExternalCallList.ExternalCallListen
builder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color));
builder.addPerson(info.getPersonReference());
+ NotificationChannelManager.applyChannel(
+ builder, mContext, Channel.EXTERNAL_CALL, info.getCall().getDetails().getAccountHandle());
+
// Where the external call supports being transferred to the local device, add an action
// to the notification to initiate the call pull process.
if (CallCompat.canPullExternalCall(info.getCall())) {
@@ -281,12 +287,19 @@ public class ExternalCallNotifier implements ExternalCallList.ExternalCallListen
publicBuilder.setSmallIcon(R.drawable.quantum_ic_call_white_24);
publicBuilder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color));
+ NotificationChannelManager.applyChannel(
+ publicBuilder,
+ mContext,
+ Channel.EXTERNAL_CALL,
+ info.getCall().getDetails().getAccountHandle());
+
builder.setPublicVersion(publicBuilder.build());
Notification notification = builder.build();
NotificationManager notificationManager =
(NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
- notificationManager.notify(NOTIFICATION_TAG, info.getNotificationId(), notification);
+ notificationManager.notify(
+ String.valueOf(info.getNotificationId()), NOTIFICATION_ID, notification);
if (!mShowingSummary && mNotifications.size() > 1) {
// If the number of notifications shown is > 1, and we're not already showing a group summary,
@@ -297,10 +310,12 @@ public class ExternalCallNotifier implements ExternalCallList.ExternalCallListen
summary.setOngoing(true);
// Make the notification prioritized over the other normal notifications.
summary.setPriority(Notification.PRIORITY_HIGH);
- summary.setGroup(NOTIFICATION_TAG);
+ summary.setGroup(NOTIFICATION_GROUP);
summary.setGroupSummary(true);
summary.setSmallIcon(R.drawable.quantum_ic_call_white_24);
- notificationManager.notify(NOTIFICATION_TAG, SUMMARY_ID, summary.build());
+ NotificationChannelManager.applyChannel(
+ summary, mContext, Channel.EXTERNAL_CALL, info.getCall().getDetails().getAccountHandle());
+ notificationManager.notify(NOTIFICATION_GROUP, NOTIFICATION_ID, summary.build());
mShowingSummary = true;
}
}
diff --git a/java/com/android/incallui/InCallActivity.java b/java/com/android/incallui/InCallActivity.java
index 307415916..7c4394872 100644
--- a/java/com/android/incallui/InCallActivity.java
+++ b/java/com/android/incallui/InCallActivity.java
@@ -32,6 +32,7 @@ import android.view.KeyEvent;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
+import com.android.dialer.common.Assert;
import com.android.dialer.common.LogUtil;
import com.android.dialer.compat.ActivityCompat;
import com.android.dialer.logging.Logger;
@@ -44,7 +45,6 @@ import com.android.incallui.answerproximitysensor.PseudoScreenState;
import com.android.incallui.call.CallList;
import com.android.incallui.call.DialerCall;
import com.android.incallui.call.DialerCall.State;
-import com.android.incallui.call.VideoUtils;
import com.android.incallui.incall.bindings.InCallBindings;
import com.android.incallui.incall.protocol.InCallButtonUiDelegate;
import com.android.incallui.incall.protocol.InCallButtonUiDelegateFactory;
@@ -89,11 +89,7 @@ public class InCallActivity extends TransactionSafeFragmentActivity
}
public static Intent getIntent(
- Context context,
- boolean showDialpad,
- boolean newOutgoingCall,
- boolean isVideoCall,
- boolean isForFullScreen) {
+ Context context, boolean showDialpad, boolean newOutgoingCall, boolean isForFullScreen) {
Intent intent = new Intent(Intent.ACTION_MAIN, null);
intent.setFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION | Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setClass(context, InCallActivity.class);
@@ -192,7 +188,22 @@ public class InCallActivity extends TransactionSafeFragmentActivity
@Override
public void finish() {
if (shouldCloseActivityOnFinish()) {
- super.finish();
+ // When user select incall ui from recents after the call is disconnected, it tries to launch
+ // a new InCallActivity but InCallPresenter is already teared down at this point, which causes
+ // crash.
+ // By calling finishAndRemoveTask() instead of finish() the task associated with
+ // InCallActivity is cleared completely. So system won't try to create a new InCallActivity in
+ // this case.
+ //
+ // Calling finish won't clear the task and normally when an activity finishes it shouldn't
+ // clear the task since there could be parent activity in the same task that's still alive.
+ // But InCallActivity is special since it's singleInstance which means it's root activity and
+ // only instance of activity in the task. So it should be safe to also remove task when
+ // finishing.
+ // It's also necessary in the sense of it's excluded from recents. So whenever the activity
+ // finishes, the task should also be removed since it doesn't make sense to go back to it in
+ // anyway anymore.
+ super.finishAndRemoveTask();
}
}
@@ -260,18 +271,12 @@ public class InCallActivity extends TransactionSafeFragmentActivity
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
- if (common.onKeyUp(keyCode, event)) {
- return true;
- }
- return super.onKeyUp(keyCode, event);
+ return common.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
- if (common.onKeyDown(keyCode, event)) {
- return true;
- }
- return super.onKeyDown(keyCode, event);
+ return common.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event);
}
public boolean isInCallScreenAnimating() {
@@ -411,13 +416,6 @@ public class InCallActivity extends TransactionSafeFragmentActivity
common.setExcludeFromRecents(exclude);
}
- public void onResolveIntent(
- DialerCall outgoingCall, boolean isNewOutgoingCall, boolean didShowAccountSelectionDialog) {
- if (didShowAccountSelectionDialog) {
- hideMainInCallFragment();
- }
- }
-
@Nullable
public FragmentManager getDialpadFragmentManager() {
InCallScreen inCallScreen = getInCallScreen();
@@ -488,7 +486,7 @@ public class InCallActivity extends TransactionSafeFragmentActivity
enableInCallOrientationEventListener(allowOrientationChange);
}
- private void hideMainInCallFragment() {
+ public void hideMainInCallFragment() {
LogUtil.i("InCallActivity.hideMainInCallFragment", "");
if (didShowInCallScreen || didShowVideoCallScreen) {
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
@@ -513,8 +511,8 @@ public class InCallActivity extends TransactionSafeFragmentActivity
}
isInShowMainInCallFragment = true;
- ShouldShowAnswerUiResult shouldShowAnswerUi = getShouldShowAnswerUi();
- boolean shouldShowVideoUi = getShouldShowVideoUi();
+ ShouldShowUiResult shouldShowAnswerUi = getShouldShowAnswerUi();
+ ShouldShowUiResult shouldShowVideoUi = getShouldShowVideoUi();
LogUtil.i(
"InCallActivity.showMainInCallFragment",
"shouldShowAnswerUi: %b, shouldShowVideoUi: %b, "
@@ -525,7 +523,7 @@ public class InCallActivity extends TransactionSafeFragmentActivity
didShowInCallScreen,
didShowVideoCallScreen);
// Only video call ui allows orientation change.
- setAllowOrientationChange(shouldShowVideoUi);
+ setAllowOrientationChange(shouldShowVideoUi.shouldShow);
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
boolean didChangeInCall;
@@ -535,9 +533,9 @@ public class InCallActivity extends TransactionSafeFragmentActivity
didChangeInCall = hideInCallScreenFragment(transaction);
didChangeVideo = hideVideoCallScreenFragment(transaction);
didChangeAnswer = showAnswerScreenFragment(transaction, shouldShowAnswerUi.call);
- } else if (shouldShowVideoUi) {
+ } else if (shouldShowVideoUi.shouldShow) {
didChangeInCall = hideInCallScreenFragment(transaction);
- didChangeVideo = showVideoCallScreenFragment(transaction);
+ didChangeVideo = showVideoCallScreenFragment(transaction, shouldShowVideoUi.call);
didChangeAnswer = hideAnswerScreenFragment(transaction);
} else {
didChangeInCall = showInCallScreenFragment(transaction);
@@ -552,17 +550,17 @@ public class InCallActivity extends TransactionSafeFragmentActivity
isInShowMainInCallFragment = false;
}
- private ShouldShowAnswerUiResult getShouldShowAnswerUi() {
+ private ShouldShowUiResult getShouldShowAnswerUi() {
DialerCall call = CallList.getInstance().getIncomingCall();
if (call != null) {
LogUtil.i("InCallActivity.getShouldShowAnswerUi", "found incoming call");
- return new ShouldShowAnswerUiResult(true, call);
+ return new ShouldShowUiResult(true, call);
}
call = CallList.getInstance().getVideoUpgradeRequestCall();
if (call != null) {
LogUtil.i("InCallActivity.getShouldShowAnswerUi", "found video upgrade request");
- return new ShouldShowAnswerUiResult(true, call);
+ return new ShouldShowUiResult(true, call);
}
// Check if we're showing the answer screen and the call is disconnected. If this condition is
@@ -574,30 +572,30 @@ public class InCallActivity extends TransactionSafeFragmentActivity
}
if (didShowAnswerScreen && (call == null || call.getState() == State.DISCONNECTED)) {
LogUtil.i("InCallActivity.getShouldShowAnswerUi", "found disconnecting incoming call");
- return new ShouldShowAnswerUiResult(true, call);
+ return new ShouldShowUiResult(true, call);
}
- return new ShouldShowAnswerUiResult(false, null);
+ return new ShouldShowUiResult(false, null);
}
- private boolean getShouldShowVideoUi() {
+ private static ShouldShowUiResult getShouldShowVideoUi() {
DialerCall call = CallList.getInstance().getFirstCall();
if (call == null) {
LogUtil.i("InCallActivity.getShouldShowVideoUi", "null call");
- return false;
+ return new ShouldShowUiResult(false, null);
}
- if (VideoUtils.isVideoCall(call)) {
+ if (call.isVideoCall()) {
LogUtil.i("InCallActivity.getShouldShowVideoUi", "found video call");
- return true;
+ return new ShouldShowUiResult(true, call);
}
- if (VideoUtils.hasSentVideoUpgradeRequest(call)) {
+ if (call.hasSentVideoUpgradeRequest()) {
LogUtil.i("InCallActivity.getShouldShowVideoUi", "upgrading to video");
- return true;
+ return new ShouldShowUiResult(true, call);
}
- return false;
+ return new ShouldShowUiResult(false, null);
}
private boolean showAnswerScreenFragment(FragmentTransaction transaction, DialerCall call) {
@@ -607,14 +605,15 @@ public class InCallActivity extends TransactionSafeFragmentActivity
return false;
}
- boolean isVideoUpgradeRequest = VideoUtils.hasReceivedVideoUpgradeRequest(call);
- int videoState = isVideoUpgradeRequest ? call.getRequestedVideoState() : call.getVideoState();
+ Assert.checkArgument(call != null, "didShowAnswerScreen was false but call was still null");
+
+ boolean isVideoUpgradeRequest = call.hasReceivedVideoUpgradeRequest();
// Check if we're already showing an answer screen for this call.
if (didShowAnswerScreen) {
AnswerScreen answerScreen = getAnswerScreen();
if (answerScreen.getCallId().equals(call.getId())
- && answerScreen.getVideoState() == videoState
+ && answerScreen.isVideoCall() == call.isVideoCall()
&& answerScreen.isVideoUpgradeRequest() == isVideoUpgradeRequest) {
return false;
}
@@ -626,7 +625,7 @@ public class InCallActivity extends TransactionSafeFragmentActivity
// Show a new answer screen.
AnswerScreen answerScreen =
- AnswerBindings.createAnswerScreen(call.getId(), videoState, isVideoUpgradeRequest);
+ AnswerBindings.createAnswerScreen(call.getId(), call.isVideoCall(), isVideoUpgradeRequest);
transaction.add(R.id.main, answerScreen.getAnswerScreenFragment(), TAG_ANSWER_SCREEN);
Logger.get(this).logScreenView(ScreenEvent.Type.INCOMING_CALL, this);
@@ -675,12 +674,21 @@ public class InCallActivity extends TransactionSafeFragmentActivity
return true;
}
- private boolean showVideoCallScreenFragment(FragmentTransaction transaction) {
+ private boolean showVideoCallScreenFragment(FragmentTransaction transaction, DialerCall call) {
if (didShowVideoCallScreen) {
- return false;
+ VideoCallScreen videoCallScreen = getVideoCallScreen();
+ if (videoCallScreen.getCallId().equals(call.getId())) {
+ return false;
+ }
+ LogUtil.i(
+ "InCallActivity.showVideoCallScreenFragment",
+ "video call fragment exists but arguments do not match");
+ hideVideoCallScreenFragment(transaction);
}
- VideoCallScreen videoCallScreen = VideoBindings.createVideoCallScreen();
+ LogUtil.i("InCallActivity.showVideoCallScreenFragment", "call: %s", call);
+
+ VideoCallScreen videoCallScreen = VideoBindings.createVideoCallScreen(call.getId());
transaction.add(R.id.main, videoCallScreen.getVideoCallScreenFragment(), TAG_VIDEO_CALL_SCREEN);
Logger.get(this).logScreenView(ScreenEvent.Type.INCALL, this);
@@ -744,11 +752,11 @@ public class InCallActivity extends TransactionSafeFragmentActivity
return super.dispatchTouchEvent(event);
}
- private static class ShouldShowAnswerUiResult {
+ private static class ShouldShowUiResult {
public final boolean shouldShow;
public final DialerCall call;
- ShouldShowAnswerUiResult(boolean shouldShow, DialerCall call) {
+ ShouldShowUiResult(boolean shouldShow, DialerCall call) {
this.shouldShow = shouldShow;
this.call = call;
}
diff --git a/java/com/android/incallui/InCallActivityCommon.java b/java/com/android/incallui/InCallActivityCommon.java
index a2467dd72..2cdb913ce 100644
--- a/java/com/android/incallui/InCallActivityCommon.java
+++ b/java/com/android/incallui/InCallActivityCommon.java
@@ -21,7 +21,6 @@ import android.app.ActivityManager.AppTask;
import android.app.ActivityManager.TaskDescription;
import android.app.AlertDialog;
import android.app.Dialog;
-import android.app.DialogFragment;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
@@ -99,6 +98,7 @@ public class InCallActivityCommon {
private String showPostCharWaitDialogCallId;
private String showPostCharWaitDialogChars;
private Dialog dialog;
+ private SelectPhoneAccountDialogFragment selectPhoneAccountDialogFragment;
private InCallOrientationEventListener inCallOrientationEventListener;
private Animation dialpadSlideInAnimation;
private Animation dialpadSlideOutAnimation;
@@ -496,11 +496,15 @@ public class InCallActivityCommon {
}
}
- public void dismissPendingDialogs() {
+ void dismissPendingDialogs() {
if (dialog != null) {
dialog.dismiss();
dialog = null;
}
+ if (selectPhoneAccountDialogFragment != null) {
+ selectPhoneAccountDialogFragment.dismiss();
+ selectPhoneAccountDialogFragment = null;
+ }
}
private static boolean shouldShowDisconnectErrorDialog(@NonNull DisconnectCause cause) {
@@ -769,9 +773,7 @@ public class InCallActivityCommon {
outgoingCall = CallList.getInstance().getPendingOutgoingCall();
}
- boolean isNewOutgoingCall = false;
if (intent.getBooleanExtra(INTENT_EXTRA_NEW_OUTGOING_CALL, false)) {
- isNewOutgoingCall = true;
intent.removeExtra(INTENT_EXTRA_NEW_OUTGOING_CALL);
// InCallActivity is responsible for disconnecting a new outgoing call if there
@@ -789,16 +791,18 @@ public class InCallActivityCommon {
}
boolean didShowAccountSelectionDialog = maybeShowAccountSelectionDialog();
- inCallActivity.onResolveIntent(outgoingCall, isNewOutgoingCall, didShowAccountSelectionDialog);
+ if (didShowAccountSelectionDialog) {
+ inCallActivity.hideMainInCallFragment();
+ }
}
private boolean maybeShowAccountSelectionDialog() {
- DialerCall call = CallList.getInstance().getWaitingForAccountCall();
- if (call == null) {
+ DialerCall waitingForAccountCall = CallList.getInstance().getWaitingForAccountCall();
+ if (waitingForAccountCall == null) {
return false;
}
- Bundle extras = call.getIntentExtras();
+ Bundle extras = waitingForAccountCall.getIntentExtras();
List<PhoneAccountHandle> phoneAccountHandles;
if (extras != null) {
phoneAccountHandles =
@@ -807,14 +811,15 @@ public class InCallActivityCommon {
phoneAccountHandles = new ArrayList<>();
}
- DialogFragment dialogFragment =
+ selectPhoneAccountDialogFragment =
SelectPhoneAccountDialogFragment.newInstance(
R.string.select_phone_account_for_calls,
true,
phoneAccountHandles,
selectAccountListener,
- call.getId());
- dialogFragment.show(inCallActivity.getFragmentManager(), TAG_SELECT_ACCOUNT_FRAGMENT);
+ waitingForAccountCall.getId());
+ selectPhoneAccountDialogFragment.show(
+ inCallActivity.getFragmentManager(), TAG_SELECT_ACCOUNT_FRAGMENT);
return true;
}
}
diff --git a/java/com/android/incallui/InCallPresenter.java b/java/com/android/incallui/InCallPresenter.java
index 97105fb78..0f3982ce4 100644
--- a/java/com/android/incallui/InCallPresenter.java
+++ b/java/com/android/incallui/InCallPresenter.java
@@ -42,23 +42,22 @@ import com.android.dialer.blocking.FilteredNumbersUtil;
import com.android.dialer.common.LogUtil;
import com.android.dialer.logging.Logger;
import com.android.dialer.logging.nano.InteractionEvent;
+import com.android.dialer.postcall.PostCall;
import com.android.dialer.telecom.TelecomUtil;
import com.android.dialer.util.TouchPointManager;
import com.android.incallui.InCallOrientationEventListener.ScreenOrientation;
import com.android.incallui.answerproximitysensor.PseudoScreenState;
import com.android.incallui.call.CallList;
import com.android.incallui.call.DialerCall;
-import com.android.incallui.call.DialerCall.SessionModificationState;
import com.android.incallui.call.ExternalCallList;
-import com.android.incallui.call.InCallVideoCallCallbackNotifier;
import com.android.incallui.call.TelecomAdapter;
-import com.android.incallui.call.VideoUtils;
import com.android.incallui.latencyreport.LatencyReport;
import com.android.incallui.legacyblocking.BlockedNumberContentObserver;
import com.android.incallui.spam.SpamCallListListener;
import com.android.incallui.util.TelecomCallUtil;
import com.android.incallui.videosurface.bindings.VideoSurfaceBindings;
import com.android.incallui.videosurface.protocol.VideoSurfaceTexture;
+import com.android.incallui.videotech.VideoTech;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
@@ -74,8 +73,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
* presenters that want to listen in on the in-call state changes. TODO: This class has become more
* of a state machine at this point. Consider renaming.
*/
-public class InCallPresenter
- implements CallList.Listener, InCallVideoCallCallbackNotifier.SessionModificationListener {
+public class InCallPresenter implements CallList.Listener {
private static final String EXTRA_FIRST_TIME_SHOWN =
"com.android.incallui.intent.extra.FIRST_TIME_SHOWN";
@@ -173,7 +171,6 @@ public class InCallPresenter
private ProximitySensor mProximitySensor;
private final PseudoScreenState mPseudoScreenState = new PseudoScreenState();
private boolean mServiceConnected;
- private boolean mAccountSelectionCancelled;
private InCallCameraManager mInCallCameraManager;
private FilteredNumberAsyncQueryHandler mFilteredQueryHandler;
private CallList.Listener mSpamCallListListener;
@@ -347,7 +344,6 @@ public class InCallPresenter
mCallList.addListener(mSpamCallListListener);
VideoPauseController.getInstance().setUp(this);
- InCallVideoCallCallbackNotifier.getInstance().addSessionModificationListener(this);
mFilteredQueryHandler = new FilteredNumberAsyncQueryHandler(context);
mContext
@@ -376,7 +372,6 @@ public class InCallPresenter
attemptCleanup();
VideoPauseController.getInstance().tearDown();
- InCallVideoCallCallbackNotifier.getInstance().removeSessionModificationListener(this);
}
private void attemptFinishActivity() {
@@ -385,12 +380,6 @@ public class InCallPresenter
if (doFinish) {
mInCallActivity.setExcludeFromRecents(true);
mInCallActivity.finish();
-
- if (mAccountSelectionCancelled) {
- // This finish is a result of account selection cancellation
- // do not include activity ending transition
- mInCallActivity.overridePendingTransition(0, 0);
- }
}
}
@@ -664,6 +653,19 @@ public class InCallPresenter
InCallState newState = getPotentialStateFromCallList(callList);
InCallState oldState = mInCallState;
Log.d(this, "onCallListChange oldState= " + oldState + " newState=" + newState);
+
+ // If the user placed a call and was asked to choose the account, but then pressed "Home", the
+ // incall activity for that call will still exist (even if it's not visible). In the case of
+ // an incoming call in that situation, just disconnect that "waiting for account" call and
+ // dismiss the dialog. The same activity will be reused to handle the new incoming call. See
+ // b/33247755 for more details.
+ DialerCall waitingForAccountCall;
+ if (newState == InCallState.INCOMING
+ && (waitingForAccountCall = callList.getWaitingForAccountCall()) != null) {
+ waitingForAccountCall.disconnect();
+ mInCallActivity.dismissPendingDialogs();
+ }
+
newState = startOrFinishUi(newState);
Log.d(this, "onCallListChange newState changed to " + newState);
@@ -705,13 +707,13 @@ public class InCallPresenter
@Override
public void onUpgradeToVideo(DialerCall call) {
- if (call.getSessionModificationState()
- == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST
+ if (call.getVideoTech().getSessionModificationState()
+ == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST
&& mInCallState == InCallPresenter.InCallState.INCOMING) {
LogUtil.i(
"InCallPresenter.onUpgradeToVideo",
"rejecting upgrade request due to existing incoming call");
- call.declineUpgradeRequest();
+ call.getVideoTech().declineVideoRequest();
}
if (mInCallActivity != null) {
@@ -721,15 +723,15 @@ public class InCallPresenter
}
@Override
- public void onSessionModificationStateChange(@SessionModificationState int newState) {
+ public void onSessionModificationStateChange(DialerCall call) {
+ int newState = call.getVideoTech().getSessionModificationState();
LogUtil.i("InCallPresenter.onSessionModificationStateChange", "state: %d", newState);
if (mProximitySensor == null) {
LogUtil.i("InCallPresenter.onSessionModificationStateChange", "proximitySensor is null");
return;
}
mProximitySensor.setIsAttemptingVideoCall(
- VideoUtils.hasSentVideoUpgradeRequest(newState)
- || VideoUtils.hasReceivedVideoUpgradeRequest(newState));
+ call.hasSentVideoUpgradeRequest() || call.hasReceivedVideoUpgradeRequest());
if (mInCallActivity != null) {
// Re-evaluate which fragment is being shown.
mInCallActivity.onPrimaryCallStateChanged();
@@ -754,19 +756,10 @@ public class InCallPresenter
if (call.isEmergencyCall()) {
FilteredNumbersUtil.recordLastEmergencyCallTime(mContext);
}
- }
-
- @Override
- public void onUpgradeToVideoRequest(DialerCall call, int videoState) {
- LogUtil.d(
- "InCallPresenter.onUpgradeToVideoRequest",
- "call = " + call + " video state = " + videoState);
- if (call == null) {
- return;
+ if (!call.getLogState().isIncoming && !mCallList.hasLiveCall()) {
+ PostCall.onCallDisconnected(mContext, call.getNumber(), call.getConnectTimeMillis());
}
-
- call.setRequestedVideoState(videoState);
}
/** Given the call list, return the state in which the in-call screen should be. */
@@ -916,6 +909,24 @@ public class InCallPresenter
&& !mInCallActivity.isFinishing());
}
+ private boolean isActivityVisible() {
+ return mInCallActivity != null && mInCallActivity.isVisible();
+ }
+
+ boolean shouldShowFullScreenNotification() {
+ /**
+ * This is to cover the case where the incall activity is started but in the background, e.g.
+ * when the user pressed Home from the account selection dialog or an existing call. In the case
+ * that incall activity is already visible, there's no need to configure the notification with a
+ * full screen intent.
+ */
+ LogUtil.d(
+ "InCallPresenter.shouldShowFullScreenNotification",
+ "isActivityVisible: %b",
+ isActivityVisible());
+ return !isActivityVisible();
+ }
+
/**
* Determines if the In-Call app is currently changing configuration.
*
@@ -1018,7 +1029,7 @@ public class InCallPresenter
// present (e.g. a call was accepted by a bluetooth or wired headset), we want to
// bring it up the UI regardless.
if (!isShowingInCallUi() && mInCallState != InCallState.NO_CALLS) {
- showInCall(showDialpad, false /* newOutgoingCall */, false /* isVideoCall */);
+ showInCall(showDialpad, false /* newOutgoingCall */);
}
}
@@ -1281,7 +1292,7 @@ public class InCallPresenter
if (showCallUi || showAccountPicker) {
Log.i(this, "Start in call UI");
- showInCall(false /* showDialpad */, !showAccountPicker /* newOutgoingCall */, false);
+ showInCall(false /* showDialpad */, !showAccountPicker /* newOutgoingCall */);
} else if (startIncomingCallSequence) {
Log.i(this, "Start Full Screen in call UI");
@@ -1332,7 +1343,7 @@ public class InCallPresenter
mCallList.getActiveCall() != null && mCallList.getIncomingCall() != null;
if (isCallWaiting) {
- showInCall(false, false, false /* isVideoCall */);
+ showInCall(false, false);
} else {
mStatusBarNotifier.updateNotification(mCallList);
}
@@ -1403,11 +1414,11 @@ public class InCallPresenter
}
}
- public void showInCall(boolean showDialpad, boolean newOutgoingCall, boolean isVideoCall) {
+ public void showInCall(boolean showDialpad, boolean newOutgoingCall) {
Log.i(this, "Showing InCallActivity");
mContext.startActivity(
InCallActivity.getIntent(
- mContext, showDialpad, newOutgoingCall, isVideoCall, false /* forFullScreen */));
+ mContext, showDialpad, newOutgoingCall, false /* forFullScreen */));
}
public void onServiceBind() {
@@ -1441,15 +1452,11 @@ public class InCallPresenter
final PhoneAccountHandle accountHandle =
intent.getParcelableExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE);
final Point touchPoint = extras.getParcelable(TouchPointManager.TOUCH_POINT);
- int videoState =
- extras.getInt(
- TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, VideoProfile.STATE_AUDIO_ONLY);
InCallPresenter.getInstance().setBoundAndWaitingForOutgoingCall(true, accountHandle);
final Intent activityIntent =
- InCallActivity.getIntent(
- mContext, false, true, VideoUtils.isVideoCall(videoState), false /* forFullScreen */);
+ InCallActivity.getIntent(mContext, false, true, false /* forFullScreen */);
activityIntent.putExtra(TouchPointManager.TOUCH_POINT, touchPoint);
mContext.startActivity(activityIntent);
}
diff --git a/java/com/android/incallui/NotificationBroadcastReceiver.java b/java/com/android/incallui/NotificationBroadcastReceiver.java
index 5c5d255cc..cef18958e 100644
--- a/java/com/android/incallui/NotificationBroadcastReceiver.java
+++ b/java/com/android/incallui/NotificationBroadcastReceiver.java
@@ -27,7 +27,6 @@ import com.android.dialer.logging.Logger;
import com.android.dialer.logging.nano.DialerImpression;
import com.android.incallui.call.CallList;
import com.android.incallui.call.DialerCall;
-import com.android.incallui.call.VideoUtils;
/**
* Accepts broadcast Intents which will be prepared by {@link StatusBarNotifier} and thus sent from
@@ -96,7 +95,7 @@ public class NotificationBroadcastReceiver extends BroadcastReceiver {
} else {
DialerCall call = callList.getVideoUpgradeRequestCall();
if (call != null) {
- call.acceptUpgradeRequest(call.getRequestedVideoState());
+ call.getVideoTech().acceptVideoRequest();
}
}
}
@@ -109,7 +108,7 @@ public class NotificationBroadcastReceiver extends BroadcastReceiver {
} else {
DialerCall call = callList.getVideoUpgradeRequestCall();
if (call != null) {
- call.declineUpgradeRequest();
+ call.getVideoTech().declineVideoRequest();
}
}
}
@@ -142,10 +141,7 @@ public class NotificationBroadcastReceiver extends BroadcastReceiver {
if (call != null) {
call.answer(videoState);
InCallPresenter.getInstance()
- .showInCall(
- false /* showDialpad */,
- false /* newOutgoingCall */,
- VideoUtils.isVideoCall(videoState));
+ .showInCall(false /* showDialpad */, false /* newOutgoingCall */);
}
}
}
diff --git a/java/com/android/incallui/ProximitySensor.java b/java/com/android/incallui/ProximitySensor.java
index 91220627c..229b58ce7 100644
--- a/java/com/android/incallui/ProximitySensor.java
+++ b/java/com/android/incallui/ProximitySensor.java
@@ -28,7 +28,7 @@ import com.android.incallui.AudioModeProvider.AudioModeListener;
import com.android.incallui.InCallPresenter.InCallState;
import com.android.incallui.InCallPresenter.InCallStateListener;
import com.android.incallui.call.CallList;
-import com.android.incallui.call.VideoUtils;
+import com.android.incallui.call.DialerCall;
/**
* Class manages the proximity sensor for the in-call UI. We enable the proximity sensor while the
@@ -103,7 +103,8 @@ public class ProximitySensor
boolean hasOngoingCall = InCallState.INCALL == newState && callList.hasLiveCall();
boolean isOffhook = (InCallState.OUTGOING == newState) || hasOngoingCall;
- boolean isVideoCall = VideoUtils.isVideoCall(callList.getActiveCall());
+ DialerCall activeCall = callList.getActiveCall();
+ boolean isVideoCall = activeCall != null && activeCall.isVideoCall();
if (isOffhook != mIsPhoneOffhook || mIsVideoCall != isVideoCall) {
mIsPhoneOffhook = isOffhook;
diff --git a/java/com/android/incallui/StatusBarNotifier.java b/java/com/android/incallui/StatusBarNotifier.java
index c7226753f..d6262be18 100644
--- a/java/com/android/incallui/StatusBarNotifier.java
+++ b/java/com/android/incallui/StatusBarNotifier.java
@@ -24,8 +24,10 @@ import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_
import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_VIDEO_UPGRADE_REQUEST;
import static com.android.incallui.NotificationBroadcastReceiver.ACTION_HANG_UP_ONGOING_CALL;
+import android.Manifest;
import android.app.ActivityManager;
import android.app.Notification;
+import android.app.Notification.Builder;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
@@ -34,6 +36,7 @@ import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
import android.media.AudioAttributes;
import android.net.Uri;
import android.os.Build.VERSION;
@@ -41,10 +44,13 @@ import android.os.Build.VERSION_CODES;
import android.support.annotation.ColorRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.annotation.RequiresPermission;
import android.support.annotation.StringRes;
import android.support.annotation.VisibleForTesting;
+import android.support.v4.os.BuildCompat;
import android.telecom.Call.Details;
import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.text.BidiFormatter;
import android.text.Spannable;
@@ -54,10 +60,13 @@ import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import com.android.contacts.common.ContactsUtils;
import com.android.contacts.common.ContactsUtils.UserType;
+import com.android.contacts.common.lettertiles.LetterTileDrawable;
import com.android.contacts.common.preference.ContactsPreferences;
import com.android.contacts.common.util.BitmapUtil;
import com.android.contacts.common.util.ContactDisplayUtils;
import com.android.dialer.common.LogUtil;
+import com.android.dialer.notification.NotificationChannelManager;
+import com.android.dialer.notification.NotificationChannelManager.Channel;
import com.android.dialer.util.DrawableConverter;
import com.android.incallui.ContactInfoCache.ContactCacheEntry;
import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback;
@@ -65,11 +74,13 @@ import com.android.incallui.InCallPresenter.InCallState;
import com.android.incallui.async.PausableExecutorImpl;
import com.android.incallui.call.CallList;
import com.android.incallui.call.DialerCall;
-import com.android.incallui.call.DialerCall.SessionModificationState;
import com.android.incallui.call.DialerCallListener;
import com.android.incallui.ringtone.DialerRingtoneManager;
import com.android.incallui.ringtone.InCallTonePlayer;
import com.android.incallui.ringtone.ToneGeneratorFactory;
+import com.android.incallui.videotech.VideoTech;
+import java.util.List;
+import java.util.Locale;
import java.util.Objects;
/** This class adds Notifications to the status bar for the in-call experience. */
@@ -79,9 +90,9 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
// Indicates that no notification is currently showing.
private static final int NOTIFICATION_NONE = 0;
// Notification for an active call. This is non-interruptive, but cannot be dismissed.
- private static final int NOTIFICATION_IN_CALL = 1;
+ private static final int NOTIFICATION_IN_CALL = R.id.notification_ongoing_call;
// Notification for incoming calls. This is interruptive and will show up as a HUN.
- private static final int NOTIFICATION_INCOMING_CALL = 2;
+ private static final int NOTIFICATION_INCOMING_CALL = R.id.notification_incoming_call;
private static final int PENDING_INTENT_REQUEST_CODE_NON_FULL_SCREEN = 0;
private static final int PENDING_INTENT_REQUEST_CODE_FULL_SCREEN = 1;
@@ -101,8 +112,9 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
private String mSavedContentTitle;
private Uri mRingtone;
private StatusBarCallListener mStatusBarCallListener;
+ private boolean mShowFullScreenIntent;
- public StatusBarNotifier(@NonNull Context context, @NonNull ContactInfoCache contactInfoCache) {
+ StatusBarNotifier(@NonNull Context context, @NonNull ContactInfoCache contactInfoCache) {
Objects.requireNonNull(context);
mContext = context;
mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext);
@@ -120,9 +132,9 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
* notifications.
*/
static void clearAllCallNotifications(Context backupContext) {
- Log.i(
- StatusBarNotifier.class.getSimpleName(),
- "Something terrible happened. Clear all InCall notifications");
+ LogUtil.i(
+ "StatusBarNotifier.clearAllCallNotifications",
+ "something terrible happened, clear all InCall notifications");
NotificationManager notificationManager =
backupContext.getSystemService(NotificationManager.class);
@@ -153,10 +165,17 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
return PendingIntent.getBroadcast(context, 0, intent, 0);
}
+ private static void setColorized(@NonNull Builder builder) {
+ if (BuildCompat.isAtLeastO()) {
+ builder.setColorized(true);
+ }
+ }
+
/** Creates notifications according to the state we receive from {@link InCallPresenter}. */
@Override
+ @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
- Log.d(this, "onStateChange");
+ LogUtil.d("StatusBarNotifier.onStateChange", "%s->%s", oldState, newState);
updateNotification(callList);
}
@@ -177,7 +196,8 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
*
* @see #updateInCallNotification(CallList)
*/
- public void updateNotification(CallList callList) {
+ @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
+ void updateNotification(CallList callList) {
updateInCallNotification(callList);
}
@@ -191,7 +211,7 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
setStatusBarCallListener(null);
}
if (mCurrentNotification != NOTIFICATION_NONE) {
- Log.d(this, "cancelInCall()...");
+ LogUtil.d("StatusBarNotifier.cancelNotification", "cancel");
mNotificationManager.cancel(mCurrentNotification);
}
mCurrentNotification = NOTIFICATION_NONE;
@@ -202,8 +222,9 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
* status bar notification based on the current telephony state, or cancels the notification if
* the phone is totally idle.
*/
+ @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
private void updateInCallNotification(CallList callList) {
- Log.d(this, "updateInCallNotification...");
+ LogUtil.d("StatusBarNotifier.updateInCallNotification", "");
final DialerCall call = getCallToShow(callList);
@@ -214,6 +235,7 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
}
}
+ @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
private void showNotification(final CallList callList, final DialerCall call) {
final boolean isIncoming =
(call.getState() == DialerCall.State.INCOMING
@@ -230,6 +252,7 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
isIncoming,
new ContactInfoCacheCallback() {
@Override
+ @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
public void onContactInfoComplete(String callId, ContactCacheEntry entry) {
DialerCall call = callList.getCallById(callId);
if (call != null) {
@@ -239,6 +262,7 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
}
@Override
+ @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
public void onImageLoadComplete(String callId, ContactCacheEntry entry) {
DialerCall call = callList.getCallById(callId);
if (call != null) {
@@ -249,6 +273,7 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
}
/** Sets up the main Ui for the notification */
+ @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
private void buildAndSendNotification(
CallList callList, DialerCall originalCall, ContactCacheEntry contactInfo) {
// This can get called to update an existing notification after contact information has come
@@ -268,8 +293,8 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
final String contentTitle = getContentTitle(contactInfo, call);
final boolean isVideoUpgradeRequest =
- call.getSessionModificationState()
- == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
+ call.getVideoTech().getSessionModificationState()
+ == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
final int notificationType;
if (callState == DialerCall.State.INCOMING
|| callState == DialerCall.State.CALL_WAITING
@@ -286,7 +311,8 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
contentTitle,
callState,
notificationType,
- contactInfo.contactRingtoneUri)) {
+ contactInfo.contactRingtoneUri,
+ InCallPresenter.getInstance().shouldShowFullScreenNotification())) {
return;
}
@@ -300,9 +326,10 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
Notification.Builder publicBuilder = new Notification.Builder(mContext);
publicBuilder
.setSmallIcon(iconResId)
- .setColor(mContext.getResources().getColor(R.color.dialer_theme_color))
+ .setColor(mContext.getResources().getColor(R.color.dialer_theme_color, mContext.getTheme()))
// Hide work call state for the lock screen notification
.setContentTitle(getContentString(call, ContactsUtils.USER_TYPE_CURRENT));
+ setColorized(publicBuilder);
setNotificationWhen(call, callState, publicBuilder);
// Builder for the notification shown when the device is unlocked or the user has set their
@@ -311,28 +338,26 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
builder.setPublicVersion(publicBuilder.build());
// Set up the main intent to send the user to the in-call screen
- builder.setContentIntent(
- createLaunchPendingIntent(false /* isFullScreen */, call.isVideoCall()));
+ builder.setContentIntent(createLaunchPendingIntent(false /* isFullScreen */));
// Set the intent as a full screen intent as well if a call is incoming
+ PhoneAccountHandle accountHandle = call.getAccountHandle();
+ if (accountHandle == null) {
+ accountHandle = getAnyPhoneAccount();
+ }
if (notificationType == NOTIFICATION_INCOMING_CALL) {
- if (!InCallPresenter.getInstance().isActivityStarted()) {
+ NotificationChannelManager.applyChannel(
+ builder, mContext, Channel.INCOMING_CALL, accountHandle);
+ if (InCallPresenter.getInstance().shouldShowFullScreenNotification()) {
configureFullScreenIntent(
- builder,
- createLaunchPendingIntent(true /* isFullScreen */, call.isVideoCall()),
- callList,
- call);
- } else {
- // If the incall screen is already up, we don't want to show HUN but regular notification
- // should still be shown. In order to do that the previous one with full screen intent
- // needs to be cancelled.
- LogUtil.d(
- "StatusBarNotifier.buildAndSendNotification",
- "cancel previous incoming call notification");
- mNotificationManager.cancel(NOTIFICATION_INCOMING_CALL);
+ builder, createLaunchPendingIntent(true /* isFullScreen */), callList, call);
}
- // Set the notification category for incoming calls
+ // Set the notification category and bump the priority for incoming calls
builder.setCategory(Notification.CATEGORY_CALL);
+ builder.setPriority(Notification.PRIORITY_MAX);
+ } else {
+ NotificationChannelManager.applyChannel(
+ builder, mContext, Channel.ONGOING_CALL, accountHandle);
}
// Set the content
@@ -340,7 +365,9 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
builder.setSmallIcon(iconResId);
builder.setContentTitle(contentTitle);
builder.setLargeIcon(largeIcon);
- builder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color));
+ builder.setColor(
+ mContext.getResources().getColor(R.color.dialer_theme_color, mContext.getTheme()));
+ setColorized(builder);
if (isVideoUpgradeRequest) {
builder.setUsesChronometer(false);
@@ -367,15 +394,20 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
}
}
if (mDialerRingtoneManager.shouldPlayCallWaitingTone(callState)) {
- Log.v(this, "Playing call waiting tone");
+ LogUtil.v("StatusBarNotifier.buildAndSendNotification", "playing call waiting tone");
mDialerRingtoneManager.playCallWaitingTone();
}
if (mCurrentNotification != notificationType && mCurrentNotification != NOTIFICATION_NONE) {
- Log.i(this, "Previous notification already showing - cancelling " + mCurrentNotification);
+ LogUtil.i(
+ "StatusBarNotifier.buildAndSendNotification",
+ "previous notification already showing - cancelling " + mCurrentNotification);
mNotificationManager.cancel(mCurrentNotification);
}
- Log.i(this, "Displaying notification for " + notificationType);
+ LogUtil.i(
+ "StatusBarNotifier.buildAndSendNotification",
+ "displaying notification for " + notificationType);
+
try {
mNotificationManager.notify(notificationType, notification);
} catch (RuntimeException e) {
@@ -385,14 +417,32 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
activityManager.getMemoryInfo(memoryInfo);
throw new RuntimeException(
String.format(
+ Locale.US,
"Error displaying notification with photo type: %d (low memory? %b, availMem: %d)",
- contactInfo.photoType, memoryInfo.lowMemory, memoryInfo.availMem),
+ contactInfo.photoType,
+ memoryInfo.lowMemory,
+ memoryInfo.availMem),
e);
}
call.getLatencyReport().onNotificationShown();
mCurrentNotification = notificationType;
}
+ @Nullable
+ @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
+ private PhoneAccountHandle getAnyPhoneAccount() {
+ PhoneAccountHandle accountHandle;
+ TelecomManager telecomManager = mContext.getSystemService(TelecomManager.class);
+ accountHandle = telecomManager.getDefaultOutgoingPhoneAccount(PhoneAccount.SCHEME_TEL);
+ if (accountHandle == null) {
+ List<PhoneAccountHandle> accountHandles = telecomManager.getCallCapablePhoneAccounts();
+ if (!accountHandles.isEmpty()) {
+ accountHandle = accountHandles.get(0);
+ }
+ }
+ return accountHandle;
+ }
+
private void createIncomingCallNotification(
DialerCall call, int state, Notification.Builder builder) {
setNotificationWhen(call, state, builder);
@@ -438,7 +488,8 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
String contentTitle,
int state,
int notificationType,
- Uri ringtone) {
+ Uri ringtone,
+ boolean showFullScreenIntent) {
// The two are different:
// if new title is not null, it should be different from saved version OR
@@ -454,13 +505,15 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
|| (mCallState != state)
|| (mSavedLargeIcon != largeIcon)
|| contentTitleChanged
- || !Objects.equals(mRingtone, ringtone);
+ || !Objects.equals(mRingtone, ringtone)
+ || mShowFullScreenIntent != showFullScreenIntent;
// If we aren't showing a notification right now or the notification type is changing,
// definitely do an update.
if (mCurrentNotification != notificationType) {
if (mCurrentNotification == NOTIFICATION_NONE) {
- Log.d(this, "Showing notification for first time.");
+ LogUtil.d(
+ "StatusBarNotifier.checkForChangeAndSaveData", "showing notification for first time.");
}
retval = true;
}
@@ -471,9 +524,11 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
mSavedLargeIcon = largeIcon;
mSavedContentTitle = contentTitle;
mRingtone = ringtone;
+ mShowFullScreenIntent = showFullScreenIntent;
if (retval) {
- Log.d(this, "Data changed. Showing notification");
+ LogUtil.d(
+ "StatusBarNotifier.checkForChangeAndSaveData", "data changed. Showing notification");
}
return retval;
@@ -520,8 +575,34 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) {
largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap();
}
+ if (contactInfo.photo == null) {
+ int width =
+ (int) mContext.getResources().getDimension(android.R.dimen.notification_large_icon_width);
+ int height =
+ (int)
+ mContext.getResources().getDimension(android.R.dimen.notification_large_icon_height);
+ int contactType = LetterTileDrawable.TYPE_DEFAULT;
+ LetterTileDrawable lettertile = new LetterTileDrawable(mContext.getResources());
+
+ // TODO: Deduplicate across Dialer. b/36195917
+ if (CallerInfoUtils.isVoiceMailNumber(mContext, call)) {
+ contactType = LetterTileDrawable.TYPE_VOICEMAIL;
+ } else if (contactInfo.isBusiness) {
+ contactType = LetterTileDrawable.TYPE_BUSINESS;
+ } else if (call.getNumberPresentation() == TelecomManager.PRESENTATION_RESTRICTED) {
+ contactType = LetterTileDrawable.TYPE_GENERIC_AVATAR;
+ }
+ lettertile.setCanonicalDialerLetterTileDetails(
+ contactInfo.namePrimary == null ? contactInfo.number : contactInfo.namePrimary,
+ contactInfo.lookupKey,
+ LetterTileDrawable.SHAPE_CIRCLE,
+ contactType);
+ largeIcon = lettertile.getBitmap(width, height);
+ }
+
if (call.isSpam()) {
- Drawable drawable = mContext.getResources().getDrawable(R.drawable.blocked_contact);
+ Drawable drawable =
+ mContext.getResources().getDrawable(R.drawable.blocked_contact, mContext.getTheme());
largeIcon = DrawableConverter.drawableToBitmap(drawable);
}
return largeIcon;
@@ -552,8 +633,8 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
// display that regardless of the state of the other calls.
if (call.getState() == DialerCall.State.ONHOLD) {
return R.drawable.ic_phone_paused_white_24dp;
- } else if (call.getSessionModificationState()
- == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+ } else if (call.getVideoTech().getSessionModificationState()
+ == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
return R.drawable.ic_videocam;
}
return R.anim.on_going_call;
@@ -594,8 +675,8 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
resId = R.string.notification_on_hold;
} else if (DialerCall.State.isDialing(call.getState())) {
resId = R.string.notification_dialing;
- } else if (call.getSessionModificationState()
- == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+ } else if (call.getVideoTech().getSessionModificationState()
+ == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
resId = R.string.notification_requesting_video_call;
}
@@ -639,64 +720,98 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
}
private void addAnswerAction(Notification.Builder builder) {
- Log.d(this, "Will show \"answer\" action in the incoming call Notification");
+ LogUtil.d(
+ "StatusBarNotifier.addAnswerAction",
+ "will show \"answer\" action in the incoming call Notification");
PendingIntent answerVoicePendingIntent =
createNotificationPendingIntent(mContext, ACTION_ANSWER_VOICE_INCOMING_CALL);
+ // We put animation resources in "anim" folder instead of "drawable", which causes Android
+ // Studio to complain.
+ // TODO: Move "anim" resources to "drawable" as recommended in AnimationDrawable doc?
+ //noinspection ResourceType
builder.addAction(
- R.anim.on_going_call,
- getActionText(R.string.notification_action_answer, R.color.notification_action_accept),
- answerVoicePendingIntent);
+ new Notification.Action.Builder(
+ Icon.createWithResource(mContext, R.anim.on_going_call),
+ getActionText(
+ R.string.notification_action_answer, R.color.notification_action_accept),
+ answerVoicePendingIntent)
+ .build());
}
private void addDismissAction(Notification.Builder builder) {
- Log.d(this, "Will show \"decline\" action in the incoming call Notification");
+ LogUtil.d(
+ "StatusBarNotifier.addDismissAction",
+ "will show \"decline\" action in the incoming call Notification");
PendingIntent declinePendingIntent =
createNotificationPendingIntent(mContext, ACTION_DECLINE_INCOMING_CALL);
builder.addAction(
- R.drawable.ic_close_dk,
- getActionText(R.string.notification_action_dismiss, R.color.notification_action_dismiss),
- declinePendingIntent);
+ new Notification.Action.Builder(
+ Icon.createWithResource(mContext, R.drawable.ic_close_dk),
+ getActionText(
+ R.string.notification_action_dismiss, R.color.notification_action_dismiss),
+ declinePendingIntent)
+ .build());
}
private void addHangupAction(Notification.Builder builder) {
- Log.d(this, "Will show \"hang-up\" action in the ongoing active call Notification");
+ LogUtil.d(
+ "StatusBarNotifier.addHangupAction",
+ "will show \"hang-up\" action in the ongoing active call Notification");
PendingIntent hangupPendingIntent =
createNotificationPendingIntent(mContext, ACTION_HANG_UP_ONGOING_CALL);
builder.addAction(
- R.drawable.ic_call_end_white_24dp,
- getActionText(R.string.notification_action_end_call, R.color.notification_action_end_call),
- hangupPendingIntent);
+ new Notification.Action.Builder(
+ Icon.createWithResource(mContext, R.drawable.ic_call_end_white_24dp),
+ getActionText(
+ R.string.notification_action_end_call, R.color.notification_action_end_call),
+ hangupPendingIntent)
+ .build());
}
private void addVideoCallAction(Notification.Builder builder) {
- Log.i(this, "Will show \"video\" action in the incoming call Notification");
+ LogUtil.i(
+ "StatusBarNotifier.addVideoCallAction",
+ "will show \"video\" action in the incoming call Notification");
PendingIntent answerVideoPendingIntent =
createNotificationPendingIntent(mContext, ACTION_ANSWER_VIDEO_INCOMING_CALL);
builder.addAction(
- R.drawable.ic_videocam,
- getActionText(
- R.string.notification_action_answer_video, R.color.notification_action_answer_video),
- answerVideoPendingIntent);
+ new Notification.Action.Builder(
+ Icon.createWithResource(mContext, R.drawable.ic_videocam),
+ getActionText(
+ R.string.notification_action_answer_video,
+ R.color.notification_action_answer_video),
+ answerVideoPendingIntent)
+ .build());
}
private void addAcceptUpgradeRequestAction(Notification.Builder builder) {
- Log.i(this, "Will show \"accept upgrade\" action in the incoming call Notification");
+ LogUtil.i(
+ "StatusBarNotifier.addAcceptUpgradeRequestAction",
+ "will show \"accept upgrade\" action in the incoming call Notification");
PendingIntent acceptVideoPendingIntent =
createNotificationPendingIntent(mContext, ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST);
builder.addAction(
- R.drawable.ic_videocam,
- getActionText(R.string.notification_action_accept, R.color.notification_action_accept),
- acceptVideoPendingIntent);
+ new Notification.Action.Builder(
+ Icon.createWithResource(mContext, R.drawable.ic_videocam),
+ getActionText(
+ R.string.notification_action_accept, R.color.notification_action_accept),
+ acceptVideoPendingIntent)
+ .build());
}
private void addDismissUpgradeRequestAction(Notification.Builder builder) {
- Log.i(this, "Will show \"dismiss upgrade\" action in the incoming call Notification");
+ LogUtil.i(
+ "StatusBarNotifier.addDismissUpgradeRequestAction",
+ "will show \"dismiss upgrade\" action in the incoming call Notification");
PendingIntent declineVideoPendingIntent =
createNotificationPendingIntent(mContext, ACTION_DECLINE_VIDEO_UPGRADE_REQUEST);
builder.addAction(
- R.drawable.ic_videocam,
- getActionText(R.string.notification_action_dismiss, R.color.notification_action_dismiss),
- declineVideoPendingIntent);
+ new Notification.Action.Builder(
+ Icon.createWithResource(mContext, R.drawable.ic_videocam),
+ getActionText(
+ R.string.notification_action_dismiss, R.color.notification_action_dismiss),
+ declineVideoPendingIntent)
+ .build());
}
/** Adds fullscreen intent to the builder. */
@@ -707,7 +822,7 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
// to the status bar). Setting fullScreenIntent will cause
// the InCallScreen to be launched immediately *unless* the
// current foreground activity is marked as "immersive".
- Log.d(this, "- Setting fullScreenIntent: " + intent);
+ LogUtil.d("StatusBarNotifier.configureFullScreenIntent", "setting fullScreenIntent: " + intent);
builder.setFullScreenIntent(intent, true);
// Ugly hack alert:
@@ -740,7 +855,9 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
&& callList.getBackgroundCall() != null));
if (isCallWaiting) {
- Log.i(this, "updateInCallNotification: call-waiting! force relaunch...");
+ LogUtil.i(
+ "StatusBarNotifier.configureFullScreenIntent",
+ "updateInCallNotification: call-waiting! force relaunch...");
// Cancel the IN_CALL_NOTIFICATION immediately before
// (re)posting it; this seems to force the
// NotificationManager to launch the fullScreenIntent.
@@ -751,21 +868,15 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
private Notification.Builder getNotificationBuilder() {
final Notification.Builder builder = new Notification.Builder(mContext);
builder.setOngoing(true);
-
- // Make the notification prioritized over the other normal notifications.
- builder.setPriority(Notification.PRIORITY_HIGH);
+ builder.setOnlyAlertOnce(true);
return builder;
}
- private PendingIntent createLaunchPendingIntent(boolean isFullScreen, boolean isVideoCall) {
+ private PendingIntent createLaunchPendingIntent(boolean isFullScreen) {
Intent intent =
InCallActivity.getIntent(
- mContext,
- false /* showDialpad */,
- false /* newOutgoingCall */,
- isVideoCall,
- isFullScreen);
+ mContext, false /* showDialpad */, false /* newOutgoingCall */, isFullScreen);
int requestCode = PENDING_INTENT_REQUEST_CODE_NON_FULL_SCREEN;
if (isFullScreen) {
@@ -832,8 +943,9 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
* bar notification as required.
*/
@Override
- public void onDialerCallSessionModificationStateChange(@SessionModificationState int state) {
- if (state == DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST) {
+ public void onDialerCallSessionModificationStateChange() {
+ if (mDialerCall.getVideoTech().getSessionModificationState()
+ == VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST) {
cleanup();
updateNotification(CallList.getInstance());
}
diff --git a/java/com/android/incallui/VideoCallPresenter.java b/java/com/android/incallui/VideoCallPresenter.java
index 971b6957a..20dc987da 100644
--- a/java/com/android/incallui/VideoCallPresenter.java
+++ b/java/com/android/incallui/VideoCallPresenter.java
@@ -21,7 +21,6 @@ import android.content.Context;
import android.graphics.Point;
import android.os.Handler;
import android.support.annotation.Nullable;
-import android.telecom.Connection;
import android.telecom.InCallService.VideoCall;
import android.telecom.VideoProfile;
import android.telecom.VideoProfile.CameraCapabilities;
@@ -36,17 +35,18 @@ import com.android.incallui.InCallPresenter.InCallStateListener;
import com.android.incallui.InCallPresenter.IncomingCallListener;
import com.android.incallui.call.CallList;
import com.android.incallui.call.DialerCall;
-import com.android.incallui.call.DialerCall.SessionModificationState;
+import com.android.incallui.call.DialerCall.CameraDirection;
import com.android.incallui.call.DialerCall.State;
import com.android.incallui.call.InCallVideoCallCallbackNotifier;
import com.android.incallui.call.InCallVideoCallCallbackNotifier.SurfaceChangeListener;
-import com.android.incallui.call.InCallVideoCallCallbackNotifier.VideoEventListener;
import com.android.incallui.call.VideoUtils;
import com.android.incallui.util.AccessibilityUtil;
import com.android.incallui.video.protocol.VideoCallScreen;
import com.android.incallui.video.protocol.VideoCallScreenDelegate;
import com.android.incallui.videosurface.protocol.VideoSurfaceDelegate;
import com.android.incallui.videosurface.protocol.VideoSurfaceTexture;
+import com.android.incallui.videotech.VideoTech;
+import com.android.incallui.videotech.VideoTech.SessionModificationState;
import java.util.Objects;
/**
@@ -78,7 +78,6 @@ public class VideoCallPresenter
InCallStateListener,
InCallDetailsListener,
SurfaceChangeListener,
- VideoEventListener,
InCallPresenter.InCallEventListener,
VideoCallScreenDelegate {
@@ -90,32 +89,6 @@ public class VideoCallPresenter
/** The current context. */
private Context mContext;
- @Override
- public boolean shouldShowCameraPermissionDialog() {
- if (mPrimaryCall == null) {
- LogUtil.i("VideoCallPresenter.shouldShowCameraPermissionDialog", "null call");
- return false;
- }
- if (mPrimaryCall.didShowCameraPermission()) {
- LogUtil.i(
- "VideoCallPresenter.shouldShowCameraPermissionDialog", "already shown for this call");
- return false;
- }
- if (!ConfigProviderBindings.get(mContext)
- .getBoolean("camera_permission_dialog_allowed", true)) {
- LogUtil.i("VideoCallPresenter.shouldShowCameraPermissionDialog", "disabled by config");
- return false;
- }
- return !VideoUtils.hasCameraPermission(mContext) || !VideoUtils.isCameraAllowedByUser(mContext);
- }
-
- @Override
- public void onCameraPermissionDialogShown() {
- if (mPrimaryCall != null) {
- mPrimaryCall.setDidShowCameraPermission(true);
- }
- }
-
/** The call the video surfaces are currently related to */
private DialerCall mPrimaryCall;
/**
@@ -231,49 +204,49 @@ public class VideoCallPresenter
// this function should never be called with null call object, however if it happens we
// should handle it gracefully.
if (call == null) {
- cameraDir = DialerCall.VideoSettings.CAMERA_DIRECTION_UNKNOWN;
+ cameraDir = CameraDirection.CAMERA_DIRECTION_UNKNOWN;
LogUtil.e(
"VideoCallPresenter.updateCameraSelection",
"call is null. Setting camera direction to default value (CAMERA_DIRECTION_UNKNOWN)");
}
// Clear camera direction if this is not a video call.
- else if (VideoUtils.isAudioCall(call) && !isVideoUpgrade(call)) {
- cameraDir = DialerCall.VideoSettings.CAMERA_DIRECTION_UNKNOWN;
- call.getVideoSettings().setCameraDir(cameraDir);
+ else if (isAudioCall(call) && !isVideoUpgrade(call)) {
+ cameraDir = CameraDirection.CAMERA_DIRECTION_UNKNOWN;
+ call.setCameraDir(cameraDir);
}
// If this is a waiting video call, default to active call's camera,
// since we don't want to change the current camera for waiting call
// without user's permission.
- else if (VideoUtils.isVideoCall(activeCall) && VideoUtils.isIncomingVideoCall(call)) {
- cameraDir = activeCall.getVideoSettings().getCameraDir();
+ else if (isVideoCall(activeCall) && isIncomingVideoCall(call)) {
+ cameraDir = activeCall.getCameraDir();
}
// Infer the camera direction from the video state and store it,
// if this is an outgoing video call.
- else if (VideoUtils.isOutgoingVideoCall(call) && !isCameraDirectionSet(call)) {
+ else if (isOutgoingVideoCall(call) && !isCameraDirectionSet(call)) {
cameraDir = toCameraDirection(call.getVideoState());
- call.getVideoSettings().setCameraDir(cameraDir);
+ call.setCameraDir(cameraDir);
}
// Use the stored camera dir if this is an outgoing video call for which camera direction
// is set.
- else if (VideoUtils.isOutgoingVideoCall(call)) {
- cameraDir = call.getVideoSettings().getCameraDir();
+ else if (isOutgoingVideoCall(call)) {
+ cameraDir = call.getCameraDir();
}
// Infer the camera direction from the video state and store it,
// if this is an active video call and camera direction is not set.
- else if (VideoUtils.isActiveVideoCall(call) && !isCameraDirectionSet(call)) {
+ else if (isActiveVideoCall(call) && !isCameraDirectionSet(call)) {
cameraDir = toCameraDirection(call.getVideoState());
- call.getVideoSettings().setCameraDir(cameraDir);
+ call.setCameraDir(cameraDir);
}
// Use the stored camera dir if this is an active video call for which camera direction
// is set.
- else if (VideoUtils.isActiveVideoCall(call)) {
- cameraDir = call.getVideoSettings().getCameraDir();
+ else if (isActiveVideoCall(call)) {
+ cameraDir = call.getCameraDir();
}
// For all other cases infer the camera direction but don't store it in the call object.
@@ -289,20 +262,18 @@ public class VideoCallPresenter
final InCallCameraManager cameraManager =
InCallPresenter.getInstance().getInCallCameraManager();
cameraManager.setUseFrontFacingCamera(
- cameraDir == DialerCall.VideoSettings.CAMERA_DIRECTION_FRONT_FACING);
+ cameraDir == CameraDirection.CAMERA_DIRECTION_FRONT_FACING);
}
private static int toCameraDirection(int videoState) {
return VideoProfile.isTransmissionEnabled(videoState)
&& !VideoProfile.isBidirectional(videoState)
- ? DialerCall.VideoSettings.CAMERA_DIRECTION_BACK_FACING
- : DialerCall.VideoSettings.CAMERA_DIRECTION_FRONT_FACING;
+ ? CameraDirection.CAMERA_DIRECTION_BACK_FACING
+ : CameraDirection.CAMERA_DIRECTION_FRONT_FACING;
}
private static boolean isCameraDirectionSet(DialerCall call) {
- return VideoUtils.isVideoCall(call)
- && call.getVideoSettings().getCameraDir()
- != DialerCall.VideoSettings.CAMERA_DIRECTION_UNKNOWN;
+ return isVideoCall(call) && call.getCameraDir() != CameraDirection.CAMERA_DIRECTION_UNKNOWN;
}
private static String toSimpleString(DialerCall call) {
@@ -350,7 +321,6 @@ public class VideoCallPresenter
// Register for surface and video events from {@link InCallVideoCallListener}s.
InCallVideoCallCallbackNotifier.getInstance().addSurfaceChangeListener(this);
- InCallVideoCallCallbackNotifier.getInstance().addVideoEventListener(this);
mCurrentVideoState = VideoProfile.STATE_AUDIO_ONLY;
mCurrentCallState = DialerCall.State.INVALID;
@@ -379,7 +349,6 @@ public class VideoCallPresenter
InCallPresenter.getInstance().getLocalVideoSurfaceTexture().setDelegate(null);
InCallVideoCallCallbackNotifier.getInstance().removeSurfaceChangeListener(this);
- InCallVideoCallCallbackNotifier.getInstance().removeVideoEventListener(this);
// Ensure that the call's camera direction is updated (most likely to UNKNOWN). Normally this
// happens after any call state changes but we're unregistering from InCallPresenter above so
@@ -447,7 +416,7 @@ public class VideoCallPresenter
showVideoUi(
mPrimaryCall.getVideoState(),
mPrimaryCall.getState(),
- mPrimaryCall.getSessionModificationState(),
+ mPrimaryCall.getVideoTech().getSessionModificationState(),
mPrimaryCall.isRemotelyHeld());
InCallPresenter.getInstance().getInCallCameraManager().onCameraPermissionGranted();
}
@@ -521,7 +490,7 @@ public class VideoCallPresenter
// change the camera or UI unless the waiting VT call becomes active.
primary = callList.getActiveCall();
currentCall = callList.getIncomingCall();
- if (!VideoUtils.isActiveVideoCall(primary)) {
+ if (!isActiveVideoCall(primary)) {
primary = callList.getIncomingCall();
}
} else if (newState == InCallPresenter.InCallState.OUTGOING) {
@@ -564,10 +533,10 @@ public class VideoCallPresenter
cancelAutoFullScreen();
if (mPrimaryCall != null) {
updateFullscreenAndGreenScreenMode(
- mPrimaryCall.getState(), mPrimaryCall.getSessionModificationState());
+ mPrimaryCall.getState(), mPrimaryCall.getVideoTech().getSessionModificationState());
} else {
updateFullscreenAndGreenScreenMode(
- State.INVALID, DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ State.INVALID, VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
}
}
@@ -622,7 +591,7 @@ public class VideoCallPresenter
updateCameraSelection(call);
String newCameraId = cameraManager.getActiveCameraId();
- if (!Objects.equals(prevCameraId, newCameraId) && VideoUtils.isActiveVideoCall(call)) {
+ if (!Objects.equals(prevCameraId, newCameraId) && isActiveVideoCall(call)) {
enableCamera(call.getVideoCall(), true);
}
}
@@ -631,7 +600,7 @@ public class VideoCallPresenter
showVideoUi(
call.getVideoState(),
call.getState(),
- call.getSessionModificationState(),
+ call.getVideoTech().getSessionModificationState(),
call.isRemotelyHeld());
}
@@ -711,12 +680,13 @@ public class VideoCallPresenter
checkForVideoStateChange(call);
checkForCallStateChange(call);
checkForOrientationAllowedChange(call);
- updateFullscreenAndGreenScreenMode(call.getState(), call.getSessionModificationState());
+ updateFullscreenAndGreenScreenMode(
+ call.getState(), call.getVideoTech().getSessionModificationState());
}
private void checkForOrientationAllowedChange(@Nullable DialerCall call) {
InCallPresenter.getInstance()
- .setInCallAllowsOrientationChange(VideoUtils.isVideoCall(call) || isVideoUpgrade(call));
+ .setInCallAllowsOrientationChange(isVideoCall(call) || isVideoUpgrade(call));
}
private void updateFullscreenAndGreenScreenMode(
@@ -775,7 +745,8 @@ public class VideoCallPresenter
private boolean isCameraRequired() {
return mPrimaryCall != null
&& isCameraRequired(
- mPrimaryCall.getVideoState(), mPrimaryCall.getSessionModificationState());
+ mPrimaryCall.getVideoState(),
+ mPrimaryCall.getVideoTech().getSessionModificationState());
}
/**
@@ -799,7 +770,10 @@ public class VideoCallPresenter
}
showVideoUi(
- newVideoState, call.getState(), call.getSessionModificationState(), call.isRemotelyHeld());
+ newVideoState,
+ call.getState(),
+ call.getVideoTech().getSessionModificationState(),
+ call.isRemotelyHeld());
// Communicate the current camera to telephony and make a request for the camera
// capabilities.
@@ -814,7 +788,9 @@ public class VideoCallPresenter
Assert.checkState(
mDeviceOrientation != InCallOrientationEventListener.SCREEN_ORIENTATION_UNKNOWN);
videoCall.setDeviceOrientation(mDeviceOrientation);
- enableCamera(videoCall, isCameraRequired(newVideoState, call.getSessionModificationState()));
+ enableCamera(
+ videoCall,
+ isCameraRequired(newVideoState, call.getVideoTech().getSessionModificationState()));
}
int previousVideoState = mCurrentVideoState;
mCurrentVideoState = newVideoState;
@@ -822,7 +798,7 @@ public class VideoCallPresenter
// adjustVideoMode may be called if we are already in a 1-way video state. In this case
// we do not want to trigger auto-fullscreen mode.
- if (!VideoUtils.isVideoCall(previousVideoState) && VideoUtils.isVideoCall(newVideoState)) {
+ if (!isVideoCall(previousVideoState) && isVideoCall(newVideoState)) {
maybeAutoEnterFullscreen(call);
}
}
@@ -832,7 +808,7 @@ public class VideoCallPresenter
return false;
}
- if (VideoUtils.isVideoCall(call)) {
+ if (isVideoCall(call)) {
return true;
}
@@ -877,7 +853,7 @@ public class VideoCallPresenter
showVideoUi(
VideoProfile.STATE_AUDIO_ONLY,
DialerCall.State.ACTIVE,
- DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST,
+ VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST,
false /* isRemotelyHeld */);
enableCamera(mVideoCall, false);
InCallPresenter.getInstance().setFullScreen(false);
@@ -918,20 +894,6 @@ public class VideoCallPresenter
}
/**
- * Handles peer video pause state changes.
- *
- * @param call The call which paused or un-pausedvideo transmission.
- * @param paused {@code True} when the video transmission is paused, {@code false} when video
- * transmission resumes.
- */
- @Override
- public void onPeerPauseStateChanged(DialerCall call, boolean paused) {
- if (!call.equals(mPrimaryCall)) {
- return;
- }
- }
-
- /**
* Handles peer video dimension changes.
*
* @param call The call which experienced a peer video dimension change.
@@ -959,17 +921,6 @@ public class VideoCallPresenter
}
/**
- * Handles any video quality changes in the call.
- *
- * @param call The call which experienced a video quality change.
- * @param videoQuality The new video call quality.
- */
- @Override
- public void onVideoQualityChanged(DialerCall call, int videoQuality) {
- // No-op
- }
-
- /**
* Handles a change to the dimensions of the local camera. Receiving the camera capabilities
* triggers the creation of the video
*
@@ -1024,42 +975,6 @@ public class VideoCallPresenter
}
/**
- * Called when call session event is raised.
- *
- * @param event The call session event.
- */
- @Override
- public void onCallSessionEvent(int event) {
- switch (event) {
- case Connection.VideoProvider.SESSION_EVENT_RX_PAUSE:
- LogUtil.v("VideoCallPresenter.onCallSessionEvent", "rx_pause");
- break;
- case Connection.VideoProvider.SESSION_EVENT_RX_RESUME:
- LogUtil.v("VideoCallPresenter.onCallSessionEvent", "rx_resume");
- break;
- case Connection.VideoProvider.SESSION_EVENT_CAMERA_FAILURE:
- LogUtil.v("VideoCallPresenter.onCallSessionEvent", "camera_failure");
- break;
- case Connection.VideoProvider.SESSION_EVENT_CAMERA_READY:
- LogUtil.v("VideoCallPresenter.onCallSessionEvent", "camera_ready");
- break;
- default:
- LogUtil.v("VideoCallPresenter.onCallSessionEvent", "unknown event = : " + event);
- break;
- }
- }
-
- /**
- * Handles a change to the call data usage
- *
- * @param dataUsage call data usage value
- */
- @Override
- public void onCallDataUsageChange(long dataUsage) {
- LogUtil.v("VideoCallPresenter.onCallDataUsageChange", "dataUsage=" + dataUsage);
- }
-
- /**
* Handles changes to the device orientation.
*
* @param orientation The screen orientation of the device (one of: {@link
@@ -1106,7 +1021,7 @@ public class VideoCallPresenter
return;
}
- if (!VideoUtils.isVideoCall(call) || call.getState() == DialerCall.State.INCOMING) {
+ if (!isVideoCall(call) || call.getState() == DialerCall.State.INCOMING) {
LogUtil.i("VideoCallPresenter.maybeExitFullscreen", "exiting fullscreen");
InCallPresenter.getInstance().setFullScreen(false);
}
@@ -1126,7 +1041,7 @@ public class VideoCallPresenter
if (call == null
|| call.getState() != DialerCall.State.ACTIVE
- || !VideoUtils.isBidirectionalVideoCall(call)
+ || !isBidirectionalVideoCall(call)
|| InCallPresenter.getInstance().isFullscreen()
|| (mContext != null && AccessibilityUtil.isTouchExplorationEnabled(mContext))) {
// Ensure any previously scheduled attempt to enter fullscreen is cancelled.
@@ -1156,6 +1071,32 @@ public class VideoCallPresenter
mHandler.removeCallbacks(mAutoFullscreenRunnable);
}
+ @Override
+ public boolean shouldShowCameraPermissionDialog() {
+ if (mPrimaryCall == null) {
+ LogUtil.i("VideoCallPresenter.shouldShowCameraPermissionDialog", "null call");
+ return false;
+ }
+ if (mPrimaryCall.didShowCameraPermission()) {
+ LogUtil.i(
+ "VideoCallPresenter.shouldShowCameraPermissionDialog", "already shown for this call");
+ return false;
+ }
+ if (!ConfigProviderBindings.get(mContext)
+ .getBoolean("camera_permission_dialog_allowed", true)) {
+ LogUtil.i("VideoCallPresenter.shouldShowCameraPermissionDialog", "disabled by config");
+ return false;
+ }
+ return !VideoUtils.hasCameraPermission(mContext) || !VideoUtils.isCameraAllowedByUser(mContext);
+ }
+
+ @Override
+ public void onCameraPermissionDialogShown() {
+ if (mPrimaryCall != null) {
+ mPrimaryCall.setDidShowCameraPermission(true);
+ }
+ }
+
private void updateRemoteVideoSurfaceDimensions() {
Activity activity = mVideoCallScreen.getVideoCallScreenFragment().getActivity();
if (activity != null) {
@@ -1166,8 +1107,8 @@ public class VideoCallPresenter
}
private static boolean isVideoUpgrade(DialerCall call) {
- return VideoUtils.hasSentVideoUpgradeRequest(call)
- || VideoUtils.hasReceivedVideoUpgradeRequest(call);
+ return call != null
+ && (call.hasSentVideoUpgradeRequest() || call.hasReceivedVideoUpgradeRequest());
}
private static boolean isVideoUpgrade(@SessionModificationState int state) {
@@ -1286,4 +1227,48 @@ public class VideoCallPresenter
/** The surface has been set on the {@link VideoCall}. */
private static final int SURFACE_SET = 3;
}
+
+ private static boolean isBidirectionalVideoCall(DialerCall call) {
+ return CompatUtils.isVideoCompatible() && VideoProfile.isBidirectional(call.getVideoState());
+ }
+
+ private static boolean isIncomingVideoCall(DialerCall call) {
+ if (!isVideoCall(call)) {
+ return false;
+ }
+ final int state = call.getState();
+ return (state == DialerCall.State.INCOMING) || (state == DialerCall.State.CALL_WAITING);
+ }
+
+ private static boolean isActiveVideoCall(DialerCall call) {
+ return isVideoCall(call) && call.getState() == DialerCall.State.ACTIVE;
+ }
+
+ private static boolean isOutgoingVideoCall(DialerCall call) {
+ if (!isVideoCall(call)) {
+ return false;
+ }
+ final int state = call.getState();
+ return DialerCall.State.isDialing(state)
+ || state == DialerCall.State.CONNECTING
+ || state == DialerCall.State.SELECT_PHONE_ACCOUNT;
+ }
+
+ private static boolean isAudioCall(DialerCall call) {
+ if (!CompatUtils.isVideoCompatible()) {
+ return true;
+ }
+
+ return call != null && VideoProfile.isAudioOnly(call.getVideoState());
+ }
+
+ private static boolean isVideoCall(@Nullable DialerCall call) {
+ return call != null && call.isVideoCall();
+ }
+
+ private static boolean isVideoCall(int videoState) {
+ return CompatUtils.isVideoCompatible()
+ && (VideoProfile.isTransmissionEnabled(videoState)
+ || VideoProfile.isReceptionEnabled(videoState));
+ }
}
diff --git a/java/com/android/incallui/VideoPauseController.java b/java/com/android/incallui/VideoPauseController.java
index 2b4357704..2595e2f8b 100644
--- a/java/com/android/incallui/VideoPauseController.java
+++ b/java/com/android/incallui/VideoPauseController.java
@@ -17,14 +17,14 @@
package com.android.incallui;
import android.support.annotation.NonNull;
-import android.telecom.VideoProfile;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
import com.android.incallui.InCallPresenter.InCallState;
import com.android.incallui.InCallPresenter.InCallStateListener;
import com.android.incallui.InCallPresenter.IncomingCallListener;
import com.android.incallui.call.CallList;
import com.android.incallui.call.DialerCall;
import com.android.incallui.call.DialerCall.State;
-import com.android.incallui.call.VideoUtils;
import java.util.Objects;
/**
@@ -32,12 +32,21 @@ import java.util.Objects;
* to the background and subsequently brought back to the foreground.
*/
class VideoPauseController implements InCallStateListener, IncomingCallListener {
-
- private static final String TAG = "VideoPauseController";
private static VideoPauseController sVideoPauseController;
private InCallPresenter mInCallPresenter;
- /** The current call context, if applicable. */
- private CallContext mPrimaryCallContext = null;
+
+ /** The current call, if applicable. */
+ private DialerCall mPrimaryCall = null;
+
+ /**
+ * The cached state of primary call, updated after onStateChange has processed.
+ *
+ * <p>These values are stored to detect specific changes in state between onStateChange calls.
+ */
+ private int mPrevCallState = State.INVALID;
+
+ private boolean mWasVideoCall = false;
+
/**
* Tracks whether the application is in the background. {@code True} if the application is in the
* background, {@code false} otherwise.
@@ -57,51 +66,9 @@ class VideoPauseController implements InCallStateListener, IncomingCallListener
return sVideoPauseController;
}
- /**
- * Determines if a given call is the same one stored in a {@link CallContext}.
- *
- * @param call The call.
- * @param callContext The call context.
- * @return {@code true} if the {@link DialerCall} is the same as the one referenced in the {@link
- * CallContext}.
- */
- private static boolean areSame(DialerCall call, CallContext callContext) {
- if (call == null && callContext == null) {
- return true;
- } else if (call == null || callContext == null) {
- return false;
- }
- return call.equals(callContext.getCall());
- }
-
- /**
- * Determines if a video call can be paused. Only a video call which is active can be paused.
- *
- * @param callContext The call context to check.
- * @return {@code true} if the call is an active video call.
- */
- private static boolean canVideoPause(CallContext callContext) {
- return isVideoCall(callContext) && callContext.getState() == DialerCall.State.ACTIVE;
- }
-
- /**
- * Determines if a call referenced by a {@link CallContext} is a video call.
- *
- * @param callContext The call context.
- * @return {@code true} if the call is a video call, {@code false} otherwise.
- */
- private static boolean isVideoCall(CallContext callContext) {
- return callContext != null && VideoUtils.isVideoCall(callContext.getVideoState());
- }
-
- /**
- * Determines if call is in incoming/waiting state.
- *
- * @param call The call context.
- * @return {@code true} if the call is in incoming or waiting state, {@code false} otherwise.
- */
- private static boolean isIncomingCall(CallContext call) {
- return call != null && isIncomingCall(call.getCall());
+ private boolean wasIncomingCall() {
+ return (mPrevCallState == DialerCall.State.CALL_WAITING
+ || mPrevCallState == DialerCall.State.INCOMING);
}
/**
@@ -119,11 +86,10 @@ class VideoPauseController implements InCallStateListener, IncomingCallListener
/**
* Determines if a call is dialing.
*
- * @param call The call context.
* @return {@code true} if the call is dialing, {@code false} otherwise.
*/
- private static boolean isDialing(CallContext call) {
- return call != null && DialerCall.State.isDialing(call.getState());
+ private boolean wasDialing() {
+ return DialerCall.State.isDialing(mPrevCallState);
}
/**
@@ -133,8 +99,8 @@ class VideoPauseController implements InCallStateListener, IncomingCallListener
* @param inCallPresenter The {@link com.android.incallui.InCallPresenter}.
*/
public void setUp(@NonNull InCallPresenter inCallPresenter) {
- log("setUp");
- mInCallPresenter = Objects.requireNonNull(inCallPresenter);
+ LogUtil.enterBlock("VideoPauseController.setUp");
+ mInCallPresenter = Assert.isNotNull(inCallPresenter);
mInCallPresenter.addListener(this);
mInCallPresenter.addIncomingCallListener(this);
}
@@ -144,7 +110,7 @@ class VideoPauseController implements InCallStateListener, IncomingCallListener
* state. Called from {@link com.android.incallui.InCallPresenter}.
*/
public void tearDown() {
- log("tearDown...");
+ LogUtil.enterBlock("VideoPauseController.tearDown");
mInCallPresenter.removeListener(this);
mInCallPresenter.removeIncomingCallListener(this);
clear();
@@ -153,7 +119,9 @@ class VideoPauseController implements InCallStateListener, IncomingCallListener
/** Clears the internal state for the {@link VideoPauseController}. */
private void clear() {
mInCallPresenter = null;
- mPrimaryCallContext = null;
+ mPrimaryCall = null;
+ mPrevCallState = State.INVALID;
+ mWasVideoCall = false;
mIsInBackground = false;
}
@@ -167,8 +135,6 @@ class VideoPauseController implements InCallStateListener, IncomingCallListener
*/
@Override
public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
- log("onStateChange, OldState=" + oldState + " NewState=" + newState);
-
DialerCall call;
if (newState == InCallState.INCOMING) {
call = callList.getIncomingCall();
@@ -182,22 +148,26 @@ class VideoPauseController implements InCallStateListener, IncomingCallListener
call = callList.getActiveCall();
}
- boolean hasPrimaryCallChanged = !areSame(call, mPrimaryCallContext);
- boolean canVideoPause = VideoUtils.canVideoPause(call);
- log("onStateChange, hasPrimaryCallChanged=" + hasPrimaryCallChanged);
- log("onStateChange, canVideoPause=" + canVideoPause);
- log("onStateChange, IsInBackground=" + mIsInBackground);
+ boolean hasPrimaryCallChanged = !Objects.equals(call, mPrimaryCall);
+ boolean canVideoPause = videoCanPause(call);
+
+ LogUtil.i(
+ "VideoPauseController.onStateChange",
+ "hasPrimaryCallChanged: %b, videoCanPause: %b, isInBackground: %b",
+ hasPrimaryCallChanged,
+ canVideoPause,
+ mIsInBackground);
if (hasPrimaryCallChanged) {
onPrimaryCallChanged(call);
return;
}
- if (isDialing(mPrimaryCallContext) && canVideoPause && mIsInBackground) {
+ if (wasDialing() && canVideoPause && mIsInBackground) {
// Bring UI to foreground if outgoing request becomes active while UI is in
// background.
bringToForeground();
- } else if (!isVideoCall(mPrimaryCallContext) && canVideoPause && mIsInBackground) {
+ } else if (!mWasVideoCall && canVideoPause && mIsInBackground) {
// Bring UI to foreground if VoLTE call becomes active while UI is in
// background.
bringToForeground();
@@ -216,27 +186,26 @@ class VideoPauseController implements InCallStateListener, IncomingCallListener
* @param call The new primary call.
*/
private void onPrimaryCallChanged(DialerCall call) {
- log("onPrimaryCallChanged: New call = " + call);
- log("onPrimaryCallChanged: Old call = " + mPrimaryCallContext);
- log("onPrimaryCallChanged, IsInBackground=" + mIsInBackground);
-
- if (areSame(call, mPrimaryCallContext)) {
+ LogUtil.i(
+ "VideoPauseController.onPrimaryCallChanged",
+ "new call: %s, old call: %s, mIsInBackground: %b",
+ call,
+ mPrimaryCall,
+ mIsInBackground);
+
+ if (Objects.equals(call, mPrimaryCall)) {
throw new IllegalStateException();
}
- final boolean canVideoPause = VideoUtils.canVideoPause(call);
+ final boolean canVideoPause = videoCanPause(call);
- if ((isIncomingCall(mPrimaryCallContext)
- || isDialing(mPrimaryCallContext)
- || (call != null && VideoProfile.isPaused(call.getVideoState())))
- && canVideoPause
- && !mIsInBackground) {
+ if ((wasIncomingCall() || wasDialing()) && canVideoPause && !mIsInBackground) {
// Send resume request for the active call, if user rejects incoming call, ends dialing
// call, or the call was previously in a paused state and UI is in the foreground.
sendRequest(call, true);
- } else if (isIncomingCall(call) && canVideoPause(mPrimaryCallContext)) {
+ } else if (isIncomingCall(call) && videoCanPause(mPrimaryCall)) {
// Send pause request if there is an active video call, and we just received a new
// incoming call.
- sendRequest(mPrimaryCallContext.getCall(), false);
+ sendRequest(mPrimaryCall, false);
}
updatePrimaryCallContext(call);
@@ -251,9 +220,14 @@ class VideoPauseController implements InCallStateListener, IncomingCallListener
*/
@Override
public void onIncomingCall(InCallState oldState, InCallState newState, DialerCall call) {
- log("onIncomingCall, OldState=" + oldState + " NewState=" + newState + " DialerCall=" + call);
-
- if (areSame(call, mPrimaryCallContext)) {
+ LogUtil.i(
+ "VideoPauseController.onIncomingCall",
+ "oldState: %s, newState: %s, call: %s",
+ oldState,
+ newState,
+ call);
+
+ if (Objects.equals(call, mPrimaryCall)) {
return;
}
@@ -267,11 +241,13 @@ class VideoPauseController implements InCallStateListener, IncomingCallListener
*/
private void updatePrimaryCallContext(DialerCall call) {
if (call == null) {
- mPrimaryCallContext = null;
- } else if (mPrimaryCallContext != null) {
- mPrimaryCallContext.update(call);
+ mPrimaryCall = null;
+ mPrevCallState = State.INVALID;
+ mWasVideoCall = false;
} else {
- mPrimaryCallContext = new CallContext(call);
+ mPrimaryCall = call;
+ mPrevCallState = call.getState();
+ mWasVideoCall = call.isVideoCall();
}
}
@@ -301,13 +277,9 @@ class VideoPauseController implements InCallStateListener, IncomingCallListener
* video provider if we are in a call.
*/
private void onResume(boolean isInCall) {
- log("onResume");
-
mIsInBackground = false;
- if (canVideoPause(mPrimaryCallContext) && isInCall) {
- sendRequest(mPrimaryCallContext.getCall(), true);
- } else {
- log("onResume. Ignoring...");
+ if (isInCall) {
+ sendRequest(mPrimaryCall, true);
}
}
@@ -319,22 +291,20 @@ class VideoPauseController implements InCallStateListener, IncomingCallListener
* video provider if we are in a call.
*/
private void onPause(boolean isInCall) {
- log("onPause");
-
mIsInBackground = true;
- if (canVideoPause(mPrimaryCallContext) && isInCall) {
- sendRequest(mPrimaryCallContext.getCall(), false);
- } else {
- log("onPause, Ignoring...");
+ if (isInCall) {
+ sendRequest(mPrimaryCall, false);
}
}
private void bringToForeground() {
+ LogUtil.enterBlock("VideoPauseController.bringToForeground");
if (mInCallPresenter != null) {
- log("Bringing UI to foreground");
mInCallPresenter.bringToForeground(false);
} else {
- loge("InCallPresenter is null. Cannot bring UI to foreground");
+ LogUtil.e(
+ "VideoPauseController.bringToForeground",
+ "InCallPresenter is null. Cannot bring UI to foreground");
}
}
@@ -345,72 +315,18 @@ class VideoPauseController implements InCallStateListener, IncomingCallListener
* @param resume If true resume request will be sent, otherwise pause request.
*/
private void sendRequest(DialerCall call, boolean resume) {
- // Check if this call supports pause/un-pause.
- if (!call.can(android.telecom.Call.Details.CAPABILITY_CAN_PAUSE_VIDEO)) {
+ if (call == null) {
return;
}
if (resume) {
- log("sending resume request, call=" + call);
- call.getVideoCall().sendSessionModifyRequest(VideoUtils.makeVideoUnPauseProfile(call));
+ call.getVideoTech().unpause();
} else {
- log("sending pause request, call=" + call);
- call.getVideoCall().sendSessionModifyRequest(VideoUtils.makeVideoPauseProfile(call));
+ call.getVideoTech().pause();
}
}
- /**
- * Logs a debug message.
- *
- * @param msg The message.
- */
- private void log(String msg) {
- Log.d(this, TAG + msg);
- }
-
- /**
- * Logs an error message.
- *
- * @param msg The message.
- */
- private void loge(String msg) {
- Log.e(this, TAG + msg);
- }
-
- /** Keeps track of the current active/foreground call. */
- private static class CallContext {
-
- private int mState = State.INVALID;
- private int mVideoState;
- private DialerCall mCall;
-
- public CallContext(@NonNull DialerCall call) {
- Objects.requireNonNull(call);
- update(call);
- }
-
- public void update(@NonNull DialerCall call) {
- mCall = Objects.requireNonNull(call);
- mState = call.getState();
- mVideoState = call.getVideoState();
- }
-
- public int getState() {
- return mState;
- }
-
- public int getVideoState() {
- return mVideoState;
- }
-
- @Override
- public String toString() {
- return String.format(
- "CallContext {CallId=%s, State=%s, VideoState=%d}", mCall.getId(), mState, mVideoState);
- }
-
- public DialerCall getCall() {
- return mCall;
- }
+ private static boolean videoCanPause(DialerCall call) {
+ return call != null && call.isVideoCall() && call.getState() == DialerCall.State.ACTIVE;
}
}
diff --git a/java/com/android/incallui/answer/bindings/AnswerBindings.java b/java/com/android/incallui/answer/bindings/AnswerBindings.java
index f7a7a0a95..442e207a0 100644
--- a/java/com/android/incallui/answer/bindings/AnswerBindings.java
+++ b/java/com/android/incallui/answer/bindings/AnswerBindings.java
@@ -23,7 +23,7 @@ import com.android.incallui.answer.protocol.AnswerScreen;
public class AnswerBindings {
public static AnswerScreen createAnswerScreen(
- String callId, int videoState, boolean isVideoUpgradeRequest) {
- return AnswerFragment.newInstance(callId, videoState, isVideoUpgradeRequest);
+ String callId, boolean isVideoCall, boolean isVideoUpgradeRequest) {
+ return AnswerFragment.newInstance(callId, isVideoCall, isVideoUpgradeRequest);
}
}
diff --git a/java/com/android/incallui/answer/impl/AnswerFragment.java b/java/com/android/incallui/answer/impl/AnswerFragment.java
index 98439ee7f..6874daea3 100644
--- a/java/com/android/incallui/answer/impl/AnswerFragment.java
+++ b/java/com/android/incallui/answer/impl/AnswerFragment.java
@@ -37,7 +37,6 @@ import android.support.annotation.StringRes;
import android.support.annotation.VisibleForTesting;
import android.support.transition.TransitionManager;
import android.support.v4.app.Fragment;
-import android.telecom.VideoProfile;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
@@ -79,10 +78,11 @@ import com.android.incallui.incall.protocol.InCallScreenDelegateFactory;
import com.android.incallui.incall.protocol.PrimaryCallState;
import com.android.incallui.incall.protocol.PrimaryInfo;
import com.android.incallui.incall.protocol.SecondaryInfo;
-import com.android.incallui.maps.StaticMapBinding;
+import com.android.incallui.maps.MapsComponent;
import com.android.incallui.sessiondata.AvatarPresenter;
import com.android.incallui.sessiondata.MultimediaFragment;
import com.android.incallui.util.AccessibilityUtil;
+import com.android.incallui.video.protocol.VideoCallScreen;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@@ -101,7 +101,7 @@ public class AnswerFragment extends Fragment
static final String ARG_CALL_ID = "call_id";
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- static final String ARG_VIDEO_STATE = "video_state";
+ static final String ARG_IS_VIDEO_CALL = "is_video_call";
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
static final String ARG_IS_VIDEO_UPGRADE_REQUEST = "is_video_upgrade_request";
@@ -143,7 +143,7 @@ public class AnswerFragment extends Fragment
private CreateCustomSmsDialogFragment createCustomSmsDialogFragment;
private SecondaryBehavior secondaryBehavior = SecondaryBehavior.REJECT_WITH_SMS;
private ContactGridManager contactGridManager;
- private AnswerVideoCallScreen answerVideoCallScreen;
+ private VideoCallScreen answerVideoCallScreen;
private Handler handler = new Handler(Looper.getMainLooper());
private enum SecondaryBehavior {
@@ -288,10 +288,10 @@ public class AnswerFragment extends Fragment
}
public static AnswerFragment newInstance(
- String callId, int videoState, boolean isVideoUpgradeRequest) {
+ String callId, boolean isVideoCall, boolean isVideoUpgradeRequest) {
Bundle bundle = new Bundle();
bundle.putString(ARG_CALL_ID, Assert.isNotNull(callId));
- bundle.putInt(ARG_VIDEO_STATE, videoState);
+ bundle.putBoolean(ARG_IS_VIDEO_CALL, isVideoCall);
bundle.putBoolean(ARG_IS_VIDEO_UPGRADE_REQUEST, isVideoUpgradeRequest);
AnswerFragment instance = new AnswerFragment();
@@ -306,18 +306,13 @@ public class AnswerFragment extends Fragment
}
@Override
- public int getVideoState() {
- return getArguments().getInt(ARG_VIDEO_STATE);
- }
-
- @Override
public boolean isVideoUpgradeRequest() {
return getArguments().getBoolean(ARG_IS_VIDEO_UPGRADE_REQUEST);
}
@Override
public void setTextResponses(List<String> textResponses) {
- if (isVideoCall()) {
+ if (isVideoCall() || isVideoUpgradeRequest()) {
LogUtil.i("AnswerFragment.setTextResponses", "no-op for video calls");
} else if (textResponses == null) {
LogUtil.i("AnswerFragment.setTextResponses", "no text responses, hiding secondary button");
@@ -336,7 +331,9 @@ public class AnswerFragment extends Fragment
private void initSecondaryButton() {
secondaryBehavior =
- isVideoCall() ? SecondaryBehavior.ANSWER_VIDEO_AS_AUDIO : SecondaryBehavior.REJECT_WITH_SMS;
+ isVideoCall() || isVideoUpgradeRequest()
+ ? SecondaryBehavior.ANSWER_VIDEO_AS_AUDIO
+ : SecondaryBehavior.REJECT_WITH_SMS;
secondaryBehavior.applyToView(secondaryButton);
secondaryButton.setOnClickListener(
@@ -351,12 +348,9 @@ public class AnswerFragment extends Fragment
secondaryButton.setAccessibilityDelegate(accessibilityDelegate);
if (isVideoCall()) {
- //noinspection WrongConstant
- if (!isVideoUpgradeRequest() && VideoProfile.isTransmissionEnabled(getVideoState())) {
- secondaryButton.setVisibility(View.VISIBLE);
- } else {
- secondaryButton.setVisibility(View.INVISIBLE);
- }
+ secondaryButton.setVisibility(View.VISIBLE);
+ } else {
+ secondaryButton.setVisibility(View.INVISIBLE);
}
}
@@ -448,11 +442,11 @@ public class AnswerFragment extends Fragment
MultimediaData multimediaData = getSessionData();
if (multimediaData != null
- && (!TextUtils.isEmpty(multimediaData.getSubject())
+ && (!TextUtils.isEmpty(multimediaData.getText())
|| (multimediaData.getImageUri() != null)
|| (multimediaData.getLocation() != null && canShowMap()))) {
// Need message fragment
- String subject = multimediaData.getSubject();
+ String subject = multimediaData.getText();
Uri imageUri = multimediaData.getImageUri();
Location location = multimediaData.getLocation();
if (!(current instanceof MultimediaFragment)
@@ -487,11 +481,11 @@ public class AnswerFragment extends Fragment
}
private boolean shouldShowAvatar() {
- return !isVideoCall();
+ return !isVideoCall() && !isVideoUpgradeRequest();
}
private boolean canShowMap() {
- return StaticMapBinding.get(getActivity().getApplication()) != null;
+ return MapsComponent.get(getContext()).getMaps().isAvailable();
}
@Override
@@ -564,7 +558,7 @@ public class AnswerFragment extends Fragment
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Bundle arguments = getArguments();
Assert.checkState(arguments.containsKey(ARG_CALL_ID));
- Assert.checkState(arguments.containsKey(ARG_VIDEO_STATE));
+ Assert.checkState(arguments.containsKey(ARG_IS_VIDEO_CALL));
Assert.checkState(arguments.containsKey(ARG_IS_VIDEO_UPGRADE_REQUEST));
buttonAcceptClicked = false;
@@ -596,7 +590,6 @@ public class AnswerFragment extends Fragment
});
updateImportanceBadgeVisibility();
- boolean isVideoCall = isVideoCall();
contactGridManager = new ContactGridManager(view, null, 0, false /* showAnonymousAvatar */);
Fragment answerMethod =
@@ -625,9 +618,9 @@ public class AnswerFragment extends Fragment
flags |= STATUS_BAR_DISABLE_BACK | STATUS_BAR_DISABLE_HOME | STATUS_BAR_DISABLE_RECENT;
}
view.setSystemUiVisibility(flags);
- if (isVideoCall) {
+ if (isVideoCall() || isVideoUpgradeRequest()) {
if (VideoUtils.hasCameraPermissionAndAllowedByUser(getContext())) {
- answerVideoCallScreen = new AnswerVideoCallScreen(this, view);
+ answerVideoCallScreen = new AnswerVideoCallScreen(getCallId(), this, view);
} else {
view.findViewById(R.id.videocall_video_off).setVisibility(View.VISIBLE);
}
@@ -649,7 +642,7 @@ public class AnswerFragment extends Fragment
updateUI();
if (savedInstanceState == null || !savedInstanceState.getBoolean(STATE_HAS_ANIMATED_ENTRY)) {
- ViewUtil.doOnPreDraw(view, false, this::animateEntry);
+ ViewUtil.doOnGlobalLayout(view, this::animateEntry);
}
}
@@ -667,7 +660,7 @@ public class AnswerFragment extends Fragment
updateUI();
if (answerVideoCallScreen != null) {
- answerVideoCallScreen.onStart();
+ answerVideoCallScreen.onVideoScreenStart();
}
}
@@ -678,7 +671,7 @@ public class AnswerFragment extends Fragment
handler.removeCallbacks(swipeHintRestoreTimer);
if (answerVideoCallScreen != null) {
- answerVideoCallScreen.onStop();
+ answerVideoCallScreen.onVideoScreenStop();
}
}
@@ -722,7 +715,7 @@ public class AnswerFragment extends Fragment
@Override
public boolean isVideoCall() {
- return VideoUtils.isVideoCall(getVideoState());
+ return getArguments().getBoolean(ARG_IS_VIDEO_CALL);
}
@Override
@@ -775,14 +768,12 @@ public class AnswerFragment extends Fragment
Animator dataContainer = createTranslation(rootView.findViewById(R.id.incall_data_container));
AnimatorSet animatorSet = new AnimatorSet();
- animatorSet
- .play(alpha)
- .with(topRow)
- .with(contactName)
- .with(bottomRow)
- .with(important)
- .with(dataContainer);
- animatorSet.setDuration(getResources().getInteger(android.R.integer.config_longAnimTime));
+ AnimatorSet.Builder builder = animatorSet.play(alpha);
+ builder.with(topRow).with(contactName).with(bottomRow).with(important).with(dataContainer);
+ if (isShowingLocationUi()) {
+ builder.with(createTranslation(rootView.findViewById(R.id.incall_location_holder)));
+ }
+ animatorSet.setDuration(getResources().getInteger(R.integer.answer_animate_entry_millis));
animatorSet.addListener(
new AnimatorListenerAdapter() {
@Override
@@ -803,14 +794,7 @@ public class AnswerFragment extends Fragment
private void acceptCallByUser(boolean answerVideoAsAudio) {
LogUtil.i("AnswerFragment.acceptCallByUser", answerVideoAsAudio ? " answerVideoAsAudio" : "");
if (!buttonAcceptClicked) {
- int desiredVideoState = getVideoState();
- if (answerVideoAsAudio) {
- desiredVideoState = VideoProfile.STATE_AUDIO_ONLY;
- }
-
- // Notify the lower layer first to start signaling ASAP.
- answerScreenDelegate.onAnswer(desiredVideoState);
-
+ answerScreenDelegate.onAnswer(answerVideoAsAudio);
buttonAcceptClicked = true;
}
}
diff --git a/java/com/android/incallui/answer/impl/AnswerVideoCallScreen.java b/java/com/android/incallui/answer/impl/AnswerVideoCallScreen.java
index 0316a5fab..06502daab 100644
--- a/java/com/android/incallui/answer/impl/AnswerVideoCallScreen.java
+++ b/java/com/android/incallui/answer/impl/AnswerVideoCallScreen.java
@@ -32,12 +32,15 @@ import com.android.incallui.videosurface.bindings.VideoSurfaceBindings;
/** Shows a video preview for an incoming call. */
public class AnswerVideoCallScreen implements VideoCallScreen {
+ @NonNull private final String callId;
@NonNull private final Fragment fragment;
@NonNull private final TextureView textureView;
@NonNull private final VideoCallScreenDelegate delegate;
- public AnswerVideoCallScreen(@NonNull Fragment fragment, @NonNull View view) {
- this.fragment = fragment;
+ public AnswerVideoCallScreen(
+ @NonNull String callId, @NonNull Fragment fragment, @NonNull View view) {
+ this.callId = Assert.isNotNull(callId);
+ this.fragment = Assert.isNotNull(fragment);
textureView =
Assert.isNotNull((TextureView) view.findViewById(R.id.incoming_preview_texture_view));
@@ -53,13 +56,15 @@ public class AnswerVideoCallScreen implements VideoCallScreen {
overlayView.setVisibility(View.VISIBLE);
}
- public void onStart() {
+ @Override
+ public void onVideoScreenStart() {
LogUtil.i("AnswerVideoCallScreen.onStart", null);
delegate.onVideoCallScreenUiReady();
delegate.getLocalVideoSurfaceTexture().attachToTextureView(textureView);
}
- public void onStop() {
+ @Override
+ public void onVideoScreenStop() {
LogUtil.i("AnswerVideoCallScreen.onStop", null);
delegate.onVideoCallScreenUiUnready();
}
@@ -98,6 +103,12 @@ public class AnswerVideoCallScreen implements VideoCallScreen {
return fragment;
}
+ @NonNull
+ @Override
+ public String getCallId() {
+ return callId;
+ }
+
private void updatePreviewVideoScaling() {
if (textureView.getWidth() == 0 || textureView.getHeight() == 0) {
LogUtil.i(
diff --git a/java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java b/java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java
index 62845b748..ff20d3a05 100644
--- a/java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java
+++ b/java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java
@@ -190,7 +190,7 @@ public class SwipeButtonHelper {
case MotionEvent.ACTION_UP:
isUp = true;
- //fallthrough_intended
+ // fall through
case MotionEvent.ACTION_CANCEL:
boolean hintOnTheRight = targetedView == rightIcon;
trackMovement(event);
diff --git a/java/com/android/incallui/answer/impl/answermethod/AnswerMethodHolder.java b/java/com/android/incallui/answer/impl/answermethod/AnswerMethodHolder.java
index 4052281b7..afa194f2e 100644
--- a/java/com/android/incallui/answer/impl/answermethod/AnswerMethodHolder.java
+++ b/java/com/android/incallui/answer/impl/answermethod/AnswerMethodHolder.java
@@ -44,4 +44,6 @@ public interface AnswerMethodHolder {
* @return true iff the current call is a video call.
*/
boolean isVideoCall();
+
+ boolean isVideoUpgradeRequest();
}
diff --git a/java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java b/java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java
index 0bc65818c..6e8e1f7bf 100644
--- a/java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java
+++ b/java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java
@@ -60,7 +60,7 @@ import com.android.incallui.answer.impl.answermethod.FlingUpDownTouchHandler.OnP
import com.android.incallui.answer.impl.classifier.FalsingManager;
import com.android.incallui.answer.impl.hint.AnswerHint;
import com.android.incallui.answer.impl.hint.AnswerHintFactory;
-import com.android.incallui.answer.impl.hint.EventPayloadLoaderImpl;
+import com.android.incallui.answer.impl.hint.PawImageLoaderImpl;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -228,7 +228,7 @@ public class FlingUpDownMethod extends AnswerMethod implements OnProgressChanged
touchHandler = FlingUpDownTouchHandler.attach(view, this, falsingManager);
answerHint =
- new AnswerHintFactory(new EventPayloadLoaderImpl())
+ new AnswerHintFactory(new PawImageLoaderImpl())
.create(getContext(), ANIMATE_DURATION_LONG_MILLIS, BOUNCE_ANIMATION_DELAY);
answerHint.onCreateView(
layoutInflater,
@@ -319,7 +319,7 @@ public class FlingUpDownMethod extends AnswerMethod implements OnProgressChanged
if (contactPuckIcon == null) {
return;
}
- if (getParent().isVideoCall()) {
+ if (getParent().isVideoCall() || getParent().isVideoUpgradeRequest()) {
contactPuckIcon.setImageResource(R.drawable.quantum_ic_videocam_white_24);
} else {
contactPuckIcon.setImageResource(R.drawable.quantum_ic_call_white_24);
@@ -348,7 +348,8 @@ public class FlingUpDownMethod extends AnswerMethod implements OnProgressChanged
}
private boolean shouldShowPhotoInPuck() {
- return getParent().isVideoCall() && contactPhoto != null;
+ return (getParent().isVideoCall() || getParent().isVideoUpgradeRequest())
+ && contactPhoto != null;
}
@Override
@@ -387,6 +388,10 @@ public class FlingUpDownMethod extends AnswerMethod implements OnProgressChanged
// Since the animation progression is controlled by user gesture instead of real timeline, the
// spec timeline can be divided into 9 slots. Each slot is equivalent to 83ms in the spec.
// Therefore, we use 9 slots of 83ms to map user gesture into the spec timeline.
+ //
+ // See specs -
+ // Accept: https://direct.googleplex.com/#/spec/8510001
+ // Decline: https://direct.googleplex.com/#/spec/3850001
final float progressSlots = 9;
// Fade out the "swipe up to answer". It only takes 1 slot to complete the fade.
@@ -414,7 +419,7 @@ public class FlingUpDownMethod extends AnswerMethod implements OnProgressChanged
contactPuckBackground.setColorFilter(destPuckColor);
// Animate decline icon
- if (isAcceptingFlow || getParent().isVideoCall()) {
+ if (isAcceptingFlow || getParent().isVideoCall() || getParent().isVideoUpgradeRequest()) {
rotateToward(contactPuckIcon, 0f);
} else {
rotateToward(contactPuckIcon, positiveAdjustedProgress * ICON_END_CALL_ROTATION_DEGREES);
diff --git a/java/com/android/incallui/answer/impl/hint/AndroidManifest.xml b/java/com/android/incallui/answer/impl/hint/AndroidManifest.xml
index b5fa6da8f..dfbba1cbf 100644
--- a/java/com/android/incallui/answer/impl/hint/AndroidManifest.xml
+++ b/java/com/android/incallui/answer/impl/hint/AndroidManifest.xml
@@ -3,7 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android">
<application>
- <receiver android:name=".EventSecretCodeListener">
+ <receiver android:name=".PawSecretCodeListener">
<intent-filter>
<action android:name="android.provider.Telephony.SECRET_CODE" />
<data android:scheme="android_secret_code" />
diff --git a/java/com/android/incallui/answer/impl/hint/AnswerHintFactory.java b/java/com/android/incallui/answer/impl/hint/AnswerHintFactory.java
index 45395a71f..eaf5b74e5 100644
--- a/java/com/android/incallui/answer/impl/hint/AnswerHintFactory.java
+++ b/java/com/android/incallui/answer/impl/hint/AnswerHintFactory.java
@@ -28,7 +28,6 @@ import com.android.dialer.common.ConfigProvider;
import com.android.dialer.common.ConfigProviderBindings;
import com.android.dialer.common.LogUtil;
import com.android.incallui.util.AccessibilityUtil;
-import java.util.Calendar;
/**
* Selects a AnswerHint to show. If there's no suitable hints {@link EmptyAnswerHint} will be used,
@@ -51,10 +50,10 @@ public class AnswerHintFactory {
@VisibleForTesting
static final String ANSWERED_COUNT_PREFERENCE_KEY = "answer_hint_answered_count";
- private final EventPayloadLoader eventPayloadLoader;
+ private final PawImageLoader pawImageLoader;
- public AnswerHintFactory(@NonNull EventPayloadLoader eventPayloadLoader) {
- this.eventPayloadLoader = Assert.isNotNull(eventPayloadLoader);
+ public AnswerHintFactory(@NonNull PawImageLoader pawImageLoader) {
+ this.pawImageLoader = Assert.isNotNull(pawImageLoader);
}
@NonNull
@@ -69,11 +68,9 @@ public class AnswerHintFactory {
}
// Display the event answer hint if the payload is available.
- Drawable eventPayload =
- eventPayloadLoader.loadPayload(
- context, System.currentTimeMillis(), Calendar.getInstance().getTimeZone());
+ Drawable eventPayload = pawImageLoader.loadPayload(context);
if (eventPayload != null) {
- return new EventAnswerHint(context, eventPayload, puckUpDuration, puckUpDelay);
+ return new PawAnswerHint(context, eventPayload, puckUpDuration, puckUpDelay);
}
return new EmptyAnswerHint();
diff --git a/java/com/android/incallui/answer/impl/hint/EventPayloadLoaderImpl.java b/java/com/android/incallui/answer/impl/hint/EventPayloadLoaderImpl.java
deleted file mode 100644
index bd8d73645..000000000
--- a/java/com/android/incallui/answer/impl/hint/EventPayloadLoaderImpl.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.incallui.answer.impl.hint;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.graphics.BitmapFactory;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.os.Build.VERSION_CODES;
-import android.preference.PreferenceManager;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
-import com.android.dialer.common.Assert;
-import com.android.dialer.common.ConfigProvider;
-import com.android.dialer.common.ConfigProviderBindings;
-import com.android.dialer.common.LogUtil;
-import java.io.InputStream;
-import java.util.TimeZone;
-import javax.crypto.Cipher;
-import javax.crypto.SecretKey;
-import javax.crypto.SecretKeyFactory;
-import javax.crypto.spec.PBEKeySpec;
-
-/** Decrypt the event payload to be shown if in a specific time range and the key is received. */
-@TargetApi(VERSION_CODES.M)
-public final class EventPayloadLoaderImpl implements EventPayloadLoader {
-
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- static final String CONFIG_EVENT_KEY = "event_key";
-
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- static final String CONFIG_EVENT_BINARY = "event_binary";
-
- // Time is stored as a UTC UNIX timestamp in milliseconds, but interpreted as local time.
- // For example, 946684800 (2000/1/1 00:00:00 @UTC) is the new year midnight at every timezone.
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- static final String CONFIG_EVENT_START_UTC_AS_LOCAL_MILLIS = "event_time_start_millis";
-
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- static final String CONFIG_EVENT_TIME_END_UTC_AS_LOCAL_MILLIS = "event_time_end_millis";
-
- @Override
- @Nullable
- public Drawable loadPayload(
- @NonNull Context context, long currentTimeUtcMillis, @NonNull TimeZone timeZone) {
- Assert.isNotNull(context);
- Assert.isNotNull(timeZone);
- ConfigProvider configProvider = ConfigProviderBindings.get(context);
-
- String pbeKey = configProvider.getString(CONFIG_EVENT_KEY, null);
- if (pbeKey == null) {
- return null;
- }
- long timeRangeStart = configProvider.getLong(CONFIG_EVENT_START_UTC_AS_LOCAL_MILLIS, 0);
- long timeRangeEnd = configProvider.getLong(CONFIG_EVENT_TIME_END_UTC_AS_LOCAL_MILLIS, 0);
-
- String eventBinary = configProvider.getString(CONFIG_EVENT_BINARY, null);
- if (eventBinary == null) {
- return null;
- }
-
- SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
- if (!preferences.getBoolean(
- EventSecretCodeListener.EVENT_ENABLED_WITH_SECRET_CODE_KEY, false)) {
- long localTimestamp = currentTimeUtcMillis + timeZone.getRawOffset();
-
- if (localTimestamp < timeRangeStart) {
- return null;
- }
-
- if (localTimestamp > timeRangeEnd) {
- return null;
- }
- }
-
- // Use openssl aes-128-cbc -in <input> -out <output> -pass <PBEKey> to generate the asset
- try (InputStream input = context.getAssets().open(eventBinary)) {
- byte[] encryptedFile = new byte[input.available()];
- input.read(encryptedFile);
-
- Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding", "BC");
-
- byte[] salt = new byte[8];
- System.arraycopy(encryptedFile, 8, salt, 0, 8);
- SecretKey key =
- SecretKeyFactory.getInstance("PBEWITHMD5AND128BITAES-CBC-OPENSSL", "BC")
- .generateSecret(new PBEKeySpec(pbeKey.toCharArray(), salt, 100));
- cipher.init(Cipher.DECRYPT_MODE, key);
-
- byte[] decryptedFile = cipher.doFinal(encryptedFile, 16, encryptedFile.length - 16);
-
- return new BitmapDrawable(
- context.getResources(),
- BitmapFactory.decodeByteArray(decryptedFile, 0, decryptedFile.length));
- } catch (Exception e) {
- // Avoid crashing dialer for any reason.
- LogUtil.e("EventPayloadLoader.loadPayload", "error decrypting payload:", e);
- return null;
- }
- }
-}
diff --git a/java/com/android/incallui/answer/impl/hint/EventAnswerHint.java b/java/com/android/incallui/answer/impl/hint/PawAnswerHint.java
index 7ee327d50..36b761f57 100644
--- a/java/com/android/incallui/answer/impl/hint/EventAnswerHint.java
+++ b/java/com/android/incallui/answer/impl/hint/PawAnswerHint.java
@@ -39,7 +39,7 @@ import com.android.dialer.common.Assert;
* An Answer hint that animates a {@link Drawable} payload with animation similar to {@link
* DotAnswerHint}.
*/
-public final class EventAnswerHint implements AnswerHint {
+public final class PawAnswerHint implements AnswerHint {
private static final long FADE_IN_DELAY_SCALE_MILLIS = 380;
private static final long FADE_IN_DURATION_SCALE_MILLIS = 200;
@@ -53,7 +53,8 @@ public final class EventAnswerHint implements AnswerHint {
private static final long FADE_OUT_DELAY_ALPHA_MILLIS = 130;
private static final long FADE_OUT_DURATION_ALPHA_MILLIS = 170;
- private static final float FADE_SCALE = 1.2f;
+ private static final float IMAGE_SCALE = 1.5f;
+ private static final float FADE_SCALE = 2.0f;
private final Context context;
private final Drawable payload;
@@ -65,7 +66,7 @@ public final class EventAnswerHint implements AnswerHint {
private View answerHintContainer;
private AnimatorSet answerGestureHintAnim;
- public EventAnswerHint(
+ public PawAnswerHint(
@NonNull Context context,
@NonNull Drawable payload,
long puckUpDurationMillis,
@@ -80,9 +81,9 @@ public final class EventAnswerHint implements AnswerHint {
public void onCreateView(
LayoutInflater inflater, ViewGroup container, View puck, TextView hintText) {
this.puck = puck;
- View view = inflater.inflate(R.layout.event_hint, container, true);
+ View view = inflater.inflate(R.layout.paw_hint, container, true);
answerHintContainer = view.findViewById(R.id.answer_hint_container);
- payloadView = view.findViewById(R.id.payload);
+ payloadView = view.findViewById(R.id.paw_image);
hintText.setTextSize(
TypedValue.COMPLEX_UNIT_PX, context.getResources().getDimension(R.dimen.hint_text_size));
((ImageView) payloadView).setImageDrawable(payload);
@@ -143,7 +144,7 @@ public final class EventAnswerHint implements AnswerHint {
createUniformScaleAnimator(
target,
FADE_SCALE,
- 1.0f,
+ IMAGE_SCALE,
FADE_IN_DURATION_SCALE_MILLIS,
FADE_IN_DELAY_SCALE_MILLIS,
new LinearInterpolator());
@@ -170,7 +171,7 @@ public final class EventAnswerHint implements AnswerHint {
Animator scale =
createUniformScaleAnimator(
target,
- 1.0f,
+ IMAGE_SCALE,
FADE_SCALE,
FADE_OUT_DURATION_SCALE_MILLIS,
scaleDelay,
@@ -178,7 +179,7 @@ public final class EventAnswerHint implements AnswerHint {
Animator alpha =
createAlphaAnimator(
target,
- 01.0f,
+ 1.0f,
0.0f,
FADE_OUT_DURATION_ALPHA_MILLIS,
FADE_OUT_DELAY_ALPHA_MILLIS,
diff --git a/java/com/android/incallui/answer/impl/hint/EventPayloadLoader.java b/java/com/android/incallui/answer/impl/hint/PawImageLoader.java
index 09e3bedf2..09e700fe0 100644
--- a/java/com/android/incallui/answer/impl/hint/EventPayloadLoader.java
+++ b/java/com/android/incallui/answer/impl/hint/PawImageLoader.java
@@ -20,11 +20,9 @@ import android.content.Context;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
-import java.util.TimeZone;
-/** Loads a {@link Drawable} payload for the {@link EventAnswerHint} if it should be displayed. */
-public interface EventPayloadLoader {
+/** Loads a {@link Drawable} payload for the {@link PawAnswerHint} if it should be displayed. */
+public interface PawImageLoader {
@Nullable
- Drawable loadPayload(
- @NonNull Context context, long currentTimeUtcMillis, @NonNull TimeZone timeZone);
+ Drawable loadPayload(@NonNull Context context);
}
diff --git a/java/com/android/incallui/answer/impl/hint/PawImageLoaderImpl.java b/java/com/android/incallui/answer/impl/hint/PawImageLoaderImpl.java
new file mode 100644
index 000000000..485a9ae37
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/PawImageLoaderImpl.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.hint;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.drawable.Drawable;
+import android.os.Build.VERSION_CODES;
+import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.dialer.common.Assert;
+
+/** Decrypt the event payload to be shown if in a specific time range and the key is received. */
+@TargetApi(VERSION_CODES.M)
+public final class PawImageLoaderImpl implements PawImageLoader {
+
+ @Override
+ @Nullable
+ public Drawable loadPayload(@NonNull Context context) {
+ Assert.isNotNull(context);
+
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ if (!preferences.getBoolean(PawSecretCodeListener.PAW_ENABLED_WITH_SECRET_CODE_KEY, false)) {
+ return null;
+ }
+ int drawableId = preferences.getInt(PawSecretCodeListener.PAW_DRAWABLE_ID_KEY, 0);
+ if (drawableId == 0) {
+ return null;
+ }
+ return context.getDrawable(drawableId);
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/hint/EventSecretCodeListener.java b/java/com/android/incallui/answer/impl/hint/PawSecretCodeListener.java
index 7cf4054a9..b4fc19c0d 100644
--- a/java/com/android/incallui/answer/impl/hint/EventSecretCodeListener.java
+++ b/java/com/android/incallui/answer/impl/hint/PawSecretCodeListener.java
@@ -24,26 +24,30 @@ import android.preference.PreferenceManager;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.widget.Toast;
+import com.android.dialer.common.Assert;
import com.android.dialer.common.ConfigProviderBindings;
import com.android.dialer.common.LogUtil;
import com.android.dialer.logging.Logger;
-import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.logging.nano.DialerImpression.Type;
+import java.util.Random;
/**
* Listen to the broadcast when the user dials "*#*#[number]#*#*" to toggle the event answer hint.
*/
-public class EventSecretCodeListener extends BroadcastReceiver {
+public class PawSecretCodeListener extends BroadcastReceiver {
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- static final String CONFIG_EVENT_SECRET_CODE = "event_secret_code";
+ static final String CONFIG_PAW_SECRET_CODE = "paw_secret_code";
- public static final String EVENT_ENABLED_WITH_SECRET_CODE_KEY = "event_enabled_with_secret_code";
+ public static final String PAW_ENABLED_WITH_SECRET_CODE_KEY = "paw_enabled_with_secret_code";
+ public static final String PAW_DRAWABLE_ID_KEY = "paw_drawable_id";
@Override
public void onReceive(Context context, Intent intent) {
String host = intent.getData().getHost();
+ Assert.checkState(!TextUtils.isEmpty(host));
String secretCode =
- ConfigProviderBindings.get(context).getString(CONFIG_EVENT_SECRET_CODE, null);
+ ConfigProviderBindings.get(context).getString(CONFIG_PAW_SECRET_CODE, "729");
if (secretCode == null) {
return;
}
@@ -51,17 +55,27 @@ public class EventSecretCodeListener extends BroadcastReceiver {
return;
}
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
- boolean wasEnabled = preferences.getBoolean(EVENT_ENABLED_WITH_SECRET_CODE_KEY, false);
+ boolean wasEnabled = preferences.getBoolean(PAW_ENABLED_WITH_SECRET_CODE_KEY, false);
if (wasEnabled) {
- preferences.edit().putBoolean(EVENT_ENABLED_WITH_SECRET_CODE_KEY, false).apply();
+ preferences.edit().putBoolean(PAW_ENABLED_WITH_SECRET_CODE_KEY, false).apply();
Toast.makeText(context, R.string.event_deactivated, Toast.LENGTH_SHORT).show();
- Logger.get(context).logImpression(DialerImpression.Type.EVENT_ANSWER_HINT_DEACTIVATED);
- LogUtil.i("EventSecretCodeListener.onReceive", "EventAnswerHint disabled");
+ Logger.get(context).logImpression(Type.EVENT_ANSWER_HINT_DEACTIVATED);
+ LogUtil.i("PawSecretCodeListener.onReceive", "PawAnswerHint disabled");
} else {
- preferences.edit().putBoolean(EVENT_ENABLED_WITH_SECRET_CODE_KEY, true).apply();
+ int drawableId;
+ if (new Random().nextBoolean()) {
+ drawableId = R.drawable.cat_paw;
+ } else {
+ drawableId = R.drawable.dog_paw;
+ }
+ preferences
+ .edit()
+ .putBoolean(PAW_ENABLED_WITH_SECRET_CODE_KEY, true)
+ .putInt(PAW_DRAWABLE_ID_KEY, drawableId)
+ .apply();
Toast.makeText(context, R.string.event_activated, Toast.LENGTH_SHORT).show();
- Logger.get(context).logImpression(DialerImpression.Type.EVENT_ANSWER_HINT_ACTIVATED);
- LogUtil.i("EventSecretCodeListener.onReceive", "EventAnswerHint enabled");
+ Logger.get(context).logImpression(Type.EVENT_ANSWER_HINT_ACTIVATED);
+ LogUtil.i("PawSecretCodeListener.onReceive", "PawAnswerHint enabled");
}
}
}
diff --git a/java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/cat_paw.webp b/java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/cat_paw.webp
new file mode 100644
index 000000000..f7ff6eb54
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/cat_paw.webp
Binary files differ
diff --git a/java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/dog_paw.webp b/java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/dog_paw.webp
new file mode 100644
index 000000000..3a232542c
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/dog_paw.webp
Binary files differ
diff --git a/java/com/android/incallui/answer/impl/hint/res/layout/event_hint.xml b/java/com/android/incallui/answer/impl/hint/res/layout/paw_hint.xml
index d505014c1..c3b12a01d 100644
--- a/java/com/android/incallui/answer/impl/hint/res/layout/event_hint.xml
+++ b/java/com/android/incallui/answer/impl/hint/res/layout/paw_hint.xml
@@ -24,9 +24,10 @@
android:clipToPadding="false"
android:visibility="gone">
<ImageView
- android:id="@+id/payload"
- android:layout_width="191dp"
- android:layout_height="773dp"
+ android:id="@+id/paw_image"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@drawable/cat_paw"
android:layout_gravity="center"
android:alpha="0"
android:rotation="-30"
diff --git a/java/com/android/incallui/answer/impl/proguard.flags b/java/com/android/incallui/answer/impl/proguard.flags
new file mode 100644
index 000000000..016352857
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/proguard.flags
@@ -0,0 +1,5 @@
+# Used in com.android.dialer.answer.impl.SmsBottomSheetFragment
+-keep class android.support.design.widget.BottomSheetBehavior {
+ public <init>(android.content.Context, android.util.AttributeSet);
+ public <init>();
+} \ No newline at end of file
diff --git a/java/com/android/incallui/answer/impl/res/values/dimens.xml b/java/com/android/incallui/answer/impl/res/values/dimens.xml
index c48b68f93..8329707a6 100644
--- a/java/com/android/incallui/answer/impl/res/values/dimens.xml
+++ b/java/com/android/incallui/answer/impl/res/values/dimens.xml
@@ -22,4 +22,5 @@
<dimen name="answer_avatar_size">0dp</dimen>
<dimen name="answer_importance_margin_bottom">0dp</dimen>
<bool name="answer_important_call_allowed">false</bool>
+ <integer name="answer_animate_entry_millis">1000</integer>
</resources>
diff --git a/java/com/android/incallui/answer/protocol/AnswerScreen.java b/java/com/android/incallui/answer/protocol/AnswerScreen.java
index 0c374eb7f..f03efefc4 100644
--- a/java/com/android/incallui/answer/protocol/AnswerScreen.java
+++ b/java/com/android/incallui/answer/protocol/AnswerScreen.java
@@ -24,7 +24,7 @@ public interface AnswerScreen {
String getCallId();
- int getVideoState();
+ boolean isVideoCall();
boolean isVideoUpgradeRequest();
diff --git a/java/com/android/incallui/answer/protocol/AnswerScreenDelegate.java b/java/com/android/incallui/answer/protocol/AnswerScreenDelegate.java
index 9934497cf..36b4e3a6b 100644
--- a/java/com/android/incallui/answer/protocol/AnswerScreenDelegate.java
+++ b/java/com/android/incallui/answer/protocol/AnswerScreenDelegate.java
@@ -27,7 +27,7 @@ public interface AnswerScreenDelegate {
void onRejectCallWithMessage(String message);
- void onAnswer(int videoState);
+ void onAnswer(boolean answerVideoAsAudio);
void onReject();
diff --git a/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java b/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java
index edc3db34b..6a2c4b493 100644
--- a/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java
+++ b/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java
@@ -23,7 +23,6 @@ import android.view.Display;
import com.android.dialer.common.ConfigProviderBindings;
import com.android.dialer.common.LogUtil;
import com.android.incallui.call.DialerCall;
-import com.android.incallui.call.DialerCall.SessionModificationState;
import com.android.incallui.call.DialerCall.State;
import com.android.incallui.call.DialerCallListener;
@@ -141,7 +140,7 @@ public class AnswerProximitySensor
public void onHandoverToWifiFailure() {}
@Override
- public void onDialerCallSessionModificationStateChange(@SessionModificationState int state) {}
+ public void onDialerCallSessionModificationStateChange() {}
@Override
public void onScreenOn() {
diff --git a/java/com/android/incallui/call/CallList.java b/java/com/android/incallui/call/CallList.java
index 862c71cf9..c88802f14 100644
--- a/java/com/android/incallui/call/CallList.java
+++ b/java/com/android/incallui/call/CallList.java
@@ -38,10 +38,10 @@ import com.android.dialer.logging.nano.DialerImpression;
import com.android.dialer.shortcuts.ShortcutUsageReporter;
import com.android.dialer.spam.Spam;
import com.android.dialer.spam.SpamBindings;
-import com.android.incallui.call.DialerCall.SessionModificationState;
import com.android.incallui.call.DialerCall.State;
import com.android.incallui.latencyreport.LatencyReport;
import com.android.incallui.util.TelecomCallUtil;
+import com.android.incallui.videotech.VideoTech;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
@@ -110,6 +110,8 @@ public class CallList implements DialerCallDelegate {
Trace.beginSection("onCallAdded");
final DialerCall call =
new DialerCall(context, this, telecomCall, latencyReport, true /* registerCallback */);
+ logSecondIncomingCall(context, call);
+
final DialerCallListenerImpl dialerCallListener = new DialerCallListenerImpl(call);
call.addListener(dialerCallListener);
LogUtil.d("CallList.onCallAdded", "callState=" + call.getState());
@@ -184,6 +186,30 @@ public class CallList implements DialerCallDelegate {
Trace.endSection();
}
+ private void logSecondIncomingCall(@NonNull Context context, @NonNull DialerCall incomingCall) {
+ DialerCall firstCall = getFirstCall();
+ if (firstCall != null) {
+ int impression = 0;
+ if (firstCall.isVideoCall()) {
+ if (incomingCall.isVideoCall()) {
+ impression = DialerImpression.Type.VIDEO_CALL_WITH_INCOMING_VIDEO_CALL;
+ } else {
+ impression = DialerImpression.Type.VIDEO_CALL_WITH_INCOMING_VOICE_CALL;
+ }
+ } else {
+ if (incomingCall.isVideoCall()) {
+ impression = DialerImpression.Type.VOICE_CALL_WITH_INCOMING_VIDEO_CALL;
+ } else {
+ impression = DialerImpression.Type.VOICE_CALL_WITH_INCOMING_VOICE_CALL;
+ }
+ }
+ Assert.checkArgument(impression != 0);
+ Logger.get(context)
+ .logCallImpression(
+ impression, incomingCall.getUniqueCallId(), incomingCall.getTimeAddedMs());
+ }
+ }
+
private static boolean isPotentialEmergencyCallback(Context context, DialerCall call) {
if (BuildCompat.isAtLeastO()) {
return call.isPotentialEmergencyCallback();
@@ -440,8 +466,8 @@ public class CallList implements DialerCallDelegate {
*/
public DialerCall getVideoUpgradeRequestCall() {
for (DialerCall call : mCallById.values()) {
- if (call.getSessionModificationState()
- == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+ if (call.getVideoTech().getSessionModificationState()
+ == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
return call;
}
}
@@ -637,17 +663,7 @@ public class CallList implements DialerCallDelegate {
*/
public void notifyCallsOfDeviceRotation(int rotation) {
for (DialerCall call : mCallById.values()) {
- // First, ensure that the call videoState has video enabled (there is no need to set
- // device orientation on a voice call which has not yet been upgraded to video).
- // Second, ensure a VideoCall is set on the call so that the change can be sent to the
- // provider (a VideoCall can be present for a call that does not currently have video,
- // but can be upgraded to video).
-
- // NOTE: is it necessary to use this order because getVideoCall references the class
- // VideoProfile which is not available on APIs <23 (M).
- if (VideoUtils.isVideoCall(call) && call.getVideoCall() != null) {
- call.getVideoCall().setDeviceOrientation(rotation);
- }
+ call.getVideoTech().setDeviceOrientation(rotation);
}
}
@@ -675,7 +691,7 @@ public class CallList implements DialerCallDelegate {
void onUpgradeToVideo(DialerCall call);
/** Called when the session modification state of a call changes. */
- void onSessionModificationStateChange(@SessionModificationState int newState);
+ void onSessionModificationStateChange(DialerCall call);
/**
* Called anytime there are changes to the call list. The change can be switching call states,
@@ -754,9 +770,9 @@ public class CallList implements DialerCallDelegate {
}
@Override
- public void onDialerCallSessionModificationStateChange(@SessionModificationState int state) {
+ public void onDialerCallSessionModificationStateChange() {
for (Listener listener : mListeners) {
- listener.onSessionModificationStateChange(state);
+ listener.onSessionModificationStateChange(mCall);
}
}
}
diff --git a/java/com/android/incallui/call/DialerCall.java b/java/com/android/incallui/call/DialerCall.java
index bd8f006dd..15a0233e8 100644
--- a/java/com/android/incallui/call/DialerCall.java
+++ b/java/com/android/incallui/call/DialerCall.java
@@ -24,6 +24,7 @@ import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Trace;
import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.telecom.Call;
import android.telecom.Call.Details;
@@ -47,10 +48,15 @@ import com.android.dialer.callintent.nano.CallSpecificAppData;
import com.android.dialer.common.Assert;
import com.android.dialer.common.ConfigProviderBindings;
import com.android.dialer.common.LogUtil;
+import com.android.dialer.enrichedcall.EnrichedCallComponent;
import com.android.dialer.logging.nano.ContactLookupResult;
-import com.android.dialer.util.CallUtil;
import com.android.incallui.latencyreport.LatencyReport;
import com.android.incallui.util.TelecomCallUtil;
+import com.android.incallui.videotech.VideoTech;
+import com.android.incallui.videotech.VideoTech.VideoTechListener;
+import com.android.incallui.videotech.empty.EmptyVideoTech;
+import com.android.incallui.videotech.ims.ImsVideoTech;
+import com.android.incallui.videotech.rcs.RcsVideoShare;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
@@ -62,11 +68,16 @@ import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
/** Describes a single call and its state. */
-public class DialerCall {
+public class DialerCall implements VideoTechListener {
public static final int CALL_HISTORY_STATUS_UNKNOWN = 0;
public static final int CALL_HISTORY_STATUS_PRESENT = 1;
public static final int CALL_HISTORY_STATUS_NOT_PRESENT = 2;
+
+ // Hard coded property for {@code Call}. Upstreamed change from Motorola.
+ // TODO(b/35359461): Move it to Telecom in framework.
+ public static final int PROPERTY_CODEC_KNOWN = 0x04000000;
+
private static final String ID_PREFIX = "DialerCall_";
private static final String CONFIG_EMERGENCY_CALLBACK_WINDOW_MILLIS =
"emergency_callback_window_millis";
@@ -82,13 +93,13 @@ public class DialerCall {
private final LatencyReport mLatencyReport;
private final String mId;
private final List<String> mChildCallIds = new ArrayList<>();
- private final VideoSettings mVideoSettings = new VideoSettings();
private final LogState mLogState = new LogState();
private final Context mContext;
private final DialerCallDelegate mDialerCallDelegate;
private final List<DialerCallListener> mListeners = new CopyOnWriteArrayList<>();
private final List<CannedTextResponsesLoadedListener> mCannedTextResponsesLoadedListeners =
new CopyOnWriteArrayList<>();
+ private final VideoTechManager mVideoTechManager;
private boolean mIsEmergencyCall;
private Uri mHandle;
@@ -98,13 +109,6 @@ public class DialerCall {
private boolean hasShownWiFiToLteHandoverToast;
private boolean doNotShowDialogForHandoffToWifiFailure;
- @SessionModificationState private int mSessionModificationState;
- private int mVideoState;
- /** mRequestedVideoState is used to store requested upgrade / downgrade video state */
- private int mRequestedVideoState = VideoProfile.STATE_AUDIO_ONLY;
-
- private InCallVideoCallCallback mVideoCallCallback;
- private boolean mIsVideoCallCallbackRegistered;
private String mChildNumber;
private String mLastForwardedNumber;
private String mCallSubject;
@@ -118,6 +122,7 @@ public class DialerCall {
private boolean didShowCameraPermission;
private String callProviderLabel;
private String callbackNumber;
+ private int mCameraDirection = CameraDirection.CAMERA_DIRECTION_UNKNOWN;
public static String getNumberFromHandle(Uri handle) {
return handle == null ? "" : handle.getSchemeSpecificPart();
@@ -125,7 +130,7 @@ public class DialerCall {
/**
* Whether the call is put on hold by remote party. This is different than the {@link
- * State.ONHOLD} state which indicates that the call is being held locally on the device.
+ * State#ONHOLD} state which indicates that the call is being held locally on the device.
*/
private boolean isRemotelyHeld;
@@ -189,7 +194,7 @@ public class DialerCall {
@Override
public void onCallDestroyed(Call call) {
LogUtil.v("TelecomCallCallback.onStateChanged", "call=" + call);
- call.unregisterCallback(this);
+ unregisterCallback();
}
@Override
@@ -248,7 +253,10 @@ public class DialerCall {
mLatencyReport = latencyReport;
mId = ID_PREFIX + Integer.toString(sIdCounter++);
- updateFromTelecomCall(registerCallback);
+ // Must be after assigning mTelecomCall
+ mVideoTechManager = new VideoTechManager(this);
+
+ updateFromTelecomCall();
if (registerCallback) {
mTelecomCall.registerCallback(mTelecomCallCallback);
@@ -348,19 +356,24 @@ public class DialerCall {
return mTelecomCall.getDetails().getStatusHints();
}
- /**
- * @return video settings of the call, null if the call is not a video call.
- * @see VideoProfile
- */
- public VideoSettings getVideoSettings() {
- return mVideoSettings;
+ public int getCameraDir() {
+ return mCameraDirection;
+ }
+
+ public void setCameraDir(int cameraDir) {
+ if (cameraDir == CameraDirection.CAMERA_DIRECTION_FRONT_FACING
+ || cameraDir == CameraDirection.CAMERA_DIRECTION_BACK_FACING) {
+ mCameraDirection = cameraDir;
+ } else {
+ mCameraDirection = CameraDirection.CAMERA_DIRECTION_UNKNOWN;
+ }
}
private void update() {
Trace.beginSection("Update");
int oldState = getState();
// We want to potentially register a video call callback here.
- updateFromTelecomCall(true /* registerCallback */);
+ updateFromTelecomCall();
if (oldState != getState() && getState() == DialerCall.State.DISCONNECTED) {
for (DialerCallListener listener : mListeners) {
listener.onDialerCallDisconnect();
@@ -373,21 +386,15 @@ public class DialerCall {
Trace.endSection();
}
- private void updateFromTelecomCall(boolean registerCallback) {
+ private void updateFromTelecomCall() {
LogUtil.v("DialerCall.updateFromTelecomCall", mTelecomCall.toString());
+
+ mVideoTechManager.dispatchCallStateChanged(mTelecomCall.getState());
+
final int translatedState = translateState(mTelecomCall.getState());
if (mState != State.BLOCKED) {
setState(translatedState);
setDisconnectCause(mTelecomCall.getDetails().getDisconnectCause());
- maybeCancelVideoUpgrade(mTelecomCall.getDetails().getVideoState());
- }
-
- if (registerCallback && mTelecomCall.getVideoCall() != null) {
- if (mVideoCallCallback == null) {
- mVideoCallCallback = new InCallVideoCallCallback(this);
- }
- mTelecomCall.getVideoCall().registerCallback(mVideoCallCallback);
- mIsVideoCallCallbackRegistered = true;
}
mChildCallIds.clear();
@@ -428,19 +435,6 @@ public class DialerCall {
}
}
}
-
- if (mSessionModificationState
- == DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE
- && isVideoCall()) {
- // We find out in {@link InCallVideoCallCallback.onSessionModifyResponseReceived}
- // whether the video upgrade request was accepted. We don't clear the session modification
- // state right away though to avoid having the UI switch from video to voice to video.
- // Once the underlying telecom call updates to video mode it's safe to clear the state.
- LogUtil.i(
- "DialerCall.updateFromTelecomCall",
- "upgraded to video, clearing session modification state");
- setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
- }
}
/**
@@ -518,25 +512,6 @@ public class DialerCall {
}
}
- /**
- * Determines if a received upgrade to video request should be cancelled. This can happen if
- * another InCall UI responds to the upgrade to video request.
- *
- * @param newVideoState The new video state.
- */
- private void maybeCancelVideoUpgrade(int newVideoState) {
- boolean isVideoStateChanged = mVideoState != newVideoState;
-
- if (mSessionModificationState
- == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST
- && isVideoStateChanged) {
-
- LogUtil.i("DialerCall.maybeCancelVideoUpgrade", "cancelling upgrade notification");
- setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
- }
- mVideoState = newVideoState;
- }
-
public String getId() {
return mId;
}
@@ -710,6 +685,7 @@ public class DialerCall {
return mTelecomCall.getDetails().hasProperty(property);
}
+ @NonNull
public String getUniqueCallId() {
return uniqueCallId;
}
@@ -733,15 +709,9 @@ public class DialerCall {
return mTelecomCall == null ? null : mTelecomCall.getDetails().getAccountHandle();
}
- /**
- * @return The {@link VideoCall} instance associated with the {@link Call}. Will return {@code
- * null} until {@link #updateFromTelecomCall(boolean)} has registered a valid callback on the
- * {@link VideoCall}.
- */
+ /** @return The {@link VideoCall} instance associated with the {@link Call}. */
public VideoCall getVideoCall() {
- return mTelecomCall == null || !mIsVideoCallCallbackRegistered
- ? null
- : mTelecomCall.getVideoCall();
+ return mTelecomCall == null ? null : mTelecomCall.getVideoCall();
}
public List<String> getChildCallIds() {
@@ -761,85 +731,23 @@ public class DialerCall {
}
public boolean isVideoCall() {
- return CallUtil.isVideoEnabled(mContext) && VideoUtils.isVideoCall(getVideoState());
+ return getVideoTech().isTransmittingOrReceiving();
}
- /**
- * Determines if the call handle is an emergency number or not and caches the result to avoid
- * repeated calls to isEmergencyNumber.
- */
- private void updateEmergencyCallState() {
- mIsEmergencyCall = TelecomCallUtil.isEmergencyCall(mTelecomCall);
+ public boolean hasReceivedVideoUpgradeRequest() {
+ return VideoUtils.hasReceivedVideoUpgradeRequest(getVideoTech().getSessionModificationState());
}
- /**
- * Gets the video state which was requested via a session modification request.
- *
- * @return The video state.
- */
- public int getRequestedVideoState() {
- return mRequestedVideoState;
+ public boolean hasSentVideoUpgradeRequest() {
+ return VideoUtils.hasSentVideoUpgradeRequest(getVideoTech().getSessionModificationState());
}
/**
- * Handles incoming session modification requests. Stores the pending video request and sets the
- * session modification state to {@link
- * DialerCall#SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST} so that we can keep
- * track of the fact the request was received. Only upgrade requests require user confirmation and
- * will be handled by this method. The remote user can turn off their own camera without
- * confirmation.
- *
- * @param videoState The requested video state.
- */
- public void setRequestedVideoState(int videoState) {
- LogUtil.v("DialerCall.setRequestedVideoState", "videoState: " + videoState);
- if (videoState == getVideoState()) {
- LogUtil.e("DialerCall.setRequestedVideoState", "clearing session modification state");
- setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
- return;
- }
-
- mRequestedVideoState = videoState;
- setSessionModificationState(
- DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST);
- for (DialerCallListener listener : mListeners) {
- listener.onDialerCallUpgradeToVideo();
- }
-
- LogUtil.i(
- "DialerCall.setRequestedVideoState",
- "mSessionModificationState: %d, videoState: %d",
- mSessionModificationState,
- videoState);
- update();
- }
-
- /**
- * Gets the current video session modification state.
- *
- * @return The session modification state.
- */
- @SessionModificationState
- public int getSessionModificationState() {
- return mSessionModificationState;
- }
-
- /**
- * Set the session modification state. Used to keep track of pending video session modification
- * operations and to inform listeners of these changes.
- *
- * @param state the new session modification state.
+ * Determines if the call handle is an emergency number or not and caches the result to avoid
+ * repeated calls to isEmergencyNumber.
*/
- public void setSessionModificationState(@SessionModificationState int state) {
- boolean hasChanged = mSessionModificationState != state;
- if (hasChanged) {
- LogUtil.i(
- "DialerCall.setSessionModificationState", "%d -> %d", mSessionModificationState, state);
- mSessionModificationState = state;
- for (DialerCallListener listener : mListeners) {
- listener.onDialerCallSessionModificationStateChange(state);
- }
- }
+ private void updateEmergencyCallState() {
+ mIsEmergencyCall = TelecomCallUtil.isEmergencyCall(mTelecomCall);
}
public LogState getLogState() {
@@ -862,24 +770,6 @@ public class DialerCall {
}
/**
- * Determines if the external call is pullable.
- *
- * <p>An external call is one which does not exist locally for the {@link
- * android.telecom.ConnectionService} it is associated with. An external call may be "pullable",
- * which means that the user can request it be transferred to the current device.
- *
- * <p>External calls are only supported in N and higher.
- *
- * @return {@code true} if the call is an external call, {@code false} otherwise.
- */
- public boolean isPullableExternalCall() {
- return VERSION.SDK_INT >= VERSION_CODES.N
- && (mTelecomCall.getDetails().getCallCapabilities()
- & CallCompat.Details.CAPABILITY_CAN_PULL_CALL)
- == CallCompat.Details.CAPABILITY_CAN_PULL_CALL;
- }
-
- /**
* Determines if answering this call will cause an ongoing video call to be dropped.
*
* @return {@code true} if answering this call will drop an ongoing video call, {@code false}
@@ -922,7 +812,7 @@ public class DialerCall {
return String.format(
Locale.US,
"[%s, %s, %s, %s, children:%s, parent:%s, "
- + "conferenceable:%s, videoState:%s, mSessionModificationState:%d, VideoSettings:%s]",
+ + "conferenceable:%s, videoState:%s, mSessionModificationState:%d, CameraDir:%s]",
mId,
State.toString(getState()),
Details.capabilitiesToString(mTelecomCall.getDetails().getCallCapabilities()),
@@ -931,8 +821,8 @@ public class DialerCall {
getParentId(),
this.mTelecomCall.getConferenceableCalls(),
VideoProfile.videoStateToString(mTelecomCall.getDetails().getVideoState()),
- mSessionModificationState,
- getVideoSettings());
+ getVideoTech().getSessionModificationState(),
+ getCameraDir());
}
public String toSimpleString() {
@@ -1012,20 +902,6 @@ public class DialerCall {
mTelecomCall.unregisterCallback(mTelecomCallCallback);
}
- public void acceptUpgradeRequest(int videoState) {
- LogUtil.i("DialerCall.acceptUpgradeRequest", "videoState: " + videoState);
- VideoProfile videoProfile = new VideoProfile(videoState);
- getVideoCall().sendSessionModifyResponse(videoProfile);
- setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
- }
-
- public void declineUpgradeRequest() {
- LogUtil.i("DialerCall.declineUpgradeRequest", "");
- VideoProfile videoProfile = new VideoProfile(getVideoState());
- getVideoCall().sendSessionModifyResponse(videoProfile);
- setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
- }
-
public void phoneAccountSelected(PhoneAccountHandle accountHandle, boolean setDefault) {
LogUtil.i(
"DialerCall.phoneAccountSelected",
@@ -1064,6 +940,10 @@ public class DialerCall {
mTelecomCall.answer(videoState);
}
+ public void answer() {
+ answer(mTelecomCall.getDetails().getVideoState());
+ }
+
public void reject(boolean rejectWithMessage, String message) {
LogUtil.i("DialerCall.reject", "");
mTelecomCall.reject(rejectWithMessage, message);
@@ -1095,6 +975,10 @@ public class DialerCall {
return mContext.getSystemService(TelecomManager.class).getPhoneAccount(accountHandle);
}
+ public VideoTech getVideoTech() {
+ return mVideoTechManager.getVideoTech();
+ }
+
public String getCallbackNumber() {
if (callbackNumber == null) {
// Show the emergency callback number if either:
@@ -1146,6 +1030,39 @@ public class DialerCall {
return null;
}
+ @Override
+ public void onVideoTechStateChanged() {
+ update();
+ }
+
+ @Override
+ public void onSessionModificationStateChanged() {
+ for (DialerCallListener listener : mListeners) {
+ listener.onDialerCallSessionModificationStateChange();
+ }
+ }
+
+ @Override
+ public void onCameraDimensionsChanged(int width, int height) {
+ InCallVideoCallCallbackNotifier.getInstance().cameraDimensionsChanged(this, width, height);
+ }
+
+ @Override
+ public void onPeerDimensionsChanged(int width, int height) {
+ InCallVideoCallCallbackNotifier.getInstance().peerDimensionsChanged(this, width, height);
+ }
+
+ @Override
+ public void onVideoUpgradeRequestReceived() {
+ LogUtil.enterBlock("DialerCall.onVideoUpgradeRequestReceived");
+
+ for (DialerCallListener listener : mListeners) {
+ listener.onDialerCallUpgradeToVideo();
+ }
+
+ update();
+ }
+
/**
* Specifies whether a number is in the call history or not. {@link #CALL_HISTORY_STATUS_UNKNOWN}
* means there is no result.
@@ -1191,8 +1108,8 @@ public class DialerCall {
case CONFERENCED:
return true;
default:
+ return false;
}
- return false;
}
public static boolean isDialing(int state) {
@@ -1239,71 +1156,11 @@ public class DialerCall {
}
}
- /**
- * Defines different states of session modify requests, which are used to upgrade to video, or
- * downgrade to audio.
- */
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({
- SESSION_MODIFICATION_STATE_NO_REQUEST,
- SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE,
- SESSION_MODIFICATION_STATE_REQUEST_FAILED,
- SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST,
- SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT,
- SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED,
- SESSION_MODIFICATION_STATE_REQUEST_REJECTED,
- SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE
- })
- public @interface SessionModificationState {}
-
- public static final int SESSION_MODIFICATION_STATE_NO_REQUEST = 0;
- public static final int SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE = 1;
- public static final int SESSION_MODIFICATION_STATE_REQUEST_FAILED = 2;
- public static final int SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST = 3;
- public static final int SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT = 4;
- public static final int SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED = 5;
- public static final int SESSION_MODIFICATION_STATE_REQUEST_REJECTED = 6;
- public static final int SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE = 7;
-
- public static class VideoSettings {
-
+ /** Camera direction constants */
+ public static class CameraDirection {
public static final int CAMERA_DIRECTION_UNKNOWN = -1;
public static final int CAMERA_DIRECTION_FRONT_FACING = CameraCharacteristics.LENS_FACING_FRONT;
public static final int CAMERA_DIRECTION_BACK_FACING = CameraCharacteristics.LENS_FACING_BACK;
-
- private int mCameraDirection = CAMERA_DIRECTION_UNKNOWN;
-
- /**
- * Gets the camera direction. if camera direction is set to CAMERA_DIRECTION_UNKNOWN, the video
- * state of the call should be used to infer the camera direction.
- *
- * @see {@link CameraCharacteristics#LENS_FACING_FRONT}
- * @see {@link CameraCharacteristics#LENS_FACING_BACK}
- */
- public int getCameraDir() {
- return mCameraDirection;
- }
-
- /**
- * Sets the camera direction. if camera direction is set to CAMERA_DIRECTION_UNKNOWN, the video
- * state of the call should be used to infer the camera direction.
- *
- * @see {@link CameraCharacteristics#LENS_FACING_FRONT}
- * @see {@link CameraCharacteristics#LENS_FACING_BACK}
- */
- public void setCameraDir(int cameraDirection) {
- if (cameraDirection == CAMERA_DIRECTION_FRONT_FACING
- || cameraDirection == CAMERA_DIRECTION_BACK_FACING) {
- mCameraDirection = cameraDirection;
- } else {
- mCameraDirection = CAMERA_DIRECTION_UNKNOWN;
- }
- }
-
- @Override
- public String toString() {
- return "(CameraDir:" + getCameraDir() + ")";
- }
}
/**
@@ -1394,6 +1251,48 @@ public class DialerCall {
}
}
+ private static class VideoTechManager {
+ private final EmptyVideoTech emptyVideoTech = new EmptyVideoTech();
+ private final VideoTech[] videoTechs;
+ private VideoTech savedTech;
+
+ VideoTechManager(DialerCall call) {
+ String phoneNumber = call.getNumber();
+
+ // Insert order here determines the priority of that video tech option
+ videoTechs =
+ new VideoTech[] {
+ new ImsVideoTech(call, call.mTelecomCall),
+ new RcsVideoShare(
+ EnrichedCallComponent.get(call.mContext).getEnrichedCallManager(),
+ call,
+ phoneNumber != null ? phoneNumber : "")
+ };
+ }
+
+ VideoTech getVideoTech() {
+ if (savedTech != null) {
+ return savedTech;
+ }
+
+ for (VideoTech tech : videoTechs) {
+ if (tech.isAvailable()) {
+ // Remember the first VideoTech that becomes available and always use it
+ savedTech = tech;
+ return savedTech;
+ }
+ }
+
+ return emptyVideoTech;
+ }
+
+ void dispatchCallStateChanged(int newState) {
+ for (VideoTech videoTech : videoTechs) {
+ videoTech.onCallStateChanged(newState);
+ }
+ }
+ }
+
/** Called when canned text responses have been loaded. */
public interface CannedTextResponsesLoadedListener {
void onCannedTextResponsesLoaded(DialerCall call);
diff --git a/java/com/android/incallui/call/DialerCallListener.java b/java/com/android/incallui/call/DialerCallListener.java
index b426cd72e..fece103fa 100644
--- a/java/com/android/incallui/call/DialerCallListener.java
+++ b/java/com/android/incallui/call/DialerCallListener.java
@@ -16,8 +16,6 @@
package com.android.incallui.call;
-import com.android.incallui.call.DialerCall.SessionModificationState;
-
/** Used to monitor state changes in a dialer call. */
public interface DialerCallListener {
@@ -31,7 +29,7 @@ public interface DialerCallListener {
void onDialerCallUpgradeToVideo();
- void onDialerCallSessionModificationStateChange(@SessionModificationState int state);
+ void onDialerCallSessionModificationStateChange();
void onWiFiToLteHandover();
diff --git a/java/com/android/incallui/call/InCallVideoCallCallback.java b/java/com/android/incallui/call/InCallVideoCallCallback.java
deleted file mode 100644
index f897ac9dd..000000000
--- a/java/com/android/incallui/call/InCallVideoCallCallback.java
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.incallui.call;
-
-import android.os.Handler;
-import android.support.annotation.Nullable;
-import android.telecom.Connection;
-import android.telecom.Connection.VideoProvider;
-import android.telecom.InCallService.VideoCall;
-import android.telecom.VideoProfile;
-import android.telecom.VideoProfile.CameraCapabilities;
-import com.android.dialer.common.LogUtil;
-import com.android.incallui.call.DialerCall.SessionModificationState;
-
-/** Implements the InCallUI VideoCall Callback. */
-public class InCallVideoCallCallback extends VideoCall.Callback implements Runnable {
-
- private static final int CLEAR_FAILED_REQUEST_TIMEOUT_MILLIS = 4000;
-
- private final DialerCall call;
- @Nullable private Handler handler;
- @SessionModificationState private int newSessionModificationState;
-
- public InCallVideoCallCallback(DialerCall call) {
- this.call = call;
- }
-
- @Override
- public void onSessionModifyRequestReceived(VideoProfile videoProfile) {
- LogUtil.i(
- "InCallVideoCallCallback.onSessionModifyRequestReceived", "videoProfile: " + videoProfile);
- int previousVideoState = VideoUtils.getUnPausedVideoState(call.getVideoState());
- int newVideoState = VideoUtils.getUnPausedVideoState(videoProfile.getVideoState());
-
- boolean wasVideoCall = VideoUtils.isVideoCall(previousVideoState);
- boolean isVideoCall = VideoUtils.isVideoCall(newVideoState);
-
- if (wasVideoCall && !isVideoCall) {
- LogUtil.v(
- "InCallVideoCallCallback.onSessionModifyRequestReceived",
- "call downgraded to " + newVideoState);
- } else if (previousVideoState != newVideoState) {
- InCallVideoCallCallbackNotifier.getInstance().upgradeToVideoRequest(call, newVideoState);
- }
- }
-
- /**
- * @param status Status of the session modify request. Valid values are {@link
- * Connection.VideoProvider#SESSION_MODIFY_REQUEST_SUCCESS}, {@link
- * Connection.VideoProvider#SESSION_MODIFY_REQUEST_FAIL}, {@link
- * Connection.VideoProvider#SESSION_MODIFY_REQUEST_INVALID}
- * @param responseProfile The actual profile changes made by the peer device.
- */
- @Override
- public void onSessionModifyResponseReceived(
- int status, VideoProfile requestedProfile, VideoProfile responseProfile) {
- LogUtil.i(
- "InCallVideoCallCallback.onSessionModifyResponseReceived",
- "status: %d, "
- + "requestedProfile: %s, responseProfile: %s, current session modification state: %d",
- status,
- requestedProfile,
- responseProfile,
- call.getSessionModificationState());
-
- if (call.getSessionModificationState()
- == DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE) {
- if (handler == null) {
- handler = new Handler();
- } else {
- handler.removeCallbacks(this);
- }
-
- newSessionModificationState = getDialerSessionModifyStateTelecomStatus(status);
- if (status != VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS) {
- // This will update the video UI to display the error message.
- call.setSessionModificationState(newSessionModificationState);
- }
-
- // Wait for 4 seconds and then clean the session modification state. This allows the video UI
- // to stay up so that the user can read the error message.
- //
- // If the other person accepted the upgrade request then this will keep the video UI up until
- // the call's video state change. Without this we would switch to the voice call and then
- // switch back to video UI.
- handler.postDelayed(this, CLEAR_FAILED_REQUEST_TIMEOUT_MILLIS);
- } else if (call.getSessionModificationState()
- == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
- call.setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
- } else if (call.getSessionModificationState()
- == DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE) {
- call.setSessionModificationState(getDialerSessionModifyStateTelecomStatus(status));
- } else {
- LogUtil.i(
- "InCallVideoCallCallback.onSessionModifyResponseReceived",
- "call is not waiting for " + "response, doing nothing");
- }
- }
-
- @SessionModificationState
- private int getDialerSessionModifyStateTelecomStatus(int telecomStatus) {
- switch (telecomStatus) {
- case VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS:
- return DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST;
- case VideoProvider.SESSION_MODIFY_REQUEST_FAIL:
- case VideoProvider.SESSION_MODIFY_REQUEST_INVALID:
- // Check if it's already video call, which means the request is not video upgrade request.
- if (VideoUtils.isVideoCall(call.getVideoState())) {
- return DialerCall.SESSION_MODIFICATION_STATE_REQUEST_FAILED;
- } else {
- return DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED;
- }
- case VideoProvider.SESSION_MODIFY_REQUEST_TIMED_OUT:
- return DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT;
- case VideoProvider.SESSION_MODIFY_REQUEST_REJECTED_BY_REMOTE:
- return DialerCall.SESSION_MODIFICATION_STATE_REQUEST_REJECTED;
- default:
- LogUtil.e(
- "InCallVideoCallCallback.getDialerSessionModifyStateTelecomStatus",
- "unknown status: %d",
- telecomStatus);
- return DialerCall.SESSION_MODIFICATION_STATE_REQUEST_FAILED;
- }
- }
-
- @Override
- public void onCallSessionEvent(int event) {
- InCallVideoCallCallbackNotifier.getInstance().callSessionEvent(event);
- }
-
- @Override
- public void onPeerDimensionsChanged(int width, int height) {
- InCallVideoCallCallbackNotifier.getInstance().peerDimensionsChanged(call, width, height);
- }
-
- @Override
- public void onVideoQualityChanged(int videoQuality) {
- InCallVideoCallCallbackNotifier.getInstance().videoQualityChanged(call, videoQuality);
- }
-
- /**
- * Handles a change to the call data usage. No implementation as the in-call UI does not display
- * data usage.
- *
- * @param dataUsage The updated data usage.
- */
- @Override
- public void onCallDataUsageChanged(long dataUsage) {
- LogUtil.v("InCallVideoCallCallback.onCallDataUsageChanged", "dataUsage = " + dataUsage);
- InCallVideoCallCallbackNotifier.getInstance().callDataUsageChanged(dataUsage);
- }
-
- /**
- * Handles changes to the camera capabilities. No implementation as the in-call UI does not make
- * use of camera capabilities.
- *
- * @param cameraCapabilities The changed camera capabilities.
- */
- @Override
- public void onCameraCapabilitiesChanged(CameraCapabilities cameraCapabilities) {
- if (cameraCapabilities != null) {
- InCallVideoCallCallbackNotifier.getInstance()
- .cameraDimensionsChanged(
- call, cameraCapabilities.getWidth(), cameraCapabilities.getHeight());
- }
- }
-
- /**
- * Called 4 seconds after the remote user responds to the video upgrade request. We use this to
- * clear the session modify state.
- */
- @Override
- public void run() {
- if (call.getSessionModificationState() == newSessionModificationState) {
- LogUtil.i("InCallVideoCallCallback.onSessionModifyResponseReceived", "clearing state");
- call.setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
- } else {
- LogUtil.i(
- "InCallVideoCallCallback.onSessionModifyResponseReceived",
- "session modification state has changed, not clearing state");
- }
- }
-}
diff --git a/java/com/android/incallui/call/InCallVideoCallCallbackNotifier.java b/java/com/android/incallui/call/InCallVideoCallCallbackNotifier.java
index 4a949263c..1cb9f742e 100644
--- a/java/com/android/incallui/call/InCallVideoCallCallbackNotifier.java
+++ b/java/com/android/incallui/call/InCallVideoCallCallbackNotifier.java
@@ -18,16 +18,12 @@ package com.android.incallui.call;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
-import com.android.dialer.common.LogUtil;
import java.util.Collections;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
-/**
- * Class used by {@link InCallService.VideoCallCallback} to notify interested parties of incoming
- * events.
- */
+/** Class used to notify interested parties of incoming video related events. */
public class InCallVideoCallCallbackNotifier {
/** Singleton instance of this class. */
@@ -37,12 +33,6 @@ public class InCallVideoCallCallbackNotifier {
* ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is load factor before
* resizing, 1 means we only expect a single thread to access the map so make only a single shard
*/
- private final Set<SessionModificationListener> mSessionModificationListeners =
- Collections.newSetFromMap(
- new ConcurrentHashMap<SessionModificationListener, Boolean>(8, 0.9f, 1));
-
- private final Set<VideoEventListener> mVideoEventListeners =
- Collections.newSetFromMap(new ConcurrentHashMap<VideoEventListener, Boolean>(8, 0.9f, 1));
private final Set<SurfaceChangeListener> mSurfaceChangeListeners =
Collections.newSetFromMap(new ConcurrentHashMap<SurfaceChangeListener, Boolean>(8, 0.9f, 1));
@@ -55,48 +45,6 @@ public class InCallVideoCallCallbackNotifier {
}
/**
- * Adds a new {@link SessionModificationListener}.
- *
- * @param listener The listener.
- */
- public void addSessionModificationListener(@NonNull SessionModificationListener listener) {
- Objects.requireNonNull(listener);
- mSessionModificationListeners.add(listener);
- }
-
- /**
- * Remove a {@link SessionModificationListener}.
- *
- * @param listener The listener.
- */
- public void removeSessionModificationListener(@Nullable SessionModificationListener listener) {
- if (listener != null) {
- mSessionModificationListeners.remove(listener);
- }
- }
-
- /**
- * Adds a new {@link VideoEventListener}.
- *
- * @param listener The listener.
- */
- public void addVideoEventListener(@NonNull VideoEventListener listener) {
- Objects.requireNonNull(listener);
- mVideoEventListeners.add(listener);
- }
-
- /**
- * Remove a {@link VideoEventListener}.
- *
- * @param listener The listener.
- */
- public void removeVideoEventListener(@Nullable VideoEventListener listener) {
- if (listener != null) {
- mVideoEventListeners.remove(listener);
- }
- }
-
- /**
* Adds a new {@link SurfaceChangeListener}.
*
* @param listener The listener.
@@ -118,56 +66,6 @@ public class InCallVideoCallCallbackNotifier {
}
/**
- * Inform listeners of an upgrade to video request for a call.
- *
- * @param call The call.
- * @param videoState The video state we want to upgrade to.
- */
- public void upgradeToVideoRequest(DialerCall call, int videoState) {
- LogUtil.v(
- "InCallVideoCallCallbackNotifier.upgradeToVideoRequest",
- "call = " + call + " new video state = " + videoState);
- for (SessionModificationListener listener : mSessionModificationListeners) {
- listener.onUpgradeToVideoRequest(call, videoState);
- }
- }
-
- /**
- * Inform listeners of a call session event.
- *
- * @param event The call session event.
- */
- public void callSessionEvent(int event) {
- for (VideoEventListener listener : mVideoEventListeners) {
- listener.onCallSessionEvent(event);
- }
- }
-
- /**
- * Inform listeners of a downgrade to audio.
- *
- * @param call The call.
- * @param paused The paused state.
- */
- public void peerPausedStateChanged(DialerCall call, boolean paused) {
- for (VideoEventListener listener : mVideoEventListeners) {
- listener.onPeerPauseStateChanged(call, paused);
- }
- }
-
- /**
- * Inform listeners of any change in the video quality of the call
- *
- * @param call The call.
- * @param videoQuality The updated video quality of the call.
- */
- public void videoQualityChanged(DialerCall call, int videoQuality) {
- for (VideoEventListener listener : mVideoEventListeners) {
- listener.onVideoQualityChanged(call, videoQuality);
- }
- }
-
- /**
* Inform listeners of a change to peer dimensions.
*
* @param call The call.
@@ -194,67 +92,6 @@ public class InCallVideoCallCallbackNotifier {
}
/**
- * Inform listeners of a change to call data usage.
- *
- * @param dataUsage data usage value
- */
- public void callDataUsageChanged(long dataUsage) {
- for (VideoEventListener listener : mVideoEventListeners) {
- listener.onCallDataUsageChange(dataUsage);
- }
- }
-
- /** Listener interface for any class that wants to be notified of upgrade to video request. */
- public interface SessionModificationListener {
-
- /**
- * Called when a peer request is received to upgrade an audio-only call to a video call.
- *
- * @param call The call the request was received for.
- * @param videoState The requested video state.
- */
- void onUpgradeToVideoRequest(DialerCall call, int videoState);
- }
-
- /**
- * Listener interface for any class that wants to be notified of video events, including pause and
- * un-pause of peer video, video quality changes.
- */
- public interface VideoEventListener {
-
- /**
- * Called when the peer pauses or un-pauses video transmission.
- *
- * @param call The call which paused or un-paused video transmission.
- * @param paused {@code True} when the video transmission is paused, {@code false} otherwise.
- */
- void onPeerPauseStateChanged(DialerCall call, boolean paused);
-
- /**
- * Called when the video quality changes.
- *
- * @param call The call whose video quality changes.
- * @param videoCallQuality - values are QUALITY_HIGH, MEDIUM, LOW and UNKNOWN.
- */
- void onVideoQualityChanged(DialerCall call, int videoCallQuality);
-
- /*
- * Called when call data usage value is requested or when call data usage value is updated
- * because of a call state change
- *
- * @param dataUsage call data usage value
- */
- void onCallDataUsageChange(long dataUsage);
-
- /**
- * Called when call session event is raised.
- *
- * @param event The call session event.
- */
- void onCallSessionEvent(int event);
- }
-
- /**
* Listener interface for any class that wants to be notified of changes to the video surfaces.
*/
public interface SurfaceChangeListener {
diff --git a/java/com/android/incallui/call/VideoUtils.java b/java/com/android/incallui/call/VideoUtils.java
index 80fbfb1cc..b99b73222 100644
--- a/java/com/android/incallui/call/VideoUtils.java
+++ b/java/com/android/incallui/call/VideoUtils.java
@@ -19,113 +19,24 @@ package com.android.incallui.call;
import android.content.Context;
import android.content.pm.PackageManager;
import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
-import android.telecom.VideoProfile;
-import com.android.dialer.compat.CompatUtils;
import com.android.dialer.util.DialerUtils;
-import com.android.incallui.call.DialerCall.SessionModificationState;
-import java.util.Objects;
+import com.android.incallui.videotech.VideoTech;
+import com.android.incallui.videotech.VideoTech.SessionModificationState;
public class VideoUtils {
private static final String PREFERENCE_CAMERA_ALLOWED_BY_USER = "camera_allowed_by_user";
- public static boolean isVideoCall(@Nullable DialerCall call) {
- return call != null && isVideoCall(call.getVideoState());
- }
-
- public static boolean isVideoCall(int videoState) {
- return CompatUtils.isVideoCompatible()
- && (VideoProfile.isTransmissionEnabled(videoState)
- || VideoProfile.isReceptionEnabled(videoState));
- }
-
- public static boolean hasSentVideoUpgradeRequest(@Nullable DialerCall call) {
- return call != null && hasSentVideoUpgradeRequest(call.getSessionModificationState());
- }
-
public static boolean hasSentVideoUpgradeRequest(@SessionModificationState int state) {
- return state == DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE
- || state == DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED
- || state == DialerCall.SESSION_MODIFICATION_STATE_REQUEST_REJECTED
- || state == DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT;
- }
-
- public static boolean hasReceivedVideoUpgradeRequest(@Nullable DialerCall call) {
- return call != null && hasReceivedVideoUpgradeRequest(call.getSessionModificationState());
+ return state == VideoTech.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE
+ || state == VideoTech.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED
+ || state == VideoTech.SESSION_MODIFICATION_STATE_REQUEST_REJECTED
+ || state == VideoTech.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT;
}
public static boolean hasReceivedVideoUpgradeRequest(@SessionModificationState int state) {
- return state == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
- }
-
- public static boolean isBidirectionalVideoCall(DialerCall call) {
- return CompatUtils.isVideoCompatible() && VideoProfile.isBidirectional(call.getVideoState());
- }
-
- public static boolean isTransmissionEnabled(DialerCall call) {
- if (!CompatUtils.isVideoCompatible()) {
- return false;
- }
-
- return VideoProfile.isTransmissionEnabled(call.getVideoState());
- }
-
- public static boolean isIncomingVideoCall(DialerCall call) {
- if (!VideoUtils.isVideoCall(call)) {
- return false;
- }
- final int state = call.getState();
- return (state == DialerCall.State.INCOMING) || (state == DialerCall.State.CALL_WAITING);
- }
-
- public static boolean isActiveVideoCall(DialerCall call) {
- return VideoUtils.isVideoCall(call) && call.getState() == DialerCall.State.ACTIVE;
- }
-
- public static boolean isOutgoingVideoCall(DialerCall call) {
- if (!VideoUtils.isVideoCall(call)) {
- return false;
- }
- final int state = call.getState();
- return DialerCall.State.isDialing(state)
- || state == DialerCall.State.CONNECTING
- || state == DialerCall.State.SELECT_PHONE_ACCOUNT;
- }
-
- public static boolean isAudioCall(DialerCall call) {
- if (!CompatUtils.isVideoCompatible()) {
- return true;
- }
-
- return call != null && VideoProfile.isAudioOnly(call.getVideoState());
- }
-
- // TODO (ims-vt) Check if special handling is needed for CONF calls.
- public static boolean canVideoPause(DialerCall call) {
- return isVideoCall(call) && call.getState() == DialerCall.State.ACTIVE;
- }
-
- public static VideoProfile makeVideoPauseProfile(@NonNull DialerCall call) {
- Objects.requireNonNull(call);
- if (VideoProfile.isAudioOnly(call.getVideoState())) {
- throw new IllegalStateException();
- }
- return new VideoProfile(getPausedVideoState(call.getVideoState()));
- }
-
- public static VideoProfile makeVideoUnPauseProfile(@NonNull DialerCall call) {
- Objects.requireNonNull(call);
- return new VideoProfile(getUnPausedVideoState(call.getVideoState()));
- }
-
- public static int getUnPausedVideoState(int videoState) {
- return videoState & (~VideoProfile.STATE_PAUSED);
- }
-
- public static int getPausedVideoState(int videoState) {
- return videoState | VideoProfile.STATE_PAUSED;
+ return state == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
}
public static boolean hasCameraPermissionAndAllowedByUser(@NonNull Context context) {
diff --git a/java/com/android/incallui/maps/StaticMapFactory.java b/java/com/android/incallui/calllocation/CallLocation.java
index a35013886..15a6a8e49 100644
--- a/java/com/android/incallui/maps/StaticMapFactory.java
+++ b/java/com/android/incallui/calllocation/CallLocation.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,15 +14,19 @@
* limitations under the License
*/
-package com.android.incallui.maps;
+package com.android.incallui.calllocation;
-import android.location.Location;
+import android.content.Context;
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 {
+/** Used to show the user's location during an emergency call. */
+public interface CallLocation {
+
+ boolean canGetLocation(@NonNull Context context);
@NonNull
- Fragment getStaticMap(@NonNull Location location);
+ Fragment getLocationFragment(@NonNull Context context);
+
+ void close();
}
diff --git a/java/com/android/incallui/calllocation/CallLocationComponent.java b/java/com/android/incallui/calllocation/CallLocationComponent.java
new file mode 100644
index 000000000..6b1faf299
--- /dev/null
+++ b/java/com/android/incallui/calllocation/CallLocationComponent.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation;
+
+import android.content.Context;
+import dagger.Subcomponent;
+import com.android.incallui.calllocation.stub.StubCallLocationModule;
+
+/** Subcomponent that can be used to access the call location implementation. */
+public class CallLocationComponent {
+ private static CallLocationComponent instance;
+ private CallLocation callLocation;
+
+ public CallLocation getCallLocation(){
+ if (callLocation == null) {
+ callLocation = new StubCallLocationModule.StubCallLocation();
+ }
+ return callLocation;
+ }
+
+ public static CallLocationComponent get(Context context) {
+ if (instance == null) {
+ instance = new CallLocationComponent();
+ }
+ return instance;
+ }
+
+ /** Used to refer to the root application component. */
+ public interface HasComponent {
+ CallLocationComponent callLocationComponent();
+ }
+}
diff --git a/java/com/android/incallui/calllocation/impl/AndroidManifest.xml b/java/com/android/incallui/calllocation/impl/AndroidManifest.xml
new file mode 100644
index 000000000..550c5808c
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.incallui.calllocation.impl">
+
+ <application>
+ <meta-data
+ android:name="com.google.android.gms.version"
+ android:value="@integer/google_play_services_version"/>
+ </application>
+</manifest>
diff --git a/java/com/android/incallui/calllocation/impl/AuthException.java b/java/com/android/incallui/calllocation/impl/AuthException.java
new file mode 100644
index 000000000..26def2fc9
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/AuthException.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+/** For detecting backend authorization errors */
+public class AuthException extends Exception {
+
+ public AuthException(String detailMessage) {
+ super(detailMessage);
+ }
+}
diff --git a/java/com/android/incallui/calllocation/impl/CallLocationImpl.java b/java/com/android/incallui/calllocation/impl/CallLocationImpl.java
new file mode 100644
index 000000000..20f5ffb0f
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/CallLocationImpl.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import android.content.Context;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.v4.app.Fragment;
+import com.android.dialer.common.Assert;
+import com.android.incallui.calllocation.CallLocation;
+import javax.inject.Inject;
+
+/** Uses Google Play Services to show the user's location during an emergency call. */
+public class CallLocationImpl implements CallLocation {
+
+ private LocationHelper locationHelper;
+ private LocationFragment locationFragment;
+
+ @Inject
+ public CallLocationImpl() {}
+
+ @MainThread
+ @Override
+ public boolean canGetLocation(@NonNull Context context) {
+ Assert.isMainThread();
+ return LocationHelper.canGetLocation(context);
+ }
+
+ @MainThread
+ @NonNull
+ @Override
+ public Fragment getLocationFragment(@NonNull Context context) {
+ Assert.isMainThread();
+ if (locationFragment == null) {
+ locationFragment = new LocationFragment();
+ locationHelper = new LocationHelper(context);
+ locationHelper.addLocationListener(locationFragment.getPresenter());
+ }
+ return locationFragment;
+ }
+
+ @MainThread
+ @Override
+ public void close() {
+ Assert.isMainThread();
+ if (locationFragment != null) {
+ locationHelper.removeLocationListener(locationFragment.getPresenter());
+ locationHelper.close();
+ locationFragment = null;
+ locationHelper = null;
+ }
+ }
+}
diff --git a/java/com/android/incallui/calllocation/impl/CallLocationModule.java b/java/com/android/incallui/calllocation/impl/CallLocationModule.java
new file mode 100644
index 000000000..73e85554e
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/CallLocationModule.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import com.android.incallui.calllocation.CallLocation;
+import dagger.Binds;
+import dagger.Module;
+
+/** This module provides an instance of call location. */
+@Module
+public abstract class CallLocationModule {
+
+ @Binds
+ public abstract CallLocation bindCallLocation(CallLocationImpl callLocation);
+}
diff --git a/java/com/android/incallui/calllocation/impl/DownloadMapImageTask.java b/java/com/android/incallui/calllocation/impl/DownloadMapImageTask.java
new file mode 100644
index 000000000..801b0d35c
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/DownloadMapImageTask.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import android.graphics.drawable.Drawable;
+import android.location.Location;
+import android.net.TrafficStats;
+import android.os.AsyncTask;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.calllocation.impl.LocationPresenter.LocationUi;
+import java.io.InputStream;
+import java.lang.ref.WeakReference;
+import java.net.URL;
+
+class DownloadMapImageTask extends AsyncTask<Location, Void, Drawable> {
+
+ private static final String STATIC_MAP_SRC_NAME = "src";
+
+ private final WeakReference<LocationUi> mUiReference;
+
+ public DownloadMapImageTask(WeakReference<LocationUi> uiReference) {
+ mUiReference = uiReference;
+ }
+
+ @Override
+ protected Drawable doInBackground(Location... locations) {
+ LocationUi ui = mUiReference.get();
+ if (ui == null) {
+ return null;
+ }
+ if (locations == null || locations.length == 0) {
+ LogUtil.e("DownloadMapImageTask.doInBackground", "No location provided");
+ return null;
+ }
+
+ try {
+ URL mapUrl = new URL(LocationUrlBuilder.getStaticMapUrl(ui.getContext(), locations[0]));
+ InputStream content = (InputStream) mapUrl.getContent();
+
+ TrafficStats.setThreadStatsTag(TrafficStatsTags.DOWNLOAD_LOCATION_MAP_TAG);
+ return Drawable.createFromStream(content, STATIC_MAP_SRC_NAME);
+ } catch (Exception ex) {
+ LogUtil.e("DownloadMapImageTask.doInBackground", "Exception!!!", ex);
+ return null;
+ } finally {
+ TrafficStats.clearThreadStatsTag();
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Drawable mapImage) {
+ LocationUi ui = mUiReference.get();
+ if (ui == null) {
+ return;
+ }
+
+ try {
+ ui.setMap(mapImage);
+ } catch (Exception ex) {
+ LogUtil.e("DownloadMapImageTask.onPostExecute", "Exception!!!", ex);
+ }
+ }
+}
diff --git a/java/com/android/incallui/calllocation/impl/GoogleLocationSettingHelper.java b/java/com/android/incallui/calllocation/impl/GoogleLocationSettingHelper.java
new file mode 100644
index 000000000..18a80b8ce
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/GoogleLocationSettingHelper.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.Settings.Secure;
+import android.provider.Settings.SettingNotFoundException;
+import com.android.dialer.common.LogUtil;
+
+/**
+ * Helper class to check if Google Location Services is enabled. This class is based on
+ * https://docs.google.com/a/google.com/document/d/1sGm8pHgGY1QmxbLCwTZuWQASEDN7CFW9EPSZXAuGQfo
+ */
+public class GoogleLocationSettingHelper {
+
+ /** User has disagreed to use location for Google services. */
+ public static final int USE_LOCATION_FOR_SERVICES_OFF = 0;
+ /** User has agreed to use location for Google services. */
+ public static final int USE_LOCATION_FOR_SERVICES_ON = 1;
+ /** The user has neither agreed nor disagreed to use location for Google services yet. */
+ public static final int USE_LOCATION_FOR_SERVICES_NOT_SET = 2;
+
+ private static final String GOOGLE_SETTINGS_AUTHORITY = "com.google.settings";
+ private static final Uri GOOGLE_SETTINGS_CONTENT_URI =
+ Uri.parse("content://" + GOOGLE_SETTINGS_AUTHORITY + "/partner");
+ private static final String NAME = "name";
+ private static final String VALUE = "value";
+ private static final String USE_LOCATION_FOR_SERVICES = "use_location_for_services";
+
+ /** Determine if Google apps need to conform to the USE_LOCATION_FOR_SERVICES setting. */
+ public static boolean isEnforceable(Context context) {
+ final ResolveInfo ri =
+ context
+ .getPackageManager()
+ .resolveActivity(
+ new Intent("com.google.android.gsf.GOOGLE_APPS_LOCATION_SETTINGS"),
+ PackageManager.MATCH_DEFAULT_ONLY);
+ return ri != null;
+ }
+
+ /**
+ * Get the current value for the 'Use value for location' setting.
+ *
+ * @return One of {@link #USE_LOCATION_FOR_SERVICES_NOT_SET}, {@link
+ * #USE_LOCATION_FOR_SERVICES_OFF} or {@link #USE_LOCATION_FOR_SERVICES_ON}.
+ */
+ private static int getUseLocationForServices(Context context) {
+ final ContentResolver resolver = context.getContentResolver();
+ Cursor c = null;
+ String stringValue = null;
+ try {
+ c =
+ resolver.query(
+ GOOGLE_SETTINGS_CONTENT_URI,
+ new String[] {VALUE},
+ NAME + "=?",
+ new String[] {USE_LOCATION_FOR_SERVICES},
+ null);
+ if (c != null && c.moveToNext()) {
+ stringValue = c.getString(0);
+ }
+ } catch (final RuntimeException e) {
+ LogUtil.e(
+ "GoogleLocationSettingHelper.getUseLocationForServices",
+ "Failed to get 'Use My Location' setting",
+ e);
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ if (stringValue == null) {
+ return USE_LOCATION_FOR_SERVICES_NOT_SET;
+ }
+ int value;
+ try {
+ value = Integer.parseInt(stringValue);
+ } catch (final NumberFormatException nfe) {
+ value = USE_LOCATION_FOR_SERVICES_NOT_SET;
+ }
+ return value;
+ }
+
+ /** Whether or not the system location setting is enable */
+ public static boolean isSystemLocationSettingEnabled(Context context) {
+ try {
+ return Secure.getInt(context.getContentResolver(), Secure.LOCATION_MODE)
+ != Secure.LOCATION_MODE_OFF;
+ } catch (SettingNotFoundException e) {
+ LogUtil.e(
+ "GoogleLocationSettingHelper.isSystemLocationSettingEnabled",
+ "Failed to get System Location setting",
+ e);
+ return false;
+ }
+ }
+
+ /** Convenience method that returns true is GLS is ON or if it's not enforceable. */
+ public static boolean isGoogleLocationServicesEnabled(Context context) {
+ return !isEnforceable(context)
+ || getUseLocationForServices(context) == USE_LOCATION_FOR_SERVICES_ON;
+ }
+}
diff --git a/java/com/android/incallui/calllocation/impl/HttpFetcher.java b/java/com/android/incallui/calllocation/impl/HttpFetcher.java
new file mode 100644
index 000000000..7bfbaa6ef
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/HttpFetcher.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import static com.android.dialer.util.DialerUtils.closeQuietly;
+
+import android.content.Context;
+import android.net.Uri;
+import android.net.Uri.Builder;
+import android.os.SystemClock;
+import android.util.Pair;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.util.MoreStrings;
+import com.google.android.common.http.UrlRules;
+import java.io.ByteArrayOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.ProtocolException;
+import java.net.URL;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/** Utility for making http requests. */
+public class HttpFetcher {
+
+ // Phone number
+ public static final String PARAM_ID = "id";
+ // auth token
+ public static final String PARAM_ACCESS_TOKEN = "access_token";
+ private static final String TAG = HttpFetcher.class.getSimpleName();
+
+ /**
+ * Send a http request to the given url.
+ *
+ * @param urlString The url to request.
+ * @return The response body as a byte array. Or {@literal null} if status code is not 2xx.
+ * @throws java.io.IOException when an error occurs.
+ */
+ public static byte[] sendRequestAsByteArray(
+ Context context, String urlString, String requestMethod, List<Pair<String, String>> headers)
+ throws IOException, AuthException {
+ Objects.requireNonNull(urlString);
+
+ URL url = reWriteUrl(context, urlString);
+ if (url == null) {
+ return null;
+ }
+
+ HttpURLConnection conn = null;
+ InputStream is = null;
+ boolean isError = false;
+ final long start = SystemClock.uptimeMillis();
+ try {
+ conn = (HttpURLConnection) url.openConnection();
+ setMethodAndHeaders(conn, requestMethod, headers);
+ int responseCode = conn.getResponseCode();
+ LogUtil.i("HttpFetcher.sendRequestAsByteArray", "response code: " + responseCode);
+ // All 2xx codes are successful.
+ if (responseCode / 100 == 2) {
+ is = conn.getInputStream();
+ } else {
+ is = conn.getErrorStream();
+ isError = true;
+ }
+
+ final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ final byte[] buffer = new byte[1024];
+ int bytesRead;
+
+ while ((bytesRead = is.read(buffer)) != -1) {
+ baos.write(buffer, 0, bytesRead);
+ }
+
+ if (isError) {
+ handleBadResponse(url.toString(), baos.toByteArray());
+ if (responseCode == 401) {
+ throw new AuthException("Auth error");
+ }
+ return null;
+ }
+
+ byte[] response = baos.toByteArray();
+ LogUtil.i("HttpFetcher.sendRequestAsByteArray", "received " + response.length + " bytes");
+ long end = SystemClock.uptimeMillis();
+ LogUtil.i("HttpFetcher.sendRequestAsByteArray", "fetch took " + (end - start) + " ms");
+ return response;
+ } finally {
+ closeQuietly(is);
+ if (conn != null) {
+ conn.disconnect();
+ }
+ }
+ }
+
+ /**
+ * Send a http request to the given url.
+ *
+ * @return The response body as a InputStream. Or {@literal null} if status code is not 2xx.
+ * @throws java.io.IOException when an error occurs.
+ */
+ public static InputStream sendRequestAsInputStream(
+ Context context, String urlString, String requestMethod, List<Pair<String, String>> headers)
+ throws IOException, AuthException {
+ Objects.requireNonNull(urlString);
+
+ URL url = reWriteUrl(context, urlString);
+ if (url == null) {
+ return null;
+ }
+
+ HttpURLConnection httpUrlConnection = null;
+ boolean isSuccess = false;
+ try {
+ httpUrlConnection = (HttpURLConnection) url.openConnection();
+ setMethodAndHeaders(httpUrlConnection, requestMethod, headers);
+ int responseCode = httpUrlConnection.getResponseCode();
+ LogUtil.i("HttpFetcher.sendRequestAsInputStream", "response code: " + responseCode);
+
+ if (responseCode == 401) {
+ throw new AuthException("Auth error");
+ } else if (responseCode / 100 == 2) { // All 2xx codes are successful.
+ InputStream is = httpUrlConnection.getInputStream();
+ if (is != null) {
+ is = new HttpInputStreamWrapper(httpUrlConnection, is);
+ isSuccess = true;
+ return is;
+ }
+ }
+
+ return null;
+ } finally {
+ if (httpUrlConnection != null && !isSuccess) {
+ httpUrlConnection.disconnect();
+ }
+ }
+ }
+
+ /**
+ * Set http method and headers.
+ *
+ * @param conn The connection to add headers to.
+ * @param requestMethod request method
+ * @param headers http headers where the first item in the pair is the key and second item is the
+ * value.
+ */
+ private static void setMethodAndHeaders(
+ HttpURLConnection conn, String requestMethod, List<Pair<String, String>> headers)
+ throws ProtocolException {
+ conn.setRequestMethod(requestMethod);
+ if (headers != null) {
+ for (Pair<String, String> pair : headers) {
+ conn.setRequestProperty(pair.first, pair.second);
+ }
+ }
+ }
+
+ private static String obfuscateUrl(String urlString) {
+ final Uri uri = Uri.parse(urlString);
+ final Builder builder =
+ new Builder().scheme(uri.getScheme()).authority(uri.getAuthority()).path(uri.getPath());
+ final Set<String> names = uri.getQueryParameterNames();
+ for (String name : names) {
+ if (PARAM_ACCESS_TOKEN.equals(name)) {
+ builder.appendQueryParameter(name, "token");
+ } else {
+ final String value = uri.getQueryParameter(name);
+ if (PARAM_ID.equals(name)) {
+ builder.appendQueryParameter(name, MoreStrings.toSafeString(value));
+ } else {
+ builder.appendQueryParameter(name, value);
+ }
+ }
+ }
+ return builder.toString();
+ }
+
+ /** Same as {@link #getRequestAsString(Context, String, String, List)} with null headers. */
+ public static String getRequestAsString(Context context, String urlString)
+ throws IOException, AuthException {
+ return getRequestAsString(context, urlString, "GET" /* Default to get. */, null);
+ }
+
+ /**
+ * Send a http request to the given url.
+ *
+ * @param context The android context.
+ * @param urlString The url to request.
+ * @param headers Http headers to pass in the request. {@literal null} is allowed.
+ * @return The response body as a String. Or {@literal null} if status code is not 2xx.
+ * @throws java.io.IOException when an error occurs.
+ */
+ public static String getRequestAsString(
+ Context context, String urlString, String requestMethod, List<Pair<String, String>> headers)
+ throws IOException, AuthException {
+ final byte[] byteArr = sendRequestAsByteArray(context, urlString, requestMethod, headers);
+ if (byteArr == null) {
+ // Encountered error response... just return.
+ return null;
+ }
+ final String response = new String(byteArr);
+ LogUtil.i("HttpFetcher.getRequestAsString", "response body: " + response);
+ return response;
+ }
+
+ /**
+ * Lookup up url re-write rules from gServices and apply to the given url.
+ *
+ * <p>https://wiki.corp.google.com/twiki/bin/view/Main/AndroidGservices#URL_Rewriting_Rules
+ *
+ * @return The new url.
+ */
+ private static URL reWriteUrl(Context context, String url) {
+ final UrlRules rules = UrlRules.getRules(context.getContentResolver());
+ final UrlRules.Rule rule = rules.matchRule(url);
+ final String newUrl = rule.apply(url);
+
+ if (newUrl == null) {
+ if (LogUtil.isDebugEnabled()) {
+ // Url is blocked by re-write.
+ LogUtil.i(
+ "HttpFetcher.reWriteUrl",
+ "url " + obfuscateUrl(url) + " is blocked. Ignoring request.");
+ }
+ return null;
+ }
+
+ if (LogUtil.isDebugEnabled()) {
+ LogUtil.i("HttpFetcher.reWriteUrl", "fetching " + obfuscateUrl(newUrl));
+ if (!newUrl.equals(url)) {
+ LogUtil.i(
+ "HttpFetcher.reWriteUrl",
+ "Original url: " + obfuscateUrl(url) + ", after re-write: " + obfuscateUrl(newUrl));
+ }
+ }
+
+ URL urlObject = null;
+ try {
+ urlObject = new URL(newUrl);
+ } catch (MalformedURLException e) {
+ LogUtil.e("HttpFetcher.reWriteUrl", "failed to parse url: " + url, e);
+ }
+ return urlObject;
+ }
+
+ private static void handleBadResponse(String url, byte[] response) {
+ LogUtil.i("HttpFetcher.handleBadResponse", "Got bad response code from url: " + url);
+ LogUtil.i("HttpFetcher.handleBadResponse", new String(response));
+ }
+
+ /** Disconnect {@link HttpURLConnection} when InputStream is closed */
+ private static class HttpInputStreamWrapper extends FilterInputStream {
+
+ final HttpURLConnection mHttpUrlConnection;
+ final long mStartMillis = SystemClock.uptimeMillis();
+
+ public HttpInputStreamWrapper(HttpURLConnection conn, InputStream in) {
+ super(in);
+ mHttpUrlConnection = conn;
+ }
+
+ @Override
+ public void close() throws IOException {
+ super.close();
+ mHttpUrlConnection.disconnect();
+ if (LogUtil.isDebugEnabled()) {
+ long endMillis = SystemClock.uptimeMillis();
+ LogUtil.i("HttpFetcher.close", "fetch took " + (endMillis - mStartMillis) + " ms");
+ }
+ }
+ }
+}
diff --git a/java/com/android/incallui/calllocation/impl/LocationFragment.java b/java/com/android/incallui/calllocation/impl/LocationFragment.java
new file mode 100644
index 000000000..b152cd683
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/LocationFragment.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import android.animation.LayoutTransition;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.location.Location;
+import android.os.Bundle;
+import android.os.Handler;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.ViewAnimator;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.baseui.BaseFragment;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Fragment which shows location during E911 calls, to supplement the user with accurate location
+ * information in case the user is asked for their location by the emergency responder.
+ *
+ * <p>If location data is inaccurate, stale, or unavailable, this should not be shown.
+ */
+public class LocationFragment extends BaseFragment<LocationPresenter, LocationPresenter.LocationUi>
+ implements LocationPresenter.LocationUi {
+
+ private static final String ADDRESS_DELIMITER = ",";
+
+ // Indexes used to animate fading between views
+ private static final int LOADING_VIEW_INDEX = 0;
+ private static final int LOCATION_VIEW_INDEX = 1;
+ private static final long TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(5);
+
+ private ViewAnimator viewAnimator;
+ private ImageView locationMap;
+ private TextView addressLine1;
+ private TextView addressLine2;
+ private TextView latLongLine;
+ private Location location;
+ private ViewGroup locationLayout;
+
+ private boolean isMapSet;
+ private boolean isAddressSet;
+ private boolean isLocationSet;
+ private boolean hasTimeoutStarted;
+
+ private final Handler handler = new Handler();
+ private final Runnable dataTimeoutRunnable =
+ () -> {
+ LogUtil.i(
+ "LocationFragment.dataTimeoutRunnable",
+ "timed out so animate any future layout changes");
+ locationLayout.setLayoutTransition(new LayoutTransition());
+ showLocationNow();
+ };
+
+ @Override
+ public LocationPresenter createPresenter() {
+ return new LocationPresenter();
+ }
+
+ @Override
+ public LocationPresenter.LocationUi getUi() {
+ return this;
+ }
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final View view = inflater.inflate(R.layout.location_fragment, container, false);
+ viewAnimator = (ViewAnimator) view.findViewById(R.id.location_view_animator);
+ locationMap = (ImageView) view.findViewById(R.id.location_map);
+ addressLine1 = (TextView) view.findViewById(R.id.address_line_one);
+ addressLine2 = (TextView) view.findViewById(R.id.address_line_two);
+ latLongLine = (TextView) view.findViewById(R.id.lat_long_line);
+ locationLayout = (ViewGroup) view.findViewById(R.id.location_layout);
+ view.setOnClickListener(
+ v -> {
+ LogUtil.enterBlock("LocationFragment.onCreateView");
+ launchMap();
+ });
+ return view;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ handler.removeCallbacks(dataTimeoutRunnable);
+ }
+
+ @Override
+ public void setMap(Drawable mapImage) {
+ LogUtil.enterBlock("LocationFragment.setMap");
+ isMapSet = true;
+ locationMap.setVisibility(View.VISIBLE);
+ locationMap.setImageDrawable(mapImage);
+ displayWhenReady();
+ }
+
+ @Override
+ public void setAddress(String address) {
+ LogUtil.i("LocationFragment.setAddress", address);
+ isAddressSet = true;
+ addressLine1.setVisibility(View.VISIBLE);
+ addressLine2.setVisibility(View.VISIBLE);
+ if (TextUtils.isEmpty(address)) {
+ addressLine1.setText(null);
+ addressLine2.setText(null);
+ } else {
+
+ // Split the address after the first delimiter for display, if present.
+ // For example, "1600 Amphitheatre Parkway, Mountain View, CA 94043"
+ // => "1600 Amphitheatre Parkway"
+ // => "Mountain View, CA 94043"
+ int splitIndex = address.indexOf(ADDRESS_DELIMITER);
+ if (splitIndex >= 0) {
+ updateText(addressLine1, address.substring(0, splitIndex).trim());
+ updateText(addressLine2, address.substring(splitIndex + 1).trim());
+ } else {
+ updateText(addressLine1, address);
+ updateText(addressLine2, null);
+ }
+ }
+ displayWhenReady();
+ }
+
+ @Override
+ public void setLocation(Location location) {
+ LogUtil.i("LocationFragment.setLocation", String.valueOf(location));
+ isLocationSet = true;
+ this.location = location;
+
+ if (location != null) {
+ latLongLine.setVisibility(View.VISIBLE);
+ latLongLine.setText(
+ getContext()
+ .getString(
+ R.string.lat_long_format, location.getLatitude(), location.getLongitude()));
+ }
+ displayWhenReady();
+ }
+
+ private void displayWhenReady() {
+ // Show the location if all data has loaded, otherwise prime the timeout
+ if (isMapSet && isAddressSet && isLocationSet) {
+ showLocationNow();
+ } else if (!hasTimeoutStarted) {
+ handler.postDelayed(dataTimeoutRunnable, TIMEOUT_MILLIS);
+ hasTimeoutStarted = true;
+ }
+ }
+
+ private void showLocationNow() {
+ handler.removeCallbacks(dataTimeoutRunnable);
+ if (viewAnimator.getDisplayedChild() != LOCATION_VIEW_INDEX) {
+ viewAnimator.setDisplayedChild(LOCATION_VIEW_INDEX);
+ }
+ }
+
+ @Override
+ public Context getContext() {
+ return getActivity();
+ }
+
+ private void launchMap() {
+ if (location != null) {
+ startActivity(
+ LocationUrlBuilder.getShowMapIntent(
+ location, addressLine1.getText(), addressLine2.getText()));
+ }
+ }
+
+ private static void updateText(TextView view, String text) {
+ if (!Objects.equals(text, view.getText())) {
+ view.setText(text);
+ }
+ }
+}
diff --git a/java/com/android/incallui/calllocation/impl/LocationHelper.java b/java/com/android/incallui/calllocation/impl/LocationHelper.java
new file mode 100644
index 000000000..645e9b86a
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/LocationHelper.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import android.content.Context;
+import android.location.Location;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.annotation.MainThread;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.util.PermissionsUtil;
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.api.GoogleApiClient;
+import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks;
+import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener;
+import com.google.android.gms.common.api.ResultCallback;
+import com.google.android.gms.common.api.Status;
+import com.google.android.gms.location.LocationListener;
+import com.google.android.gms.location.LocationRequest;
+import com.google.android.gms.location.LocationServices;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Uses the Fused location service to get location and pass updates on to listeners. */
+public class LocationHelper {
+
+ private static final int MIN_UPDATE_INTERVAL_MS = 30 * 1000;
+ private static final int LAST_UPDATE_THRESHOLD_MS = 60 * 1000;
+ private static final int LOCATION_ACCURACY_THRESHOLD_METERS = 100;
+
+ private final LocationHelperInternal locationHelperInternal;
+ private final List<LocationListener> listeners = new ArrayList<>();
+
+ @MainThread
+ LocationHelper(Context context) {
+ Assert.isMainThread();
+ Assert.checkArgument(canGetLocation(context));
+ locationHelperInternal = new LocationHelperInternal(context);
+ }
+
+ static boolean canGetLocation(Context context) {
+ if (!PermissionsUtil.hasLocationPermissions(context)) {
+ LogUtil.i("LocationHelper.canGetLocation", "no location permissions.");
+ return false;
+ }
+
+ // Ensure that both system location setting is on and google location services are enabled.
+ if (!GoogleLocationSettingHelper.isGoogleLocationServicesEnabled(context)
+ || !GoogleLocationSettingHelper.isSystemLocationSettingEnabled(context)) {
+ LogUtil.i("LocationHelper.canGetLocation", "location service is disabled.");
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Whether the location is valid. We consider it valid if it was recorded within the specified
+ * time threshold of the present and has an accuracy less than the specified distance threshold.
+ *
+ * @param location The location to determine the validity of.
+ * @return {@code true} if the location is valid, and {@code false} otherwise.
+ */
+ static boolean isValidLocation(Location location) {
+ if (location != null) {
+ long locationTimeMs = location.getTime();
+ long elapsedTimeMs = System.currentTimeMillis() - locationTimeMs;
+ if (elapsedTimeMs > LAST_UPDATE_THRESHOLD_MS) {
+ LogUtil.i("LocationHelper.isValidLocation", "stale location, age: " + elapsedTimeMs);
+ return false;
+ }
+ if (location.getAccuracy() > LOCATION_ACCURACY_THRESHOLD_METERS) {
+ LogUtil.i("LocationHelper.isValidLocation", "poor accuracy: " + location.getAccuracy());
+ return false;
+ }
+ return true;
+ }
+ LogUtil.i("LocationHelper.isValidLocation", "no location");
+ return false;
+ }
+
+ @MainThread
+ void addLocationListener(LocationListener listener) {
+ Assert.isMainThread();
+ listeners.add(listener);
+ }
+
+ @MainThread
+ void removeLocationListener(LocationListener listener) {
+ Assert.isMainThread();
+ listeners.remove(listener);
+ }
+
+ @MainThread
+ void close() {
+ Assert.isMainThread();
+ LogUtil.enterBlock("LocationHelper.close");
+ listeners.clear();
+
+ if (locationHelperInternal != null) {
+ locationHelperInternal.close();
+ }
+ }
+
+ @MainThread
+ void onLocationChanged(Location location, boolean isConnected) {
+ Assert.isMainThread();
+ LogUtil.i("LocationHelper.onLocationChanged", "location: " + location);
+
+ for (LocationListener listener : listeners) {
+ listener.onLocationChanged(location);
+ }
+ }
+
+ /**
+ * This class contains all the asynchronous callbacks. It only posts location changes back to the
+ * outer class on the main thread.
+ */
+ private class LocationHelperInternal
+ implements ConnectionCallbacks, OnConnectionFailedListener, LocationListener {
+
+ private final GoogleApiClient apiClient;
+ private final ConnectivityManager connectivityManager;
+ private final Handler mainThreadHandler = new Handler();
+
+ @MainThread
+ LocationHelperInternal(Context context) {
+ Assert.isMainThread();
+ apiClient =
+ new GoogleApiClient.Builder(context)
+ .addApi(LocationServices.API)
+ .addConnectionCallbacks(this)
+ .addOnConnectionFailedListener(this)
+ .build();
+
+ LogUtil.i("LocationHelperInternal", "Connecting to location service...");
+ apiClient.connect();
+
+ connectivityManager =
+ (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ }
+
+ void close() {
+ if (apiClient.isConnected()) {
+ LogUtil.i("LocationHelperInternal", "disconnecting");
+ LocationServices.FusedLocationApi.removeLocationUpdates(apiClient, this);
+ apiClient.disconnect();
+ }
+ }
+
+ @Override
+ public void onConnected(Bundle bundle) {
+ LogUtil.enterBlock("LocationHelperInternal.onConnected");
+ LocationRequest locationRequest =
+ LocationRequest.create()
+ .setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY)
+ .setInterval(MIN_UPDATE_INTERVAL_MS)
+ .setFastestInterval(MIN_UPDATE_INTERVAL_MS);
+
+ LocationServices.FusedLocationApi.requestLocationUpdates(apiClient, locationRequest, this)
+ .setResultCallback(
+ new ResultCallback<Status>() {
+ @Override
+ public void onResult(Status status) {
+ if (status.getStatus().isSuccess()) {
+ onLocationChanged(LocationServices.FusedLocationApi.getLastLocation(apiClient));
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onConnectionSuspended(int i) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onConnectionFailed(ConnectionResult result) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onLocationChanged(Location location) {
+ // Post new location on main thread
+ mainThreadHandler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ LocationHelper.this.onLocationChanged(location, isConnected());
+ }
+ });
+ }
+
+ /** @return Whether the phone is connected to data. */
+ private boolean isConnected() {
+ if (connectivityManager == null) {
+ return false;
+ }
+ NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
+ return networkInfo != null && networkInfo.isConnectedOrConnecting();
+ }
+ }
+}
diff --git a/java/com/android/incallui/calllocation/impl/LocationPresenter.java b/java/com/android/incallui/calllocation/impl/LocationPresenter.java
new file mode 100644
index 000000000..a56fd3b3c
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/LocationPresenter.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.location.Location;
+import android.os.AsyncTask;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.baseui.Presenter;
+import com.android.incallui.baseui.Ui;
+import com.google.android.gms.location.LocationListener;
+import java.lang.ref.WeakReference;
+import java.util.Objects;
+
+/**
+ * Presenter for the {@code LocationFragment}.
+ *
+ * <p>Performs lookup for the address and map image to show.
+ */
+public class LocationPresenter extends Presenter<LocationPresenter.LocationUi>
+ implements LocationListener {
+
+ private Location mLastLocation;
+ private AsyncTask mDownloadMapTask;
+ private AsyncTask mReverseGeocodeTask;
+
+ LocationPresenter() {}
+
+ @Override
+ public void onUiReady(LocationUi ui) {
+ LogUtil.i("LocationPresenter.onUiReady", "");
+ super.onUiReady(ui);
+ updateLocation(mLastLocation, true);
+ }
+
+ @Override
+ public void onUiUnready(LocationUi ui) {
+ LogUtil.i("LocationPresenter.onUiUnready", "");
+ super.onUiUnready(ui);
+
+ if (mDownloadMapTask != null) {
+ mDownloadMapTask.cancel(true);
+ }
+ if (mReverseGeocodeTask != null) {
+ mReverseGeocodeTask.cancel(true);
+ }
+ }
+
+ @Override
+ public void onLocationChanged(Location location) {
+ LogUtil.i("LocationPresenter.onLocationChanged", "");
+ updateLocation(location, false);
+ }
+
+ private void updateLocation(Location location, boolean forceUpdate) {
+ LogUtil.i("LocationPresenter.updateLocation", "location: " + location);
+ if (forceUpdate || !Objects.equals(mLastLocation, location)) {
+ mLastLocation = location;
+ if (LocationHelper.isValidLocation(location)) {
+ LocationUi ui = getUi();
+ mDownloadMapTask = new DownloadMapImageTask(new WeakReference<>(ui)).execute(location);
+ mReverseGeocodeTask = new ReverseGeocodeTask(new WeakReference<>(ui)).execute(location);
+ if (ui != null) {
+ ui.setLocation(location);
+ } else {
+ LogUtil.i("LocationPresenter.updateLocation", "no Ui");
+ }
+ }
+ }
+ }
+
+ /** UI interface */
+ public interface LocationUi extends Ui {
+
+ void setAddress(String address);
+
+ void setMap(Drawable mapImage);
+
+ void setLocation(Location location);
+
+ Context getContext();
+ }
+}
diff --git a/java/com/android/incallui/calllocation/impl/LocationUrlBuilder.java b/java/com/android/incallui/calllocation/impl/LocationUrlBuilder.java
new file mode 100644
index 000000000..a57bdf613
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/LocationUrlBuilder.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.location.Location;
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import java.util.Locale;
+
+class LocationUrlBuilder {
+
+ // Static Map API path constants.
+ private static final String HTTPS_SCHEME = "https";
+ private static final String MAPS_API_DOMAIN = "maps.googleapis.com";
+ private static final String MAPS_PATH = "maps";
+ private static final String API_PATH = "api";
+ private static final String STATIC_MAP_PATH = "staticmap";
+ private static final String GEOCODE_PATH = "geocode";
+ private static final String GEOCODE_OUTPUT_TYPE = "json";
+
+ // Static Map API parameter constants.
+ private static final String KEY_PARAM_KEY = "key";
+ private static final String CENTER_PARAM_KEY = "center";
+ private static final String ZOOM_PARAM_KEY = "zoom";
+ private static final String SCALE_PARAM_KEY = "scale";
+ private static final String SIZE_PARAM_KEY = "size";
+ private static final String MARKERS_PARAM_KEY = "markers";
+
+ private static final String ZOOM_PARAM_VALUE = Integer.toString(16);
+
+ private static final String LAT_LONG_DELIMITER = ",";
+
+ private static final String MARKER_DELIMITER = "|";
+ private static final String MARKER_STYLE_DELIMITER = ":";
+ private static final String MARKER_STYLE_COLOR = "color";
+ private static final String MARKER_STYLE_COLOR_RED = "red";
+
+ private static final String LAT_LNG_PARAM_KEY = "latlng";
+
+ private static final String ANDROID_API_KEY_VALUE = "AIzaSyAXdDnif6B7sBYxU8hzw9qAp3pRPVHs060";
+ private static final String BROWSER_API_KEY_VALUE = "AIzaSyBfLlvWYndiQ3RFEHli65qGQH36QIxdyCI";
+
+ /**
+ * Generates the URL to a static map image for the given location.
+ *
+ * <p>This image has the following characteristics:
+ *
+ * <p>- It is centered at the given latitude and longitutde. - It is scaled according to the
+ * device's pixel density. - There is a red marker at the given latitude and longitude.
+ *
+ * <p>Source: https://developers.google.com/maps/documentation/staticmaps/
+ *
+ * @param contxt The context.
+ * @param Location A location.
+ * @return The URL of a static map image url of the given location.
+ */
+ public static String getStaticMapUrl(Context context, Location location) {
+ final Uri.Builder builder = new Uri.Builder();
+ Resources res = context.getResources();
+ String size =
+ res.getDimensionPixelSize(R.dimen.location_map_width)
+ + "x"
+ + res.getDimensionPixelSize(R.dimen.location_map_height);
+
+ builder
+ .scheme(HTTPS_SCHEME)
+ .authority(MAPS_API_DOMAIN)
+ .appendPath(MAPS_PATH)
+ .appendPath(API_PATH)
+ .appendPath(STATIC_MAP_PATH)
+ .appendQueryParameter(CENTER_PARAM_KEY, getFormattedLatLng(location))
+ .appendQueryParameter(ZOOM_PARAM_KEY, ZOOM_PARAM_VALUE)
+ .appendQueryParameter(SIZE_PARAM_KEY, size)
+ .appendQueryParameter(SCALE_PARAM_KEY, Float.toString(res.getDisplayMetrics().density))
+ .appendQueryParameter(MARKERS_PARAM_KEY, getMarkerUrlParamValue(location))
+ .appendQueryParameter(KEY_PARAM_KEY, ANDROID_API_KEY_VALUE);
+
+ return builder.build().toString();
+ }
+
+ /**
+ * Generates the URL for a request to reverse geocode the given location.
+ *
+ * <p>Source: https://developers.google.com/maps/documentation/geocoding/#ReverseGeocoding
+ *
+ * @param Location A location.
+ */
+ public static String getReverseGeocodeUrl(Location location) {
+ final Uri.Builder builder = new Uri.Builder();
+
+ builder
+ .scheme(HTTPS_SCHEME)
+ .authority(MAPS_API_DOMAIN)
+ .appendPath(MAPS_PATH)
+ .appendPath(API_PATH)
+ .appendPath(GEOCODE_PATH)
+ .appendPath(GEOCODE_OUTPUT_TYPE)
+ .appendQueryParameter(LAT_LNG_PARAM_KEY, getFormattedLatLng(location))
+ .appendQueryParameter(KEY_PARAM_KEY, BROWSER_API_KEY_VALUE);
+
+ return builder.build().toString();
+ }
+
+ public static Intent getShowMapIntent(
+ Location location, @Nullable CharSequence addressLine1, @Nullable CharSequence addressLine2) {
+
+ String latLong = getFormattedLatLng(location);
+ String url = String.format(Locale.US, "geo: %s?q=%s", latLong, latLong);
+
+ // Add a map label
+ if (addressLine1 != null) {
+ if (addressLine2 != null) {
+ url +=
+ String.format(Locale.US, "(%s, %s)", addressLine1.toString(), addressLine2.toString());
+ } else {
+ url += String.format(Locale.US, "(%s)", addressLine1.toString());
+ }
+ } else {
+ // TODO: i18n
+ url +=
+ String.format(
+ Locale.US,
+ "(Latitude: %f, Longitude: %f)",
+ location.getLatitude(),
+ location.getLongitude());
+ }
+
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+ intent.setPackage("com.google.android.apps.maps");
+ return intent;
+ }
+
+ /**
+ * Returns a comma-separated latitude and longitude pair, formatted for use as a URL parameter
+ * value.
+ *
+ * @param location A location.
+ * @return The comma-separated latitude and longitude pair of that location.
+ */
+ @VisibleForTesting
+ static String getFormattedLatLng(Location location) {
+ return location.getLatitude() + LAT_LONG_DELIMITER + location.getLongitude();
+ }
+
+ /**
+ * Returns the URL parameter value for the marker, specifying its style and position.
+ *
+ * @param location A location.
+ * @return The URL parameter value for the marker.
+ */
+ @VisibleForTesting
+ static String getMarkerUrlParamValue(Location location) {
+ return MARKER_STYLE_COLOR
+ + MARKER_STYLE_DELIMITER
+ + MARKER_STYLE_COLOR_RED
+ + MARKER_DELIMITER
+ + getFormattedLatLng(location);
+ }
+}
diff --git a/java/com/android/incallui/calllocation/impl/ReverseGeocodeTask.java b/java/com/android/incallui/calllocation/impl/ReverseGeocodeTask.java
new file mode 100644
index 000000000..eb5957b05
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/ReverseGeocodeTask.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import android.location.Location;
+import android.net.TrafficStats;
+import android.os.AsyncTask;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.calllocation.impl.LocationPresenter.LocationUi;
+import java.lang.ref.WeakReference;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+class ReverseGeocodeTask extends AsyncTask<Location, Void, String> {
+
+ // Below are the JSON keys for the reverse geocode response.
+ // Source: https://developers.google.com/maps/documentation/geocoding/#ReverseGeocoding
+ private static final String JSON_KEY_RESULTS = "results";
+ private static final String JSON_KEY_ADDRESS = "formatted_address";
+ private static final String JSON_KEY_ADDRESS_COMPONENTS = "address_components";
+ private static final String JSON_KEY_PREMISE = "premise";
+ private static final String JSON_KEY_TYPES = "types";
+ private static final String JSON_KEY_LONG_NAME = "long_name";
+ private static final String JSON_KEY_SHORT_NAME = "short_name";
+
+ private WeakReference<LocationUi> mUiReference;
+
+ public ReverseGeocodeTask(WeakReference<LocationUi> uiReference) {
+ mUiReference = uiReference;
+ }
+
+ @Override
+ protected String doInBackground(Location... locations) {
+ LocationUi ui = mUiReference.get();
+ if (ui == null) {
+ return null;
+ }
+ if (locations == null || locations.length == 0) {
+ LogUtil.e("ReverseGeocodeTask.onLocationChanged", "No location provided");
+ return null;
+ }
+
+ try {
+ String address = null;
+ String url = LocationUrlBuilder.getReverseGeocodeUrl(locations[0]);
+
+ TrafficStats.setThreadStatsTag(TrafficStatsTags.REVERSE_GEOCODE_TAG);
+ String jsonResponse = HttpFetcher.getRequestAsString(ui.getContext(), url);
+
+ // Parse the JSON response for the formatted address of the first result.
+ JSONObject responseObject = new JSONObject(jsonResponse);
+ if (responseObject != null) {
+ JSONArray results = responseObject.optJSONArray(JSON_KEY_RESULTS);
+ if (results != null && results.length() > 0) {
+ JSONObject topResult = results.optJSONObject(0);
+ if (topResult != null) {
+ address = topResult.getString(JSON_KEY_ADDRESS);
+
+ // Strip off the Premise component from the address, if present.
+ JSONArray components = topResult.optJSONArray(JSON_KEY_ADDRESS_COMPONENTS);
+ if (components != null) {
+ boolean stripped = false;
+ for (int i = 0; !stripped && i < components.length(); i++) {
+ JSONObject component = components.optJSONObject(i);
+ JSONArray types = component.optJSONArray(JSON_KEY_TYPES);
+ if (types != null) {
+ for (int j = 0; !stripped && j < types.length(); j++) {
+ if (JSON_KEY_PREMISE.equals(types.getString(j))) {
+ String premise = null;
+ if (component.has(JSON_KEY_SHORT_NAME)
+ && address.startsWith(component.getString(JSON_KEY_SHORT_NAME))) {
+ premise = component.getString(JSON_KEY_SHORT_NAME);
+ } else if (component.has(JSON_KEY_LONG_NAME)
+ && address.startsWith(component.getString(JSON_KEY_LONG_NAME))) {
+ premise = component.getString(JSON_KEY_SHORT_NAME);
+ }
+ if (premise != null) {
+ int index = address.indexOf(',', premise.length());
+ if (index > 0 && index < address.length()) {
+ address = address.substring(index + 1).trim();
+ }
+ stripped = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Strip off the country, if its USA. Note: unfortunately the country in the formatted
+ // address field doesn't match the country in the address component fields (USA != US)
+ // so we can't easily strip off the country for all cases, thus this hack.
+ if (address.endsWith(", USA")) {
+ address = address.substring(0, address.length() - 5);
+ }
+ }
+ }
+ }
+
+ return address;
+ } catch (AuthException ex) {
+ LogUtil.e("ReverseGeocodeTask.onLocationChanged", "AuthException", ex);
+ return null;
+ } catch (JSONException ex) {
+ LogUtil.e("ReverseGeocodeTask.onLocationChanged", "JSONException", ex);
+ return null;
+ } catch (Exception ex) {
+ LogUtil.e("ReverseGeocodeTask.onLocationChanged", "Exception!!!", ex);
+ return null;
+ } finally {
+ TrafficStats.clearThreadStatsTag();
+ }
+ }
+
+ @Override
+ protected void onPostExecute(String address) {
+ LocationUi ui = mUiReference.get();
+ if (ui == null) {
+ return;
+ }
+
+ try {
+ ui.setAddress(address);
+ } catch (Exception ex) {
+ LogUtil.e("ReverseGeocodeTask.onPostExecute", "Exception!!!", ex);
+ }
+ }
+}
diff --git a/java/com/android/incallui/calllocation/impl/TrafficStatsTags.java b/java/com/android/incallui/calllocation/impl/TrafficStatsTags.java
new file mode 100644
index 000000000..02cc2e083
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/TrafficStatsTags.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+/** Constants used for logging */
+public class TrafficStatsTags {
+
+ /**
+ * Must be greater than {@link com.android.contacts.common.util.TrafficStatsTags#TAG_MAX}, to
+ * respect the namespace of the tags in ContactsCommon.
+ */
+ public static final int DOWNLOAD_LOCATION_MAP_TAG = 0xd000;
+
+ public static final int REVERSE_GEOCODE_TAG = 0xd001;
+}
diff --git a/java/com/android/incallui/calllocation/impl/res/layout/location_fragment.xml b/java/com/android/incallui/calllocation/impl/res/layout/location_fragment.xml
new file mode 100644
index 000000000..a6bd07542
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/res/layout/location_fragment.xml
@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+~ Copyright (C) 2015 The Android Open Source Project
+~
+~ Licensed under the Apache License, Version 2.0 (the "License");
+~ you may not use this file except in compliance with the License.
+~ You may obtain a copy of the License at
+~
+~ http://www.apache.org/licenses/LICENSE-2.0
+~
+~ Unless required by applicable law or agreed to in writing, software
+~ distributed under the License is distributed on an "AS IS" BASIS,
+~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+~ See the License for the specific language governing permissions and
+~ limitations under the License
+-->
+
+<ViewAnimator xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/location_view_animator"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:layout_marginBottom="16dp"
+ android:background="@android:color/white"
+ android:elevation="2dp"
+ android:inAnimation="@android:anim/fade_in"
+ android:measureAllChildren="true"
+ android:outAnimation="@android:anim/fade_out">
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/location_loading_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:orientation="vertical">
+
+ <ProgressBar
+ android:id="@+id/location_loading_spinner"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="28dp"
+ android:layout_marginBottom="12dp"
+ android:layout_gravity="center_horizontal"/>
+
+ <TextView
+ android:id="@+id/location_loading_text"
+ style="@style/LocationLoadingTextStyle"
+ android:layout_width="match_parent"
+ android:layout_height="24sp"
+ android:layout_marginBottom="20dp"
+ android:layout_marginStart="24dp"
+ android:layout_marginEnd="24dp"
+ android:gravity="center"
+ android:text="@string/location_loading"/>
+
+ </LinearLayout>
+
+ <GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/location_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:columnCount="2"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/location_address_title"
+ style="@style/LocationAddressTitleTextStyle"
+ android:layout_width="0dp"
+ android:layout_height="20sp"
+ android:layout_marginTop="16dp"
+ android:layout_marginBottom="4dp"
+ android:layout_marginStart="16dp"
+ android:layout_columnWeight="1"
+ android:text="@string/location_title"/>
+
+ <ImageView
+ android:id="@+id/location_map"
+ android:layout_width="@dimen/location_map_width"
+ android:layout_height="@dimen/location_map_height"
+ android:layout_margin="16dp"
+ android:layout_gravity="end|center_vertical"
+ android:layout_rowSpan="4"
+ android:contentDescription="@string/location_map_description"
+ android:scaleType="centerCrop"
+ android:visibility="invisible"
+ tools:src="?android:colorPrimaryDark"
+ tools:visibility="visible"/>
+
+ <TextView
+ android:id="@+id/address_line_one"
+ style="@style/LocationAddressTextStyle"
+ android:layout_width="0dp"
+ android:layout_height="24sp"
+ android:layout_marginStart="16dp"
+ android:layout_columnWeight="1"
+ android:ellipsize="end"
+ android:lines="1"
+ android:visibility="invisible"
+ tools:text="1600 Amphitheatre Pkwy And a bit"
+ tools:visibility="visible"/>
+
+ <TextView
+ android:id="@+id/address_line_two"
+ style="@style/LocationAddressTextStyle"
+ android:layout_width="0dp"
+ android:layout_height="24sp"
+ android:layout_marginStart="16dp"
+ android:layout_columnWeight="1"
+ android:ellipsize="end"
+ android:lines="1"
+ android:visibility="invisible"
+ tools:text="Mountain View, CA 94043"
+ tools:visibility="visible"/>
+
+ <TextView
+ android:id="@+id/lat_long_line"
+ style="@style/LocationLatLongTextStyle"
+ android:layout_width="0dp"
+ android:layout_height="24sp"
+ android:layout_marginBottom="12dp"
+ android:layout_marginStart="16dp"
+ android:layout_columnWeight="1"
+ android:ellipsize="end"
+ android:lines="1"
+ android:visibility="invisible"
+ tools:text="Lat: 37.421719, Long: -122.085297"
+ tools:visibility="visible"/>
+
+ </GridLayout>
+
+</ViewAnimator>
diff --git a/java/com/android/incallui/calllocation/impl/res/values/dimens.xml b/java/com/android/incallui/calllocation/impl/res/values/dimens.xml
new file mode 100644
index 000000000..1f4181607
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/res/values/dimens.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2014 Google Inc. All Rights Reserved. -->
+<resources>
+ <dimen name="location_map_width">92dp</dimen>
+ <dimen name="location_map_height">92dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/calllocation/impl/res/values/strings.xml b/java/com/android/incallui/calllocation/impl/res/values/strings.xml
new file mode 100644
index 000000000..ef7c1624c
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/res/values/strings.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+ <!-- Description for location map shown during emergency calls. [CHAR LIMIT=NONE] -->
+ <string name="location_map_description">Emergency Location Map</string>
+
+ <!-- Label for the address and map shown during emergency calls. [CHAR LIMIT=20] -->
+ <string name="location_title">You are here</string>
+
+ <string name="lat_long_format"><xliff:g id="latitude">%f</xliff:g>, <xliff:g id="longitude">%f</xliff:g></string>
+
+ <!-- Progress indicator loading text. [CHAR LIMIT=20] -->
+ <string name="location_loading">Finding your location</string>
+
+</resources>
diff --git a/java/com/android/incallui/calllocation/impl/res/values/styles.xml b/java/com/android/incallui/calllocation/impl/res/values/styles.xml
new file mode 100644
index 000000000..866a4edb6
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/res/values/styles.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2015 Google Inc. All Rights Reserved. -->
+<resources>
+
+ <style name="LocationAddressTitleTextStyle">
+ <item name="android:textSize">14sp</item>
+ <item name="android:textColor">#dd000000</item>
+ <item name="android:fontFamily">sans-serif-medium</item>
+ </style>
+
+ <style name="LocationAddressTextStyle">
+ <item name="android:textSize">16sp</item>
+ <item name="android:textColor">#dd000000</item>
+ <item name="android:fontFamily">sans-serif</item>
+ </style>
+
+ <style name="LocationLatLongTextStyle">
+ <item name="android:textSize">14sp</item>
+ <item name="android:textColor">#88000000</item>
+ <item name="android:fontFamily">sans-serif</item>
+ </style>
+
+ <style name="LocationLoadingTextStyle">
+ <item name="android:textSize">14sp</item>
+ <item name="android:textColor">#dd000000</item>
+ <item name="android:fontFamily">sans-serif</item>
+ </style>
+</resources>
diff --git a/java/com/android/incallui/calllocation/stub/StubCallLocationModule.java b/java/com/android/incallui/calllocation/stub/StubCallLocationModule.java
new file mode 100644
index 000000000..fc198c724
--- /dev/null
+++ b/java/com/android/incallui/calllocation/stub/StubCallLocationModule.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.stub;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.v4.app.Fragment;
+import com.android.dialer.common.Assert;
+import com.android.incallui.calllocation.CallLocation;
+import dagger.Binds;
+import dagger.Module;
+import javax.inject.Inject;
+
+/** This module provides an instance of call location. */
+@Module
+public abstract class StubCallLocationModule {
+
+ @Binds
+ public abstract CallLocation bindCallLocation(StubCallLocation callLocation);
+
+ static public class StubCallLocation implements CallLocation {
+ @Inject
+ public StubCallLocation() {}
+
+ @Override
+ public boolean canGetLocation(@NonNull Context context) {
+ return false;
+ }
+
+ @Override
+ @NonNull
+ public Fragment getLocationFragment(@NonNull Context context) {
+ return null;
+ }
+
+ @Override
+ public void close() {
+ }
+ }
+}
diff --git a/java/com/android/incallui/commontheme/res/anim/blinking.xml b/java/com/android/incallui/commontheme/res/anim/blinking.xml
new file mode 100644
index 000000000..aaec18c56
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/anim/blinking.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <alpha
+ android:duration="@android:integer/config_mediumAnimTime"
+ android:fromAlpha="0.0"
+ android:interpolator="@android:anim/linear_interpolator"
+ android:repeatCount="infinite"
+ android:repeatMode="reverse"
+ android:toAlpha="1.0"/>
+</set> \ No newline at end of file
diff --git a/java/com/android/incallui/contactgrid/BottomRow.java b/java/com/android/incallui/contactgrid/BottomRow.java
index aaf7e8214..6ddce4533 100644
--- a/java/com/android/incallui/contactgrid/BottomRow.java
+++ b/java/com/android/incallui/contactgrid/BottomRow.java
@@ -31,10 +31,10 @@ import com.android.incallui.incall.protocol.PrimaryInfo;
* Gets the content of the bottom row. For example:
*
* <ul>
- * <li>Mobile +1 (650) 253-0000
- * <li>[HD icon] 00:15
- * <li>Call ended
- * <li>Hanging up
+ * <li>Mobile +1 (650) 253-0000
+ * <li>[HD attempting icon]/[HD icon] 00:15
+ * <li>Call ended
+ * <li>Hanging up
* </ul>
*/
public class BottomRow {
@@ -45,6 +45,7 @@ public class BottomRow {
@Nullable public final CharSequence label;
public final boolean isTimerVisible;
public final boolean isWorkIconVisible;
+ public final boolean isHdAttemptinIconVisible;
public final boolean isHdIconVisible;
public final boolean isForwardIconVisible;
public final boolean isSpamIconVisible;
@@ -54,6 +55,7 @@ public class BottomRow {
@Nullable CharSequence label,
boolean isTimerVisible,
boolean isWorkIconVisible,
+ boolean isHdAttemptinIconVisible,
boolean isHdIconVisible,
boolean isForwardIconVisible,
boolean isSpamIconVisible,
@@ -61,6 +63,7 @@ public class BottomRow {
this.label = label;
this.isTimerVisible = isTimerVisible;
this.isWorkIconVisible = isWorkIconVisible;
+ this.isHdAttemptinIconVisible = isHdAttemptinIconVisible;
this.isHdIconVisible = isHdIconVisible;
this.isForwardIconVisible = isForwardIconVisible;
this.isSpamIconVisible = isSpamIconVisible;
@@ -76,6 +79,7 @@ public class BottomRow {
boolean isForwardIconVisible = state.isForwardedNumber;
boolean isWorkIconVisible = state.isWorkCall;
boolean isHdIconVisible = state.isHdAudioCall && !isForwardIconVisible;
+ boolean isHdAttemptingIconVisible = state.isHdAttempting;
boolean isSpamIconVisible = false;
boolean shouldPopulateAccessibilityEvent = true;
@@ -110,6 +114,7 @@ public class BottomRow {
label,
isTimerVisible,
isWorkIconVisible,
+ isHdAttemptingIconVisible,
isHdIconVisible,
isForwardIconVisible,
isSpamIconVisible,
diff --git a/java/com/android/incallui/contactgrid/ContactGridManager.java b/java/com/android/incallui/contactgrid/ContactGridManager.java
index 81c225163..a0b687c2d 100644
--- a/java/com/android/incallui/contactgrid/ContactGridManager.java
+++ b/java/com/android/incallui/contactgrid/ContactGridManager.java
@@ -18,10 +18,14 @@ package com.android.incallui.contactgrid;
import android.content.Context;
import android.os.SystemClock;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.telecom.TelecomManager;
import android.text.TextUtils;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
import android.widget.Chronometer;
import android.widget.ImageView;
import android.widget.TextView;
@@ -56,7 +60,7 @@ public class ContactGridManager {
@Nullable private ImageView avatarImageView;
// Row 2: Mobile +1 (650) 253-0000
- // Row 2: [HD icon] 00:15
+ // Row 2: [HD attempting icon]/[HD icon] 00:15
// Row 2: Call ended
// Row 2: Hanging up
// Row 2: [Alert sign] Suspected spam caller
@@ -77,7 +81,6 @@ public class ContactGridManager {
private PrimaryCallState primaryCallState = PrimaryCallState.createEmptyPrimaryCallState();
private final LetterTileDrawable letterTile;
-
public ContactGridManager(
View view, @Nullable ImageView avatarImageView, int avatarSize, boolean showAnonymousAvatar) {
context = view.getContext();
@@ -227,6 +230,24 @@ public class ContactGridManager {
}
/**
+ * Returns the appropriate LetterTileDrawable.TYPE_ based on a given call state.
+ *
+ * <p>If no special state is detected, yields TYPE_DEFAULT.
+ */
+ private static @LetterTileDrawable.ContactType int getContactTypeForPrimaryCallState(
+ @NonNull PrimaryCallState callState, @NonNull PrimaryInfo primaryInfo) {
+ if (callState.isVoiceMailNumber) {
+ return LetterTileDrawable.TYPE_VOICEMAIL;
+ } else if (callState.isBusinessNumber) {
+ return LetterTileDrawable.TYPE_BUSINESS;
+ } else if (primaryInfo.numberPresentation == TelecomManager.PRESENTATION_RESTRICTED) {
+ return LetterTileDrawable.TYPE_GENERIC_AVATAR;
+ } else {
+ return LetterTileDrawable.TYPE_DEFAULT;
+ }
+ }
+
+ /**
* Updates row 1. For example:
*
* <ul>
@@ -255,7 +276,7 @@ public class ContactGridManager {
if (avatarImageView != null) {
if (hideAvatar) {
avatarImageView.setVisibility(View.GONE);
- } else if (avatarImageView != null && avatarSize > 0 && updateAvatarVisibility()) {
+ } else if (avatarSize > 0 && updateAvatarVisibility()) {
boolean hasPhoto =
primaryInfo.photo != null && primaryInfo.photoType == ContactPhotoType.CONTACT;
// Contact has a photo, don't render a letter tile.
@@ -265,27 +286,29 @@ public class ContactGridManager {
context, primaryInfo.photo, avatarSize, avatarSize));
// Contact has a name, that isn't a number.
} else {
- int contactType =
- primaryCallState.isVoiceMailNumber
- ? LetterTileDrawable.TYPE_VOICEMAIL
- : LetterTileDrawable.TYPE_DEFAULT;
letterTile.setCanonicalDialerLetterTileDetails(
primaryInfo.name,
primaryInfo.contactInfoLookupKey,
LetterTileDrawable.SHAPE_CIRCLE,
- contactType);
+ getContactTypeForPrimaryCallState(primaryCallState, primaryInfo));
+
+ // By invalidating the avatarImageView we force a redraw of the letter tile.
+ // This is required to properly display the updated letter tile iconography based on the
+ // contact type, because the background drawable reference cached in the view, and the
+ // view is not aware of the mutations made to the background.
+ avatarImageView.invalidate();
avatarImageView.setBackground(letterTile);
+ }
}
}
}
- }
/**
* Updates row 2. For example:
*
* <ul>
* <li>Mobile +1 (650) 253-0000
- * <li>[HD icon] 00:15
+ * <li>[HD attempting icon]/[HD icon] 00:15
* <li>Call ended
* <li>Hanging up
* </ul>
@@ -296,7 +319,15 @@ public class ContactGridManager {
bottomTextView.setText(info.label);
bottomTextView.setAllCaps(info.isSpamIconVisible);
workIconImageView.setVisibility(info.isWorkIconVisible ? View.VISIBLE : View.GONE);
- hdIconImageView.setVisibility(info.isHdIconVisible ? View.VISIBLE : View.GONE);
+ boolean wasHdIconVisible = hdIconImageView.getVisibility() == View.VISIBLE;
+ if (!wasHdIconVisible && info.isHdAttemptinIconVisible) {
+ Animation animation = AnimationUtils.loadAnimation(context, R.anim.blinking);
+ hdIconImageView.startAnimation(animation);
+ } else if (wasHdIconVisible && !info.isHdAttemptinIconVisible) {
+ hdIconImageView.clearAnimation();
+ }
+ hdIconImageView.setVisibility(
+ info.isHdIconVisible || info.isHdAttemptinIconVisible ? View.VISIBLE : View.GONE);
forwardIconImageView.setVisibility(info.isForwardIconVisible ? View.VISIBLE : View.GONE);
spamIconImageView.setVisibility(info.isSpamIconVisible ? View.VISIBLE : View.GONE);
diff --git a/java/com/android/incallui/contactgrid/TopRow.java b/java/com/android/incallui/contactgrid/TopRow.java
index a340fd0a0..ecd5eea64 100644
--- a/java/com/android/incallui/contactgrid/TopRow.java
+++ b/java/com/android/incallui/contactgrid/TopRow.java
@@ -21,10 +21,10 @@ import android.graphics.drawable.Drawable;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.android.dialer.common.Assert;
-import com.android.incallui.call.DialerCall;
import com.android.incallui.call.DialerCall.State;
import com.android.incallui.call.VideoUtils;
import com.android.incallui.incall.protocol.PrimaryCallState;
+import com.android.incallui.videotech.VideoTech;
/**
* Gets the content of the top row. For example:
@@ -95,7 +95,7 @@ public class TopRow {
}
private static CharSequence getLabelForIncoming(Context context, PrimaryCallState state) {
- if (VideoUtils.isVideoCall(state.videoState)) {
+ if (state.isVideoCall) {
return getLabelForIncomingVideo(context, state.isWifi);
} else if (state.isWifi && !TextUtils.isEmpty(state.connectionLabel)) {
return state.connectionLabel;
@@ -120,7 +120,7 @@ public class TopRow {
if (!TextUtils.isEmpty(state.connectionLabel) && !state.isWifi) {
return context.getString(R.string.incall_calling_via_template, state.connectionLabel);
} else {
- if (VideoUtils.isVideoCall(state.videoState)) {
+ if (state.isVideoCall) {
if (state.isWifi) {
return context.getString(R.string.incall_wifi_video_call_requesting);
} else {
@@ -144,18 +144,18 @@ public class TopRow {
private static CharSequence getLabelForVideoRequest(Context context, PrimaryCallState state) {
switch (state.sessionModificationState) {
- case DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE:
+ case VideoTech.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE:
return context.getString(R.string.incall_video_call_requesting);
- case DialerCall.SESSION_MODIFICATION_STATE_REQUEST_FAILED:
- case DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED:
+ case VideoTech.SESSION_MODIFICATION_STATE_REQUEST_FAILED:
+ case VideoTech.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED:
return context.getString(R.string.incall_video_call_request_failed);
- case DialerCall.SESSION_MODIFICATION_STATE_REQUEST_REJECTED:
+ case VideoTech.SESSION_MODIFICATION_STATE_REQUEST_REJECTED:
return context.getString(R.string.incall_video_call_request_rejected);
- case DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT:
+ case VideoTech.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT:
return context.getString(R.string.incall_video_call_request_timed_out);
- case DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST:
+ case VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST:
return getLabelForIncomingVideo(context, state.isWifi);
- case DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST:
+ case VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST:
default:
Assert.fail();
return null;
diff --git a/java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_bottom_row.xml b/java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_bottom_row.xml
index 3900be556..b7a3fe7d4 100644
--- a/java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_bottom_row.xml
+++ b/java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_bottom_row.xml
@@ -4,8 +4,8 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:orientation="horizontal"
android:gravity="center_horizontal"
+ android:orientation="horizontal"
tools:showIn="@layout/incall_contact_grid">
<ImageView
android:id="@id/contactgrid_workIcon"
diff --git a/java/com/android/incallui/hold/res/layout/incall_on_hold_banner.xml b/java/com/android/incallui/hold/res/layout/incall_on_hold_banner.xml
index c213af5da..6128ae585 100644
--- a/java/com/android/incallui/hold/res/layout/incall_on_hold_banner.xml
+++ b/java/com/android/incallui/hold/res/layout/incall_on_hold_banner.xml
@@ -39,7 +39,6 @@
style="@style/Dialer.Incall.TextAppearance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:textAllCaps="true"
android:textColor="@android:color/white"
android:text="@string/incall_on_hold"/>
</LinearLayout>
diff --git a/java/com/android/incallui/incall/impl/AutoValue_MappedButtonConfig_MappingInfo.java b/java/com/android/incallui/incall/impl/AutoValue_MappedButtonConfig_MappingInfo.java
deleted file mode 100644
index addebc484..000000000
--- a/java/com/android/incallui/incall/impl/AutoValue_MappedButtonConfig_MappingInfo.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.incallui.incall.impl;
-
-import javax.annotation.Generated;
-
-@Generated("com.google.auto.value.processor.AutoValueProcessor")
- final class AutoValue_MappedButtonConfig_MappingInfo extends MappedButtonConfig.MappingInfo {
-
- private final int slot;
- private final int slotOrder;
- private final int conflictOrder;
-
- private AutoValue_MappedButtonConfig_MappingInfo(
- int slot,
- int slotOrder,
- int conflictOrder) {
- this.slot = slot;
- this.slotOrder = slotOrder;
- this.conflictOrder = conflictOrder;
- }
-
- @Override
- public int getSlot() {
- return slot;
- }
-
- @Override
- public int getSlotOrder() {
- return slotOrder;
- }
-
- @Override
- public int getConflictOrder() {
- return conflictOrder;
- }
-
- @Override
- public String toString() {
- return "MappingInfo{"
- + "slot=" + slot + ", "
- + "slotOrder=" + slotOrder + ", "
- + "conflictOrder=" + conflictOrder
- + "}";
- }
-
- @Override
- public boolean equals(Object o) {
- if (o == this) {
- return true;
- }
- if (o instanceof MappedButtonConfig.MappingInfo) {
- MappedButtonConfig.MappingInfo that = (MappedButtonConfig.MappingInfo) o;
- return (this.slot == that.getSlot())
- && (this.slotOrder == that.getSlotOrder())
- && (this.conflictOrder == that.getConflictOrder());
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- int h = 1;
- h *= 1000003;
- h ^= this.slot;
- h *= 1000003;
- h ^= this.slotOrder;
- h *= 1000003;
- h ^= this.conflictOrder;
- return h;
- }
-
- static final class Builder extends MappedButtonConfig.MappingInfo.Builder {
- private Integer slot;
- private Integer slotOrder;
- private Integer conflictOrder;
- Builder() {
- }
- private Builder(MappedButtonConfig.MappingInfo source) {
- this.slot = source.getSlot();
- this.slotOrder = source.getSlotOrder();
- this.conflictOrder = source.getConflictOrder();
- }
- @Override
- public MappedButtonConfig.MappingInfo.Builder setSlot(int slot) {
- this.slot = slot;
- return this;
- }
- @Override
- public MappedButtonConfig.MappingInfo.Builder setSlotOrder(int slotOrder) {
- this.slotOrder = slotOrder;
- return this;
- }
- @Override
- public MappedButtonConfig.MappingInfo.Builder setConflictOrder(int conflictOrder) {
- this.conflictOrder = conflictOrder;
- return this;
- }
- @Override
- public MappedButtonConfig.MappingInfo build() {
- String missing = "";
- if (this.slot == null) {
- missing += " slot";
- }
- if (this.slotOrder == null) {
- missing += " slotOrder";
- }
- if (this.conflictOrder == null) {
- missing += " conflictOrder";
- }
- if (!missing.isEmpty()) {
- throw new IllegalStateException("Missing required properties:" + missing);
- }
- return new AutoValue_MappedButtonConfig_MappingInfo(
- this.slot,
- this.slotOrder,
- this.conflictOrder);
- }
- }
-
-}
diff --git a/java/com/android/incallui/incall/impl/FakeDragAnimation.java b/java/com/android/incallui/incall/impl/FakeDragAnimation.java
new file mode 100644
index 000000000..c84c3c409
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/FakeDragAnimation.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.incall.impl;
+
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.support.v4.view.ViewPager;
+import android.support.v4.view.animation.FastOutSlowInInterpolator;
+
+/**
+ * An animation that controls the fake drag of a {@link ViewPager}. See {@link
+ * ViewPager#fakeDragBy(float)} for more details.
+ */
+public class FakeDragAnimation implements AnimatorUpdateListener {
+
+ /** The view to animate. */
+ private final ViewPager pager;
+
+ private final ValueAnimator animator;
+ private int oldDragPosition;
+
+ public FakeDragAnimation(ViewPager pager) {
+ this.pager = pager;
+ animator = ValueAnimator.ofInt(0, pager.getWidth());
+ animator.addUpdateListener(this);
+ animator.setInterpolator(new FastOutSlowInInterpolator());
+ animator.setDuration(600);
+ }
+
+ public void start() {
+ animator.start();
+ }
+
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ if (!pager.isFakeDragging()) {
+ pager.beginFakeDrag();
+ }
+ int dragPosition = (Integer) animation.getAnimatedValue();
+ int dragOffset = dragPosition - oldDragPosition;
+ oldDragPosition = dragPosition;
+ pager.fakeDragBy(-dragOffset);
+
+ if (animation.getAnimatedFraction() == 1) {
+ pager.endFakeDrag();
+ }
+ }
+}
diff --git a/java/com/android/incallui/incall/impl/InCallFragment.java b/java/com/android/incallui/incall/impl/InCallFragment.java
index ef8a1edd8..3f31651a0 100644
--- a/java/com/android/incallui/incall/impl/InCallFragment.java
+++ b/java/com/android/incallui/incall/impl/InCallFragment.java
@@ -213,9 +213,7 @@ public class InCallFragment extends Fragment
@Override
public void setPrimary(@NonNull PrimaryInfo primaryInfo) {
LogUtil.i("InCallFragment.setPrimary", primaryInfo.toString());
- if (adapter == null) {
- initAdapter(primaryInfo.multimediaData);
- }
+ setAdapterMedia(primaryInfo.multimediaData);
contactGridManager.setPrimary(primaryInfo);
if (primaryInfo.shouldShowLocation) {
@@ -241,9 +239,13 @@ public class InCallFragment extends Fragment
}
}
- private void initAdapter(MultimediaData multimediaData) {
- adapter = new InCallPagerAdapter(getChildFragmentManager(), multimediaData);
- pager.setAdapter(adapter);
+ private void setAdapterMedia(MultimediaData multimediaData) {
+ if (adapter == null) {
+ adapter = new InCallPagerAdapter(getChildFragmentManager(), multimediaData);
+ pager.setAdapter(adapter);
+ } else {
+ adapter.setAttachments(multimediaData);
+ }
if (adapter.getCount() > 1) {
tabLayout.setVisibility(pager.getVisibility());
@@ -251,16 +253,13 @@ public class InCallFragment extends Fragment
if (!stateRestored) {
new Handler()
.postDelayed(
- new Runnable() {
- @Override
- public void run() {
- // In order to prevent user confusion and educate the user on our UI, we animate
- // the view pager to the button grid after 2 seconds show them when the UI is
- // that they are more familiar with.
- pager.setCurrentItem(adapter.getButtonGridPosition());
- }
+ () -> {
+ // In order to prevent user confusion and educate the user on our UI, we animate
+ // the view pager to the button grid after a short period to show them where the
+ // UI that they are more familiar with is located.
+ new FakeDragAnimation(pager).start();
},
- 2000);
+ 333);
}
} else {
tabLayout.setVisibility(View.GONE);
@@ -479,23 +478,39 @@ public class InCallFragment extends Fragment
@Override
public boolean isShowingLocationUi() {
- Fragment fragment = getChildFragmentManager().findFragmentById(R.id.incall_location_holder);
+ Fragment fragment = getLocationFragment();
return fragment != null && fragment.isVisible();
}
@Override
public void showLocationUi(@Nullable Fragment locationUi) {
- boolean isShowing = isShowingLocationUi();
- if (!isShowing && locationUi != null) {
+ boolean isVisible = isShowingLocationUi();
+ if (locationUi != null && !isVisible) {
// Show the location fragment.
getChildFragmentManager()
.beginTransaction()
.replace(R.id.incall_location_holder, locationUi)
.commitAllowingStateLoss();
- } else if (isShowing && locationUi == null) {
+ } else if (locationUi == null && isVisible) {
// Hide the location fragment
- Fragment fragment = getChildFragmentManager().findFragmentById(R.id.incall_location_holder);
- getChildFragmentManager().beginTransaction().remove(fragment).commitAllowingStateLoss();
+ getChildFragmentManager()
+ .beginTransaction()
+ .remove(getLocationFragment())
+ .commitAllowingStateLoss();
}
}
+
+ @Override
+ public void onMultiWindowModeChanged(boolean isInMultiWindowMode) {
+ super.onMultiWindowModeChanged(isInMultiWindowMode);
+ if (isInMultiWindowMode == isShowingLocationUi()) {
+ LogUtil.i("InCallFragment.onMultiWindowModeChanged", "hide = " + isInMultiWindowMode);
+ // Need to show or hide location
+ showLocationUi(isInMultiWindowMode ? null : getLocationFragment());
+ }
+ }
+
+ private Fragment getLocationFragment() {
+ return getChildFragmentManager().findFragmentById(R.id.incall_location_holder);
+ }
}
diff --git a/java/com/android/incallui/incall/impl/InCallPagerAdapter.java b/java/com/android/incallui/incall/impl/InCallPagerAdapter.java
index 50eb4c8c3..2e2183565 100644
--- a/java/com/android/incallui/incall/impl/InCallPagerAdapter.java
+++ b/java/com/android/incallui/incall/impl/InCallPagerAdapter.java
@@ -19,17 +19,18 @@ package com.android.incallui.incall.impl;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
-import android.support.v4.app.FragmentPagerAdapter;
+import android.support.v4.app.FragmentStatePagerAdapter;
+import android.support.v4.view.PagerAdapter;
import android.text.TextUtils;
import com.android.dialer.multimedia.MultimediaData;
import com.android.incallui.sessiondata.MultimediaFragment;
/** View pager adapter for in call ui. */
-public class InCallPagerAdapter extends FragmentPagerAdapter {
+public class InCallPagerAdapter extends FragmentStatePagerAdapter {
- @Nullable private final MultimediaData attachments;
+ @Nullable private MultimediaData attachments;
- public InCallPagerAdapter(FragmentManager fragmentManager, MultimediaData attachments) {
+ public InCallPagerAdapter(FragmentManager fragmentManager, @Nullable MultimediaData attachments) {
super(fragmentManager);
this.attachments = attachments;
}
@@ -47,13 +48,27 @@ public class InCallPagerAdapter extends FragmentPagerAdapter {
@Override
public int getCount() {
if (attachments != null
- && (!TextUtils.isEmpty(attachments.getSubject()) || attachments.hasImageData())) {
+ && (!TextUtils.isEmpty(attachments.getText()) || attachments.hasImageData())) {
return 2;
}
return 1;
}
+ public void setAttachments(@Nullable MultimediaData attachments) {
+ if (this.attachments != attachments) {
+ this.attachments = attachments;
+ notifyDataSetChanged();
+ }
+ }
+
public int getButtonGridPosition() {
return getCount() - 1;
}
+
+ //this is called when notifyDataSetChanged() is called
+ @Override
+ public int getItemPosition(Object object) {
+ // refresh all fragments when data set changed
+ return PagerAdapter.POSITION_NONE;
+ }
}
diff --git a/java/com/android/incallui/incall/impl/MappedButtonConfig.java b/java/com/android/incallui/incall/impl/MappedButtonConfig.java
index ecdb5dfea..722983796 100644
--- a/java/com/android/incallui/incall/impl/MappedButtonConfig.java
+++ b/java/com/android/incallui/incall/impl/MappedButtonConfig.java
@@ -22,7 +22,7 @@ import android.util.ArraySet;
import com.android.dialer.common.Assert;
import com.android.incallui.incall.protocol.InCallButtonIds;
import com.android.incallui.incall.protocol.InCallButtonIdsExtension;
-
+import com.google.auto.value.AutoValue;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -151,7 +151,7 @@ final class MappedButtonConfig {
}
/** Holds information about button mapping. */
-
+ @AutoValue
abstract static class MappingInfo {
/** The Ui slot into which a given button desires to be placed. */
@@ -179,7 +179,7 @@ final class MappedButtonConfig {
}
/** Class used to build instances of {@link MappingInfo}. */
-
+ @AutoValue.Builder
abstract static class Builder {
public abstract Builder setSlot(int slot);
diff --git a/java/com/android/incallui/incall/protocol/PrimaryCallState.java b/java/com/android/incallui/incall/protocol/PrimaryCallState.java
index 782090832..6e1680b4b 100644
--- a/java/com/android/incallui/incall/protocol/PrimaryCallState.java
+++ b/java/com/android/incallui/incall/protocol/PrimaryCallState.java
@@ -18,15 +18,15 @@ package com.android.incallui.incall.protocol;
import android.graphics.drawable.Drawable;
import android.telecom.DisconnectCause;
-import android.telecom.VideoProfile;
import com.android.incallui.call.DialerCall;
-import com.android.incallui.call.DialerCall.SessionModificationState;
+import com.android.incallui.videotech.VideoTech;
+import com.android.incallui.videotech.VideoTech.SessionModificationState;
import java.util.Locale;
/** State of the primary call. */
public class PrimaryCallState {
public final int state;
- public final int videoState;
+ public final boolean isVideoCall;
@SessionModificationState public final int sessionModificationState;
public final DisconnectCause disconnectCause;
public final String connectionLabel;
@@ -37,19 +37,21 @@ public class PrimaryCallState {
public final boolean isWifi;
public final boolean isConference;
public final boolean isWorkCall;
+ public final boolean isHdAttempting;
public final boolean isHdAudioCall;
public final boolean isForwardedNumber;
public final boolean shouldShowContactPhoto;
public final long connectTimeMillis;
public final boolean isVoiceMailNumber;
public final boolean isRemotelyHeld;
+ public final boolean isBusinessNumber;
// TODO: Convert to autovalue. b/34502119
public static PrimaryCallState createEmptyPrimaryCallState() {
return new PrimaryCallState(
DialerCall.State.IDLE,
- VideoProfile.STATE_AUDIO_ONLY,
- DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST,
+ false, /* isVideoCall */
+ VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST,
new DisconnectCause(DisconnectCause.UNKNOWN),
null, /* connectionLabel */
null, /* connectionIcon */
@@ -59,17 +61,19 @@ public class PrimaryCallState {
false /* isWifi */,
false /* isConference */,
false /* isWorkCall */,
+ false /* isHdAttempting */,
false /* isHdAudioCall */,
false /* isForwardedNumber */,
false /* shouldShowContactPhoto */,
0,
false /* isVoiceMailNumber */,
- false /* isRemotelyHeld */);
+ false /* isRemotelyHeld */,
+ false /* isBusinessNumber */);
}
public PrimaryCallState(
int state,
- int videoState,
+ boolean isVideoCall,
@SessionModificationState int sessionModificationState,
DisconnectCause disconnectCause,
String connectionLabel,
@@ -80,14 +84,16 @@ public class PrimaryCallState {
boolean isWifi,
boolean isConference,
boolean isWorkCall,
+ boolean isHdAttempting,
boolean isHdAudioCall,
boolean isForwardedNumber,
boolean shouldShowContactPhoto,
long connectTimeMillis,
boolean isVoiceMailNumber,
- boolean isRemotelyHeld) {
+ boolean isRemotelyHeld,
+ boolean isBusinessNumber) {
this.state = state;
- this.videoState = videoState;
+ this.isVideoCall = isVideoCall;
this.sessionModificationState = sessionModificationState;
this.disconnectCause = disconnectCause;
this.connectionLabel = connectionLabel;
@@ -98,12 +104,14 @@ public class PrimaryCallState {
this.isWifi = isWifi;
this.isConference = isConference;
this.isWorkCall = isWorkCall;
+ this.isHdAttempting = isHdAttempting;
this.isHdAudioCall = isHdAudioCall;
this.isForwardedNumber = isForwardedNumber;
this.shouldShowContactPhoto = shouldShowContactPhoto;
this.connectTimeMillis = connectTimeMillis;
this.isVoiceMailNumber = isVoiceMailNumber;
this.isRemotelyHeld = isRemotelyHeld;
+ this.isBusinessNumber = isBusinessNumber;
}
@Override
diff --git a/java/com/android/incallui/incall/protocol/PrimaryInfo.java b/java/com/android/incallui/incall/protocol/PrimaryInfo.java
index 1833ed22e..c1709501d 100644
--- a/java/com/android/incallui/incall/protocol/PrimaryInfo.java
+++ b/java/com/android/incallui/incall/protocol/PrimaryInfo.java
@@ -41,6 +41,7 @@ public class PrimaryInfo {
// Used for consistent LetterTile coloring.
@Nullable public final String contactInfoLookupKey;
@Nullable public final MultimediaData multimediaData;
+ public final int numberPresentation;
// TODO: Convert to autovalue. b/34502119
public static PrimaryInfo createEmptyPrimaryInfo() {
@@ -59,7 +60,8 @@ public class PrimaryInfo {
false,
false,
null,
- null);
+ null,
+ -1);
}
public PrimaryInfo(
@@ -77,7 +79,8 @@ public class PrimaryInfo {
boolean answeringDisconnectsOngoingCall,
boolean shouldShowLocation,
@Nullable String contactInfoLookupKey,
- @Nullable MultimediaData multimediaData) {
+ @Nullable MultimediaData multimediaData,
+ int numberPresentation) {
this.number = number;
this.name = name;
this.nameIsNumber = nameIsNumber;
@@ -93,6 +96,7 @@ public class PrimaryInfo {
this.shouldShowLocation = shouldShowLocation;
this.contactInfoLookupKey = contactInfoLookupKey;
this.multimediaData = multimediaData;
+ this.numberPresentation = numberPresentation;
}
@Override
diff --git a/java/com/android/incallui/maps/Maps.java b/java/com/android/incallui/maps/Maps.java
new file mode 100644
index 000000000..648cf9f24
--- /dev/null
+++ b/java/com/android/incallui/maps/Maps.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.maps;
+
+import android.location.Location;
+import android.support.annotation.NonNull;
+import android.support.v4.app.Fragment;
+
+/** Used to create a fragment that can display a static map at the given location. */
+public interface Maps {
+ /**
+ * Used to check if maps is available. This will return false if Dialer was compiled without
+ * support for Google Play Services.
+ */
+ boolean isAvailable();
+
+ @NonNull
+ Fragment createStaticMapFragment(@NonNull Location location);
+}
diff --git a/java/com/android/incallui/maps/MapsComponent.java b/java/com/android/incallui/maps/MapsComponent.java
new file mode 100644
index 000000000..1ca17b781
--- /dev/null
+++ b/java/com/android/incallui/maps/MapsComponent.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.maps;
+
+import android.content.Context;
+import com.android.dialer.inject.HasRootComponent;
+import dagger.Subcomponent;
+import com.android.incallui.maps.stub.StubMapsModule;
+
+/** Subcomponent that can be used to access the maps implementation. */
+public class MapsComponent {
+
+ private static MapsComponent instance;
+ private Maps maps;
+
+ public Maps getMaps() {
+ if (maps == null) {
+ maps = new StubMapsModule.StubMaps();
+ }
+ return maps;
+ }
+
+ public static MapsComponent get(Context context) {
+ if (instance == null) {
+ instance = new MapsComponent();
+ }
+ return instance;
+ }
+
+
+ /** Used to refer to the root application component. */
+ public interface HasComponent {
+ MapsComponent mapsComponent();
+ }
+}
diff --git a/java/com/android/incallui/maps/StaticMapBinding.java b/java/com/android/incallui/maps/StaticMapBinding.java
deleted file mode 100644
index 9d24ef27a..000000000
--- a/java/com/android/incallui/maps/StaticMapBinding.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.incallui.maps;
-
-import android.app.Application;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
-
-/** Utility for getting a {@link StaticMapFactory} */
-public class StaticMapBinding {
-
- @Nullable
- public static StaticMapFactory get(@NonNull Application application) {
- if (useTestingInstance) {
- return testingInstance;
- }
- if (application instanceof StaticMapFactory) {
- return ((StaticMapFactory) application);
- }
- return null;
- }
-
- private static StaticMapFactory testingInstance;
- private static boolean useTestingInstance;
-
- @VisibleForTesting
- public static void setForTesting(@Nullable StaticMapFactory staticMapFactory) {
- testingInstance = staticMapFactory;
- useTestingInstance = true;
- }
-
- @VisibleForTesting
- public static void clearForTesting() {
- useTestingInstance = false;
- }
-}
diff --git a/java/com/android/incallui/maps/impl/AndroidManifest.xml b/java/com/android/incallui/maps/impl/AndroidManifest.xml
new file mode 100644
index 000000000..4ad0b3b7e
--- /dev/null
+++ b/java/com/android/incallui/maps/impl/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.incallui.maps.impl">
+
+ <application>
+ <meta-data
+ android:name="com.google.android.gms.version"
+ android:value="@integer/google_play_services_version"/>
+ </application>
+</manifest>
diff --git a/java/com/android/incallui/maps/impl/MapsImpl.java b/java/com/android/incallui/maps/impl/MapsImpl.java
new file mode 100644
index 000000000..2cecee93e
--- /dev/null
+++ b/java/com/android/incallui/maps/impl/MapsImpl.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.maps.impl;
+
+import android.location.Location;
+import android.support.annotation.NonNull;
+import android.support.v4.app.Fragment;
+import com.android.incallui.maps.Maps;
+import javax.inject.Inject;
+
+/** Uses Google Play Services APIs to create a static map fragment. */
+final class MapsImpl implements Maps {
+ @Inject
+ public MapsImpl() {}
+
+ @Override
+ public boolean isAvailable() {
+ return true;
+ }
+
+ @Override
+ @NonNull
+ public Fragment createStaticMapFragment(@NonNull Location location) {
+ return StaticMapFragment.newInstance(location);
+ }
+}
diff --git a/java/com/android/incallui/maps/impl/MapsModule.java b/java/com/android/incallui/maps/impl/MapsModule.java
new file mode 100644
index 000000000..22f2f32a7
--- /dev/null
+++ b/java/com/android/incallui/maps/impl/MapsModule.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.maps.impl;
+
+import com.android.incallui.maps.Maps;
+import dagger.Binds;
+import dagger.Module;
+import javax.inject.Singleton;
+
+/** This module provides an instance of maps. */
+@Module
+public abstract class MapsModule {
+
+ @Binds
+ @Singleton
+ public abstract Maps bindMaps(MapsImpl maps);
+}
diff --git a/java/com/android/incallui/maps/impl/StaticMapFragment.java b/java/com/android/incallui/maps/impl/StaticMapFragment.java
new file mode 100644
index 000000000..38a4c156b
--- /dev/null
+++ b/java/com/android/incallui/maps/impl/StaticMapFragment.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.maps.impl;
+
+import android.location.Location;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.google.android.gms.maps.CameraUpdateFactory;
+import com.google.android.gms.maps.GoogleMap;
+import com.google.android.gms.maps.OnMapReadyCallback;
+import com.google.android.gms.maps.SupportMapFragment;
+import com.google.android.gms.maps.model.LatLng;
+import com.google.android.gms.maps.model.MarkerOptions;
+
+/** Shows a static map centered on a specified location */
+public class StaticMapFragment extends Fragment implements OnMapReadyCallback {
+
+ private static final String ARG_LOCATION = "location";
+
+ public static StaticMapFragment newInstance(@NonNull Location location) {
+ Bundle args = new Bundle();
+ args.putParcelable(ARG_LOCATION, Assert.isNotNull(location));
+ StaticMapFragment fragment = new StaticMapFragment();
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
+ return layoutInflater.inflate(R.layout.static_map_fragment, viewGroup, false);
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle bundle) {
+ super.onViewCreated(view, bundle);
+ SupportMapFragment mapFragment =
+ (SupportMapFragment) getChildFragmentManager().findFragmentById(R.id.static_map);
+ if (mapFragment != null) {
+ mapFragment.getMapAsync(this);
+ } else {
+ LogUtil.w("StaticMapFragment.onViewCreated", "No map fragment found!");
+ }
+ }
+
+ @Override
+ public void onMapReady(GoogleMap googleMap) {
+ Location location = getArguments().getParcelable(ARG_LOCATION);
+ LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude());
+ googleMap.addMarker(new MarkerOptions().position(latLng).flat(true).draggable(false));
+ googleMap.getUiSettings().setMapToolbarEnabled(false);
+ googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, 15f));
+ }
+}
diff --git a/java/com/android/incallui/maps/impl/res/layout/static_map_fragment.xml b/java/com/android/incallui/maps/impl/res/layout/static_map_fragment.xml
new file mode 100644
index 000000000..54f41cb6e
--- /dev/null
+++ b/java/com/android/incallui/maps/impl/res/layout/static_map_fragment.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:map="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <fragment
+ android:id="@+id/static_map"
+ class="com.google.android.gms.maps.SupportMapFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ map:liteMode="true"
+ map:mapType="normal"/>
+</FrameLayout>
diff --git a/java/com/android/incallui/maps/stub/StubMapsModule.java b/java/com/android/incallui/maps/stub/StubMapsModule.java
new file mode 100644
index 000000000..72678143c
--- /dev/null
+++ b/java/com/android/incallui/maps/stub/StubMapsModule.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.maps.stub;
+
+import android.location.Location;
+import android.support.annotation.NonNull;
+import android.support.v4.app.Fragment;
+import com.android.dialer.common.Assert;
+import com.android.incallui.maps.Maps;
+import dagger.Binds;
+import dagger.Module;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/** Stub for the maps module for build variants that don't support Google Play Services. */
+@Module
+public abstract class StubMapsModule {
+
+ @Binds
+ @Singleton
+ public abstract Maps bindMaps(StubMaps maps);
+
+ static public final class StubMaps implements Maps {
+ @Inject
+ public StubMaps() {}
+
+ @Override
+ public boolean isAvailable() {
+ return false;
+ }
+
+ @NonNull
+ @Override
+ public Fragment createStaticMapFragment(@NonNull Location location) {
+ throw Assert.createUnsupportedOperationFailException();
+ }
+ }
+}
diff --git a/java/com/android/incallui/maps/testing/TestMapsModule.java b/java/com/android/incallui/maps/testing/TestMapsModule.java
new file mode 100644
index 000000000..bb096812b
--- /dev/null
+++ b/java/com/android/incallui/maps/testing/TestMapsModule.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.maps.testing;
+
+import android.support.annotation.Nullable;
+import com.android.incallui.maps.Maps;
+import dagger.Module;
+import dagger.Provides;
+
+/** This module provides a instance of maps for testing. */
+@Module
+public final class TestMapsModule {
+
+ @Nullable private static Maps maps;
+
+ public static void setMaps(@Nullable Maps maps) {
+ TestMapsModule.maps = maps;
+ }
+
+ @Provides
+ static Maps getMaps() {
+ return maps;
+ }
+
+ private TestMapsModule() {}
+}
diff --git a/java/com/android/incallui/res/values/strings.xml b/java/com/android/incallui/res/values/strings.xml
index 252d131de..0b95a9cc6 100644
--- a/java/com/android/incallui/res/values/strings.xml
+++ b/java/com/android/incallui/res/values/strings.xml
@@ -223,17 +223,6 @@
<item>ABSENTNUMBER</item>
</string-array>
- <!-- Preference for Voicemail service provider under "Voicemail" settings.
- [CHAR LIMIT=40] -->
- <string name="voicemail_provider">Service</string>
-
- <!-- Preference for Voicemail setting of each provider.
- [CHAR LIMIT=40] -->
- <string name="voicemail_settings">Setup</string>
-
- <!-- String to display in voicemail number summary when no voicemail num is set -->
- <string name="voicemail_number_not_set">&lt;Not set&gt;</string>
-
<!-- Title displayed above settings coming after voicemail in the call features screen -->
<string name="other_settings">Other call settings</string>
@@ -242,26 +231,6 @@
<!-- Use this to describe the select contact button in EditPhoneNumberPreference; currently for screen readers through accessibility. -->
<string name="selectContact">select contact</string>
- <!-- Dialog title for the vibration settings for voicemail notifications [CHAR LIMIT=40] -->
- <string msgid="8731372580674292759" name="voicemail_notification_vibrate_when_title">Vibrate</string>
- <!-- Dialog title for the vibration settings for voice mail notifications [CHAR LIMIT=40]-->
- <string msgid="8995274609647451109" name="voicemail_notification_vibarte_when_dialog_title">Vibrate</string>
-
- <!-- Voicemail ringtone title. The user clicks on this preference to select
- which sound to play when a voicemail notification is received.
- [CHAR LIMIT=30] -->
- <string name="voicemail_notification_ringtone_title">Sound</string>
-
- <!-- The default value value for voicemail notification. -->
- <string name="voicemail_notification_vibrate_when_default" translatable="false">never</string>
-
- <!-- Actual values used in our code for voicemail notifications. DO NOT TRANSLATE -->
- <string-array name="voicemail_notification_vibrate_when_values" translatable="false">
- <item>always</item>
- <item>silent</item>
- <item>never</item>
- </string-array>
-
<!-- Title for the category "ringtone", which is shown above ringtone and vibration
related settings.
[CHAR LIMIT=30] -->
diff --git a/java/com/android/incallui/sessiondata/MultimediaFragment.java b/java/com/android/incallui/sessiondata/MultimediaFragment.java
index d6f671d58..14aa0a3aa 100644
--- a/java/com/android/incallui/sessiondata/MultimediaFragment.java
+++ b/java/com/android/incallui/sessiondata/MultimediaFragment.java
@@ -31,12 +31,10 @@ import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
-import com.android.dialer.common.Assert;
import com.android.dialer.common.FragmentUtils;
import com.android.dialer.common.LogUtil;
import com.android.dialer.multimedia.MultimediaData;
-import com.android.incallui.maps.StaticMapBinding;
-import com.android.incallui.maps.StaticMapFactory;
+import com.android.incallui.maps.MapsComponent;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.GlideException;
@@ -58,17 +56,13 @@ public class MultimediaFragment extends Fragment implements AvatarPresenter {
private static final String ARG_INTERACTIVE = "interactive";
private static final String ARG_SHOW_AVATAR = "show_avatar";
private ImageView avatarImageView;
- // TODO: add click listeners
- @SuppressWarnings("unused")
- private boolean isInteractive;
private boolean showAvatar;
- private StaticMapFactory mapFactory;
public static MultimediaFragment newInstance(
@NonNull MultimediaData multimediaData, boolean isInteractive, boolean showAvatar) {
return newInstance(
- multimediaData.getSubject(),
+ multimediaData.getText(),
multimediaData.getImageUri(),
multimediaData.getLocation(),
isInteractive,
@@ -96,7 +90,6 @@ public class MultimediaFragment extends Fragment implements AvatarPresenter {
@Override
public void onCreate(@Nullable Bundle bundle) {
super.onCreate(bundle);
- isInteractive = getArguments().getBoolean(ARG_INTERACTIVE);
showAvatar = getArguments().getBoolean(ARG_SHOW_AVATAR);
}
@@ -107,10 +100,7 @@ public class MultimediaFragment extends Fragment implements AvatarPresenter {
boolean hasImage = getImageUri() != null;
boolean hasSubject = !TextUtils.isEmpty(getSubject());
boolean hasMap = getLocation() != null;
- if (hasMap) {
- mapFactory = StaticMapBinding.get(getActivity().getApplication());
- }
- if (mapFactory != null) {
+ if (hasMap && MapsComponent.get(getContext()).getMaps().isAvailable()) {
if (hasImage) {
if (hasSubject) {
return layoutInflater.inflate(
@@ -178,7 +168,7 @@ public class MultimediaFragment extends Fragment implements AvatarPresenter {
if (fragmentHolder != null) {
fragmentHolder.setClipToOutline(true);
Fragment mapFragment =
- Assert.isNotNull(mapFactory).getStaticMap(Assert.isNotNull(getLocation()));
+ MapsComponent.get(getContext()).getMaps().createStaticMapFragment(getLocation());
getChildFragmentManager()
.beginTransaction()
.replace(R.id.answer_message_frag, mapFragment)
diff --git a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image.xml b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image.xml
index 7000f83b5..0882781e7 100644
--- a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image.xml
+++ b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image.xml
@@ -46,5 +46,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/loading_spinner"
- android:layout_centerInParent="true"/>
+ android:layout_centerInParent="true"
+ android:elevation="2dp"/>
</RelativeLayout>
diff --git a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image_frag.xml b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image_frag.xml
index 9959f4dcc..c816418fc 100644
--- a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image_frag.xml
+++ b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image_frag.xml
@@ -42,6 +42,14 @@
android:outlineProvider="background"
android:scaleType="centerCrop"/>
+ <ProgressBar
+ android:id="@+id/loading_spinner"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_column="1"
+ android:layout_gravity="center"
+ android:elevation="2dp"/>
+
<FrameLayout
android:id="@id/answer_message_frag"
android:layout_width="0dp"
diff --git a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image.xml b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image.xml
index 995565455..4e6fcbadb 100644
--- a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image.xml
+++ b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image.xml
@@ -59,4 +59,12 @@
android:elevation="@dimen/answer_data_elevation"
android:outlineProvider="background"
android:scaleType="centerCrop"/>
+
+ <ProgressBar
+ android:id="@+id/loading_spinner"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_column="1"
+ android:layout_gravity="center"
+ android:elevation="2dp"/>
</GridLayout>
diff --git a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image_frag.xml b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image_frag.xml
index 387c5cf68..ffbe41bbd 100644
--- a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image_frag.xml
+++ b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image_frag.xml
@@ -61,6 +61,14 @@
android:outlineProvider="background"
android:scaleType="centerCrop"/>
+ <ProgressBar
+ android:id="@+id/loading_spinner"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_column="1"
+ android:layout_gravity="center"
+ android:elevation="2dp"/>
+
<FrameLayout
android:id="@id/answer_message_frag"
android:layout_width="0dp"
diff --git a/java/com/android/incallui/spam/SpamCallListListener.java b/java/com/android/incallui/spam/SpamCallListListener.java
index 0897842de..ed0a99e2a 100644
--- a/java/com/android/incallui/spam/SpamCallListListener.java
+++ b/java/com/android/incallui/spam/SpamCallListListener.java
@@ -17,6 +17,7 @@
package com.android.incallui.spam;
import android.app.Notification;
+import android.app.Notification.Builder;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
@@ -33,12 +34,13 @@ import com.android.dialer.common.LogUtil;
import com.android.dialer.logging.Logger;
import com.android.dialer.logging.nano.ContactLookupResult;
import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.notification.NotificationChannelManager;
+import com.android.dialer.notification.NotificationChannelManager.Channel;
import com.android.dialer.spam.Spam;
import com.android.incallui.R;
import com.android.incallui.call.CallList;
import com.android.incallui.call.DialerCall;
import com.android.incallui.call.DialerCall.CallHistoryStatus;
-import com.android.incallui.call.DialerCall.SessionModificationState;
import java.util.Random;
/**
@@ -47,7 +49,7 @@ import java.util.Random;
*/
public class SpamCallListListener implements CallList.Listener {
- static final int NOTIFICATION_ID = 1;
+ static final int NOTIFICATION_ID = R.id.notification_spam_call;
private static final String TAG = "SpamCallListListener";
private final Context context;
private final Random random;
@@ -87,7 +89,7 @@ public class SpamCallListListener implements CallList.Listener {
public void onUpgradeToVideo(DialerCall call) {}
@Override
- public void onSessionModificationStateChange(@SessionModificationState int newState) {}
+ public void onSessionModificationStateChange(DialerCall call) {}
@Override
public void onCallListChange(CallList callList) {}
@@ -173,13 +175,16 @@ public class SpamCallListListener implements CallList.Listener {
* Creates a notification builder with properties common among the two after call notifications.
*/
private Notification.Builder createAfterCallNotificationBuilder(DialerCall call) {
- return new Notification.Builder(context)
- .setContentIntent(
- createActivityPendingIntent(call, SpamNotificationActivity.ACTION_SHOW_DIALOG))
- .setCategory(Notification.CATEGORY_STATUS)
- .setPriority(Notification.PRIORITY_DEFAULT)
- .setColor(context.getColor(R.color.dialer_theme_color))
- .setSmallIcon(R.drawable.ic_call_end_white_24dp);
+ Builder builder =
+ new Builder(context)
+ .setContentIntent(
+ createActivityPendingIntent(call, SpamNotificationActivity.ACTION_SHOW_DIALOG))
+ .setCategory(Notification.CATEGORY_STATUS)
+ .setPriority(Notification.PRIORITY_DEFAULT)
+ .setColor(context.getColor(R.color.dialer_theme_color))
+ .setSmallIcon(R.drawable.ic_call_end_white_24dp);
+ NotificationChannelManager.applyChannel(builder, context, Channel.MISC, null);
+ return builder;
}
private CharSequence getDisplayNumber(DialerCall call) {
diff --git a/java/com/android/incallui/video/bindings/VideoBindings.java b/java/com/android/incallui/video/bindings/VideoBindings.java
index 934ff078a..a80a6c702 100644
--- a/java/com/android/incallui/video/bindings/VideoBindings.java
+++ b/java/com/android/incallui/video/bindings/VideoBindings.java
@@ -22,7 +22,7 @@ import com.android.incallui.video.protocol.VideoCallScreen;
/** Bindings for video module. */
public class VideoBindings {
- public static VideoCallScreen createVideoCallScreen() {
- return new VideoCallFragment();
+ public static VideoCallScreen createVideoCallScreen(String callId) {
+ return VideoCallFragment.newInstance(callId);
}
}
diff --git a/java/com/android/incallui/video/impl/VideoCallFragment.java b/java/com/android/incallui/video/impl/VideoCallFragment.java
index 77a67d032..92c8b375e 100644
--- a/java/com/android/incallui/video/impl/VideoCallFragment.java
+++ b/java/com/android/incallui/video/impl/VideoCallFragment.java
@@ -32,6 +32,7 @@ import android.renderscript.RenderScript;
import android.renderscript.ScriptIntrinsicBlur;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.view.animation.FastOutLinearInInterpolator;
@@ -92,6 +93,9 @@ public class VideoCallFragment extends Fragment
AudioRouteSelectorPresenter,
OnSystemUiVisibilityChangeListener {
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ static final String ARG_CALL_ID = "call_id";
+
private static final float BLUR_PREVIEW_RADIUS = 16.0f;
private static final float BLUR_PREVIEW_SCALE_FACTOR = 1.0f;
private static final float BLUR_REMOTE_RADIUS = 25.0f;
@@ -156,6 +160,15 @@ public class VideoCallFragment extends Fragment
}
};
+ public static VideoCallFragment newInstance(String callId) {
+ Bundle bundle = new Bundle();
+ bundle.putString(ARG_CALL_ID, Assert.isNotNull(callId));
+
+ VideoCallFragment instance = new VideoCallFragment();
+ instance.setArguments(bundle);
+ return instance;
+ }
+
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -308,22 +321,27 @@ public class VideoCallFragment extends Fragment
}
@Override
- public void onResume() {
- super.onResume();
- LogUtil.i("VideoCallFragment.onResume", null);
- inCallScreenDelegate.onInCallScreenResumed();
- }
-
- @Override
public void onStart() {
super.onStart();
LogUtil.i("VideoCallFragment.onStart", null);
+ onVideoScreenStart();
+ }
+
+ @Override
+ public void onVideoScreenStart() {
inCallButtonUiDelegate.refreshMuteState();
videoCallScreenDelegate.onVideoCallScreenUiReady();
getView().postDelayed(cameraPermissionDialogRunnable, CAMERA_PERMISSION_DIALOG_DELAY_IN_MILLIS);
}
@Override
+ public void onResume() {
+ super.onResume();
+ LogUtil.i("VideoCallFragment.onResume", null);
+ inCallScreenDelegate.onInCallScreenResumed();
+ }
+
+ @Override
public void onPause() {
super.onPause();
LogUtil.i("VideoCallFragment.onPause", null);
@@ -333,6 +351,11 @@ public class VideoCallFragment extends Fragment
public void onStop() {
super.onStop();
LogUtil.i("VideoCallFragment.onStop", null);
+ onVideoScreenStop();
+ }
+
+ @Override
+ public void onVideoScreenStop() {
getView().removeCallbacks(cameraPermissionDialogRunnable);
videoCallScreenDelegate.onVideoCallScreenUiUnready();
}
@@ -721,6 +744,12 @@ public class VideoCallFragment extends Fragment
}
@Override
+ @NonNull
+ public String getCallId() {
+ return Assert.isNotNull(getArguments().getString(ARG_CALL_ID));
+ }
+
+ @Override
public void showButton(@InCallButtonIds int buttonId, boolean show) {
LogUtil.v(
"VideoCallFragment.showButton",
diff --git a/java/com/android/incallui/video/impl/res/layout/frag_videocall.xml b/java/com/android/incallui/video/impl/res/layout/frag_videocall.xml
index dc663dda1..f8c6fc3c7 100644
--- a/java/com/android/incallui/video/impl/res/layout/frag_videocall.xml
+++ b/java/com/android/incallui/video/impl/res/layout/frag_videocall.xml
@@ -31,6 +31,7 @@
android:accessibilityTraversalBefore="@+id/videocall_speaker_button"
android:drawablePadding="8dp"
android:drawableTop="@drawable/quantum_ic_videocam_off_white_36"
+ android:drawableTint="@color/videocall_camera_off_tint"
android:padding="64dp"
android:text="@string/videocall_remote_video_off"
android:textAppearance="@style/Dialer.Incall.TextAppearance"
@@ -43,7 +44,8 @@
android:layout_height="match_parent"
android:layout_alignParentBottom="true"
android:layout_alignParentStart="true"
- android:background="@color/videocall_overlay_background_color"/>
+ android:background="@color/videocall_overlay_background_color"
+ tools:visibility="gone"/>
<TextureView
android:id="@+id/videocall_video_preview"
@@ -71,7 +73,8 @@
android:layout_height="match_parent"
android:layout_alignParentBottom="true"
android:layout_alignParentStart="true"
- android:background="@color/videocall_overlay_background_color"/>
+ android:background="@color/videocall_overlay_background_color"
+ tools:visibility="gone"/>
<ImageView
android:id="@+id/videocall_video_preview_off_overlay"
@@ -82,7 +85,9 @@
android:layout_alignRight="@+id/videocall_video_preview"
android:layout_alignTop="@+id/videocall_video_preview"
android:scaleType="center"
- android:src="@drawable/quantum_ic_videocam_off_white_36"
+ android:src="@drawable/quantum_ic_videocam_off_white_24"
+ android:tint="@color/videocall_camera_off_tint"
+ android:tintMode="src_in"
android:visibility="gone"
android:importantForAccessibility="no"
tools:visibility="visible"/>
diff --git a/java/com/android/incallui/video/impl/res/values/colors.xml b/java/com/android/incallui/video/impl/res/values/colors.xml
new file mode 100644
index 000000000..874bf9404
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/values/colors.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+ <color name="videocall_camera_off_tint">#89ffffff</color>
+</resources>
diff --git a/java/com/android/incallui/video/protocol/VideoCallScreen.java b/java/com/android/incallui/video/protocol/VideoCallScreen.java
index 0eaf692e2..bad050cd1 100644
--- a/java/com/android/incallui/video/protocol/VideoCallScreen.java
+++ b/java/com/android/incallui/video/protocol/VideoCallScreen.java
@@ -21,6 +21,10 @@ import android.support.v4.app.Fragment;
/** Interface for call video call module. */
public interface VideoCallScreen {
+ void onVideoScreenStart();
+
+ void onVideoScreenStop();
+
void showVideoViews(boolean shouldShowPreview, boolean shouldShowRemote, boolean isRemotelyHeld);
void onLocalVideoDimensionsChanged();
@@ -33,4 +37,6 @@ public interface VideoCallScreen {
boolean shouldShowFullscreen, boolean shouldShowGreenScreen);
Fragment getVideoCallScreenFragment();
+
+ String getCallId();
}
diff --git a/java/com/android/incallui/videotech/VideoTech.java b/java/com/android/incallui/videotech/VideoTech.java
new file mode 100644
index 000000000..fb2641793
--- /dev/null
+++ b/java/com/android/incallui/videotech/VideoTech.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.videotech;
+
+import android.support.annotation.IntDef;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Video calling interface. */
+public interface VideoTech {
+
+ boolean isAvailable();
+
+ boolean isTransmittingOrReceiving();
+
+ void onCallStateChanged(int newState);
+
+ @SessionModificationState
+ int getSessionModificationState();
+
+ void upgradeToVideo();
+
+ void acceptVideoRequest();
+
+ void acceptVideoRequestAsAudio();
+
+ void declineVideoRequest();
+
+ boolean isTransmitting();
+
+ void stopTransmission();
+
+ void resumeTransmission();
+
+ void pause();
+
+ void unpause();
+
+ void setCamera(String cameraId);
+
+ void setDeviceOrientation(int rotation);
+
+ /** Listener for video call events. */
+ interface VideoTechListener {
+
+ void onVideoTechStateChanged();
+
+ void onSessionModificationStateChanged();
+
+ void onCameraDimensionsChanged(int width, int height);
+
+ void onPeerDimensionsChanged(int width, int height);
+
+ void onVideoUpgradeRequestReceived();
+ }
+
+ /**
+ * Defines different states of session modify requests, which are used to upgrade to video, or
+ * downgrade to audio.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ SESSION_MODIFICATION_STATE_NO_REQUEST,
+ SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE,
+ SESSION_MODIFICATION_STATE_REQUEST_FAILED,
+ SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST,
+ SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT,
+ SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED,
+ SESSION_MODIFICATION_STATE_REQUEST_REJECTED,
+ SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE
+ })
+ @interface SessionModificationState {}
+
+ int SESSION_MODIFICATION_STATE_NO_REQUEST = 0;
+ int SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE = 1;
+ int SESSION_MODIFICATION_STATE_REQUEST_FAILED = 2;
+ int SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST = 3;
+ int SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT = 4;
+ int SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED = 5;
+ int SESSION_MODIFICATION_STATE_REQUEST_REJECTED = 6;
+ int SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE = 7;
+}
diff --git a/java/com/android/incallui/videotech/empty/EmptyVideoTech.java b/java/com/android/incallui/videotech/empty/EmptyVideoTech.java
new file mode 100644
index 000000000..bc8db4c07
--- /dev/null
+++ b/java/com/android/incallui/videotech/empty/EmptyVideoTech.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.videotech.empty;
+
+import com.android.incallui.videotech.VideoTech;
+
+/** Default video tech that is always available but doesn't do anything. */
+public class EmptyVideoTech implements VideoTech {
+
+ @Override
+ public boolean isAvailable() {
+ return false;
+ }
+
+ @Override
+ public boolean isTransmittingOrReceiving() {
+ return false;
+ }
+
+ @Override
+ public void onCallStateChanged(int newState) {}
+
+ @Override
+ public int getSessionModificationState() {
+ return VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST;
+ }
+
+ @Override
+ public void upgradeToVideo() {}
+
+ @Override
+ public void acceptVideoRequest() {}
+
+ @Override
+ public void acceptVideoRequestAsAudio() {}
+
+ @Override
+ public void declineVideoRequest() {}
+
+ @Override
+ public boolean isTransmitting() {
+ return false;
+ }
+
+ @Override
+ public void stopTransmission() {}
+
+ @Override
+ public void resumeTransmission() {}
+
+ @Override
+ public void pause() {}
+
+ @Override
+ public void unpause() {}
+
+ @Override
+ public void setCamera(String cameraId) {}
+
+ @Override
+ public void setDeviceOrientation(int rotation) {}
+}
diff --git a/java/com/android/incallui/videotech/ims/ImsVideoCallCallback.java b/java/com/android/incallui/videotech/ims/ImsVideoCallCallback.java
new file mode 100644
index 000000000..0a15f7e65
--- /dev/null
+++ b/java/com/android/incallui/videotech/ims/ImsVideoCallCallback.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.videotech.ims;
+
+import android.os.Handler;
+import android.telecom.Call;
+import android.telecom.Connection;
+import android.telecom.Connection.VideoProvider;
+import android.telecom.InCallService.VideoCall;
+import android.telecom.VideoProfile;
+import android.telecom.VideoProfile.CameraCapabilities;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.videotech.VideoTech;
+import com.android.incallui.videotech.VideoTech.SessionModificationState;
+import com.android.incallui.videotech.VideoTech.VideoTechListener;
+
+/** Receives IMS video call state updates. */
+public class ImsVideoCallCallback extends VideoCall.Callback {
+ private static final int CLEAR_FAILED_REQUEST_TIMEOUT_MILLIS = 4000;
+ private final Handler handler = new Handler();
+ private final Call call;
+ private final ImsVideoTech videoTech;
+ private final VideoTechListener listener;
+ private int requestedVideoState = VideoProfile.STATE_AUDIO_ONLY;
+
+ ImsVideoCallCallback(final Call call, ImsVideoTech videoTech, VideoTechListener listener) {
+ this.call = call;
+ this.videoTech = videoTech;
+ this.listener = listener;
+ }
+
+ @Override
+ public void onSessionModifyRequestReceived(VideoProfile videoProfile) {
+ LogUtil.i(
+ "ImsVideoCallCallback.onSessionModifyRequestReceived", "videoProfile: " + videoProfile);
+
+ int previousVideoState = ImsVideoTech.getUnpausedVideoState(call.getDetails().getVideoState());
+ int newVideoState = ImsVideoTech.getUnpausedVideoState(videoProfile.getVideoState());
+
+ boolean wasVideoCall = VideoProfile.isVideo(previousVideoState);
+ boolean isVideoCall = VideoProfile.isVideo(newVideoState);
+
+ if (wasVideoCall && !isVideoCall) {
+ LogUtil.i(
+ "ImsVideoTech.onSessionModifyRequestReceived", "call downgraded to %d", newVideoState);
+ } else if (previousVideoState != newVideoState) {
+ requestedVideoState = newVideoState;
+ videoTech.setSessionModificationState(
+ VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST);
+ listener.onVideoUpgradeRequestReceived();
+ }
+ }
+
+ /**
+ * @param status Status of the session modify request. Valid values are {@link
+ * Connection.VideoProvider#SESSION_MODIFY_REQUEST_SUCCESS}, {@link
+ * Connection.VideoProvider#SESSION_MODIFY_REQUEST_FAIL}, {@link
+ * Connection.VideoProvider#SESSION_MODIFY_REQUEST_INVALID}
+ * @param responseProfile The actual profile changes made by the peer device.
+ */
+ @Override
+ public void onSessionModifyResponseReceived(
+ int status, VideoProfile requestedProfile, VideoProfile responseProfile) {
+ LogUtil.i(
+ "ImsVideoCallCallback.onSessionModifyResponseReceived",
+ "status: %d, requestedProfile: %s, responseProfile: %s, session modification state: %d",
+ status,
+ requestedProfile,
+ responseProfile,
+ videoTech.getSessionModificationState());
+
+ if (videoTech.getSessionModificationState()
+ == VideoTech.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE) {
+ handler.removeCallbacksAndMessages(null); // Clear everything
+
+ final int newSessionModificationState = getSessionModificationStateFromTelecomStatus(status);
+ if (status != VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS) {
+ // This will update the video UI to display the error message.
+ videoTech.setSessionModificationState(newSessionModificationState);
+ }
+
+ // Wait for 4 seconds and then clean the session modification state. This allows the video UI
+ // to stay up so that the user can read the error message.
+ //
+ // If the other person accepted the upgrade request then this will keep the video UI up until
+ // the call's video state change. Without this we would switch to the voice call and then
+ // switch back to video UI.
+ handler.postDelayed(
+ () -> {
+ if (videoTech.getSessionModificationState() == newSessionModificationState) {
+ LogUtil.i("ImsVideoCallCallback.onSessionModifyResponseReceived", "clearing state");
+ videoTech.setSessionModificationState(
+ VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ } else {
+ LogUtil.i(
+ "ImsVideoCallCallback.onSessionModifyResponseReceived",
+ "session modification state has changed, not clearing state");
+ }
+ },
+ CLEAR_FAILED_REQUEST_TIMEOUT_MILLIS);
+ } else if (videoTech.getSessionModificationState()
+ == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+ videoTech.setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ } else if (videoTech.getSessionModificationState()
+ == VideoTech.SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE) {
+ videoTech.setSessionModificationState(getSessionModificationStateFromTelecomStatus(status));
+ } else {
+ LogUtil.i(
+ "ImsVideoCallCallback.onSessionModifyResponseReceived",
+ "call is not waiting for response, doing nothing");
+ }
+ }
+
+ @SessionModificationState
+ private int getSessionModificationStateFromTelecomStatus(int telecomStatus) {
+ switch (telecomStatus) {
+ case VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS:
+ return VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST;
+ case VideoProvider.SESSION_MODIFY_REQUEST_FAIL:
+ case VideoProvider.SESSION_MODIFY_REQUEST_INVALID:
+ // Check if it's already video call, which means the request is not video upgrade request.
+ if (VideoProfile.isVideo(call.getDetails().getVideoState())) {
+ return VideoTech.SESSION_MODIFICATION_STATE_REQUEST_FAILED;
+ } else {
+ return VideoTech.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED;
+ }
+ case VideoProvider.SESSION_MODIFY_REQUEST_TIMED_OUT:
+ return VideoTech.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT;
+ case VideoProvider.SESSION_MODIFY_REQUEST_REJECTED_BY_REMOTE:
+ return VideoTech.SESSION_MODIFICATION_STATE_REQUEST_REJECTED;
+ default:
+ LogUtil.e(
+ "ImsVideoCallCallback.getSessionModificationStateFromTelecomStatus",
+ "unknown status: %d",
+ telecomStatus);
+ return VideoTech.SESSION_MODIFICATION_STATE_REQUEST_FAILED;
+ }
+ }
+
+ @Override
+ public void onCallSessionEvent(int event) {
+ switch (event) {
+ case Connection.VideoProvider.SESSION_EVENT_RX_PAUSE:
+ LogUtil.i("ImsVideoCallCallback.onCallSessionEvent", "rx_pause");
+ break;
+ case Connection.VideoProvider.SESSION_EVENT_RX_RESUME:
+ LogUtil.i("ImsVideoCallCallback.onCallSessionEvent", "rx_resume");
+ break;
+ case Connection.VideoProvider.SESSION_EVENT_CAMERA_FAILURE:
+ LogUtil.i("ImsVideoCallCallback.onCallSessionEvent", "camera_failure");
+ break;
+ case Connection.VideoProvider.SESSION_EVENT_CAMERA_READY:
+ LogUtil.i("ImsVideoCallCallback.onCallSessionEvent", "camera_ready");
+ break;
+ default:
+ LogUtil.i("ImsVideoCallCallback.onCallSessionEvent", "unknown event = : " + event);
+ break;
+ }
+ }
+
+ @Override
+ public void onPeerDimensionsChanged(int width, int height) {
+ listener.onPeerDimensionsChanged(width, height);
+ }
+
+ @Override
+ public void onVideoQualityChanged(int videoQuality) {
+ LogUtil.i("ImsVideoCallCallback.onVideoQualityChanged", "videoQuality: %d", videoQuality);
+ }
+
+ @Override
+ public void onCallDataUsageChanged(long dataUsage) {
+ LogUtil.i("ImsVideoCallCallback.onCallDataUsageChanged", "dataUsage: %d", dataUsage);
+ }
+
+ @Override
+ public void onCameraCapabilitiesChanged(CameraCapabilities cameraCapabilities) {
+ if (cameraCapabilities != null) {
+ listener.onCameraDimensionsChanged(
+ cameraCapabilities.getWidth(), cameraCapabilities.getHeight());
+ }
+ }
+
+ int getRequestedVideoState() {
+ return requestedVideoState;
+ }
+}
diff --git a/java/com/android/incallui/videotech/ims/ImsVideoTech.java b/java/com/android/incallui/videotech/ims/ImsVideoTech.java
new file mode 100644
index 000000000..890e5c80c
--- /dev/null
+++ b/java/com/android/incallui/videotech/ims/ImsVideoTech.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.videotech.ims;
+
+import android.os.Build;
+import android.telecom.Call;
+import android.telecom.Call.Details;
+import android.telecom.VideoProfile;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.videotech.VideoTech;
+
+/** ViLTE implementation */
+public class ImsVideoTech implements VideoTech {
+ private final Call call;
+ private final VideoTechListener listener;
+ private ImsVideoCallCallback callback;
+ private @SessionModificationState int sessionModificationState =
+ VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST;
+ private int previousVideoState = VideoProfile.STATE_AUDIO_ONLY;
+
+ public ImsVideoTech(VideoTechListener listener, Call call) {
+ this.listener = listener;
+ this.call = call;
+ }
+
+ @Override
+ public boolean isAvailable() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ return false;
+ }
+
+ boolean hasCapabilities =
+ call.getDetails().can(Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_TX)
+ && call.getDetails().can(Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_RX);
+
+ return call.getVideoCall() != null
+ && (hasCapabilities || VideoProfile.isVideo(call.getDetails().getVideoState()));
+ }
+
+ @Override
+ public boolean isTransmittingOrReceiving() {
+ return VideoProfile.isVideo(call.getDetails().getVideoState());
+ }
+
+ @Override
+ public void onCallStateChanged(int newState) {
+ if (!isAvailable()) {
+ return;
+ }
+
+ if (callback == null) {
+ callback = new ImsVideoCallCallback(call, this, listener);
+ call.getVideoCall().registerCallback(callback);
+ }
+
+ if (getSessionModificationState()
+ == VideoTech.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE
+ && isTransmittingOrReceiving()) {
+ // We don't clear the session modification state right away when we find out the video upgrade
+ // request was accepted to avoid having the UI switch from video to voice to video.
+ // Once the underlying telecom call updates to video mode it's safe to clear the state.
+ LogUtil.i(
+ "ImsVideoTech.onCallStateChanged",
+ "upgraded to video, clearing session modification state");
+ setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+
+ // Determines if a received upgrade to video request should be cancelled. This can happen if
+ // another InCall UI responds to the upgrade to video request.
+ int newVideoState = call.getDetails().getVideoState();
+ if (newVideoState != previousVideoState
+ && sessionModificationState
+ == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+ LogUtil.i("ImsVideoTech.onCallStateChanged", "cancelling upgrade notification");
+ setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+ previousVideoState = newVideoState;
+ }
+
+ @Override
+ public int getSessionModificationState() {
+ return sessionModificationState;
+ }
+
+ void setSessionModificationState(@SessionModificationState int state) {
+ if (state != sessionModificationState) {
+ LogUtil.i(
+ "ImsVideoTech.setSessionModificationState", "%d -> %d", sessionModificationState, state);
+ sessionModificationState = state;
+ listener.onSessionModificationStateChanged();
+ }
+ }
+
+ @Override
+ public void upgradeToVideo() {
+ LogUtil.enterBlock("ImsVideoTech.upgradeToVideo");
+
+ int unpausedVideoState = getUnpausedVideoState(call.getDetails().getVideoState());
+ call.getVideoCall()
+ .sendSessionModifyRequest(
+ new VideoProfile(unpausedVideoState | VideoProfile.STATE_BIDIRECTIONAL));
+ setSessionModificationState(
+ VideoTech.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE);
+ }
+
+ @Override
+ public void acceptVideoRequest() {
+ int requestedVideoState = callback.getRequestedVideoState();
+ Assert.checkArgument(requestedVideoState != VideoProfile.STATE_AUDIO_ONLY);
+ LogUtil.i("ImsVideoTech.acceptUpgradeRequest", "videoState: " + requestedVideoState);
+ call.getVideoCall().sendSessionModifyResponse(new VideoProfile(requestedVideoState));
+ setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+
+ @Override
+ public void acceptVideoRequestAsAudio() {
+ LogUtil.enterBlock("ImsVideoTech.acceptVideoRequestAsAudio");
+ call.getVideoCall().sendSessionModifyResponse(new VideoProfile(VideoProfile.STATE_AUDIO_ONLY));
+ setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+
+ @Override
+ public void declineVideoRequest() {
+ LogUtil.enterBlock("ImsVideoTech.declineUpgradeRequest");
+ call.getVideoCall()
+ .sendSessionModifyResponse(new VideoProfile(call.getDetails().getVideoState()));
+ setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+
+ @Override
+ public boolean isTransmitting() {
+ return VideoProfile.isTransmissionEnabled(call.getDetails().getVideoState());
+ }
+
+ @Override
+ public void stopTransmission() {
+ LogUtil.enterBlock("ImsVideoTech.stopTransmission");
+
+ int unpausedVideoState = getUnpausedVideoState(call.getDetails().getVideoState());
+ call.getVideoCall()
+ .sendSessionModifyRequest(
+ new VideoProfile(unpausedVideoState & ~VideoProfile.STATE_TX_ENABLED));
+ }
+
+ @Override
+ public void resumeTransmission() {
+ LogUtil.enterBlock("ImsVideoTech.resumeTransmission");
+
+ int unpausedVideoState = getUnpausedVideoState(call.getDetails().getVideoState());
+ call.getVideoCall()
+ .sendSessionModifyRequest(
+ new VideoProfile(unpausedVideoState | VideoProfile.STATE_TX_ENABLED));
+ setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE);
+ }
+
+ @Override
+ public void pause() {
+ if (canPause()) {
+ LogUtil.i("ImsVideoTech.pause", "sending pause request");
+ int pausedVideoState = call.getDetails().getVideoState() | VideoProfile.STATE_PAUSED;
+ call.getVideoCall().sendSessionModifyRequest(new VideoProfile(pausedVideoState));
+ } else {
+ LogUtil.i("ImsVideoTech.pause", "not sending request: canPause: %b", canPause());
+ }
+ }
+
+ @Override
+ public void unpause() {
+ if (canPause()) {
+ LogUtil.i("ImsVideoTech.unpause", "sending unpause request");
+ int unpausedVideoState = getUnpausedVideoState(call.getDetails().getVideoState());
+ call.getVideoCall().sendSessionModifyRequest(new VideoProfile(unpausedVideoState));
+ } else {
+ LogUtil.i("ImsVideoTech.unpause", "not sending request: canPause: %b", canPause());
+ }
+ }
+
+ @Override
+ public void setCamera(String cameraId) {
+ call.getVideoCall().setCamera(cameraId);
+ call.getVideoCall().requestCameraCapabilities();
+ }
+
+ @Override
+ public void setDeviceOrientation(int rotation) {
+ call.getVideoCall().setDeviceOrientation(rotation);
+ }
+
+ private boolean canPause() {
+ return call.getDetails().can(Details.CAPABILITY_CAN_PAUSE_VIDEO)
+ && call.getState() == Call.STATE_ACTIVE;
+ }
+
+ static int getUnpausedVideoState(int videoState) {
+ return videoState & (~VideoProfile.STATE_PAUSED);
+ }
+}
diff --git a/java/com/android/incallui/videotech/rcs/RcsVideoShare.java b/java/com/android/incallui/videotech/rcs/RcsVideoShare.java
new file mode 100644
index 000000000..2cb43036f
--- /dev/null
+++ b/java/com/android/incallui/videotech/rcs/RcsVideoShare.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.videotech.rcs;
+
+import android.support.annotation.NonNull;
+import android.telecom.Call;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.enrichedcall.EnrichedCallCapabilities;
+import com.android.dialer.enrichedcall.EnrichedCallManager;
+import com.android.dialer.enrichedcall.EnrichedCallManager.CapabilitiesListener;
+import com.android.dialer.enrichedcall.Session;
+import com.android.dialer.enrichedcall.videoshare.VideoShareListener;
+import com.android.incallui.videotech.VideoTech;
+
+/** Allows the in-call UI to make video calls over RCS. */
+public class RcsVideoShare implements VideoTech, CapabilitiesListener, VideoShareListener {
+ private final EnrichedCallManager enrichedCallManager;
+ private final VideoTechListener listener;
+ private final String callingNumber;
+ private int previousCallState = Call.STATE_NEW;
+ private long inviteSessionId = Session.NO_SESSION_ID;
+ private long transmittingSessionId = Session.NO_SESSION_ID;
+ private long receivingSessionId = Session.NO_SESSION_ID;
+
+ private @SessionModificationState int sessionModificationState =
+ VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST;
+
+ public RcsVideoShare(
+ @NonNull EnrichedCallManager enrichedCallManager,
+ @NonNull VideoTechListener listener,
+ @NonNull String callingNumber) {
+ this.enrichedCallManager = Assert.isNotNull(enrichedCallManager);
+ this.listener = Assert.isNotNull(listener);
+ this.callingNumber = Assert.isNotNull(callingNumber);
+
+ enrichedCallManager.registerCapabilitiesListener(this);
+ enrichedCallManager.registerVideoShareListener(this);
+ }
+
+ @Override
+ public boolean isAvailable() {
+ EnrichedCallCapabilities capabilities = enrichedCallManager.getCapabilities(callingNumber);
+ return capabilities != null && capabilities.supportsVideoShare();
+ }
+
+ @Override
+ public boolean isTransmittingOrReceiving() {
+ return transmittingSessionId != Session.NO_SESSION_ID
+ || receivingSessionId != Session.NO_SESSION_ID;
+ }
+
+ @Override
+ public void onCallStateChanged(int newState) {
+ if (newState == Call.STATE_DISCONNECTING) {
+ enrichedCallManager.unregisterVideoShareListener(this);
+ enrichedCallManager.unregisterCapabilitiesListener(this);
+ }
+
+ if (newState != previousCallState && newState == Call.STATE_ACTIVE) {
+ // Per spec, request capabilities when the call becomes active
+ enrichedCallManager.requestCapabilities(callingNumber);
+ }
+
+ previousCallState = newState;
+ }
+
+ @Override
+ public int getSessionModificationState() {
+ return sessionModificationState;
+ }
+
+ private void setSessionModificationState(@SessionModificationState int state) {
+ if (state != sessionModificationState) {
+ LogUtil.i(
+ "RcsVideoShare.setSessionModificationState", "%d -> %d", sessionModificationState, state);
+ sessionModificationState = state;
+ listener.onSessionModificationStateChanged();
+ }
+ }
+
+ @Override
+ public void upgradeToVideo() {
+ LogUtil.enterBlock("RcsVideoShare.upgradeToVideo");
+ transmittingSessionId = enrichedCallManager.startVideoShareSession(callingNumber);
+ if (transmittingSessionId != Session.NO_SESSION_ID) {
+ setSessionModificationState(
+ VideoTech.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE);
+ }
+ }
+
+ @Override
+ public void acceptVideoRequest() {
+ LogUtil.enterBlock("RcsVideoShare.acceptVideoRequest");
+ if (enrichedCallManager.acceptVideoShareSession(inviteSessionId)) {
+ receivingSessionId = inviteSessionId;
+ }
+ inviteSessionId = Session.NO_SESSION_ID;
+ setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+
+ @Override
+ public void acceptVideoRequestAsAudio() {
+ throw Assert.createUnsupportedOperationFailException();
+ }
+
+ @Override
+ public void declineVideoRequest() {
+ LogUtil.enterBlock("RcsVideoTech.declineUpgradeRequest");
+ enrichedCallManager.endVideoShareSession(
+ enrichedCallManager.getVideoShareInviteSessionId(callingNumber));
+ inviteSessionId = Session.NO_SESSION_ID;
+ setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+
+ @Override
+ public boolean isTransmitting() {
+ return transmittingSessionId != Session.NO_SESSION_ID;
+ }
+
+ @Override
+ public void stopTransmission() {
+ LogUtil.enterBlock("RcsVideoTech.stopTransmission");
+ }
+
+ @Override
+ public void resumeTransmission() {
+ LogUtil.enterBlock("RcsVideoTech.resumeTransmission");
+ }
+
+ @Override
+ public void pause() {}
+
+ @Override
+ public void unpause() {}
+
+ @Override
+ public void setCamera(String cameraId) {}
+
+ @Override
+ public void setDeviceOrientation(int rotation) {}
+
+ @Override
+ public void onCapabilitiesUpdated() {
+ listener.onVideoTechStateChanged();
+ }
+
+ @Override
+ public void onVideoShareChanged() {
+ long existingInviteSessionId = inviteSessionId;
+
+ inviteSessionId = enrichedCallManager.getVideoShareInviteSessionId(callingNumber);
+ if (inviteSessionId != Session.NO_SESSION_ID) {
+ if (existingInviteSessionId == Session.NO_SESSION_ID) {
+ // This is a new invite
+ setSessionModificationState(
+ VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST);
+ listener.onVideoUpgradeRequestReceived();
+ }
+ } else {
+ setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+
+ if (sessionIsClosed(transmittingSessionId)) {
+ LogUtil.i("RcsVideoShare.onSessionClosed", "transmitting session closed");
+ transmittingSessionId = Session.NO_SESSION_ID;
+ }
+
+ if (sessionIsClosed(receivingSessionId)) {
+ LogUtil.i("RcsVideoShare.onSessionClosed", "receiving session closed");
+ receivingSessionId = Session.NO_SESSION_ID;
+ }
+
+ listener.onVideoTechStateChanged();
+ }
+
+ private boolean sessionIsClosed(long sessionId) {
+ return sessionId != Session.NO_SESSION_ID
+ && enrichedCallManager.getVideoShareSession(sessionId) == null;
+ }
+}