Merge "Tests for 3rd call reception in DSDA." into udc-dev
diff --git a/Android.bp b/Android.bp
index c5141ca..501b438 100644
--- a/Android.bp
+++ b/Android.bp
@@ -27,6 +27,7 @@
],
static_libs: [
"androidx.annotation_annotation",
+ "androidx.core_core",
],
libs: [
"services",
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index d42dcff..ab067d9 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -63,6 +63,8 @@
<uses-permission android:name="android.permission.WRITE_BLOCKED_NUMBERS"/>
<uses-permission android:name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
+ <uses-permission android:name="android.permission.USE_COLORIZED_NOTIFICATIONS"/>
+ <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
<uses-permission android:name="com.android.phone.permission.ACCESS_LAST_KNOWN_CELL_ID"/>
<uses-permission android:name="android.permission.STATUS_BAR_SERVICE" />
diff --git a/res/drawable/gm_phonelink.xml b/res/drawable/gm_phonelink.xml
new file mode 100644
index 0000000..2ffba0e
--- /dev/null
+++ b/res/drawable/gm_phonelink.xml
@@ -0,0 +1,11 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?android:attr/colorControlNormal"
+ android:autoMirrored="true">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M5,6h16L21,4L5,4c-1.1,0 -2,0.9 -2,2v11L1,17v3h11v-3L5,17L5,6zM21,8h-6c-0.55,0 -1,0.45 -1,1v10c0,0.55 0.45,1 1,1h6c0.55,0 1,-0.45 1,-1L22,9c0,-0.55 -0.45,-1 -1,-1zM20,17h-4v-7h4v7z"/>
+</vector>
diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml
index 4af6351..883ce52 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -25,7 +25,7 @@
<string name="notification_missedCallsMsg" msgid="5055782736170916682">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> ચૂકી ગયેલા કૉલ"</string>
<string name="notification_missedCallTicker" msgid="6731461957487087769">"<xliff:g id="MISSED_CALL_FROM">%s</xliff:g> નો કૉલ ચૂકી ગયાં"</string>
<string name="notification_missedCall_call_back" msgid="7900333283939789732">"કૉલ બેક"</string>
- <string name="notification_missedCall_message" msgid="4054698824390076431">"સંદેશ"</string>
+ <string name="notification_missedCall_message" msgid="4054698824390076431">"મેસેજ"</string>
<string name="notification_disconnectedCall_title" msgid="1790131923692416928">"ડિસ્કનેક્ટ કરેલો કૉલ"</string>
<string name="notification_disconnectedCall_body" msgid="600491714584417536">"ઇમર્જન્સી કૉલને કારણે <xliff:g id="CALLER">%s</xliff:g>નો કૉલ ડિસ્કનેક્ટ કરવામાં આવ્યો છે."</string>
<string name="notification_disconnectedCall_generic_body" msgid="5282765206349184853">"ઇમર્જન્સી કૉલને કારણે તમારો કૉલ ડિસ્કનેક્ટ કરવામાં આવ્યો છે."</string>
diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml
index 8109de2..68d8078 100644
--- a/res/values-kn/strings.xml
+++ b/res/values-kn/strings.xml
@@ -53,7 +53,7 @@
<string name="no_vm_number" msgid="2179959110602180844">"ಧ್ವನಿಮೇಲ್ ಸಂಖ್ಯೆಯು ಕಾಣೆಯಾಗಿದೆ"</string>
<string name="no_vm_number_msg" msgid="1339245731058529388">"ಸಿಮ್ ಕಾರ್ಡ್ನಲ್ಲಿ ಯಾವುದೇ ಧ್ವನಿಮೇಲ್ ಸಂಖ್ಯೆಯನ್ನು ಸಂಗ್ರಹಿಸಿಲ್ಲ."</string>
<string name="add_vm_number_str" msgid="5179510133063168998">"ಸಂಖ್ಯೆಯನ್ನು ಸೇರಿಸಿ"</string>
- <string name="change_default_dialer_dialog_title" msgid="5861469279421508060">"<xliff:g id="NEW_APP">%s</xliff:g> ಅನ್ನು ನಿಮ್ಮ ಡಿಫಾಲ್ಟ್ ಫೋನ್ ಅಪ್ಲಿಕೇಶನ್ ಆಗಿ ಮಾಡುವುದೇ?"</string>
+ <string name="change_default_dialer_dialog_title" msgid="5861469279421508060">"<xliff:g id="NEW_APP">%s</xliff:g> ಅನ್ನು ನಿಮ್ಮ ಡಿಫಾಲ್ಟ್ ಫೋನ್ ಆ್ಯಪ್ ಆಗಿ ಮಾಡಬೇಕೆ?"</string>
<string name="change_default_dialer_dialog_affirmative" msgid="8604665314757739550">"ಡಿಫಾಲ್ಟ್ ಹೊಂದಿಸಿ"</string>
<string name="change_default_dialer_dialog_negative" msgid="8648669840052697821">"ರದ್ದುಮಾಡಿ"</string>
<string name="change_default_dialer_warning_message" msgid="8461963987376916114">"<xliff:g id="NEW_APP">%s</xliff:g> ಗೆ ನಿಮ್ಮ ಕರೆಗಳ ಎಲ್ಲಾ ಅಂಶಗಳನ್ನು ನಿಯಂತ್ರಿಸಲು ಮತ್ತು ಕರೆಗಳನ್ನು ಮಾಡಲು ಸಾಧ್ಯವಾಗುತ್ತದೆ. ನೀವು ವಿಶ್ವಾಸವಿರಿಸಿರುವಂತಹ ಅಪ್ಲಿಕೇಶನ್ಗಳನ್ನು ಮಾತ್ರ ನಿಮ್ಮ ಡಿಫಾಲ್ಟ್ ಅಪ್ಲಿಕೇಶನ್ ಆಗಿ ಹೊಂದಿಸಬೇಕು."</string>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index d67df4b..ec278f0 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -321,6 +321,10 @@
<string name="notification_channel_disconnected_calls">Disconnected calls</string>
<!-- Notification channel name for a channel containing crashed phone apps service notifications. -->
<string name="notification_channel_in_call_service_crash">Crashed phone apps</string>
+ <!-- Notification channel name for a channel containing notifications related to call streaming.
+ Call streaming is a feature where an app can use another device like a tablet to see and
+ control a call taking place on their phone. -->
+ <string name="notification_channel_call_streaming">Call streaming</string>
<!-- Alert dialog content used to inform the user that placing a new outgoing call will end the
ongoing call in the app "other_app". -->
@@ -395,4 +399,20 @@
<string name="callendpoint_name_streaming">External</string>
<!-- The user-visible name of the unknown new type CallEndpoint -->
<string name="callendpoint_name_unknown">Unknown</string>
+
+ <!-- The content of a notification shown when a call is being streamed to another device.
+ Call streaming is a feature where a user can see and interact with a call from another
+ device like a tablet while the call takes place on their phone. -->
+ <string name="call_streaming_notification_body">Streaming audio to other device</string>
+ <!-- A notification action which is shown when a call is being streamed to another device.
+ Tapping the action will hang up the call.
+ Call streaming is a feature where a user can see and interact with a call from another
+ device like a tablet while the call takes place on their phone. -->
+ <string name="call_streaming_notification_action_hang_up">Hang up</string>
+ <!-- A notification action which is shown when a call is being streamed to another device.
+ Tapping the action will move the call back to the phone from the device it is being
+ streamed to.
+ Call streaming is a feature where a user can see and interact with a call from another
+ device like a tablet while the call takes place on their phone. -->
+ <string name="call_streaming_notification_action_switch_here">Switch here</string>
</resources>
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 743156e..42f02fb 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -414,6 +414,16 @@
private boolean mIsEmergencyCall;
+ /**
+ * Flag indicating if ECBM is active for the target phone account. This only applies to MT calls
+ * in the scenario of work profiles (when the profile is paused and the user has only registered
+ * a work sim). Normally, MT calls made to the work sim should be rejected when the work apps
+ * are paused. However, when the admin makes a MO ecall, ECBM should be enabled for that sim to
+ * allow non-emergency MT calls. MO calls don't apply because the phone account would be
+ * rejected from selection if the owner is not placing the call.
+ */
+ private boolean mIsInECBM;
+
// The Call is considered an emergency call for testing, but will not actually connect to
// emergency services.
private boolean mIsTestEmergencyCall;
@@ -989,6 +999,9 @@
s.append(SimpleDateFormat.getDateTimeInstance().format(new Date(getCreationTimeMillis())));
s.append("]");
s.append(isIncoming() ? "(MT - incoming)" : "(MO - outgoing)");
+ s.append("(User=");
+ s.append(getInitiatingUser());
+ s.append(")");
s.append("\n\t");
PhoneAccountHandle targetPhoneAccountHandle = getTargetPhoneAccount();
@@ -1592,6 +1605,21 @@
}
/**
+ * @return {@code true} if the target phone account is in ECBM.
+ */
+ public boolean isInECBM() {
+ return mIsInECBM;
+ }
+
+ /**
+ * Set if the target phone account is in ECBM.
+ * @param isInEcbm {@code true} if target phone account is in ECBM, {@code false} otherwise.
+ */
+ public void setIsInECBM(boolean isInECBM) {
+ mIsInECBM = isInECBM;
+ }
+
+ /**
* @return {@code true} if the network has identified this call as an emergency call.
*/
public boolean isNetworkIdentifiedEmergencyCall() {
@@ -1682,6 +1710,11 @@
public void setTargetPhoneAccount(PhoneAccountHandle accountHandle) {
if (!Objects.equals(mTargetPhoneAccountHandle, accountHandle)) {
mTargetPhoneAccountHandle = accountHandle;
+ // Update the last MO emergency call in the helper, if applicable.
+ if (isEmergencyCall() && !isIncoming()) {
+ mCallsManager.getEmergencyCallHelper().setLastOutgoingEmergencyCallPAH(
+ accountHandle);
+ }
for (Listener l : mListeners) {
l.onTargetPhoneAccountChanged(this);
}
@@ -4086,6 +4119,15 @@
* @param extras The extras.
*/
public void onConnectionEvent(String event, Bundle extras) {
+ if (mIsTransactionalCall) {
+ // send the Event directly to the ICS via the InCallController listener
+ for (Listener l : mListeners) {
+ l.onConnectionEvent(this, event, extras);
+ }
+ // Don't run the below block since it applies to Calls that are attached to a
+ // ConnectionService
+ return;
+ }
// Don't log call quality reports; they're quite frequent and will clog the log.
if (!Connection.EVENT_CALL_QUALITY_REPORT.equals(event)) {
Log.addEvent(this, LogUtils.Events.CONNECTION_EVENT, event);
diff --git a/src/com/android/server/telecom/CallStreamingController.java b/src/com/android/server/telecom/CallStreamingController.java
index 6276a7d..d90524d 100644
--- a/src/com/android/server/telecom/CallStreamingController.java
+++ b/src/com/android/server/telecom/CallStreamingController.java
@@ -87,6 +87,14 @@
mStreamingCall = null;
mTransactionalServiceWrapper = null;
if (mConnection != null) {
+ // Notify service streaming stopped and then unbind.
+ try {
+ mService.onCallStreamingStopped();
+ } catch (RemoteException e) {
+ // Could not notify stop streaming; we're about to just unbind so this is
+ // unfortunate but not the end of the world.
+ Log.e(this, e, "resetController: failed to notify stop streaming.");
+ }
mContext.unbindService(mConnection);
mConnection = null;
}
@@ -140,7 +148,7 @@
@Override
public CompletableFuture<VoipCallTransactionResult> processTransaction(Void v) {
- Log.d(this, "processTransaction");
+ Log.i(this, "processTransaction");
CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
if (mEnterInterception) {
@@ -178,9 +186,8 @@
@SuppressLint("LongLogTag")
@Override
public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
- Log.d(this, "processTransaction");
+ Log.i(this, "processTransaction");
CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
-
RoleManager roleManager = mContext.getSystemService(RoleManager.class);
PackageManager packageManager = mContext.getPackageManager();
if (roleManager == null || packageManager == null) {
@@ -198,7 +205,7 @@
VoipCallTransactionResult.RESULT_FAILED, MESSAGE));
return future;
}
-
+ Log.i(this, "processTransaction: servicePackage=%s", holders.get(0));
Intent serviceIntent = new Intent(CallStreamingService.SERVICE_INTERFACE);
serviceIntent.setPackage(holders.get(0));
List<ResolveInfo> infos = packageManager.queryIntentServicesAsUser(serviceIntent,
@@ -223,7 +230,7 @@
Intent intent = new Intent(CallStreamingService.SERVICE_INTERFACE);
intent.setComponent(serviceInfo.getComponentName());
- mConnection = new CallStreamingServiceConnection(mCall, mWrapper, future);
+ mConnection = new CallStreamingServiceConnection(mCall, mWrapper, future);
if (!mContext.bindServiceAsUser(intent, mConnection, Context.BIND_AUTO_CREATE
| Context.BIND_FOREGROUND_SERVICE
| Context.BIND_SCHEDULE_LIKE_TOP_APP, mUserHandle)) {
@@ -232,7 +239,6 @@
VoipCallTransactionResult.RESULT_FAILED,
"STREAMING_FAILED_SENDER_BINDING_ERROR"));
}
-
return future;
}
}
@@ -249,7 +255,7 @@
@SuppressLint("LongLogTag")
@Override
public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
- Log.d(this, "processTransaction");
+ Log.i(this, "processTransaction (unbindStreaming");
CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
resetController();
@@ -280,11 +286,13 @@
case CallState.ON_HOLD:
transaction = new CallStreamingStateChangeTransaction(
StreamingCall.STATE_HOLDING);
+ break;
case CallState.DISCONNECTING:
case CallState.DISCONNECTED:
Log.addEvent(call, LogUtils.Events.STOP_STREAMING);
transaction = new CallStreamingStateChangeTransaction(
StreamingCall.STATE_DISCONNECTED);
+ break;
default:
// ignore
}
@@ -374,13 +382,6 @@
}
private void clearBinding() {
- try {
- if (mService != null) {
- mService.onCallStreamingStopped();
- }
- } catch (RemoteException e) {
- Log.e(this, e, "Exception when stop call streaming");
- }
resetController();
if (!mFuture.isDone()) {
mFuture.complete(new VoipCallTransactionResult(
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index ccc8e59..13a965c 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -24,6 +24,7 @@
import static android.provider.CallLog.Calls.USER_MISSED_CALL_FILTERS_TIMEOUT;
import static android.provider.CallLog.Calls.USER_MISSED_CALL_SCREENING_SERVICE_SILENCED;
import static android.provider.CallLog.Calls.USER_MISSED_NEVER_RANG;
+import static android.provider.CallLog.Calls.USER_MISSED_NOT_RUNNING;
import static android.provider.CallLog.Calls.USER_MISSED_NO_ANSWER;
import static android.provider.CallLog.Calls.USER_MISSED_SHORT_RING;
import static android.telecom.TelecomManager.ACTION_POST_CALL;
@@ -40,6 +41,7 @@
import android.Manifest;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.AlertDialog;
import android.app.KeyguardManager;
@@ -131,6 +133,7 @@
import com.android.server.telecom.stats.CallFailureCause;
import com.android.server.telecom.ui.AudioProcessingNotification;
import com.android.server.telecom.ui.CallRedirectionTimeoutDialogActivity;
+import com.android.server.telecom.ui.CallStreamingNotification;
import com.android.server.telecom.ui.ConfirmCallDialogActivity;
import com.android.server.telecom.ui.DisconnectedCallNotifier;
import com.android.server.telecom.ui.IncomingCallNotifier;
@@ -446,6 +449,8 @@
private final CallStreamingController mCallStreamingController;
private final BlockedNumbersAdapter mBlockedNumbersAdapter;
private final TransactionManager mTransactionManager;
+ private final UserManager mUserManager;
+ private final CallStreamingNotification mCallStreamingNotification;
private final ConnectionServiceFocusManager.CallsManagerRequester mRequester =
new ConnectionServiceFocusManager.CallsManagerRequester() {
@@ -557,7 +562,8 @@
Executor asyncTaskExecutor,
BlockedNumbersAdapter blockedNumbersAdapter,
TransactionManager transactionManager,
- EmergencyCallDiagnosticLogger emergencyCallDiagnosticLogger) {
+ EmergencyCallDiagnosticLogger emergencyCallDiagnosticLogger,
+ CallStreamingNotification callStreamingNotification) {
mContext = context;
mLock = lock;
@@ -646,6 +652,7 @@
mBlockedNumbersAdapter = blockedNumbersAdapter;
mCallStreamingController = new CallStreamingController(mContext, mLock);
mVoipCallMonitor = new VoipCallMonitor(mContext, mLock);
+ mCallStreamingNotification = callStreamingNotification;
mListeners.add(mInCallWakeLockController);
mListeners.add(statusBarNotifier);
@@ -667,6 +674,7 @@
// this needs to be after the mCallAudioManager
mListeners.add(mPhoneStateBroadcaster);
mListeners.add(mVoipCallMonitor);
+ mListeners.add(mCallStreamingNotification);
mVoipCallMonitor.startMonitor();
@@ -685,6 +693,7 @@
mCallAnomalyWatchdog = callAnomalyWatchdog;
mAsyncTaskExecutor = asyncTaskExecutor;
+ mUserManager = mContext.getSystemService(UserManager.class);
}
public void setIncomingCallNotifier(IncomingCallNotifier incomingCallNotifier) {
@@ -1411,6 +1420,15 @@
extras.getInt(CallAttributes.CALL_CAPABILITIES_KEY,
CallAttributes.SUPPORTS_SET_INACTIVE), true);
call.setTargetPhoneAccount(phoneAccountHandle);
+ if (extras.containsKey(CallAttributes.DISPLAY_NAME_KEY)) {
+ CharSequence displayName = extras.getCharSequence(CallAttributes.DISPLAY_NAME_KEY);
+ if (!TextUtils.isEmpty(displayName)) {
+ call.setCallerDisplayName(displayName.toString(),
+ TelecomManager.PRESENTATION_ALLOWED);
+ }
+ }
+ // Incoming address was set via EXTRA_INCOMING_CALL_ADDRESS above.
+ call.setInitiatingUser(phoneAccountHandle.getUserHandle());
}
// Ensure new calls related to self-managed calls/connections are set as such. This will
@@ -1534,7 +1552,22 @@
CallFailureCause startFailCause =
checkIncomingCallPermitted(call, call.getTargetPhoneAccount());
- if (!isHandoverAllowed ||
+ // Check if the target phone account is possibly in ECBM.
+ call.setIsInECBM(getEmergencyCallHelper()
+ .isLastOutgoingEmergencyCallPAH(call.getTargetPhoneAccount()));
+ if (mUserManager.isQuietModeEnabled(call.getUserHandleFromTargetPhoneAccount())
+ && !call.isEmergencyCall() && !call.isInECBM()) {
+ Log.d(TAG, "Rejecting non-emergency call because the owner %s is not running.",
+ phoneAccountHandle.getUserHandle());
+ call.setMissedReason(USER_MISSED_NOT_RUNNING);
+ call.setStartFailCause(CallFailureCause.INVALID_USE);
+ if (isConference) {
+ notifyCreateConferenceFailed(phoneAccountHandle, call);
+ } else {
+ notifyCreateConnectionFailed(phoneAccountHandle, call);
+ }
+ }
+ else if (!isHandoverAllowed ||
(call.isSelfManaged() && !startFailCause.isSuccess())) {
if (isConference) {
notifyCreateConferenceFailed(phoneAccountHandle, call);
@@ -1680,7 +1713,6 @@
boolean isReusedCall;
Uri handle = isConference ? Uri.parse("tel:conf-factory") : participants.get(0);
Call call = reuseOutgoingCall(handle);
-
PhoneAccount account =
mPhoneAccountRegistrar.getPhoneAccount(requestedAccountHandle, initiatingUser);
Bundle phoneAccountExtra = account != null ? account.getExtras() : null;
@@ -1721,6 +1753,14 @@
call.setConnectionCapabilities(
extras.getInt(CallAttributes.CALL_CAPABILITIES_KEY,
CallAttributes.SUPPORTS_SET_INACTIVE), true);
+ if (extras.containsKey(CallAttributes.DISPLAY_NAME_KEY)) {
+ CharSequence displayName = extras.getCharSequence(
+ CallAttributes.DISPLAY_NAME_KEY);
+ if (!TextUtils.isEmpty(displayName)) {
+ call.setCallerDisplayName(displayName.toString(),
+ TelecomManager.PRESENTATION_ALLOWED);
+ }
+ }
call.setTargetPhoneAccount(requestedAccountHandle);
}
@@ -3412,10 +3452,11 @@
*/
boolean holdActiveCallForNewCall(Call call) {
Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
- Log.i(this, "holdActiveCallForNewCall, newCall: %s, activeCall: %s", call, activeCall);
+ Log.i(this, "holdActiveCallForNewCall, newCall: %s, activeCall: %s", call.getId(),
+ (activeCall == null ? "<none>" : activeCall.getId()));
if (activeCall != null && activeCall != call) {
if (canHold(activeCall)) {
- activeCall.hold();
+ activeCall.hold("swap to " + call.getId());
return true;
} else if (supportsHold(activeCall)
&& areFromSameSource(activeCall, call)) {
@@ -4900,12 +4941,25 @@
liveCallPhoneAccount);
}
- // First thing, if we are trying to make a call with the same phone account as the live
- // call, then allow it so that the connection service can make its own decision about
- // how to handle the new call relative to the current one.
+ // First thing, for managed calls, if we are trying to make a call with the same phone
+ // account as the live call, then allow it so that the connection service can make its own
+ // decision about how to handle the new call relative to the current one.
+ // Note: This behavior is primarily in place because Telephony historically manages the
+ // state of the calls it tracks by itself, holding and unholding as needed. Self-managed
+ // calls, even though from the same package are normally held/unheld automatically by
+ // Telecom. Calls within a single ConnectionService get held/unheld automatically during
+ // "swap" operations by CallsManager#holdActiveCallForNewCall. There is, however, a quirk
+ // in that if an app declares TWO different ConnectionServices, holdActiveCallForNewCall
+ // would not work correctly because focus switches between ConnectionServices, yet we
+ // tended to assume that if the calls are from the same package that the hold/unhold should
+ // be done by the app. That was a bad assumption as it meant that we could have two active
+ // calls.
+ // TODO(b/280826075): We need to come back and revisit all this logic in a holistic manner.
if (PhoneAccountHandle.areFromSamePackage(liveCallPhoneAccount,
- call.getTargetPhoneAccount())) {
- Log.i(this, "makeRoomForOutgoingCall: phoneAccount matches.");
+ call.getTargetPhoneAccount())
+ && !call.isSelfManaged()
+ && !liveCall.isSelfManaged()) {
+ Log.i(this, "makeRoomForOutgoingCall: managed phoneAccount matches");
call.getAnalytics().setCallIsAdditional(true);
liveCall.getAnalytics().setCallIsInterrupted(true);
return true;
@@ -5485,6 +5539,13 @@
impl.dump(pw);
pw.decreaseIndent();
}
+
+ if (mConnectionSvrFocusMgr != null) {
+ pw.println("mConnectionSvrFocusMgr:");
+ pw.increaseIndent();
+ mConnectionSvrFocusMgr.dump(pw);
+ pw.decreaseIndent();
+ }
}
/**
@@ -6326,4 +6387,30 @@
public CallStreamingController getCallStreamingController() {
return mCallStreamingController;
}
+
+ /**
+ * Given a call identified by call id, get the instance from the list of calls.
+ * @param callId the call id.
+ * @return the call, or null if not found.
+ */
+ public @Nullable Call getCall(@NonNull String callId) {
+ Optional<Call> foundCall = mCalls.stream().filter(
+ c -> c.getId().equals(callId)).findFirst();
+ if (foundCall.isPresent()) {
+ return foundCall.get();
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Triggers stopping of call streaming for a call by launching a stop streaming transaction.
+ * @param call the call.
+ */
+ public void stopCallStreaming(@NonNull Call call) {
+ if (call.getTransactionServiceWrapper() == null) {
+ return;
+ }
+ call.getTransactionServiceWrapper().stopCallStreaming(call);
+ }
}
diff --git a/src/com/android/server/telecom/ConnectionServiceFocusManager.java b/src/com/android/server/telecom/ConnectionServiceFocusManager.java
index 6fbc494..3694727 100644
--- a/src/com/android/server/telecom/ConnectionServiceFocusManager.java
+++ b/src/com/android/server/telecom/ConnectionServiceFocusManager.java
@@ -25,8 +25,10 @@
import android.telecom.Log;
import android.telecom.Logging.Session;
import android.text.TextUtils;
+import android.util.LocalLog;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
import java.util.ArrayList;
import java.util.List;
@@ -41,6 +43,7 @@
public class ConnectionServiceFocusManager {
private static final String TAG = "ConnectionSvrFocusMgr";
private static final int GET_CURRENT_FOCUS_TIMEOUT_MILLIS = 1000;
+ private final LocalLog mLocalLog = new LocalLog(20);
/** Factory interface used to create the {@link ConnectionServiceFocusManager} instance. */
public interface ConnectionServiceFocusManagerFactory {
@@ -124,6 +127,11 @@
* @return {@code True} if this call can receive focus, {@code false} otherwise.
*/
boolean isFocusable();
+
+ /**
+ * @return the ID of the focusable for debug purposes.
+ */
+ String getId();
}
/** Interface define a call back for focus request event. */
@@ -361,10 +369,11 @@
}
private void updateCurrentFocusCall() {
+ CallFocus previousFocus = mCurrentFocusCall;
mCurrentFocusCall = null;
if (mCurrentFocus == null) {
- Log.d(this, "updateCurrentFocusCall: mCurrentFocus is null");
+ Log.i(this, "updateCurrentFocusCall: mCurrentFocus is null");
return;
}
@@ -377,11 +386,16 @@
for (CallFocus call : calls) {
if (PRIORITY_FOCUS_CALL_STATE.contains(call.getState())) {
mCurrentFocusCall = call;
+ if (previousFocus != call) {
+ mLocalLog.log(call.getId());
+ }
Log.i(this, "updateCurrentFocusCall %s", mCurrentFocusCall);
return;
}
}
-
+ if (previousFocus != null) {
+ mLocalLog.log("<none>");
+ }
Log.i(this, "updateCurrentFocusCall = null");
}
@@ -477,6 +491,11 @@
}
}
+ public void dump(IndentingPrintWriter pw) {
+ pw.println("Call Focus History:");
+ mLocalLog.dump(pw);
+ }
+
private final class FocusManagerHandler extends Handler {
FocusManagerHandler(Looper looper) {
super(looper);
diff --git a/src/com/android/server/telecom/EmergencyCallHelper.java b/src/com/android/server/telecom/EmergencyCallHelper.java
index a213e26..fbb666d 100644
--- a/src/com/android/server/telecom/EmergencyCallHelper.java
+++ b/src/com/android/server/telecom/EmergencyCallHelper.java
@@ -21,6 +21,8 @@
import android.content.pm.PackageManager;
import android.os.UserHandle;
import android.telecom.Log;
+import android.telecom.PhoneAccountHandle;
+
import com.android.internal.annotations.VisibleForTesting;
/**
@@ -34,6 +36,7 @@
private final DefaultDialerCache mDefaultDialerCache;
private final Timeouts.Adapter mTimeoutsAdapter;
private UserHandle mLocationPermissionGrantedToUser;
+ private PhoneAccountHandle mLastOutgoingEmergencyCallPAH;
//stores the original state of permissions that dialer had
private boolean mHadFineLocation = false;
@@ -46,6 +49,7 @@
private boolean mBackgroundLocationGranted = false;
private long mLastEmergencyCallTimestampMillis;
+ private long mLastOutgoingEmergencyCallTimestampMillis;
@VisibleForTesting
public EmergencyCallHelper(
@@ -63,7 +67,7 @@
grantLocationPermission(userHandle);
}
if (call != null && call.isEmergencyCall()) {
- recordEmergencyCallTime();
+ recordEmergencyCall(call);
}
}
@@ -78,15 +82,37 @@
return mLastEmergencyCallTimestampMillis;
}
- private void recordEmergencyCallTime() {
- mLastEmergencyCallTimestampMillis = System.currentTimeMillis();
+ void setLastOutgoingEmergencyCallPAH(PhoneAccountHandle accountHandle) {
+ mLastOutgoingEmergencyCallPAH = accountHandle;
}
- private boolean isInEmergencyCallbackWindow() {
- return System.currentTimeMillis() - getLastEmergencyCallTimeMillis()
+ public boolean isLastOutgoingEmergencyCallPAH(PhoneAccountHandle currentCallHandle) {
+ boolean ecbmActive = mLastOutgoingEmergencyCallPAH != null
+ && isInEmergencyCallbackWindow(mLastOutgoingEmergencyCallTimestampMillis)
+ && currentCallHandle != null
+ && currentCallHandle.equals(mLastOutgoingEmergencyCallPAH);
+ if (ecbmActive) {
+ Log.i(this, "ECBM is enabled for %s. The last recorded call timestamp was at %s",
+ currentCallHandle, mLastOutgoingEmergencyCallTimestampMillis);
+ }
+
+ return ecbmActive;
+ }
+
+ boolean isInEmergencyCallbackWindow(long lastEmergencyCallTimestampMillis) {
+ return System.currentTimeMillis() - lastEmergencyCallTimestampMillis
< mTimeoutsAdapter.getEmergencyCallbackWindowMillis(mContext.getContentResolver());
}
+ private void recordEmergencyCall(Call call) {
+ mLastEmergencyCallTimestampMillis = System.currentTimeMillis();
+ if (!call.isIncoming()) {
+ // ECBM is applicable to MO emergency calls
+ mLastOutgoingEmergencyCallTimestampMillis = mLastEmergencyCallTimestampMillis;
+ mLastOutgoingEmergencyCallPAH = call.getTargetPhoneAccount();
+ }
+ }
+
private boolean shouldGrantTemporaryLocationPermission(Call call) {
if (!mContext.getResources().getBoolean(R.bool.grant_location_permission_enabled)) {
Log.i(this, "ShouldGrantTemporaryLocationPermission, disabled by config");
@@ -96,7 +122,8 @@
Log.i(this, "ShouldGrantTemporaryLocationPermission, no call");
return false;
}
- if (!call.isEmergencyCall() && !isInEmergencyCallbackWindow()) {
+ if (!call.isEmergencyCall() && !isInEmergencyCallbackWindow(
+ getLastEmergencyCallTimeMillis())) {
Log.i(this, "ShouldGrantTemporaryLocationPermission, not emergency");
return false;
}
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index be27dd1..3d3e3b4 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -338,7 +338,10 @@
UserHandle userToBind = getUserFromCall(call);
boolean isManagedProfile = UserUtil.isManagedProfile(mContext, userToBind);
// Note that UserHandle.CURRENT fails to capture the work profile, so we need to handle
- // it separately to ensure that the ICS is bound to the appropriate user.
+ // it separately to ensure that the ICS is bound to the appropriate user. If ECBM is
+ // active, we know that a work sim was previously used to place a MO emergency call. We
+ // need to ensure that we bind to the CURRENT_USER in this case, as the work user would
+ // not be running (handled in getUserFromCall).
userToBind = isManagedProfile ? userToBind : UserHandle.CURRENT;
if (!mContext.bindServiceAsUser(intent, mServiceConnection,
Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE
@@ -2601,7 +2604,8 @@
UserManager userManager = mContext.getSystemService(UserManager.class);
// Emergency call should never be blocked, so if the user associated with call is in
// quite mode, use the primary user for the emergency call.
- if (call.isEmergencyCall() && userManager.isQuietModeEnabled(userFromCall)) {
+ if ((call.isEmergencyCall() || call.isInECBM())
+ && userManager.isQuietModeEnabled(userFromCall)) {
return mCallsManager.getCurrentUserHandle();
}
return userFromCall;
diff --git a/src/com/android/server/telecom/Ringer.java b/src/com/android/server/telecom/Ringer.java
index cdacab0..45fb2af 100644
--- a/src/com/android/server/telecom/Ringer.java
+++ b/src/com/android/server/telecom/Ringer.java
@@ -626,10 +626,9 @@
public boolean shouldRingForContact(Call call) {
// avoid re-computing manager.matcherCallFilter(Bundle)
if (call.wasDndCheckComputedForCall()) {
- Log.v(this, "shouldRingForContact: returning computation from DndCallFilter.");
+ Log.i(this, "shouldRingForContact: returning computation from DndCallFilter.");
return !call.isCallSuppressedByDoNotDisturb();
}
-
final Uri contactUri = call.getHandle();
final Bundle peopleExtras = new Bundle();
if (contactUri != null) {
@@ -637,13 +636,7 @@
personList.add(new Person.Builder().setUri(contactUri.toString()).build());
peopleExtras.putParcelableArrayList(Notification.EXTRA_PEOPLE_LIST, personList);
}
-
- // query NotificationManager
- boolean shouldRing = mNotificationManager.matchesCallFilter(peopleExtras);
- // store the suppressed status in the call object
- call.setCallIsSuppressedByDoNotDisturb(!shouldRing);
-
- return shouldRing;
+ return mNotificationManager.matchesCallFilter(peopleExtras);
}
private boolean hasExternalRinger(Call foregroundCall) {
diff --git a/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java b/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java
index 0be90e0..523b841 100644
--- a/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java
+++ b/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java
@@ -101,6 +101,10 @@
public static final String ACTION_CANCEL_REDIRECTED_CALL =
"com.android.server.telecom.CANCEL_REDIRECTED_CALL";
+ public static final String ACTION_HANGUP_CALL = "com.android.server.telecom.HANGUP_CALL";
+ public static final String ACTION_STOP_STREAMING =
+ "com.android.server.telecom.ACTION_STOP_STREAMING";
+
public static final String EXTRA_USERHANDLE = "userhandle";
public static final String EXTRA_REDIRECTION_OUTGOING_CALL_ID =
"android.telecom.extra.REDIRECTION_OUTGOING_CALL_ID";
@@ -242,6 +246,26 @@
} finally {
Log.endSession();
}
+ } else if (ACTION_HANGUP_CALL.equals(action)) {
+ Log.startSession("TBIP.aHC", "streamingDialog");
+ try {
+ Call call = mCallsManager.getCall(intent.getData().getSchemeSpecificPart());
+ if (call != null) {
+ mCallsManager.disconnectCall(call);
+ }
+ } finally {
+ Log.endSession();
+ }
+ } else if (ACTION_STOP_STREAMING.equals(action)) {
+ Log.startSession("TBIP.aSS", "streamingDialog");
+ try {
+ Call call = mCallsManager.getCall(intent.getData().getSchemeSpecificPart());
+ if (call != null) {
+ mCallsManager.stopCallStreaming(call);
+ }
+ } finally {
+ Log.endSession();
+ }
}
}
diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java
index 8477d49..d3ca0b7 100644
--- a/src/com/android/server/telecom/TelecomSystem.java
+++ b/src/com/android/server/telecom/TelecomSystem.java
@@ -48,6 +48,7 @@
import com.android.server.telecom.components.UserCallIntentProcessor;
import com.android.server.telecom.components.UserCallIntentProcessorFactory;
import com.android.server.telecom.ui.AudioProcessingNotification;
+import com.android.server.telecom.ui.CallStreamingNotification;
import com.android.server.telecom.ui.DisconnectedCallNotifier;
import com.android.server.telecom.ui.IncomingCallNotifier;
import com.android.server.telecom.ui.MissedCallNotifierImpl.MissedCallNotifierImplFactory;
@@ -350,6 +351,12 @@
mLock, timeoutsAdapter, clockProxy, emergencyCallDiagnosticLogger);
TransactionManager transactionManager = TransactionManager.getInstance();
+
+ CallStreamingNotification callStreamingNotification =
+ new CallStreamingNotification(mContext,
+ packageName -> AppLabelProxy.Util.getAppLabel(
+ mContext.getPackageManager(), packageName), asyncTaskExecutor);
+
mCallsManager = new CallsManager(
mContext,
mLock,
@@ -386,7 +393,8 @@
asyncTaskExecutor,
blockedNumbersAdapter,
transactionManager,
- emergencyCallDiagnosticLogger);
+ emergencyCallDiagnosticLogger,
+ callStreamingNotification);
mIncomingCallNotifier = incomingCallNotifier;
incomingCallNotifier.setCallsManagerProxy(new IncomingCallNotifier.CallsManagerProxy() {
diff --git a/src/com/android/server/telecom/TransactionalServiceWrapper.java b/src/com/android/server/telecom/TransactionalServiceWrapper.java
index d83e551..ec95f39 100644
--- a/src/com/android/server/telecom/TransactionalServiceWrapper.java
+++ b/src/com/android/server/telecom/TransactionalServiceWrapper.java
@@ -682,6 +682,7 @@
public void stopCallStreaming(Call call) {
+ Log.i(this, "stopCallStreaming; callid=%s", call.getId());
if (call != null && call.isStreaming()) {
VoipCallTransaction stopStreamingTransaction = createStopStreamingTransaction(call);
addTransactionsToManager(stopStreamingTransaction, new ResultReceiver(null));
diff --git a/src/com/android/server/telecom/ui/CallStreamingNotification.java b/src/com/android/server/telecom/ui/CallStreamingNotification.java
new file mode 100644
index 0000000..752d8c8
--- /dev/null
+++ b/src/com/android/server/telecom/ui/CallStreamingNotification.java
@@ -0,0 +1,345 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.ui;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Person;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.UserHandle;
+import android.telecom.Log;
+import android.telecom.PhoneAccount;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.style.ForegroundColorSpan;
+import androidx.core.graphics.drawable.RoundedBitmapDrawable;
+import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.telecom.AppLabelProxy;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManagerListenerBase;
+import com.android.server.telecom.R;
+import com.android.server.telecom.TelecomBroadcastIntentProcessor;
+import com.android.server.telecom.components.TelecomBroadcastReceiver;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Class responsible for tracking if there is a call which is being streamed and posting a
+ * notification which informs the user that a call is streaming. The user has two possible actions:
+ * disconnect the call, bring the call back to the current device (stop streaming).
+ */
+public class CallStreamingNotification extends CallsManagerListenerBase implements Call.Listener {
+ // URI scheme used for data related to the notification actions.
+ public static final String CALL_ID_SCHEME = "callid";
+ // The default streaming notification ID.
+ private static final int STREAMING_NOTIFICATION_ID = 90210;
+ // Tag for streaming notification.
+ private static final String NOTIFICATION_TAG =
+ CallStreamingNotification.class.getSimpleName();
+
+ private final Context mContext;
+ private final NotificationManager mNotificationManager;
+ // Used to get the app name for the notification.
+ private final AppLabelProxy mAppLabelProxy;
+ // An executor that can be used to fire off async tasks that do not block Telecom in any manner.
+ private final Executor mAsyncTaskExecutor;
+ // The call which is treaming.
+ private Call mStreamingCall;
+ // Lock for notification post/remove -- these happen outside the Telecom sync lock.
+ private final Object mNotificationLock = new Object();
+
+ // Whether the notification is showing.
+ @GuardedBy("mNotificationLock")
+ private boolean mIsNotificationShowing = false;
+ @GuardedBy("mNotificationLock")
+ private UserHandle mNotificationUserHandle;
+
+ public CallStreamingNotification(@NonNull Context context,
+ @NonNull AppLabelProxy appLabelProxy,
+ @NonNull Executor asyncTaskExecutor) {
+ mContext = context;
+ mNotificationManager = context.getSystemService(NotificationManager.class);
+ mAppLabelProxy = appLabelProxy;
+ mAsyncTaskExecutor = asyncTaskExecutor;
+ }
+
+ @Override
+ public void onCallAdded(Call call) {
+ if (call.isStreaming()) {
+ trackStreamingCall(call);
+ enqueueStreamingNotification(call);
+ }
+ }
+
+ @Override
+ public void onCallRemoved(Call call) {
+ if (call == mStreamingCall) {
+ trackStreamingCall(null);
+ dequeueStreamingNotification();
+ }
+ }
+
+ /**
+ * Handles streaming state changes for a call.
+ * @param call the call
+ * @param isStreaming whether it is streaming or not
+ */
+ @Override
+ public void onCallStreamingStateChanged(Call call, boolean isStreaming) {
+ Log.i(this, "onCallStreamingStateChanged: call=%s, isStreaming=%b", call.getId(),
+ isStreaming);
+
+ if (isStreaming) {
+ trackStreamingCall(call);
+ enqueueStreamingNotification(call);
+ } else {
+ trackStreamingCall(null);
+ dequeueStreamingNotification();
+ }
+ }
+
+ /**
+ * Handles changes to the caller info for a call. Used to ensure we can update the photo uri
+ * if one was found.
+ * @param call the call which the caller info changed on.
+ */
+ @Override
+ public void onCallerInfoChanged(Call call) {
+ if (call == mStreamingCall) {
+ Log.i(this, "onCallerInfoChanged: call=%s, photoUri=%b", call.getId(),
+ call.getContactPhotoUri());
+ enqueueStreamingNotification(call);
+ }
+ }
+
+ /**
+ * Change the streaming call we are tracking.
+ * @param call the call.
+ */
+ private void trackStreamingCall(Call call) {
+ if (mStreamingCall != null) {
+ mStreamingCall.removeListener(this);
+ }
+ mStreamingCall = call;
+ if (mStreamingCall != null) {
+ mStreamingCall.addListener(this);
+ }
+ }
+
+ /**
+ * Enqueue an async task to post/repost the streaming notification.
+ * Note: This happens INSIDE the telecom lock.
+ * @param call the call to post notification for.
+ */
+ private void enqueueStreamingNotification(Call call) {
+ final Bitmap contactPhotoBitmap = call.getPhotoIcon();
+ mAsyncTaskExecutor.execute(() -> {
+ Icon contactPhotoIcon = null;
+ try {
+ if (contactPhotoBitmap != null) {
+ // Make the icon rounded... because there has to be hoops to jump through.
+ RoundedBitmapDrawable roundedDrawable = RoundedBitmapDrawableFactory.create(
+ mContext.getResources(), contactPhotoBitmap);
+ roundedDrawable.setCornerRadius(Math.max(contactPhotoBitmap.getWidth(),
+ contactPhotoBitmap.getHeight()) / 2.0f);
+ contactPhotoIcon = Icon.createWithBitmap(drawableToBitmap(roundedDrawable,
+ contactPhotoBitmap.getWidth(), contactPhotoBitmap.getHeight()));
+ }
+ } catch (Exception e) {
+ // All loads of things can do wrong when working with bitmaps and images, so to
+ // ensure Telecom doesn't crash, lets try/catch to be sure.
+ Log.e(this, e, "enqueueStreamingNotification: Couldn't build rounded icon");
+ }
+ showStreamingNotification(call.getId(),
+ call.getUserHandleFromTargetPhoneAccount(), call.getCallerDisplayName(),
+ call.getHandle(), contactPhotoIcon,
+ call.getTargetPhoneAccount().getComponentName().getPackageName(),
+ call.getConnectTimeMillis());
+ });
+ }
+
+ /**
+ * Dequeues the call streaming notification.
+ * Note: This is yo be called within the Telecom sync lock to launch the task to remove the call
+ * streaming notification.
+ */
+ private void dequeueStreamingNotification() {
+ mAsyncTaskExecutor.execute(() -> hideStreamingNotification());
+ }
+
+ /**
+ * Show the call streaming notification. This is intended to run outside the Telecom sync lock.
+ *
+ * @param callId the call ID we're streaming.
+ * @param userHandle the userhandle for the call.
+ * @param callerName the name of the caller/callee associated with the call
+ * @param callerAddress the address associated with the caller/callee
+ * @param photoIcon the contact photo icon if available
+ * @param appPackageName the package name for the app to post the notification for
+ * @param connectTimeMillis when the call connected (for chronometer in the notification)
+ */
+ private void showStreamingNotification(final String callId, final UserHandle userHandle,
+ String callerName, Uri callerAddress, Icon photoIcon, String appPackageName,
+ long connectTimeMillis) {
+ Log.i(this, "showStreamingNotification; callid=%s, hasPhoto=%b", callId, photoIcon != null);
+
+ // Use the caller name for the label if available, default to app name if none.
+ if (TextUtils.isEmpty(callerName)) {
+ // App did not provide a caller name, so default to app's name.
+ callerName = mAppLabelProxy.getAppLabel(appPackageName).toString();
+ }
+
+ // Action to hangup; this can use the default hangup action from the call style
+ // notification.
+ Intent hangupIntent = new Intent(TelecomBroadcastIntentProcessor.ACTION_HANGUP_CALL,
+ Uri.fromParts(CALL_ID_SCHEME, callId, null),
+ mContext, TelecomBroadcastReceiver.class);
+ PendingIntent hangupPendingIntent = PendingIntent.getBroadcast(mContext, 0, hangupIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+
+ // Action to switch here.
+ Intent switchHereIntent = new Intent(TelecomBroadcastIntentProcessor.ACTION_STOP_STREAMING,
+ Uri.fromParts(CALL_ID_SCHEME, callId, null),
+ mContext, TelecomBroadcastReceiver.class);
+ PendingIntent switchHerePendingIntent = PendingIntent.getBroadcast(mContext, 0,
+ switchHereIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+
+ // Apply a span to the string to colorize it using the "answer" color.
+ Spannable spannable = new SpannableString(
+ mContext.getString(R.string.call_streaming_notification_action_switch_here));
+ spannable.setSpan(new ForegroundColorSpan(
+ com.android.internal.R.color.call_notification_answer_color), 0, spannable.length(),
+ Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+
+ // Use the "phone link" icon per mock.
+ Icon switchHereIcon = Icon.createWithResource(mContext, R.drawable.gm_phonelink);
+ Notification.Action.Builder switchHereBuilder = new Notification.Action.Builder(
+ switchHereIcon,
+ spannable,
+ switchHerePendingIntent);
+ Notification.Action switchHereAction = switchHereBuilder.build();
+
+ // Notifications use a "person" entity to identify caller/callee.
+ Person.Builder personBuilder = new Person.Builder()
+ .setName(callerName);
+
+ // Some apps use phone numbers to identify; these are something the notification framework
+ // can lookup in contacts to provide more data
+ if (callerAddress != null && PhoneAccount.SCHEME_TEL.equals(callerAddress)) {
+ personBuilder.setUri(callerAddress.toString());
+ }
+ if (photoIcon != null) {
+ personBuilder.setIcon(photoIcon);
+ }
+ Person person = personBuilder.build();
+
+ // Call Style notification requires a full screen intent, so we'll just link in a null
+ // pending intent
+ Intent nullIntent = new Intent();
+ PendingIntent nullPendingIntent = PendingIntent.getBroadcast(mContext, 0, nullIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+
+ Notification.Builder builder = new Notification.Builder(mContext,
+ NotificationChannelManager.CHANNEL_ID_CALL_STREAMING)
+ // Use call style to get the general look and feel for the notification; it provides
+ // a hangup action with the right action already so we can leverage that. The
+ // "switch here" action will be a custom action defined later.
+ .setStyle(Notification.CallStyle.forOngoingCall(person, hangupPendingIntent))
+ .setSmallIcon(R.drawable.ic_phone)
+ .setContentText(mContext.getString(
+ R.string.call_streaming_notification_body))
+ // Report call time
+ .setWhen(connectTimeMillis)
+ .setShowWhen(true)
+ .setUsesChronometer(true)
+ // Set the full screen intent; this is just tricking notification manager into
+ // letting us use this style. Sssh.
+ .setFullScreenIntent(nullPendingIntent, true)
+ .setColorized(true)
+ .addAction(switchHereAction);
+ Notification notification = builder.build();
+
+ synchronized(mNotificationLock) {
+ mIsNotificationShowing = true;
+ mNotificationUserHandle = userHandle;
+ try {
+ mNotificationManager.notifyAsUser(NOTIFICATION_TAG, STREAMING_NOTIFICATION_ID,
+ notification, userHandle);
+ } catch (Exception e) {
+ // We don't want to crash Telecom if something changes with the requirements for the
+ // notification.
+ Log.e(this, e, "Notification post failed.");
+ }
+ }
+ }
+
+ /**
+ * Removes the posted streaming notification. Intended to run outside the telecom lock.
+ */
+ private void hideStreamingNotification() {
+ Log.i(this, "hideStreamingNotification");
+ synchronized(mNotificationLock) {
+ if (mIsNotificationShowing) {
+ mIsNotificationShowing = false;
+ mNotificationManager.cancelAsUser(NOTIFICATION_TAG,
+ STREAMING_NOTIFICATION_ID, mNotificationUserHandle);
+ }
+ }
+ }
+
+ public static Bitmap drawableToBitmap(@Nullable Drawable drawable, int width, int height) {
+ if (drawable == null) {
+ return null;
+ }
+
+ Bitmap bitmap;
+ if (drawable instanceof BitmapDrawable) {
+ bitmap = ((BitmapDrawable) drawable).getBitmap();
+ } else {
+ if (width > 0 || height > 0) {
+ bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ } else if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
+ // Needed for drawables that are just a colour.
+ bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
+ } else {
+ bitmap =
+ Bitmap.createBitmap(
+ drawable.getIntrinsicWidth(),
+ drawable.getIntrinsicHeight(),
+ Bitmap.Config.ARGB_8888);
+ }
+
+ Canvas canvas = new Canvas(bitmap);
+ drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ drawable.draw(canvas);
+ }
+ return bitmap;
+ }
+}
diff --git a/src/com/android/server/telecom/ui/NotificationChannelManager.java b/src/com/android/server/telecom/ui/NotificationChannelManager.java
index 58794a6..a0baa03 100644
--- a/src/com/android/server/telecom/ui/NotificationChannelManager.java
+++ b/src/com/android/server/telecom/ui/NotificationChannelManager.java
@@ -40,6 +40,7 @@
public static final String CHANNEL_ID_AUDIO_PROCESSING = "TelecomBackgroundAudioProcessing";
public static final String CHANNEL_ID_DISCONNECTED_CALLS = "TelecomDisconnectedCalls";
public static final String CHANNEL_ID_IN_CALL_SERVICE_CRASH = "TelecomInCallServiceCrash";
+ public static final String CHANNEL_ID_CALL_STREAMING = "TelecomCallStreaming";
private BroadcastReceiver mLocaleChangeReceiver = new BroadcastReceiver() {
@Override
@@ -63,6 +64,7 @@
createOrUpdateChannel(context, CHANNEL_ID_AUDIO_PROCESSING);
createOrUpdateChannel(context, CHANNEL_ID_DISCONNECTED_CALLS);
createOrUpdateChannel(context, CHANNEL_ID_IN_CALL_SERVICE_CRASH);
+ createOrUpdateChannel(context, CHANNEL_ID_CALL_STREAMING);
}
private void createOrUpdateChannel(Context context, String channelId) {
@@ -127,6 +129,14 @@
lights = true;
vibration = true;
sound = null;
+ case CHANNEL_ID_CALL_STREAMING:
+ name = context.getText(R.string.notification_channel_call_streaming);
+ importance = NotificationManager.IMPORTANCE_DEFAULT;
+ canShowBadge = false;
+ lights = false;
+ vibration = false;
+ sound = null;
+ break;
}
NotificationChannel channel = new NotificationChannel(channelId, name, importance);
diff --git a/src/com/android/server/telecom/voip/IncomingCallTransaction.java b/src/com/android/server/telecom/voip/IncomingCallTransaction.java
index c0bb93d..d35030c 100644
--- a/src/com/android/server/telecom/voip/IncomingCallTransaction.java
+++ b/src/com/android/server/telecom/voip/IncomingCallTransaction.java
@@ -17,6 +17,7 @@
package com.android.server.telecom.voip;
import static android.telecom.CallAttributes.CALL_CAPABILITIES_KEY;
+import static android.telecom.CallAttributes.DISPLAY_NAME_KEY;
import android.os.Bundle;
import android.telecom.CallAttributes;
@@ -80,6 +81,9 @@
mExtras.putString(TelecomManager.TRANSACTION_CALL_ID_KEY, mCallId);
mExtras.putInt(CALL_CAPABILITIES_KEY, callAttributes.getCallCapabilities());
mExtras.putInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE, callAttributes.getCallType());
+ mExtras.putCharSequence(DISPLAY_NAME_KEY, callAttributes.getDisplayName());
+ mExtras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS,
+ callAttributes.getAddress());
return mExtras;
}
}
diff --git a/src/com/android/server/telecom/voip/OutgoingCallTransaction.java b/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
index 0b17da2..b2625e6 100644
--- a/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
+++ b/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
@@ -18,6 +18,7 @@
import static android.Manifest.permission.CALL_PRIVILEGED;
import static android.telecom.CallAttributes.CALL_CAPABILITIES_KEY;
+import static android.telecom.CallAttributes.DISPLAY_NAME_KEY;
import static android.telecom.CallException.CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME;
import android.content.Context;
@@ -126,6 +127,7 @@
mExtras.putInt(CALL_CAPABILITIES_KEY, callAttributes.getCallCapabilities());
mExtras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
callAttributes.getCallType());
+ mExtras.putCharSequence(DISPLAY_NAME_KEY, callAttributes.getDisplayName());
return mExtras;
}
}
diff --git a/src/com/android/server/telecom/voip/VoipCallTransaction.java b/src/com/android/server/telecom/voip/VoipCallTransaction.java
index a1cc13c..38f1d54 100644
--- a/src/com/android/server/telecom/voip/VoipCallTransaction.java
+++ b/src/com/android/server/telecom/voip/VoipCallTransaction.java
@@ -18,6 +18,7 @@
import android.os.Handler;
import android.os.HandlerThread;
+import android.telecom.Log;
import com.android.server.telecom.LoggedHandlerExecutor;
import com.android.server.telecom.TelecomSystem;
@@ -80,7 +81,11 @@
}
finish();
return null;
- });
+ })
+ .exceptionally((throwable) -> {
+ Log.e(this, throwable, "Error while executing transaction.");
+ return null;
+ });;
}
public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
diff --git a/testapps/AndroidManifest.xml b/testapps/AndroidManifest.xml
index dd8258a..645a42b 100644
--- a/testapps/AndroidManifest.xml
+++ b/testapps/AndroidManifest.xml
@@ -259,6 +259,15 @@
</intent-filter>
</service>
+ <service android:name="com.android.server.telecom.testapps.OtherSelfManagedConnectionService"
+ android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
+ android:process="com.android.server.telecom.testapps.SelfMangingCallingApp"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.telecom.ConnectionService"/>
+ </intent-filter>
+ </service>
+
<receiver android:exported="false"
android:process="com.android.server.telecom.testapps.SelfMangingCallingApp"
android:name="com.android.server.telecom.testapps.SelfManagedCallNotificationReceiver"/>
diff --git a/testapps/res/layout/self_managed_sample_main.xml b/testapps/res/layout/self_managed_sample_main.xml
index d26d629..98b879a 100644
--- a/testapps/res/layout/self_managed_sample_main.xml
+++ b/testapps/res/layout/self_managed_sample_main.xml
@@ -55,6 +55,12 @@
android:layout_height="wrap_content"
android:background="@color/test_call_b_color"
android:text="2"/>
+ <RadioButton
+ android:id="@+id/useAcct3Button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@color/test_call_c_color"
+ android:text="3"/>
</RadioGroup>
<TextView
android:id="@+id/hasFocus"
diff --git a/testapps/res/values/colors.xml b/testapps/res/values/colors.xml
index 3939e78..9447ac8 100644
--- a/testapps/res/values/colors.xml
+++ b/testapps/res/values/colors.xml
@@ -17,4 +17,5 @@
<resources>
<color name="test_call_a_color">#f2eebf</color>
<color name="test_call_b_color">#afc5e6</color>
+ <color name="test_call_c_color">#c5afe6</color>
</resources>
diff --git a/testapps/src/com/android/server/telecom/testapps/OtherSelfManagedConnectionService.java b/testapps/src/com/android/server/telecom/testapps/OtherSelfManagedConnectionService.java
new file mode 100644
index 0000000..7bb9830
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/OtherSelfManagedConnectionService.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.testapps;
+
+public class OtherSelfManagedConnectionService extends SelfManagedConnectionService {
+}
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java
index d4661ff..273b060 100644
--- a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java
@@ -46,20 +46,27 @@
public static String SELF_MANAGED_ACCOUNT_1 = "1";
public static String SELF_MANAGED_ACCOUNT_2 = "2";
+ public static String SELF_MANAGED_ACCOUNT_1A = "1A";
public static String SELF_MANAGED_ACCOUNT_3 = "3";
public static String SELF_MANAGED_NAME_1 = "SuperCall";
public static String SELF_MANAGED_NAME_2 = "Mega Call";
- public static String SELF_MANAGED_NAME_3 = "SM Call";
+ public static String SELF_MANAGED_NAME_1A = "SM Call";
+ public static String SELF_MANAGED_NAME_3 = "Sep Process";
public static String CUSTOM_URI_SCHEME = "custom";
private static SelfManagedCallList sInstance;
private static ComponentName COMPONENT_NAME = new ComponentName(
SelfManagedCallList.class.getPackage().getName(),
SelfManagedConnectionService.class.getName());
+ private static ComponentName OTHER_COMPONENT_NAME = new ComponentName(
+ SelfManagedCallList.class.getPackage().getName(),
+ OtherSelfManagedConnectionService.class.getName());
private static Uri SELF_MANAGED_ADDRESS_1 = Uri.fromParts(PhoneAccount.SCHEME_TEL, "555-1212",
"");
private static Uri SELF_MANAGED_ADDRESS_2 = Uri.fromParts(PhoneAccount.SCHEME_SIP,
"me@test.org", "");
+ private static Uri SELF_MANAGED_ADDRESS_3 = Uri.fromParts(PhoneAccount.SCHEME_SIP,
+ "hilda@test.org", "");
private static Map<String, PhoneAccountHandle> mPhoneAccounts = new ArrayMap();
public static SelfManagedCallList getInstance() {
@@ -101,20 +108,29 @@
SELF_MANAGED_NAME_1, true /* areCallsLogged */);
registerPhoneAccount(context, SELF_MANAGED_ACCOUNT_2, SELF_MANAGED_ADDRESS_2,
SELF_MANAGED_NAME_2, false /* areCallsLogged */);
- registerPhoneAccount(context, SELF_MANAGED_ACCOUNT_3, SELF_MANAGED_ADDRESS_1,
- SELF_MANAGED_NAME_3, true /* areCallsLogged */);
+ registerPhoneAccount(context, SELF_MANAGED_ACCOUNT_1A, SELF_MANAGED_ADDRESS_1,
+ SELF_MANAGED_NAME_1A, true /* areCallsLogged */);
+ registerPhoneAccount(context, SELF_MANAGED_ACCOUNT_1A, SELF_MANAGED_ADDRESS_1,
+ SELF_MANAGED_NAME_1A, true /* areCallsLogged */);
+ registerPhoneAccount(context, OTHER_COMPONENT_NAME, SELF_MANAGED_ACCOUNT_3,
+ SELF_MANAGED_ADDRESS_3, SELF_MANAGED_NAME_3, false /* areCallsLogged */);
}
public void registerPhoneAccount(Context context, String id, Uri address, String name,
- boolean areCallsLogged) {
- PhoneAccountHandle handle = new PhoneAccountHandle(COMPONENT_NAME, id);
+ boolean areCallsLogged) {
+ registerPhoneAccount(context, COMPONENT_NAME, id, address, name, areCallsLogged);
+ }
+
+ public void registerPhoneAccount(Context context, ComponentName componentName, String id,
+ Uri address, String name, boolean areCallsLogged) {
+ PhoneAccountHandle handle = new PhoneAccountHandle(componentName, id);
mPhoneAccounts.put(id, handle);
Bundle extras = new Bundle();
extras.putBoolean(PhoneAccount.EXTRA_SUPPORTS_HANDOVER_TO, true);
if (areCallsLogged) {
extras.putBoolean(PhoneAccount.EXTRA_LOG_SELF_MANAGED_CALLS, true);
}
- if (id.equals(SELF_MANAGED_ACCOUNT_3)) {
+ if (id.equals(SELF_MANAGED_ACCOUNT_1A)) {
extras.putBoolean(PhoneAccount.EXTRA_ADD_SELF_MANAGED_CALLS_TO_INCALLSERVICE, true);
}
PhoneAccount.Builder builder = PhoneAccount.builder(handle, name)
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallListAdapter.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallListAdapter.java
index 75ceb62..475f255 100644
--- a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallListAdapter.java
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallListAdapter.java
@@ -166,8 +166,10 @@
SelfManagedConnection.EXTRA_PHONE_ACCOUNT_HANDLE);
if (phoneAccountHandle.getId().equals(SelfManagedCallList.SELF_MANAGED_ACCOUNT_1)) {
result.setBackgroundColor(result.getContext().getColor(R.color.test_call_a_color));
- } else {
+ } else if (phoneAccountHandle.getId().equals(SelfManagedCallList.SELF_MANAGED_ACCOUNT_2)) {
result.setBackgroundColor(result.getContext().getColor(R.color.test_call_b_color));
+ } else {
+ result.setBackgroundColor(result.getContext().getColor(R.color.test_call_c_color));
}
CallAudioState audioState = connection.getCallAudioState();
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java
index 44410d2..5cdaf3d 100644
--- a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java
@@ -43,8 +43,6 @@
import android.widget.TextView;
import android.widget.Toast;
-import com.android.server.telecom.testapps.R;
-
import java.util.Objects;
/**
@@ -66,6 +64,7 @@
private Button mDisableCarMode;
private RadioButton mUseAcct1Button;
private RadioButton mUseAcct2Button;
+ private RadioButton mUseAcct3Button;
private CheckBox mHoldableCheckbox;
private CheckBox mVideoCallCheckbox;
private EditText mNumber;
@@ -165,6 +164,7 @@
}));
mUseAcct1Button = findViewById(R.id.useAcct1Button);
mUseAcct2Button = findViewById(R.id.useAcct2Button);
+ mUseAcct3Button = findViewById(R.id.useAcct3Button);
mHasFocus = findViewById(R.id.hasFocus);
mVideoCallCheckbox = findViewById(R.id.videoCall);
mHoldableCheckbox = findViewById(R.id.holdable);
@@ -183,6 +183,8 @@
return mCallList.getPhoneAccountHandle(SelfManagedCallList.SELF_MANAGED_ACCOUNT_1);
} else if (mUseAcct2Button.isChecked()) {
return mCallList.getPhoneAccountHandle(SelfManagedCallList.SELF_MANAGED_ACCOUNT_2);
+ } else if (mUseAcct3Button.isChecked()) {
+ return mCallList.getPhoneAccountHandle(SelfManagedCallList.SELF_MANAGED_ACCOUNT_3);
}
return null;
}
@@ -214,8 +216,7 @@
private void placeSelfManagedOutgoingCall() {
TelecomManager tm = TelecomManager.from(this);
- PhoneAccountHandle phoneAccountHandle = mCallList.getPhoneAccountHandle(
- SelfManagedCallList.SELF_MANAGED_ACCOUNT_3);
+ PhoneAccountHandle phoneAccountHandle = getSelectedPhoneAccountHandle();
if (mCheckIfPermittedBeforeCalling.isChecked()) {
Toast.makeText(this, R.string.outgoingCallNotPermitted, Toast.LENGTH_SHORT).show();
@@ -264,7 +265,7 @@
private void placeSelfManagedIncomingCall() {
TelecomManager tm = TelecomManager.from(this);
PhoneAccountHandle phoneAccountHandle = mCallList.getPhoneAccountHandle(
- SelfManagedCallList.SELF_MANAGED_ACCOUNT_3);
+ SelfManagedCallList.SELF_MANAGED_ACCOUNT_1A);
if (mCheckIfPermittedBeforeCalling.isChecked()) {
if (!tm.isIncomingCallPermitted(phoneAccountHandle)) {
diff --git a/testapps/streamingtest/Android.bp b/testapps/streamingtest/Android.bp
new file mode 100644
index 0000000..bd0a582
--- /dev/null
+++ b/testapps/streamingtest/Android.bp
@@ -0,0 +1,31 @@
+//
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "streamingTestApp",
+ static_libs: [
+ "androidx.legacy_legacy-support-v4",
+ "guava",
+ ],
+ srcs: ["src/**/*.java"],
+ platform_apis: true,
+ certificate: "platform",
+ privileged: true,
+}
diff --git a/testapps/streamingtest/AndroidManifest.xml b/testapps/streamingtest/AndroidManifest.xml
new file mode 100644
index 0000000..47e4abc
--- /dev/null
+++ b/testapps/streamingtest/AndroidManifest.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ coreApp="true"
+ package="com.android.server.telecom.streamingtest">
+
+ <uses-sdk android:minSdkVersion="28"
+ android:targetSdkVersion="33"/>
+
+ <uses-permission android:name="android.permission.RECORD_AUDIO"/>
+ <uses-permission android:name="android.permission.CALL_AUDIO_INTERCEPTION"/>
+
+ <application android:label="Streaming Test App">
+ <uses-library android:name="android.test.runner"/>
+
+ <service android:name="com.android.server.telecom.streamingtest.StreamingService"
+ android:exported="true"
+ android:permission="android.permission.BIND_CALL_STREAMING_SERVICE">
+ <intent-filter>
+ <action android:name="android.telecom.CallStreamingService"/>
+ </intent-filter>
+ </service>
+ </application>
+</manifest>
diff --git a/testapps/streamingtest/src/com/android/server/telecom/streamingtest/StreamingService.java b/testapps/streamingtest/src/com/android/server/telecom/streamingtest/StreamingService.java
new file mode 100644
index 0000000..c76b349
--- /dev/null
+++ b/testapps/streamingtest/src/com/android/server/telecom/streamingtest/StreamingService.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.streamingtest;
+
+import android.annotation.NonNull;
+import android.content.Intent;
+import android.telecom.CallStreamingService;
+import android.telecom.StreamingCall;
+import android.telecom.Log;
+
+public class StreamingService extends CallStreamingService {
+ @Override
+ public void onCallStreamingStarted(@NonNull StreamingCall call) {
+ Log.i(this, "onCallStreamingStarted: call %s", call);
+ }
+
+ @Override
+ public void onCallStreamingStopped() {
+ Log.i(this, "onCallStreamingStopped");
+ }
+
+ @Override
+ public void onCallStreamingStateChanged(int state) {
+ Log.i(this, "onCallStreamingStateChanged; state=%d", state);
+ }
+
+ @Override
+ public boolean onUnbind(Intent intent) {
+ Log.i(this, "onUnbind");
+ return false;
+ }
+}
diff --git a/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/InCallActivity.java b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/InCallActivity.java
index b868b70..3e53800 100644
--- a/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/InCallActivity.java
+++ b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/InCallActivity.java
@@ -57,6 +57,11 @@
setContentView(R.layout.in_call_activity);
Bundle extras = getIntent().getExtras();
+ // Copy the extras with properties like call direction into the extras so the below
+ // code can access them.
+ if (extras != null && extras.containsKey(Utils.sEXTRAS_KEY)) {
+ extras.putAll(extras.getBundle(Utils.sEXTRAS_KEY));
+ }
if (extras != null) {
mCallDirection = extras.getInt(Utils.sCALL_DIRECTION_KEY, DIRECTION_INCOMING);
}
@@ -211,7 +216,7 @@
Utils.PHONE_ACCOUNT_HANDLE,
mCallDirection,
"Alan Turing",
- Uri.parse("tel:6506959001")).build();
+ Uri.parse("tel:+16506959001")).build();
mTelecomManager.addCall(callAttributes, Runnable::run,
new OutcomeReceiver<CallControl, CallException>() {
diff --git a/tests/src/com/android/server/telecom/tests/BasicCallTests.java b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
index 453450d..9047da3 100644
--- a/tests/src/com/android/server/telecom/tests/BasicCallTests.java
+++ b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
@@ -44,6 +44,7 @@
import android.os.Bundle;
import android.os.Process;
import android.os.UserHandle;
+import android.os.UserManager;
import android.provider.BlockedNumberContract;
import android.telecom.Call;
import android.telecom.CallAudioState;
diff --git a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
index 2d7fcc7..8a7d22c 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -16,6 +16,8 @@
package com.android.server.telecom.tests;
+import static android.provider.CallLog.Calls.USER_MISSED_NOT_RUNNING;
+
import static junit.framework.Assert.assertNotNull;
import static junit.framework.TestCase.fail;
@@ -58,7 +60,9 @@
import android.os.ResultReceiver;
import android.os.SystemClock;
import android.os.UserHandle;
+import android.os.UserManager;
import android.provider.BlockedNumberContract;
+import android.provider.Telephony;
import android.telecom.CallException;
import android.telecom.CallScreeningService;
import android.telecom.CallerInfo;
@@ -121,6 +125,7 @@
import com.android.server.telecom.callfiltering.BlockedNumbersAdapter;
import com.android.server.telecom.callfiltering.CallFilteringResult;
import com.android.server.telecom.ui.AudioProcessingNotification;
+import com.android.server.telecom.ui.CallStreamingNotification;
import com.android.server.telecom.ui.DisconnectedCallNotifier;
import com.android.server.telecom.ui.ToastFactory;
import com.android.server.telecom.voip.TransactionManager;
@@ -166,6 +171,8 @@
ComponentName.unflattenFromString("com.voip/.Stuff"), "Voip1");
private static final PhoneAccountHandle SELF_MANAGED_HANDLE = new PhoneAccountHandle(
ComponentName.unflattenFromString("com.baz/.Self"), "Self");
+ private static final PhoneAccountHandle SELF_MANAGED_2_HANDLE = new PhoneAccountHandle(
+ ComponentName.unflattenFromString("com.baz/.Self2"), "Self2");
private static final PhoneAccount SIM_1_ACCOUNT = new PhoneAccount.Builder(SIM_1_HANDLE, "Sim1")
.setCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION
| PhoneAccount.CAPABILITY_CALL_PROVIDER
@@ -190,6 +197,11 @@
.setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
.setIsEnabled(true)
.build();
+ private static final PhoneAccount SELF_MANAGED_2_ACCOUNT = new PhoneAccount.Builder(
+ SELF_MANAGED_2_HANDLE, "Self2")
+ .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
+ .setIsEnabled(true)
+ .build();
private static final Uri TEST_ADDRESS = Uri.parse("tel:555-1212");
private static final Uri TEST_ADDRESS2 = Uri.parse("tel:555-1213");
private static final Uri TEST_ADDRESS3 = Uri.parse("tel:555-1214");
@@ -246,6 +258,7 @@
@Mock private Ringer.AccessibilityManagerAdapter mAccessibilityManagerAdapter;
@Mock private BlockedNumbersAdapter mBlockedNumbersAdapter;
@Mock private PhoneCapability mPhoneCapability;
+ @Mock private CallStreamingNotification mCallStreamingNotification;
private CallsManager mCallsManager;
@@ -316,7 +329,8 @@
command -> command.run(),
mBlockedNumbersAdapter,
TransactionManager.getTestInstance(),
- mEmergencyCallDiagnosticLogger);
+ mEmergencyCallDiagnosticLogger,
+ mCallStreamingNotification);
when(mPhoneAccountRegistrar.getPhoneAccount(
eq(SELF_MANAGED_HANDLE), any())).thenReturn(SELF_MANAGED_ACCOUNT);
@@ -818,7 +832,7 @@
mCallsManager.answerCall(incomingCall, VideoProfile.STATE_AUDIO_ONLY);
// THEN the ongoing call is held and the focus request for incoming call is sent
- verify(ongoingCall).hold();
+ verify(ongoingCall).hold(anyString());
verifyFocusRequestAndExecuteCallback(incomingCall);
// and the incoming call is answered.
@@ -1084,7 +1098,7 @@
mCallsManager.markCallAsActive(newCall);
// THEN the ongoing call is held
- verify(ongoingCall).hold();
+ verify(ongoingCall).hold(anyString());
verifyFocusRequestAndExecuteCallback(newCall);
// and the new call is active
@@ -1741,6 +1755,57 @@
assertTrue(mCallsManager.makeRoomForOutgoingCall(ongoingCall2));
}
+ /**
+ * Test where a VoIP app adds another new call and has one active already; ensure we hold the
+ * active call. This assumes same connection service in the same app.
+ */
+ @SmallTest
+ @Test
+ public void testMakeRoomForOutgoingCallForSameVoipApp() {
+ Call activeCall = addSpyCall(SELF_MANAGED_HANDLE, null /* connMgr */,
+ CallState.ACTIVE, Connection.CAPABILITY_HOLD | Connection.CAPABILITY_SUPPORT_HOLD,
+ 0 /* properties */);
+ Call newDialingCall = createCall(SELF_MANAGED_HANDLE, CallState.DIALING);
+ newDialingCall.setConnectionProperties(Connection.CAPABILITY_HOLD
+ | Connection.CAPABILITY_SUPPORT_HOLD);
+ assertTrue(mCallsManager.makeRoomForOutgoingCall(newDialingCall));
+ verify(activeCall).hold(anyString());
+ }
+
+ /**
+ * Test where a VoIP app adds another new call and has one active already; ensure we hold the
+ * active call. This assumes different connection services in the same app.
+ */
+ @SmallTest
+ @Test
+ public void testMakeRoomForOutgoingCallForSameVoipAppDifferentConnectionService() {
+ Call activeCall = addSpyCall(SELF_MANAGED_HANDLE, null /* connMgr */,
+ CallState.ACTIVE, Connection.CAPABILITY_HOLD | Connection.CAPABILITY_SUPPORT_HOLD,
+ 0 /* properties */);
+ Call newDialingCall = createCall(SELF_MANAGED_2_HANDLE, CallState.DIALING);
+ newDialingCall.setConnectionProperties(Connection.CAPABILITY_HOLD
+ | Connection.CAPABILITY_SUPPORT_HOLD);
+ assertTrue(mCallsManager.makeRoomForOutgoingCall(newDialingCall));
+ verify(activeCall).hold(anyString());
+ }
+
+ /**
+ * Test where a VoIP app adds another new call and has one active already; ensure we hold the
+ * active call. This assumes different connection services in the same app.
+ */
+ @SmallTest
+ @Test
+ public void testMakeRoomForOutgoingCallForSameNonVoipApp() {
+ Call activeCall = addSpyCall(SIM_1_HANDLE, null /* connMgr */,
+ CallState.ACTIVE, Connection.CAPABILITY_HOLD | Connection.CAPABILITY_SUPPORT_HOLD,
+ 0 /* properties */);
+ Call newDialingCall = createCall(SIM_1_HANDLE, CallState.DIALING);
+ newDialingCall.setConnectionProperties(Connection.CAPABILITY_HOLD
+ | Connection.CAPABILITY_SUPPORT_HOLD);
+ assertTrue(mCallsManager.makeRoomForOutgoingCall(newDialingCall));
+ verify(activeCall, never()).hold(anyString());
+ }
+
@SmallTest
@Test
public void testMakeRoomForOutgoingCallHasOutgoingCallSelectingAccount() {
@@ -2415,6 +2480,65 @@
assertEquals(DEFAULT_CALL_SCREENING_APP, outgoingCall.getPostCallPackageName());
}
+ @SmallTest
+ @Test
+ public void testRejectIncomingCallOnPAHInactive() throws Exception {
+ ConnectionServiceWrapper service = mock(ConnectionServiceWrapper.class);
+ doReturn(SIM_2_HANDLE.getComponentName()).when(service).getComponentName();
+ mCallsManager.addConnectionServiceRepositoryCache(SIM_2_HANDLE.getComponentName(),
+ SIM_2_HANDLE.getUserHandle(), service);
+
+ UserManager um = mContext.getSystemService(UserManager.class);
+ when(um.isQuietModeEnabled(eq(SIM_2_HANDLE.getUserHandle()))).thenReturn(true);
+ Call newCall = mCallsManager.processIncomingCallIntent(
+ SIM_2_HANDLE, new Bundle(), false);
+
+ verify(service, timeout(TEST_TIMEOUT)).createConnectionFailed(any());
+ assertFalse(newCall.isInECBM());
+ assertEquals(USER_MISSED_NOT_RUNNING, newCall.getMissedReason());
+ }
+
+ @SmallTest
+ @Test
+ public void testAcceptIncomingCallOnPAHInactiveAndECBMActive() throws Exception {
+ ConnectionServiceWrapper service = mock(ConnectionServiceWrapper.class);
+ doReturn(SIM_2_HANDLE.getComponentName()).when(service).getComponentName();
+ mCallsManager.addConnectionServiceRepositoryCache(SIM_2_HANDLE.getComponentName(),
+ SIM_2_HANDLE.getUserHandle(), service);
+
+ when(mEmergencyCallHelper.isLastOutgoingEmergencyCallPAH(eq(SIM_2_HANDLE)))
+ .thenReturn(true);
+ UserManager um = mContext.getSystemService(UserManager.class);
+ when(um.isQuietModeEnabled(eq(SIM_2_HANDLE.getUserHandle()))).thenReturn(true);
+ Call newCall = mCallsManager.processIncomingCallIntent(
+ SIM_2_HANDLE, new Bundle(), false);
+
+ assertTrue(newCall.isInECBM());
+ verify(service, timeout(TEST_TIMEOUT).times(0)).createConnectionFailed(any());
+ }
+
+ @SmallTest
+ @Test
+ public void testAcceptIncomingEmergencyCallOnPAHInactive() throws Exception {
+ ConnectionServiceWrapper service = mock(ConnectionServiceWrapper.class);
+ doReturn(SIM_2_HANDLE.getComponentName()).when(service).getComponentName();
+ mCallsManager.addConnectionServiceRepositoryCache(SIM_2_HANDLE.getComponentName(),
+ SIM_2_HANDLE.getUserHandle(), service);
+
+ Bundle extras = new Bundle();
+ extras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, TEST_ADDRESS);
+ TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
+ UserManager um = mContext.getSystemService(UserManager.class);
+ when(um.isQuietModeEnabled(eq(SIM_2_HANDLE.getUserHandle()))).thenReturn(true);
+ when(tm.isEmergencyNumber(any(String.class))).thenReturn(true);
+ Call newCall = mCallsManager.processIncomingCallIntent(
+ SIM_2_HANDLE, extras, false);
+
+ assertFalse(newCall.isInECBM());
+ assertTrue(newCall.isEmergencyCall());
+ verify(service, timeout(TEST_TIMEOUT).times(0)).createConnectionFailed(any());
+ }
+
public class LatchedOutcomeReceiver implements OutcomeReceiver<Boolean,
CallException> {
CountDownLatch mCountDownLatch;
@@ -3026,6 +3150,10 @@
mClockProxy,
mToastFactory);
ongoingCall.setState(initialState, "just cuz");
+ if (targetPhoneAccount == SELF_MANAGED_HANDLE
+ || targetPhoneAccount == SELF_MANAGED_2_HANDLE) {
+ ongoingCall.setIsSelfManaged(true);
+ }
return ongoingCall;
}
diff --git a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
index 7d7a829..16fd630 100644
--- a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
+++ b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
@@ -595,6 +595,34 @@
@MediumTest
@Test
public void
+ testBindToService_UserAssociatedWithCallIsInQuietMode_NonEmergCallECBM_BindsToPrimaryUser()
+ throws Exception {
+ when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mUserHandle);
+ when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+ when(mMockCall.isEmergencyCall()).thenReturn(false);
+ when(mMockCall.isInECBM()).thenReturn(true);
+ when(mMockCall.getUserHandleFromTargetPhoneAccount()).thenReturn(DUMMY_USER_HANDLE);
+ when(mMockContext.getSystemService(eq(UserManager.class)))
+ .thenReturn(mMockUserManager);
+ when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(true);
+ setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
+ setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
+
+ mInCallController.bindToServices(mMockCall);
+
+ ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
+ verify(mMockContext, times(1)).bindServiceAsUser(
+ bindIntentCaptor.capture(),
+ any(ServiceConnection.class),
+ eq(serviceBindingFlags),
+ eq(mUserHandle));
+ Intent bindIntent = bindIntentCaptor.getValue();
+ assertEquals(InCallService.SERVICE_INTERFACE, bindIntent.getAction());
+ }
+
+ @MediumTest
+ @Test
+ public void
testBindToService_UserAssociatedWithCallNotInQuietMode_EmergCallInCallUi_BindsToAssociatedUser()
throws Exception {
when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mUserHandle);
diff --git a/tests/src/com/android/server/telecom/tests/RingerTest.java b/tests/src/com/android/server/telecom/tests/RingerTest.java
index abbfe34..a02415c 100644
--- a/tests/src/com/android/server/telecom/tests/RingerTest.java
+++ b/tests/src/com/android/server/telecom/tests/RingerTest.java
@@ -435,42 +435,65 @@
}
/**
- * assert {@link Ringer#shouldRingForContact(Call, Context) } sets the Call object with suppress
- * caller
- *
- * @throws Exception; should not throw exception.
+ * test shouldRingForContact will suppress the incoming call if matchesCallFilter returns
+ * false (meaning DND is ON and the caller cannot bypass the settings)
*/
@Test
- public void testShouldRingForContact_CallSuppressed() throws Exception {
+ public void testShouldRingForContact_CallSuppressed() {
// WHEN
when(mockCall1.wasDndCheckComputedForCall()).thenReturn(false);
when(mockCall1.getHandle()).thenReturn(Uri.parse(""));
-
when(mContext.getSystemService(NotificationManager.class)).thenReturn(
mockNotificationManager);
+ // suppress the call
when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(false);
- // THEN
+ // run the method under test
assertFalse(mRingerUnderTest.shouldRingForContact(mockCall1));
- verify(mockCall1, atLeastOnce()).setCallIsSuppressedByDoNotDisturb(true);
+
+ // THEN
+ // verify we never set the call object and matchesCallFilter is called
+ verify(mockCall1, never()).setCallIsSuppressedByDoNotDisturb(true);
+ verify(mockNotificationManager, times(1))
+ .matchesCallFilter(any(Bundle.class));
}
/**
- * assert {@link Ringer#shouldRingForContact(Call, Context) } sets the Call object with ring
- * caller
- *
- * @throws Exception; should not throw exception.
+ * test shouldRingForContact will alert the user of an incoming call if matchesCallFilter
+ * returns true (meaning DND is NOT suppressing the caller)
*/
@Test
- public void testShouldRingForContact_CallShouldRing() throws Exception {
+ public void testShouldRingForContact_CallShouldRing() {
// WHEN
when(mockCall1.wasDndCheckComputedForCall()).thenReturn(false);
when(mockCall1.getHandle()).thenReturn(Uri.parse(""));
+ // alert the user of the call
when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(true);
- // THEN
+ // run the method under test
assertTrue(mRingerUnderTest.shouldRingForContact(mockCall1));
- verify(mockCall1, atLeastOnce()).setCallIsSuppressedByDoNotDisturb(false);
+
+ // THEN
+ // verify we never set the call object and matchesCallFilter is called
+ verify(mockCall1, never()).setCallIsSuppressedByDoNotDisturb(false);
+ verify(mockNotificationManager, times(1))
+ .matchesCallFilter(any(Bundle.class));
+ }
+
+ /**
+ * ensure Telecom does not re-query the NotificationManager if the call object already has
+ * the result.
+ */
+ @Test
+ public void testShouldRingForContact_matchesCallFilterIsAlreadyComputed() {
+ // WHEN
+ when(mockCall1.wasDndCheckComputedForCall()).thenReturn(true);
+ when(mockCall1.isCallSuppressedByDoNotDisturb()).thenReturn(true);
+
+ // THEN
+ assertFalse(mRingerUnderTest.shouldRingForContact(mockCall1));
+ verify(mockCall1, never()).setCallIsSuppressedByDoNotDisturb(false);
+ verify(mockNotificationManager, never()).matchesCallFilter(any(Bundle.class));
}
@Test