diff --git a/Android.mk b/Android.mk index 09177930e..d3c331672 100644 --- a/Android.mk +++ b/Android.mk @@ -12,7 +12,7 @@ LOCAL_PACKAGE_NAME := Telecom LOCAL_CERTIFICATE := platform LOCAL_PRIVILEGED_MODULE := true -LOCAL_PROGUARD_FLAGS := $(proguard.flags) +LOCAL_PROGUARD_FLAG_FILES := proguard.flags # Workaround for "local variable type mismatch" error. LOCAL_DX_FLAGS += --no-locals diff --git a/proguard.flags b/proguard.flags index e52ac207f..357336ba9 100644 --- a/proguard.flags +++ b/proguard.flags @@ -1,4 +1,8 @@ -verbose - -# Keep @VisibleForTesting elements -keep @com.android.internal.annotations.VisibleForTesting class * +-keep class com.android.server.telecom.TelecomSystem { + *; +} +-keep class com.android.server.telecom.Log { + *; +} diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java index 464975fca..45072e460 100644 --- a/src/com/android/server/telecom/Call.java +++ b/src/com/android/server/telecom/Call.java @@ -94,7 +94,7 @@ interface Listener { boolean onCanceledViaNewOutgoingCallBroadcast(Call call); } - abstract static class ListenerBase implements Listener { + public abstract static class ListenerBase implements Listener { @Override public void onSuccessfulOutgoingCall(Call call, int callState) {} @Override @@ -327,7 +327,7 @@ public void run() { * one that was registered with the {@link PhoneAccount#CAPABILITY_CALL_PROVIDER} flag. * @param isIncoming True if this is an incoming call. */ - Call( + public Call( Context context, CallsManager callsManager, ConnectionServiceRepository repository, @@ -384,11 +384,11 @@ public void run() { mConnectTimeMillis = connectTimeMillis; } - void addListener(Listener listener) { + public void addListener(Listener listener) { mListeners.add(listener); } - void removeListener(Listener listener) { + public void removeListener(Listener listener) { if (listener != null) { mListeners.remove(listener); } @@ -445,7 +445,7 @@ private boolean shouldContinueProcessingAfterDisconnect() { * misbehave and they do this very often. The result is that we do not enforce state transitions * and instead keep the code resilient to unexpected state changes. */ - void setState(int newState) { + public void setState(int newState) { if (mState != newState) { Log.v(this, "setState %s -> %s", mState, newState); @@ -491,7 +491,7 @@ boolean isConference() { return mIsConference; } - Uri getHandle() { + public Uri getHandle() { return mHandle; } @@ -504,7 +504,7 @@ void setHandle(Uri handle) { setHandle(handle, TelecomManager.PRESENTATION_ALLOWED); } - void setHandle(Uri handle, int presentation) { + public void setHandle(Uri handle, int presentation) { if (!Objects.equals(handle, mHandle) || presentation != mHandlePresentation) { mHandlePresentation = presentation; if (mHandlePresentation == TelecomManager.PRESENTATION_RESTRICTED || @@ -548,15 +548,15 @@ void setCallerDisplayName(String callerDisplayName, int presentation) { } } - String getName() { + public String getName() { return mCallerInfo == null ? null : mCallerInfo.name; } - Bitmap getPhotoIcon() { + public Bitmap getPhotoIcon() { return mCallerInfo == null ? null : mCallerInfo.cachedPhotoIcon; } - Drawable getPhoto() { + public Drawable getPhoto() { return mCallerInfo == null ? null : mCallerInfo.cachedPhoto; } @@ -564,13 +564,13 @@ Drawable getPhoto() { * @param disconnectCause The reason for the disconnection, represented by * {@link android.telecom.DisconnectCause}. */ - void setDisconnectCause(DisconnectCause disconnectCause) { + public void setDisconnectCause(DisconnectCause disconnectCause) { // TODO: Consider combining this method with a setDisconnected() method that is totally // separate from setState. mDisconnectCause = disconnectCause; } - DisconnectCause getDisconnectCause() { + public DisconnectCause getDisconnectCause() { return mDisconnectCause; } @@ -655,11 +655,11 @@ long getAgeMillis() { * @return The time when this call object was created and added to the set of pending outgoing * calls. */ - long getCreationTimeMillis() { + public long getCreationTimeMillis() { return mCreationTimeMillis; } - void setCreationTimeMillis(long time) { + public void setCreationTimeMillis(long time) { mCreationTimeMillis = time; } @@ -1321,7 +1321,7 @@ private void maybeLoadCannedSmsResponses() { if (mIsIncoming && isRespondViaSmsCapable() && !mCannedSmsResponsesLoadingStarted) { Log.d(this, "maybeLoadCannedSmsResponses: starting task to load messages"); mCannedSmsResponsesLoadingStarted = true; - TelecomSystem.getInstance().getRespondViaSmsManager().loadCannedTextMessages( + mCallsManager.getRespondViaSmsManager().loadCannedTextMessages( new Response>() { @Override public void onResult(Void request, List... result) { diff --git a/src/com/android/server/telecom/CallAudioManager.java b/src/com/android/server/telecom/CallAudioManager.java index 53b6261c8..3b090a57c 100644 --- a/src/com/android/server/telecom/CallAudioManager.java +++ b/src/com/android/server/telecom/CallAudioManager.java @@ -37,6 +37,7 @@ final class CallAudioManager extends CallsManagerListenerBase private final AudioManager mAudioManager; private final BluetoothManager mBluetoothManager; private final WiredHeadsetManager mWiredHeadsetManager; + private final CallsManager mCallsManager; private AudioState mAudioState; private int mAudioFocusStreamType; @@ -45,12 +46,17 @@ final class CallAudioManager extends CallsManagerListenerBase private boolean mWasSpeakerOn; private int mMostRecentlyUsedMode = AudioManager.MODE_IN_CALL; - CallAudioManager(Context context, StatusBarNotifier statusBarNotifier, - WiredHeadsetManager wiredHeadsetManager) { + CallAudioManager( + Context context, + StatusBarNotifier statusBarNotifier, + WiredHeadsetManager wiredHeadsetManager, + CallsManager callsManager) { mStatusBarNotifier = statusBarNotifier; mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); mBluetoothManager = new BluetoothManager(context, this); mWiredHeadsetManager = wiredHeadsetManager; + mCallsManager = callsManager; + mWiredHeadsetManager.addListener(this); saveAudioState(getInitialAudioState(null)); @@ -78,7 +84,7 @@ public void onCallAdded(Call call) { public void onCallRemoved(Call call) { // If we didn't already have focus, there's nothing to do. if (hasFocus()) { - if (TelecomSystem.getInstance().getCallsManager().getCalls().isEmpty()) { + if (mCallsManager.getCalls().isEmpty()) { Log.v(this, "all calls removed, reseting system audio to default state"); setInitialAudioState(null, false /* force */); mWasSpeakerOn = false; @@ -99,7 +105,7 @@ public void onIncomingCallAnswered(Call call) { // We do two things: // (1) If this is the first call, then we can to turn on bluetooth if available. // (2) Unmute the audio for the new incoming call. - boolean isOnlyCall = TelecomSystem.getInstance().getCallsManager().getCalls().size() == 1; + boolean isOnlyCall = mCallsManager.getCalls().size() == 1; if (isOnlyCall && mBluetoothManager.isBluetoothAvailable()) { mBluetoothManager.connectBluetoothAudio(); route = AudioState.ROUTE_BLUETOOTH; @@ -175,7 +181,7 @@ void mute(boolean shouldMute) { Log.v(this, "mute, shouldMute: %b", shouldMute); // Don't mute if there are any emergency calls. - if (TelecomSystem.getInstance().getCallsManager().hasEmergencyCall()) { + if (mCallsManager.hasEmergencyCall()) { shouldMute = false; Log.v(this, "ignoring mute for emergency call"); } @@ -325,7 +331,7 @@ private void setSystemAudioState( } if (!oldAudioState.equals(mAudioState)) { - TelecomSystem.getInstance().getCallsManager() + mCallsManager .onAudioStateChanged(oldAudioState, mAudioState); updateAudioForForegroundCall(); } @@ -360,7 +366,7 @@ private void updateAudioStreamAndMode() { requestAudioFocusAndSetMode(AudioManager.STREAM_RING, AudioManager.MODE_RINGTONE); } else { Call foregroundCall = getForegroundCall(); - Call waitingForAccountSelectionCall = TelecomSystem.getInstance().getCallsManager() + Call waitingForAccountSelectionCall = mCallsManager .getFirstCallWithState(CallState.PRE_DIAL_WAIT); if (foregroundCall != null && waitingForAccountSelectionCall == null) { // In the case where there is a call that is waiting for account selection, @@ -504,7 +510,7 @@ private void setInitialAudioState(Call call, boolean force) { } private void updateAudioForForegroundCall() { - Call call = TelecomSystem.getInstance().getCallsManager().getForegroundCall(); + Call call = mCallsManager.getForegroundCall(); if (call != null && call.getConnectionService() != null) { call.getConnectionService().onAudioStateChanged(call, mAudioState); } @@ -514,7 +520,7 @@ private void updateAudioForForegroundCall() { * Returns the current foreground call in order to properly set the audio mode. */ private Call getForegroundCall() { - Call call = TelecomSystem.getInstance().getCallsManager().getForegroundCall(); + Call call = mCallsManager.getForegroundCall(); // We ignore any foreground call that is in the ringing state because we deal with ringing // calls exclusively through the mIsRinging variable set by {@link Ringer}. @@ -526,7 +532,7 @@ private Call getForegroundCall() { } private boolean hasRingingForegroundCall() { - Call call = TelecomSystem.getInstance().getCallsManager().getForegroundCall(); + Call call = mCallsManager.getForegroundCall(); return call != null && call.getState() == CallState.RINGING; } diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java index 2c6109fbc..e6443c898 100644 --- a/src/com/android/server/telecom/CallsManager.java +++ b/src/com/android/server/telecom/CallsManager.java @@ -106,6 +106,7 @@ void onConnectionServiceChanged( private final DtmfLocalTonePlayer mDtmfLocalTonePlayer; private final InCallController mInCallController; private final CallAudioManager mCallAudioManager; + private RespondViaSmsManager mRespondViaSmsManager; private final Ringer mRinger; private final InCallWakeLockController mInCallWakeLockController; // For this set initial table size to 16 because we add 13 listeners in @@ -139,27 +140,32 @@ void onConnectionServiceChanged( /** * Initializes the required Telecom components. */ - CallsManager(Context context, MissedCallNotifier missedCallNotifier, + CallsManager( + Context context, + MissedCallNotifier missedCallNotifier, PhoneAccountRegistrar phoneAccountRegistrar, - RespondViaSmsManager respondViaSmsManager) { + HeadsetMediaButtonFactory headsetMediaButtonFactory, + ProximitySensorManagerFactory proximitySensorManagerFactory, + InCallWakeLockControllerFactory inCallWakeLockControllerFactory) { mContext = context; mPhoneAccountRegistrar = phoneAccountRegistrar; mMissedCallNotifier = missedCallNotifier; StatusBarNotifier statusBarNotifier = new StatusBarNotifier(context, this); mWiredHeadsetManager = new WiredHeadsetManager(context); - mCallAudioManager = new CallAudioManager(context, statusBarNotifier, mWiredHeadsetManager); + mCallAudioManager = new CallAudioManager( + context, statusBarNotifier, mWiredHeadsetManager, this); InCallTonePlayer.Factory playerFactory = new InCallTonePlayer.Factory(mCallAudioManager); mRinger = new Ringer(mCallAudioManager, this, playerFactory, context); - mHeadsetMediaButton = new HeadsetMediaButton(context, this); + mHeadsetMediaButton = headsetMediaButtonFactory.create(context, this); mTtyManager = new TtyManager(context, mWiredHeadsetManager); - mProximitySensorManager = new ProximitySensorManager(context); - mPhoneStateBroadcaster = new PhoneStateBroadcaster(); + mProximitySensorManager = proximitySensorManagerFactory.create(context, this); + mPhoneStateBroadcaster = new PhoneStateBroadcaster(this); mCallLogManager = new CallLogManager(context); - mInCallController = new InCallController(context); + mInCallController = new InCallController(context, this); mDtmfLocalTonePlayer = new DtmfLocalTonePlayer(context); - mConnectionServiceRepository = new ConnectionServiceRepository(mPhoneAccountRegistrar, - context); - mInCallWakeLockController = new InCallWakeLockController(context, this); + mConnectionServiceRepository = + new ConnectionServiceRepository(mPhoneAccountRegistrar, mContext, this); + mInCallWakeLockController = inCallWakeLockControllerFactory.create(context, this); mListeners.add(statusBarNotifier); mListeners.add(mCallLogManager); @@ -172,10 +178,21 @@ void onConnectionServiceChanged( mListeners.add(missedCallNotifier); mListeners.add(mDtmfLocalTonePlayer); mListeners.add(mHeadsetMediaButton); - mListeners.add(respondViaSmsManager); mListeners.add(mProximitySensorManager); } + public void setRespondViaSmsManager(RespondViaSmsManager respondViaSmsManager) { + if (mRespondViaSmsManager != null) { + mListeners.remove(mRespondViaSmsManager); + } + mRespondViaSmsManager = respondViaSmsManager; + mListeners.add(respondViaSmsManager); + } + + public RespondViaSmsManager getRespondViaSmsManager() { + return mRespondViaSmsManager; + } + @Override public void onSuccessfulOutgoingCall(Call call, int callState) { Log.v(this, "onSuccessfulOutgoingCall, %s", call); diff --git a/src/com/android/server/telecom/CallsManagerListenerBase.java b/src/com/android/server/telecom/CallsManagerListenerBase.java index ffc594754..6b5470984 100644 --- a/src/com/android/server/telecom/CallsManagerListenerBase.java +++ b/src/com/android/server/telecom/CallsManagerListenerBase.java @@ -21,7 +21,7 @@ /** * Provides a default implementation for listeners of CallsManager. */ -class CallsManagerListenerBase implements CallsManager.CallsManagerListener { +public class CallsManagerListenerBase implements CallsManager.CallsManagerListener { @Override public void onCallAdded(Call call) { } diff --git a/src/com/android/server/telecom/ConnectionServiceRepository.java b/src/com/android/server/telecom/ConnectionServiceRepository.java index 5490c2d00..d18d0516e 100644 --- a/src/com/android/server/telecom/ConnectionServiceRepository.java +++ b/src/com/android/server/telecom/ConnectionServiceRepository.java @@ -33,6 +33,7 @@ final class ConnectionServiceRepository { new HashMap<>(); private final PhoneAccountRegistrar mPhoneAccountRegistrar; private final Context mContext; + private final CallsManager mCallsManager; private final ServiceBinder.Listener mUnbindListener = new ServiceBinder.Listener() { @@ -42,9 +43,13 @@ public void onUnbind(ConnectionServiceWrapper service) { } }; - ConnectionServiceRepository(PhoneAccountRegistrar phoneAccountRegistrar, Context context) { + ConnectionServiceRepository( + PhoneAccountRegistrar phoneAccountRegistrar, + Context context, + CallsManager callsManager) { mPhoneAccountRegistrar = phoneAccountRegistrar; mContext = context; + mCallsManager = callsManager; } ConnectionServiceWrapper getService(ComponentName componentName, UserHandle userHandle) { @@ -55,6 +60,7 @@ ConnectionServiceWrapper getService(ComponentName componentName, UserHandle user componentName, this, mPhoneAccountRegistrar, + mCallsManager, mContext, userHandle); service.addListener(mUnbindListener); diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java index 2f125cf11..82553038d 100644 --- a/src/com/android/server/telecom/ConnectionServiceWrapper.java +++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java @@ -104,7 +104,7 @@ public void handleMessage(Message msg) { case MSG_SET_ACTIVE: call = mCallIdMapper.getCall(msg.obj); if (call != null) { - TelecomSystem.getInstance().getCallsManager().markCallAsActive(call); + mCallsManager.markCallAsActive(call); } else { //Log.w(this, "setActive, unknown call id: %s", msg.obj); } @@ -112,7 +112,7 @@ public void handleMessage(Message msg) { case MSG_SET_RINGING: call = mCallIdMapper.getCall(msg.obj); if (call != null) { - TelecomSystem.getInstance().getCallsManager().markCallAsRinging(call); + mCallsManager.markCallAsRinging(call); } else { //Log.w(this, "setRinging, unknown call id: %s", msg.obj); } @@ -120,7 +120,7 @@ public void handleMessage(Message msg) { case MSG_SET_DIALING: call = mCallIdMapper.getCall(msg.obj); if (call != null) { - TelecomSystem.getInstance().getCallsManager().markCallAsDialing(call); + mCallsManager.markCallAsDialing(call); } else { //Log.w(this, "setDialing, unknown call id: %s", msg.obj); } @@ -132,7 +132,7 @@ public void handleMessage(Message msg) { DisconnectCause disconnectCause = (DisconnectCause) args.arg2; Log.d(this, "disconnect call %s %s", disconnectCause, call); if (call != null) { - TelecomSystem.getInstance().getCallsManager() + mCallsManager .markCallAsDisconnected(call, disconnectCause); } else { //Log.w(this, "setDisconnected, unknown call id: %s", args.arg1); @@ -145,7 +145,7 @@ public void handleMessage(Message msg) { case MSG_SET_ON_HOLD: call = mCallIdMapper.getCall(msg.obj); if (call != null) { - TelecomSystem.getInstance().getCallsManager().markCallAsOnHold(call); + mCallsManager.markCallAsOnHold(call); } else { //Log.w(this, "setOnHold, unknown call id: %s", msg.obj); } @@ -225,7 +225,7 @@ public void handleMessage(Message msg) { parcelableConference.getPhoneAccount() != null) { phAcc = parcelableConference.getPhoneAccount(); } - Call conferenceCall = TelecomSystem.getInstance().getCallsManager().createConferenceCall( + Call conferenceCall = mCallsManager.createConferenceCall( phAcc, parcelableConference); mCallIdMapper.addCall(conferenceCall, id); conferenceCall.setConnectionService(ConnectionServiceWrapper.this); @@ -248,10 +248,10 @@ public void handleMessage(Message msg) { call = mCallIdMapper.getCall(msg.obj); if (call != null) { if (call.isAlive()) { - TelecomSystem.getInstance().getCallsManager().markCallAsDisconnected( + mCallsManager.markCallAsDisconnected( call, new DisconnectCause(DisconnectCause.REMOTE)); } else { - TelecomSystem.getInstance().getCallsManager().markCallAsRemoved(call); + mCallsManager.markCallAsRemoved(call); } } break; @@ -381,7 +381,7 @@ public void handleMessage(Message msg) { try { String callId = (String)args.arg1; ParcelableConnection connection = (ParcelableConnection)args.arg2; - Call existingCall = TelecomSystem.getInstance().getCallsManager() + Call existingCall = mCallsManager .createCallForExistingConnection(callId, connection); mCallIdMapper.addCall(existingCall, callId); @@ -401,7 +401,7 @@ public void handleCreateConnectionComplete( String callId, ConnectionRequest request, ParcelableConnection connection) { - logIncoming("handleCreateConnectionComplete %s", request); + logIncoming("handleCreateConnectionComplete %s", callId); if (mCallIdMapper.isValidCallId(callId)) { SomeArgs args = SomeArgs.obtain(); args.arg1 = callId; @@ -627,6 +627,7 @@ public void addExistingConnection(String callId, ParcelableConnection connection private IConnectionService mServiceInterface; private final ConnectionServiceRepository mConnectionServiceRepository; private final PhoneAccountRegistrar mPhoneAccountRegistrar; + private final CallsManager mCallsManager; /** * Creates a connection service. @@ -634,6 +635,7 @@ public void addExistingConnection(String callId, ParcelableConnection connection * @param componentName The component name of the service with which to bind. * @param connectionServiceRepository Connection service repository. * @param phoneAccountRegistrar Phone account registrar + * @param callsManager Calls manager * @param context The context. * @param userHandle The {@link UserHandle} to use when binding. */ @@ -641,6 +643,7 @@ public void addExistingConnection(String callId, ParcelableConnection connection ComponentName componentName, ConnectionServiceRepository connectionServiceRepository, PhoneAccountRegistrar phoneAccountRegistrar, + CallsManager callsManager, Context context, UserHandle userHandle) { super(ConnectionService.SERVICE_INTERFACE, componentName, context, userHandle); @@ -650,6 +653,7 @@ public void addExistingConnection(String callId, ParcelableConnection connection // To do this, we must proxy remote ConnectionService objects }); mPhoneAccountRegistrar = phoneAccountRegistrar; + mCallsManager = callsManager; } /** See {@link IConnectionService#addConnectionServiceAdapter}. */ @@ -715,7 +719,7 @@ public void onFailure() { mBinder.bind(callback); } - /** @see ConnectionService#abort(String) */ + /** @see IConnectionService#abort(String) */ void abort(Call call) { // Clear out any pending outgoing call data final String callId = mCallIdMapper.getCallId(call); @@ -732,7 +736,7 @@ void abort(Call call) { removeCall(call, new DisconnectCause(DisconnectCause.LOCAL)); } - /** @see ConnectionService#hold(String) */ + /** @see IConnectionService#hold(String) */ void hold(Call call) { final String callId = mCallIdMapper.getCallId(call); if (callId != null && isServiceValid("hold")) { @@ -744,7 +748,7 @@ void hold(Call call) { } } - /** @see ConnectionService#unhold(String) */ + /** @see IConnectionService#unhold(String) */ void unhold(Call call) { final String callId = mCallIdMapper.getCallId(call); if (callId != null && isServiceValid("unhold")) { @@ -756,7 +760,7 @@ void unhold(Call call) { } } - /** @see ConnectionService#onAudioStateChanged(String,AudioState) */ + /** @see IConnectionService#onAudioStateChanged(String,AudioState) */ void onAudioStateChanged(Call activeCall, AudioState audioState) { final String callId = mCallIdMapper.getCallId(activeCall); if (callId != null && isServiceValid("onAudioStateChanged")) { @@ -768,7 +772,7 @@ void onAudioStateChanged(Call activeCall, AudioState audioState) { } } - /** @see ConnectionService#disconnect(String) */ + /** @see IConnectionService#disconnect(String) */ void disconnect(Call call) { final String callId = mCallIdMapper.getCallId(call); if (callId != null && isServiceValid("disconnect")) { @@ -780,7 +784,7 @@ void disconnect(Call call) { } } - /** @see ConnectionService#answer(String,int) */ + /** @see IConnectionService#answer(String) */ void answer(Call call, int videoState) { final String callId = mCallIdMapper.getCallId(call); if (callId != null && isServiceValid("answer")) { @@ -796,7 +800,7 @@ void answer(Call call, int videoState) { } } - /** @see ConnectionService#reject(String) */ + /** @see IConnectionService#reject(String) */ void reject(Call call) { final String callId = mCallIdMapper.getCallId(call); if (callId != null && isServiceValid("reject")) { @@ -808,7 +812,7 @@ void reject(Call call) { } } - /** @see ConnectionService#playDtmfTone(String,char) */ + /** @see IConnectionService#playDtmfTone(String,char) */ void playDtmfTone(Call call, char digit) { final String callId = mCallIdMapper.getCallId(call); if (callId != null && isServiceValid("playDtmfTone")) { @@ -820,7 +824,7 @@ void playDtmfTone(Call call, char digit) { } } - /** @see ConnectionService#stopDtmfTone(String) */ + /** @see IConnectionService#stopDtmfTone(String) */ void stopDtmfTone(Call call) { final String callId = mCallIdMapper.getCallId(call); if (callId != null && isServiceValid("stopDtmfTone")) { @@ -933,7 +937,7 @@ protected void setServiceInterface(IBinder binder) { // outgoing calls to try the next service. This needs to happen before CallsManager // tries to clean up any calls still associated with this service. handleConnectionServiceDeath(); - TelecomSystem.getInstance().getCallsManager().handleConnectionServiceDeath(this); + mCallsManager.handleConnectionServiceDeath(this); mServiceInterface = null; } else { mServiceInterface = IConnectionService.Stub.asInterface(binder); @@ -1063,10 +1067,6 @@ private void setRemoteServices( } private void noRemoteServices(RemoteServiceCallback callback) { - try { - callback.onResult(Collections.EMPTY_LIST, Collections.EMPTY_LIST); - } catch (RemoteException e) { - Log.e(this, e, "Contacting ConnectionService %s", this.getComponentName()); - } + setRemoteServices(callback, Collections.EMPTY_LIST, Collections.EMPTY_LIST); } } diff --git a/src/com/android/server/telecom/CreateConnectionProcessor.java b/src/com/android/server/telecom/CreateConnectionProcessor.java index 31114dfdd..1d8ef286a 100644 --- a/src/com/android/server/telecom/CreateConnectionProcessor.java +++ b/src/com/android/server/telecom/CreateConnectionProcessor.java @@ -102,6 +102,7 @@ public boolean equals(Object obj) { CreateConnectionProcessor( Call call, ConnectionServiceRepository repository, CreateConnectionResponse response, PhoneAccountRegistrar phoneAccountRegistrar, Context context) { + Log.v(this, "CreateConnectionProcessor created for Call = %s", call); mCall = call; mRepository = repository; mResponse = response; diff --git a/src/com/android/server/telecom/HeadsetMediaButton.java b/src/com/android/server/telecom/HeadsetMediaButton.java index f0ea1e90d..8c1488c6d 100644 --- a/src/com/android/server/telecom/HeadsetMediaButton.java +++ b/src/com/android/server/telecom/HeadsetMediaButton.java @@ -25,7 +25,7 @@ /** * Static class to handle listening to the headset media buttons. */ -final class HeadsetMediaButton extends CallsManagerListenerBase { +public class HeadsetMediaButton extends CallsManagerListenerBase { // Types of media button presses static final int SHORT_PRESS = 1; @@ -54,7 +54,7 @@ public boolean onMediaButtonEvent(Intent intent) { private final MediaSession mSession; - HeadsetMediaButton(Context context, CallsManager callsManager) { + public HeadsetMediaButton(Context context, CallsManager callsManager) { mCallsManager = callsManager; // Create a MediaSession but don't enable it yet. This is a diff --git a/src/com/android/server/telecom/HeadsetMediaButtonFactory.java b/src/com/android/server/telecom/HeadsetMediaButtonFactory.java new file mode 100644 index 000000000..becabbff2 --- /dev/null +++ b/src/com/android/server/telecom/HeadsetMediaButtonFactory.java @@ -0,0 +1,31 @@ +/* + * Copyright 2015, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.telecom; + +import android.content.Context; + +/** + * This is a TEMPORARY fix to make the {@link HeadsetMediaButton} object injectable for testing. + * Class {@link HeadsetMediaButton} itself is not testable because it grabs lots of special stuff + * from its {@code Context} that cannot be conveniently mocked. + * + * TODO: Replace with a better design. + */ +public interface HeadsetMediaButtonFactory { + + HeadsetMediaButton create(Context context, CallsManager callsManager); +} diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java index b846243a1..9c2efd9f6 100644 --- a/src/com/android/server/telecom/InCallController.java +++ b/src/com/android/server/telecom/InCallController.java @@ -139,9 +139,11 @@ public void onConferenceableCallsChanged(Call call) { private final ComponentName mInCallComponentName; private final Context mContext; + private final CallsManager mCallsManager; - public InCallController(Context context) { + public InCallController(Context context, CallsManager callsManager) { mContext = context; + mCallsManager = callsManager; Resources resources = mContext.getResources(); mInCallComponentName = new ComponentName( @@ -175,7 +177,7 @@ public void onCallAdded(Call call) { @Override public void onCallRemoved(Call call) { Log.i(this, "onCallRemoved: %s", call); - if (TelecomSystem.getInstance().getCallsManager().getCalls().isEmpty()) { + if (mCallsManager.getCalls().isEmpty()) { // TODO: Wait for all messages to be delivered to the service before unbinding. unbind(); } @@ -357,7 +359,7 @@ private void onConnected(ComponentName componentName, IBinder service) { try { inCallService.setInCallAdapter( new InCallAdapter( - TelecomSystem.getInstance().getCallsManager(), + mCallsManager, mCallIdMapper)); mInCallServices.put(componentName, inCallService); } catch (RemoteException e) { @@ -367,25 +369,24 @@ private void onConnected(ComponentName componentName, IBinder service) { } // Upon successful connection, send the state of the world to the service. - Collection calls = TelecomSystem.getInstance().getCallsManager().getCalls(); + Collection calls = mCallsManager.getCalls(); if (!calls.isEmpty()) { Log.i(this, "Adding %s calls to InCallService after onConnected: %s", calls.size(), componentName); for (Call call : calls) { try { // Track the call if we don't already know about it. - Log.i(this, "addCall after binding: %s", call); addCall(call); - - inCallService.addCall(toParcelableCall(call, + inCallService.addCall(toParcelableCall( + call, componentName.equals(mInCallComponentName) /* includeVideoProvider */)); } catch (RemoteException ignored) { } } onAudioStateChanged( null, - TelecomSystem.getInstance().getCallsManager().getAudioState()); - onCanAddCallChanged(TelecomSystem.getInstance().getCallsManager().canAddCall()); + mCallsManager.getAudioState()); + onCanAddCallChanged(mCallsManager.canAddCall()); } else { unbind(); } @@ -411,7 +412,7 @@ private void onDisconnected(ComponentName disconnectedComponent) { // implementations. if (disconnectedComponent.equals(mInCallComponentName)) { Log.i(this, "In-call UI %s disconnected.", disconnectedComponent); - TelecomSystem.getInstance().getCallsManager().disconnectAllCalls(); + mCallsManager.disconnectAllCalls(); unbind(); } else { Log.i(this, "In-Call Service %s suddenly disconnected", disconnectedComponent); @@ -468,7 +469,7 @@ private ParcelableCall toParcelableCall(Call call, boolean includeVideoProvider) // If this is a single-SIM device, the "default SIM" will always be the only SIM. boolean isDefaultSmsAccount = - TelecomSystem.getInstance().getCallsManager().getPhoneAccountRegistrar() + mCallsManager.getPhoneAccountRegistrar() .isUserSelectedSmsPhoneAccount(call.getTargetPhoneAccount()); if (call.isRespondViaSmsCapable() && isDefaultSmsAccount) { capabilities |= android.telecom.Call.Details.CAPABILITY_RESPOND_VIA_TEXT; diff --git a/src/com/android/server/telecom/InCallWakeLockControllerFactory.java b/src/com/android/server/telecom/InCallWakeLockControllerFactory.java new file mode 100644 index 000000000..86335ba53 --- /dev/null +++ b/src/com/android/server/telecom/InCallWakeLockControllerFactory.java @@ -0,0 +1,31 @@ +/* + * Copyright 2015, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.telecom; + +import android.content.Context; + +/** + * This is a TEMPORARY fix to make the {@link InCallWakeLockController} object injectable for + * testing. Class {@link InCallWakeLockController} itself is not testable because it grabs lots of + * special stuff from its {@code Context} that cannot be conveniently mocked. + * + * TODO: Replace with a better design. + */ +public interface InCallWakeLockControllerFactory { + + InCallWakeLockController create(Context context, CallsManager callsManager); +} diff --git a/src/com/android/server/telecom/Log.java b/src/com/android/server/telecom/Log.java index 3ec826751..451e86d2f 100644 --- a/src/com/android/server/telecom/Log.java +++ b/src/com/android/server/telecom/Log.java @@ -25,13 +25,16 @@ import java.util.IllegalFormatException; import java.util.Locale; +import com.android.internal.annotations.VisibleForTesting; + /** * Manages logging for the entire module. */ +@VisibleForTesting public class Log { // Generic tag for all In Call logging - private static final String TAG = "Telecom"; + private static String TAG = "Telecom"; public static final boolean FORCE_LOGGING = false; /* STOP SHIP if true */ public static final boolean SYSTRACE_DEBUG = false; /* STOP SHIP if true */ @@ -43,6 +46,11 @@ public class Log { private Log() {} + @VisibleForTesting + public static void setTag(String tag) { + TAG = tag; + } + public static boolean isLoggable(int level) { return FORCE_LOGGING || android.util.Log.isLoggable(TAG, level); } diff --git a/src/com/android/server/telecom/MissedCallNotifier.java b/src/com/android/server/telecom/MissedCallNotifier.java index dbbc49e06..1dec123b9 100644 --- a/src/com/android/server/telecom/MissedCallNotifier.java +++ b/src/com/android/server/telecom/MissedCallNotifier.java @@ -16,339 +16,14 @@ package com.android.server.telecom; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.TaskStackBuilder; -import android.content.AsyncQueryHandler; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.UserHandle; -import android.provider.CallLog; -import android.provider.CallLog.Calls; -import android.telecom.CallState; -import android.telecom.DisconnectCause; -import android.telecom.PhoneAccount; -import android.telephony.PhoneNumberUtils; -import android.text.BidiFormatter; -import android.text.TextDirectionHeuristics; -import android.text.TextUtils; - -// TODO: Needed for move to system service: import com.android.internal.R; - /** * Creates a notification for calls that the user missed (neither answered nor rejected). - * TODO: Make TelephonyManager.clearMissedCalls call into this class. */ -class MissedCallNotifier extends CallsManagerListenerBase { - - private static final String[] CALL_LOG_PROJECTION = new String[] { - Calls._ID, - Calls.NUMBER, - Calls.NUMBER_PRESENTATION, - Calls.DATE, - Calls.DURATION, - Calls.TYPE, - }; - - private static final int CALL_LOG_COLUMN_ID = 0; - private static final int CALL_LOG_COLUMN_NUMBER = 1; - private static final int CALL_LOG_COLUMN_NUMBER_PRESENTATION = 2; - private static final int CALL_LOG_COLUMN_DATE = 3; - private static final int CALL_LOG_COLUMN_DURATION = 4; - private static final int CALL_LOG_COLUMN_TYPE = 5; - - private static final int MISSED_CALL_NOTIFICATION_ID = 1; - - private final Context mContext; - private CallsManager mCallsManager; - private final NotificationManager mNotificationManager; - - // Used to track the number of missed calls. - private int mMissedCallCount = 0; - - MissedCallNotifier(Context context) { - mContext = context; - mNotificationManager = - (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - - updateOnStartup(); - } - - void setCallsManager(CallsManager callsManager) { - this.mCallsManager = callsManager; - } - - /** {@inheritDoc} */ - @Override - public void onCallStateChanged(Call call, int oldState, int newState) { - if (oldState == CallState.RINGING && newState == CallState.DISCONNECTED && - call.getDisconnectCause().getCode() == DisconnectCause.MISSED) { - showMissedCallNotification(call); - } - } - - /** Clears missed call notification and marks the call log's missed calls as read. */ - void clearMissedCalls() { - AsyncTask.execute(new Runnable() { - @Override - public void run() { - // Clear the list of new missed calls from the call log. - ContentValues values = new ContentValues(); - values.put(Calls.NEW, 0); - values.put(Calls.IS_READ, 1); - StringBuilder where = new StringBuilder(); - where.append(Calls.NEW); - where.append(" = 1 AND "); - where.append(Calls.TYPE); - where.append(" = ?"); - mContext.getContentResolver().update(Calls.CONTENT_URI, values, where.toString(), - new String[]{ Integer.toString(Calls.MISSED_TYPE) }); - } - }); - cancelMissedCallNotification(); - } - - /** - * Create a system notification for the missed call. - * - * @param call The missed call. - */ - void showMissedCallNotification(Call call) { - mMissedCallCount++; - - final int titleResId; - final String expandedText; // The text in the notification's line 1 and 2. - - // Display the first line of the notification: - // 1 missed call: - // More than 1 missed call: + "missed calls" - if (mMissedCallCount == 1) { - titleResId = R.string.notification_missedCallTitle; - expandedText = getNameForCall(call); - } else { - titleResId = R.string.notification_missedCallsTitle; - expandedText = - mContext.getString(R.string.notification_missedCallsMsg, mMissedCallCount); - } - - // Create the notification. - Notification.Builder builder = new Notification.Builder(mContext); - builder.setSmallIcon(android.R.drawable.stat_notify_missed_call) - .setColor(mContext.getResources().getColor(R.color.theme_color)) - .setWhen(call.getCreationTimeMillis()) - .setContentTitle(mContext.getText(titleResId)) - .setContentText(expandedText) - .setContentIntent(createCallLogPendingIntent()) - .setAutoCancel(true) - .setDeleteIntent(createClearMissedCallsPendingIntent()); - - Uri handleUri = call.getHandle(); - String handle = handleUri == null ? null : handleUri.getSchemeSpecificPart(); - - // Add additional actions when there is only 1 missed call, like call-back and SMS. - if (mMissedCallCount == 1) { - Log.d(this, "Add actions with number %s.", Log.piiHandle(handle)); - - if (!TextUtils.isEmpty(handle) - && !TextUtils.equals(handle, mContext.getString(R.string.handle_restricted))) { - builder.addAction(R.drawable.stat_sys_phone_call, - mContext.getString(R.string.notification_missedCall_call_back), - createCallBackPendingIntent(handleUri)); - - builder.addAction(R.drawable.ic_text_holo_dark, - mContext.getString(R.string.notification_missedCall_message), - createSendSmsFromNotificationPendingIntent(handleUri)); - } - - Bitmap photoIcon = call.getPhotoIcon(); - if (photoIcon != null) { - builder.setLargeIcon(photoIcon); - } else { - Drawable photo = call.getPhoto(); - if (photo != null && photo instanceof BitmapDrawable) { - builder.setLargeIcon(((BitmapDrawable) photo).getBitmap()); - } - } - } else { - Log.d(this, "Suppress actions. handle: %s, missedCalls: %d.", Log.piiHandle(handle), - mMissedCallCount); - } - - Notification notification = builder.build(); - configureLedOnNotification(notification); - - Log.i(this, "Adding missed call notification for %s.", call); - mNotificationManager.notifyAsUser( - null /* tag */ , MISSED_CALL_NOTIFICATION_ID, notification, UserHandle.CURRENT); - } - - /** Cancels the "missed call" notification. */ - private void cancelMissedCallNotification() { - // Reset the number of missed calls to 0. - mMissedCallCount = 0; - mNotificationManager.cancel(MISSED_CALL_NOTIFICATION_ID); - } - - /** - * Returns the name to use in the missed call notification. - */ - private String getNameForCall(Call call) { - String handle = call.getHandle() == null ? null : call.getHandle().getSchemeSpecificPart(); - String name = call.getName(); - - if (!TextUtils.isEmpty(name) && TextUtils.isGraphic(name)) { - return name; - } else if (!TextUtils.isEmpty(handle)) { - // A handle should always be displayed LTR using {@link BidiFormatter} regardless of the - // content of the rest of the notification. - // TODO: Does this apply to SIP addresses? - BidiFormatter bidiFormatter = BidiFormatter.getInstance(); - return bidiFormatter.unicodeWrap(handle, TextDirectionHeuristics.LTR); - } else { - // Use "unknown" if the call is unidentifiable. - return mContext.getString(R.string.unknown); - } - } - - /** - * Creates a new pending intent that sends the user to the call log. - * - * @return The pending intent. - */ - private PendingIntent createCallLogPendingIntent() { - Intent intent = new Intent(Intent.ACTION_VIEW, null); - intent.setType(CallLog.Calls.CONTENT_TYPE); - - TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(mContext); - taskStackBuilder.addNextIntent(intent); - - return taskStackBuilder.getPendingIntent(0, 0); - } - - /** - * Creates an intent to be invoked when the missed call notification is cleared. - */ - private PendingIntent createClearMissedCallsPendingIntent() { - return createTelecomPendingIntent( - TelecomBroadcastIntentProcessor.ACTION_CLEAR_MISSED_CALLS, null); - } - - /** - * Creates an intent to be invoked when the user opts to "call back" from the missed call - * notification. - * - * @param handle The handle to call back. - */ - private PendingIntent createCallBackPendingIntent(Uri handle) { - return createTelecomPendingIntent( - TelecomBroadcastIntentProcessor.ACTION_CALL_BACK_FROM_NOTIFICATION, handle); - } - - /** - * Creates an intent to be invoked when the user opts to "send sms" from the missed call - * notification. - */ - private PendingIntent createSendSmsFromNotificationPendingIntent(Uri handle) { - return createTelecomPendingIntent( - TelecomBroadcastIntentProcessor.ACTION_SEND_SMS_FROM_NOTIFICATION, - Uri.fromParts(Constants.SCHEME_SMSTO, handle.getSchemeSpecificPart(), null)); - } - - /** - * Creates generic pending intent from the specified parameters to be received by - * {@link TelecomBroadcastIntentProcessor}. - * - * @param action The intent action. - * @param data The intent data. - */ - private PendingIntent createTelecomPendingIntent(String action, Uri data) { - Intent intent = new Intent(action, data, mContext, TelecomBroadcastIntentProcessor.class); - return PendingIntent.getBroadcast(mContext, 0, intent, 0); - } - - /** - * Configures a notification to emit the blinky notification light. - */ - private void configureLedOnNotification(Notification notification) { - notification.flags |= Notification.FLAG_SHOW_LIGHTS; - notification.defaults |= Notification.DEFAULT_LIGHTS; - } - - /** - * Adds the missed call notification on startup if there are unread missed calls. - */ - private void updateOnStartup() { - Log.d(this, "updateOnStartup()..."); - - // instantiate query handler - AsyncQueryHandler queryHandler = new AsyncQueryHandler(mContext.getContentResolver()) { - @Override - protected void onQueryComplete(int token, Object cookie, Cursor cursor) { - Log.d(MissedCallNotifier.this, "onQueryComplete()..."); - if (cursor != null) { - try { - while (cursor.moveToNext()) { - // Get data about the missed call from the cursor - final String handleString = cursor.getString(CALL_LOG_COLUMN_NUMBER); - final int presentation = - cursor.getInt(CALL_LOG_COLUMN_NUMBER_PRESENTATION); - final long date = cursor.getLong(CALL_LOG_COLUMN_DATE); - - final Uri handle; - if (presentation != Calls.PRESENTATION_ALLOWED - || TextUtils.isEmpty(handleString)) { - handle = null; - } else { - handle = Uri.fromParts(PhoneNumberUtils.isUriNumber(handleString) ? - PhoneAccount.SCHEME_SIP : PhoneAccount.SCHEME_TEL, - handleString, null); - } - - // Convert the data to a call object - Call call = new Call(mContext, mCallsManager, - null, null, null, null, null, true, - false); - call.setDisconnectCause(new DisconnectCause(DisconnectCause.MISSED)); - call.setState(CallState.DISCONNECTED); - call.setCreationTimeMillis(date); +public interface MissedCallNotifier extends CallsManager.CallsManagerListener { - // Listen for the update to the caller information before posting the - // notification so that we have the contact info and photo. - call.addListener(new Call.ListenerBase() { - @Override - public void onCallerInfoChanged(Call call) { - call.removeListener(this); // No longer need to listen to call - // changes after the contact info - // is retrieved. - showMissedCallNotification(call); - } - }); - // Set the handle here because that is what triggers the contact info - // query. - call.setHandle(handle, presentation); - } - } finally { - cursor.close(); - } - } - } - }; + void setCallsManager(CallsManager callsManager); - // setup query spec, look for all Missed calls that are new. - StringBuilder where = new StringBuilder("type="); - where.append(Calls.MISSED_TYPE); - where.append(" AND new=1"); + void clearMissedCalls(); - // start the query - queryHandler.startQuery(0, null, Calls.CONTENT_URI, CALL_LOG_PROJECTION, - where.toString(), null, Calls.DEFAULT_SORT_ORDER); - } + void showMissedCallNotification(Call call); } diff --git a/src/com/android/server/telecom/PhoneStateBroadcaster.java b/src/com/android/server/telecom/PhoneStateBroadcaster.java index 1a1f42786..bf0d3b858 100644 --- a/src/com/android/server/telecom/PhoneStateBroadcaster.java +++ b/src/com/android/server/telecom/PhoneStateBroadcaster.java @@ -29,10 +29,12 @@ */ final class PhoneStateBroadcaster extends CallsManagerListenerBase { + private final CallsManager mCallsManager; private final ITelephonyRegistry mRegistry; private int mCurrentState = TelephonyManager.CALL_STATE_IDLE; - public PhoneStateBroadcaster() { + public PhoneStateBroadcaster(CallsManager callsManager) { + mCallsManager = callsManager; mRegistry = ITelephonyRegistry.Stub.asInterface(ServiceManager.getService( "telephony.registry")); if (mRegistry == null) { @@ -44,7 +46,7 @@ public PhoneStateBroadcaster() { public void onCallStateChanged(Call call, int oldState, int newState) { if ((newState == CallState.DIALING || newState == CallState.ACTIVE || newState == CallState.ON_HOLD) && - !TelecomSystem.getInstance().getCallsManager().hasRingingCall()) { + !mCallsManager.hasRingingCall()) { /* * EXTRA_STATE_RINGING takes precedence over EXTRA_STATE_OFFHOOK, so if there is * already a ringing call, don't broadcast EXTRA_STATE_OFFHOOK. @@ -64,11 +66,10 @@ public void onCallAdded(Call call) { public void onCallRemoved(Call call) { // Recalculate the current phone state based on the consolidated state of the remaining // calls in the call list. - final CallsManager callsManager = TelecomSystem.getInstance().getCallsManager(); int callState = TelephonyManager.CALL_STATE_IDLE; - if (callsManager.hasRingingCall()) { + if (mCallsManager.hasRingingCall()) { callState = TelephonyManager.CALL_STATE_RINGING; - } else if (callsManager.getFirstCallWithState(CallState.DIALING, CallState.ACTIVE, + } else if (mCallsManager.getFirstCallWithState(CallState.DIALING, CallState.ACTIVE, CallState.ON_HOLD) != null) { callState = TelephonyManager.CALL_STATE_OFFHOOK; } diff --git a/src/com/android/server/telecom/ProximitySensorManager.java b/src/com/android/server/telecom/ProximitySensorManager.java index d18e4ac9e..5fddb89b1 100644 --- a/src/com/android/server/telecom/ProximitySensorManager.java +++ b/src/com/android/server/telecom/ProximitySensorManager.java @@ -26,8 +26,9 @@ public class ProximitySensorManager extends CallsManagerListenerBase { private static final String TAG = ProximitySensorManager.class.getSimpleName(); private final PowerManager.WakeLock mProximityWakeLock; + private final CallsManager mCallsManager; - public ProximitySensorManager(Context context) { + public ProximitySensorManager(Context context, CallsManager callsManager) { PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); if (pm.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) { @@ -36,12 +37,14 @@ public ProximitySensorManager(Context context) { } else { mProximityWakeLock = null; } + + mCallsManager = callsManager; Log.d(this, "onCreate: mProximityWakeLock: ", mProximityWakeLock); } @Override public void onCallRemoved(Call call) { - if (TelecomSystem.getInstance().getCallsManager().getCalls().isEmpty()) { + if (mCallsManager.getCalls().isEmpty()) { Log.i(this, "All calls removed, resetting proximity sensor to default state"); turnOff(true); } @@ -52,7 +55,7 @@ public void onCallRemoved(Call call) { * Turn the proximity sensor on. */ void turnOn() { - if (TelecomSystem.getInstance().getCallsManager().getCalls().isEmpty()) { + if (mCallsManager.getCalls().isEmpty()) { Log.w(this, "Asking to turn on prox sensor without a call? I don't think so."); return; } diff --git a/src/com/android/server/telecom/ProximitySensorManagerFactory.java b/src/com/android/server/telecom/ProximitySensorManagerFactory.java new file mode 100644 index 000000000..b73636f7d --- /dev/null +++ b/src/com/android/server/telecom/ProximitySensorManagerFactory.java @@ -0,0 +1,32 @@ +/* + * Copyright 2015, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.telecom; + +import android.content.Context; + +/** + * This is a TEMPORARY fix to make the {@link ProximitySensorManager} object injectable for testing. + * Class {@link ProximitySensorManager} itself is not testable because it grabs lots of special + * stuff from its {@code Context} that cannot be conveniently mocked. + * + * TODO: Replace with a better design. + */ +public interface ProximitySensorManagerFactory { + + ProximitySensorManager create(Context context, CallsManager callsManager); + +} diff --git a/src/com/android/server/telecom/RespondViaSmsManager.java b/src/com/android/server/telecom/RespondViaSmsManager.java index 3a3432eac..874ca4c66 100644 --- a/src/com/android/server/telecom/RespondViaSmsManager.java +++ b/src/com/android/server/telecom/RespondViaSmsManager.java @@ -44,6 +44,8 @@ public class RespondViaSmsManager extends CallsManagerListenerBase { private static final int MSG_CANNED_TEXT_MESSAGES_READY = 1; private static final int MSG_SHOW_SENT_TOAST = 2; + private final CallsManager mCallsManager; + private final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { @@ -80,7 +82,9 @@ public void handleMessage(Message msg) { } }; - public RespondViaSmsManager() {} + public RespondViaSmsManager(CallsManager callsManager) { + mCallsManager = callsManager; + } /** * Read the (customizable) canned responses from SharedPreferences, @@ -139,9 +143,7 @@ public void run() { @Override public void onIncomingCallRejected(Call call, boolean rejectWithMessage, String textMessage) { if (rejectWithMessage && call.getHandle() != null) { - PhoneAccountRegistrar phoneAccountRegistrar = - TelecomSystem.getInstance().getCallsManager().getPhoneAccountRegistrar(); - int subId = phoneAccountRegistrar.getSubscriptionIdForPhoneAccount( + int subId = mCallsManager.getPhoneAccountRegistrar().getSubscriptionIdForPhoneAccount( call.getTargetPhoneAccount()); rejectCallWithMessage(call.getContext(), call.getHandle().getSchemeSpecificPart(), textMessage, subId); diff --git a/src/com/android/server/telecom/ServiceBinder.java b/src/com/android/server/telecom/ServiceBinder.java index 8baf3ecd4..a4bcab0ef 100644 --- a/src/com/android/server/telecom/ServiceBinder.java +++ b/src/com/android/server/telecom/ServiceBinder.java @@ -21,8 +21,6 @@ import android.content.Intent; import android.content.ServiceConnection; import android.os.IBinder; -import android.os.IInterface; -import android.os.Process; import android.os.UserHandle; import android.text.TextUtils; import android.util.ArraySet; diff --git a/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java b/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java index 3110d469c..2b22ea9be 100644 --- a/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java +++ b/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java @@ -20,27 +20,25 @@ import android.content.Intent; import android.os.UserHandle; -/** - * Handles miscellaneous Telecom broadcast intents. This should be visible from outside, but - * should not be in the "exported" state. - */ public final class TelecomBroadcastIntentProcessor { /** The action used to send SMS response for the missed call notification. */ - static final String ACTION_SEND_SMS_FROM_NOTIFICATION = + public static final String ACTION_SEND_SMS_FROM_NOTIFICATION = "com.android.server.telecom.ACTION_SEND_SMS_FROM_NOTIFICATION"; /** The action used to call a handle back for the missed call notification. */ - static final String ACTION_CALL_BACK_FROM_NOTIFICATION = + public static final String ACTION_CALL_BACK_FROM_NOTIFICATION = "com.android.server.telecom.ACTION_CALL_BACK_FROM_NOTIFICATION"; /** The action used to clear missed calls. */ - static final String ACTION_CLEAR_MISSED_CALLS = + public static final String ACTION_CLEAR_MISSED_CALLS = "com.android.server.telecom.ACTION_CLEAR_MISSED_CALLS"; private final Context mContext; + private final CallsManager mCallsManager; - public TelecomBroadcastIntentProcessor(Context context) { + public TelecomBroadcastIntentProcessor(Context context, CallsManager callsManager) { mContext = context; + mCallsManager = callsManager; } public void processIntent(Intent intent) { @@ -48,8 +46,7 @@ public void processIntent(Intent intent) { Log.v(this, "Action received: %s.", action); - MissedCallNotifier missedCallNotifier = TelecomSystem.getInstance() - .getCallsManager().getMissedCallNotifier(); + MissedCallNotifier missedCallNotifier = mCallsManager.getMissedCallNotifier(); // Send an SMS from the missed call notification. if (ACTION_SEND_SMS_FROM_NOTIFICATION.equals(action)) { diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java index 476411c6b..9c0b707c8 100644 --- a/src/com/android/server/telecom/TelecomServiceImpl.java +++ b/src/com/android/server/telecom/TelecomServiceImpl.java @@ -84,10 +84,10 @@ public void handleMessage(Message msg) { Object result = null; switch (msg.what) { case MSG_SILENCE_RINGER: - TelecomSystem.getInstance().getCallsManager().getRinger().silence(); + mCallsManager.getRinger().silence(); break; case MSG_SHOW_CALL_SCREEN: - TelecomSystem.getInstance().getCallsManager().getInCallController().bringToForeground(msg.arg1 == 1); + mCallsManager.getInCallController().bringToForeground(msg.arg1 == 1); break; case MSG_END_CALL: result = endCallInternal(); @@ -96,13 +96,13 @@ public void handleMessage(Message msg) { acceptRingingCallInternal(); break; case MSG_CANCEL_MISSED_CALLS_NOTIFICATION: - TelecomSystem.getInstance().getMissedCallNotifier().clearMissedCalls(); + mCallsManager.getMissedCallNotifier().clearMissedCalls(); break; case MSG_IS_TTY_SUPPORTED: - result = TelecomSystem.getInstance().getCallsManager().isTtySupported(); + result = mCallsManager.isTtySupported(); break; case MSG_GET_CURRENT_TTY_MODE: - result = TelecomSystem.getInstance().getCallsManager().getCurrentTtyMode(); + result = mCallsManager.getCurrentTtyMode(); break; case MSG_NEW_INCOMING_CALL: if (request.arg == null || !(request.arg instanceof Intent)) { @@ -140,13 +140,17 @@ public void handleMessage(Message msg) { private final MainThreadHandler mMainThreadHandler = new MainThreadHandler(); - private AppOpsManager mAppOpsManager; - private UserManager mUserManager; - private PackageManager mPackageManager; - private TelecomBinderImpl mBinderImpl; - private CallsManager mCallsManager; - - public TelecomServiceImpl(Context context, CallsManager callsManager) { + private final AppOpsManager mAppOpsManager; + private final UserManager mUserManager; + private final PackageManager mPackageManager; + private final TelecomBinderImpl mBinderImpl; + private final CallsManager mCallsManager; + private final PhoneAccountRegistrar mPhoneAccountRegistrar; + + public TelecomServiceImpl( + Context context, + CallsManager callsManager, + PhoneAccountRegistrar phoneAccountRegistrar) { mContext = context; mAppOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE); mBinderImpl = new TelecomBinderImpl(); @@ -155,6 +159,7 @@ public TelecomServiceImpl(Context context, CallsManager callsManager) { mPackageManager = mContext.getPackageManager(); mCallsManager = callsManager; + mPhoneAccountRegistrar = phoneAccountRegistrar; } public IBinder getBinder() { @@ -172,7 +177,7 @@ public PhoneAccountHandle getDefaultOutgoingPhoneAccount(String uriScheme) { long token = Binder.clearCallingIdentity(); try { PhoneAccountHandle defaultOutgoingPhoneAccount = - TelecomSystem.getInstance().getPhoneAccountRegistrar().getDefaultOutgoingPhoneAccount(uriScheme); + mCallsManager.getPhoneAccountRegistrar().getDefaultOutgoingPhoneAccount(uriScheme); // Make sure that the calling user can see this phone account. if (defaultOutgoingPhoneAccount != null && !isVisibleToCaller(defaultOutgoingPhoneAccount)) { @@ -192,7 +197,7 @@ public PhoneAccountHandle getDefaultOutgoingPhoneAccount(String uriScheme) { public PhoneAccountHandle getUserSelectedOutgoingPhoneAccount() { try { PhoneAccountHandle userSelectedOutgoingPhoneAccount = - TelecomSystem.getInstance().getPhoneAccountRegistrar().getUserSelectedOutgoingPhoneAccount(); + mCallsManager.getPhoneAccountRegistrar().getUserSelectedOutgoingPhoneAccount(); // Make sure that the calling user can see this phone account. if (!isVisibleToCaller(userSelectedOutgoingPhoneAccount)) { Log.w(this, "No account found for the calling user"); @@ -210,7 +215,7 @@ public void setUserSelectedOutgoingPhoneAccount(PhoneAccountHandle accountHandle enforceModifyPermission(); try { - TelecomSystem.getInstance().getPhoneAccountRegistrar().setUserSelectedOutgoingPhoneAccount(accountHandle); + mCallsManager.getPhoneAccountRegistrar().setUserSelectedOutgoingPhoneAccount(accountHandle); } catch (Exception e) { Log.e(this, e, "setUserSelectedOutgoingPhoneAccount"); throw e; @@ -223,7 +228,7 @@ public List getCallCapablePhoneAccounts() { long token = Binder.clearCallingIdentity(); try { return filterForAccountsVisibleToCaller( - TelecomSystem.getInstance().getPhoneAccountRegistrar().getCallCapablePhoneAccounts()); + mCallsManager.getPhoneAccountRegistrar().getCallCapablePhoneAccounts()); } catch (Exception e) { Log.e(this, e, "getCallCapablePhoneAccounts"); throw e; @@ -238,7 +243,7 @@ public List getPhoneAccountsSupportingScheme(String uriSchem long token = Binder.clearCallingIdentity(); try { return filterForAccountsVisibleToCaller( - TelecomSystem.getInstance().getPhoneAccountRegistrar().getCallCapablePhoneAccounts(uriScheme)); + mCallsManager.getPhoneAccountRegistrar().getCallCapablePhoneAccounts(uriScheme)); } catch (Exception e) { Log.e(this, e, "getPhoneAccountsSupportingScheme %s", uriScheme); throw e; @@ -251,7 +256,7 @@ public List getPhoneAccountsSupportingScheme(String uriSchem public List getPhoneAccountsForPackage(String packageName) { try { return filterForAccountsVisibleToCaller( - TelecomSystem.getInstance().getPhoneAccountRegistrar().getPhoneAccountsForPackage(packageName)); + mCallsManager.getPhoneAccountRegistrar().getPhoneAccountsForPackage(packageName)); } catch (Exception e) { Log.e(this, e, "getPhoneAccountsForPackage %s", packageName); throw e; @@ -265,7 +270,7 @@ public PhoneAccount getPhoneAccount(PhoneAccountHandle accountHandle) { Log.w(this, "%s is not visible for the calling user", accountHandle); return null; } - return TelecomSystem.getInstance().getPhoneAccountRegistrar().getPhoneAccountInternal(accountHandle); + return mCallsManager.getPhoneAccountRegistrar().getPhoneAccountInternal(accountHandle); } catch (Exception e) { Log.e(this, e, "getPhoneAccount %s", accountHandle); throw e; @@ -286,7 +291,7 @@ public int getAllPhoneAccountsCount() { @Override public List getAllPhoneAccounts() { try { - List allPhoneAccounts = TelecomSystem.getInstance().getPhoneAccountRegistrar().getAllPhoneAccounts(); + List allPhoneAccounts = mCallsManager.getPhoneAccountRegistrar().getAllPhoneAccounts(); List profilePhoneAccounts = new ArrayList<>(allPhoneAccounts.size()); for (PhoneAccount phoneAccount : profilePhoneAccounts) { if (isVisibleToCaller(phoneAccount)) { @@ -304,7 +309,7 @@ public List getAllPhoneAccounts() { public List getAllPhoneAccountHandles() { try { return filterForAccountsVisibleToCaller( - TelecomSystem.getInstance().getPhoneAccountRegistrar().getAllPhoneAccountHandles()); + mCallsManager.getPhoneAccountRegistrar().getAllPhoneAccountHandles()); } catch (Exception e) { Log.e(this, e, "getAllPhoneAccounts"); throw e; @@ -314,7 +319,7 @@ public List getAllPhoneAccountHandles() { @Override public PhoneAccountHandle getSimCallManager() { try { - PhoneAccountHandle accountHandle = TelecomSystem.getInstance().getPhoneAccountRegistrar().getSimCallManager(); + PhoneAccountHandle accountHandle = mCallsManager.getPhoneAccountRegistrar().getSimCallManager(); if (!isVisibleToCaller(accountHandle)) { Log.w(this, "%s is not visible for the calling user", accountHandle); return null; @@ -331,7 +336,7 @@ public void setSimCallManager(PhoneAccountHandle accountHandle) { enforceModifyPermission(); try { - TelecomSystem.getInstance().getPhoneAccountRegistrar().setSimCallManager(accountHandle); + mCallsManager.getPhoneAccountRegistrar().setSimCallManager(accountHandle); } catch (Exception e) { Log.e(this, e, "setSimCallManager"); throw e; @@ -344,7 +349,7 @@ public List getSimCallManagers() { long token = Binder.clearCallingIdentity(); try { return filterForAccountsVisibleToCaller( - TelecomSystem.getInstance().getPhoneAccountRegistrar().getConnectionManagerPhoneAccounts()); + mCallsManager.getPhoneAccountRegistrar().getConnectionManagerPhoneAccounts()); } catch (Exception e) { Log.e(this, e, "getSimCallManagers"); throw e; @@ -372,7 +377,7 @@ public void registerPhoneAccount(PhoneAccount account) { } enforceUserHandleMatchesCaller(account.getAccountHandle()); - TelecomSystem.getInstance().getPhoneAccountRegistrar().registerPhoneAccount(account); + mPhoneAccountRegistrar.registerPhoneAccount(account); // Broadcast an intent indicating the phone account which was registered. long token = Binder.clearCallingIdentity(); @@ -395,7 +400,7 @@ public void unregisterPhoneAccount(PhoneAccountHandle accountHandle) { enforcePhoneAccountModificationForPackage( accountHandle.getComponentName().getPackageName()); enforceUserHandleMatchesCaller(accountHandle); - TelecomSystem.getInstance().getPhoneAccountRegistrar().unregisterPhoneAccount(accountHandle); + mCallsManager.getPhoneAccountRegistrar().unregisterPhoneAccount(accountHandle); } catch (Exception e) { Log.e(this, e, "unregisterPhoneAccount %s", accountHandle); throw e; @@ -406,7 +411,7 @@ public void unregisterPhoneAccount(PhoneAccountHandle accountHandle) { public void clearAccounts(String packageName) { try { enforcePhoneAccountModificationForPackage(packageName); - TelecomSystem.getInstance().getPhoneAccountRegistrar().clearAccounts(packageName, Binder.getCallingUserHandle()); + mCallsManager.getPhoneAccountRegistrar().clearAccounts(packageName, Binder.getCallingUserHandle()); } catch (Exception e) { Log.e(this, e, "clearAccounts %s", packageName); throw e; @@ -424,7 +429,7 @@ public boolean isVoiceMailNumber(PhoneAccountHandle accountHandle, String number Log.w(this, "%s is not visible for the calling user", accountHandle); return false; } - return TelecomSystem.getInstance().getPhoneAccountRegistrar().isVoiceMailNumber(accountHandle, number); + return mCallsManager.getPhoneAccountRegistrar().isVoiceMailNumber(accountHandle, number); } catch (Exception e) { Log.e(this, e, "getSubscriptionIdForPhoneAccount"); throw e; @@ -445,7 +450,7 @@ public boolean hasVoiceMailNumber(PhoneAccountHandle accountHandle) { int subId = SubscriptionManager.getDefaultVoiceSubId(); if (accountHandle != null) { - subId = TelecomSystem.getInstance().getPhoneAccountRegistrar().getSubscriptionIdForPhoneAccount(accountHandle); + subId = mCallsManager.getPhoneAccountRegistrar().getSubscriptionIdForPhoneAccount(accountHandle); } return !TextUtils.isEmpty(getTelephonyManager().getVoiceMailNumber(subId)); } catch (Exception e) { @@ -502,7 +507,7 @@ public boolean isInCall() { enforceReadPermission(); // Do not use sendRequest() with this method since it could cause a deadlock with // audio service, which we call into from the main thread: AudioManager.setMode(). - final int callState = TelecomSystem.getInstance().getCallsManager().getCallState(); + final int callState = mCallsManager.getCallState(); return callState == TelephonyManager.CALL_STATE_OFFHOOK || callState == TelephonyManager.CALL_STATE_RINGING; } @@ -513,7 +518,7 @@ public boolean isInCall() { @Override public boolean isRinging() { enforceReadPermission(); - return TelecomSystem.getInstance().getCallsManager().getCallState() == TelephonyManager.CALL_STATE_RINGING; + return mCallsManager.getCallState() == TelephonyManager.CALL_STATE_RINGING; } /** @@ -521,7 +526,7 @@ public boolean isRinging() { */ @Override public int getCallState() { - return TelecomSystem.getInstance().getCallsManager().getCallState(); + return mCallsManager.getCallState(); } /** @@ -596,7 +601,8 @@ public boolean handlePinMmiForPhoneAccount(PhoneAccountHandle accountHandle, long token = Binder.clearCallingIdentity(); boolean retval = false; try { - int subId = TelecomSystem.getInstance().getPhoneAccountRegistrar().getSubscriptionIdForPhoneAccount(accountHandle); + int subId = mCallsManager.getPhoneAccountRegistrar() + .getSubscriptionIdForPhoneAccount(accountHandle); retval = getTelephonyManager().handlePinMmiForSubscriber(subId, dialString); } finally { Binder.restoreCallingIdentity(token); @@ -621,7 +627,8 @@ public Uri getAdnUriForPhoneAccount(PhoneAccountHandle accountHandle) { long token = Binder.clearCallingIdentity(); String retval = "content://icc/adn/"; try { - long subId = TelecomSystem.getInstance().getPhoneAccountRegistrar().getSubscriptionIdForPhoneAccount(accountHandle); + long subId = mCallsManager.getPhoneAccountRegistrar() + .getSubscriptionIdForPhoneAccount(accountHandle); retval = retval + "subId/" + subId; } finally { Binder.restoreCallingIdentity(token); @@ -723,15 +730,15 @@ protected void dump(FileDescriptor fd, final PrintWriter writer, String[] args) } final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " "); - if (TelecomSystem.getInstance().getCallsManager() != null) { + if (mCallsManager != null) { pw.println("CallsManager: "); pw.increaseIndent(); - TelecomSystem.getInstance().getCallsManager().dump(pw); + mCallsManager.dump(pw); pw.decreaseIndent(); pw.println("PhoneAccountRegistrar: "); pw.increaseIndent(); - TelecomSystem.getInstance().getPhoneAccountRegistrar().dump(pw); + mCallsManager.getPhoneAccountRegistrar().dump(pw); pw.decreaseIndent(); } } @@ -746,7 +753,8 @@ private boolean isVisibleToCaller(PhoneAccountHandle accountHandle) { return false; } - return isVisibleToCaller(TelecomSystem.getInstance().getPhoneAccountRegistrar().getPhoneAccountInternal(accountHandle)); + return isVisibleToCaller(mCallsManager.getPhoneAccountRegistrar() + .getPhoneAccountInternal(accountHandle)); } private boolean isVisibleToCaller(PhoneAccount account) { @@ -825,7 +833,7 @@ private boolean isPackageSystemApp(String packageName) { } private void acceptRingingCallInternal() { - Call call = TelecomSystem.getInstance().getCallsManager().getFirstCallWithState(CallState.RINGING); + Call call = mCallsManager.getFirstCallWithState(CallState.RINGING); if (call != null) { call.answer(call.getVideoState()); } @@ -834,9 +842,9 @@ private void acceptRingingCallInternal() { private boolean endCallInternal() { // Always operate on the foreground call if one exists, otherwise get the first call in // priority order by call-state. - Call call = TelecomSystem.getInstance().getCallsManager().getForegroundCall(); + Call call = mCallsManager.getForegroundCall(); if (call == null) { - call = TelecomSystem.getInstance().getCallsManager().getFirstCallWithState( + call = mCallsManager.getFirstCallWithState( CallState.ACTIVE, CallState.DIALING, CallState.RINGING, diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java index 3634b8a6b..1506d2777 100644 --- a/src/com/android/server/telecom/TelecomSystem.java +++ b/src/com/android/server/telecom/TelecomSystem.java @@ -77,25 +77,38 @@ public static void setInstance(TelecomSystem instance) { INSTANCE = instance; } - public TelecomSystem(Context context) { + public TelecomSystem( + Context context, + MissedCallNotifier missedCallNotifier, + HeadsetMediaButtonFactory headsetMediaButtonFactory, + ProximitySensorManagerFactory proximitySensorManagerFactory, + InCallWakeLockControllerFactory inCallWakeLockControllerFactory) { mContext = context.getApplicationContext(); - mMissedCallNotifier = new MissedCallNotifier(mContext); + mMissedCallNotifier = missedCallNotifier; mPhoneAccountRegistrar = new PhoneAccountRegistrar(mContext); - mRespondViaSmsManager = new RespondViaSmsManager(); - mCallsManager = new CallsManager( - mContext, mMissedCallNotifier, mPhoneAccountRegistrar, mRespondViaSmsManager); - Log.i(this, "CallsManager initialized"); + mContext, + mMissedCallNotifier, + mPhoneAccountRegistrar, + headsetMediaButtonFactory, + proximitySensorManagerFactory, + inCallWakeLockControllerFactory); + + mRespondViaSmsManager = new RespondViaSmsManager(mCallsManager); + mCallsManager.setRespondViaSmsManager(mRespondViaSmsManager); + mMissedCallNotifier.setCallsManager(mCallsManager); mContext.registerReceiver(mUserSwitchedReceiver, USER_SWITCHED_FILTER); mBluetoothPhoneServiceImpl = - new BluetoothPhoneServiceImpl(context, mCallsManager, mPhoneAccountRegistrar); - mCallIntentProcessor = new CallIntentProcessor(context, mCallsManager); - mTelecomBroadcastIntentProcessor = new TelecomBroadcastIntentProcessor(context); - mTelecomServiceImpl = new TelecomServiceImpl(context, mCallsManager); + new BluetoothPhoneServiceImpl(mContext, mCallsManager, mPhoneAccountRegistrar); + mCallIntentProcessor = new CallIntentProcessor(mContext, mCallsManager); + mTelecomBroadcastIntentProcessor = new TelecomBroadcastIntentProcessor( + mContext, mCallsManager); + mTelecomServiceImpl = + new TelecomServiceImpl(mContext, mCallsManager, mPhoneAccountRegistrar); } public MissedCallNotifier getMissedCallNotifier() { diff --git a/src/com/android/server/telecom/components/TelecomService.java b/src/com/android/server/telecom/components/TelecomService.java index 0b8238bc6..8d5190b01 100644 --- a/src/com/android/server/telecom/components/TelecomService.java +++ b/src/com/android/server/telecom/components/TelecomService.java @@ -18,11 +18,20 @@ import android.app.Service; import android.bluetooth.BluetoothAdapter; +import android.content.Context; import android.content.Intent; import android.os.IBinder; +import com.android.server.telecom.CallsManager; +import com.android.server.telecom.HeadsetMediaButton; +import com.android.server.telecom.HeadsetMediaButtonFactory; +import com.android.server.telecom.InCallWakeLockControllerFactory; +import com.android.server.telecom.ProximitySensorManagerFactory; +import com.android.server.telecom.InCallWakeLockController; import com.android.server.telecom.Log; +import com.android.server.telecom.ProximitySensorManager; import com.android.server.telecom.TelecomSystem; +import com.android.server.telecom.ui.MissedCallNotifierImpl; /** * Implementation of the ITelecom interface. @@ -34,7 +43,32 @@ public IBinder onBind(Intent intent) { Log.d(this, "onBind"); // We are guaranteed that the TelecomService will be started before any other // components in this package because it is started and kept running by the system. - TelecomSystem.setInstance(new TelecomSystem(this)); + TelecomSystem.setInstance( + new TelecomSystem( + this, + new MissedCallNotifierImpl(getApplicationContext()), + new HeadsetMediaButtonFactory() { + @Override + public HeadsetMediaButton create(Context context, + CallsManager callsManager) { + return new HeadsetMediaButton(context, callsManager); + } + }, + new ProximitySensorManagerFactory() { + @Override + public ProximitySensorManager create( + Context context, + CallsManager callsManager) { + return new ProximitySensorManager(context, callsManager); + } + }, + new InCallWakeLockControllerFactory() { + @Override + public InCallWakeLockController create(Context context, + CallsManager callsManager) { + return new InCallWakeLockController(context, callsManager); + } + })); // Start the BluetoothPhoneService if (BluetoothAdapter.getDefaultAdapter() != null) { startService(new Intent(this, BluetoothPhoneService.class)); diff --git a/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java b/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java new file mode 100644 index 000000000..cd62d9171 --- /dev/null +++ b/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java @@ -0,0 +1,369 @@ +/* + * Copyright 2014, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.telecom.ui; + +import com.android.server.telecom.Call; +import com.android.server.telecom.CallsManager; +import com.android.server.telecom.CallsManagerListenerBase; +import com.android.server.telecom.Constants; +import com.android.server.telecom.MissedCallNotifier; +import com.android.server.telecom.Log; +import com.android.server.telecom.R; +import com.android.server.telecom.TelecomBroadcastIntentProcessor; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.TaskStackBuilder; +import android.content.AsyncQueryHandler; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.UserHandle; +import android.provider.CallLog.Calls; +import android.telecom.CallState; +import android.telecom.DisconnectCause; +import android.telecom.PhoneAccount; +import android.telephony.PhoneNumberUtils; +import android.text.BidiFormatter; +import android.text.TextDirectionHeuristics; +import android.text.TextUtils; + +// TODO: Needed for move to system service: import com.android.internal.R; + +/** + * Creates a notification for calls that the user missed (neither answered nor rejected). + * + * TODO: Make TelephonyManager.clearMissedCalls call into this class. + * + * TODO: Reduce dependencies in this implementation; remove the need to create a new Call + * simply to look up caller metadata, and if possible, make it unnecessary to get a + * direct reference to the CallsManager. Try to make this class simply handle the UI + * and Android-framework entanglements of missed call notification. + */ +public class MissedCallNotifierImpl extends CallsManagerListenerBase implements + MissedCallNotifier { + + private static final String[] CALL_LOG_PROJECTION = new String[] { + Calls._ID, + Calls.NUMBER, + Calls.NUMBER_PRESENTATION, + Calls.DATE, + Calls.DURATION, + Calls.TYPE, + }; + + private static final int CALL_LOG_COLUMN_ID = 0; + private static final int CALL_LOG_COLUMN_NUMBER = 1; + private static final int CALL_LOG_COLUMN_NUMBER_PRESENTATION = 2; + private static final int CALL_LOG_COLUMN_DATE = 3; + private static final int CALL_LOG_COLUMN_DURATION = 4; + private static final int CALL_LOG_COLUMN_TYPE = 5; + + private static final int MISSED_CALL_NOTIFICATION_ID = 1; + + private final Context mContext; + private CallsManager mCallsManager; + private final NotificationManager mNotificationManager; + + // Used to track the number of missed calls. + private int mMissedCallCount = 0; + + public MissedCallNotifierImpl(Context context) { + mContext = context; + mNotificationManager = + (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + + updateOnStartup(); + } + + public void setCallsManager(CallsManager callsManager) { + this.mCallsManager = callsManager; + } + + /** {@inheritDoc} */ + @Override + public void onCallStateChanged(Call call, int oldState, int newState) { + if (oldState == CallState.RINGING && newState == CallState.DISCONNECTED && + call.getDisconnectCause().getCode() == DisconnectCause.MISSED) { + showMissedCallNotification(call); + } + } + + /** Clears missed call notification and marks the call log's missed calls as read. */ + public void clearMissedCalls() { + AsyncTask.execute(new Runnable() { + @Override + public void run() { + // Clear the list of new missed calls from the call log. + ContentValues values = new ContentValues(); + values.put(Calls.NEW, 0); + values.put(Calls.IS_READ, 1); + StringBuilder where = new StringBuilder(); + where.append(Calls.NEW); + where.append(" = 1 AND "); + where.append(Calls.TYPE); + where.append(" = ?"); + mContext.getContentResolver().update(Calls.CONTENT_URI, values, where.toString(), + new String[]{ Integer.toString(Calls.MISSED_TYPE) }); + } + }); + cancelMissedCallNotification(); + } + + /** + * Create a system notification for the missed call. + * + * @param call The missed call. + */ + public void showMissedCallNotification(Call call) { + mMissedCallCount++; + + final int titleResId; + final String expandedText; // The text in the notification's line 1 and 2. + + // Display the first line of the notification: + // 1 missed call: + // More than 1 missed call: + "missed calls" + if (mMissedCallCount == 1) { + titleResId = R.string.notification_missedCallTitle; + expandedText = getNameForCall(call); + } else { + titleResId = R.string.notification_missedCallsTitle; + expandedText = + mContext.getString(R.string.notification_missedCallsMsg, mMissedCallCount); + } + + // Create the notification. + Notification.Builder builder = new Notification.Builder(mContext); + builder.setSmallIcon(android.R.drawable.stat_notify_missed_call) + .setColor(mContext.getResources().getColor(R.color.theme_color)) + .setWhen(call.getCreationTimeMillis()) + .setContentTitle(mContext.getText(titleResId)) + .setContentText(expandedText) + .setContentIntent(createCallLogPendingIntent()) + .setAutoCancel(true) + .setDeleteIntent(createClearMissedCallsPendingIntent()); + + Uri handleUri = call.getHandle(); + String handle = handleUri == null ? null : handleUri.getSchemeSpecificPart(); + + // Add additional actions when there is only 1 missed call, like call-back and SMS. + if (mMissedCallCount == 1) { + Log.d(this, "Add actions with number %s.", Log.piiHandle(handle)); + + if (!TextUtils.isEmpty(handle) + && !TextUtils.equals(handle, mContext.getString(R.string.handle_restricted))) { + builder.addAction(R.drawable.stat_sys_phone_call, + mContext.getString(R.string.notification_missedCall_call_back), + createCallBackPendingIntent(handleUri)); + + builder.addAction(R.drawable.ic_text_holo_dark, + mContext.getString(R.string.notification_missedCall_message), + createSendSmsFromNotificationPendingIntent(handleUri)); + } + + Bitmap photoIcon = call.getPhotoIcon(); + if (photoIcon != null) { + builder.setLargeIcon(photoIcon); + } else { + Drawable photo = call.getPhoto(); + if (photo != null && photo instanceof BitmapDrawable) { + builder.setLargeIcon(((BitmapDrawable) photo).getBitmap()); + } + } + } else { + Log.d(this, "Suppress actions. handle: %s, missedCalls: %d.", Log.piiHandle(handle), + mMissedCallCount); + } + + Notification notification = builder.build(); + configureLedOnNotification(notification); + + Log.i(this, "Adding missed call notification for %s.", call); + mNotificationManager.notifyAsUser( + null /* tag */ , MISSED_CALL_NOTIFICATION_ID, notification, UserHandle.CURRENT); + } + + /** Cancels the "missed call" notification. */ + private void cancelMissedCallNotification() { + // Reset the number of missed calls to 0. + mMissedCallCount = 0; + mNotificationManager.cancel(MISSED_CALL_NOTIFICATION_ID); + } + + /** + * Returns the name to use in the missed call notification. + */ + private String getNameForCall(Call call) { + String handle = call.getHandle() == null ? null : call.getHandle().getSchemeSpecificPart(); + String name = call.getName(); + + if (!TextUtils.isEmpty(name) && TextUtils.isGraphic(name)) { + return name; + } else if (!TextUtils.isEmpty(handle)) { + // A handle should always be displayed LTR using {@link BidiFormatter} regardless of the + // content of the rest of the notification. + // TODO: Does this apply to SIP addresses? + BidiFormatter bidiFormatter = BidiFormatter.getInstance(); + return bidiFormatter.unicodeWrap(handle, TextDirectionHeuristics.LTR); + } else { + // Use "unknown" if the call is unidentifiable. + return mContext.getString(R.string.unknown); + } + } + + /** + * Creates a new pending intent that sends the user to the call log. + * + * @return The pending intent. + */ + private PendingIntent createCallLogPendingIntent() { + Intent intent = new Intent(Intent.ACTION_VIEW, null); + intent.setType(Calls.CONTENT_TYPE); + + TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(mContext); + taskStackBuilder.addNextIntent(intent); + + return taskStackBuilder.getPendingIntent(0, 0); + } + + /** + * Creates an intent to be invoked when the missed call notification is cleared. + */ + private PendingIntent createClearMissedCallsPendingIntent() { + return createTelecomPendingIntent( + TelecomBroadcastIntentProcessor.ACTION_CLEAR_MISSED_CALLS, null); + } + + /** + * Creates an intent to be invoked when the user opts to "call back" from the missed call + * notification. + * + * @param handle The handle to call back. + */ + private PendingIntent createCallBackPendingIntent(Uri handle) { + return createTelecomPendingIntent( + TelecomBroadcastIntentProcessor.ACTION_CALL_BACK_FROM_NOTIFICATION, handle); + } + + /** + * Creates an intent to be invoked when the user opts to "send sms" from the missed call + * notification. + */ + private PendingIntent createSendSmsFromNotificationPendingIntent(Uri handle) { + return createTelecomPendingIntent( + TelecomBroadcastIntentProcessor.ACTION_SEND_SMS_FROM_NOTIFICATION, + Uri.fromParts(Constants.SCHEME_SMSTO, handle.getSchemeSpecificPart(), null)); + } + + /** + * Creates generic pending intent from the specified parameters to be received by + * {@link TelecomBroadcastIntentProcessor}. + * + * @param action The intent action. + * @param data The intent data. + */ + private PendingIntent createTelecomPendingIntent(String action, Uri data) { + Intent intent = new Intent(action, data, mContext, TelecomBroadcastIntentProcessor.class); + return PendingIntent.getBroadcast(mContext, 0, intent, 0); + } + + /** + * Configures a notification to emit the blinky notification light. + */ + private void configureLedOnNotification(Notification notification) { + notification.flags |= Notification.FLAG_SHOW_LIGHTS; + notification.defaults |= Notification.DEFAULT_LIGHTS; + } + + /** + * Adds the missed call notification on startup if there are unread missed calls. + */ + private void updateOnStartup() { + Log.d(this, "updateOnStartup()..."); + + // instantiate query handler + AsyncQueryHandler queryHandler = new AsyncQueryHandler(mContext.getContentResolver()) { + @Override + protected void onQueryComplete(int token, Object cookie, Cursor cursor) { + Log.d(MissedCallNotifierImpl.this, "onQueryComplete()..."); + if (cursor != null) { + try { + while (cursor.moveToNext()) { + // Get data about the missed call from the cursor + final String handleString = cursor.getString(CALL_LOG_COLUMN_NUMBER); + final int presentation = + cursor.getInt(CALL_LOG_COLUMN_NUMBER_PRESENTATION); + final long date = cursor.getLong(CALL_LOG_COLUMN_DATE); + + final Uri handle; + if (presentation != Calls.PRESENTATION_ALLOWED + || TextUtils.isEmpty(handleString)) { + handle = null; + } else { + handle = Uri.fromParts(PhoneNumberUtils.isUriNumber(handleString) ? + PhoneAccount.SCHEME_SIP : PhoneAccount.SCHEME_TEL, + handleString, null); + } + + // Convert the data to a call object + Call call = new Call(mContext, mCallsManager, + null, null, null, null, null, true, + false); + call.setDisconnectCause(new DisconnectCause(DisconnectCause.MISSED)); + call.setState(CallState.DISCONNECTED); + call.setCreationTimeMillis(date); + + // Listen for the update to the caller information before posting the + // notification so that we have the contact info and photo. + call.addListener(new Call.ListenerBase() { + @Override + public void onCallerInfoChanged(Call call) { + call.removeListener(this); // No longer need to listen to call + // changes after the contact info + // is retrieved. + showMissedCallNotification(call); + } + }); + // Set the handle here because that is what triggers the contact info + // query. + call.setHandle(handle, presentation); + } + } finally { + cursor.close(); + } + } + } + }; + + // setup query spec, look for all Missed calls that are new. + StringBuilder where = new StringBuilder("type="); + where.append(Calls.MISSED_TYPE); + where.append(" AND new=1"); + + // start the query + queryHandler.startQuery(0, null, Calls.CONTENT_URI, CALL_LOG_PROJECTION, + where.toString(), null, Calls.DEFAULT_SORT_ORDER); + } +} diff --git a/tests/src/com/android/server/telecom/tests/ComponentContextHolder.java b/tests/src/com/android/server/telecom/tests/ComponentContextHolder.java new file mode 100644 index 000000000..692da37c4 --- /dev/null +++ b/tests/src/com/android/server/telecom/tests/ComponentContextHolder.java @@ -0,0 +1,302 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.telecom.tests; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; + +import com.android.internal.telecom.IConnectionService; +import com.android.internal.telecom.IInCallService; + +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.IContentProvider; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.media.AudioManager; +import android.os.IInterface; +import android.os.UserHandle; +import android.telecom.ConnectionService; +import android.telecom.InCallService; +import android.telecom.PhoneAccount; +import android.telephony.TelephonyManager; +import android.test.mock.MockContext; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.when; + +/** + * Controls a test {@link Context} as would be provided by the Android framework to an + * {@code Activity}, {@code Service} or other system-instantiated component. + * + * The {@link Context} created by this object is "hollow" but its {@code applicationContext} + * property points to an application context implementing all the nontrivial functionality. + */ +public class ComponentContextHolder implements TestDoubleHolder { + + public class TestApplicationContext extends MockContext { + @Override + public PackageManager getPackageManager() { + return mPackageManager; + } + + @Override + public File getFilesDir() { + try { + return File.createTempFile("temp", "temp").getParentFile(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean bindServiceAsUser( + Intent serviceIntent, + ServiceConnection connection, + int flags, + UserHandle userHandle) { + // TODO: Implement "as user" functionality + return bindService(serviceIntent, connection, flags); + } + + @Override + public boolean bindService( + Intent serviceIntent, + ServiceConnection connection, + int flags) { + if (mServiceByServiceConnection.containsKey(connection)) { + throw new RuntimeException("ServiceConnection already bound: " + connection); + } + IInterface service = mServiceByComponentName.get(serviceIntent.getComponent()); + if (service == null) { + throw new RuntimeException("ServiceConnection not found: " + + serviceIntent.getComponent()); + } + mServiceByServiceConnection.put(connection, service); + connection.onServiceConnected(serviceIntent.getComponent(), service.asBinder()); + return true; + } + + @Override + public void unbindService( + ServiceConnection connection) { + IInterface service = mServiceByServiceConnection.remove(connection); + if (service == null) { + throw new RuntimeException("ServiceConnection not found: " + connection); + } + connection.onServiceDisconnected(mComponentNameByService.get(service)); + } + + @Override + public Object getSystemService(String name) { + switch (name) { + case Context.AUDIO_SERVICE: + return mAudioManager; + case Context.TELEPHONY_SERVICE: + return mTelephonyManager; + default: + return null; + } + } + + @Override + public Resources getResources() { + return mResources; + } + + @Override + public String getOpPackageName() { + return "test"; + } + + @Override + public ContentResolver getContentResolver() { + return new ContentResolver(this) { + @Override + protected IContentProvider acquireProvider(Context c, String name) { + return null; + } + + @Override + public boolean releaseProvider(IContentProvider icp) { + return false; + } + + @Override + protected IContentProvider acquireUnstableProvider(Context c, String name) { + return null; + } + + @Override + public boolean releaseUnstableProvider(IContentProvider icp) { + return false; + } + + @Override + public void unstableProviderDied(IContentProvider icp) { + + } + }; + } + + @Override + public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { + // TODO -- this is called by WiredHeadsetManager!!! + return null; + } + + @Override + public void sendBroadcast(Intent intent) { + // TODO -- need to ensure this is captured + } + + @Override + public void sendBroadcast(Intent intent, String receiverPermission) { + // TODO -- need to ensure this is captured + } + }; + + private final Multimap mComponentNamesByAction = + ArrayListMultimap.create(); + private final Map mServiceByComponentName = new HashMap<>(); + private final Map mServiceInfoByComponentName = new HashMap<>(); + private final Map mComponentNameByService = new HashMap<>(); + private final Map mServiceByServiceConnection = new HashMap<>(); + + private final Context mContext = new MockContext() { + @Override + public Context getApplicationContext() { + return mApplicationContextSpy; + } + }; + + // The application context is the most important object this class provides to the system + // under test. + private final Context mApplicationContext = new TestApplicationContext(); + + // We then create a spy on the application context allowing standard Mockito-style + // when(...) logic to be used to add specific little responses where needed. + + private final Context mApplicationContextSpy = Mockito.spy(mApplicationContext); + private final PackageManager mPackageManager = Mockito.mock(PackageManager.class); + private final AudioManager mAudioManager = Mockito.mock(AudioManager.class); + private final TelephonyManager mTelephonyManager = Mockito.mock(TelephonyManager.class); + private final Resources mResources = Mockito.mock(Resources.class); + private final Configuration mResourceConfiguration = new Configuration(); + + public ComponentContextHolder() { + MockitoAnnotations.initMocks(this); + when(mResources.getConfiguration()).thenReturn(mResourceConfiguration); + mResourceConfiguration.setLocale(Locale.TAIWAN); + + // TODO: Move into actual tests + when(mAudioManager.isWiredHeadsetOn()).thenReturn(false); + + doAnswer(new Answer>() { + @Override + public List answer(InvocationOnMock invocation) throws Throwable { + return doQueryIntentServices( + (Intent) invocation.getArguments()[0], + (Integer) invocation.getArguments()[1]); + } + }).when(mPackageManager).queryIntentServices((Intent) any(), anyInt()); + + doAnswer(new Answer>() { + @Override + public List answer(InvocationOnMock invocation) throws Throwable { + return doQueryIntentServices( + (Intent) invocation.getArguments()[0], + (Integer) invocation.getArguments()[1]); + } + }).when(mPackageManager).queryIntentServicesAsUser((Intent) any(), anyInt(), anyInt()); + + when(mTelephonyManager.getSubIdForPhoneAccount((PhoneAccount) any())).thenReturn(1); + } + + @Override + public Context getTestDouble() { + return mContext; + } + + public void addConnectionService( + ComponentName componentName, + IConnectionService service) + throws Exception { + addService(ConnectionService.SERVICE_INTERFACE, componentName, service); + ServiceInfo serviceInfo = new ServiceInfo(); + serviceInfo.permission = android.Manifest.permission.BIND_CONNECTION_SERVICE; + serviceInfo.packageName = componentName.getPackageName(); + serviceInfo.name = componentName.getClassName(); + mServiceInfoByComponentName.put(componentName, serviceInfo); + } + + public void addInCallService( + ComponentName componentName, + IInCallService service) + throws Exception { + addService(InCallService.SERVICE_INTERFACE, componentName, service); + ServiceInfo serviceInfo = new ServiceInfo(); + serviceInfo.permission = android.Manifest.permission.BIND_INCALL_SERVICE; + serviceInfo.packageName = componentName.getPackageName(); + serviceInfo.name = componentName.getClassName(); + mServiceInfoByComponentName.put(componentName, serviceInfo); + } + + public void putResource(int id, String value) { + when(mResources.getString(eq(id))).thenReturn(value); + } + + private void addService(String action, ComponentName name, IInterface service) { + mComponentNamesByAction.put(action, name); + mServiceByComponentName.put(name, service); + mComponentNameByService.put(service, name); + } + + private List doQueryIntentServices(Intent intent, int flags) { + List result = new ArrayList<>(); + for (ComponentName componentName : mComponentNamesByAction.get(intent.getAction())) { + ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.serviceInfo = mServiceInfoByComponentName.get(componentName); + result.add(resolveInfo); + } + return result; + } +} diff --git a/tests/src/com/android/server/telecom/tests/ConnectionServiceHolder.java b/tests/src/com/android/server/telecom/tests/ConnectionServiceHolder.java new file mode 100644 index 000000000..668d6c2d2 --- /dev/null +++ b/tests/src/com/android/server/telecom/tests/ConnectionServiceHolder.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.telecom.tests; + +import com.android.internal.telecom.IConnectionService; +import com.android.internal.telecom.IConnectionServiceAdapter; + +import android.os.IBinder; +import android.os.RemoteException; +import android.telecom.AudioState; +import android.telecom.ConnectionRequest; +import android.telecom.PhoneAccountHandle; + +/** + * Controls a test {@link IConnectionService} as would be provided by a source of connectivity + * to the Telecom framework. + */ +public class ConnectionServiceHolder implements TestDoubleHolder { + + private final IConnectionService mConnectionService = new IConnectionService.Stub() { + @Override + public void addConnectionServiceAdapter(IConnectionServiceAdapter adapter) + throws RemoteException { + } + + @Override + public void removeConnectionServiceAdapter(IConnectionServiceAdapter adapter) + throws RemoteException { + + } + + @Override + public void createConnection(PhoneAccountHandle connectionManagerPhoneAccount, + String callId, + ConnectionRequest request, boolean isIncoming, boolean isUnknown) + throws RemoteException { + + } + + @Override + public void abort(String callId) throws RemoteException { + + } + + @Override + public void answerVideo(String callId, int videoState) throws RemoteException { + + } + + @Override + public void answer(String callId) throws RemoteException { + + } + + @Override + public void reject(String callId) throws RemoteException { + + } + + @Override + public void disconnect(String callId) throws RemoteException { + + } + + @Override + public void hold(String callId) throws RemoteException { + + } + + @Override + public void unhold(String callId) throws RemoteException { + + } + + @Override + public void onAudioStateChanged(String activeCallId, AudioState audioState) + throws RemoteException { + + } + + @Override + public void playDtmfTone(String callId, char digit) throws RemoteException { + + } + + @Override + public void stopDtmfTone(String callId) throws RemoteException { + + } + + @Override + public void conference(String conferenceCallId, String callId) throws RemoteException { + + } + + @Override + public void splitFromConference(String callId) throws RemoteException { + + } + + @Override + public void mergeConference(String conferenceCallId) throws RemoteException { + + } + + @Override + public void swapConference(String conferenceCallId) throws RemoteException { + + } + + @Override + public void onPostDialContinue(String callId, boolean proceed) throws RemoteException { + + } + + @Override + public IBinder asBinder() { + return null; + } + }; + + @Override + public IConnectionService getTestDouble() { + return mConnectionService; + } + +} diff --git a/tests/src/com/android/server/telecom/tests/MockitoHelper.java b/tests/src/com/android/server/telecom/tests/MockitoHelper.java new file mode 100644 index 000000000..32b91f970 --- /dev/null +++ b/tests/src/com/android/server/telecom/tests/MockitoHelper.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.telecom.tests; + +import android.content.Context; +import android.util.Log; + +/** + * Helper for Mockito-based test cases. + */ +public final class MockitoHelper { + private static final String TAG = "MockitoHelper"; + private static final String DEXCACHE = "dexmaker.dexcache"; + + private ClassLoader mOriginalClassLoader; + private Thread mContextThread; + + /** + * Creates a new helper, which in turn will set the context classloader so + * it can load Mockito resources. + * + * @param packageClass test case class + */ + public void setUp(Context context, Class packageClass) throws Exception { + // makes a copy of the context classloader + mContextThread = Thread.currentThread(); + mOriginalClassLoader = mContextThread.getContextClassLoader(); + ClassLoader newClassLoader = packageClass.getClassLoader(); + Log.v(TAG, "Changing context classloader from " + mOriginalClassLoader + + " to " + newClassLoader); + mContextThread.setContextClassLoader(newClassLoader); + System.setProperty(DEXCACHE, context.getCacheDir().toString()); + } + + /** + * Restores the context classloader to the previous value. + */ + public void tearDown() throws Exception { + Log.v(TAG, "Restoring context classloader to " + mOriginalClassLoader); + mContextThread.setContextClassLoader(mOriginalClassLoader); + System.clearProperty(DEXCACHE); + } +} \ No newline at end of file diff --git a/tests/src/com/android/server/telecom/tests/SimpleTelecomTest.java b/tests/src/com/android/server/telecom/tests/SimpleTelecomTest.java new file mode 100644 index 000000000..8e19a558d --- /dev/null +++ b/tests/src/com/android/server/telecom/tests/SimpleTelecomTest.java @@ -0,0 +1,425 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.telecom.tests; + +import com.android.internal.telecom.IConnectionService; +import com.android.internal.telecom.IConnectionServiceAdapter; +import com.android.internal.telecom.IInCallAdapter; +import com.android.internal.telecom.IInCallService; +import com.android.internal.telecom.IVideoProvider; +import com.android.server.telecom.CallsManager; +import com.android.server.telecom.HeadsetMediaButton; +import com.android.server.telecom.HeadsetMediaButtonFactory; +import com.android.server.telecom.InCallWakeLockControllerFactory; +import com.android.server.telecom.MissedCallNotifier; +import com.android.server.telecom.ProximitySensorManagerFactory; +import com.android.server.telecom.InCallWakeLockController; +import com.android.server.telecom.ProximitySensorManager; +import com.android.server.telecom.TelecomSystem; + +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.RemoteException; +import android.os.UserHandle; +import android.telecom.CallState; +import android.telecom.Connection; +import android.telecom.ConnectionRequest; +import android.telecom.DisconnectCause; +import android.telecom.ParcelableCall; +import android.telecom.ParcelableConnection; +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import android.telecom.StatusHints; +import android.telecom.TelecomManager; +import android.test.AndroidTestCase; +import android.util.Log; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Collections; +import java.util.List; + +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class SimpleTelecomTest extends AndroidTestCase { + + private static final String TAG = "Telecom-TEST"; + + /////////////////////////////////////////////////////////////////////////// + // Telecom specific mock objects + + @Mock + MissedCallNotifier mMissedCallNotifier; + @Mock + HeadsetMediaButtonFactory mHeadsetMediaButtonFactory; + @Mock + ProximitySensorManagerFactory mProximitySensorManagerFactory; + @Mock + InCallWakeLockControllerFactory mInCallWakeLockControllerFactory; + @Mock HeadsetMediaButton mHeadsetMediaButton; + @Mock ProximitySensorManager mProximitySensorManager; + @Mock InCallWakeLockController mInCallWakeLockController; + + /////////////////////////////////////////////////////////////////////////// + // Connection service + + PhoneAccount mTestPhoneAccount = PhoneAccount.builder( + new PhoneAccountHandle( + new ComponentName("connection-service-package", "connection-service-class"), + "test-account-id"), + "test phone account") + .addSupportedUriScheme("tel") + .setCapabilities( + PhoneAccount.CAPABILITY_CALL_PROVIDER | + PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION) + .build(); + @Mock IConnectionService.Stub mConnectionService; + IConnectionServiceAdapter mConnectionServiceAdapter; + + /////////////////////////////////////////////////////////////////////////// + // In-Call service + + ComponentName mIncallComponentName = new ComponentName("incall-package", "incall-class"); + @Mock IInCallService.Stub mInCallService; + IInCallAdapter mIInCallAdapter; + + private ComponentContextHolder mContextHolder; + private TelecomSystem mSystem; + + private ConnectionRequest mConnectionRequest; + private String mConnectionId; + + private ParcelableCall mParcelableCall; + + private Looper mMainLooper; + private Looper mTestLooper; + + /////////////////////////////////////////////////////////////////////////// + // Captured values for outgoing call processing + + Intent mNewOutgoingCallIntent; + BroadcastReceiver mNewOutgoingCallReceiver; + + private MockitoHelper mMockitoHelper = new MockitoHelper(); + + @Override + public void setUp() throws Exception { + mMockitoHelper.setUp(getContext(), getClass()); + + mMainLooper = Looper.getMainLooper(); + mTestLooper = Looper.myLooper(); + + mContextHolder = new ComponentContextHolder(); + MockitoAnnotations.initMocks(this); + + mContextHolder.putResource( + com.android.server.telecom.R.string.ui_default_package, + mIncallComponentName.getPackageName()); + mContextHolder.putResource( + com.android.server.telecom.R.string.incall_default_class, + mIncallComponentName.getClassName()); + + com.android.server.telecom.Log.setTag(TAG); + + when(mHeadsetMediaButtonFactory.create( + any(Context.class), + any(CallsManager.class))) + .thenReturn(mHeadsetMediaButton); + + when(mInCallWakeLockControllerFactory.create( + any(Context.class), + any(CallsManager.class))) + .thenReturn(mInCallWakeLockController); + + when(mProximitySensorManagerFactory.create((Context) any(), (CallsManager) any())) + .thenReturn(mProximitySensorManager); + + // Set up connection service + + mContextHolder.addConnectionService( + mTestPhoneAccount.getAccountHandle().getComponentName(), + mConnectionService); + when(mConnectionService.asBinder()).thenReturn(mConnectionService); + when(mConnectionService.queryLocalInterface(anyString())) + .thenReturn(mConnectionService); + + // Set up in-call service + + mContextHolder.addInCallService( + mIncallComponentName, + mInCallService); + when(mInCallService.asBinder()).thenReturn(mInCallService); + when(mInCallService.queryLocalInterface(anyString())) + .thenReturn(mInCallService); + + runOnMainThreadAndWait(new Runnable() { + @Override + public void run() { + mSystem = new TelecomSystem( + mContextHolder.getTestDouble(), + mMissedCallNotifier, + mHeadsetMediaButtonFactory, + mProximitySensorManagerFactory, + mInCallWakeLockControllerFactory); + mSystem.getPhoneAccountRegistrar().registerPhoneAccount(mTestPhoneAccount); + } + }); + } + + @Override + public void tearDown() throws Exception { + mMockitoHelper.tearDown(); + mSystem = null; + } + + public void testSimpleOutgoingCall() throws Exception { + + // Arrange to receive the first set of notifications when Telecom receives an Intent + // to make an outgoing call + doAnswer(new Answer() { + @Override + public Object answer(final InvocationOnMock invocation) { + mIInCallAdapter = (IInCallAdapter) invocation.getArguments()[0]; + return null; + } + }).when(mInCallService).setInCallAdapter((IInCallAdapter) any()); + verify(mInCallService, never()).addCall((ParcelableCall) any()); + + doAnswer(new Answer() { + @Override + public Object answer(final InvocationOnMock invocation) { + mNewOutgoingCallIntent = (Intent) invocation.getArguments()[0]; + mNewOutgoingCallReceiver = (BroadcastReceiver) invocation.getArguments()[3]; + return null; + } + }).when(mContextHolder.getTestDouble().getApplicationContext()) + .sendOrderedBroadcastAsUser( + any(Intent.class), + any(UserHandle.class), + anyString(), + any(BroadcastReceiver.class), + any(Handler.class), + anyInt(), + anyString(), + any(Bundle.class)); + + doAnswer(new Answer() { + @Override + public Object answer(final InvocationOnMock invocation) { + mParcelableCall = (ParcelableCall) invocation.getArguments()[0]; + return null; + } + }).when(mInCallService).addCall((ParcelableCall) any()); + + doAnswer(new Answer() { + @Override + public Object answer(final InvocationOnMock invocation) { + mParcelableCall = (ParcelableCall) invocation.getArguments()[0]; + return null; + } + }).when(mInCallService).updateCall((ParcelableCall) any()); + + runOnMainThreadAndWait(new Runnable() { + @Override + public void run() { + // Start an outgoing phone call + String number = "650-555-1212"; + Intent actionCallIntent = new Intent(); + actionCallIntent.setData(Uri.parse("tel:" + number)); + actionCallIntent.putExtra(Intent.EXTRA_PHONE_NUMBER, number); + actionCallIntent.putExtra( + TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, + mTestPhoneAccount.getAccountHandle()); + actionCallIntent.setAction(Intent.ACTION_CALL); + mSystem.getCallIntentProcessor().processIntent(actionCallIntent); + } + }); + + // Sanity check that the in-call adapter is now set + assertNotNull(mIInCallAdapter); + assertNotNull(mNewOutgoingCallIntent); + assertNotNull(mNewOutgoingCallReceiver); + + // Arrange to receive the Connection Service adapter + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) { + mConnectionServiceAdapter = (IConnectionServiceAdapter) invocation + .getArguments()[0]; + return null; + } + }).when(mConnectionService).addConnectionServiceAdapter((IConnectionServiceAdapter) any()); + + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) { + mConnectionId = (String) invocation.getArguments()[1]; + mConnectionRequest = (ConnectionRequest) invocation.getArguments()[2]; + return null; + } + }).when(mConnectionService).createConnection( + any(PhoneAccountHandle.class), + anyString(), + any(ConnectionRequest.class), + anyBoolean(), + anyBoolean()); + + // Pass on the new outgoing call Intent + runOnMainThreadAndWait(new Runnable() { + @Override + public void run() { + // Set a dummy PendingResult so the BroadcastReceiver agrees to accept onReceive() + mNewOutgoingCallReceiver.setPendingResult( + new BroadcastReceiver.PendingResult(0, "", null, 0, true, false, null, 0)); + mNewOutgoingCallReceiver.setResultData( + mNewOutgoingCallIntent.getStringExtra(Intent.EXTRA_PHONE_NUMBER)); + mNewOutgoingCallReceiver.onReceive( + mContextHolder.getTestDouble(), + mNewOutgoingCallIntent); + } + }); + + runOnMainThreadAndWait(new Runnable() { + @Override + public void run() { + assertNotNull(mConnectionServiceAdapter); + assertNotNull(mConnectionRequest); + assertNotNull(mConnectionId); + } + }); + + mConnectionServiceAdapter.handleCreateConnectionComplete( + mConnectionId, + mConnectionRequest, + new ParcelableConnection( + mConnectionRequest.getAccountHandle(), + Connection.STATE_DIALING, + 0, + (Uri) null, + 0, + "caller display name", + 0, + (IVideoProvider) null, + 0, + false, + false, + (StatusHints) null, + (DisconnectCause) null, + (List) Collections.EMPTY_LIST)); + mConnectionServiceAdapter.setDialing(mConnectionId); + mConnectionServiceAdapter.setActive(mConnectionId); + + runOnMainThreadAndWait(new Runnable() { + @Override + public void run() { + assertNotNull(mParcelableCall); + assertEquals(CallState.ACTIVE, mParcelableCall.getState()); + } + }); + + runOnMainThreadAndWait(new Runnable() { + @Override + public void run() { + try { + mIInCallAdapter.disconnectCall(mParcelableCall.getId()); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + }); + + runOnMainThreadAndWait(new Runnable() { + @Override + public void run() { + assertNotNull(mParcelableCall); + assertEquals(CallState.ACTIVE, mParcelableCall.getState()); + try { + verify(mConnectionService).disconnect(mConnectionId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + }); + + mConnectionServiceAdapter.setDisconnected( + mConnectionId, + new DisconnectCause(DisconnectCause.LOCAL)); + + runOnMainThreadAndWait(new Runnable() { + @Override + public void run() { + assertEquals(CallState.DISCONNECTED, mParcelableCall.getState()); + } + }); + } + + private void runOnMainThreadAndWait(Runnable task) { + runOn(mMainLooper, task); + } + + private void runOnTestThreadAndWait(Runnable task) { + runOn(mTestLooper, task); + } + + private void runOn(Looper looper, final Runnable task) { + final Object lock = new Object(); + synchronized (lock) { + new Handler(looper).post(new Runnable() { + @Override + public void run() { + task.run(); + synchronized (lock) { + lock.notifyAll(); + } + } + }); + try { + lock.wait(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + private String exceptionToString(Throwable t) { + StringWriter sw = new StringWriter(); + t.printStackTrace(new PrintWriter(sw)); + return sw.toString(); + } + + private void log(String msg) { + Log.i(TAG, getClass().getSimpleName() + " - " + msg); + } +} diff --git a/tests/src/com/android/server/telecom/tests/MockConnectionService.java b/tests/src/com/android/server/telecom/tests/TestDoubleHolder.java similarity index 63% rename from tests/src/com/android/server/telecom/tests/MockConnectionService.java rename to tests/src/com/android/server/telecom/tests/TestDoubleHolder.java index 62448cda8..7c34f84e1 100644 --- a/tests/src/com/android/server/telecom/tests/MockConnectionService.java +++ b/tests/src/com/android/server/telecom/tests/TestDoubleHolder.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,17 @@ package com.android.server.telecom.tests; -import android.telecom.ConnectionService; - /** - * A non-functional {@link android.telecom.ConnectionService} to use for unit tests. + * An object that provides a control interface for configuring a test double. + * + * TODO: Come up with a better name for this. */ -public class MockConnectionService extends ConnectionService { +public interface TestDoubleHolder { + /** + * Obtain the actual test double provided by this holder. + * + * @return the test double. + */ + T getTestDouble(); } diff --git a/tests/src/com/android/server/telecom/tests/unit/PhoneAccountRegistrarTest.java b/tests/src/com/android/server/telecom/tests/unit/PhoneAccountRegistrarTest.java index e63e79f11..022456636 100644 --- a/tests/src/com/android/server/telecom/tests/unit/PhoneAccountRegistrarTest.java +++ b/tests/src/com/android/server/telecom/tests/unit/PhoneAccountRegistrarTest.java @@ -357,4 +357,4 @@ private PhoneAccountRegistrar.State makeQuickState() { s.simCallManager = new PhoneAccountHandle(new ComponentName("pkg0", "cls0"), "id1"); return s; } -} \ No newline at end of file +}