Require MODIFY_PHONE_STATE for preferred TTY mode am: 266f2436b9
Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/services/Telecomm/+/12673305
Change-Id: I2a73bd57d38a489b275ee8017e762afcba94e4ef
diff --git a/src/com/android/server/telecom/CallIdMapper.java b/src/com/android/server/telecom/CallIdMapper.java
index da2c199..72a51f5 100644
--- a/src/com/android/server/telecom/CallIdMapper.java
+++ b/src/com/android/server/telecom/CallIdMapper.java
@@ -20,6 +20,7 @@
import com.android.internal.annotations.VisibleForTesting;
+import java.util.Collection;
import java.util.Map;
/** Utility to map {@link Call} objects to unique IDs. IDs are generated when a call is added. */
@@ -132,6 +133,10 @@
return mCalls.getValue(callId);
}
+ Collection<Call> getCalls() {
+ return mCalls.mPrimaryMap.values();
+ }
+
void clear() {
mCalls.clear();
}
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 1c46209..d785e47 100755
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -173,6 +173,7 @@
void onConnectionTimeChanged(Call call);
void onConferenceStateChanged(Call call, boolean isConference);
void onCdmaConferenceSwap(Call call);
+ void onSetCamera(Call call, String cameraId);
}
/** Interface used to define the action which is executed delay under some condition. */
@@ -1011,6 +1012,18 @@
}
}
+ /**
+ * Handles a change to the currently active camera for a call by notifying listeners.
+ * @param call The call.
+ * @param cameraId The ID of the camera in use, or {@code null} if no camera is in use.
+ */
+ @Override
+ public void onSetCamera(Call call, String cameraId) {
+ for (CallsManagerListener listener : mListeners) {
+ listener.onSetCamera(call, cameraId);
+ }
+ }
+
public Collection<Call> getCalls() {
return Collections.unmodifiableCollection(mCalls);
}
diff --git a/src/com/android/server/telecom/CallsManagerListenerBase.java b/src/com/android/server/telecom/CallsManagerListenerBase.java
index e0d2831..55c7b53 100644
--- a/src/com/android/server/telecom/CallsManagerListenerBase.java
+++ b/src/com/android/server/telecom/CallsManagerListenerBase.java
@@ -104,4 +104,8 @@
@Override
public void onCdmaConferenceSwap(Call call) {
}
+
+ @Override
+ public void onSetCamera(Call call, String cameraId) {
+ }
}
diff --git a/src/com/android/server/telecom/DeviceIdleControllerAdapter.java b/src/com/android/server/telecom/DeviceIdleControllerAdapter.java
new file mode 100644
index 0000000..d3a798a
--- /dev/null
+++ b/src/com/android/server/telecom/DeviceIdleControllerAdapter.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.PowerWhitelistManager;
+import android.os.RemoteException;
+import android.telecom.Log;
+
+import com.android.internal.telecom.IDeviceIdleControllerAdapter;
+
+/**
+ * Telecom is in the same process as the {@link PowerWhitelistManager}, so we can not make direct
+ * calls to the manager interface, since they will fail in the DeviceIdleController
+ * (see {@link Context#enforceCallingPermission(String, String)}). Instead, we must access it
+ * through SystemService#getLocalService, which is only accessible to the Telecom
+ * core loader service (TelecomLoaderService). Unfortunately, due to the architecture, this means
+ * we must use a Binder to allow services such as this to be accessible.
+ */
+public class DeviceIdleControllerAdapter {
+
+ private static final String TAG = "DeviceIdleControllerAdapter";
+
+ private final IDeviceIdleControllerAdapter mAdapter;
+
+ public DeviceIdleControllerAdapter(IDeviceIdleControllerAdapter adapter) {
+ mAdapter = adapter;
+ }
+
+ /**
+ * Exempts an application from power restrictions for the duration specified. See
+ * DeviceIdleController for more information on how this works.
+ */
+ public void exemptAppTemporarilyForEvent(@NonNull String packageName, long duration,
+ int userHandle, @NonNull String reason) {
+ try {
+ mAdapter.exemptAppTemporarilyForEvent(packageName, duration, userHandle, reason);
+ } catch (RemoteException e) {
+ Log.w(TAG, "exemptAppTemporarilyForEvent e=" + e.getMessage());
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index 6de6dea..72a4a39 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -16,8 +16,11 @@
package com.android.server.telecom;
+import static android.os.Process.myUid;
+
import android.Manifest;
import android.annotation.NonNull;
+import android.app.AppOpsManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.ComponentName;
@@ -214,7 +217,7 @@
Log.startSession("ICSBC.oSD", Log.getPackageAbbreviation(name));
synchronized (mLock) {
try {
- Log.d(this, "onDisconnected: %s", name);
+ Log.d(this, "onServiceDisconnected: %s", name);
mIsBound = false;
onDisconnected();
} finally {
@@ -335,6 +338,8 @@
mInCallServiceInfo.getDisconnectTime()
- mInCallServiceInfo.getBindingStartTime(), mIsNullBinding);
}
+
+ InCallController.this.onDisconnected(mInCallServiceInfo);
} else {
Log.i(InCallController.this, "ICSBC#disconnect: already disconnected; %s",
mInCallServiceInfo);
@@ -867,6 +872,7 @@
private final CallIdMapper mCallIdMapper = new CallIdMapper(Call::getId);
private final Context mContext;
+ private final AppOpsManager mAppOpsManager;
private final TelecomSystem.SyncRoot mLock;
private final CallsManager mCallsManager;
private final SystemStateHelper mSystemStateHelper;
@@ -884,12 +890,24 @@
private final CarModeTracker mCarModeTracker;
+ /**
+ * The package name of the app which is showing the calling UX.
+ */
+ private String mCurrentUserInterfacePackageName = null;
+
+ /**
+ * {@code true} if InCallController is tracking a managed, not external call which is using the
+ * microphone, {@code false} otherwise.
+ */
+ private boolean mIsCallUsingMicrophone = false;
+
public InCallController(Context context, TelecomSystem.SyncRoot lock, CallsManager callsManager,
SystemStateHelper systemStateHelper,
DefaultDialerCache defaultDialerCache, Timeouts.Adapter timeoutsAdapter,
EmergencyCallHelper emergencyCallHelper, CarModeTracker carModeTracker,
ClockProxy clockProxy) {
mContext = context;
+ mAppOpsManager = context.getSystemService(AppOpsManager.class);
mLock = lock;
mCallsManager = callsManager;
mSystemStateHelper = systemStateHelper;
@@ -975,6 +993,7 @@
}
call.removeListener(mCallListener);
mCallIdMapper.removeCall(call);
+ maybeTrackMicrophoneUse(isMuted());
}
@Override
@@ -1048,10 +1067,12 @@
}
Log.i(this, "External call removed from components: %s", componentsUpdated);
}
+ maybeTrackMicrophoneUse(isMuted());
}
@Override
public void onCallStateChanged(Call call, int oldState, int newState) {
+ maybeTrackMicrophoneUse(isMuted());
updateCall(call);
}
@@ -1069,6 +1090,7 @@
if (!mInCallServices.isEmpty()) {
Log.i(this, "Calling onAudioStateChanged, audioState: %s -> %s", oldCallAudioState,
newCallAudioState);
+ maybeTrackMicrophoneUse(newCallAudioState.isMuted());
for (IInCallService inCallService : mInCallServices.values()) {
try {
inCallService.onCallAudioStateChanged(newCallAudioState);
@@ -1133,6 +1155,23 @@
updateCall(call);
}
+ /**
+ * Track changes to camera usage for a call.
+ * @param call The call.
+ * @param cameraId The id of the camera to use, or {@code null} if camera is off.
+ */
+ @Override
+ public void onSetCamera(Call call, String cameraId) {
+ Log.i(this, "onSetCamera callId=%s, cameraId=%s", call.getId(), cameraId);
+ if (cameraId != null) {
+ mAppOpsManager.startOp(AppOpsManager.OP_PHONE_CALL_CAMERA, myUid(),
+ mContext.getOpPackageName(), false, null, null);
+ } else {
+ mAppOpsManager.finishOp(AppOpsManager.OP_PHONE_CALL_CAMERA, myUid(),
+ mContext.getOpPackageName(), null);
+ }
+ }
+
void bringToForeground(boolean showDialpad) {
if (!mInCallServices.isEmpty()) {
for (IInCallService inCallService : mInCallServices.values()) {
@@ -1509,6 +1548,11 @@
private boolean onConnected(InCallServiceInfo info, IBinder service) {
Log.i(this, "onConnected to %s", info.getComponentName());
+ if (info.getType() == IN_CALL_SERVICE_TYPE_CAR_MODE_UI
+ || info.getType() == IN_CALL_SERVICE_TYPE_SYSTEM_UI
+ || info.getType() == IN_CALL_SERVICE_TYPE_DIALER_UI) {
+ trackCallingUserInterfaceStarted(info);
+ }
IInCallService inCallService = IInCallService.Stub.asInterface(service);
mInCallServices.put(info, inCallService);
@@ -1559,7 +1603,11 @@
inCallService.onCanAddCallChanged(mCallsManager.canAddCall());
} catch (RemoteException ignored) {
}
- mBindingFuture.complete(true);
+ // Don't complete the binding future for non-ui incalls
+ if (info.getType() != IN_CALL_SERVICE_TYPE_NON_UI) {
+ mBindingFuture.complete(true);
+ }
+
Log.i(this, "%s calls sent to InCallService.", numCallsSent);
return true;
}
@@ -1571,7 +1619,11 @@
*/
private void onDisconnected(InCallServiceInfo disconnectedInfo) {
Log.i(this, "onDisconnected from %s", disconnectedInfo.getComponentName());
-
+ if (disconnectedInfo.getType() == IN_CALL_SERVICE_TYPE_CAR_MODE_UI
+ || disconnectedInfo.getType() == IN_CALL_SERVICE_TYPE_SYSTEM_UI
+ || disconnectedInfo.getType() == IN_CALL_SERVICE_TYPE_DIALER_UI) {
+ trackCallingUserInterfaceStopped(disconnectedInfo);
+ }
mInCallServices.remove(disconnectedInfo);
}
@@ -1637,6 +1689,7 @@
mCallIdMapper.addCall(call);
call.addListener(mCallListener);
}
+ maybeTrackMicrophoneUse(isMuted());
}
/**
@@ -1811,6 +1864,69 @@
}
}
+ /**
+ * Tracks start of microphone use on binding to the current calling UX.
+ * @param info
+ */
+ private void trackCallingUserInterfaceStarted(InCallServiceInfo info) {
+ String packageName = info.getComponentName().getPackageName();
+ if (!Objects.equals(mCurrentUserInterfacePackageName, packageName)) {
+ Log.i(this, "trackCallingUserInterfaceStarted: %s is now calling UX.", packageName);
+ mCurrentUserInterfacePackageName = packageName;
+ }
+ maybeTrackMicrophoneUse(isMuted());
+ }
+
+ /**
+ * Tracks stop of microphone use on unbind from the current calling UX.
+ * @param info
+ */
+ private void trackCallingUserInterfaceStopped(InCallServiceInfo info) {
+ maybeTrackMicrophoneUse(isMuted());
+ mCurrentUserInterfacePackageName = null;
+ String packageName = info.getComponentName().getPackageName();
+ Log.i(this, "trackCallingUserInterfaceStopped: %s is no longer calling UX", packageName);
+ }
+
+ /**
+ * As calls are added, removed and change between external and non-external status, track
+ * whether the current active calling UX is using the microphone. We assume if there is a
+ * managed call present and the mic is not muted that the microphone is in use.
+ */
+ private void maybeTrackMicrophoneUse(boolean isMuted) {
+ boolean wasTrackingManagedCall = mIsCallUsingMicrophone;
+ mIsCallUsingMicrophone = isTrackingManagedAliveCall() && !isMuted;
+ if (wasTrackingManagedCall != mIsCallUsingMicrophone) {
+ if (mIsCallUsingMicrophone) {
+ mAppOpsManager.startOp(AppOpsManager.OP_PHONE_CALL_MICROPHONE, myUid(),
+ mContext.getOpPackageName(), false, null, null);
+ } else {
+ mAppOpsManager.finishOp(AppOpsManager.OP_PHONE_CALL_MICROPHONE, myUid(),
+ mContext.getOpPackageName(), null);
+ }
+ }
+ }
+
+ /**
+ * @return {@code true} if InCallController is tracking a managed call (i.e. not self managed
+ * and not external) that is active.
+ */
+ private boolean isTrackingManagedAliveCall() {
+ return mCallIdMapper.getCalls().stream().anyMatch(c -> !c.isExternalCall()
+ && !c.isSelfManaged() && c.isAlive() && c.getState() != CallState.ON_HOLD
+ && c.getState() != CallState.AUDIO_PROCESSING);
+ }
+
+ /**
+ * @return {@code true} if the audio is currently muted, {@code false} otherwise.
+ */
+ private boolean isMuted() {
+ if (mCallsManager.getAudioState() == null) {
+ return false;
+ }
+ return mCallsManager.getAudioState().isMuted();
+ }
+
private void sendCrashedInCallServiceNotification(String packageName) {
PackageManager packageManager = mContext.getPackageManager();
CharSequence appName;
diff --git a/src/com/android/server/telecom/InternalServiceRetrieverAdapter.java b/src/com/android/server/telecom/InternalServiceRetrieverAdapter.java
new file mode 100644
index 0000000..4a2c3d1
--- /dev/null
+++ b/src/com/android/server/telecom/InternalServiceRetrieverAdapter.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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;
+
+import android.os.RemoteException;
+
+import com.android.internal.telecom.IInternalServiceRetriever;
+
+/**
+ * Contains all services that Telecom must access that are only accessible as a local service as
+ * part of the SYSTEM process.
+ */
+public class InternalServiceRetrieverAdapter {
+
+ private final IInternalServiceRetriever mRetriever;
+
+ public InternalServiceRetrieverAdapter(IInternalServiceRetriever retriever) {
+ mRetriever = retriever;
+ }
+
+ public DeviceIdleControllerAdapter getDeviceIdleController() {
+ try {
+ return new DeviceIdleControllerAdapter(mRetriever.getDeviceIdleController());
+ } catch (RemoteException e) {
+ // This should not happen - if it does, there is a bad configuration as this should
+ // all be local.
+ throw new IllegalStateException(e);
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java
index 807cc2d..08389b9 100644
--- a/src/com/android/server/telecom/TelecomSystem.java
+++ b/src/com/android/server/telecom/TelecomSystem.java
@@ -207,7 +207,8 @@
ClockProxy clockProxy,
RoleManagerAdapter roleManagerAdapter,
IncomingCallFilter.Factory incomingCallFilterFactory,
- ContactsAsyncHelper.Factory contactsAsyncHelperFactory) {
+ ContactsAsyncHelper.Factory contactsAsyncHelperFactory,
+ DeviceIdleControllerAdapter deviceIdleControllerAdapter) {
mContext = context.getApplicationContext();
LogUtils.initLogging(mContext);
DefaultDialerManagerAdapter defaultDialerAdapter =
@@ -241,7 +242,8 @@
SystemStateHelper systemStateHelper = new SystemStateHelper(mContext);
mMissedCallNotifier = missedCallNotifierImplFactory
- .makeMissedCallNotifierImpl(mContext, mPhoneAccountRegistrar, defaultDialerCache);
+ .makeMissedCallNotifierImpl(mContext, mPhoneAccountRegistrar, defaultDialerCache,
+ deviceIdleControllerAdapter);
DisconnectedCallNotifier.Factory disconnectedCallNotifierFactory =
new DisconnectedCallNotifier.Default();
diff --git a/src/com/android/server/telecom/Timeouts.java b/src/com/android/server/telecom/Timeouts.java
index a701b88..2309596 100644
--- a/src/com/android/server/telecom/Timeouts.java
+++ b/src/com/android/server/telecom/Timeouts.java
@@ -211,4 +211,15 @@
public static long getCallRecordingToneRepeatIntervalMillis(ContentResolver contentResolver) {
return get(contentResolver, "call_recording_tone_repeat_interval", 15000L /* 15 seconds */);
}
+
+ /**
+ * Returns the number of milliseconds for which the system should exempt the default dialer from
+ * power save restrictions due to the dialer needing to handle a missed call notification
+ * (update call log, check VVM, etc...).
+ */
+ public static long getDialerMissedCallPowerSaveExemptionTimeMillis(
+ ContentResolver contentResolver) {
+ return get(contentResolver, "dialer_missed_call_power_save_exemption_time_millis",
+ 30000L /*30 seconds*/);
+ }
}
diff --git a/src/com/android/server/telecom/VideoProviderProxy.java b/src/com/android/server/telecom/VideoProviderProxy.java
index 364e0f4..df11403 100644
--- a/src/com/android/server/telecom/VideoProviderProxy.java
+++ b/src/com/android/server/telecom/VideoProviderProxy.java
@@ -55,6 +55,7 @@
*/
public interface Listener {
void onSessionModifyRequestReceived(Call call, VideoProfile videoProfile);
+ void onSetCamera(Call call, String cameraId);
}
/**
@@ -346,6 +347,12 @@
return;
}
}
+
+ // Inform other Telecom components of the change in camera status.
+ for (Listener listener : mListeners) {
+ listener.onSetCamera(mCall, cameraId);
+ }
+
try {
mConectionServiceVideoProvider.setCamera(cameraId, callingPackage,
targetSdkVersion);
diff --git a/src/com/android/server/telecom/callfiltering/CallScreeningServiceFilter.java b/src/com/android/server/telecom/callfiltering/CallScreeningServiceFilter.java
index 1e52c5a..486cd8d 100644
--- a/src/com/android/server/telecom/callfiltering/CallScreeningServiceFilter.java
+++ b/src/com/android/server/telecom/callfiltering/CallScreeningServiceFilter.java
@@ -289,7 +289,12 @@
public void unbindCallScreeningService() {
if (mConnection != null) {
- mContext.unbindService(mConnection);
+ try {
+ mContext.unbindService(mConnection);
+ } catch (IllegalArgumentException e) {
+ Log.i(this, "Exception when unbind service %s : %s", mConnection,
+ e.getMessage());
+ }
}
mConnection = null;
}
diff --git a/src/com/android/server/telecom/components/TelecomService.java b/src/com/android/server/telecom/components/TelecomService.java
index e20da80..b6705f4 100644
--- a/src/com/android/server/telecom/components/TelecomService.java
+++ b/src/com/android/server/telecom/components/TelecomService.java
@@ -30,6 +30,10 @@
import android.telecom.Log;
import android.telecom.CallerInfoAsyncQuery;
+
+import com.android.internal.telecom.IInternalServiceRetriever;
+import com.android.internal.telecom.ITelecomLoader;
+import com.android.internal.telecom.ITelecomService;
import com.android.server.telecom.AsyncRingtonePlayer;
import com.android.server.telecom.BluetoothAdapterProxy;
import com.android.server.telecom.BluetoothPhoneServiceImpl;
@@ -41,16 +45,17 @@
import com.android.server.telecom.ConnectionServiceFocusManager;
import com.android.server.telecom.ContactsAsyncHelper;
import com.android.server.telecom.DefaultDialerCache;
+import com.android.server.telecom.DeviceIdleControllerAdapter;
import com.android.server.telecom.HeadsetMediaButton;
import com.android.server.telecom.HeadsetMediaButtonFactory;
import com.android.server.telecom.InCallWakeLockControllerFactory;
import com.android.server.telecom.CallAudioManager;
+import com.android.server.telecom.InternalServiceRetrieverAdapter;
import com.android.server.telecom.PhoneAccountRegistrar;
import com.android.server.telecom.PhoneNumberUtilsAdapterImpl;
import com.android.server.telecom.ProximitySensorManagerFactory;
import com.android.server.telecom.InCallWakeLockController;
import com.android.server.telecom.ProximitySensorManager;
-import com.android.server.telecom.R;
import com.android.server.telecom.RoleManagerAdapterImpl;
import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.TelecomWakeLock;
@@ -68,10 +73,17 @@
@Override
public IBinder onBind(Intent intent) {
Log.d(this, "onBind");
- initializeTelecomSystem(this);
- synchronized (getTelecomSystem().getLock()) {
- return getTelecomSystem().getTelecomServiceImpl().getBinder();
- }
+ return new ITelecomLoader.Stub() {
+ @Override
+ public ITelecomService createTelecomService(IInternalServiceRetriever retriever) {
+ InternalServiceRetrieverAdapter adapter =
+ new InternalServiceRetrieverAdapter(retriever);
+ initializeTelecomSystem(TelecomService.this, adapter);
+ synchronized (getTelecomSystem().getLock()) {
+ return getTelecomSystem().getTelecomServiceImpl().getBinder();
+ }
+ }
+ };
}
/**
@@ -84,7 +96,8 @@
*
* @param context
*/
- static void initializeTelecomSystem(Context context) {
+ static void initializeTelecomSystem(Context context,
+ InternalServiceRetrieverAdapter internalServiceRetriever) {
if (TelecomSystem.getInstance() == null) {
NotificationChannelManager notificationChannelManager =
new NotificationChannelManager();
@@ -98,9 +111,11 @@
public MissedCallNotifierImpl makeMissedCallNotifierImpl(
Context context,
PhoneAccountRegistrar phoneAccountRegistrar,
- DefaultDialerCache defaultDialerCache) {
+ DefaultDialerCache defaultDialerCache,
+ DeviceIdleControllerAdapter idleControllerAdapter) {
return new MissedCallNotifierImpl(context,
- phoneAccountRegistrar, defaultDialerCache);
+ phoneAccountRegistrar, defaultDialerCache,
+ idleControllerAdapter);
}
},
new CallerInfoAsyncQueryFactory() {
@@ -191,7 +206,8 @@
new RoleManagerAdapterImpl(context,
(RoleManager) context.getSystemService(Context.ROLE_SERVICE)),
new IncomingCallFilter.Factory(),
- new ContactsAsyncHelper.Factory()));
+ new ContactsAsyncHelper.Factory(),
+ internalServiceRetriever.getDeviceIdleController()));
}
if (BluetoothAdapter.getDefaultAdapter() != null) {
context.startService(new Intent(context, BluetoothPhoneService.class));
diff --git a/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java b/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
index a0eca8f..e37fe6b 100644
--- a/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
+++ b/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
@@ -19,8 +19,10 @@
import static android.Manifest.permission.READ_PHONE_STATE;
import android.annotation.NonNull;
+import android.app.BroadcastOptions;
import android.content.ContentProvider;
import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
import android.telecom.Logging.Runnable;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
@@ -29,11 +31,13 @@
import com.android.server.telecom.CallsManagerListenerBase;
import com.android.server.telecom.Constants;
import com.android.server.telecom.DefaultDialerCache;
+import com.android.server.telecom.DeviceIdleControllerAdapter;
import com.android.server.telecom.MissedCallNotifier;
import com.android.server.telecom.PhoneAccountRegistrar;
import com.android.server.telecom.R;
import com.android.server.telecom.TelecomBroadcastIntentProcessor;
import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.Timeouts;
import com.android.server.telecom.components.TelecomBroadcastReceiver;
import android.app.Notification;
@@ -86,7 +90,8 @@
public interface MissedCallNotifierImplFactory {
MissedCallNotifier makeMissedCallNotifierImpl(Context context,
PhoneAccountRegistrar phoneAccountRegistrar,
- DefaultDialerCache defaultDialerCache);
+ DefaultDialerCache defaultDialerCache,
+ DeviceIdleControllerAdapter deviceIdleControllerAdapter);
}
public interface NotificationBuilderFactory {
@@ -124,12 +129,14 @@
private static final int MISSED_CALL_NOTIFICATION_ID = 1;
private static final String NOTIFICATION_TAG = MissedCallNotifierImpl.class.getSimpleName();
+ private static final String MISSED_CALL_POWER_SAVE_REASON = "missed-call";
private final Context mContext;
private final PhoneAccountRegistrar mPhoneAccountRegistrar;
private final NotificationManager mNotificationManager;
private final NotificationBuilderFactory mNotificationBuilderFactory;
private final DefaultDialerCache mDefaultDialerCache;
+ private final DeviceIdleControllerAdapter mDeviceIdleControllerAdapter;
private UserHandle mCurrentUserHandle;
// Used to track the number of missed calls.
@@ -138,19 +145,22 @@
private List<UserHandle> mUsersToLoadAfterBootComplete = new ArrayList<>();
public MissedCallNotifierImpl(Context context, PhoneAccountRegistrar phoneAccountRegistrar,
- DefaultDialerCache defaultDialerCache) {
+ DefaultDialerCache defaultDialerCache,
+ DeviceIdleControllerAdapter deviceIdleControllerAdapter) {
this(context, phoneAccountRegistrar, defaultDialerCache,
- new DefaultNotificationBuilderFactory());
+ new DefaultNotificationBuilderFactory(), deviceIdleControllerAdapter);
}
public MissedCallNotifierImpl(Context context,
PhoneAccountRegistrar phoneAccountRegistrar,
DefaultDialerCache defaultDialerCache,
- NotificationBuilderFactory notificationBuilderFactory) {
+ NotificationBuilderFactory notificationBuilderFactory,
+ DeviceIdleControllerAdapter deviceIdleControllerAdapter) {
mContext = context;
mPhoneAccountRegistrar = phoneAccountRegistrar;
mNotificationManager =
(NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ mDeviceIdleControllerAdapter = deviceIdleControllerAdapter;
mDefaultDialerCache = defaultDialerCache;
mNotificationBuilderFactory = notificationBuilderFactory;
@@ -162,7 +172,8 @@
public void clearMissedCalls(UserHandle userHandle) {
// If the default dialer is showing the missed call notification then it will modify the
// call log and we don't have to do anything here.
- if (!shouldManageNotificationThroughDefaultDialer(userHandle)) {
+ String dialerPackage = getDefaultDialerPackage(userHandle);
+ if (!shouldManageNotificationThroughDefaultDialer(dialerPackage, userHandle)) {
markMissedCallsAsRead(userHandle);
}
cancelMissedCallNotification(userHandle);
@@ -194,6 +205,15 @@
}.prepare());
}
+ private String getDefaultDialerPackage(UserHandle userHandle) {
+ String dialerPackage = mDefaultDialerCache.getDefaultDialerApplication(
+ userHandle.getIdentifier());
+ if (TextUtils.isEmpty(dialerPackage)) {
+ return null;
+ }
+ return dialerPackage;
+ }
+
/**
* Returns the missed-call notification intent to send to the default dialer for the given user.
* Note, the passed in userHandle is always the non-managed user for SIM calls (multi-user
@@ -204,18 +224,16 @@
* handle of the phone account. This could be a managed user. In that case we return the default
* dialer for the given user which could be a managed (work profile) dialer.
*/
- private Intent getShowMissedCallIntentForDefaultDialer(UserHandle userHandle) {
- String dialerPackage = mDefaultDialerCache.getDefaultDialerApplication(
- userHandle.getIdentifier());
- if (TextUtils.isEmpty(dialerPackage)) {
- return null;
- }
+ private Intent getShowMissedCallIntentForDefaultDialer(String dialerPackage) {
return new Intent(TelecomManager.ACTION_SHOW_MISSED_CALLS_NOTIFICATION)
.setPackage(dialerPackage);
}
- private boolean shouldManageNotificationThroughDefaultDialer(UserHandle userHandle) {
- Intent intent = getShowMissedCallIntentForDefaultDialer(userHandle);
+ private boolean shouldManageNotificationThroughDefaultDialer(String dialerPackage,
+ UserHandle userHandle) {
+ if (TextUtils.isEmpty(dialerPackage)) return false;
+
+ Intent intent = getShowMissedCallIntentForDefaultDialer(dialerPackage);
if (intent == null) {
return false;
}
@@ -225,9 +243,29 @@
return receivers.size() > 0;
}
- private void sendNotificationThroughDefaultDialer(CallInfo callInfo, UserHandle userHandle) {
+ /**
+ * For dialers that manage missed call handling themselves, we must temporarily add them to the
+ * power save exemption list, as they must perform operations such as modifying the call log and
+ * power save restrictions can cause these types of operations to not complete (sometimes
+ * causing ANRs).
+ */
+ private Bundle exemptFromPowerSavingTemporarily(String dialerPackage, UserHandle handle) {
+ if (TextUtils.isEmpty(dialerPackage)) {
+ return null;
+ }
+ BroadcastOptions bopts = BroadcastOptions.makeBasic();
+ long duration = Timeouts.getDialerMissedCallPowerSaveExemptionTimeMillis(
+ mContext.getContentResolver());
+ mDeviceIdleControllerAdapter.exemptAppTemporarilyForEvent(dialerPackage, duration,
+ handle.getIdentifier(), MISSED_CALL_POWER_SAVE_REASON);
+ bopts.setTemporaryAppWhitelistDuration(duration);
+ return bopts.toBundle();
+ }
+
+ private void sendNotificationThroughDefaultDialer(String dialerPackage, CallInfo callInfo,
+ UserHandle userHandle) {
int count = mMissedCallCounts.get(userHandle).get();
- Intent intent = getShowMissedCallIntentForDefaultDialer(userHandle)
+ Intent intent = getShowMissedCallIntentForDefaultDialer(dialerPackage)
.setFlags(Intent.FLAG_RECEIVER_FOREGROUND)
.putExtra(TelecomManager.EXTRA_CLEAR_MISSED_CALLS_INTENT,
createClearMissedCallsPendingIntent(userHandle))
@@ -248,7 +286,8 @@
Log.w(this, "Showing missed calls through default dialer.");
- mContext.sendBroadcastAsUser(intent, userHandle, READ_PHONE_STATE);
+ Bundle options = exemptFromPowerSavingTemporarily(dialerPackage, userHandle);
+ mContext.sendBroadcastAsUser(intent, userHandle, READ_PHONE_STATE, options);
}
/**
@@ -276,8 +315,9 @@
mMissedCallCounts.putIfAbsent(userHandle, new AtomicInteger(0));
int missCallCounts = mMissedCallCounts.get(userHandle).incrementAndGet();
- if (shouldManageNotificationThroughDefaultDialer(userHandle)) {
- sendNotificationThroughDefaultDialer(callInfo, userHandle);
+ String dialerPackage = getDefaultDialerPackage(userHandle);
+ if (shouldManageNotificationThroughDefaultDialer(dialerPackage, userHandle)) {
+ sendNotificationThroughDefaultDialer(dialerPackage, callInfo, userHandle);
return;
}
@@ -393,8 +433,9 @@
mMissedCallCounts.putIfAbsent(userHandle, new AtomicInteger(0));
mMissedCallCounts.get(userHandle).set(0);
- if (shouldManageNotificationThroughDefaultDialer(userHandle)) {
- sendNotificationThroughDefaultDialer(null, userHandle);
+ String dialerPackage = getDefaultDialerPackage(userHandle);
+ if (shouldManageNotificationThroughDefaultDialer(dialerPackage, userHandle)) {
+ sendNotificationThroughDefaultDialer(dialerPackage, null, userHandle);
return;
}
diff --git a/tests/src/com/android/server/telecom/tests/CallScreeningServiceFilterTest.java b/tests/src/com/android/server/telecom/tests/CallScreeningServiceFilterTest.java
index 1345c01..c7b3a7e 100644
--- a/tests/src/com/android/server/telecom/tests/CallScreeningServiceFilterTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallScreeningServiceFilterTest.java
@@ -23,6 +23,7 @@
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -211,6 +212,19 @@
@SmallTest
@Test
+ public void testUnbindingException() {
+ // Make sure that exceptions when unbinding won't make the device crash.
+ doThrow(new IllegalArgumentException()).when(mContext)
+ .unbindService(nullable(ServiceConnection.class));
+ CallScreeningServiceFilter filter = new CallScreeningServiceFilter(mCall, PKG_NAME,
+ CallScreeningServiceFilter.PACKAGE_TYPE_CARRIER, mContext, mCallsManager,
+ mAppLabelProxy, mParcelableCallUtilsConverter);
+ filter.startFilterLookup(inputResult);
+ filter.unbindCallScreeningService();
+ }
+
+ @SmallTest
+ @Test
public void testAllowCall() throws Exception {
CallScreeningServiceFilter filter = new CallScreeningServiceFilter(mCall, PKG_NAME,
CallScreeningServiceFilter.PACKAGE_TYPE_CARRIER, mContext, mCallsManager,
diff --git a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
index 7effc47..af062d7 100644
--- a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
+++ b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
@@ -39,6 +39,7 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
+import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
@@ -52,6 +53,7 @@
import android.os.Handler;
import android.os.IInterface;
import android.os.PersistableBundle;
+import android.os.PowerWhitelistManager;
import android.os.UserHandle;
import android.os.UserManager;
import android.telecom.CallAudioState;
@@ -320,6 +322,12 @@
}
@Override
+ public void sendBroadcastAsUser(Intent intent, UserHandle user, String receiverPermission,
+ Bundle options) {
+ // Override so that this can be verified via spy.
+ }
+
+ @Override
public void sendOrderedBroadcastAsUser(Intent intent, UserHandle user,
String receiverPermission, BroadcastReceiver resultReceiver, Handler scheduler,
int initialCode, String initialData, Bundle initialExtras) {
@@ -434,6 +442,7 @@
ArrayListMultimap.create();
private final Map<ComponentName, IInterface> mServiceByComponentName = new HashMap<>();
private final Map<ComponentName, ServiceInfo> mServiceInfoByComponentName = new HashMap<>();
+ private final Map<ComponentName, ActivityInfo> mActivityInfoByComponentName = new HashMap<>();
private final Map<IInterface, ComponentName> mComponentNameByService = new HashMap<>();
private final Map<ServiceConnection, IInterface> mServiceByServiceConnection = new HashMap<>();
@@ -507,6 +516,24 @@
}
}).when(mPackageManager).queryIntentServicesAsUser((Intent) any(), anyInt(), anyInt());
+ doAnswer(new Answer<List<ResolveInfo>>() {
+ @Override
+ public List<ResolveInfo> answer(InvocationOnMock invocation) throws Throwable {
+ return doQueryIntentReceivers(
+ (Intent) invocation.getArguments()[0],
+ (Integer) invocation.getArguments()[1]);
+ }
+ }).when(mPackageManager).queryBroadcastReceivers((Intent) any(), anyInt());
+
+ doAnswer(new Answer<List<ResolveInfo>>() {
+ @Override
+ public List<ResolveInfo> answer(InvocationOnMock invocation) throws Throwable {
+ return doQueryIntentReceivers(
+ (Intent) invocation.getArguments()[0],
+ (Integer) invocation.getArguments()[1]);
+ }
+ }).when(mPackageManager).queryBroadcastReceiversAsUser((Intent) any(), anyInt(), anyInt());
+
// By default, tests use non-ui apps instead of 3rd party companion apps.
when(mPackageManager.checkPermission(
matches(Manifest.permission.CALL_COMPANION_APP), anyString()))
@@ -585,6 +612,14 @@
eq(componentName.getPackageName()))).thenReturn(PackageManager.PERMISSION_GRANTED);
}
+ public void addIntentReceiver(String action, ComponentName name) {
+ mComponentNamesByAction.put(action, name);
+ ActivityInfo activityInfo = new ActivityInfo();
+ activityInfo.packageName = name.getPackageName();
+ activityInfo.name = name.getClassName();
+ mActivityInfoByComponentName.put(name, activityInfo);
+ }
+
public void putResource(int id, final String value) {
when(mResources.getText(eq(id))).thenReturn(value);
when(mResources.getString(eq(id))).thenReturn(value);
@@ -635,4 +670,14 @@
}
return result;
}
+
+ private List<ResolveInfo> doQueryIntentReceivers(Intent intent, int flags) {
+ List<ResolveInfo> result = new ArrayList<>();
+ for (ComponentName componentName : mComponentNamesByAction.get(intent.getAction())) {
+ ResolveInfo resolveInfo = new ResolveInfo();
+ resolveInfo.activityInfo = mActivityInfoByComponentName.get(componentName);
+ result.add(resolveInfo);
+ }
+ return result;
+ }
}
diff --git a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
index df3bf82..78b3332 100644
--- a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
+++ b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
@@ -23,6 +23,7 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.matches;
import static org.mockito.ArgumentMatchers.nullable;
@@ -39,6 +40,7 @@
import static org.mockito.Mockito.when;
import android.Manifest;
+import android.app.AppOpsManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.UiModeManager;
@@ -58,8 +60,8 @@
import android.os.IBinder;
import android.os.Looper;
import android.os.UserHandle;
+import android.telecom.CallAudioState;
import android.telecom.InCallService;
-import android.telecom.Log;
import android.telecom.ParcelableCall;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
@@ -98,6 +100,7 @@
import java.util.Collections;
import java.util.LinkedList;
+import java.util.List;
import java.util.concurrent.CompletableFuture;
@RunWith(JUnit4.class)
@@ -109,6 +112,7 @@
@Mock PackageManager mMockPackageManager;
@Mock Call mMockCall;
@Mock Resources mMockResources;
+ @Mock AppOpsManager mMockAppOpsManager;
@Mock MockContext mMockContext;
@Mock Timeouts.Adapter mTimeoutsAdapter;
@Mock DefaultDialerCache mDefaultDialerCache;
@@ -133,6 +137,10 @@
private static final String CAR2_CLASS = "carcls";
private static final int CAR_UID = 4;
private static final int CAR2_UID = 5;
+ private static final String NONUI_PKG = "nonui_pkg";
+ private static final String NONUI_CLASS = "nonui_cls";
+ private static final int NONUI_UID = 6;
+
private static final PhoneAccountHandle PA_HANDLE =
new PhoneAccountHandle(new ComponentName("pa_pkg", "pa_cls"), "pa_id");
@@ -148,6 +156,7 @@
MockitoAnnotations.initMocks(this);
when(mMockCall.getAnalytics()).thenReturn(new Analytics.CallInfo());
doReturn(mMockResources).when(mMockContext).getResources();
+ doReturn(mMockAppOpsManager).when(mMockContext).getSystemService(AppOpsManager.class);
doReturn(SYS_PKG).when(mMockResources).getString(
com.android.internal.R.string.config_defaultDialer);
doReturn(SYS_CLASS).when(mMockResources).getString(R.string.incall_default_class);
@@ -158,11 +167,11 @@
mEmergencyCallHelper = new EmergencyCallHelper(mMockContext, mDefaultDialerCache,
mTimeoutsAdapter);
when(mMockCallsManager.getRoleManagerAdapter()).thenReturn(mMockRoleManagerAdapter);
+ when(mMockContext.getSystemService(eq(Context.NOTIFICATION_SERVICE)))
+ .thenReturn(mNotificationManager);
mInCallController = new InCallController(mMockContext, mLock, mMockCallsManager,
mMockSystemStateHelper, mDefaultDialerCache, mTimeoutsAdapter,
mEmergencyCallHelper, new CarModeTracker(), mClockProxy);
- when(mMockContext.getSystemService(eq(Context.NOTIFICATION_SERVICE)))
- .thenReturn(mNotificationManager);
// Companion Apps don't have CONTROL_INCALL_EXPERIENCE permission.
doAnswer(invocation -> {
int uid = invocation.getArgument(0);
@@ -177,6 +186,8 @@
return new String[] { CAR_PKG };
case CAR2_UID:
return new String[] { CAR2_PKG };
+ case NONUI_UID:
+ return new String[] { NONUI_PKG };
}
return null;
}).when(mMockPackageManager).getPackagesForUid(anyInt());
@@ -189,6 +200,10 @@
when(mMockPackageManager.checkPermission(
matches(Manifest.permission.CONTROL_INCALL_EXPERIENCE),
matches(CAR2_PKG))).thenReturn(PackageManager.PERMISSION_GRANTED);
+ when(mMockPackageManager.checkPermission(
+ matches(Manifest.permission.CONTROL_INCALL_EXPERIENCE),
+ matches(NONUI_PKG))).thenReturn(PackageManager.PERMISSION_GRANTED);
+ when(mMockCallsManager.getAudioState()).thenReturn(new CallAudioState(false, 0, 0));
}
@Override
@@ -886,13 +901,15 @@
nullable(ContentResolver.class))).thenReturn(500L);
when(mMockCallsManager.getCalls()).thenReturn(Collections.singletonList(mMockCall));
- setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
+ setupMockPackageManager(true /* default */, true /* nonui */, true /* system */,
+ false /* external calls */,
+ false /* self mgd in default*/, false /* self mgd in car*/);
mInCallController.bindToServices(mMockCall);
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
ArgumentCaptor<ServiceConnection> serviceConnectionCaptor =
ArgumentCaptor.forClass(ServiceConnection.class);
- verify(mMockContext, times(1)).bindServiceAsUser(
+ verify(mMockContext, times(2)).bindServiceAsUser(
bindIntentCaptor.capture(),
serviceConnectionCaptor.capture(),
eq(Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE
@@ -905,13 +922,39 @@
// Start the connection, make sure we don't unbind, and make sure that we don't send
// anything to the in-call service yet.
- ServiceConnection serviceConnection = serviceConnectionCaptor.getValue();
+ List<ServiceConnection> serviceConnections = serviceConnectionCaptor.getAllValues();
+ List<Intent> intents = bindIntentCaptor.getAllValues();
+
+ // Find the non-ui service and have it connect first.
+ int nonUiIdx = findFirstIndexMatching(intents,
+ i -> NONUI_PKG.equals(i.getComponent().getPackageName()));
+ if (nonUiIdx < 0) {
+ fail("Did not bind to non-ui incall");
+ }
+
+ {
+ ComponentName nonUiComponentName = new ComponentName(NONUI_PKG, NONUI_CLASS);
+ IBinder mockBinder = mock(IBinder.class);
+ IInCallService mockInCallService = mock(IInCallService.class);
+ when(mockBinder.queryLocalInterface(anyString())).thenReturn(mockInCallService);
+ serviceConnections.get(nonUiIdx).onServiceConnected(nonUiComponentName, mockBinder);
+
+ // Make sure the non-ui binding didn't trigger the future.
+ assertFalse(bindTimeout.isDone());
+ }
+
+ int defDialerIdx = findFirstIndexMatching(intents,
+ i -> DEF_PKG.equals(i.getComponent().getPackageName()));
+ if (defDialerIdx < 0) {
+ fail("Did not bind to default dialer incall");
+ }
+
ComponentName defDialerComponentName = new ComponentName(DEF_PKG, DEF_CLASS);
IBinder mockBinder = mock(IBinder.class);
IInCallService mockInCallService = mock(IInCallService.class);
when(mockBinder.queryLocalInterface(anyString())).thenReturn(mockInCallService);
- serviceConnection.onServiceConnected(defDialerComponentName, mockBinder);
+ serviceConnections.get(defDialerIdx).onServiceConnected(defDialerComponentName, mockBinder);
verify(mockInCallService).setInCallAdapter(nullable(IInCallAdapter.class));
// Make sure that the future completed without timing out.
@@ -1103,9 +1146,21 @@
}};
}
+ private ResolveInfo getNonUiResolveinfo() {
+ return new ResolveInfo() {{
+ serviceInfo = new ServiceInfo();
+ serviceInfo.packageName = NONUI_PKG;
+ serviceInfo.name = NONUI_CLASS;
+ serviceInfo.applicationInfo = new ApplicationInfo();
+ serviceInfo.applicationInfo.uid = NONUI_UID;
+ serviceInfo.enabled = true;
+ serviceInfo.permission = Manifest.permission.BIND_INCALL_SERVICE;
+ }};
+ }
+
private void setupMockPackageManager(final boolean useDefaultDialer,
final boolean useSystemDialer, final boolean includeExternalCalls) {
- setupMockPackageManager(useDefaultDialer, useSystemDialer, includeExternalCalls,
+ setupMockPackageManager(useDefaultDialer, false, useSystemDialer, includeExternalCalls,
false /* self mgd */, false /* self mgd */);
}
@@ -1113,6 +1168,16 @@
final boolean useSystemDialer, final boolean includeExternalCalls,
final boolean includeSelfManagedCallsInDefaultDialer,
final boolean includeSelfManagedCallsInCarModeDialer) {
+ setupMockPackageManager(useDefaultDialer, false /* nonui */, useSystemDialer,
+ includeExternalCalls, includeSelfManagedCallsInDefaultDialer,
+ includeSelfManagedCallsInCarModeDialer);
+ }
+
+ private void setupMockPackageManager(final boolean useDefaultDialer,
+ final boolean useNonUiInCalls,
+ final boolean useSystemDialer, final boolean includeExternalCalls,
+ final boolean includeSelfManagedCallsInDefaultDialer,
+ final boolean includeSelfManagedCallsInCarModeDialer) {
doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
@@ -1147,7 +1212,13 @@
resolveInfo.add(getCarModeResolveinfo(CAR2_PKG, CAR2_CLASS,
includeExternalCalls, includeSelfManagedCallsInCarModeDialer));
}
+ } else {
+ // InCallController uses a blank package name when querying for non-ui incalls
+ if (useNonUiInCalls) {
+ resolveInfo.add(getNonUiResolveinfo());
+ }
}
+
return resolveInfo;
}
}).when(mMockPackageManager).queryIntentServicesAsUser(
diff --git a/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java b/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java
index 10a0194..9dd90f5 100644
--- a/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java
+++ b/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java
@@ -16,6 +16,10 @@
package com.android.server.telecom.tests;
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
+
+import android.app.BroadcastOptions;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
@@ -43,6 +47,7 @@
import com.android.server.telecom.CallerInfoLookupHelper;
import com.android.server.telecom.Constants;
import com.android.server.telecom.DefaultDialerCache;
+import com.android.server.telecom.DeviceIdleControllerAdapter;
import com.android.server.telecom.MissedCallNotifier;
import com.android.server.telecom.PhoneAccountRegistrar;
import com.android.server.telecom.TelecomBroadcastIntentProcessor;
@@ -67,6 +72,8 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
@@ -143,6 +150,9 @@
private static final UserHandle SECONARY_USER = UserHandle.of(12);
private static final int NO_CAPABILITY = 0;
private static final int TEST_TIMEOUT = 1000;
+ private static final long TEST_POWER_EXEMPT_TIME_MS = 1000;
+ private static final ComponentName COMPONENT_NAME = new ComponentName(
+ "com.anything", "com.whatever");
@Mock
private NotificationManager mNotificationManager;
@@ -155,6 +165,7 @@
@Mock TelecomSystem mTelecomSystem;
@Mock private DefaultDialerCache mDefaultDialerCache;
+ @Mock private DeviceIdleControllerAdapter mDeviceIdleControllerAdapter;
@Override
@Before
@@ -246,7 +257,8 @@
makeNotificationBuilderFactory(builders);
MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext,
- mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory);
+ mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory,
+ mDeviceIdleControllerAdapter);
missedCallNotifier.showMissedCallNotification(fakeCall);
missedCallNotifier.showMissedCallNotification(fakeCall);
@@ -401,7 +413,8 @@
makeNotificationBuilderFactory(builder1);
MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext,
- mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory);
+ mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory,
+ mDeviceIdleControllerAdapter);
PhoneAccount phoneAccount = makePhoneAccount(PRIMARY_USER, NO_CAPABILITY);
MissedCallNotifier.CallInfo fakeCall =
@@ -461,7 +474,8 @@
makeNotificationBuilderFactory(builder1);
MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext,
- mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory);
+ mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory,
+ mDeviceIdleControllerAdapter);
// AsyncQueryHandler used in reloadFromDatabase interacts poorly with the below
// timeout-verify, so run this in a new handler to mitigate that.
@@ -530,7 +544,8 @@
makeNotificationBuilderFactory(builder1);
MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext,
- mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory);
+ mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory,
+ mDeviceIdleControllerAdapter);
// AsyncQueryHandler used in reloadFromDatabase interacts poorly with the below
// timeout-verify, so run this in a new handler to mitigate that.
@@ -560,6 +575,49 @@
nullable(Notification.class), eq(PRIMARY_USER));
}
+ @SmallTest
+ @Test
+ public void testDialerHandleMissedCall() {
+ // Configure Notifier to send missed call intent and let dialer handle
+ enableDialerHandlesMissedCall();
+
+ Notification.Builder builder1 = makeNotificationBuilder("builder1");
+ MissedCallNotifierImpl.NotificationBuilderFactory fakeBuilderFactory =
+ makeNotificationBuilderFactory(builder1);
+
+ MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext,
+ mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory,
+ mDeviceIdleControllerAdapter);
+ PhoneAccount phoneAccount = makePhoneAccount(PRIMARY_USER, NO_CAPABILITY);
+
+ MissedCallNotifier.CallInfo fakeCall =
+ makeFakeCallInfo(SIP_CALL_HANDLE, CALLER_NAME, CALL_TIMESTAMP,
+ phoneAccount.getAccountHandle());
+ missedCallNotifier.showMissedCallNotification(fakeCall);
+
+ ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+ ArgumentCaptor<Bundle> bundleCaptor =
+ ArgumentCaptor.forClass(Bundle.class);
+ verify(mDeviceIdleControllerAdapter).exemptAppTemporarilyForEvent(
+ eq(COMPONENT_NAME.getPackageName()), anyLong(), anyInt(), any());
+ verify(mContext).sendBroadcastAsUser(
+ intentCaptor.capture(),
+ any(),
+ eq(android.Manifest.permission.READ_PHONE_STATE), bundleCaptor.capture());
+ assertNotNull("Not expecting null intent", intentCaptor.getValue());
+ assertEquals("Incorrect intent received",
+ TelecomManager.ACTION_SHOW_MISSED_CALLS_NOTIFICATION,
+ intentCaptor.getValue().getAction());
+ assertNotNull("Not expecting null options bundle", bundleCaptor.getValue());
+ BroadcastOptions options = new BroadcastOptions(bundleCaptor.getValue());
+ assertTrue("App must have a temporary exemption set.",
+ options.getTemporaryAppWhitelistDuration() > 0);
+
+ // A notification should never be posted by Telecom
+ verify(mNotificationManager, never()).notifyAsUser(nullable(String.class), anyInt(),
+ nullable(Notification.class), eq(PRIMARY_USER));
+ }
+
private Notification.Builder makeNotificationBuilder(String label) {
Notification.Builder builder = spy(new Notification.Builder(mContext));
Notification notification = mock(Notification.class);
@@ -592,14 +650,14 @@
private MissedCallNotifier makeMissedCallNotifier(
NotificationBuilderFactory fakeBuilderFactory, UserHandle currentUser) {
MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext,
- mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory);
+ mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory,
+ mDeviceIdleControllerAdapter);
missedCallNotifier.setCurrentUserHandle(currentUser);
return missedCallNotifier;
}
private PhoneAccount makePhoneAccount(UserHandle userHandle, int capability) {
- ComponentName componentName = new ComponentName("com.anything", "com.whatever");
- PhoneAccountHandle phoneAccountHandle = new PhoneAccountHandle(componentName, "id",
+ PhoneAccountHandle phoneAccountHandle = new PhoneAccountHandle(COMPONENT_NAME, "id",
userHandle);
PhoneAccount.Builder builder = new PhoneAccount.Builder(phoneAccountHandle, "test");
builder.setCapabilities(capability);
@@ -609,6 +667,13 @@
return phoneAccount;
}
+ private void enableDialerHandlesMissedCall() {
+ doReturn(COMPONENT_NAME.getPackageName()).when(mDefaultDialerCache).
+ getDefaultDialerApplication(anyInt());
+ mComponentContextFixture.addIntentReceiver(
+ TelecomManager.ACTION_SHOW_MISSED_CALLS_NOTIFICATION, COMPONENT_NAME);
+ }
+
private IContentProvider getContentProviderForUser(int userId) {
return mContext.getContentResolver().acquireProvider(userId + "@call_log");
}
diff --git a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
index 442c310..68b10c6 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
@@ -38,6 +38,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.app.AppOpsManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ContentResolver;
@@ -78,6 +79,7 @@
import com.android.server.telecom.ClockProxy;
import com.android.server.telecom.ConnectionServiceFocusManager;
import com.android.server.telecom.ContactsAsyncHelper;
+import com.android.server.telecom.DeviceIdleControllerAdapter;
import com.android.server.telecom.HeadsetMediaButton;
import com.android.server.telecom.HeadsetMediaButtonFactory;
import com.android.server.telecom.InCallWakeLockController;
@@ -207,6 +209,7 @@
@Mock ClockProxy mClockProxy;
@Mock RoleManagerAdapter mRoleManagerAdapter;
@Mock ToneGenerator mToneGenerator;
+ @Mock DeviceIdleControllerAdapter mDeviceIdleControllerAdapter;
final ComponentName mInCallServiceComponentNameX =
new ComponentName(
@@ -348,6 +351,8 @@
doReturn(mSpyContext).when(mSpyContext).getApplicationContext();
doNothing().when(mSpyContext).sendBroadcastAsUser(any(), any(), any());
+ doReturn(mock(AppOpsManager.class)).when(mSpyContext).getSystemService(AppOpsManager.class);
+
mHandlerThread = new HandlerThread("TelecomHandlerThread");
mHandlerThread.start();
@@ -475,7 +480,8 @@
when(mRoleManagerAdapter.getDefaultCallScreeningApp()).thenReturn(null);
mTelecomSystem = new TelecomSystem(
mComponentContextFixture.getTestDouble(),
- (context, phoneAccountRegistrar, defaultDialerCache) -> mMissedCallNotifier,
+ (context, phoneAccountRegistrar, defaultDialerCache, mDeviceIdleControllerAdapter)
+ -> mMissedCallNotifier,
mCallerInfoAsyncQueryFactoryFixture.getTestDouble(),
headsetMediaButtonFactory,
proximitySensorManagerFactory,
@@ -535,7 +541,7 @@
ContactsAsyncHelper.ContentResolverAdapter adapter) {
return new ContactsAsyncHelper(adapter, mHandlerThread.getLooper());
}
- });
+ }, mDeviceIdleControllerAdapter);
mComponentContextFixture.setTelecomManager(new TelecomManager(
mComponentContextFixture.getTestDouble(),
diff --git a/tests/src/com/android/server/telecom/tests/TelecomTestCase.java b/tests/src/com/android/server/telecom/tests/TelecomTestCase.java
index b0b1ec0..264e087 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomTestCase.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomTestCase.java
@@ -25,8 +25,10 @@
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
+import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
public abstract class TelecomTestCase {
protected static final String TESTING_TAG = "Telecom-TEST";
@@ -75,4 +77,13 @@
}
}
}
+
+ protected static <T> int findFirstIndexMatching(List<T> items, Predicate<T> matcher) {
+ for (int i = 0; i < items.size(); i++) {
+ if (matcher.test(items.get(i))) {
+ return i;
+ }
+ }
+ return -1;
+ }
}