Renaming Telecomm to Telecom.
- Changing package from android.telecomm to android.telecom
- Changing package from com.android.telecomm to
com.android.server.telecomm.
- Renaming TelecommManager to TelecomManager.
Bug: 17364651
Change-Id: Ib7b20ba6348948afb391450b4eef8919261f3272
diff --git a/src/com/android/server/telecom/AsyncResultCallback.java b/src/com/android/server/telecom/AsyncResultCallback.java
new file mode 100644
index 0000000..edbda3b
--- /dev/null
+++ b/src/com/android/server/telecom/AsyncResultCallback.java
@@ -0,0 +1,24 @@
+/*
+ * 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;
+
+/**
+ * Generic result interface for use with async method callback.
+ */
+interface AsyncResultCallback<T> {
+ void onResult(T result, int errorCode, String errorMsg);
+}
diff --git a/src/com/android/server/telecom/AsyncRingtonePlayer.java b/src/com/android/server/telecom/AsyncRingtonePlayer.java
new file mode 100644
index 0000000..ad57b00
--- /dev/null
+++ b/src/com/android/server/telecom/AsyncRingtonePlayer.java
@@ -0,0 +1,161 @@
+/*
+ * 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;
+
+import android.media.AudioManager;
+import android.media.Ringtone;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.provider.Settings;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * Plays the default ringtone. Uses {@link Ringtone} in a separate thread so that this class can be
+ * used from the main thread.
+ */
+class AsyncRingtonePlayer {
+ // Message codes used with the ringtone thread.
+ static final int EVENT_PLAY = 1;
+ static final int EVENT_STOP = 2;
+
+ /** Handler running on the ringtone thread. */
+ private Handler mHandler;
+
+ /** The current ringtone. Only used by the ringtone thread. */
+ private Ringtone mRingtone;
+
+ /** Plays the ringtone. */
+ void play(Uri ringtone) {
+ Log.d(this, "Posting play.");
+ postMessage(EVENT_PLAY, true /* shouldCreateHandler */, ringtone);
+ }
+
+ /** Stops playing the ringtone. */
+ void stop() {
+ Log.d(this, "Posting stop.");
+ postMessage(EVENT_STOP, false /* shouldCreateHandler */, null);
+ }
+
+ /**
+ * Posts a message to the ringtone-thread handler. Creates the handler if specified by the
+ * parameter shouldCreateHandler.
+ *
+ * @param messageCode The message to post.
+ * @param shouldCreateHandler True when a handler should be created to handle this message.
+ */
+ private void postMessage(int messageCode, boolean shouldCreateHandler, Uri ringtone) {
+ synchronized(this) {
+ if (mHandler == null && shouldCreateHandler) {
+ mHandler = getNewHandler();
+ }
+
+ if (mHandler == null) {
+ Log.d(this, "Message %d skipped because there is no handler.", messageCode);
+ } else {
+ mHandler.obtainMessage(messageCode, ringtone).sendToTarget();
+ }
+ }
+ }
+
+ /**
+ * Creates a new ringtone Handler running in its own thread.
+ */
+ private Handler getNewHandler() {
+ Preconditions.checkState(mHandler == null);
+
+ HandlerThread thread = new HandlerThread("ringtone-player");
+ thread.start();
+
+ return new Handler(thread.getLooper()) {
+ @Override
+ public void handleMessage(Message msg) {
+ switch(msg.what) {
+ case EVENT_PLAY:
+ handlePlay((Uri) msg.obj);
+ break;
+ case EVENT_STOP:
+ handleStop();
+ break;
+ }
+ }
+ };
+ }
+
+ /**
+ * Starts the actual playback of the ringtone. Executes on ringtone-thread.
+ */
+ private void handlePlay(Uri ringtoneUri) {
+ ThreadUtil.checkNotOnMainThread();
+ Log.i(this, "Play ringtone.");
+
+ if (mRingtone == null) {
+ mRingtone = getRingtone(ringtoneUri);
+
+ // Cancel everything if there is no ringtone.
+ if (mRingtone == null) {
+ handleStop();
+ return;
+ }
+ }
+
+ if (mRingtone.isPlaying()) {
+ Log.d(this, "Ringtone already playing.");
+ } else {
+ mRingtone.play();
+ Log.d(this, "Ringtone.play() invoked.");
+ }
+ }
+
+ /**
+ * Stops the playback of the ringtone. Executes on the ringtone-thread.
+ */
+ private void handleStop() {
+ ThreadUtil.checkNotOnMainThread();
+ Log.i(this, "Stop ringtone.");
+
+ if (mRingtone != null) {
+ Log.d(this, "Ringtone.stop() invoked.");
+ mRingtone.stop();
+ mRingtone = null;
+ }
+
+ synchronized(this) {
+ if (mHandler.hasMessages(EVENT_PLAY)) {
+ Log.v(this, "Keeping alive ringtone thread for pending messages.");
+ } else {
+ mHandler.removeMessages(EVENT_STOP);
+ mHandler.getLooper().quitSafely();
+ mHandler = null;
+ Log.v(this, "Handler cleared.");
+ }
+ }
+ }
+
+ private Ringtone getRingtone(Uri ringtoneUri) {
+ if (ringtoneUri == null) {
+ ringtoneUri = Settings.System.DEFAULT_RINGTONE_URI;
+ }
+
+ Ringtone ringtone = RingtoneManager.getRingtone(TelecomApp.getInstance(), ringtoneUri);
+ ringtone.setStreamType(AudioManager.STREAM_RING);
+ return ringtone;
+ }
+}
diff --git a/src/com/android/server/telecom/BluetoothManager.java b/src/com/android/server/telecom/BluetoothManager.java
new file mode 100644
index 0000000..9b5fd26
--- /dev/null
+++ b/src/com/android/server/telecom/BluetoothManager.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadset;
+import android.bluetooth.BluetoothProfile;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.SystemClock;
+
+import java.util.List;
+
+/**
+ * Listens to and caches bluetooth headset state. Used By the CallAudioManager for maintaining
+ * overall audio state. Also provides method for connecting the bluetooth headset to the phone call.
+ */
+public class BluetoothManager {
+
+ private final BluetoothProfile.ServiceListener mBluetoothProfileServiceListener =
+ new BluetoothProfile.ServiceListener() {
+ @Override
+ public void onServiceConnected(int profile, BluetoothProfile proxy) {
+ mBluetoothHeadset = (BluetoothHeadset) proxy;
+ Log.v(this, "- Got BluetoothHeadset: " + mBluetoothHeadset);
+ }
+
+ @Override
+ public void onServiceDisconnected(int profile) {
+ mBluetoothHeadset = null;
+ }
+ };
+
+ /**
+ * Receiver for misc intent broadcasts the BluetoothManager cares about.
+ */
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+
+ if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) {
+ int bluetoothHeadsetState = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
+ BluetoothHeadset.STATE_DISCONNECTED);
+ Log.d(this, "mReceiver: HEADSET_STATE_CHANGED_ACTION");
+ Log.d(this, "==> new state: %s ", bluetoothHeadsetState);
+ updateBluetoothState();
+ } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
+ int bluetoothHeadsetAudioState =
+ intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
+ BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
+ Log.d(this, "mReceiver: HEADSET_AUDIO_STATE_CHANGED_ACTION");
+ Log.d(this, "==> new state: %s", bluetoothHeadsetAudioState);
+ updateBluetoothState();
+ }
+ }
+ };
+
+ private final BluetoothAdapter mBluetoothAdapter;
+ private final CallAudioManager mCallAudioManager;
+
+ private BluetoothHeadset mBluetoothHeadset;
+ private boolean mBluetoothConnectionPending = false;
+ private long mBluetoothConnectionRequestTime;
+
+
+ public BluetoothManager(Context context, CallAudioManager callAudioManager) {
+ mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+ mCallAudioManager = callAudioManager;
+
+ if (mBluetoothAdapter != null) {
+ mBluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener,
+ BluetoothProfile.HEADSET);
+ }
+
+ // Register for misc other intent broadcasts.
+ IntentFilter intentFilter =
+ new IntentFilter(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
+ intentFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
+ context.registerReceiver(mReceiver, intentFilter);
+ }
+
+ //
+ // Bluetooth helper methods.
+ //
+ // - BluetoothAdapter is the Bluetooth system service. If
+ // getDefaultAdapter() returns null
+ // then the device is not BT capable. Use BluetoothDevice.isEnabled()
+ // to see if BT is enabled on the device.
+ //
+ // - BluetoothHeadset is the API for the control connection to a
+ // Bluetooth Headset. This lets you completely connect/disconnect a
+ // headset (which we don't do from the Phone UI!) but also lets you
+ // get the address of the currently active headset and see whether
+ // it's currently connected.
+
+ /**
+ * @return true if the Bluetooth on/off switch in the UI should be
+ * available to the user (i.e. if the device is BT-capable
+ * and a headset is connected.)
+ */
+ boolean isBluetoothAvailable() {
+ Log.v(this, "isBluetoothAvailable()...");
+
+ // There's no need to ask the Bluetooth system service if BT is enabled:
+ //
+ // BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+ // if ((adapter == null) || !adapter.isEnabled()) {
+ // Log.d(this, " ==> FALSE (BT not enabled)");
+ // return false;
+ // }
+ // Log.d(this, " - BT enabled! device name " + adapter.getName()
+ // + ", address " + adapter.getAddress());
+ //
+ // ...since we already have a BluetoothHeadset instance. We can just
+ // call isConnected() on that, and assume it'll be false if BT isn't
+ // enabled at all.
+
+ // Check if there's a connected headset, using the BluetoothHeadset API.
+ boolean isConnected = false;
+ if (mBluetoothHeadset != null) {
+ List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
+
+ if (deviceList.size() > 0) {
+ isConnected = true;
+ for (int i = 0; i < deviceList.size(); i++) {
+ BluetoothDevice device = deviceList.get(i);
+ Log.v(this, "state = " + mBluetoothHeadset.getConnectionState(device)
+ + "for headset: " + device);
+ }
+ }
+ }
+
+ Log.v(this, " ==> " + isConnected);
+ return isConnected;
+ }
+
+ /**
+ * @return true if a BT Headset is available, and its audio is currently connected.
+ */
+ boolean isBluetoothAudioConnected() {
+ if (mBluetoothHeadset == null) {
+ Log.v(this, "isBluetoothAudioConnected: ==> FALSE (null mBluetoothHeadset)");
+ return false;
+ }
+ List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
+
+ if (deviceList.isEmpty()) {
+ return false;
+ }
+ for (int i = 0; i < deviceList.size(); i++) {
+ BluetoothDevice device = deviceList.get(i);
+ boolean isAudioOn = mBluetoothHeadset.isAudioConnected(device);
+ Log.v(this, "isBluetoothAudioConnected: ==> isAudioOn = " + isAudioOn
+ + "for headset: " + device);
+ if (isAudioOn) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Helper method used to control the onscreen "Bluetooth" indication;
+ *
+ * @return true if a BT device is available and its audio is currently connected,
+ * <b>or</b> if we issued a BluetoothHeadset.connectAudio()
+ * call within the last 5 seconds (which presumably means
+ * that the BT audio connection is currently being set
+ * up, and will be connected soon.)
+ */
+ /* package */ boolean isBluetoothAudioConnectedOrPending() {
+ if (isBluetoothAudioConnected()) {
+ Log.v(this, "isBluetoothAudioConnectedOrPending: ==> TRUE (really connected)");
+ return true;
+ }
+
+ // If we issued a connectAudio() call "recently enough", even
+ // if BT isn't actually connected yet, let's still pretend BT is
+ // on. This makes the onscreen indication more responsive.
+ if (mBluetoothConnectionPending) {
+ long timeSinceRequest =
+ SystemClock.elapsedRealtime() - mBluetoothConnectionRequestTime;
+ if (timeSinceRequest < 5000 /* 5 seconds */) {
+ Log.v(this, "isBluetoothAudioConnectedOrPending: ==> TRUE (requested "
+ + timeSinceRequest + " msec ago)");
+ return true;
+ } else {
+ Log.v(this, "isBluetoothAudioConnectedOrPending: ==> FALSE (request too old: "
+ + timeSinceRequest + " msec ago)");
+ mBluetoothConnectionPending = false;
+ return false;
+ }
+ }
+
+ Log.v(this, "isBluetoothAudioConnectedOrPending: ==> FALSE");
+ return false;
+ }
+
+ /**
+ * Notified audio manager of a change to the bluetooth state.
+ */
+ void updateBluetoothState() {
+ mCallAudioManager.onBluetoothStateChange(this);
+ }
+
+ void connectBluetoothAudio() {
+ Log.v(this, "connectBluetoothAudio()...");
+ if (mBluetoothHeadset != null) {
+ mBluetoothHeadset.connectAudio();
+ }
+
+ // Watch out: The bluetooth connection doesn't happen instantly;
+ // the connectAudio() call returns instantly but does its real
+ // work in another thread. The mBluetoothConnectionPending flag
+ // is just a little trickery to ensure that the onscreen UI updates
+ // instantly. (See isBluetoothAudioConnectedOrPending() above.)
+ mBluetoothConnectionPending = true;
+ mBluetoothConnectionRequestTime = SystemClock.elapsedRealtime();
+ }
+
+ void disconnectBluetoothAudio() {
+ Log.v(this, "disconnectBluetoothAudio()...");
+ if (mBluetoothHeadset != null) {
+ mBluetoothHeadset.disconnectAudio();
+ }
+ mBluetoothConnectionPending = false;
+ }
+
+ private void dumpBluetoothState() {
+ Log.d(this, "============== dumpBluetoothState() =============");
+ Log.d(this, "= isBluetoothAvailable: " + isBluetoothAvailable());
+ Log.d(this, "= isBluetoothAudioConnected: " + isBluetoothAudioConnected());
+ Log.d(this, "= isBluetoothAudioConnectedOrPending: " +
+ isBluetoothAudioConnectedOrPending());
+ Log.d(this, "=");
+ if (mBluetoothAdapter != null) {
+ if (mBluetoothHeadset != null) {
+ List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
+
+ if (deviceList.size() > 0) {
+ BluetoothDevice device = deviceList.get(0);
+ Log.d(this, "= BluetoothHeadset.getCurrentDevice: " + device);
+ Log.d(this, "= BluetoothHeadset.State: "
+ + mBluetoothHeadset.getConnectionState(device));
+ Log.d(this, "= BluetoothHeadset audio connected: " +
+ mBluetoothHeadset.isAudioConnected(device));
+ }
+ } else {
+ Log.d(this, "= mBluetoothHeadset is null");
+ }
+ } else {
+ Log.d(this, "= mBluetoothAdapter is null; device is not BT capable");
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
new file mode 100644
index 0000000..6f7177b
--- /dev/null
+++ b/src/com/android/server/telecom/Call.java
@@ -0,0 +1,1232 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.provider.ContactsContract.Contacts;
+import android.telecom.CallState;
+import android.telecom.Connection;
+import android.telecom.GatewayInfo;
+import android.telecom.ParcelableConnection;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.PhoneCapabilities;
+import android.telecom.Response;
+import android.telecom.StatusHints;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
+import android.telephony.DisconnectCause;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+
+import com.android.internal.telecom.IVideoProvider;
+import com.android.internal.telephony.CallerInfo;
+import com.android.internal.telephony.CallerInfoAsyncQuery;
+import com.android.internal.telephony.CallerInfoAsyncQuery.OnQueryCompleteListener;
+import com.android.internal.telephony.SmsApplication;
+import com.android.server.telecom.ContactsAsyncHelper.OnImageLoadCompleteListener;
+
+import com.google.common.base.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Encapsulates all aspects of a given phone call throughout its lifecycle, starting
+ * from the time the call intent was received by Telecom (vs. the time the call was
+ * connected etc).
+ */
+final class Call implements CreateConnectionResponse {
+ /**
+ * Listener for events on the call.
+ */
+ interface Listener {
+ void onSuccessfulOutgoingCall(Call call, int callState);
+ void onFailedOutgoingCall(Call call, int errorCode, String errorMsg);
+ void onSuccessfulIncomingCall(Call call);
+ void onFailedIncomingCall(Call call);
+ void onRingbackRequested(Call call, boolean ringbackRequested);
+ void onPostDialWait(Call call, String remaining);
+ void onCallCapabilitiesChanged(Call call);
+ void onParentChanged(Call call);
+ void onChildrenChanged(Call call);
+ void onCannedSmsResponsesLoaded(Call call);
+ void onVideoCallProviderChanged(Call call);
+ void onCallerInfoChanged(Call call);
+ void onIsVoipAudioModeChanged(Call call);
+ void onStatusHintsChanged(Call call);
+ void onHandleChanged(Call call);
+ void onCallerDisplayNameChanged(Call call);
+ void onVideoStateChanged(Call call);
+ void onTargetPhoneAccountChanged(Call call);
+ void onConnectionManagerPhoneAccountChanged(Call call);
+ void onPhoneAccountChanged(Call call);
+ void onConferenceableCallsChanged(Call call);
+ }
+
+ abstract static class ListenerBase implements Listener {
+ @Override
+ public void onSuccessfulOutgoingCall(Call call, int callState) {}
+ @Override
+ public void onFailedOutgoingCall(Call call, int errorCode, String errorMsg) {}
+ @Override
+ public void onSuccessfulIncomingCall(Call call) {}
+ @Override
+ public void onFailedIncomingCall(Call call) {}
+ @Override
+ public void onRingbackRequested(Call call, boolean ringbackRequested) {}
+ @Override
+ public void onPostDialWait(Call call, String remaining) {}
+ @Override
+ public void onCallCapabilitiesChanged(Call call) {}
+ @Override
+ public void onParentChanged(Call call) {}
+ @Override
+ public void onChildrenChanged(Call call) {}
+ @Override
+ public void onCannedSmsResponsesLoaded(Call call) {}
+ @Override
+ public void onVideoCallProviderChanged(Call call) {}
+ @Override
+ public void onCallerInfoChanged(Call call) {}
+ @Override
+ public void onIsVoipAudioModeChanged(Call call) {}
+ @Override
+ public void onStatusHintsChanged(Call call) {}
+ @Override
+ public void onHandleChanged(Call call) {}
+ @Override
+ public void onCallerDisplayNameChanged(Call call) {}
+ @Override
+ public void onVideoStateChanged(Call call) {}
+ @Override
+ public void onTargetPhoneAccountChanged(Call call) {}
+ @Override
+ public void onConnectionManagerPhoneAccountChanged(Call call) {}
+ @Override
+ public void onPhoneAccountChanged(Call call) {}
+ @Override
+ public void onConferenceableCallsChanged(Call call) {}
+ }
+
+ private static final OnQueryCompleteListener sCallerInfoQueryListener =
+ new OnQueryCompleteListener() {
+ /** ${inheritDoc} */
+ @Override
+ public void onQueryComplete(int token, Object cookie, CallerInfo callerInfo) {
+ if (cookie != null) {
+ ((Call) cookie).setCallerInfo(callerInfo, token);
+ }
+ }
+ };
+
+ private static final OnImageLoadCompleteListener sPhotoLoadListener =
+ new OnImageLoadCompleteListener() {
+ /** ${inheritDoc} */
+ @Override
+ public void onImageLoadComplete(
+ int token, Drawable photo, Bitmap photoIcon, Object cookie) {
+ if (cookie != null) {
+ ((Call) cookie).setPhoto(photo, photoIcon, token);
+ }
+ }
+ };
+
+ private final Runnable mDirectToVoicemailRunnable = new Runnable() {
+ @Override
+ public void run() {
+ processDirectToVoicemail();
+ }
+ };
+
+ /** True if this is an incoming call. */
+ private final boolean mIsIncoming;
+
+ /**
+ * The time this call was created. Beyond logging and such, may also be used for bookkeeping
+ * and specifically for marking certain call attempts as failed attempts.
+ */
+ private final long mCreationTimeMillis = System.currentTimeMillis();
+
+ /** The gateway information associated with this call. This stores the original call handle
+ * that the user is attempting to connect to via the gateway, the actual handle to dial in
+ * order to connect the call via the gateway, as well as the package name of the gateway
+ * service. */
+ private GatewayInfo mGatewayInfo;
+
+ private PhoneAccountHandle mConnectionManagerPhoneAccountHandle;
+
+ private PhoneAccountHandle mTargetPhoneAccountHandle;
+
+ private final Handler mHandler = new Handler();
+
+ private final List<Call> mConferenceableCalls = new ArrayList<>();
+
+ private PhoneAccountHandle mPhoneAccountHandle;
+
+ private long mConnectTimeMillis = 0;
+
+ /** The state of the call. */
+ private int mState;
+
+ /** The handle with which to establish this call. */
+ private Uri mHandle;
+
+ /**
+ * The presentation requirements for the handle. See {@link TelecomManager} for valid values.
+ */
+ private int mHandlePresentation;
+
+ /** The caller display name (CNAP) set by the connection service. */
+ private String mCallerDisplayName;
+
+ /**
+ * The presentation requirements for the handle. See {@link TelecomManager} for valid values.
+ */
+ private int mCallerDisplayNamePresentation;
+
+ /**
+ * The connection service which is attempted or already connecting this call.
+ */
+ private ConnectionServiceWrapper mConnectionService;
+
+ private boolean mIsEmergencyCall;
+
+ private boolean mSpeakerphoneOn;
+
+ /**
+ * Tracks the video states which were applicable over the duration of a call.
+ * See {@link VideoProfile} for a list of valid video states.
+ */
+ private int mVideoStateHistory;
+
+ private int mVideoState;
+
+ /**
+ * Disconnect cause for the call. Only valid if the state of the call is STATE_DISCONNECTED.
+ * See {@link android.telephony.DisconnectCause}.
+ */
+ private int mDisconnectCause = DisconnectCause.NOT_VALID;
+
+ /**
+ * Additional disconnect information provided by the connection service.
+ */
+ private String mDisconnectMessage;
+
+ /** Info used by the connection services. */
+ private Bundle mExtras = Bundle.EMPTY;
+
+ /** Set of listeners on this call.
+ *
+ * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
+ * load factor before resizing, 1 means we only expect a single thread to
+ * access the map so make only a single shard
+ */
+ private final Set<Listener> mListeners = Collections.newSetFromMap(
+ new ConcurrentHashMap<Listener, Boolean>(8, 0.9f, 1));
+
+ private CreateConnectionProcessor mCreateConnectionProcessor;
+
+ /** Caller information retrieved from the latest contact query. */
+ private CallerInfo mCallerInfo;
+
+ /** The latest token used with a contact info query. */
+ private int mQueryToken = 0;
+
+ /** Whether this call is requesting that Telecom play the ringback tone on its behalf. */
+ private boolean mRingbackRequested = false;
+
+ /** Whether direct-to-voicemail query is pending. */
+ private boolean mDirectToVoicemailQueryPending;
+
+ private int mCallCapabilities;
+
+ private boolean mIsConference = false;
+
+ private Call mParentCall = null;
+
+ private List<Call> mChildCalls = new LinkedList<>();
+
+ /** Set of text message responses allowed for this call, if applicable. */
+ private List<String> mCannedSmsResponses = Collections.EMPTY_LIST;
+
+ /** Whether an attempt has been made to load the text message responses. */
+ private boolean mCannedSmsResponsesLoadingStarted = false;
+
+ private IVideoProvider mVideoProvider;
+
+ private boolean mIsVoipAudioMode;
+ private StatusHints mStatusHints;
+ private final ConnectionServiceRepository mRepository;
+
+ /**
+ * Persists the specified parameters and initializes the new instance.
+ *
+ * @param handle The handle to dial.
+ * @param gatewayInfo Gateway information to use for the call.
+ * @param connectionManagerPhoneAccountHandle Account to use for the service managing the call.
+ * This account must be one that was registered with the
+ * {@link PhoneAccount#CAPABILITY_CONNECTION_MANAGER} flag.
+ * @param targetPhoneAccountHandle Account information to use for the call. This account must be
+ * one that was registered with the {@link PhoneAccount#CAPABILITY_CALL_PROVIDER} flag.
+ * @param isIncoming True if this is an incoming call.
+ */
+ Call(
+ ConnectionServiceRepository repository,
+ Uri handle,
+ GatewayInfo gatewayInfo,
+ PhoneAccountHandle connectionManagerPhoneAccountHandle,
+ PhoneAccountHandle targetPhoneAccountHandle,
+ boolean isIncoming,
+ boolean isConference) {
+ mState = isConference ? CallState.ACTIVE : CallState.NEW;
+ mRepository = repository;
+ setHandle(handle);
+ setHandle(handle, TelecomManager.PRESENTATION_ALLOWED);
+ mGatewayInfo = gatewayInfo;
+ mConnectionManagerPhoneAccountHandle = connectionManagerPhoneAccountHandle;
+ mTargetPhoneAccountHandle = targetPhoneAccountHandle;
+ mIsIncoming = isIncoming;
+ mIsConference = isConference;
+ maybeLoadCannedSmsResponses();
+ }
+
+ void addListener(Listener listener) {
+ mListeners.add(listener);
+ }
+
+ void removeListener(Listener listener) {
+ if (listener != null) {
+ mListeners.remove(listener);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ String component = null;
+ if (mConnectionService != null && mConnectionService.getComponentName() != null) {
+ component = mConnectionService.getComponentName().flattenToShortString();
+ }
+
+ return String.format(Locale.US, "[%s, %s, %s, %s, %d]", System.identityHashCode(this),
+ mState, component, Log.piiHandle(mHandle), getVideoState());
+ }
+
+ int getState() {
+ return mState;
+ }
+
+ /**
+ * Sets the call state. Although there exists the notion of appropriate state transitions
+ * (see {@link CallState}), in practice those expectations break down when cellular systems
+ * 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) {
+ if (mState != newState) {
+ Log.v(this, "setState %s -> %s", mState, newState);
+ mState = newState;
+ maybeLoadCannedSmsResponses();
+
+ if (mState == CallState.DISCONNECTED) {
+ fixParentAfterDisconnect();
+ }
+ }
+ }
+
+ void setRingbackRequested(boolean ringbackRequested) {
+ mRingbackRequested = ringbackRequested;
+ for (Listener l : mListeners) {
+ l.onRingbackRequested(this, mRingbackRequested);
+ }
+ }
+
+ boolean isRingbackRequested() {
+ return mRingbackRequested;
+ }
+
+ boolean isConference() {
+ return mIsConference;
+ }
+
+ Uri getHandle() {
+ return mHandle;
+ }
+
+ int getHandlePresentation() {
+ return mHandlePresentation;
+ }
+
+
+ void setHandle(Uri handle) {
+ setHandle(handle, TelecomManager.PRESENTATION_ALLOWED);
+ }
+
+ void setHandle(Uri handle, int presentation) {
+ if (!Objects.equals(handle, mHandle) || presentation != mHandlePresentation) {
+ mHandle = handle;
+ mHandlePresentation = presentation;
+ mIsEmergencyCall = mHandle != null && PhoneNumberUtils.isLocalEmergencyNumber(
+ TelecomApp.getInstance(), mHandle.getSchemeSpecificPart());
+ startCallerInfoLookup();
+ for (Listener l : mListeners) {
+ l.onHandleChanged(this);
+ }
+ }
+ }
+
+ String getCallerDisplayName() {
+ return mCallerDisplayName;
+ }
+
+ int getCallerDisplayNamePresentation() {
+ return mCallerDisplayNamePresentation;
+ }
+
+ void setCallerDisplayName(String callerDisplayName, int presentation) {
+ if (!TextUtils.equals(callerDisplayName, mCallerDisplayName) ||
+ presentation != mCallerDisplayNamePresentation) {
+ mCallerDisplayName = callerDisplayName;
+ mCallerDisplayNamePresentation = presentation;
+ for (Listener l : mListeners) {
+ l.onCallerDisplayNameChanged(this);
+ }
+ }
+ }
+
+ String getName() {
+ return mCallerInfo == null ? null : mCallerInfo.name;
+ }
+
+ Bitmap getPhotoIcon() {
+ return mCallerInfo == null ? null : mCallerInfo.cachedPhotoIcon;
+ }
+
+ Drawable getPhoto() {
+ return mCallerInfo == null ? null : mCallerInfo.cachedPhoto;
+ }
+
+ /**
+ * @param disconnectCause The reason for the disconnection, any of
+ * {@link android.telephony.DisconnectCause}.
+ * @param disconnectMessage Optional message about the disconnect.
+ */
+ void setDisconnectCause(int disconnectCause, String disconnectMessage) {
+ // TODO: Consider combining this method with a setDisconnected() method that is totally
+ // separate from setState.
+ mDisconnectCause = disconnectCause;
+ mDisconnectMessage = disconnectMessage;
+ }
+
+ int getDisconnectCause() {
+ return mDisconnectCause;
+ }
+
+ String getDisconnectMessage() {
+ return mDisconnectMessage;
+ }
+
+ boolean isEmergencyCall() {
+ return mIsEmergencyCall;
+ }
+
+ /**
+ * @return The original handle this call is associated with. In-call services should use this
+ * handle when indicating in their UI the handle that is being called.
+ */
+ public Uri getOriginalHandle() {
+ if (mGatewayInfo != null && !mGatewayInfo.isEmpty()) {
+ return mGatewayInfo.getOriginalAddress();
+ }
+ return getHandle();
+ }
+
+ GatewayInfo getGatewayInfo() {
+ return mGatewayInfo;
+ }
+
+ void setGatewayInfo(GatewayInfo gatewayInfo) {
+ mGatewayInfo = gatewayInfo;
+ }
+
+ PhoneAccountHandle getConnectionManagerPhoneAccount() {
+ return mConnectionManagerPhoneAccountHandle;
+ }
+
+ void setConnectionManagerPhoneAccount(PhoneAccountHandle accountHandle) {
+ if (!Objects.equals(mConnectionManagerPhoneAccountHandle, accountHandle)) {
+ mConnectionManagerPhoneAccountHandle = accountHandle;
+ for (Listener l : mListeners) {
+ l.onConnectionManagerPhoneAccountChanged(this);
+ }
+ }
+
+ }
+
+ PhoneAccountHandle getTargetPhoneAccount() {
+ return mTargetPhoneAccountHandle;
+ }
+
+ void setTargetPhoneAccount(PhoneAccountHandle accountHandle) {
+ if (!Objects.equals(mTargetPhoneAccountHandle, accountHandle)) {
+ mTargetPhoneAccountHandle = accountHandle;
+ for (Listener l : mListeners) {
+ l.onTargetPhoneAccountChanged(this);
+ }
+ }
+ }
+
+ boolean isIncoming() {
+ return mIsIncoming;
+ }
+
+ /**
+ * @return The "age" of this call object in milliseconds, which typically also represents the
+ * period since this call was added to the set pending outgoing calls, see
+ * mCreationTimeMillis.
+ */
+ long getAgeMillis() {
+ return System.currentTimeMillis() - mCreationTimeMillis;
+ }
+
+ /**
+ * @return The time when this call object was created and added to the set of pending outgoing
+ * calls.
+ */
+ long getCreationTimeMillis() {
+ return mCreationTimeMillis;
+ }
+
+ long getConnectTimeMillis() {
+ return mConnectTimeMillis;
+ }
+
+ void setConnectTimeMillis(long connectTimeMillis) {
+ mConnectTimeMillis = connectTimeMillis;
+ }
+
+ int getCallCapabilities() {
+ return mCallCapabilities;
+ }
+
+ void setCallCapabilities(int callCapabilities) {
+ Log.v(this, "setCallCapabilities: %s", PhoneCapabilities.toString(callCapabilities));
+ if (mCallCapabilities != callCapabilities) {
+ mCallCapabilities = callCapabilities;
+ for (Listener l : mListeners) {
+ l.onCallCapabilitiesChanged(this);
+ }
+ }
+ }
+
+ Call getParentCall() {
+ return mParentCall;
+ }
+
+ List<Call> getChildCalls() {
+ return mChildCalls;
+ }
+
+ ConnectionServiceWrapper getConnectionService() {
+ return mConnectionService;
+ }
+
+ void setConnectionService(ConnectionServiceWrapper service) {
+ Preconditions.checkNotNull(service);
+
+ clearConnectionService();
+
+ service.incrementAssociatedCallCount();
+ mConnectionService = service;
+ mConnectionService.addCall(this);
+ }
+
+ /**
+ * Clears the associated connection service.
+ */
+ void clearConnectionService() {
+ if (mConnectionService != null) {
+ ConnectionServiceWrapper serviceTemp = mConnectionService;
+ mConnectionService = null;
+ serviceTemp.removeCall(this);
+
+ // Decrementing the count can cause the service to unbind, which itself can trigger the
+ // service-death code. Since the service death code tries to clean up any associated
+ // calls, we need to make sure to remove that information (e.g., removeCall()) before
+ // we decrement. Technically, invoking removeCall() prior to decrementing is all that is
+ // necessary, but cleaning up mConnectionService prior to triggering an unbind is good
+ // to do.
+ decrementAssociatedCallCount(serviceTemp);
+ }
+ }
+
+ private void processDirectToVoicemail() {
+ if (mDirectToVoicemailQueryPending) {
+ if (mCallerInfo != null && mCallerInfo.shouldSendToVoicemail) {
+ Log.i(this, "Directing call to voicemail: %s.", this);
+ // TODO: Once we move State handling from CallsManager to Call, we
+ // will not need to set STATE_RINGING state prior to calling reject.
+ setState(CallState.RINGING);
+ reject(false, null);
+ } else {
+ // TODO: Make this class (not CallsManager) responsible for changing
+ // the call state to STATE_RINGING.
+
+ // TODO: Replace this with state transition to STATE_RINGING.
+ for (Listener l : mListeners) {
+ l.onSuccessfulIncomingCall(this);
+ }
+ }
+
+ mDirectToVoicemailQueryPending = false;
+ }
+ }
+
+ /**
+ * Starts the create connection sequence. Upon completion, there should exist an active
+ * connection through a connection service (or the call will have failed).
+ */
+ void startCreateConnection() {
+ Preconditions.checkState(mCreateConnectionProcessor == null);
+ mCreateConnectionProcessor = new CreateConnectionProcessor(this, mRepository, this);
+ mCreateConnectionProcessor.process();
+ }
+
+ @Override
+ public void handleCreateConnectionSuccess(
+ CallIdMapper idMapper,
+ ParcelableConnection connection) {
+ Log.v(this, "handleCreateConnectionSuccessful %s", connection);
+ mCreateConnectionProcessor = null;
+ setTargetPhoneAccount(connection.getPhoneAccount());
+ setHandle(connection.getHandle(), connection.getHandlePresentation());
+ setCallerDisplayName(
+ connection.getCallerDisplayName(), connection.getCallerDisplayNamePresentation());
+ setCallCapabilities(connection.getCapabilities());
+ setVideoProvider(connection.getVideoProvider());
+ setVideoState(connection.getVideoState());
+ setRingbackRequested(connection.isRingbackRequested());
+ setIsVoipAudioMode(connection.getIsVoipAudioMode());
+ setStatusHints(connection.getStatusHints());
+
+ mConferenceableCalls.clear();
+ for (String id : connection.getConferenceableConnectionIds()) {
+ mConferenceableCalls.add(idMapper.getCall(id));
+ }
+
+ if (mIsIncoming) {
+ // We do not handle incoming calls immediately when they are verified by the connection
+ // service. We allow the caller-info-query code to execute first so that we can read the
+ // direct-to-voicemail property before deciding if we want to show the incoming call to
+ // the user or if we want to reject the call.
+ mDirectToVoicemailQueryPending = true;
+
+ // Timeout the direct-to-voicemail lookup execution so that we dont wait too long before
+ // showing the user the incoming call screen.
+ mHandler.postDelayed(mDirectToVoicemailRunnable, Timeouts.getDirectToVoicemailMillis());
+ } else {
+ for (Listener l : mListeners) {
+ l.onSuccessfulOutgoingCall(this,
+ getStateFromConnectionState(connection.getState()));
+ }
+ }
+ }
+
+ @Override
+ public void handleCreateConnectionFailure(int code, String msg) {
+ mCreateConnectionProcessor = null;
+ clearConnectionService();
+ setDisconnectCause(code, msg);
+ CallsManager.getInstance().markCallAsDisconnected(this, code, msg);
+
+ if (mIsIncoming) {
+ for (Listener listener : mListeners) {
+ listener.onFailedIncomingCall(this);
+ }
+ } else {
+ for (Listener listener : mListeners) {
+ listener.onFailedOutgoingCall(this, code, msg);
+ }
+ }
+ }
+
+ /**
+ * Plays the specified DTMF tone.
+ */
+ void playDtmfTone(char digit) {
+ if (mConnectionService == null) {
+ Log.w(this, "playDtmfTone() request on a call without a connection service.");
+ } else {
+ Log.i(this, "Send playDtmfTone to connection service for call %s", this);
+ mConnectionService.playDtmfTone(this, digit);
+ }
+ }
+
+ /**
+ * Stops playing any currently playing DTMF tone.
+ */
+ void stopDtmfTone() {
+ if (mConnectionService == null) {
+ Log.w(this, "stopDtmfTone() request on a call without a connection service.");
+ } else {
+ Log.i(this, "Send stopDtmfTone to connection service for call %s", this);
+ mConnectionService.stopDtmfTone(this);
+ }
+ }
+
+ /**
+ * Attempts to disconnect the call through the connection service.
+ */
+ void disconnect() {
+ if (mState == CallState.NEW || mState == CallState.PRE_DIAL_WAIT ||
+ mState == CallState.CONNECTING) {
+ Log.v(this, "Aborting call %s", this);
+ abort();
+ } else if (mState != CallState.ABORTED && mState != CallState.DISCONNECTED) {
+ if (mConnectionService == null) {
+ Log.e(this, new Exception(), "disconnect() request on a call without a"
+ + " connection service.");
+ } else {
+ Log.i(this, "Send disconnect to connection service for call: %s", this);
+ // The call isn't officially disconnected until the connection service
+ // confirms that the call was actually disconnected. Only then is the
+ // association between call and connection service severed, see
+ // {@link CallsManager#markCallAsDisconnected}.
+ mConnectionService.disconnect(this);
+ }
+ }
+ }
+
+ void abort() {
+ if (mCreateConnectionProcessor != null) {
+ mCreateConnectionProcessor.abort();
+ } else if (mState == CallState.NEW || mState == CallState.PRE_DIAL_WAIT
+ || mState == CallState.CONNECTING) {
+ handleCreateConnectionFailure(DisconnectCause.OUTGOING_CANCELED, null);
+ } else {
+ Log.v(this, "Cannot abort a call which isn't either PRE_DIAL_WAIT or CONNECTING");
+ }
+ }
+
+ /**
+ * Answers the call if it is ringing.
+ *
+ * @param videoState The video state in which to answer the call.
+ */
+ void answer(int videoState) {
+ Preconditions.checkNotNull(mConnectionService);
+
+ // Check to verify that the call is still in the ringing state. A call can change states
+ // between the time the user hits 'answer' and Telecom receives the command.
+ if (isRinging("answer")) {
+ // At this point, we are asking the connection service to answer but we don't assume
+ // that it will work. Instead, we wait until confirmation from the connectino service
+ // that the call is in a non-STATE_RINGING state before changing the UI. See
+ // {@link ConnectionServiceAdapter#setActive} and other set* methods.
+ mConnectionService.answer(this, videoState);
+ }
+ }
+
+ /**
+ * Rejects the call if it is ringing.
+ *
+ * @param rejectWithMessage Whether to send a text message as part of the call rejection.
+ * @param textMessage An optional text message to send as part of the rejection.
+ */
+ void reject(boolean rejectWithMessage, String textMessage) {
+ Preconditions.checkNotNull(mConnectionService);
+
+ // Check to verify that the call is still in the ringing state. A call can change states
+ // between the time the user hits 'reject' and Telecomm receives the command.
+ if (isRinging("reject")) {
+ mConnectionService.reject(this);
+ }
+ }
+
+ /**
+ * Puts the call on hold if it is currently active.
+ */
+ void hold() {
+ Preconditions.checkNotNull(mConnectionService);
+
+ if (mState == CallState.ACTIVE) {
+ mConnectionService.hold(this);
+ }
+ }
+
+ /**
+ * Releases the call from hold if it is currently active.
+ */
+ void unhold() {
+ Preconditions.checkNotNull(mConnectionService);
+
+ if (mState == CallState.ON_HOLD) {
+ mConnectionService.unhold(this);
+ }
+ }
+
+ /** Checks if this is a live call or not. */
+ boolean isAlive() {
+ switch (mState) {
+ case CallState.NEW:
+ case CallState.RINGING:
+ case CallState.DISCONNECTED:
+ case CallState.ABORTED:
+ return false;
+ default:
+ return true;
+ }
+ }
+
+ boolean isActive() {
+ return mState == CallState.ACTIVE;
+ }
+
+ Bundle getExtras() {
+ return mExtras;
+ }
+
+ void setExtras(Bundle extras) {
+ mExtras = extras;
+ }
+
+ /**
+ * @return the uri of the contact associated with this call.
+ */
+ Uri getContactUri() {
+ if (mCallerInfo == null || !mCallerInfo.contactExists) {
+ return null;
+ }
+ return Contacts.getLookupUri(mCallerInfo.contactIdOrZero, mCallerInfo.lookupKey);
+ }
+
+ Uri getRingtone() {
+ return mCallerInfo == null ? null : mCallerInfo.contactRingtoneUri;
+ }
+
+ void onPostDialWait(String remaining) {
+ for (Listener l : mListeners) {
+ l.onPostDialWait(this, remaining);
+ }
+ }
+
+ void postDialContinue(boolean proceed) {
+ mConnectionService.onPostDialContinue(this, proceed);
+ }
+
+ void conferenceWith(Call otherCall) {
+ if (mConnectionService == null) {
+ Log.w(this, "conference requested on a call without a connection service.");
+ } else {
+ mConnectionService.conference(this, otherCall);
+ }
+ }
+
+ void splitFromConference() {
+ if (mConnectionService == null) {
+ Log.w(this, "splitting from conference call without a connection service");
+ } else {
+ mConnectionService.splitFromConference(this);
+ }
+ }
+
+ void mergeConference() {
+ if (mConnectionService == null) {
+ Log.w(this, "merging conference calls without a connection service.");
+ } else if (can(PhoneCapabilities.MERGE_CONFERENCE)) {
+ mConnectionService.mergeConference(this);
+ }
+ }
+
+ void swapConference() {
+ if (mConnectionService == null) {
+ Log.w(this, "swapping conference calls without a connection service.");
+ } else if (can(PhoneCapabilities.SWAP_CONFERENCE)) {
+ mConnectionService.swapConference(this);
+ }
+ }
+
+ void setParentCall(Call parentCall) {
+ if (parentCall == this) {
+ Log.e(this, new Exception(), "setting the parent to self");
+ return;
+ }
+ if (parentCall == mParentCall) {
+ // nothing to do
+ return;
+ }
+ Preconditions.checkState(parentCall == null || mParentCall == null);
+
+ Call oldParent = mParentCall;
+ if (mParentCall != null) {
+ mParentCall.removeChildCall(this);
+ }
+ mParentCall = parentCall;
+ if (mParentCall != null) {
+ mParentCall.addChildCall(this);
+ }
+
+ for (Listener l : mListeners) {
+ l.onParentChanged(this);
+ }
+ }
+
+ void setConferenceableCalls(List<Call> conferenceableCalls) {
+ mConferenceableCalls.clear();
+ mConferenceableCalls.addAll(conferenceableCalls);
+ }
+
+ List<Call> getConferenceableCalls() {
+ return mConferenceableCalls;
+ }
+
+ private boolean can(int capability) {
+ return (mCallCapabilities & capability) == capability;
+ }
+
+ private void addChildCall(Call call) {
+ if (!mChildCalls.contains(call)) {
+ mChildCalls.add(call);
+
+ for (Listener l : mListeners) {
+ l.onChildrenChanged(this);
+ }
+ }
+ }
+
+ private void removeChildCall(Call call) {
+ if (mChildCalls.remove(call)) {
+ for (Listener l : mListeners) {
+ l.onChildrenChanged(this);
+ }
+ }
+ }
+
+ /**
+ * Return whether the user can respond to this {@code Call} via an SMS message.
+ *
+ * @return true if the "Respond via SMS" feature should be enabled
+ * for this incoming call.
+ *
+ * The general rule is that we *do* allow "Respond via SMS" except for
+ * the few (relatively rare) cases where we know for sure it won't
+ * work, namely:
+ * - a bogus or blank incoming number
+ * - a call from a SIP address
+ * - a "call presentation" that doesn't allow the number to be revealed
+ *
+ * In all other cases, we allow the user to respond via SMS.
+ *
+ * Note that this behavior isn't perfect; for example we have no way
+ * to detect whether the incoming call is from a landline (with most
+ * networks at least), so we still enable this feature even though
+ * SMSes to that number will silently fail.
+ */
+ boolean isRespondViaSmsCapable() {
+ if (mState != CallState.RINGING) {
+ return false;
+ }
+
+ if (getHandle() == null) {
+ // No incoming number known or call presentation is "PRESENTATION_RESTRICTED", in
+ // other words, the user should not be able to see the incoming phone number.
+ return false;
+ }
+
+ if (PhoneNumberUtils.isUriNumber(getHandle().toString())) {
+ // The incoming number is actually a URI (i.e. a SIP address),
+ // not a regular PSTN phone number, and we can't send SMSes to
+ // SIP addresses.
+ // (TODO: That might still be possible eventually, though. Is
+ // there some SIP-specific equivalent to sending a text message?)
+ return false;
+ }
+
+ // Is there a valid SMS application on the phone?
+ if (SmsApplication.getDefaultRespondViaMessageApplication(TelecomApp.getInstance(),
+ true /*updateIfNeeded*/) == null) {
+ return false;
+ }
+
+ // TODO: with some carriers (in certain countries) you *can* actually
+ // tell whether a given number is a mobile phone or not. So in that
+ // case we could potentially return false here if the incoming call is
+ // from a land line.
+
+ // If none of the above special cases apply, it's OK to enable the
+ // "Respond via SMS" feature.
+ return true;
+ }
+
+ List<String> getCannedSmsResponses() {
+ return mCannedSmsResponses;
+ }
+
+ /**
+ * We need to make sure that before we move a call to the disconnected state, it no
+ * longer has any parent/child relationships. We want to do this to ensure that the InCall
+ * Service always has the right data in the right order. We also want to do it in telecom so
+ * that the insurance policy lives in the framework side of things.
+ */
+ private void fixParentAfterDisconnect() {
+ setParentCall(null);
+ }
+
+ /**
+ * @return True if the call is ringing, else logs the action name.
+ */
+ private boolean isRinging(String actionName) {
+ if (mState == CallState.RINGING) {
+ return true;
+ }
+
+ Log.i(this, "Request to %s a non-ringing call %s", actionName, this);
+ return false;
+ }
+
+ @SuppressWarnings("rawtypes")
+ private void decrementAssociatedCallCount(ServiceBinder binder) {
+ if (binder != null) {
+ binder.decrementAssociatedCallCount();
+ }
+ }
+
+ /**
+ * Looks up contact information based on the current handle.
+ */
+ private void startCallerInfoLookup() {
+ String number = mHandle == null ? null : mHandle.getSchemeSpecificPart();
+
+ mQueryToken++; // Updated so that previous queries can no longer set the information.
+ mCallerInfo = null;
+ if (!TextUtils.isEmpty(number)) {
+ Log.v(this, "Looking up information for: %s.", Log.piiHandle(number));
+ CallerInfoAsyncQuery.startQuery(
+ mQueryToken,
+ TelecomApp.getInstance(),
+ number,
+ sCallerInfoQueryListener,
+ this);
+ }
+ }
+
+ /**
+ * Saves the specified caller info if the specified token matches that of the last query
+ * that was made.
+ *
+ * @param callerInfo The new caller information to set.
+ * @param token The token used with this query.
+ */
+ private void setCallerInfo(CallerInfo callerInfo, int token) {
+ Preconditions.checkNotNull(callerInfo);
+
+ if (mQueryToken == token) {
+ mCallerInfo = callerInfo;
+ Log.i(this, "CallerInfo received for %s: %s", Log.piiHandle(mHandle), callerInfo);
+
+ if (mCallerInfo.contactDisplayPhotoUri != null) {
+ Log.d(this, "Searching person uri %s for call %s",
+ mCallerInfo.contactDisplayPhotoUri, this);
+ ContactsAsyncHelper.startObtainPhotoAsync(
+ token,
+ TelecomApp.getInstance(),
+ mCallerInfo.contactDisplayPhotoUri,
+ sPhotoLoadListener,
+ this);
+ // Do not call onCallerInfoChanged yet in this case. We call it in setPhoto().
+ } else {
+ for (Listener l : mListeners) {
+ l.onCallerInfoChanged(this);
+ }
+ }
+
+ processDirectToVoicemail();
+ }
+ }
+
+ CallerInfo getCallerInfo() {
+ return mCallerInfo;
+ }
+
+ /**
+ * Saves the specified photo information if the specified token matches that of the last query.
+ *
+ * @param photo The photo as a drawable.
+ * @param photoIcon The photo as a small icon.
+ * @param token The token used with this query.
+ */
+ private void setPhoto(Drawable photo, Bitmap photoIcon, int token) {
+ if (mQueryToken == token) {
+ mCallerInfo.cachedPhoto = photo;
+ mCallerInfo.cachedPhotoIcon = photoIcon;
+
+ for (Listener l : mListeners) {
+ l.onCallerInfoChanged(this);
+ }
+ }
+ }
+
+ private void maybeLoadCannedSmsResponses() {
+ if (mIsIncoming && isRespondViaSmsCapable() && !mCannedSmsResponsesLoadingStarted) {
+ Log.d(this, "maybeLoadCannedSmsResponses: starting task to load messages");
+ mCannedSmsResponsesLoadingStarted = true;
+ RespondViaSmsManager.getInstance().loadCannedTextMessages(
+ new Response<Void, List<String>>() {
+ @Override
+ public void onResult(Void request, List<String>... result) {
+ if (result.length > 0) {
+ Log.d(this, "maybeLoadCannedSmsResponses: got %s", result[0]);
+ mCannedSmsResponses = result[0];
+ for (Listener l : mListeners) {
+ l.onCannedSmsResponsesLoaded(Call.this);
+ }
+ }
+ }
+
+ @Override
+ public void onError(Void request, int code, String msg) {
+ Log.w(Call.this, "Error obtaining canned SMS responses: %d %s", code,
+ msg);
+ }
+ }
+ );
+ } else {
+ Log.d(this, "maybeLoadCannedSmsResponses: doing nothing");
+ }
+ }
+
+ /**
+ * Sets speakerphone option on when call begins.
+ */
+ public void setStartWithSpeakerphoneOn(boolean startWithSpeakerphone) {
+ mSpeakerphoneOn = startWithSpeakerphone;
+ }
+
+ /**
+ * Returns speakerphone option.
+ *
+ * @return Whether or not speakerphone should be set automatically when call begins.
+ */
+ public boolean getStartWithSpeakerphoneOn() {
+ return mSpeakerphoneOn;
+ }
+
+ /**
+ * Sets a video call provider for the call.
+ */
+ public void setVideoProvider(IVideoProvider videoProvider) {
+ mVideoProvider = videoProvider;
+ for (Listener l : mListeners) {
+ l.onVideoCallProviderChanged(Call.this);
+ }
+ }
+
+ /**
+ * @return Return the {@link Connection.VideoProvider} binder.
+ */
+ public IVideoProvider getVideoProvider() {
+ return mVideoProvider;
+ }
+
+ /**
+ * The current video state for the call.
+ * Valid values: see {@link VideoProfile.VideoState}.
+ */
+ public int getVideoState() {
+ return mVideoState;
+ }
+
+ /**
+ * Returns the video states which were applicable over the duration of a call.
+ * See {@link VideoProfile} for a list of valid video states.
+ *
+ * @return The video states applicable over the duration of the call.
+ */
+ public int getVideoStateHistory() {
+ return mVideoStateHistory;
+ }
+
+ /**
+ * Determines the current video state for the call.
+ * For an outgoing call determines the desired video state for the call.
+ * Valid values: see {@link VideoProfile.VideoState}
+ *
+ * @param videoState The video state for the call.
+ */
+ public void setVideoState(int videoState) {
+ // Track which video states were applicable over the duration of the call.
+ mVideoStateHistory = mVideoStateHistory | videoState;
+
+ mVideoState = videoState;
+ for (Listener l : mListeners) {
+ l.onVideoStateChanged(this);
+ }
+ }
+
+ public boolean getIsVoipAudioMode() {
+ return mIsVoipAudioMode;
+ }
+
+ public void setIsVoipAudioMode(boolean audioModeIsVoip) {
+ mIsVoipAudioMode = audioModeIsVoip;
+ for (Listener l : mListeners) {
+ l.onIsVoipAudioModeChanged(this);
+ }
+ }
+
+ public StatusHints getStatusHints() {
+ return mStatusHints;
+ }
+
+ public void setStatusHints(StatusHints statusHints) {
+ mStatusHints = statusHints;
+ for (Listener l : mListeners) {
+ l.onStatusHintsChanged(this);
+ }
+ }
+
+ static int getStateFromConnectionState(int state) {
+ switch (state) {
+ case Connection.STATE_INITIALIZING:
+ return CallState.CONNECTING;
+ case Connection.STATE_ACTIVE:
+ return CallState.ACTIVE;
+ case Connection.STATE_DIALING:
+ return CallState.DIALING;
+ case Connection.STATE_DISCONNECTED:
+ return CallState.DISCONNECTED;
+ case Connection.STATE_HOLDING:
+ return CallState.ON_HOLD;
+ case Connection.STATE_NEW:
+ return CallState.NEW;
+ case Connection.STATE_RINGING:
+ return CallState.RINGING;
+ }
+ return CallState.DISCONNECTED;
+ }
+}
diff --git a/src/com/android/server/telecom/CallActivity.java b/src/com/android/server/telecom/CallActivity.java
new file mode 100644
index 0000000..96ffb96
--- /dev/null
+++ b/src/com/android/server/telecom/CallActivity.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2013 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.app.Activity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.UserManager;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telephony.DisconnectCause;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.widget.Toast;
+
+/**
+ * Activity that handles system CALL actions and forwards them to {@link CallsManager}.
+ * Handles all three CALL action types: CALL, CALL_PRIVILEGED, and CALL_EMERGENCY.
+ *
+ * Pre-L, the only way apps were were allowed to make outgoing emergency calls was the
+ * ACTION_CALL_PRIVILEGED action (which requires the system only CALL_PRIVILEGED permission).
+ *
+ * In L, any app that has the CALL_PRIVILEGED permission can continue to make outgoing emergency
+ * calls via ACTION_CALL_PRIVILEGED.
+ *
+ * In addition, the default dialer (identified via {@link android.telecom.TelecomManager#getDefaultPhoneApp()}
+ * will also be granted the ability to make emergency outgoing calls using the CALL action. In
+ * order to do this, it must call startActivityForResult on the CALL intent to allow its package
+ * name to be passed to {@link CallActivity}. Calling startActivity will continue to work on all
+ * non-emergency numbers just like it did pre-L.
+ */
+public class CallActivity extends Activity {
+
+ private CallsManager mCallsManager = CallsManager.getInstance();
+ private boolean mIsVoiceCapable;
+
+ /**
+ * {@inheritDoc}
+ *
+ * This method is the single point of entry for the CALL intent, which is used by built-in apps
+ * like Contacts & Dialer, as well as 3rd party apps to initiate outgoing calls.
+ */
+ @Override
+ protected void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+
+ mIsVoiceCapable = isVoiceCapable();
+
+ // TODO: This activity will be displayed until the next screen which could be
+ // the in-call UI and error dialog or potentially a call-type selection dialog.
+ // Traditionally, this has been a black screen with a spinner. We need to reevaluate if this
+ // is still desired and add back if necessary. Currently, the activity is set to NoDisplay
+ // theme which means it shows no UI.
+
+ Intent intent = getIntent();
+ Configuration configuration = getResources().getConfiguration();
+
+ Log.d(this, "onCreate: this = %s, bundle = %s", this, bundle);
+ Log.d(this, " - intent = %s", intent);
+ Log.d(this, " - configuration = %s", configuration);
+
+ // TODO: Figure out if there is something to restore from bundle.
+ // See OutgoingCallBroadcaster in services/Telephony for more.
+
+ processIntent(intent);
+
+ // This activity does not have associated UI, so close.
+ finish();
+
+ Log.d(this, "onCreate: end");
+ }
+
+ /**
+ * Processes intents sent to the activity.
+ *
+ * @param intent The intent.
+ */
+ private void processIntent(Intent intent) {
+ // Ensure call intents are not processed on devices that are not capable of calling.
+ if (!mIsVoiceCapable) {
+ setResult(RESULT_CANCELED);
+ return;
+ }
+
+ String action = intent.getAction();
+
+ // TODO: Check for non-voice capable devices before reading any intents.
+
+ if (Intent.ACTION_CALL.equals(action) ||
+ Intent.ACTION_CALL_PRIVILEGED.equals(action) ||
+ Intent.ACTION_CALL_EMERGENCY.equals(action)) {
+ processOutgoingCallIntent(intent);
+ } else if (TelecomManager.ACTION_INCOMING_CALL.equals(action)) {
+ processIncomingCallIntent(intent);
+ }
+ }
+
+ /**
+ * Processes CALL, CALL_PRIVILEGED, and CALL_EMERGENCY intents.
+ *
+ * @param intent Call intent containing data about the handle to call.
+ */
+ private void processOutgoingCallIntent(Intent intent) {
+ Uri handle = intent.getData();
+ String scheme = handle.getScheme();
+ String uriString = handle.getSchemeSpecificPart();
+
+ if (!PhoneAccount.SCHEME_VOICEMAIL.equals(scheme)) {
+ handle = Uri.fromParts(PhoneNumberUtils.isUriNumber(uriString) ?
+ PhoneAccount.SCHEME_SIP : PhoneAccount.SCHEME_TEL, uriString, null);
+ }
+
+ UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE);
+ if (userManager.hasUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS)
+ && !TelephonyUtil.shouldProcessAsEmergency(this, handle)) {
+ // Only emergency calls are allowed for users with the DISALLOW_OUTGOING_CALLS
+ // restriction.
+ Toast.makeText(this, getResources().getString(R.string.outgoing_call_not_allowed),
+ Toast.LENGTH_SHORT).show();
+ Log.d(this, "Rejecting non-emergency phone call due to DISALLOW_OUTGOING_CALLS "
+ + "restriction");
+ return;
+ }
+
+ PhoneAccountHandle phoneAccountHandle = intent.getParcelableExtra(
+ TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE);
+
+ Bundle clientExtras = null;
+ if (intent.hasExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS)) {
+ clientExtras = intent.getBundleExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS);
+ }
+ if (clientExtras == null) {
+ clientExtras = Bundle.EMPTY;
+ }
+
+ // Send to CallsManager to ensure the InCallUI gets kicked off before the broadcast returns
+ Call call = mCallsManager.startOutgoingCall(handle, phoneAccountHandle, clientExtras);
+
+ if (call == null) {
+ setResult(RESULT_CANCELED);
+ } else {
+ NewOutgoingCallIntentBroadcaster broadcaster = new NewOutgoingCallIntentBroadcaster(
+ mCallsManager, call, intent, isDefaultDialer());
+ final int result = broadcaster.processIntent();
+ final boolean success = result == DisconnectCause.NOT_DISCONNECTED;
+
+ if (!success && call != null) {
+ disconnectCallAndShowErrorDialog(call, result);
+ }
+ setResult(success ? RESULT_OK : RESULT_CANCELED);
+ }
+ }
+
+ /**
+ * Processes INCOMING_CALL intents. Grabs the connection service information from the intent
+ * extra and forwards that to the CallsManager to start the incoming call flow.
+ *
+ * @param intent The incoming call intent.
+ */
+ private void processIncomingCallIntent(Intent intent) {
+ PhoneAccountHandle phoneAccountHandle = intent.getParcelableExtra(
+ TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE);
+ if (phoneAccountHandle == null) {
+ Log.w(this, "Rejecting incoming call due to null phone account");
+ return;
+ }
+ if (phoneAccountHandle.getComponentName() == null) {
+ Log.w(this, "Rejecting incoming call due to null component name");
+ return;
+ }
+
+ Bundle clientExtras = null;
+ if (intent.hasExtra(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS)) {
+ clientExtras = intent.getBundleExtra(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS);
+ }
+ if (clientExtras == null) {
+ clientExtras = Bundle.EMPTY;
+ }
+
+ Log.d(this, "Processing incoming call from connection service [%s]",
+ phoneAccountHandle.getComponentName());
+ mCallsManager.processIncomingCallIntent(phoneAccountHandle, clientExtras);
+ }
+
+ private boolean isDefaultDialer() {
+ final String packageName = getCallingPackage();
+ if (TextUtils.isEmpty(packageName)) {
+ return false;
+ }
+
+ final TelecomManager telecomManager =
+ (TelecomManager) getSystemService(Context.TELECOM_SERVICE);
+ final ComponentName defaultPhoneApp = telecomManager.getDefaultPhoneApp();
+ return (defaultPhoneApp != null
+ && TextUtils.equals(defaultPhoneApp.getPackageName(), packageName));
+ }
+
+ /**
+ * Returns whether the device is voice-capable (e.g. a phone vs a tablet).
+ *
+ * @return {@code True} if the device is voice-capable.
+ */
+ private boolean isVoiceCapable() {
+ return getApplicationContext().getResources().getBoolean(
+ com.android.internal.R.bool.config_voice_capable);
+ }
+
+ private void disconnectCallAndShowErrorDialog(Call call, int errorCode) {
+ call.disconnect();
+ final Intent errorIntent = new Intent(this, ErrorDialogActivity.class);
+ int errorMessageId = -1;
+ switch (errorCode) {
+ case DisconnectCause.INVALID_NUMBER:
+ errorMessageId = R.string.outgoing_call_error_no_phone_number_supplied;
+ break;
+ case DisconnectCause.VOICEMAIL_NUMBER_MISSING:
+ errorIntent.putExtra(ErrorDialogActivity.SHOW_MISSING_VOICEMAIL_NO_DIALOG_EXTRA,
+ true);
+ break;
+ }
+ if (errorMessageId != -1) {
+ errorIntent.putExtra(ErrorDialogActivity.ERROR_MESSAGE_ID_EXTRA, errorMessageId);
+ }
+ startActivity(errorIntent);
+ }
+}
diff --git a/src/com/android/server/telecom/CallAudioManager.java b/src/com/android/server/telecom/CallAudioManager.java
new file mode 100644
index 0000000..312f58f
--- /dev/null
+++ b/src/com/android/server/telecom/CallAudioManager.java
@@ -0,0 +1,495 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.telecom.AudioState;
+import android.telecom.CallState;
+
+import com.google.common.base.Preconditions;
+
+import java.util.Objects;
+
+/**
+ * This class manages audio modes, streams and other properties.
+ */
+final class CallAudioManager extends CallsManagerListenerBase
+ implements WiredHeadsetManager.Listener {
+ private static final int STREAM_NONE = -1;
+
+ private final StatusBarNotifier mStatusBarNotifier;
+ private final AudioManager mAudioManager;
+ private final BluetoothManager mBluetoothManager;
+ private final WiredHeadsetManager mWiredHeadsetManager;
+
+ private AudioState mAudioState;
+ private int mAudioFocusStreamType;
+ private boolean mIsRinging;
+ private boolean mIsTonePlaying;
+ private boolean mWasSpeakerOn;
+ private int mMostRecentlyUsedMode = AudioManager.MODE_IN_CALL;
+
+ CallAudioManager(Context context, StatusBarNotifier statusBarNotifier,
+ WiredHeadsetManager wiredHeadsetManager) {
+ mStatusBarNotifier = statusBarNotifier;
+ mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ mBluetoothManager = new BluetoothManager(context, this);
+ mWiredHeadsetManager = wiredHeadsetManager;
+ mWiredHeadsetManager.addListener(this);
+
+ saveAudioState(getInitialAudioState(null));
+ mAudioFocusStreamType = STREAM_NONE;
+ }
+
+ AudioState getAudioState() {
+ return mAudioState;
+ }
+
+ @Override
+ public void onCallAdded(Call call) {
+ onCallUpdated(call);
+
+ if (hasFocus() && getForegroundCall() == call) {
+ if (!call.isIncoming()) {
+ // Unmute new outgoing call.
+ setSystemAudioState(false, mAudioState.route, mAudioState.supportedRouteMask);
+ }
+ }
+ }
+
+ @Override
+ public void onCallRemoved(Call call) {
+ // If we didn't already have focus, there's nothing to do.
+ if (hasFocus()) {
+ if (CallsManager.getInstance().getCalls().isEmpty()) {
+ Log.v(this, "all calls removed, reseting system audio to default state");
+ setInitialAudioState(null, false /* force */);
+ mWasSpeakerOn = false;
+ }
+ updateAudioStreamAndMode();
+ }
+ }
+
+ @Override
+ public void onCallStateChanged(Call call, int oldState, int newState) {
+ onCallUpdated(call);
+ }
+
+ @Override
+ public void onIncomingCallAnswered(Call call) {
+ int route = mAudioState.route;
+
+ // 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 = CallsManager.getInstance().getCalls().size() == 1;
+ if (isOnlyCall && mBluetoothManager.isBluetoothAvailable()) {
+ mBluetoothManager.connectBluetoothAudio();
+ route = AudioState.ROUTE_BLUETOOTH;
+ }
+
+ setSystemAudioState(false /* isMute */, route, mAudioState.supportedRouteMask);
+ }
+
+ @Override
+ public void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall) {
+ onCallUpdated(newForegroundCall);
+ // Ensure that the foreground call knows about the latest audio state.
+ updateAudioForForegroundCall();
+ }
+
+ @Override
+ public void onIsVoipAudioModeChanged(Call call) {
+ updateAudioStreamAndMode();
+ }
+
+ /**
+ * Updates the audio route when the headset plugged in state changes. For example, if audio is
+ * being routed over speakerphone and a headset is plugged in then switch to wired headset.
+ */
+ @Override
+ public void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn) {
+ // This can happen even when there are no calls and we don't have focus.
+ if (!hasFocus()) {
+ return;
+ }
+
+ int newRoute = AudioState.ROUTE_EARPIECE;
+ if (newIsPluggedIn) {
+ newRoute = AudioState.ROUTE_WIRED_HEADSET;
+ } else if (mWasSpeakerOn) {
+ Call call = getForegroundCall();
+ if (call != null && call.isAlive()) {
+ // Restore the speaker state.
+ newRoute = AudioState.ROUTE_SPEAKER;
+ }
+ }
+ setSystemAudioState(mAudioState.isMuted, newRoute, calculateSupportedRoutes());
+ }
+
+ void toggleMute() {
+ mute(!mAudioState.isMuted);
+ }
+
+ void mute(boolean shouldMute) {
+ if (!hasFocus()) {
+ return;
+ }
+
+ Log.v(this, "mute, shouldMute: %b", shouldMute);
+
+ // Don't mute if there are any emergency calls.
+ if (CallsManager.getInstance().hasEmergencyCall()) {
+ shouldMute = false;
+ Log.v(this, "ignoring mute for emergency call");
+ }
+
+ if (mAudioState.isMuted != shouldMute) {
+ setSystemAudioState(shouldMute, mAudioState.route, mAudioState.supportedRouteMask);
+ }
+ }
+
+ /**
+ * Changed the audio route, for example from earpiece to speaker phone.
+ *
+ * @param route The new audio route to use. See {@link AudioState}.
+ */
+ void setAudioRoute(int route) {
+ // This can happen even when there are no calls and we don't have focus.
+ if (!hasFocus()) {
+ return;
+ }
+
+ Log.v(this, "setAudioRoute, route: %s", AudioState.audioRouteToString(route));
+
+ // Change ROUTE_WIRED_OR_EARPIECE to a single entry.
+ int newRoute = selectWiredOrEarpiece(route, mAudioState.supportedRouteMask);
+
+ // If route is unsupported, do nothing.
+ if ((mAudioState.supportedRouteMask | newRoute) == 0) {
+ Log.wtf(this, "Asking to set to a route that is unsupported: %d", newRoute);
+ return;
+ }
+
+ if (mAudioState.route != newRoute) {
+ // Remember the new speaker state so it can be restored when the user plugs and unplugs
+ // a headset.
+ mWasSpeakerOn = newRoute == AudioState.ROUTE_SPEAKER;
+ setSystemAudioState(mAudioState.isMuted, newRoute, mAudioState.supportedRouteMask);
+ }
+ }
+
+ void setIsRinging(boolean isRinging) {
+ if (mIsRinging != isRinging) {
+ Log.v(this, "setIsRinging %b -> %b", mIsRinging, isRinging);
+ mIsRinging = isRinging;
+ updateAudioStreamAndMode();
+ }
+ }
+
+ /**
+ * Sets the tone playing status. Some tones can play even when there are no live calls and this
+ * status indicates that we should keep audio focus even for tones that play beyond the life of
+ * calls.
+ *
+ * @param isPlayingNew The status to set.
+ */
+ void setIsTonePlaying(boolean isPlayingNew) {
+ ThreadUtil.checkOnMainThread();
+
+ if (mIsTonePlaying != isPlayingNew) {
+ Log.v(this, "mIsTonePlaying %b -> %b.", mIsTonePlaying, isPlayingNew);
+ mIsTonePlaying = isPlayingNew;
+ updateAudioStreamAndMode();
+ }
+ }
+
+ /**
+ * Updates the audio routing according to the bluetooth state.
+ */
+ void onBluetoothStateChange(BluetoothManager bluetoothManager) {
+ // This can happen even when there are no calls and we don't have focus.
+ if (!hasFocus()) {
+ return;
+ }
+
+ int supportedRoutes = calculateSupportedRoutes();
+ int newRoute = mAudioState.route;
+ if (bluetoothManager.isBluetoothAudioConnectedOrPending()) {
+ newRoute = AudioState.ROUTE_BLUETOOTH;
+ } else if (mAudioState.route == AudioState.ROUTE_BLUETOOTH) {
+ newRoute = selectWiredOrEarpiece(AudioState.ROUTE_WIRED_OR_EARPIECE, supportedRoutes);
+ // Do not switch to speaker when bluetooth disconnects.
+ mWasSpeakerOn = false;
+ }
+
+ setSystemAudioState(mAudioState.isMuted, newRoute, supportedRoutes);
+ }
+
+ boolean isBluetoothAudioOn() {
+ return mBluetoothManager.isBluetoothAudioConnected();
+ }
+
+ boolean isBluetoothDeviceAvailable() {
+ return mBluetoothManager.isBluetoothAvailable();
+ }
+
+ private void saveAudioState(AudioState audioState) {
+ mAudioState = audioState;
+ mStatusBarNotifier.notifyMute(mAudioState.isMuted);
+ mStatusBarNotifier.notifySpeakerphone(mAudioState.route == AudioState.ROUTE_SPEAKER);
+ }
+
+ private void onCallUpdated(Call call) {
+ boolean wasNotVoiceCall = mAudioFocusStreamType != AudioManager.STREAM_VOICE_CALL;
+ updateAudioStreamAndMode();
+
+ // If we transition from not voice call to voice call, we need to set an initial state.
+ if (wasNotVoiceCall && mAudioFocusStreamType == AudioManager.STREAM_VOICE_CALL) {
+ setInitialAudioState(call, true /* force */);
+ }
+ }
+
+ private void setSystemAudioState(boolean isMuted, int route, int supportedRouteMask) {
+ setSystemAudioState(false /* force */, isMuted, route, supportedRouteMask);
+ }
+
+ private void setSystemAudioState(
+ boolean force, boolean isMuted, int route, int supportedRouteMask) {
+ if (!hasFocus()) {
+ return;
+ }
+
+ AudioState oldAudioState = mAudioState;
+ saveAudioState(new AudioState(isMuted, route, supportedRouteMask));
+ if (!force && Objects.equals(oldAudioState, mAudioState)) {
+ return;
+ }
+ Log.i(this, "changing audio state from %s to %s", oldAudioState, mAudioState);
+
+ // Mute.
+ if (mAudioState.isMuted != mAudioManager.isMicrophoneMute()) {
+ Log.i(this, "changing microphone mute state to: %b", mAudioState.isMuted);
+ mAudioManager.setMicrophoneMute(mAudioState.isMuted);
+ }
+
+ // Audio route.
+ if (mAudioState.route == AudioState.ROUTE_BLUETOOTH) {
+ turnOnSpeaker(false);
+ turnOnBluetooth(true);
+ } else if (mAudioState.route == AudioState.ROUTE_SPEAKER) {
+ turnOnBluetooth(false);
+ turnOnSpeaker(true);
+ } else if (mAudioState.route == AudioState.ROUTE_EARPIECE ||
+ mAudioState.route == AudioState.ROUTE_WIRED_HEADSET) {
+ turnOnBluetooth(false);
+ turnOnSpeaker(false);
+ }
+
+ if (!oldAudioState.equals(mAudioState)) {
+ CallsManager.getInstance().onAudioStateChanged(oldAudioState, mAudioState);
+ updateAudioForForegroundCall();
+ }
+ }
+
+ private void turnOnSpeaker(boolean on) {
+ // Wired headset and earpiece work the same way
+ if (mAudioManager.isSpeakerphoneOn() != on) {
+ Log.i(this, "turning speaker phone %s", on);
+ mAudioManager.setSpeakerphoneOn(on);
+ }
+ }
+
+ private void turnOnBluetooth(boolean on) {
+ if (mBluetoothManager.isBluetoothAvailable()) {
+ boolean isAlreadyOn = mBluetoothManager.isBluetoothAudioConnectedOrPending();
+ if (on != isAlreadyOn) {
+ Log.i(this, "connecting bluetooth %s", on);
+ if (on) {
+ mBluetoothManager.connectBluetoothAudio();
+ } else {
+ mBluetoothManager.disconnectBluetoothAudio();
+ }
+ }
+ }
+ }
+
+ private void updateAudioStreamAndMode() {
+ Log.v(this, "updateAudioStreamAndMode, mIsRinging: %b, mIsTonePlaying: %b", mIsRinging,
+ mIsTonePlaying);
+ if (mIsRinging) {
+ requestAudioFocusAndSetMode(AudioManager.STREAM_RING, AudioManager.MODE_RINGTONE);
+ } else {
+ Call call = getForegroundCall();
+ if (call != null) {
+ int mode = call.getIsVoipAudioMode() ?
+ AudioManager.MODE_IN_COMMUNICATION : AudioManager.MODE_IN_CALL;
+ requestAudioFocusAndSetMode(AudioManager.STREAM_VOICE_CALL, mode);
+ } else if (mIsTonePlaying) {
+ // There is no call, however, we are still playing a tone, so keep focus.
+ // Since there is no call from which to determine the mode, use the most
+ // recently used mode instead.
+ requestAudioFocusAndSetMode(
+ AudioManager.STREAM_VOICE_CALL, mMostRecentlyUsedMode);
+ } else if (!hasRingingForegroundCall()) {
+ abandonAudioFocus();
+ } else {
+ // mIsRinging is false, but there is a foreground ringing call present. Don't
+ // abandon audio focus immediately to prevent audio focus from getting lost between
+ // the time it takes for the foreground call to transition from RINGING to ACTIVE/
+ // DISCONNECTED. When the call eventually transitions to the next state, audio
+ // focus will be correctly abandoned by the if clause above.
+ }
+ }
+ }
+
+ private void requestAudioFocusAndSetMode(int stream, int mode) {
+ Log.v(this, "requestAudioFocusAndSetMode, stream: %d -> %d", mAudioFocusStreamType, stream);
+ Preconditions.checkState(stream != STREAM_NONE);
+
+ // Even if we already have focus, if the stream is different we update audio manager to give
+ // it a hint about the purpose of our focus.
+ if (mAudioFocusStreamType != stream) {
+ Log.v(this, "requesting audio focus for stream: %d", stream);
+ mAudioManager.requestAudioFocusForCall(stream,
+ AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+ }
+ mAudioFocusStreamType = stream;
+
+ setMode(mode);
+ }
+
+ private void abandonAudioFocus() {
+ if (hasFocus()) {
+ setMode(AudioManager.MODE_NORMAL);
+ Log.v(this, "abandoning audio focus");
+ mAudioManager.abandonAudioFocusForCall();
+ mAudioFocusStreamType = STREAM_NONE;
+ }
+ }
+
+ /**
+ * Sets the audio mode.
+ *
+ * @param newMode Mode constant from AudioManager.MODE_*.
+ */
+ private void setMode(int newMode) {
+ Preconditions.checkState(hasFocus());
+ int oldMode = mAudioManager.getMode();
+ Log.v(this, "Request to change audio mode from %d to %d", oldMode, newMode);
+ if (oldMode != newMode) {
+ mAudioManager.setMode(newMode);
+ mMostRecentlyUsedMode = newMode;
+ }
+ }
+
+ private int selectWiredOrEarpiece(int route, int supportedRouteMask) {
+ // Since they are mutually exclusive and one is ALWAYS valid, we allow a special input of
+ // ROUTE_WIRED_OR_EARPIECE so that callers dont have to make a call to check which is
+ // supported before calling setAudioRoute.
+ if (route == AudioState.ROUTE_WIRED_OR_EARPIECE) {
+ route = AudioState.ROUTE_WIRED_OR_EARPIECE & supportedRouteMask;
+ if (route == 0) {
+ Log.wtf(this, "One of wired headset or earpiece should always be valid.");
+ // assume earpiece in this case.
+ route = AudioState.ROUTE_EARPIECE;
+ }
+ }
+ return route;
+ }
+
+ private int calculateSupportedRoutes() {
+ int routeMask = AudioState.ROUTE_SPEAKER;
+
+ if (mWiredHeadsetManager.isPluggedIn()) {
+ routeMask |= AudioState.ROUTE_WIRED_HEADSET;
+ } else {
+ routeMask |= AudioState.ROUTE_EARPIECE;
+ }
+
+ if (mBluetoothManager.isBluetoothAvailable()) {
+ routeMask |= AudioState.ROUTE_BLUETOOTH;
+ }
+
+ return routeMask;
+ }
+
+ private AudioState getInitialAudioState(Call call) {
+ int supportedRouteMask = calculateSupportedRoutes();
+ int route = selectWiredOrEarpiece(
+ AudioState.ROUTE_WIRED_OR_EARPIECE, supportedRouteMask);
+
+ // We want the UI to indicate that "bluetooth is in use" in two slightly different cases:
+ // (a) The obvious case: if a bluetooth headset is currently in use for an ongoing call.
+ // (b) The not-so-obvious case: if an incoming call is ringing, and we expect that audio
+ // *will* be routed to a bluetooth headset once the call is answered. In this case, just
+ // check if the headset is available. Note this only applies when we are dealing with
+ // the first call.
+ if (call != null && mBluetoothManager.isBluetoothAvailable()) {
+ switch(call.getState()) {
+ case CallState.ACTIVE:
+ case CallState.ON_HOLD:
+ case CallState.DIALING:
+ case CallState.RINGING:
+ route = AudioState.ROUTE_BLUETOOTH;
+ break;
+ default:
+ break;
+ }
+ }
+
+ return new AudioState(false, route, supportedRouteMask);
+ }
+
+ private void setInitialAudioState(Call call, boolean force) {
+ AudioState audioState = getInitialAudioState(call);
+ Log.v(this, "setInitialAudioState %s, %s", audioState, call);
+ setSystemAudioState(
+ force, audioState.isMuted, audioState.route, audioState.supportedRouteMask);
+ }
+
+ private void updateAudioForForegroundCall() {
+ Call call = CallsManager.getInstance().getForegroundCall();
+ if (call != null && call.getConnectionService() != null) {
+ call.getConnectionService().onAudioStateChanged(call, mAudioState);
+ }
+ }
+
+ /**
+ * Returns the current foreground call in order to properly set the audio mode.
+ */
+ private Call getForegroundCall() {
+ Call call = CallsManager.getInstance().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}.
+ if (call != null && call.getState() == CallState.RINGING) {
+ call = null;
+ }
+ return call;
+ }
+
+ private boolean hasRingingForegroundCall() {
+ Call call = CallsManager.getInstance().getForegroundCall();
+ return call != null && call.getState() == CallState.RINGING;
+ }
+
+ private boolean hasFocus() {
+ return mAudioFocusStreamType != STREAM_NONE;
+ }
+}
diff --git a/src/com/android/server/telecom/CallIdMapper.java b/src/com/android/server/telecom/CallIdMapper.java
new file mode 100644
index 0000000..40a50a5
--- /dev/null
+++ b/src/com/android/server/telecom/CallIdMapper.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import com.google.common.collect.HashBiMap;
+
+/** Utility to map {@link Call} objects to unique IDs. IDs are generated when a call is added. */
+class CallIdMapper {
+ private final HashBiMap<String, Call> mCalls = HashBiMap.create();
+ private final String mCallIdPrefix;
+ private static int sIdCount;
+
+ CallIdMapper(String callIdPrefix) {
+ ThreadUtil.checkOnMainThread();
+ mCallIdPrefix = callIdPrefix + "@";
+ }
+
+ void replaceCall(Call newCall, Call callToReplace) {
+ ThreadUtil.checkOnMainThread();
+
+ // Use the old call's ID for the new call.
+ String callId = getCallId(callToReplace);
+ mCalls.put(callId, newCall);
+ }
+
+ void addCall(Call call, String id) {
+ if (call == null) {
+ return;
+ }
+ ThreadUtil.checkOnMainThread();
+ mCalls.put(id, call);
+ }
+
+ void addCall(Call call) {
+ ThreadUtil.checkOnMainThread();
+ addCall(call, getNewId());
+ }
+
+ void removeCall(Call call) {
+ if (call == null) {
+ return;
+ }
+ ThreadUtil.checkOnMainThread();
+ mCalls.inverse().remove(call);
+ }
+
+ void removeCall(String callId) {
+ ThreadUtil.checkOnMainThread();
+ mCalls.remove(callId);
+ }
+
+ String getCallId(Call call) {
+ if (call == null) {
+ return null;
+ }
+ ThreadUtil.checkOnMainThread();
+ return mCalls.inverse().get(call);
+ }
+
+ Call getCall(Object objId) {
+ ThreadUtil.checkOnMainThread();
+
+ String callId = null;
+ if (objId instanceof String) {
+ callId = (String) objId;
+ }
+ if (!isValidCallId(callId) && !isValidConferenceId(callId)) {
+ return null;
+ }
+
+ return mCalls.get(callId);
+ }
+
+ void clear() {
+ mCalls.clear();
+ }
+
+ boolean isValidCallId(String callId) {
+ // Note, no need for thread check, this method is thread safe.
+ return callId != null && callId.startsWith(mCallIdPrefix);
+ }
+
+ boolean isValidConferenceId(String callId) {
+ return callId != null;
+ }
+
+ String getNewId() {
+ sIdCount++;
+ return mCallIdPrefix + sIdCount;
+ }
+}
diff --git a/src/com/android/server/telecom/CallLogManager.java b/src/com/android/server/telecom/CallLogManager.java
new file mode 100644
index 0000000..a29ef54
--- /dev/null
+++ b/src/com/android/server/telecom/CallLogManager.java
@@ -0,0 +1,295 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.CallLog.Calls;
+import android.telecom.CallState;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.VideoProfile;
+import android.telephony.DisconnectCause;
+import android.telephony.PhoneNumberUtils;
+
+import com.android.internal.telephony.CallerInfo;
+import com.android.internal.telephony.PhoneConstants;
+
+/**
+ * Helper class that provides functionality to write information about calls and their associated
+ * caller details to the call log. All logging activity will be performed asynchronously in a
+ * background thread to avoid blocking on the main thread.
+ */
+final class CallLogManager extends CallsManagerListenerBase {
+ /**
+ * Parameter object to hold the arguments to add a call in the call log DB.
+ */
+ private static class AddCallArgs {
+ /**
+ * @param callerInfo Caller details.
+ * @param number The phone number to be logged.
+ * @param presentation Number presentation of the phone number to be logged.
+ * @param callType The type of call (e.g INCOMING_TYPE). @see
+ * {@link android.provider.CallLog} for the list of values.
+ * @param features The features of the call (e.g. FEATURES_VIDEO). @see
+ * {@link android.provider.CallLog} for the list of values.
+ * @param creationDate Time when the call was created (milliseconds since epoch).
+ * @param durationInMillis Duration of the call (milliseconds).
+ * @param dataUsage Data usage in bytes, or null if not applicable.
+ */
+ public AddCallArgs(Context context, CallerInfo callerInfo, String number,
+ int presentation, int callType, int features, PhoneAccountHandle accountHandle,
+ long creationDate, long durationInMillis, Long dataUsage) {
+ this.context = context;
+ this.callerInfo = callerInfo;
+ this.number = number;
+ this.presentation = presentation;
+ this.callType = callType;
+ this.features = features;
+ this.accountHandle = accountHandle;
+ this.timestamp = creationDate;
+ this.durationInSec = (int)(durationInMillis / 1000);
+ this.dataUsage = dataUsage;
+ }
+ // Since the members are accessed directly, we don't use the
+ // mXxxx notation.
+ public final Context context;
+ public final CallerInfo callerInfo;
+ public final String number;
+ public final int presentation;
+ public final int callType;
+ public final int features;
+ public final PhoneAccountHandle accountHandle;
+ public final long timestamp;
+ public final int durationInSec;
+ public final Long dataUsage;
+ }
+
+ private static final String TAG = CallLogManager.class.getSimpleName();
+
+ private final Context mContext;
+
+ public CallLogManager(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public void onCallStateChanged(Call call, int oldState, int newState) {
+ boolean isNewlyDisconnected =
+ newState == CallState.DISCONNECTED || newState == CallState.ABORTED;
+ boolean isCallCanceled = isNewlyDisconnected &&
+ call.getDisconnectCause() == DisconnectCause.OUTGOING_CANCELED;
+
+ // Log newly disconnected calls only if:
+ // 1) It was not in the "choose account" phase when disconnected
+ // 2) It is a conference call
+ // 3) Call was not explicitly canceled
+ if (isNewlyDisconnected &&
+ (oldState != CallState.PRE_DIAL_WAIT &&
+ !call.isConference() &&
+ !isCallCanceled)) {
+ int type;
+ if (!call.isIncoming()) {
+ type = Calls.OUTGOING_TYPE;
+ } else if (oldState == CallState.RINGING) {
+ type = Calls.MISSED_TYPE;
+ } else {
+ type = Calls.INCOMING_TYPE;
+ }
+ logCall(call, type);
+ }
+ }
+
+ /**
+ * Logs a call to the call log based on the {@link Call} object passed in.
+ *
+ * @param call The call object being logged
+ * @param callLogType The type of call log entry to log this call as. See:
+ * {@link android.provider.CallLog.Calls#INCOMING_TYPE}
+ * {@link android.provider.CallLog.Calls#OUTGOING_TYPE}
+ * {@link android.provider.CallLog.Calls#MISSED_TYPE}
+ */
+ private void logCall(Call call, int callLogType) {
+ final long creationTime = call.getCreationTimeMillis();
+ final long age = call.getAgeMillis();
+
+ final String logNumber = getLogNumber(call);
+
+ Log.d(TAG, "logNumber set to: %s", Log.pii(logNumber));
+
+ final int presentation = getPresentation(call);
+ final PhoneAccountHandle accountHandle = call.getTargetPhoneAccount();
+
+ // TODO(vt): Once data usage is available, wire it up here.
+ int callFeatures = getCallFeatures(call.getVideoStateHistory());
+ logCall(call.getCallerInfo(), logNumber, presentation, callLogType, callFeatures,
+ accountHandle, creationTime, age, null);
+ }
+
+ /**
+ * Inserts a call into the call log, based on the parameters passed in.
+ *
+ * @param callerInfo Caller details.
+ * @param number The number the call was made to or from.
+ * @param presentation
+ * @param callType The type of call.
+ * @param features The features of the call.
+ * @param start The start time of the call, in milliseconds.
+ * @param duration The duration of the call, in milliseconds.
+ * @param dataUsage The data usage for the call, null if not applicable.
+ */
+ private void logCall(
+ CallerInfo callerInfo,
+ String number,
+ int presentation,
+ int callType,
+ int features,
+ PhoneAccountHandle accountHandle,
+ long start,
+ long duration,
+ Long dataUsage) {
+ boolean isEmergencyNumber = PhoneNumberUtils.isLocalEmergencyNumber(mContext, number);
+
+ // On some devices, to avoid accidental redialing of emergency numbers, we *never* log
+ // emergency calls to the Call Log. (This behavior is set on a per-product basis, based
+ // on carrier requirements.)
+ final boolean okToLogEmergencyNumber =
+ mContext.getResources().getBoolean(R.bool.allow_emergency_numbers_in_call_log);
+
+ // Don't log emergency numbers if the device doesn't allow it.
+ final boolean isOkToLogThisCall = !isEmergencyNumber || okToLogEmergencyNumber;
+
+ if (isOkToLogThisCall) {
+ Log.d(TAG, "Logging Calllog entry: " + callerInfo + ", "
+ + Log.pii(number) + "," + presentation + ", " + callType
+ + ", " + start + ", " + duration);
+ AddCallArgs args = new AddCallArgs(mContext, callerInfo, number, presentation,
+ callType, features, accountHandle, start, duration, dataUsage);
+ logCallAsync(args);
+ } else {
+ Log.d(TAG, "Not adding emergency call to call log.");
+ }
+ }
+
+ /**
+ * Based on the video state of the call, determines the call features applicable for the call.
+ *
+ * @param videoState The video state.
+ * @return The call features.
+ */
+ private static int getCallFeatures(int videoState) {
+ if ((videoState & VideoProfile.VideoState.TX_ENABLED)
+ == VideoProfile.VideoState.TX_ENABLED) {
+ return Calls.FEATURES_VIDEO;
+ }
+ return 0;
+ }
+
+ /**
+ * Retrieve the phone number from the call, and then process it before returning the
+ * actual number that is to be logged.
+ *
+ * @param call The phone connection.
+ * @return the phone number to be logged.
+ */
+ private String getLogNumber(Call call) {
+ Uri handle = call.getOriginalHandle();
+
+ if (handle == null) {
+ return null;
+ }
+
+ String handleString = handle.getSchemeSpecificPart();
+ if (!PhoneNumberUtils.isUriNumber(handleString)) {
+ handleString = PhoneNumberUtils.stripSeparators(handleString);
+ }
+ return handleString;
+ }
+
+ /**
+ * Gets the presentation from the {@link Call}.
+ *
+ * TODO: There needs to be a way to pass information from
+ * Connection.getNumberPresentation() into a {@link Call} object. Until then, always return
+ * PhoneConstants.PRESENTATION_ALLOWED. On top of that, we might need to introduce
+ * getNumberPresentation to the ContactInfo object as well.
+ *
+ * @param call The call object to retrieve caller details from.
+ * @return The number presentation constant to insert into the call logs.
+ */
+ private int getPresentation(Call call) {
+ return PhoneConstants.PRESENTATION_ALLOWED;
+ }
+
+ /**
+ * Adds the call defined by the parameters in the provided AddCallArgs to the CallLogProvider
+ * using an AsyncTask to avoid blocking the main thread.
+ *
+ * @param args Prepopulated call details.
+ * @return A handle to the AsyncTask that will add the call to the call log asynchronously.
+ */
+ public AsyncTask<AddCallArgs, Void, Uri[]> logCallAsync(AddCallArgs args) {
+ return new LogCallAsyncTask().execute(args);
+ }
+
+ /**
+ * Helper AsyncTask to access the call logs database asynchronously since database operations
+ * can take a long time depending on the system's load. Since it extends AsyncTask, it uses
+ * its own thread pool.
+ */
+ private class LogCallAsyncTask extends AsyncTask<AddCallArgs, Void, Uri[]> {
+ @Override
+ protected Uri[] doInBackground(AddCallArgs... callList) {
+ int count = callList.length;
+ Uri[] result = new Uri[count];
+ for (int i = 0; i < count; i++) {
+ AddCallArgs c = callList[i];
+
+ try {
+ // May block.
+ result[i] = Calls.addCall(c.callerInfo, c.context, c.number, c.presentation,
+ c.callType, c.features, c.accountHandle, c.timestamp, c.durationInSec,
+ c.dataUsage, true /* addForAllUsers */);
+ } catch (Exception e) {
+ // This is very rare but may happen in legitimate cases.
+ // E.g. If the phone is encrypted and thus write request fails, it may cause
+ // some kind of Exception (right now it is IllegalArgumentException, but this
+ // might change).
+ //
+ // We don't want to crash the whole process just because of that, so just log
+ // it instead.
+ Log.e(TAG, e, "Exception raised during adding CallLog entry.");
+ result[i] = null;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Performs a simple sanity check to make sure the call was written in the database.
+ * Typically there is only one result per call so it is easy to identify which one failed.
+ */
+ @Override
+ protected void onPostExecute(Uri[] result) {
+ for (Uri uri : result) {
+ if (uri == null) {
+ Log.w(TAG, "Failed to write call to the log.");
+ }
+ }
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
new file mode 100644
index 0000000..d0aa49f
--- /dev/null
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -0,0 +1,871 @@
+/*
+ * Copyright (C) 2013 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.net.Uri;
+import android.os.Bundle;
+import android.telecom.AudioState;
+import android.telecom.CallState;
+import android.telecom.GatewayInfo;
+import android.telecom.ParcelableConference;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.DisconnectCause;
+import android.telephony.TelephonyManager;
+
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Singleton.
+ *
+ * NOTE: by design most APIs are package private, use the relevant adapter/s to allow
+ * access from other packages specifically refraining from passing the CallsManager instance
+ * beyond the com.android.server.telecom package boundary.
+ */
+public final class CallsManager extends Call.ListenerBase {
+
+ // TODO: Consider renaming this CallsManagerPlugin.
+ interface CallsManagerListener {
+ void onCallAdded(Call call);
+ void onCallRemoved(Call call);
+ void onCallStateChanged(Call call, int oldState, int newState);
+ void onConnectionServiceChanged(
+ Call call,
+ ConnectionServiceWrapper oldService,
+ ConnectionServiceWrapper newService);
+ void onIncomingCallAnswered(Call call);
+ void onIncomingCallRejected(Call call, boolean rejectWithMessage, String textMessage);
+ void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall);
+ void onAudioStateChanged(AudioState oldAudioState, AudioState newAudioState);
+ void onRingbackRequested(Call call, boolean ringback);
+ void onIsConferencedChanged(Call call);
+ void onIsVoipAudioModeChanged(Call call);
+ void onVideoStateChanged(Call call);
+ }
+
+ private static final CallsManager INSTANCE = new CallsManager();
+
+ /**
+ * The main call repository. Keeps an instance of all live calls. New incoming and outgoing
+ * calls are added to the map and removed when the calls move to the disconnected state.
+ *
+ * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
+ * load factor before resizing, 1 means we only expect a single thread to
+ * access the map so make only a single shard
+ */
+ private final Set<Call> mCalls = Collections.newSetFromMap(
+ new ConcurrentHashMap<Call, Boolean>(8, 0.9f, 1));
+
+ private final ConnectionServiceRepository mConnectionServiceRepository =
+ new ConnectionServiceRepository();
+ private final DtmfLocalTonePlayer mDtmfLocalTonePlayer = new DtmfLocalTonePlayer();
+ private final InCallController mInCallController = new InCallController();
+ private final CallAudioManager mCallAudioManager;
+ private final Ringer mRinger;
+ // For this set initial table size to 16 because we add 13 listeners in
+ // the CallsManager constructor.
+ private final Set<CallsManagerListener> mListeners = Collections.newSetFromMap(
+ new ConcurrentHashMap<CallsManagerListener, Boolean>(16, 0.9f, 1));
+ private final HeadsetMediaButton mHeadsetMediaButton;
+ private final WiredHeadsetManager mWiredHeadsetManager;
+ private final TtyManager mTtyManager;
+ private final ProximitySensorManager mProximitySensorManager;
+
+ /**
+ * The call the user is currently interacting with. This is the call that should have audio
+ * focus and be visible in the in-call UI.
+ */
+ private Call mForegroundCall;
+
+ /** Singleton accessor. */
+ static CallsManager getInstance() {
+ return INSTANCE;
+ }
+
+ /**
+ * Initializes the required Telecom components.
+ */
+ private CallsManager() {
+ TelecomApp app = TelecomApp.getInstance();
+
+ StatusBarNotifier statusBarNotifier = new StatusBarNotifier(app, this);
+ mWiredHeadsetManager = new WiredHeadsetManager(app);
+ mCallAudioManager = new CallAudioManager(app, statusBarNotifier, mWiredHeadsetManager);
+ InCallTonePlayer.Factory playerFactory = new InCallTonePlayer.Factory(mCallAudioManager);
+ mRinger = new Ringer(mCallAudioManager, this, playerFactory, app);
+ mHeadsetMediaButton = new HeadsetMediaButton(app, this);
+ mTtyManager = new TtyManager(app, mWiredHeadsetManager);
+ mProximitySensorManager = new ProximitySensorManager(app);
+
+ mListeners.add(statusBarNotifier);
+ mListeners.add(new CallLogManager(app));
+ mListeners.add(new PhoneStateBroadcaster());
+ mListeners.add(mInCallController);
+ mListeners.add(mRinger);
+ mListeners.add(new RingbackPlayer(this, playerFactory));
+ mListeners.add(new InCallToneMonitor(playerFactory, this));
+ mListeners.add(mCallAudioManager);
+ mListeners.add(app.getMissedCallNotifier());
+ mListeners.add(mDtmfLocalTonePlayer);
+ mListeners.add(mHeadsetMediaButton);
+ mListeners.add(RespondViaSmsManager.getInstance());
+ mListeners.add(mProximitySensorManager);
+ }
+
+ @Override
+ public void onSuccessfulOutgoingCall(Call call, int callState) {
+ Log.v(this, "onSuccessfulOutgoingCall, %s", call);
+
+ setCallState(call, callState);
+ if (!mCalls.contains(call)) {
+ // Call was not added previously in startOutgoingCall due to it being a potential MMI
+ // code, so add it now.
+ addCall(call);
+ }
+
+ // The call's ConnectionService has been updated.
+ for (CallsManagerListener listener : mListeners) {
+ listener.onConnectionServiceChanged(call, null, call.getConnectionService());
+ }
+
+ markCallAsDialing(call);
+ }
+
+ @Override
+ public void onFailedOutgoingCall(Call call, int errorCode, String errorMsg) {
+ Log.v(this, "onFailedOutgoingCall, call: %s", call);
+
+ // TODO: Replace disconnect cause with more specific disconnect causes.
+ markCallAsDisconnected(call, errorCode, errorMsg);
+ }
+
+ @Override
+ public void onSuccessfulIncomingCall(Call call) {
+ Log.d(this, "onSuccessfulIncomingCall");
+ setCallState(call, CallState.RINGING);
+ addCall(call);
+ }
+
+ @Override
+ public void onFailedIncomingCall(Call call) {
+ setCallState(call, CallState.DISCONNECTED);
+ call.removeListener(this);
+ }
+
+ @Override
+ public void onRingbackRequested(Call call, boolean ringback) {
+ for (CallsManagerListener listener : mListeners) {
+ listener.onRingbackRequested(call, ringback);
+ }
+ }
+
+ @Override
+ public void onPostDialWait(Call call, String remaining) {
+ mInCallController.onPostDialWait(call, remaining);
+ }
+
+ @Override
+ public void onParentChanged(Call call) {
+ for (CallsManagerListener listener : mListeners) {
+ listener.onIsConferencedChanged(call);
+ }
+ }
+
+ @Override
+ public void onChildrenChanged(Call call) {
+ for (CallsManagerListener listener : mListeners) {
+ listener.onIsConferencedChanged(call);
+ }
+ }
+
+ @Override
+ public void onIsVoipAudioModeChanged(Call call) {
+ for (CallsManagerListener listener : mListeners) {
+ listener.onIsVoipAudioModeChanged(call);
+ }
+ }
+
+ @Override
+ public void onVideoStateChanged(Call call) {
+ for (CallsManagerListener listener : mListeners) {
+ listener.onVideoStateChanged(call);
+ }
+ }
+
+ ImmutableCollection<Call> getCalls() {
+ return ImmutableList.copyOf(mCalls);
+ }
+
+ Call getForegroundCall() {
+ return mForegroundCall;
+ }
+
+ Ringer getRinger() {
+ return mRinger;
+ }
+
+ InCallController getInCallController() {
+ return mInCallController;
+ }
+
+ boolean hasEmergencyCall() {
+ for (Call call : mCalls) {
+ if (call.isEmergencyCall()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ AudioState getAudioState() {
+ return mCallAudioManager.getAudioState();
+ }
+
+ boolean isTtySupported() {
+ return mTtyManager.isTtySupported();
+ }
+
+ int getCurrentTtyMode() {
+ return mTtyManager.getCurrentTtyMode();
+ }
+
+ /**
+ * Starts the process to attach the call to a connection service.
+ *
+ * @param phoneAccountHandle The phone account which contains the component name of the
+ * connection service to use for this call.
+ * @param extras The optional extras Bundle passed with the intent used for the incoming call.
+ */
+ void processIncomingCallIntent(PhoneAccountHandle phoneAccountHandle, Bundle extras) {
+ Log.d(this, "processIncomingCallIntent");
+ Uri handle = extras.getParcelable(TelephonyManager.EXTRA_INCOMING_NUMBER);
+ Call call = new Call(
+ mConnectionServiceRepository,
+ handle,
+ null /* gatewayInfo */,
+ null /* connectionManagerPhoneAccount */,
+ phoneAccountHandle,
+ true /* isIncoming */,
+ false /* isConference */);
+
+ call.setExtras(extras);
+ // TODO: Move this to be a part of addCall()
+ call.addListener(this);
+ call.startCreateConnection();
+ }
+
+ /**
+ * Kicks off the first steps to creating an outgoing call so that InCallUI can launch.
+ *
+ * @param handle Handle to connect the call with.
+ * @param phoneAccountHandle The phone account which contains the component name of the
+ * connection service to use for this call.
+ * @param extras The optional extras Bundle passed with the intent used for the incoming call.
+ */
+ Call startOutgoingCall(Uri handle, PhoneAccountHandle phoneAccountHandle, Bundle extras) {
+ // We only allow a single outgoing call at any given time. Before placing a call, make sure
+ // there doesn't already exist another outgoing call.
+ Call call = getFirstCallWithState(CallState.NEW, CallState.DIALING,
+ CallState.CONNECTING, CallState.PRE_DIAL_WAIT);
+
+ if (call != null) {
+ Log.i(this, "Canceling simultaneous outgoing call.");
+ return null;
+ }
+
+ TelecomApp app = TelecomApp.getInstance();
+
+ // Only dial with the requested phoneAccount if it is still valid. Otherwise treat this call
+ // as if a phoneAccount was not specified (does the default behavior instead).
+ // Note: We will not attempt to dial with a requested phoneAccount if it is disabled.
+ if (phoneAccountHandle != null) {
+ List<PhoneAccountHandle> enabledAccounts =
+ app.getPhoneAccountRegistrar().getEnabledPhoneAccounts(handle.getScheme());
+ if (!enabledAccounts.contains(phoneAccountHandle)) {
+ phoneAccountHandle = null;
+ }
+ }
+
+ if (phoneAccountHandle == null) {
+ // No preset account, check if default exists that supports the URI scheme for the
+ // handle.
+ PhoneAccountHandle defaultAccountHandle =
+ app.getPhoneAccountRegistrar().getDefaultOutgoingPhoneAccount(
+ handle.getScheme());
+ if (defaultAccountHandle != null) {
+ phoneAccountHandle = defaultAccountHandle;
+ }
+ }
+
+ // Create a call with original handle. The handle may be changed when the call is attached
+ // to a connection service, but in most cases will remain the same.
+ call = new Call(
+ mConnectionServiceRepository,
+ handle,
+ null /* gatewayInfo */,
+ null /* connectionManagerPhoneAccount */,
+ phoneAccountHandle,
+ false /* isIncoming */,
+ false /* isConference */);
+ call.setExtras(extras);
+
+ final boolean emergencyCall = TelephonyUtil.shouldProcessAsEmergency(app, call.getHandle());
+ if (phoneAccountHandle == null && !emergencyCall) {
+ // This is the state where the user is expected to select an account
+ call.setState(CallState.PRE_DIAL_WAIT);
+ } else {
+ call.setState(CallState.CONNECTING);
+ }
+
+ if (!isPotentialMMICode(handle)) {
+ addCall(call);
+ } else {
+ call.addListener(this);
+ }
+
+ return call;
+ }
+
+ /**
+ * Attempts to issue/connect the specified call.
+ *
+ * @param handle Handle to connect the call with.
+ * @param gatewayInfo Optional gateway information that can be used to route the call to the
+ * actual dialed handle via a gateway provider. May be null.
+ * @param speakerphoneOn Whether or not to turn the speakerphone on once the call connects.
+ * @param videoState The desired video state for the outgoing call.
+ */
+ void placeOutgoingCall(Call call, Uri handle, GatewayInfo gatewayInfo, boolean speakerphoneOn,
+ int videoState) {
+ if (call == null) {
+ // don't do anything if the call no longer exists
+ Log.i(this, "Canceling unknown call.");
+ return;
+ }
+
+ final Uri uriHandle = (gatewayInfo == null) ? handle : gatewayInfo.getGatewayAddress();
+
+ if (gatewayInfo == null) {
+ Log.i(this, "Creating a new outgoing call with handle: %s", Log.piiHandle(uriHandle));
+ } else {
+ Log.i(this, "Creating a new outgoing call with gateway handle: %s, original handle: %s",
+ Log.pii(uriHandle), Log.pii(handle));
+ }
+
+ call.setHandle(uriHandle);
+ call.setGatewayInfo(gatewayInfo);
+ call.setStartWithSpeakerphoneOn(speakerphoneOn);
+ call.setVideoState(videoState);
+
+ TelecomApp app = TelecomApp.getInstance();
+ final boolean emergencyCall = TelephonyUtil.shouldProcessAsEmergency(app, call.getHandle());
+ if (emergencyCall) {
+ // Emergency -- CreateConnectionProcessor will choose accounts automatically
+ call.setTargetPhoneAccount(null);
+ }
+
+ if (call.getTargetPhoneAccount() != null || emergencyCall) {
+ // If the account has been set, proceed to place the outgoing call.
+ // Otherwise the connection will be initiated when the account is set by the user.
+ call.startCreateConnection();
+ }
+ }
+
+ /**
+ * Attempts to start a conference call for the specified call.
+ *
+ * @param call The call to conference.
+ * @param otherCall The other call to conference with.
+ */
+ void conference(Call call, Call otherCall) {
+ call.conferenceWith(otherCall);
+ }
+
+ /**
+ * Instructs Telecom to answer the specified call. Intended to be invoked by the in-call
+ * app through {@link InCallAdapter} after Telecom notifies it of an incoming call followed by
+ * the user opting to answer said call.
+ *
+ * @param call The call to answer.
+ * @param videoState The video state in which to answer the call.
+ */
+ void answerCall(Call call, int videoState) {
+ if (!mCalls.contains(call)) {
+ Log.i(this, "Request to answer a non-existent call %s", call);
+ } else {
+ // If the foreground call is not the ringing call and it is currently isActive() or
+ // STATE_DIALING, put it on hold before answering the call.
+ if (mForegroundCall != null && mForegroundCall != call &&
+ (mForegroundCall.isActive() ||
+ mForegroundCall.getState() == CallState.DIALING)) {
+ Log.v(this, "Holding active/dialing call %s before answering incoming call %s.",
+ mForegroundCall, call);
+ mForegroundCall.hold();
+ // TODO: Wait until we get confirmation of the active call being
+ // on-hold before answering the new call.
+ // TODO: Import logic from CallManager.acceptCall()
+ }
+
+ for (CallsManagerListener listener : mListeners) {
+ listener.onIncomingCallAnswered(call);
+ }
+
+ // We do not update the UI until we get confirmation of the answer() through
+ // {@link #markCallAsActive}.
+ call.answer(videoState);
+ }
+ }
+
+ /**
+ * Instructs Telecom to reject the specified call. Intended to be invoked by the in-call
+ * app through {@link InCallAdapter} after Telecom notifies it of an incoming call followed by
+ * the user opting to reject said call.
+ */
+ void rejectCall(Call call, boolean rejectWithMessage, String textMessage) {
+ if (!mCalls.contains(call)) {
+ Log.i(this, "Request to reject a non-existent call %s", call);
+ } else {
+ for (CallsManagerListener listener : mListeners) {
+ listener.onIncomingCallRejected(call, rejectWithMessage, textMessage);
+ }
+ call.reject(rejectWithMessage, textMessage);
+ }
+ }
+
+ /**
+ * Instructs Telecom to play the specified DTMF tone within the specified call.
+ *
+ * @param digit The DTMF digit to play.
+ */
+ void playDtmfTone(Call call, char digit) {
+ if (!mCalls.contains(call)) {
+ Log.i(this, "Request to play DTMF in a non-existent call %s", call);
+ } else {
+ call.playDtmfTone(digit);
+ mDtmfLocalTonePlayer.playTone(call, digit);
+ }
+ }
+
+ /**
+ * Instructs Telecom to stop the currently playing DTMF tone, if any.
+ */
+ void stopDtmfTone(Call call) {
+ if (!mCalls.contains(call)) {
+ Log.i(this, "Request to stop DTMF in a non-existent call %s", call);
+ } else {
+ call.stopDtmfTone();
+ mDtmfLocalTonePlayer.stopTone(call);
+ }
+ }
+
+ /**
+ * Instructs Telecom to continue (or not) the current post-dial DTMF string, if any.
+ */
+ void postDialContinue(Call call, boolean proceed) {
+ if (!mCalls.contains(call)) {
+ Log.i(this, "Request to continue post-dial string in a non-existent call %s", call);
+ } else {
+ call.postDialContinue(proceed);
+ }
+ }
+
+ /**
+ * Instructs Telecom to disconnect the specified call. Intended to be invoked by the
+ * in-call app through {@link InCallAdapter} for an ongoing call. This is usually triggered by
+ * the user hitting the end-call button.
+ */
+ void disconnectCall(Call call) {
+ Log.v(this, "disconnectCall %s", call);
+
+ if (!mCalls.contains(call)) {
+ Log.w(this, "Unknown call (%s) asked to disconnect", call);
+ } else {
+ call.disconnect();
+ }
+ }
+
+ /**
+ * Instructs Telecom to disconnect all calls.
+ */
+ void disconnectAllCalls() {
+ Log.v(this, "disconnectAllCalls");
+
+ for (Call call : mCalls) {
+ disconnectCall(call);
+ }
+ }
+
+
+ /**
+ * Instructs Telecom to put the specified call on hold. Intended to be invoked by the
+ * in-call app through {@link InCallAdapter} for an ongoing call. This is usually triggered by
+ * the user hitting the hold button during an active call.
+ */
+ void holdCall(Call call) {
+ if (!mCalls.contains(call)) {
+ Log.w(this, "Unknown call (%s) asked to be put on hold", call);
+ } else {
+ Log.d(this, "Putting call on hold: (%s)", call);
+ call.hold();
+ }
+ }
+
+ /**
+ * Instructs Telecom to release the specified call from hold. Intended to be invoked by
+ * the in-call app through {@link InCallAdapter} for an ongoing call. This is usually triggered
+ * by the user hitting the hold button during a held call.
+ */
+ void unholdCall(Call call) {
+ if (!mCalls.contains(call)) {
+ Log.w(this, "Unknown call (%s) asked to be removed from hold", call);
+ } else {
+ Log.d(this, "unholding call: (%s)", call);
+ for (Call c : mCalls) {
+ if (c != null && c.isAlive() && c != call) {
+ c.hold();
+ }
+ }
+ call.unhold();
+ }
+ }
+
+ /** Called by the in-call UI to change the mute state. */
+ void mute(boolean shouldMute) {
+ mCallAudioManager.mute(shouldMute);
+ }
+
+ /**
+ * Called by the in-call UI to change the audio route, for example to change from earpiece to
+ * speaker phone.
+ */
+ void setAudioRoute(int route) {
+ mCallAudioManager.setAudioRoute(route);
+ }
+
+ /** Called by the in-call UI to turn the proximity sensor on. */
+ void turnOnProximitySensor() {
+ mProximitySensorManager.turnOn();
+ }
+
+ /**
+ * Called by the in-call UI to turn the proximity sensor off.
+ * @param screenOnImmediately If true, the screen will be turned on immediately. Otherwise,
+ * the screen will be kept off until the proximity sensor goes negative.
+ */
+ void turnOffProximitySensor(boolean screenOnImmediately) {
+ mProximitySensorManager.turnOff(screenOnImmediately);
+ }
+
+ void phoneAccountSelected(Call call, PhoneAccountHandle account) {
+ if (!mCalls.contains(call)) {
+ Log.i(this, "Attemped to add account to unknown call %s", call);
+ } else {
+ call.setTargetPhoneAccount(account);
+ call.startCreateConnection();
+ }
+ }
+
+ /** Called when the audio state changes. */
+ void onAudioStateChanged(AudioState oldAudioState, AudioState newAudioState) {
+ Log.v(this, "onAudioStateChanged, audioState: %s -> %s", oldAudioState, newAudioState);
+ for (CallsManagerListener listener : mListeners) {
+ listener.onAudioStateChanged(oldAudioState, newAudioState);
+ }
+ }
+
+ void markCallAsRinging(Call call) {
+ setCallState(call, CallState.RINGING);
+ }
+
+ void markCallAsDialing(Call call) {
+ setCallState(call, CallState.DIALING);
+ }
+
+ void markCallAsActive(Call call) {
+ if (call.getConnectTimeMillis() == 0) {
+ call.setConnectTimeMillis(System.currentTimeMillis());
+ }
+ setCallState(call, CallState.ACTIVE);
+
+ if (call.getStartWithSpeakerphoneOn()) {
+ setAudioRoute(AudioState.ROUTE_SPEAKER);
+ }
+ }
+
+ void markCallAsOnHold(Call call) {
+ setCallState(call, CallState.ON_HOLD);
+ }
+
+ /**
+ * Marks the specified call as STATE_DISCONNECTED and notifies the in-call app. If this was the last
+ * live call, then also disconnect from the in-call controller.
+ *
+ * @param disconnectCause The disconnect reason, see {@link android.telephony.DisconnectCause}.
+ * @param disconnectMessage Optional message about the disconnect.
+ */
+ void markCallAsDisconnected(Call call, int disconnectCause, String disconnectMessage) {
+ call.setDisconnectCause(disconnectCause, disconnectMessage);
+ setCallState(call, CallState.DISCONNECTED);
+ removeCall(call);
+ }
+
+ /**
+ * Removes an existing disconnected call, and notifies the in-call app.
+ */
+ void markCallAsRemoved(Call call) {
+ removeCall(call);
+ }
+
+ /**
+ * Cleans up any calls currently associated with the specified connection service when the
+ * service binder disconnects unexpectedly.
+ *
+ * @param service The connection service that disconnected.
+ */
+ void handleConnectionServiceDeath(ConnectionServiceWrapper service) {
+ if (service != null) {
+ for (Call call : mCalls) {
+ if (call.getConnectionService() == service) {
+ markCallAsDisconnected(call, DisconnectCause.ERROR_UNSPECIFIED, null);
+ }
+ }
+ }
+ }
+
+ boolean hasAnyCalls() {
+ return !mCalls.isEmpty();
+ }
+
+ boolean hasActiveOrHoldingCall() {
+ return getFirstCallWithState(CallState.ACTIVE, CallState.ON_HOLD) != null;
+ }
+
+ boolean hasRingingCall() {
+ return getFirstCallWithState(CallState.RINGING) != null;
+ }
+
+ boolean onMediaButton(int type) {
+ if (hasAnyCalls()) {
+ if (HeadsetMediaButton.SHORT_PRESS == type) {
+ Call ringingCall = getFirstCallWithState(CallState.RINGING);
+ if (ringingCall == null) {
+ mCallAudioManager.toggleMute();
+ return true;
+ } else {
+ ringingCall.answer(ringingCall.getVideoState());
+ return true;
+ }
+ } else if (HeadsetMediaButton.LONG_PRESS == type) {
+ Log.d(this, "handleHeadsetHook: longpress -> hangup");
+ Call callToHangup = getFirstCallWithState(
+ CallState.RINGING, CallState.DIALING, CallState.ACTIVE, CallState.ON_HOLD);
+ if (callToHangup != null) {
+ callToHangup.disconnect();
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Checks to see if the specified call is the only high-level call and if so, enable the
+ * "Add-call" button. We allow you to add a second call but not a third or beyond.
+ *
+ * @param call The call to test for add-call.
+ * @return Whether the add-call feature should be enabled for the call.
+ */
+ protected boolean isAddCallCapable(Call call) {
+ if (call.getParentCall() != null) {
+ // Never true for child calls.
+ return false;
+ }
+
+ // Loop through all the other calls and there exists a top level (has no parent) call
+ // that is not the specified call, return false.
+ for (Call otherCall : mCalls) {
+ if (call != otherCall && otherCall.getParentCall() == null) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns the first call that it finds with the given states. The states are treated as having
+ * priority order so that any call with the first state will be returned before any call with
+ * states listed later in the parameter list.
+ */
+ Call getFirstCallWithState(int... states) {
+ for (int currentState : states) {
+ // check the foreground first
+ if (mForegroundCall != null && mForegroundCall.getState() == currentState) {
+ return mForegroundCall;
+ }
+
+ for (Call call : mCalls) {
+ if (currentState == call.getState()) {
+ return call;
+ }
+ }
+ }
+ return null;
+ }
+
+ Call createConferenceCall(
+ PhoneAccountHandle phoneAccount,
+ ParcelableConference parcelableConference) {
+ Call call = new Call(
+ mConnectionServiceRepository,
+ null /* handle */,
+ null /* gatewayInfo */,
+ null /* connectionManagerPhoneAccount */,
+ phoneAccount,
+ false /* isIncoming */,
+ true /* isConference */);
+
+ setCallState(call, Call.getStateFromConnectionState(parcelableConference.getState()));
+ if (call.getState() == CallState.ACTIVE) {
+ call.setConnectTimeMillis(System.currentTimeMillis());
+ }
+ call.setCallCapabilities(parcelableConference.getCapabilities());
+
+ // TODO: Move this to be a part of addCall()
+ call.addListener(this);
+ addCall(call);
+ return call;
+ }
+
+
+ /**
+ * Adds the specified call to the main list of live calls.
+ *
+ * @param call The call to add.
+ */
+ private void addCall(Call call) {
+ Log.v(this, "addCall(%s)", call);
+
+ call.addListener(this);
+ mCalls.add(call);
+
+ // TODO: Update mForegroundCall prior to invoking
+ // onCallAdded for calls which immediately take the foreground (like the first call).
+ for (CallsManagerListener listener : mListeners) {
+ listener.onCallAdded(call);
+ }
+ updateForegroundCall();
+ }
+
+ private void removeCall(Call call) {
+ Log.v(this, "removeCall(%s)", call);
+
+ call.setParentCall(null); // need to clean up parent relationship before destroying.
+ call.removeListener(this);
+ call.clearConnectionService();
+
+ boolean shouldNotify = false;
+ if (mCalls.contains(call)) {
+ mCalls.remove(call);
+ shouldNotify = true;
+ }
+
+ // Only broadcast changes for calls that are being tracked.
+ if (shouldNotify) {
+ for (CallsManagerListener listener : mListeners) {
+ listener.onCallRemoved(call);
+ }
+ updateForegroundCall();
+ }
+ }
+
+ /**
+ * Sets the specified state on the specified call.
+ *
+ * @param call The call.
+ * @param newState The new state of the call.
+ */
+ private void setCallState(Call call, int newState) {
+ if (call == null) {
+ return;
+ }
+ int oldState = call.getState();
+ Log.i(this, "setCallState %s -> %s, call: %s", CallState.toString(oldState),
+ CallState.toString(newState), call);
+ if (newState != oldState) {
+ // Unfortunately, in the telephony world the radio is king. So if the call notifies
+ // us that the call is in a particular state, we allow it even if it doesn't make
+ // sense (e.g., STATE_ACTIVE -> STATE_RINGING).
+ // TODO: Consider putting a stop to the above and turning CallState
+ // into a well-defined state machine.
+ // TODO: Define expected state transitions here, and log when an
+ // unexpected transition occurs.
+ call.setState(newState);
+
+ // Only broadcast state change for calls that are being tracked.
+ if (mCalls.contains(call)) {
+ for (CallsManagerListener listener : mListeners) {
+ listener.onCallStateChanged(call, oldState, newState);
+ }
+ updateForegroundCall();
+ }
+ }
+ }
+
+ /**
+ * Checks which call should be visible to the user and have audio focus.
+ */
+ private void updateForegroundCall() {
+ Call newForegroundCall = null;
+ for (Call call : mCalls) {
+ // TODO: Foreground-ness needs to be explicitly set. No call, regardless
+ // of its state will be foreground by default and instead the connection service should
+ // be notified when its calls enter and exit foreground state. Foreground will mean that
+ // the call should play audio and listen to microphone if it wants.
+
+ // Active calls have priority.
+ if (call.isActive()) {
+ newForegroundCall = call;
+ break;
+ }
+
+ if (call.isAlive() || call.getState() == CallState.RINGING) {
+ newForegroundCall = call;
+ // Don't break in case there's an active call that has priority.
+ }
+ }
+
+ if (newForegroundCall != mForegroundCall) {
+ Log.v(this, "Updating foreground call, %s -> %s.", mForegroundCall, newForegroundCall);
+ Call oldForegroundCall = mForegroundCall;
+ mForegroundCall = newForegroundCall;
+
+ for (CallsManagerListener listener : mListeners) {
+ listener.onForegroundCallChanged(oldForegroundCall, mForegroundCall);
+ }
+ }
+ }
+
+ private boolean isPotentialMMICode(Uri handle) {
+ return (handle != null && handle.getSchemeSpecificPart() != null
+ && handle.getSchemeSpecificPart().contains("#"));
+ }
+}
diff --git a/src/com/android/server/telecom/CallsManagerListenerBase.java b/src/com/android/server/telecom/CallsManagerListenerBase.java
new file mode 100644
index 0000000..93b2a24
--- /dev/null
+++ b/src/com/android/server/telecom/CallsManagerListenerBase.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.telecom.AudioState;
+
+/**
+ * Provides a default implementation for listeners of CallsManager.
+ */
+class CallsManagerListenerBase implements CallsManager.CallsManagerListener {
+ @Override
+ public void onCallAdded(Call call) {
+ }
+
+ @Override
+ public void onCallRemoved(Call call) {
+ }
+
+ @Override
+ public void onCallStateChanged(Call call, int oldState, int newState) {
+ }
+
+ @Override
+ public void onConnectionServiceChanged(
+ Call call,
+ ConnectionServiceWrapper oldService,
+ ConnectionServiceWrapper newService) {
+ }
+
+ @Override
+ public void onIncomingCallAnswered(Call call) {
+ }
+
+ @Override
+ public void onIncomingCallRejected(Call call, boolean rejectWithMessage, String textMessage) {
+ }
+
+ @Override
+ public void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall) {
+ }
+
+ @Override
+ public void onAudioStateChanged(AudioState oldAudioState, AudioState newAudioState) {
+ }
+
+ @Override
+ public void onRingbackRequested(Call call, boolean ringback) {
+ }
+
+ @Override
+ public void onIsConferencedChanged(Call call) {
+ }
+
+ @Override
+ public void onIsVoipAudioModeChanged(Call call) {
+ }
+
+ @Override
+ public void onVideoStateChanged(Call call) {
+ }
+}
diff --git a/src/com/android/server/telecom/ConnectionServiceRepository.java b/src/com/android/server/telecom/ConnectionServiceRepository.java
new file mode 100644
index 0000000..d7a6d3e
--- /dev/null
+++ b/src/com/android/server/telecom/ConnectionServiceRepository.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2013 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.ComponentName;
+
+import java.util.HashMap;
+
+/**
+ * Searches for and returns connection services.
+ */
+final class ConnectionServiceRepository
+ implements ServiceBinder.Listener<ConnectionServiceWrapper> {
+ private final HashMap<ComponentName, ConnectionServiceWrapper> mServiceCache =
+ new HashMap<ComponentName, ConnectionServiceWrapper>();
+
+ ConnectionServiceRepository() {
+ }
+
+ ConnectionServiceWrapper getService(ComponentName componentName) {
+ ConnectionServiceWrapper service = mServiceCache.get(componentName);
+ if (service == null) {
+ service = new ConnectionServiceWrapper(
+ componentName,
+ this,
+ TelecomApp.getInstance().getPhoneAccountRegistrar());
+ service.addListener(this);
+ mServiceCache.put(componentName, service);
+ }
+ return service;
+ }
+
+ /**
+ * Removes the specified service from the cache when the service unbinds.
+ *
+ * {@inheritDoc}
+ */
+ @Override
+ public void onUnbind(ConnectionServiceWrapper service) {
+ mServiceCache.remove(service.getComponentName());
+ }
+}
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
new file mode 100644
index 0000000..669f9a5
--- /dev/null
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -0,0 +1,1003 @@
+/*
+ * 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;
+
+import android.content.ComponentName;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.RemoteException;
+import android.telecom.AudioState;
+import android.telecom.Connection;
+import android.telecom.ConnectionRequest;
+import android.telecom.ConnectionService;
+import android.telecom.GatewayInfo;
+import android.telecom.ParcelableConference;
+import android.telecom.ParcelableConnection;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.StatusHints;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
+import android.telephony.DisconnectCause;
+
+import com.android.internal.os.SomeArgs;
+import com.android.internal.telecom.IConnectionService;
+import com.android.internal.telecom.IConnectionServiceAdapter;
+import com.android.internal.telecom.IVideoProvider;
+import com.android.internal.telecom.RemoteServiceCallback;
+import com.google.common.base.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Wrapper for {@link IConnectionService}s, handles binding to {@link IConnectionService} and keeps
+ * track of when the object can safely be unbound. Other classes should not use
+ * {@link IConnectionService} directly and instead should use this class to invoke methods of
+ * {@link IConnectionService}.
+ */
+final class ConnectionServiceWrapper extends ServiceBinder<IConnectionService> {
+ private static final int MSG_HANDLE_CREATE_CONNECTION_COMPLETE = 1;
+ private static final int MSG_SET_ACTIVE = 2;
+ private static final int MSG_SET_RINGING = 3;
+ private static final int MSG_SET_DIALING = 4;
+ private static final int MSG_SET_DISCONNECTED = 5;
+ private static final int MSG_SET_ON_HOLD = 6;
+ private static final int MSG_SET_RINGBACK_REQUESTED = 7;
+ private static final int MSG_SET_CALL_CAPABILITIES = 8;
+ private static final int MSG_SET_IS_CONFERENCED = 9;
+ private static final int MSG_ADD_CONFERENCE_CALL = 10;
+ private static final int MSG_REMOVE_CALL = 11;
+ private static final int MSG_ON_POST_DIAL_WAIT = 12;
+ private static final int MSG_QUERY_REMOTE_CALL_SERVICES = 13;
+ private static final int MSG_SET_VIDEO_PROVIDER = 14;
+ private static final int MSG_SET_IS_VOIP_AUDIO_MODE = 15;
+ private static final int MSG_SET_STATUS_HINTS = 16;
+ private static final int MSG_SET_ADDRESS = 17;
+ private static final int MSG_SET_CALLER_DISPLAY_NAME = 18;
+ private static final int MSG_SET_VIDEO_STATE = 19;
+ private static final int MSG_SET_CONFERENCEABLE_CONNECTIONS = 20;
+
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ Call call;
+ switch (msg.what) {
+ case MSG_HANDLE_CREATE_CONNECTION_COMPLETE: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ try {
+ String callId = (String) args.arg1;
+ ConnectionRequest request = (ConnectionRequest) args.arg2;
+ ParcelableConnection connection = (ParcelableConnection) args.arg3;
+ handleCreateConnectionComplete(callId, request, connection);
+ } finally {
+ args.recycle();
+ }
+ break;
+ }
+ case MSG_SET_ACTIVE:
+ call = mCallIdMapper.getCall(msg.obj);
+ if (call != null) {
+ mCallsManager.markCallAsActive(call);
+ } else {
+ //Log.w(this, "setActive, unknown call id: %s", msg.obj);
+ }
+ break;
+ case MSG_SET_RINGING:
+ call = mCallIdMapper.getCall(msg.obj);
+ if (call != null) {
+ mCallsManager.markCallAsRinging(call);
+ } else {
+ //Log.w(this, "setRinging, unknown call id: %s", msg.obj);
+ }
+ break;
+ case MSG_SET_DIALING:
+ call = mCallIdMapper.getCall(msg.obj);
+ if (call != null) {
+ mCallsManager.markCallAsDialing(call);
+ } else {
+ //Log.w(this, "setDialing, unknown call id: %s", msg.obj);
+ }
+ break;
+ case MSG_SET_DISCONNECTED: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ try {
+ call = mCallIdMapper.getCall(args.arg1);
+ String disconnectMessage = (String) args.arg2;
+ int disconnectCause = args.argi1;
+ Log.d(this, "disconnect call %s %s", args.arg1, call);
+ if (call != null) {
+ mCallsManager.markCallAsDisconnected(call, disconnectCause,
+ disconnectMessage);
+ } else {
+ //Log.w(this, "setDisconnected, unknown call id: %s", args.arg1);
+ }
+ } finally {
+ args.recycle();
+ }
+ break;
+ }
+ case MSG_SET_ON_HOLD:
+ call = mCallIdMapper.getCall(msg.obj);
+ if (call != null) {
+ mCallsManager.markCallAsOnHold(call);
+ } else {
+ //Log.w(this, "setOnHold, unknown call id: %s", msg.obj);
+ }
+ break;
+ case MSG_SET_RINGBACK_REQUESTED: {
+ call = mCallIdMapper.getCall(msg.obj);
+ if (call != null) {
+ call.setRingbackRequested(msg.arg1 == 1);
+ } else {
+ //Log.w(this, "setRingback, unknown call id: %s", args.arg1);
+ }
+ break;
+ }
+ case MSG_SET_CALL_CAPABILITIES: {
+ call = mCallIdMapper.getCall(msg.obj);
+ if (call != null) {
+ call.setCallCapabilities(msg.arg1);
+ } else {
+ //Log.w(ConnectionServiceWrapper.this,
+ // "setCallCapabilities, unknown call id: %s", msg.obj);
+ }
+ break;
+ }
+ case MSG_SET_IS_CONFERENCED: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ try {
+ Call childCall = mCallIdMapper.getCall(args.arg1);
+ Log.d(this, "SET_IS_CONFERENCE: %s %s", args.arg1, args.arg2);
+ if (childCall != null) {
+ String conferenceCallId = (String) args.arg2;
+ if (conferenceCallId == null) {
+ Log.d(this, "unsetting parent: %s", args.arg1);
+ childCall.setParentCall(null);
+ } else {
+ Call conferenceCall = mCallIdMapper.getCall(conferenceCallId);
+ childCall.setParentCall(conferenceCall);
+ }
+ } else {
+ //Log.w(this, "setIsConferenced, unknown call id: %s", args.arg1);
+ }
+ } finally {
+ args.recycle();
+ }
+ break;
+ }
+ case MSG_ADD_CONFERENCE_CALL: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ try {
+ String id = (String) args.arg1;
+ if (mCallIdMapper.getCall(id) != null) {
+ Log.w(this, "Attempting to add a conference call using an existing " +
+ "call id %s", id);
+ break;
+ }
+ ParcelableConference parcelableConference =
+ (ParcelableConference) args.arg2;
+ // need to create a new Call
+ Call conferenceCall = mCallsManager.createConferenceCall(
+ null, parcelableConference);
+ mCallIdMapper.addCall(conferenceCall, id);
+ conferenceCall.setConnectionService(ConnectionServiceWrapper.this);
+
+ Log.d(this, "adding children to conference %s",
+ parcelableConference.getConnectionIds());
+ for (String callId : parcelableConference.getConnectionIds()) {
+ Call childCall = mCallIdMapper.getCall(callId);
+ Log.d(this, "found child: %s", callId);
+ if (childCall != null) {
+ childCall.setParentCall(conferenceCall);
+ }
+ }
+ } finally {
+ args.recycle();
+ }
+ break;
+ }
+ case MSG_REMOVE_CALL: {
+ call = mCallIdMapper.getCall(msg.obj);
+ if (call != null) {
+ if (call.isActive()) {
+ mCallsManager.markCallAsDisconnected(
+ call, DisconnectCause.NORMAL, null);
+ } else {
+ mCallsManager.markCallAsRemoved(call);
+ }
+ }
+ break;
+ }
+ case MSG_ON_POST_DIAL_WAIT: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ try {
+ call = mCallIdMapper.getCall(args.arg1);
+ if (call != null) {
+ String remaining = (String) args.arg2;
+ call.onPostDialWait(remaining);
+ } else {
+ //Log.w(this, "onPostDialWait, unknown call id: %s", args.arg1);
+ }
+ } finally {
+ args.recycle();
+ }
+ break;
+ }
+ case MSG_QUERY_REMOTE_CALL_SERVICES: {
+ queryRemoteConnectionServices((RemoteServiceCallback) msg.obj);
+ break;
+ }
+ case MSG_SET_VIDEO_PROVIDER: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ try {
+ call = mCallIdMapper.getCall(args.arg1);
+ IVideoProvider videoProvider = (IVideoProvider) args.arg2;
+ if (call != null) {
+ call.setVideoProvider(videoProvider);
+ }
+ } finally {
+ args.recycle();
+ }
+ break;
+ }
+ case MSG_SET_IS_VOIP_AUDIO_MODE: {
+ call = mCallIdMapper.getCall(msg.obj);
+ if (call != null) {
+ call.setIsVoipAudioMode(msg.arg1 == 1);
+ }
+ break;
+ }
+ case MSG_SET_STATUS_HINTS: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ try {
+ call = mCallIdMapper.getCall(args.arg1);
+ StatusHints statusHints = (StatusHints) args.arg2;
+ if (call != null) {
+ call.setStatusHints(statusHints);
+ }
+ } finally {
+ args.recycle();
+ }
+ break;
+ }
+ case MSG_SET_ADDRESS: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ try {
+ call = mCallIdMapper.getCall(args.arg1);
+ if (call != null) {
+ call.setHandle((Uri) args.arg2, args.argi1);
+ }
+ } finally {
+ args.recycle();
+ }
+ break;
+ }
+ case MSG_SET_CALLER_DISPLAY_NAME: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ try {
+ call = mCallIdMapper.getCall(args.arg1);
+ if (call != null) {
+ call.setCallerDisplayName((String) args.arg2, args.argi1);
+ }
+ } finally {
+ args.recycle();
+ }
+ break;
+ }
+ case MSG_SET_VIDEO_STATE: {
+ call = mCallIdMapper.getCall(msg.obj);
+ if (call != null) {
+ call.setVideoState(msg.arg1);
+ }
+ break;
+ }
+ case MSG_SET_CONFERENCEABLE_CONNECTIONS: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ try {
+ call = mCallIdMapper.getCall(args.arg1);
+ if (call != null ){
+ @SuppressWarnings("unchecked")
+ List<String> conferenceableIds = (List<String>) args.arg2;
+ List<Call> conferenceableCalls =
+ new ArrayList<>(conferenceableIds.size());
+ for (String otherId : (List<String>) args.arg2) {
+ Call otherCall = mCallIdMapper.getCall(otherId);
+ if (otherCall != null && otherCall != call) {
+ conferenceableCalls.add(otherCall);
+ }
+ }
+ call.setConferenceableCalls(conferenceableCalls);
+ }
+ } finally {
+ args.recycle();
+ }
+ break;
+ }
+ }
+ }
+ };
+
+ private final class Adapter extends IConnectionServiceAdapter.Stub {
+
+ @Override
+ public void handleCreateConnectionComplete(
+ String callId,
+ ConnectionRequest request,
+ ParcelableConnection connection) {
+ logIncoming("handleCreateConnectionComplete %s", request);
+ if (mCallIdMapper.isValidCallId(callId)) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = callId;
+ args.arg2 = request;
+ args.arg3 = connection;
+ mHandler.obtainMessage(MSG_HANDLE_CREATE_CONNECTION_COMPLETE, args)
+ .sendToTarget();
+ }
+ }
+
+ @Override
+ public void setActive(String callId) {
+ logIncoming("setActive %s", callId);
+ if (mCallIdMapper.isValidCallId(callId) || mCallIdMapper.isValidConferenceId(callId)) {
+ mHandler.obtainMessage(MSG_SET_ACTIVE, callId).sendToTarget();
+ }
+ }
+
+ @Override
+ public void setRinging(String callId) {
+ logIncoming("setRinging %s", callId);
+ if (mCallIdMapper.isValidCallId(callId)) {
+ mHandler.obtainMessage(MSG_SET_RINGING, callId).sendToTarget();
+ }
+ }
+
+ @Override
+ public void setVideoProvider(String callId, IVideoProvider videoProvider) {
+ logIncoming("setVideoProvider %s", callId);
+ if (mCallIdMapper.isValidCallId(callId)) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = callId;
+ args.arg2 = videoProvider;
+ mHandler.obtainMessage(MSG_SET_VIDEO_PROVIDER, args).sendToTarget();
+ }
+ }
+
+ @Override
+ public void setDialing(String callId) {
+ logIncoming("setDialing %s", callId);
+ if (mCallIdMapper.isValidCallId(callId)) {
+ mHandler.obtainMessage(MSG_SET_DIALING, callId).sendToTarget();
+ }
+ }
+
+ @Override
+ public void setDisconnected(
+ String callId, int disconnectCause, String disconnectMessage) {
+ logIncoming("setDisconnected %s %d %s", callId, disconnectCause, disconnectMessage);
+ if (mCallIdMapper.isValidCallId(callId) || mCallIdMapper.isValidConferenceId(callId)) {
+ Log.d(this, "disconnect call %s", callId);
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = callId;
+ args.arg2 = disconnectMessage;
+ args.argi1 = disconnectCause;
+ mHandler.obtainMessage(MSG_SET_DISCONNECTED, args).sendToTarget();
+ }
+ }
+
+ @Override
+ public void setOnHold(String callId) {
+ logIncoming("setOnHold %s", callId);
+ if (mCallIdMapper.isValidCallId(callId) || mCallIdMapper.isValidConferenceId(callId)) {
+ mHandler.obtainMessage(MSG_SET_ON_HOLD, callId).sendToTarget();
+ }
+ }
+
+ @Override
+ public void setRingbackRequested(String callId, boolean ringback) {
+ logIncoming("setRingbackRequested %s %b", callId, ringback);
+ if (mCallIdMapper.isValidCallId(callId)) {
+ mHandler.obtainMessage(MSG_SET_RINGBACK_REQUESTED, ringback ? 1 : 0, 0, callId)
+ .sendToTarget();
+ }
+ }
+
+ @Override
+ public void removeCall(String callId) {
+ logIncoming("removeCall %s", callId);
+ if (mCallIdMapper.isValidCallId(callId) || mCallIdMapper.isValidConferenceId(callId)) {
+ mHandler.obtainMessage(MSG_REMOVE_CALL, callId).sendToTarget();
+ mHandler.obtainMessage(MSG_REMOVE_CALL, callId);
+ }
+ }
+
+ @Override
+ public void setCallCapabilities(String callId, int callCapabilities) {
+ logIncoming("setCallCapabilities %s %d", callId, callCapabilities);
+ if (mCallIdMapper.isValidCallId(callId) || mCallIdMapper.isValidConferenceId(callId)) {
+ mHandler.obtainMessage(MSG_SET_CALL_CAPABILITIES, callCapabilities, 0, callId)
+ .sendToTarget();
+ } else {
+ Log.w(this, "ID not valid for setCallCapabilities");
+ }
+ }
+
+ @Override
+ public void setIsConferenced(String callId, String conferenceCallId) {
+ logIncoming("setIsConferenced %s %s", callId, conferenceCallId);
+ if (mCallIdMapper.isValidCallId(callId)) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = callId;
+ args.arg2 = conferenceCallId;
+ mHandler.obtainMessage(MSG_SET_IS_CONFERENCED, args).sendToTarget();
+ }
+ }
+
+ @Override
+ public void addConferenceCall(String callId, ParcelableConference parcelableConference) {
+ logIncoming("addConferenceCall %s %s", callId, parcelableConference);
+ // We do not check call Ids here because we do not yet know the call ID for new
+ // conference calls.
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = callId;
+ args.arg2 = parcelableConference;
+ mHandler.obtainMessage(MSG_ADD_CONFERENCE_CALL, args).sendToTarget();
+ }
+
+ @Override
+ public void onPostDialWait(String callId, String remaining) throws RemoteException {
+ logIncoming("onPostDialWait %s %s", callId, remaining);
+ if (mCallIdMapper.isValidCallId(callId)) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = callId;
+ args.arg2 = remaining;
+ mHandler.obtainMessage(MSG_ON_POST_DIAL_WAIT, args).sendToTarget();
+ }
+ }
+
+ @Override
+ public void queryRemoteConnectionServices(RemoteServiceCallback callback) {
+ logIncoming("queryRemoteCSs");
+ mHandler.obtainMessage(MSG_QUERY_REMOTE_CALL_SERVICES, callback).sendToTarget();
+ }
+
+ @Override
+ public void setVideoState(String callId, int videoState) {
+ logIncoming("setVideoState %s %d", callId, videoState);
+ if (mCallIdMapper.isValidCallId(callId)) {
+ mHandler.obtainMessage(MSG_SET_VIDEO_STATE, videoState, 0, callId).sendToTarget();
+ }
+ }
+
+ @Override
+ public void setIsVoipAudioMode(String callId, boolean isVoip) {
+ logIncoming("setIsVoipAudioMode %s %b", callId, isVoip);
+ if (mCallIdMapper.isValidCallId(callId)) {
+ mHandler.obtainMessage(MSG_SET_IS_VOIP_AUDIO_MODE, isVoip ? 1 : 0, 0,
+ callId).sendToTarget();
+ }
+ }
+
+ @Override
+ public void setStatusHints(String callId, StatusHints statusHints) {
+ logIncoming("setStatusHints %s %s", callId, statusHints);
+ if (mCallIdMapper.isValidCallId(callId)) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = callId;
+ args.arg2 = statusHints;
+ mHandler.obtainMessage(MSG_SET_STATUS_HINTS, args).sendToTarget();
+ }
+ }
+
+ @Override
+ public void setAddress(String callId, Uri address, int presentation) {
+ logIncoming("setAddress %s %s %d", callId, address, presentation);
+ if (mCallIdMapper.isValidCallId(callId)) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = callId;
+ args.arg2 = address;
+ args.argi1 = presentation;
+ mHandler.obtainMessage(MSG_SET_ADDRESS, args).sendToTarget();
+ }
+ }
+
+ @Override
+ public void setCallerDisplayName(
+ String callId, String callerDisplayName, int presentation) {
+ logIncoming("setCallerDisplayName %s %s %d", callId, callerDisplayName, presentation);
+ if (mCallIdMapper.isValidCallId(callId)) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = callId;
+ args.arg2 = callerDisplayName;
+ args.argi1 = presentation;
+ mHandler.obtainMessage(MSG_SET_CALLER_DISPLAY_NAME, args).sendToTarget();
+ }
+ }
+
+ @Override
+ public void setConferenceableConnections(
+ String callId, List<String> conferenceableCallIds) {
+ logIncoming("setConferenceableConnections %s %s", callId, conferenceableCallIds);
+ if (mCallIdMapper.isValidCallId(callId)) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = callId;
+ args.arg2 = conferenceableCallIds;
+ mHandler.obtainMessage(MSG_SET_CONFERENCEABLE_CONNECTIONS, args).sendToTarget();
+ }
+ }
+ }
+
+ private final Adapter mAdapter = new Adapter();
+ private final CallsManager mCallsManager = CallsManager.getInstance();
+ /**
+ * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
+ * load factor before resizing, 1 means we only expect a single thread to
+ * access the map so make only a single shard
+ */
+ private final Set<Call> mPendingConferenceCalls = Collections.newSetFromMap(
+ new ConcurrentHashMap<Call, Boolean>(8, 0.9f, 1));
+ private final CallIdMapper mCallIdMapper = new CallIdMapper("ConnectionService");
+ private final Map<String, CreateConnectionResponse> mPendingResponses = new HashMap<>();
+
+ private Binder mBinder = new Binder();
+ private IConnectionService mServiceInterface;
+ private final ConnectionServiceRepository mConnectionServiceRepository;
+
+ /**
+ * Creates a connection service.
+ *
+ * @param componentName The component name of the service with which to bind.
+ * @param connectionServiceRepository Connection service repository.
+ * @param phoneAccountRegistrar Phone account registrar
+ */
+ ConnectionServiceWrapper(
+ ComponentName componentName,
+ ConnectionServiceRepository connectionServiceRepository,
+ PhoneAccountRegistrar phoneAccountRegistrar) {
+ super(ConnectionService.SERVICE_INTERFACE, componentName);
+ mConnectionServiceRepository = connectionServiceRepository;
+ phoneAccountRegistrar.addListener(new PhoneAccountRegistrar.Listener() {
+ // TODO -- Upon changes to PhoneAccountRegistrar, need to re-wire connections
+ // To do this, we must proxy remote ConnectionService objects
+ });
+ }
+
+ /** See {@link IConnectionService#addConnectionServiceAdapter}. */
+ private void addConnectionServiceAdapter(IConnectionServiceAdapter adapter) {
+ if (isServiceValid("addConnectionServiceAdapter")) {
+ try {
+ logOutgoing("addConnectionServiceAdapter %s", adapter);
+ mServiceInterface.addConnectionServiceAdapter(adapter);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ /**
+ * Creates a new connection for a new outgoing call or to attach to an existing incoming call.
+ */
+ void createConnection(final Call call, final CreateConnectionResponse response) {
+ Log.d(this, "createConnection(%s) via %s.", call, getComponentName());
+ BindCallback callback = new BindCallback() {
+ @Override
+ public void onSuccess() {
+ String callId = mCallIdMapper.getCallId(call);
+ mPendingResponses.put(callId, response);
+
+ GatewayInfo gatewayInfo = call.getGatewayInfo();
+ Bundle extras = call.getExtras();
+ if (gatewayInfo != null && gatewayInfo.getGatewayProviderPackageName() != null &&
+ gatewayInfo.getOriginalAddress() != null) {
+ extras = (Bundle) extras.clone();
+ extras.putString(
+ TelecomManager.GATEWAY_PROVIDER_PACKAGE,
+ gatewayInfo.getGatewayProviderPackageName());
+ extras.putParcelable(
+ TelecomManager.GATEWAY_ORIGINAL_ADDRESS,
+ gatewayInfo.getOriginalAddress());
+ }
+
+ try {
+ mServiceInterface.createConnection(
+ call.getConnectionManagerPhoneAccount(),
+ callId,
+ new ConnectionRequest(
+ call.getTargetPhoneAccount(),
+ call.getHandle(),
+ extras,
+ call.getVideoState()),
+ call.isIncoming());
+ } catch (RemoteException e) {
+ Log.e(this, e, "Failure to createConnection -- %s", getComponentName());
+ mPendingResponses.remove(callId).handleCreateConnectionFailure(
+ DisconnectCause.OUTGOING_FAILURE, e.toString());
+ }
+ }
+
+ @Override
+ public void onFailure() {
+ Log.e(this, new Exception(), "Failure to call %s", getComponentName());
+ response.handleCreateConnectionFailure(DisconnectCause.OUTGOING_FAILURE, null);
+ }
+ };
+
+ mBinder.bind(callback);
+ }
+
+ /** @see ConnectionService#abort(String) */
+ void abort(Call call) {
+ // Clear out any pending outgoing call data
+ final String callId = mCallIdMapper.getCallId(call);
+
+ // If still bound, tell the connection service to abort.
+ if (callId != null && isServiceValid("abort")) {
+ try {
+ logOutgoing("abort %s", callId);
+ mServiceInterface.abort(callId);
+ } catch (RemoteException e) {
+ }
+ }
+
+ removeCall(call, DisconnectCause.LOCAL, null);
+ }
+
+ /** @see ConnectionService#hold(String) */
+ void hold(Call call) {
+ final String callId = mCallIdMapper.getCallId(call);
+ if (callId != null && isServiceValid("hold")) {
+ try {
+ logOutgoing("hold %s", callId);
+ mServiceInterface.hold(callId);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ /** @see ConnectionService#unhold(String) */
+ void unhold(Call call) {
+ final String callId = mCallIdMapper.getCallId(call);
+ if (callId != null && isServiceValid("unhold")) {
+ try {
+ logOutgoing("unhold %s", callId);
+ mServiceInterface.unhold(callId);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ /** @see ConnectionService#onAudioStateChanged(String,AudioState) */
+ void onAudioStateChanged(Call activeCall, AudioState audioState) {
+ final String callId = mCallIdMapper.getCallId(activeCall);
+ if (callId != null && isServiceValid("onAudioStateChanged")) {
+ try {
+ logOutgoing("onAudioStateChanged %s %s", callId, audioState);
+ mServiceInterface.onAudioStateChanged(callId, audioState);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ /** @see ConnectionService#disconnect(String) */
+ void disconnect(Call call) {
+ final String callId = mCallIdMapper.getCallId(call);
+ if (callId != null && isServiceValid("disconnect")) {
+ try {
+ logOutgoing("disconnect %s", callId);
+ mServiceInterface.disconnect(callId);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ /** @see ConnectionService#answer(String,int) */
+ void answer(Call call, int videoState) {
+ final String callId = mCallIdMapper.getCallId(call);
+ if (callId != null && isServiceValid("answer")) {
+ try {
+ logOutgoing("answer %s %d", callId, videoState);
+ if (videoState == VideoProfile.VideoState.AUDIO_ONLY) {
+ mServiceInterface.answer(callId);
+ } else {
+ mServiceInterface.answerVideo(callId, videoState);
+ }
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ /** @see ConnectionService#reject(String) */
+ void reject(Call call) {
+ final String callId = mCallIdMapper.getCallId(call);
+ if (callId != null && isServiceValid("reject")) {
+ try {
+ logOutgoing("reject %s", callId);
+ mServiceInterface.reject(callId);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ /** @see ConnectionService#playDtmfTone(String,char) */
+ void playDtmfTone(Call call, char digit) {
+ final String callId = mCallIdMapper.getCallId(call);
+ if (callId != null && isServiceValid("playDtmfTone")) {
+ try {
+ logOutgoing("playDtmfTone %s %c", callId, digit);
+ mServiceInterface.playDtmfTone(callId, digit);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ /** @see ConnectionService#stopDtmfTone(String) */
+ void stopDtmfTone(Call call) {
+ final String callId = mCallIdMapper.getCallId(call);
+ if (callId != null && isServiceValid("stopDtmfTone")) {
+ try {
+ logOutgoing("stopDtmfTone %s",callId);
+ mServiceInterface.stopDtmfTone(callId);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ void addCall(Call call) {
+ if (mCallIdMapper.getCallId(call) == null) {
+ mCallIdMapper.addCall(call);
+ }
+ }
+
+ /**
+ * Associates newCall with this connection service by replacing callToReplace.
+ */
+ void replaceCall(Call newCall, Call callToReplace) {
+ Preconditions.checkState(callToReplace.getConnectionService() == this);
+ mCallIdMapper.replaceCall(newCall, callToReplace);
+ }
+
+ void removeCall(Call call) {
+ removeCall(call, DisconnectCause.ERROR_UNSPECIFIED, null);
+ }
+
+ void removeCall(String callId, int disconnectCause, String disconnectMessage) {
+ CreateConnectionResponse response = mPendingResponses.remove(callId);
+ if (response != null) {
+ response.handleCreateConnectionFailure(disconnectCause, disconnectMessage);
+ }
+
+ mCallIdMapper.removeCall(callId);
+ }
+
+ void removeCall(Call call, int disconnectCause, String disconnectMessage) {
+ CreateConnectionResponse response = mPendingResponses.remove(mCallIdMapper.getCallId(call));
+ if (response != null) {
+ response.handleCreateConnectionFailure(disconnectCause, disconnectMessage);
+ }
+
+ mCallIdMapper.removeCall(call);
+ }
+
+ void onPostDialContinue(Call call, boolean proceed) {
+ final String callId = mCallIdMapper.getCallId(call);
+ if (callId != null && isServiceValid("onPostDialContinue")) {
+ try {
+ logOutgoing("onPostDialContinue %s %b", callId, proceed);
+ mServiceInterface.onPostDialContinue(callId, proceed);
+ } catch (RemoteException ignored) {
+ }
+ }
+ }
+
+ void conference(final Call call, Call otherCall) {
+ final String callId = mCallIdMapper.getCallId(call);
+ final String otherCallId = mCallIdMapper.getCallId(otherCall);
+ if (callId != null && otherCallId != null && isServiceValid("conference")) {
+ try {
+ logOutgoing("conference %s %s", callId, otherCallId);
+ mServiceInterface.conference(callId, otherCallId);
+ } catch (RemoteException ignored) {
+ }
+ }
+ }
+
+ void splitFromConference(Call call) {
+ final String callId = mCallIdMapper.getCallId(call);
+ if (callId != null && isServiceValid("splitFromConference")) {
+ try {
+ logOutgoing("splitFromConference %s", callId);
+ mServiceInterface.splitFromConference(callId);
+ } catch (RemoteException ignored) {
+ }
+ }
+ }
+
+ void mergeConference(Call call) {
+ final String callId = mCallIdMapper.getCallId(call);
+ if (callId != null && isServiceValid("mergeConference")) {
+ try {
+ logOutgoing("mergeConference %s", callId);
+ mServiceInterface.mergeConference(callId);
+ } catch (RemoteException ignored) {
+ }
+ }
+ }
+
+ void swapConference(Call call) {
+ final String callId = mCallIdMapper.getCallId(call);
+ if (callId != null && isServiceValid("swapConference")) {
+ try {
+ logOutgoing("swapConference %s", callId);
+ mServiceInterface.swapConference(callId);
+ } catch (RemoteException ignored) {
+ }
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void setServiceInterface(IBinder binder) {
+ if (binder == null) {
+ // We have lost our service connection. Notify the world that this service is done.
+ // We must notify the adapter before CallsManager. The adapter will force any pending
+ // 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();
+ CallsManager.getInstance().handleConnectionServiceDeath(this);
+ mServiceInterface = null;
+ } else {
+ mServiceInterface = IConnectionService.Stub.asInterface(binder);
+ addConnectionServiceAdapter(mAdapter);
+ }
+ }
+
+ private void handleCreateConnectionComplete(
+ String callId,
+ ConnectionRequest request,
+ ParcelableConnection connection) {
+ // TODO: Note we are not using parameter "request", which is a side effect of our tacit
+ // assumption that we have at most one outgoing connection attempt per ConnectionService.
+ // This may not continue to be the case.
+ if (connection.getState() == Connection.STATE_DISCONNECTED) {
+ // A connection that begins in the DISCONNECTED state is an indication of
+ // failure to connect; we handle all failures uniformly
+ removeCall(callId, connection.getDisconnectCause(), connection.getDisconnectMessage());
+ } else {
+ // Successful connection
+ if (mPendingResponses.containsKey(callId)) {
+ mPendingResponses.remove(callId)
+ .handleCreateConnectionSuccess(mCallIdMapper, connection);
+ }
+ }
+ }
+
+ /**
+ * Called when the associated connection service dies.
+ */
+ private void handleConnectionServiceDeath() {
+ if (!mPendingResponses.isEmpty()) {
+ CreateConnectionResponse[] responses = mPendingResponses.values().toArray(
+ new CreateConnectionResponse[mPendingResponses.values().size()]);
+ mPendingResponses.clear();
+ for (int i = 0; i < responses.length; i++) {
+ responses[i].handleCreateConnectionFailure(DisconnectCause.ERROR_UNSPECIFIED, null);
+ }
+ }
+ mCallIdMapper.clear();
+ }
+
+ private void logIncoming(String msg, Object... params) {
+ Log.d(this, "ConnectionService -> Telecom: " + msg, params);
+ }
+
+ private void logOutgoing(String msg, Object... params) {
+ Log.d(this, "Telecom -> ConnectionService: " + msg, params);
+ }
+
+ private void queryRemoteConnectionServices(final RemoteServiceCallback callback) {
+ PhoneAccountRegistrar registrar = TelecomApp.getInstance().getPhoneAccountRegistrar();
+
+ // Only give remote connection services to this connection service if it is listed as
+ // the connection manager.
+ PhoneAccountHandle simCallManager = registrar.getSimCallManager();
+ Log.d(this, "queryRemoteConnectionServices finds simCallManager = %s", simCallManager);
+ if (simCallManager == null ||
+ !simCallManager.getComponentName().equals(getComponentName())) {
+ noRemoteServices(callback);
+ return;
+ }
+
+ // Make a list of ConnectionServices that are listed as being associated with SIM accounts
+ final Set<ConnectionServiceWrapper> simServices = Collections.newSetFromMap(
+ new ConcurrentHashMap<ConnectionServiceWrapper, Boolean>(8, 0.9f, 1));
+ for (PhoneAccountHandle handle : registrar.getEnabledPhoneAccounts()) {
+ PhoneAccount account = registrar.getPhoneAccount(handle);
+ if ((account.getCapabilities() & PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION) != 0) {
+ ConnectionServiceWrapper service =
+ mConnectionServiceRepository.getService(handle.getComponentName());
+ if (service != null) {
+ simServices.add(service);
+ }
+ }
+ }
+
+ final List<ComponentName> simServiceComponentNames = new ArrayList<>();
+ final List<IBinder> simServiceBinders = new ArrayList<>();
+
+ Log.v(this, "queryRemoteConnectionServices, simServices = %s", simServices);
+
+ for (ConnectionServiceWrapper simService : simServices) {
+ if (simService == this) {
+ // Only happens in the unlikely case that a SIM service is also a SIM call manager
+ continue;
+ }
+
+ final ConnectionServiceWrapper currentSimService = simService;
+
+ currentSimService.mBinder.bind(new BindCallback() {
+ @Override
+ public void onSuccess() {
+ Log.d(this, "Adding simService %s", currentSimService.getComponentName());
+ simServiceComponentNames.add(currentSimService.getComponentName());
+ simServiceBinders.add(currentSimService.mServiceInterface.asBinder());
+ maybeComplete();
+ }
+
+ @Override
+ public void onFailure() {
+ Log.d(this, "Failed simService %s", currentSimService.getComponentName());
+ // We know maybeComplete() will always be a no-op from now on, so go ahead and
+ // signal failure of the entire request
+ noRemoteServices(callback);
+ }
+
+ private void maybeComplete() {
+ if (simServiceComponentNames.size() == simServices.size()) {
+ setRemoteServices(callback, simServiceComponentNames, simServiceBinders);
+ }
+ }
+ });
+ }
+ }
+
+ private void setRemoteServices(
+ RemoteServiceCallback callback,
+ List<ComponentName> componentNames,
+ List<IBinder> binders) {
+ try {
+ callback.onResult(componentNames, binders);
+ } catch (RemoteException e) {
+ Log.e(this, e, "Contacting ConnectionService %s",
+ ConnectionServiceWrapper.this.getComponentName());
+ }
+ }
+
+ 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());
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/Constants.java b/src/com/android/server/telecom/Constants.java
new file mode 100644
index 0000000..bc6e350
--- /dev/null
+++ b/src/com/android/server/telecom/Constants.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+/**
+ * App-wide constants for the phone app.
+ *
+ * Any constants that need to be shared between two or more classes within
+ * the com.android.phone package should be defined here. (Constants that
+ * are private to only one class can go in that class's .java file.)
+ */
+public class Constants {
+ //
+ // URI schemes
+ //
+
+ public static final String SCHEME_SMSTO = "smsto";
+}
diff --git a/src/com/android/server/telecom/ContactsAsyncHelper.java b/src/com/android/server/telecom/ContactsAsyncHelper.java
new file mode 100644
index 0000000..aa99ac3
--- /dev/null
+++ b/src/com/android/server/telecom/ContactsAsyncHelper.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.app.Notification;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Helper class for loading contacts photo asynchronously.
+ */
+public final class ContactsAsyncHelper {
+ private static final String LOG_TAG = ContactsAsyncHelper.class.getSimpleName();
+
+ /**
+ * Interface for a WorkerHandler result return.
+ */
+ public interface OnImageLoadCompleteListener {
+ /**
+ * Called when the image load is complete.
+ *
+ * @param token Integer passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int,
+ * Context, Uri, OnImageLoadCompleteListener, Object)}.
+ * @param photo Drawable object obtained by the async load.
+ * @param photoIcon Bitmap object obtained by the async load.
+ * @param cookie Object passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int,
+ * Context, Uri, OnImageLoadCompleteListener, Object)}. Can be null iff. the original
+ * cookie is null.
+ */
+ public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon,
+ Object cookie);
+ }
+
+ // constants
+ private static final int EVENT_LOAD_IMAGE = 1;
+
+ private static final Handler sResultHandler = new Handler(Looper.getMainLooper()) {
+ /** Called when loading is done. */
+ @Override
+ public void handleMessage(Message msg) {
+ WorkerArgs args = (WorkerArgs) msg.obj;
+ switch (msg.arg1) {
+ case EVENT_LOAD_IMAGE:
+ if (args.listener != null) {
+ Log.d(this, "Notifying listener: " + args.listener.toString() +
+ " image: " + args.displayPhotoUri + " completed");
+ args.listener.onImageLoadComplete(msg.what, args.photo, args.photoIcon,
+ args.cookie);
+ }
+ break;
+ default:
+ }
+ }
+ };
+
+ /** Handler run on a worker thread to load photo asynchronously. */
+ private static final Handler sThreadHandler;
+
+ static {
+ HandlerThread thread = new HandlerThread("ContactsAsyncWorker");
+ thread.start();
+ sThreadHandler = new WorkerHandler(thread.getLooper());
+ }
+
+ private ContactsAsyncHelper() {}
+
+ private static final class WorkerArgs {
+ public Context context;
+ public Uri displayPhotoUri;
+ public Drawable photo;
+ public Bitmap photoIcon;
+ public Object cookie;
+ public OnImageLoadCompleteListener listener;
+ }
+
+ /**
+ * Thread worker class that handles the task of opening the stream and loading
+ * the images.
+ */
+ private static class WorkerHandler extends Handler {
+ public WorkerHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ WorkerArgs args = (WorkerArgs) msg.obj;
+
+ switch (msg.arg1) {
+ case EVENT_LOAD_IMAGE:
+ InputStream inputStream = null;
+ try {
+ try {
+ inputStream = args.context.getContentResolver()
+ .openInputStream(args.displayPhotoUri);
+ } catch (Exception e) {
+ Log.e(this, e, "Error opening photo input stream");
+ }
+
+ if (inputStream != null) {
+ args.photo = Drawable.createFromStream(inputStream,
+ args.displayPhotoUri.toString());
+
+ // This assumes Drawable coming from contact database is usually
+ // BitmapDrawable and thus we can have (down)scaled version of it.
+ args.photoIcon = getPhotoIconWhenAppropriate(args.context, args.photo);
+
+ Log.d(this, "Loading image: " + msg.arg1 +
+ " token: " + msg.what + " image URI: " + args.displayPhotoUri);
+ } else {
+ args.photo = null;
+ args.photoIcon = null;
+ Log.d(this, "Problem with image: " + msg.arg1 +
+ " token: " + msg.what + " image URI: " + args.displayPhotoUri +
+ ", using default image.");
+ }
+ } finally {
+ if (inputStream != null) {
+ try {
+ inputStream.close();
+ } catch (IOException e) {
+ Log.e(this, e, "Unable to close input stream.");
+ }
+ }
+ }
+ break;
+ default:
+ }
+
+ // send the reply to the enclosing class.
+ Message reply = sResultHandler.obtainMessage(msg.what);
+ reply.arg1 = msg.arg1;
+ reply.obj = msg.obj;
+ reply.sendToTarget();
+ }
+
+ /**
+ * Returns a Bitmap object suitable for {@link Notification}'s large icon. This might
+ * return null when the given Drawable isn't BitmapDrawable, or if the system fails to
+ * create a scaled Bitmap for the Drawable.
+ */
+ private Bitmap getPhotoIconWhenAppropriate(Context context, Drawable photo) {
+ if (!(photo instanceof BitmapDrawable)) {
+ return null;
+ }
+ int iconSize = context.getResources()
+ .getDimensionPixelSize(R.dimen.notification_icon_size);
+ Bitmap orgBitmap = ((BitmapDrawable) photo).getBitmap();
+ int orgWidth = orgBitmap.getWidth();
+ int orgHeight = orgBitmap.getHeight();
+ int longerEdge = orgWidth > orgHeight ? orgWidth : orgHeight;
+ // We want downscaled one only when the original icon is too big.
+ if (longerEdge > iconSize) {
+ float ratio = ((float) longerEdge) / iconSize;
+ int newWidth = (int) (orgWidth / ratio);
+ int newHeight = (int) (orgHeight / ratio);
+ // If the longer edge is much longer than the shorter edge, the latter may
+ // become 0 which will cause a crash.
+ if (newWidth <= 0 || newHeight <= 0) {
+ Log.w(this, "Photo icon's width or height become 0.");
+ return null;
+ }
+
+ // It is sure ratio >= 1.0f in any case and thus the newly created Bitmap
+ // should be smaller than the original.
+ return Bitmap.createScaledBitmap(orgBitmap, newWidth, newHeight, true);
+ } else {
+ return orgBitmap;
+ }
+ }
+ }
+
+ /**
+ * Starts an asynchronous image load. After finishing the load,
+ * {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)}
+ * will be called.
+ *
+ * @param token Arbitrary integer which will be returned as the first argument of
+ * {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)}
+ * @param context Context object used to do the time-consuming operation.
+ * @param displayPhotoUri Uri to be used to fetch the photo
+ * @param listener Callback object which will be used when the asynchronous load is done.
+ * Can be null, which means only the asynchronous load is done while there's no way to
+ * obtain the loaded photos.
+ * @param cookie Arbitrary object the caller wants to remember, which will become the
+ * fourth argument of {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable,
+ * Bitmap, Object)}. Can be null, at which the callback will also has null for the argument.
+ */
+ public static final void startObtainPhotoAsync(int token, Context context, Uri displayPhotoUri,
+ OnImageLoadCompleteListener listener, Object cookie) {
+ ThreadUtil.checkOnMainThread();
+
+ // in case the source caller info is null, the URI will be null as well.
+ // just update using the placeholder image in this case.
+ if (displayPhotoUri == null) {
+ Log.wtf(LOG_TAG, "Uri is missing");
+ return;
+ }
+
+ // Added additional Cookie field in the callee to handle arguments
+ // sent to the callback function.
+
+ // setup arguments
+ WorkerArgs args = new WorkerArgs();
+ args.cookie = cookie;
+ args.context = context;
+ args.displayPhotoUri = displayPhotoUri;
+ args.listener = listener;
+
+ // setup message arguments
+ Message msg = sThreadHandler.obtainMessage(token);
+ msg.arg1 = EVENT_LOAD_IMAGE;
+ msg.obj = args;
+
+ Log.d(LOG_TAG, "Begin loading image: " + args.displayPhotoUri +
+ ", displaying default image for now.");
+
+ // notify the thread to begin working
+ sThreadHandler.sendMessage(msg);
+ }
+
+
+}
diff --git a/src/com/android/server/telecom/CreateConnectionProcessor.java b/src/com/android/server/telecom/CreateConnectionProcessor.java
new file mode 100644
index 0000000..e4c5463
--- /dev/null
+++ b/src/com/android/server/telecom/CreateConnectionProcessor.java
@@ -0,0 +1,307 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.telecom.ParcelableConnection;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.DisconnectCause;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * This class creates connections to place new outgoing calls to attached to an existing incoming
+ * call. In either case, this class cycles through a set of connection services until:
+ * - a connection service returns a newly created connection in which case the call is displayed
+ * to the user
+ * - a connection service cancels the process, in which case the call is aborted
+ */
+final class CreateConnectionProcessor {
+
+ // Describes information required to attempt to make a phone call
+ private static class CallAttemptRecord {
+ // The PhoneAccount describing the target connection service which we will
+ // contact in order to process an attempt
+ public final PhoneAccountHandle connectionManagerPhoneAccount;
+ // The PhoneAccount which we will tell the target connection service to use
+ // for attempting to make the actual phone call
+ public final PhoneAccountHandle targetPhoneAccount;
+
+ public CallAttemptRecord(
+ PhoneAccountHandle connectionManagerPhoneAccount,
+ PhoneAccountHandle targetPhoneAccount) {
+ this.connectionManagerPhoneAccount = connectionManagerPhoneAccount;
+ this.targetPhoneAccount = targetPhoneAccount;
+ }
+
+ @Override
+ public String toString() {
+ return "CallAttemptRecord("
+ + Objects.toString(connectionManagerPhoneAccount) + ","
+ + Objects.toString(targetPhoneAccount) + ")";
+ }
+
+ /**
+ * Determines if this instance of {@code CallAttemptRecord} has the same underlying
+ * {@code PhoneAccountHandle}s as another instance.
+ *
+ * @param obj The other instance to compare against.
+ * @return {@code True} if the {@code CallAttemptRecord}s are equal.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof CallAttemptRecord) {
+ CallAttemptRecord other = (CallAttemptRecord) obj;
+ return Objects.equals(connectionManagerPhoneAccount,
+ other.connectionManagerPhoneAccount) &&
+ Objects.equals(targetPhoneAccount, other.targetPhoneAccount);
+ }
+ return false;
+ }
+ }
+
+ private final Call mCall;
+ private final ConnectionServiceRepository mRepository;
+ private List<CallAttemptRecord> mAttemptRecords;
+ private Iterator<CallAttemptRecord> mAttemptRecordIterator;
+ private CreateConnectionResponse mResponse;
+ private int mLastErrorCode = DisconnectCause.OUTGOING_FAILURE;
+ private String mLastErrorMsg;
+
+ CreateConnectionProcessor(
+ Call call, ConnectionServiceRepository repository, CreateConnectionResponse response) {
+ mCall = call;
+ mRepository = repository;
+ mResponse = response;
+ }
+
+ void process() {
+ Log.v(this, "process");
+ mAttemptRecords = new ArrayList<>();
+ if (mCall.getTargetPhoneAccount() != null) {
+ mAttemptRecords.add(new CallAttemptRecord(
+ mCall.getTargetPhoneAccount(), mCall.getTargetPhoneAccount()));
+ }
+ adjustAttemptsForConnectionManager();
+ adjustAttemptsForEmergency();
+ mAttemptRecordIterator = mAttemptRecords.iterator();
+ attemptNextPhoneAccount();
+ }
+
+ void abort() {
+ Log.v(this, "abort");
+
+ // Clear the response first to prevent attemptNextConnectionService from attempting any
+ // more services.
+ CreateConnectionResponse response = mResponse;
+ mResponse = null;
+
+ ConnectionServiceWrapper service = mCall.getConnectionService();
+ if (service != null) {
+ service.abort(mCall);
+ mCall.clearConnectionService();
+ }
+ if (response != null) {
+ response.handleCreateConnectionFailure(DisconnectCause.OUTGOING_CANCELED, null);
+ }
+ }
+
+ private void attemptNextPhoneAccount() {
+ Log.v(this, "attemptNextPhoneAccount");
+ PhoneAccountRegistrar registrar = TelecomApp.getInstance().getPhoneAccountRegistrar();
+ CallAttemptRecord attempt = null;
+ if (mAttemptRecordIterator.hasNext()) {
+ attempt = mAttemptRecordIterator.next();
+
+ if (!registrar.phoneAccountHasPermission(attempt.connectionManagerPhoneAccount)) {
+ Log.w(this,
+ "Connection mgr does not have BIND_CONNECTION_SERVICE for attempt: %s",
+ attempt);
+ attemptNextPhoneAccount();
+ return;
+ }
+
+ // If the target PhoneAccount differs from the ConnectionManager phone acount, ensure it
+ // also has BIND_CONNECTION_SERVICE permission.
+ if (!attempt.connectionManagerPhoneAccount.equals(attempt.targetPhoneAccount) &&
+ !registrar.phoneAccountHasPermission(attempt.targetPhoneAccount)) {
+ Log.w(this,
+ "Target PhoneAccount does not have BIND_CONNECTION_SERVICE for attempt: %s",
+ attempt);
+ attemptNextPhoneAccount();
+ return;
+ }
+ }
+
+ if (mResponse != null && attempt != null) {
+ Log.i(this, "Trying attempt %s", attempt);
+ ConnectionServiceWrapper service =
+ mRepository.getService(
+ attempt.connectionManagerPhoneAccount.getComponentName());
+ if (service == null) {
+ Log.i(this, "Found no connection service for attempt %s", attempt);
+ attemptNextPhoneAccount();
+ } else {
+ mCall.setConnectionManagerPhoneAccount(attempt.connectionManagerPhoneAccount);
+ mCall.setTargetPhoneAccount(attempt.targetPhoneAccount);
+ mCall.setConnectionService(service);
+ Log.i(this, "Attempting to call from %s", service.getComponentName());
+ service.createConnection(mCall, new Response(service));
+ }
+ } else {
+ Log.v(this, "attemptNextPhoneAccount, no more accounts, failing");
+ if (mResponse != null) {
+ mResponse.handleCreateConnectionFailure(mLastErrorCode, mLastErrorMsg);
+ mResponse = null;
+ mCall.clearConnectionService();
+ }
+ }
+ }
+
+ private boolean shouldSetConnectionManager() {
+ Context context = TelecomApp.getInstance();
+ if (!context.getResources().getBoolean(R.bool.connection_manager_enabled)) {
+ // Connection Manager support has been turned off, disregard.
+ return false;
+ }
+
+ if (mAttemptRecords.size() == 0) {
+ return false;
+ }
+
+ if (mAttemptRecords.size() > 1) {
+ Log.d(this, "shouldSetConnectionManager, error, mAttemptRecords should not have more "
+ + "than 1 record");
+ return false;
+ }
+
+ PhoneAccountRegistrar registrar = TelecomApp.getInstance().getPhoneAccountRegistrar();
+ PhoneAccountHandle connectionManager = registrar.getSimCallManager();
+ if (connectionManager == null) {
+ return false;
+ }
+
+ PhoneAccountHandle targetPhoneAccountHandle = mAttemptRecords.get(0).targetPhoneAccount;
+ if (Objects.equals(connectionManager, targetPhoneAccountHandle)) {
+ return false;
+ }
+
+ // Connection managers are only allowed to manage SIM subscriptions.
+ PhoneAccount targetPhoneAccount = registrar.getPhoneAccount(targetPhoneAccountHandle);
+ boolean isSimSubscription = (targetPhoneAccount.getCapabilities() &
+ PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION) != 0;
+ if (!isSimSubscription) {
+ return false;
+ }
+
+ return true;
+ }
+
+ // If there exists a registered connection manager then use it.
+ private void adjustAttemptsForConnectionManager() {
+ if (shouldSetConnectionManager()) {
+ CallAttemptRecord record = new CallAttemptRecord(
+ TelecomApp.getInstance().getPhoneAccountRegistrar().getSimCallManager(),
+ mAttemptRecords.get(0).targetPhoneAccount);
+ Log.v(this, "setConnectionManager, changing %s -> %s",
+ mAttemptRecords.get(0).targetPhoneAccount, record);
+ mAttemptRecords.set(0, record);
+ } else {
+ Log.v(this, "setConnectionManager, not changing");
+ }
+ }
+
+ // If we are possibly attempting to call a local emergency number, ensure that the
+ // plain PSTN connection services are listed, and nothing else.
+ private void adjustAttemptsForEmergency() {
+ if (TelephonyUtil.shouldProcessAsEmergency(TelecomApp.getInstance(), mCall.getHandle())) {
+ Log.i(this, "Emergency number detected");
+ mAttemptRecords.clear();
+ List<PhoneAccount> allAccounts = TelecomApp.getInstance()
+ .getPhoneAccountRegistrar().getAllPhoneAccounts();
+ // First, add SIM phone accounts which can place emergency calls.
+ for (PhoneAccount phoneAccount : allAccounts) {
+ if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS) &&
+ phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
+ Log.i(this, "Will try PSTN account %s for emergency",
+ phoneAccount.getAccountHandle());
+ mAttemptRecords.add(
+ new CallAttemptRecord(
+ phoneAccount.getAccountHandle(),
+ phoneAccount.getAccountHandle()));
+ }
+ }
+
+ // Next, add the connection manager account as a backup if it can place emergency calls.
+ PhoneAccountHandle callManagerHandle = TelecomApp.getInstance()
+ .getPhoneAccountRegistrar().getSimCallManager();
+ if (callManagerHandle != null) {
+ PhoneAccount callManager = TelecomApp.getInstance()
+ .getPhoneAccountRegistrar().getPhoneAccount(callManagerHandle);
+ if (callManager.hasCapabilities(PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS)) {
+ CallAttemptRecord callAttemptRecord = new CallAttemptRecord(callManagerHandle,
+ TelecomApp.getInstance().getPhoneAccountRegistrar().
+ getDefaultOutgoingPhoneAccount(mCall.getHandle().getScheme())
+ );
+
+ if (!mAttemptRecords.contains(callAttemptRecord)) {
+ Log.i(this, "Will try Connection Manager account %s for emergency",
+ callManager);
+ mAttemptRecords.add(callAttemptRecord);
+ }
+ }
+ }
+ }
+ }
+
+ private class Response implements CreateConnectionResponse {
+ private final ConnectionServiceWrapper mService;
+
+ Response(ConnectionServiceWrapper service) {
+ mService = service;
+ }
+
+ @Override
+ public void handleCreateConnectionSuccess(
+ CallIdMapper idMapper,
+ ParcelableConnection connection) {
+ if (mResponse == null) {
+ // Nobody is listening for this connection attempt any longer; ask the responsible
+ // ConnectionService to tear down any resources associated with the call
+ mService.abort(mCall);
+ } else {
+ // Success -- share the good news and remember that we are no longer interested
+ // in hearing about any more attempts
+ mResponse.handleCreateConnectionSuccess(idMapper, connection);
+ mResponse = null;
+ }
+ }
+
+ @Override
+ public void handleCreateConnectionFailure(int code, String msg) {
+ // Failure of some sort; record the reasons for failure and try again if possible
+ Log.d(CreateConnectionProcessor.this, "Connection failed: %d (%s)", code, msg);
+ mLastErrorCode = code;
+ mLastErrorMsg = msg;
+ attemptNextPhoneAccount();
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/CreateConnectionResponse.java b/src/com/android/server/telecom/CreateConnectionResponse.java
new file mode 100644
index 0000000..a1ffa38
--- /dev/null
+++ b/src/com/android/server/telecom/CreateConnectionResponse.java
@@ -0,0 +1,27 @@
+/*
+ * 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;
+
+import android.telecom.ParcelableConnection;
+
+/**
+ * A callback for providing the result of creating a connection.
+ */
+interface CreateConnectionResponse {
+ void handleCreateConnectionSuccess(CallIdMapper idMapper, ParcelableConnection connection);
+ void handleCreateConnectionFailure(int code, String message);
+}
diff --git a/src/com/android/server/telecom/DtmfLocalTonePlayer.java b/src/com/android/server/telecom/DtmfLocalTonePlayer.java
new file mode 100644
index 0000000..eb3f9a0
--- /dev/null
+++ b/src/com/android/server/telecom/DtmfLocalTonePlayer.java
@@ -0,0 +1,155 @@
+/*
+ * 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;
+
+import android.media.AudioManager;
+import android.media.ToneGenerator;
+import android.provider.Settings;
+
+import com.google.common.collect.ImmutableMap;
+
+import java.util.Map;
+
+/**
+ * Plays DTMF tones locally for the caller to hear. In order to reduce (1) the amount of times we
+ * check the "play local tones" setting and (2) the length of time we keep the tone generator, this
+ * class employs a concept of a call "session" that starts and stops when the foreground call
+ * changes.
+ */
+class DtmfLocalTonePlayer extends CallsManagerListenerBase {
+ private static final Map<Character, Integer> TONE_MAP =
+ ImmutableMap.<Character, Integer>builder()
+ .put('1', ToneGenerator.TONE_DTMF_1)
+ .put('2', ToneGenerator.TONE_DTMF_2)
+ .put('3', ToneGenerator.TONE_DTMF_3)
+ .put('4', ToneGenerator.TONE_DTMF_4)
+ .put('5', ToneGenerator.TONE_DTMF_5)
+ .put('6', ToneGenerator.TONE_DTMF_6)
+ .put('7', ToneGenerator.TONE_DTMF_7)
+ .put('8', ToneGenerator.TONE_DTMF_8)
+ .put('9', ToneGenerator.TONE_DTMF_9)
+ .put('0', ToneGenerator.TONE_DTMF_0)
+ .put('#', ToneGenerator.TONE_DTMF_P)
+ .put('*', ToneGenerator.TONE_DTMF_S)
+ .build();
+
+ /** Generator used to actually play the tone. */
+ private ToneGenerator mToneGenerator;
+
+ /** The current call associated with an existing dtmf session. */
+ private Call mCall;
+
+ /** {@inheritDoc} */
+ @Override
+ public void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall) {
+ endDtmfSession(oldForegroundCall);
+ startDtmfSession(newForegroundCall);
+ }
+
+ /**
+ * Starts playing the dtmf tone specified by c.
+ *
+ * @param call The associated call.
+ * @param c The digit to play.
+ */
+ void playTone(Call call, char c) {
+ // Do nothing if it is not the right call.
+ if (mCall != call) {
+ return;
+ }
+
+ if (mToneGenerator == null) {
+ Log.d(this, "playTone: mToneGenerator == null, %c.", c);
+ } else {
+ Log.d(this, "starting local tone: %c.", c);
+ if (TONE_MAP.containsKey(c)) {
+ mToneGenerator.startTone(TONE_MAP.get(c), -1 /* toneDuration */);
+ }
+ }
+ }
+
+ /**
+ * Stops any currently playing dtmf tone.
+ *
+ * @param call The associated call.
+ */
+ void stopTone(Call call) {
+ // Do nothing if it's not the right call.
+ if (mCall != call) {
+ return;
+ }
+
+ if (mToneGenerator == null) {
+ Log.d(this, "stopTone: mToneGenerator == null.");
+ } else {
+ Log.d(this, "stopping local tone.");
+ mToneGenerator.stopTone();
+ }
+ }
+
+ /**
+ * Runs initialization requires to play local tones during a call.
+ *
+ * @param call The call associated with this dtmf session.
+ */
+ private void startDtmfSession(Call call) {
+ if (call == null) {
+ return;
+ }
+ TelecomApp app = TelecomApp.getInstance();
+
+ final boolean areLocalTonesEnabled;
+ if (app.getResources().getBoolean(R.bool.allow_local_dtmf_tones)) {
+ areLocalTonesEnabled = Settings.System.getInt(
+ app.getContentResolver(), Settings.System.DTMF_TONE_WHEN_DIALING, 1) == 1;
+ } else {
+ areLocalTonesEnabled = false;
+ }
+
+ mCall = call;
+
+ if (areLocalTonesEnabled) {
+ if (mToneGenerator == null) {
+ try {
+ mToneGenerator = new ToneGenerator(AudioManager.STREAM_DTMF, 80);
+ } catch (RuntimeException e) {
+ Log.e(this, e, "Error creating local tone generator.");
+ mToneGenerator = null;
+ }
+ }
+ }
+ }
+
+ /**
+ * Releases resources needed for playing local dtmf tones.
+ *
+ * @param call The call associated with the session to end.
+ */
+ private void endDtmfSession(Call call) {
+ if (call != null && mCall == call) {
+ // Do a stopTone() in case the sessions ends before we are told to stop the tone.
+ stopTone(call);
+
+ mCall = null;
+
+ if (mToneGenerator != null) {
+ mToneGenerator.release();
+ mToneGenerator = null;
+ }
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/ErrorDialogActivity.java b/src/com/android/server/telecom/ErrorDialogActivity.java
new file mode 100644
index 0000000..0e6bca0
--- /dev/null
+++ b/src/com/android/server/telecom/ErrorDialogActivity.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2013 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.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+
+/**
+ * Used to display an error dialog from within the Telecom service when an outgoing call fails
+ */
+public class ErrorDialogActivity extends Activity {
+ private static final String TAG = ErrorDialogActivity.class.getSimpleName();
+
+ public static final String SHOW_MISSING_VOICEMAIL_NO_DIALOG_EXTRA = "show_missing_voicemail";
+ public static final String ERROR_MESSAGE_ID_EXTRA = "error_message_id";
+
+ /**
+ * Intent action to bring up Voicemail Provider settings.
+ */
+ public static final String ACTION_ADD_VOICEMAIL =
+ "com.android.phone.CallFeaturesSetting.ADD_VOICEMAIL";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ final boolean showVoicemailDialog = getIntent().getBooleanExtra(
+ SHOW_MISSING_VOICEMAIL_NO_DIALOG_EXTRA, false);
+
+ if (showVoicemailDialog) {
+ showMissingVoicemailErrorDialog();
+ } else {
+ final int error = getIntent().getIntExtra(ERROR_MESSAGE_ID_EXTRA, -1);
+ if (error == -1) {
+ Log.w(TAG, "ErrorDialogActivity called with no error type extra.");
+ finish();
+ } else {
+ showGenericErrorDialog(error);
+ }
+ }
+ }
+
+ private void showGenericErrorDialog(int resid) {
+ final CharSequence msg = getResources().getText(resid);
+ final DialogInterface.OnClickListener clickListener;
+ final DialogInterface.OnCancelListener cancelListener;
+
+ clickListener = new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ finish();
+ }
+ };
+
+ cancelListener = new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ finish();
+ }
+ };
+
+ final AlertDialog errorDialog = new AlertDialog.Builder(this)
+ .setMessage(msg).setPositiveButton(android.R.string.ok, clickListener)
+ .setOnCancelListener(cancelListener).create();
+
+ errorDialog.show();
+ }
+
+ private void showMissingVoicemailErrorDialog() {
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.no_vm_number)
+ .setMessage(R.string.no_vm_number_msg)
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ finish();
+ }})
+ .setNegativeButton(R.string.add_vm_number_str,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ addVoiceMailNumberPanel(dialog);
+ }})
+ .setOnCancelListener(new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ finish();
+ }}).show();
+ }
+
+
+ private void addVoiceMailNumberPanel(DialogInterface dialog) {
+ if (dialog != null) {
+ dialog.dismiss();
+ }
+
+ // Navigate to the Voicemail setting in the Call Settings activity.
+ Intent intent = new Intent(ACTION_ADD_VOICEMAIL);
+ startActivity(intent);
+ finish();
+ }
+
+ @Override
+ public void finish() {
+ super.finish();
+ // Don't show the return to previous task animation to avoid showing a black screen.
+ // Just dismiss the dialog and undim the previous activity immediately.
+ overridePendingTransition(0, 0);
+ }
+}
diff --git a/src/com/android/server/telecom/HeadsetMediaButton.java b/src/com/android/server/telecom/HeadsetMediaButton.java
new file mode 100644
index 0000000..f0ea1e9
--- /dev/null
+++ b/src/com/android/server/telecom/HeadsetMediaButton.java
@@ -0,0 +1,103 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioAttributes;
+import android.media.session.MediaSession;
+import android.view.KeyEvent;
+
+/**
+ * Static class to handle listening to the headset media buttons.
+ */
+final class HeadsetMediaButton extends CallsManagerListenerBase {
+
+ // Types of media button presses
+ static final int SHORT_PRESS = 1;
+ static final int LONG_PRESS = 2;
+
+ private static final AudioAttributes AUDIO_ATTRIBUTES = new AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
+ .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION).build();
+
+ private final MediaSession.Callback mSessionCallback = new MediaSession.Callback() {
+ @Override
+ public boolean onMediaButtonEvent(Intent intent) {
+ KeyEvent event = (KeyEvent) intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
+ Log.v(this, "SessionCallback.onMediaButton()... event = %s.", event);
+ if ((event != null) && (event.getKeyCode() == KeyEvent.KEYCODE_HEADSETHOOK)) {
+ Log.v(this, "SessionCallback: HEADSETHOOK");
+ boolean consumed = handleHeadsetHook(event);
+ Log.v(this, "==> handleHeadsetHook(): consumed = %b.", consumed);
+ return consumed;
+ }
+ return true;
+ }
+ };
+
+ private final CallsManager mCallsManager;
+
+ private final MediaSession mSession;
+
+ HeadsetMediaButton(Context context, CallsManager callsManager) {
+ mCallsManager = callsManager;
+
+ // Create a MediaSession but don't enable it yet. This is a
+ // replacement for MediaButtonReceiver
+ mSession = new MediaSession(context, HeadsetMediaButton.class.getSimpleName());
+ mSession.setCallback(mSessionCallback);
+ mSession.setFlags(MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY
+ | MediaSession.FLAG_HANDLES_MEDIA_BUTTONS);
+ mSession.setPlaybackToLocal(AUDIO_ATTRIBUTES);
+ }
+
+ /**
+ * Handles the wired headset button while in-call.
+ *
+ * @return true if we consumed the event.
+ */
+ private boolean handleHeadsetHook(KeyEvent event) {
+ Log.d(this, "handleHeadsetHook()...%s %s", event.getAction(), event.getRepeatCount());
+
+ if (event.isLongPress()) {
+ return mCallsManager.onMediaButton(LONG_PRESS);
+ } else if (event.getAction() == KeyEvent.ACTION_UP && event.getRepeatCount() == 0) {
+ return mCallsManager.onMediaButton(SHORT_PRESS);
+ }
+
+ return true;
+ }
+
+ /** ${inheritDoc} */
+ @Override
+ public void onCallAdded(Call call) {
+ if (!mSession.isActive()) {
+ mSession.setActive(true);
+ }
+ }
+
+ /** ${inheritDoc} */
+ @Override
+ public void onCallRemoved(Call call) {
+ if (!mCallsManager.hasAnyCalls()) {
+ if (mSession.isActive()) {
+ mSession.setActive(false);
+ }
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/InCallAdapter.java b/src/com/android/server/telecom/InCallAdapter.java
new file mode 100644
index 0000000..5a93464
--- /dev/null
+++ b/src/com/android/server/telecom/InCallAdapter.java
@@ -0,0 +1,347 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.os.Handler;
+import android.os.Message;
+import android.telecom.PhoneAccountHandle;
+
+import com.android.internal.os.SomeArgs;
+import com.android.internal.telecom.IInCallAdapter;
+
+/**
+ * Receives call commands and updates from in-call app and passes them through to CallsManager.
+ * {@link InCallController} creates an instance of this class and passes it to the in-call app after
+ * binding to it. This adapter can receive commands and updates until the in-call app is unbound.
+ */
+class InCallAdapter extends IInCallAdapter.Stub {
+ private static final int MSG_ANSWER_CALL = 0;
+ private static final int MSG_REJECT_CALL = 1;
+ private static final int MSG_PLAY_DTMF_TONE = 2;
+ private static final int MSG_STOP_DTMF_TONE = 3;
+ private static final int MSG_POST_DIAL_CONTINUE = 4;
+ private static final int MSG_DISCONNECT_CALL = 5;
+ private static final int MSG_HOLD_CALL = 6;
+ private static final int MSG_UNHOLD_CALL = 7;
+ private static final int MSG_MUTE = 8;
+ private static final int MSG_SET_AUDIO_ROUTE = 9;
+ private static final int MSG_CONFERENCE = 10;
+ private static final int MSG_SPLIT_FROM_CONFERENCE = 11;
+ private static final int MSG_SWAP_WITH_BACKGROUND_CALL = 12;
+ private static final int MSG_PHONE_ACCOUNT_SELECTED = 13;
+ private static final int MSG_TURN_ON_PROXIMITY_SENSOR = 14;
+ private static final int MSG_TURN_OFF_PROXIMITY_SENSOR = 15;
+ private static final int MSG_MERGE_CONFERENCE = 16;
+ private static final int MSG_SWAP_CONFERENCE = 17;
+
+ private final class InCallAdapterHandler extends Handler {
+ @Override
+ public void handleMessage(Message msg) {
+ Call call;
+ switch (msg.what) {
+ case MSG_ANSWER_CALL: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ try {
+ call = mCallIdMapper.getCall(args.arg1);
+ int videoState = (int) args.arg2;
+ if (call != null) {
+ mCallsManager.answerCall(call, videoState);
+ } else {
+ Log.w(this, "answerCall, unknown call id: %s", msg.obj);
+ }
+ } finally {
+ args.recycle();
+ }
+ break;
+ }
+ case MSG_REJECT_CALL: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ try {
+ call = mCallIdMapper.getCall(args.arg1);
+ boolean rejectWithMessage = args.argi1 == 1;
+ String textMessage = (String) args.arg2;
+ if (call != null) {
+ mCallsManager.rejectCall(call, rejectWithMessage, textMessage);
+ } else {
+ Log.w(this, "setRingback, unknown call id: %s", args.arg1);
+ }
+ } finally {
+ args.recycle();
+ }
+ break;
+ }
+ case MSG_PLAY_DTMF_TONE:
+ call = mCallIdMapper.getCall(msg.obj);
+ if (call != null) {
+ mCallsManager.playDtmfTone(call, (char) msg.arg1);
+ } else {
+ Log.w(this, "playDtmfTone, unknown call id: %s", msg.obj);
+ }
+ break;
+ case MSG_STOP_DTMF_TONE:
+ call = mCallIdMapper.getCall(msg.obj);
+ if (call != null) {
+ mCallsManager.stopDtmfTone(call);
+ } else {
+ Log.w(this, "stopDtmfTone, unknown call id: %s", msg.obj);
+ }
+ break;
+ case MSG_POST_DIAL_CONTINUE:
+ call = mCallIdMapper.getCall(msg.obj);
+ mCallsManager.postDialContinue(call, msg.arg1 == 1);
+ call = mCallIdMapper.getCall(msg.obj);
+ if (call != null) {
+ mCallsManager.postDialContinue(call, msg.arg1 == 1);
+ } else {
+ Log.w(this, "postDialContinue, unknown call id: %s", msg.obj);
+ }
+ break;
+ case MSG_DISCONNECT_CALL:
+ call = mCallIdMapper.getCall(msg.obj);
+ if (call != null) {
+ mCallsManager.disconnectCall(call);
+ } else {
+ Log.w(this, "disconnectCall, unknown call id: %s", msg.obj);
+ }
+ break;
+ case MSG_HOLD_CALL:
+ call = mCallIdMapper.getCall(msg.obj);
+ if (call != null) {
+ mCallsManager.holdCall(call);
+ } else {
+ Log.w(this, "holdCall, unknown call id: %s", msg.obj);
+ }
+ break;
+ case MSG_UNHOLD_CALL:
+ call = mCallIdMapper.getCall(msg.obj);
+ if (call != null) {
+ mCallsManager.unholdCall(call);
+ } else {
+ Log.w(this, "unholdCall, unknown call id: %s", msg.obj);
+ }
+ break;
+ case MSG_PHONE_ACCOUNT_SELECTED: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ try {
+ call = mCallIdMapper.getCall(args.arg1);
+ if (call != null) {
+ mCallsManager.phoneAccountSelected(call, (PhoneAccountHandle) args.arg2);
+ } else {
+ Log.w(this, "phoneAccountSelected, unknown call id: %s", args.arg1);
+ }
+ } finally {
+ args.recycle();
+ }
+ break;
+ }
+ case MSG_MUTE:
+ mCallsManager.mute(msg.arg1 == 1);
+ break;
+ case MSG_SET_AUDIO_ROUTE:
+ mCallsManager.setAudioRoute(msg.arg1);
+ break;
+ case MSG_CONFERENCE: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ try {
+ call = mCallIdMapper.getCall(args.arg1);
+ Call otherCall = mCallIdMapper.getCall(args.arg2);
+ if (call != null && otherCall != null) {
+ mCallsManager.conference(call, otherCall);
+ } else {
+ Log.w(this, "conference, unknown call id: %s", msg.obj);
+ }
+ } finally {
+ args.recycle();
+ }
+ break;
+ }
+ case MSG_SPLIT_FROM_CONFERENCE:
+ call = mCallIdMapper.getCall(msg.obj);
+ if (call != null) {
+ call.splitFromConference();
+ } else {
+ Log.w(this, "splitFromConference, unknown call id: %s", msg.obj);
+ }
+ break;
+ case MSG_TURN_ON_PROXIMITY_SENSOR:
+ mCallsManager.turnOnProximitySensor();
+ break;
+ case MSG_TURN_OFF_PROXIMITY_SENSOR:
+ mCallsManager.turnOffProximitySensor((boolean) msg.obj);
+ break;
+ case MSG_MERGE_CONFERENCE:
+ call = mCallIdMapper.getCall(msg.obj);
+ if (call != null) {
+ call.mergeConference();
+ } else {
+ Log.w(this, "mergeConference, unknown call id: %s", msg.obj);
+ }
+ break;
+ case MSG_SWAP_CONFERENCE:
+ call = mCallIdMapper.getCall(msg.obj);
+ if (call != null) {
+ call.swapConference();
+ } else {
+ Log.w(this, "swapConference, unknown call id: %s", msg.obj);
+ }
+ break;
+ }
+ }
+ }
+
+ private final CallsManager mCallsManager;
+ private final Handler mHandler = new InCallAdapterHandler();
+ private final CallIdMapper mCallIdMapper;
+
+ /** Persists the specified parameters. */
+ public InCallAdapter(CallsManager callsManager, CallIdMapper callIdMapper) {
+ ThreadUtil.checkOnMainThread();
+ mCallsManager = callsManager;
+ mCallIdMapper = callIdMapper;
+ }
+
+ @Override
+ public void answerCall(String callId, int videoState) {
+ Log.d(this, "answerCall(%s,%d)", callId, videoState);
+ if (mCallIdMapper.isValidCallId(callId)) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = callId;
+ args.arg2 = videoState;
+ mHandler.obtainMessage(MSG_ANSWER_CALL, args).sendToTarget();
+ }
+ }
+
+ @Override
+ public void rejectCall(String callId, boolean rejectWithMessage, String textMessage) {
+ Log.d(this, "rejectCall(%s,%b,%s)", callId, rejectWithMessage, textMessage);
+ if (mCallIdMapper.isValidCallId(callId)) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = callId;
+ args.argi1 = rejectWithMessage ? 1 : 0;
+ args.arg2 = textMessage;
+ mHandler.obtainMessage(MSG_REJECT_CALL, args).sendToTarget();
+ }
+ }
+
+ @Override
+ public void playDtmfTone(String callId, char digit) {
+ Log.d(this, "playDtmfTone(%s,%c)", callId, digit);
+ if (mCallIdMapper.isValidCallId(callId)) {
+ mHandler.obtainMessage(MSG_PLAY_DTMF_TONE, (int) digit, 0, callId).sendToTarget();
+ }
+ }
+
+ @Override
+ public void stopDtmfTone(String callId) {
+ Log.d(this, "stopDtmfTone(%s)", callId);
+ if (mCallIdMapper.isValidCallId(callId)) {
+ mHandler.obtainMessage(MSG_STOP_DTMF_TONE, callId).sendToTarget();
+ }
+ }
+
+ @Override
+ public void postDialContinue(String callId, boolean proceed) {
+ Log.d(this, "postDialContinue(%s)", callId);
+ if (mCallIdMapper.isValidCallId(callId)) {
+ mHandler.obtainMessage(MSG_POST_DIAL_CONTINUE, proceed ? 1 : 0, 0, callId).sendToTarget();
+ }
+ }
+
+ @Override
+ public void disconnectCall(String callId) {
+ Log.v(this, "disconnectCall: %s", callId);
+ if (mCallIdMapper.isValidCallId(callId)) {
+ mHandler.obtainMessage(MSG_DISCONNECT_CALL, callId).sendToTarget();
+ }
+ }
+
+ @Override
+ public void holdCall(String callId) {
+ if (mCallIdMapper.isValidCallId(callId)) {
+ mHandler.obtainMessage(MSG_HOLD_CALL, callId).sendToTarget();
+ }
+ }
+
+ @Override
+ public void unholdCall(String callId) {
+ if (mCallIdMapper.isValidCallId(callId)) {
+ mHandler.obtainMessage(MSG_UNHOLD_CALL, callId).sendToTarget();
+ }
+ }
+
+ @Override
+ public void phoneAccountSelected(String callId, PhoneAccountHandle accountHandle) {
+ if (mCallIdMapper.isValidCallId(callId)) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = callId;
+ args.arg2 = accountHandle;
+ mHandler.obtainMessage(MSG_PHONE_ACCOUNT_SELECTED, args).sendToTarget();
+ }
+ }
+
+ @Override
+ public void mute(boolean shouldMute) {
+ mHandler.obtainMessage(MSG_MUTE, shouldMute ? 1 : 0, 0).sendToTarget();
+ }
+
+ @Override
+ public void setAudioRoute(int route) {
+ mHandler.obtainMessage(MSG_SET_AUDIO_ROUTE, route, 0).sendToTarget();
+ }
+
+ @Override
+ public void conference(String callId, String otherCallId) {
+ if (mCallIdMapper.isValidCallId(callId) &&
+ mCallIdMapper.isValidCallId(otherCallId)) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = callId;
+ args.arg2 = otherCallId;
+ mHandler.obtainMessage(MSG_CONFERENCE, args).sendToTarget();
+ }
+ }
+
+ @Override
+ public void splitFromConference(String callId) {
+ if (mCallIdMapper.isValidCallId(callId)) {
+ mHandler.obtainMessage(MSG_SPLIT_FROM_CONFERENCE, callId).sendToTarget();
+ }
+ }
+
+ @Override
+ public void mergeConference(String callId) {
+ if (mCallIdMapper.isValidCallId(callId)) {
+ mHandler.obtainMessage(MSG_MERGE_CONFERENCE, callId).sendToTarget();
+ }
+ }
+
+ @Override
+ public void swapConference(String callId) {
+ if (mCallIdMapper.isValidCallId(callId)) {
+ mHandler.obtainMessage(MSG_SWAP_CONFERENCE, callId).sendToTarget();
+ }
+ }
+
+ @Override
+ public void turnOnProximitySensor() {
+ mHandler.obtainMessage(MSG_TURN_ON_PROXIMITY_SENSOR).sendToTarget();
+ }
+
+ @Override
+ public void turnOffProximitySensor(boolean screenOnImmediately) {
+ mHandler.obtainMessage(MSG_TURN_OFF_PROXIMITY_SENSOR, screenOnImmediately).sendToTarget();
+ }
+}
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
new file mode 100644
index 0000000..26ac4a5
--- /dev/null
+++ b/src/com/android/server/telecom/InCallController.java
@@ -0,0 +1,514 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.Manifest;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.telecom.AudioState;
+import android.telecom.CallProperties;
+import android.telecom.CallState;
+import android.telecom.InCallService;
+import android.telecom.ParcelableCall;
+import android.telecom.PhoneCapabilities;
+import android.telecom.TelecomManager;
+import android.util.ArrayMap;
+
+import com.android.internal.telecom.IInCallService;
+import com.google.common.collect.ImmutableCollection;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Binds to {@link IInCallService} and provides the service to {@link CallsManager} through which it
+ * can send updates to the in-call app. This class is created and owned by CallsManager and retains
+ * a binding to the {@link IInCallService} (implemented by the in-call app).
+ */
+public final class InCallController extends CallsManagerListenerBase {
+ /**
+ * Used to bind to the in-call app and triggers the start of communication between
+ * this class and in-call app.
+ */
+ private class InCallServiceConnection implements ServiceConnection {
+ /** {@inheritDoc} */
+ @Override public void onServiceConnected(ComponentName name, IBinder service) {
+ Log.d(this, "onServiceConnected: %s", name);
+ onConnected(name, service);
+ }
+
+ /** {@inheritDoc} */
+ @Override public void onServiceDisconnected(ComponentName name) {
+ Log.d(this, "onDisconnected: %s", name);
+ onDisconnected(name);
+ }
+ }
+
+ private final Call.Listener mCallListener = new Call.ListenerBase() {
+ @Override
+ public void onCallCapabilitiesChanged(Call call) {
+ updateCall(call);
+ }
+
+ @Override
+ public void onCannedSmsResponsesLoaded(Call call) {
+ updateCall(call);
+ }
+
+ @Override
+ public void onVideoCallProviderChanged(Call call) {
+ updateCall(call);
+ }
+
+ @Override
+ public void onStatusHintsChanged(Call call) {
+ updateCall(call);
+ }
+
+ @Override
+ public void onHandleChanged(Call call) {
+ updateCall(call);
+ }
+
+ @Override
+ public void onCallerDisplayNameChanged(Call call) {
+ updateCall(call);
+ }
+
+ @Override
+ public void onVideoStateChanged(Call call) {
+ updateCall(call);
+ }
+
+ @Override
+ public void onTargetPhoneAccountChanged(Call call) {
+ updateCall(call);
+ }
+
+ @Override
+ public void onConferenceableCallsChanged(Call call) {
+ updateCall(call);
+ }
+ };
+
+ /**
+ * Maintains a binding connection to the in-call app(s).
+ * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
+ * load factor before resizing, 1 means we only expect a single thread to
+ * access the map so make only a single shard
+ */
+ private final Map<ComponentName, InCallServiceConnection> mServiceConnections =
+ new ConcurrentHashMap<ComponentName, InCallServiceConnection>(8, 0.9f, 1);
+
+ /** The in-call app implementations, see {@link IInCallService}. */
+ private final Map<ComponentName, IInCallService> mInCallServices = new ArrayMap<>();
+
+ private final CallIdMapper mCallIdMapper = new CallIdMapper("InCall");
+
+ /** The {@link ComponentName} of the default InCall UI. */
+ private final ComponentName mInCallComponentName;
+
+ public InCallController() {
+ Context context = TelecomApp.getInstance();
+ Resources resources = context.getResources();
+
+ mInCallComponentName = new ComponentName(
+ resources.getString(R.string.ui_default_package),
+ resources.getString(R.string.incall_default_class));
+ }
+
+ @Override
+ public void onCallAdded(Call call) {
+ if (mInCallServices.isEmpty()) {
+ bind();
+ } else {
+ Log.i(this, "onCallAdded: %s", call);
+ // Track the call if we don't already know about it.
+ addCall(call);
+
+ for (Map.Entry<ComponentName, IInCallService> entry : mInCallServices.entrySet()) {
+ ComponentName componentName = entry.getKey();
+ IInCallService inCallService = entry.getValue();
+
+ ParcelableCall parcelableCall = toParcelableCall(call,
+ componentName.equals(mInCallComponentName) /* includeVideoProvider */);
+ try {
+ inCallService.addCall(parcelableCall);
+ } catch (RemoteException ignored) {
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onCallRemoved(Call call) {
+ Log.i(this, "onCallRemoved: %s", call);
+ if (CallsManager.getInstance().getCalls().isEmpty()) {
+ // TODO: Wait for all messages to be delivered to the service before unbinding.
+ unbind();
+ }
+ call.removeListener(mCallListener);
+ mCallIdMapper.removeCall(call);
+ }
+
+ @Override
+ public void onCallStateChanged(Call call, int oldState, int newState) {
+ updateCall(call);
+ }
+
+ @Override
+ public void onConnectionServiceChanged(
+ Call call,
+ ConnectionServiceWrapper oldService,
+ ConnectionServiceWrapper newService) {
+ updateCall(call);
+ }
+
+ @Override
+ public void onAudioStateChanged(AudioState oldAudioState, AudioState newAudioState) {
+ if (!mInCallServices.isEmpty()) {
+ Log.i(this, "Calling onAudioStateChanged, audioState: %s -> %s", oldAudioState,
+ newAudioState);
+ for (IInCallService inCallService : mInCallServices.values()) {
+ try {
+ inCallService.onAudioStateChanged(newAudioState);
+ } catch (RemoteException ignored) {
+ }
+ }
+ }
+ }
+
+ void onPostDialWait(Call call, String remaining) {
+ if (!mInCallServices.isEmpty()) {
+ Log.i(this, "Calling onPostDialWait, remaining = %s", remaining);
+ for (IInCallService inCallService : mInCallServices.values()) {
+ try {
+ inCallService.setPostDialWait(mCallIdMapper.getCallId(call), remaining);
+ } catch (RemoteException ignored) {
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onIsConferencedChanged(Call call) {
+ Log.d(this, "onIsConferencedChanged %s", call);
+ updateCall(call);
+ }
+
+ void bringToForeground(boolean showDialpad) {
+ if (!mInCallServices.isEmpty()) {
+ for (IInCallService inCallService : mInCallServices.values()) {
+ try {
+ inCallService.bringToForeground(showDialpad);
+ } catch (RemoteException ignored) {
+ }
+ }
+ } else {
+ Log.w(this, "Asking to bring unbound in-call UI to foreground.");
+ }
+ }
+
+ /**
+ * Unbinds an existing bound connection to the in-call app.
+ */
+ private void unbind() {
+ ThreadUtil.checkOnMainThread();
+ Iterator<Map.Entry<ComponentName, InCallServiceConnection>> iterator =
+ mServiceConnections.entrySet().iterator();
+ while (iterator.hasNext()) {
+ Log.i(this, "Unbinding from InCallService %s");
+ TelecomApp.getInstance().unbindService(iterator.next().getValue());
+ iterator.remove();
+ }
+ mInCallServices.clear();
+ }
+
+ /**
+ * Binds to the in-call app if not already connected by binding directly to the saved
+ * component name of the {@link IInCallService} implementation.
+ */
+ private void bind() {
+ ThreadUtil.checkOnMainThread();
+ if (mInCallServices.isEmpty()) {
+ mServiceConnections.clear();
+ Context context = TelecomApp.getInstance();
+ PackageManager packageManager = TelecomApp.getInstance().getPackageManager();
+ Intent serviceIntent = new Intent(InCallService.SERVICE_INTERFACE);
+
+ for (ResolveInfo entry : packageManager.queryIntentServices(serviceIntent, 0)) {
+ ServiceInfo serviceInfo = entry.serviceInfo;
+ if (serviceInfo != null) {
+ boolean hasServiceBindPermission = serviceInfo.permission != null &&
+ serviceInfo.permission.equals(
+ Manifest.permission.BIND_INCALL_SERVICE);
+ boolean hasControlInCallPermission = packageManager.checkPermission(
+ Manifest.permission.CONTROL_INCALL_EXPERIENCE,
+ serviceInfo.packageName) == PackageManager.PERMISSION_GRANTED;
+
+ if (!hasServiceBindPermission) {
+ Log.w(this, "InCallService does not have BIND_INCALL_SERVICE permission: " +
+ serviceInfo.packageName);
+ continue;
+ }
+
+ if (!hasControlInCallPermission) {
+ Log.w(this,
+ "InCall UI does not have CONTROL_INCALL_EXPERIENCE permission: " +
+ serviceInfo.packageName);
+ continue;
+ }
+
+ Log.i(this, "Attempting to bind to InCall " + serviceInfo.packageName);
+ InCallServiceConnection inCallServiceConnection = new InCallServiceConnection();
+ ComponentName componentName = new ComponentName(serviceInfo.packageName,
+ serviceInfo.name);
+
+ if (!mServiceConnections.containsKey(componentName)) {
+ Intent intent = new Intent(InCallService.SERVICE_INTERFACE);
+ intent.setComponent(componentName);
+
+ if (context.bindServiceAsUser(intent, inCallServiceConnection,
+ Context.BIND_AUTO_CREATE, UserHandle.CURRENT)) {
+ mServiceConnections.put(componentName, inCallServiceConnection);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Persists the {@link IInCallService} instance and starts the communication between
+ * this class and in-call app by sending the first update to in-call app. This method is
+ * called after a successful binding connection is established.
+ *
+ * @param componentName The service {@link ComponentName}.
+ * @param service The {@link IInCallService} implementation.
+ */
+ private void onConnected(ComponentName componentName, IBinder service) {
+ ThreadUtil.checkOnMainThread();
+
+ Log.i(this, "onConnected to %s", componentName);
+
+ IInCallService inCallService = IInCallService.Stub.asInterface(service);
+
+ try {
+ inCallService.setInCallAdapter(new InCallAdapter(CallsManager.getInstance(),
+ mCallIdMapper));
+ mInCallServices.put(componentName, inCallService);
+ } catch (RemoteException e) {
+ Log.e(this, e, "Failed to set the in-call adapter.");
+ return;
+ }
+
+ // Upon successful connection, send the state of the world to the service.
+ ImmutableCollection<Call> calls = CallsManager.getInstance().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,
+ componentName.equals(mInCallComponentName) /* includeVideoProvider */));
+ } catch (RemoteException ignored) {
+ }
+ }
+ onAudioStateChanged(null, CallsManager.getInstance().getAudioState());
+ } else {
+ unbind();
+ }
+ }
+
+ /**
+ * Cleans up an instance of in-call app after the service has been unbound.
+ *
+ * @param disconnectedComponent The {@link ComponentName} of the service which disconnected.
+ */
+ private void onDisconnected(ComponentName disconnectedComponent) {
+ Log.i(this, "onDisconnected from %s", disconnectedComponent);
+ ThreadUtil.checkOnMainThread();
+ Context context = TelecomApp.getInstance();
+
+ if (mInCallServices.containsKey(disconnectedComponent)) {
+ mInCallServices.remove(disconnectedComponent);
+ }
+
+ if (mServiceConnections.containsKey(disconnectedComponent)) {
+ // One of the services that we were bound to has disconnected. If the default in-call UI
+ // has disconnected, disconnect all calls and un-bind all other InCallService
+ // implementations.
+ if (disconnectedComponent.equals(mInCallComponentName)) {
+ Log.i(this, "In-call UI %s disconnected.", disconnectedComponent);
+ CallsManager.getInstance().disconnectAllCalls();
+ unbind();
+ } else {
+ Log.i(this, "In-Call Service %s suddenly disconnected", disconnectedComponent);
+ // Else, if it wasn't the default in-call UI, then one of the other in-call services
+ // disconnected and, well, that's probably their fault. Clear their state and
+ // ignore.
+ InCallServiceConnection serviceConnection =
+ mServiceConnections.get(disconnectedComponent);
+
+ // We still need to call unbind even though it disconnected.
+ context.unbindService(serviceConnection);
+
+ mServiceConnections.remove(disconnectedComponent);
+ mInCallServices.remove(disconnectedComponent);
+ }
+ }
+ }
+
+ /**
+ * Informs all {@link InCallService} instances of the updated call information. Changes to the
+ * video provider are only communicated to the default in-call UI.
+ *
+ * @param call The {@link Call}.
+ */
+ private void updateCall(Call call) {
+ if (!mInCallServices.isEmpty()) {
+ for (Map.Entry<ComponentName, IInCallService> entry : mInCallServices.entrySet()) {
+ ComponentName componentName = entry.getKey();
+ IInCallService inCallService = entry.getValue();
+ ParcelableCall parcelableCall = toParcelableCall(call,
+ componentName.equals(mInCallComponentName) /* includeVideoProvider */);
+
+ Log.v(this, "updateCall %s ==> %s", call, parcelableCall);
+ try {
+ inCallService.updateCall(parcelableCall);
+ } catch (RemoteException ignored) {
+ }
+ }
+ }
+ }
+
+ /**
+ * Parcels all information for a {@link Call} into a new {@link ParcelableCall} instance.
+ *
+ * @param call The {@link Call} to parcel.
+ * @param includeVideoProvider When {@code true}, the {@link IVideoProvider} is included in the
+ * parcelled call. When {@code false}, the {@link IVideoProvider} is not included.
+ * @return The {@link ParcelableCall} containing all call information from the {@link Call}.
+ */
+ private ParcelableCall toParcelableCall(Call call, boolean includeVideoProvider) {
+ String callId = mCallIdMapper.getCallId(call);
+
+ int capabilities = call.getCallCapabilities();
+ if (CallsManager.getInstance().isAddCallCapable(call)) {
+ capabilities |= PhoneCapabilities.ADD_CALL;
+ }
+
+ // Disable mute and add call for emergency calls.
+ if (call.isEmergencyCall()) {
+ capabilities &= ~PhoneCapabilities.MUTE;
+ capabilities &= ~PhoneCapabilities.ADD_CALL;
+ }
+
+ int properties = call.isConference() ? CallProperties.CONFERENCE : 0;
+
+ int state = call.getState();
+ if (state == CallState.ABORTED) {
+ state = CallState.DISCONNECTED;
+ }
+
+ String parentCallId = null;
+ Call parentCall = call.getParentCall();
+ if (parentCall != null) {
+ parentCallId = mCallIdMapper.getCallId(parentCall);
+ }
+
+ long connectTimeMillis = call.getConnectTimeMillis();
+ List<Call> childCalls = call.getChildCalls();
+ List<String> childCallIds = new ArrayList<>();
+ if (!childCalls.isEmpty()) {
+ connectTimeMillis = Long.MAX_VALUE;
+ for (Call child : childCalls) {
+ connectTimeMillis = Math.min(child.getConnectTimeMillis(), connectTimeMillis);
+ childCallIds.add(mCallIdMapper.getCallId(child));
+ }
+ }
+
+ if (call.isRespondViaSmsCapable()) {
+ capabilities |= PhoneCapabilities.RESPOND_VIA_TEXT;
+ }
+
+ Uri handle = call.getHandlePresentation() == TelecomManager.PRESENTATION_ALLOWED ?
+ call.getHandle() : null;
+ String callerDisplayName = call.getCallerDisplayNamePresentation() ==
+ TelecomManager.PRESENTATION_ALLOWED ? call.getCallerDisplayName() : null;
+
+ List<Call> conferenceableCalls = call.getConferenceableCalls();
+ List<String> conferenceableCallIds = new ArrayList<String>(conferenceableCalls.size());
+ for (Call otherCall : conferenceableCalls) {
+ String otherId = mCallIdMapper.getCallId(otherCall);
+ if (otherId != null) {
+ conferenceableCallIds.add(otherId);
+ }
+ }
+
+ return new ParcelableCall(
+ callId,
+ state,
+ call.getDisconnectCause(),
+ call.getDisconnectMessage(),
+ call.getCannedSmsResponses(),
+ capabilities,
+ properties,
+ connectTimeMillis,
+ handle,
+ call.getHandlePresentation(),
+ callerDisplayName,
+ call.getCallerDisplayNamePresentation(),
+ call.getGatewayInfo(),
+ call.getTargetPhoneAccount(),
+ includeVideoProvider ? call.getVideoProvider() : null,
+ parentCallId,
+ childCallIds,
+ call.getStatusHints(),
+ call.getVideoState(),
+ conferenceableCallIds,
+ call.getExtras());
+ }
+
+ /**
+ * Adds the call to the list of calls tracked by the {@link InCallController}.
+ * @param call The call to add.
+ */
+ private void addCall(Call call) {
+ if (mCallIdMapper.getCallId(call) == null) {
+ mCallIdMapper.addCall(call);
+ call.addListener(mCallListener);
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/InCallToneMonitor.java b/src/com/android/server/telecom/InCallToneMonitor.java
new file mode 100644
index 0000000..2ffb499
--- /dev/null
+++ b/src/com/android/server/telecom/InCallToneMonitor.java
@@ -0,0 +1,96 @@
+/*
+ * 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;
+
+import android.telecom.CallState;
+import android.telephony.DisconnectCause;
+
+import java.util.Collection;
+
+/**
+ * Monitors events from CallsManager and plays in-call tones for events which require them, such as
+ * different type of call disconnections (busy tone, congestion tone, etc).
+ */
+public final class InCallToneMonitor extends CallsManagerListenerBase {
+ private final InCallTonePlayer.Factory mPlayerFactory;
+
+ private final CallsManager mCallsManager;
+
+ InCallToneMonitor(InCallTonePlayer.Factory playerFactory, CallsManager callsManager) {
+ mPlayerFactory = playerFactory;
+ mCallsManager = callsManager;
+ }
+
+ @Override
+ public void onCallStateChanged(Call call, int oldState, int newState) {
+ if (mCallsManager.getForegroundCall() != call) {
+ // We only play tones for foreground calls.
+ return;
+ }
+
+ if (newState == CallState.DISCONNECTED) {
+ int toneToPlay = InCallTonePlayer.TONE_INVALID;
+
+ Log.v(this, "Disconnect cause: %d.", call.getDisconnectCause());
+
+ switch(call.getDisconnectCause()) {
+ case DisconnectCause.BUSY:
+ toneToPlay = InCallTonePlayer.TONE_BUSY;
+ break;
+ case DisconnectCause.CONGESTION:
+ toneToPlay = InCallTonePlayer.TONE_CONGESTION;
+ break;
+ case DisconnectCause.CDMA_REORDER:
+ toneToPlay = InCallTonePlayer.TONE_REORDER;
+ break;
+ case DisconnectCause.CDMA_INTERCEPT:
+ toneToPlay = InCallTonePlayer.TONE_INTERCEPT;
+ break;
+ case DisconnectCause.CDMA_DROP:
+ toneToPlay = InCallTonePlayer.TONE_CDMA_DROP;
+ break;
+ case DisconnectCause.OUT_OF_SERVICE:
+ toneToPlay = InCallTonePlayer.TONE_OUT_OF_SERVICE;
+ break;
+ case DisconnectCause.UNOBTAINABLE_NUMBER:
+ toneToPlay = InCallTonePlayer.TONE_UNOBTAINABLE_NUMBER;
+ break;
+ case DisconnectCause.ERROR_UNSPECIFIED:
+ toneToPlay = InCallTonePlayer.TONE_CALL_ENDED;
+ break;
+ case DisconnectCause.NORMAL:
+ case DisconnectCause.LOCAL:
+ // Only play the disconnect sound on normal disconnects if there are no other
+ // calls present beyond the one that is currently disconnected.
+ Collection<Call> allCalls = mCallsManager.getCalls();
+ if (allCalls.size() == 1) {
+ if (!allCalls.contains(call)) {
+ Log.wtf(this, "Disconnecting call not found %s.", call);
+ }
+ toneToPlay = InCallTonePlayer.TONE_CALL_ENDED;
+ }
+ break;
+ }
+
+ Log.d(this, "Found a disconnected call with tone to play %d.", toneToPlay);
+
+ if (toneToPlay != InCallTonePlayer.TONE_INVALID) {
+ mPlayerFactory.createPlayer(toneToPlay).startTone();
+ }
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/InCallTonePlayer.java b/src/com/android/server/telecom/InCallTonePlayer.java
new file mode 100644
index 0000000..2ffe599
--- /dev/null
+++ b/src/com/android/server/telecom/InCallTonePlayer.java
@@ -0,0 +1,260 @@
+/*
+ * 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;
+
+import android.media.AudioManager;
+import android.media.ToneGenerator;
+import android.os.Handler;
+import android.os.Looper;
+
+/**
+ * Play a call-related tone (ringback, busy signal, etc.) through ToneGenerator. To use, create an
+ * instance using InCallTonePlayer.Factory (passing in the TONE_* constant for the tone you want)
+ * and start() it. Implemented on top of {@link Thread} so that the tone plays in its own thread.
+ */
+public final class InCallTonePlayer extends Thread {
+
+ /**
+ * Factory used to create InCallTonePlayers. Exists to aid with testing mocks.
+ */
+ public static class Factory {
+ private final CallAudioManager mCallAudioManager;
+
+ Factory(CallAudioManager callAudioManager) {
+ mCallAudioManager = callAudioManager;
+ }
+
+ InCallTonePlayer createPlayer(int tone) {
+ return new InCallTonePlayer(tone, mCallAudioManager);
+ }
+ }
+
+ // The possible tones that we can play.
+ public static final int TONE_INVALID = 0;
+ public static final int TONE_BUSY = 1;
+ public static final int TONE_CALL_ENDED = 2;
+ public static final int TONE_OTA_CALL_ENDED = 3;
+ public static final int TONE_CALL_WAITING = 4;
+ public static final int TONE_CDMA_DROP = 5;
+ public static final int TONE_CONGESTION = 6;
+ public static final int TONE_INTERCEPT = 7;
+ public static final int TONE_OUT_OF_SERVICE = 8;
+ public static final int TONE_REDIAL = 9;
+ public static final int TONE_REORDER = 10;
+ public static final int TONE_RING_BACK = 11;
+ public static final int TONE_UNOBTAINABLE_NUMBER = 12;
+ public static final int TONE_VOICE_PRIVACY = 13;
+
+ private static final int RELATIVE_VOLUME_EMERGENCY = 100;
+ private static final int RELATIVE_VOLUME_HIPRI = 80;
+ private static final int RELATIVE_VOLUME_LOPRI = 50;
+
+ // Buffer time (in msec) to add on to the tone timeout value. Needed mainly when the timeout
+ // value for a tone is exact duration of the tone itself.
+ private static final int TIMEOUT_BUFFER_MILLIS = 20;
+
+ // The tone state.
+ private static final int STATE_OFF = 0;
+ private static final int STATE_ON = 1;
+ private static final int STATE_STOPPED = 2;
+
+ /**
+ * Keeps count of the number of actively playing tones so that we can notify CallAudioManager
+ * when we need focus and when it can be release. This should only be manipulated from the main
+ * thread.
+ */
+ private static int sTonesPlaying = 0;
+
+ private final CallAudioManager mCallAudioManager;
+
+ private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
+
+ /** The ID of the tone to play. */
+ private final int mToneId;
+
+ /** Current state of the tone player. */
+ private int mState;
+
+ /**
+ * Initializes the tone player. Private; use the {@link Factory} to create tone players.
+ *
+ * @param toneId ID of the tone to play, see TONE_* constants.
+ */
+ private InCallTonePlayer(int toneId, CallAudioManager callAudioManager) {
+ mState = STATE_OFF;
+ mToneId = toneId;
+ mCallAudioManager = callAudioManager;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void run() {
+ ToneGenerator toneGenerator = null;
+ try {
+ Log.d(this, "run(toneId = %s)", mToneId);
+
+ final int toneType; // Passed to ToneGenerator.startTone.
+ final int toneVolume; // Passed to the ToneGenerator constructor.
+ final int toneLengthMillis;
+
+ switch (mToneId) {
+ case TONE_BUSY:
+ // TODO: CDMA-specific tones
+ toneType = ToneGenerator.TONE_SUP_BUSY;
+ toneVolume = RELATIVE_VOLUME_HIPRI;
+ toneLengthMillis = 4000;
+ break;
+ case TONE_CALL_ENDED:
+ toneType = ToneGenerator.TONE_PROP_PROMPT;
+ toneVolume = RELATIVE_VOLUME_HIPRI;
+ toneLengthMillis = 4000;
+ break;
+ case TONE_OTA_CALL_ENDED:
+ // TODO: fill in
+ throw new IllegalStateException("OTA Call ended NYI.");
+ case TONE_CALL_WAITING:
+ toneType = ToneGenerator.TONE_SUP_CALL_WAITING;
+ toneVolume = RELATIVE_VOLUME_HIPRI;
+ toneLengthMillis = Integer.MAX_VALUE - TIMEOUT_BUFFER_MILLIS;
+ break;
+ case TONE_CDMA_DROP:
+ toneType = ToneGenerator.TONE_CDMA_CALLDROP_LITE;
+ toneVolume = RELATIVE_VOLUME_LOPRI;
+ toneLengthMillis = 375;
+ break;
+ case TONE_CONGESTION:
+ toneType = ToneGenerator.TONE_SUP_CONGESTION;
+ toneVolume = RELATIVE_VOLUME_HIPRI;
+ toneLengthMillis = 4000;
+ break;
+ case TONE_INTERCEPT:
+ toneType = ToneGenerator.TONE_CDMA_ABBR_INTERCEPT;
+ toneVolume = RELATIVE_VOLUME_LOPRI;
+ toneLengthMillis = 500;
+ break;
+ case TONE_OUT_OF_SERVICE:
+ toneType = ToneGenerator.TONE_CDMA_CALLDROP_LITE;
+ toneVolume = RELATIVE_VOLUME_LOPRI;
+ toneLengthMillis = 375;
+ break;
+ case TONE_REDIAL:
+ toneType = ToneGenerator.TONE_CDMA_ALERT_AUTOREDIAL_LITE;
+ toneVolume = RELATIVE_VOLUME_LOPRI;
+ toneLengthMillis = 5000;
+ break;
+ case TONE_REORDER:
+ toneType = ToneGenerator.TONE_CDMA_REORDER;
+ toneVolume = RELATIVE_VOLUME_HIPRI;
+ toneLengthMillis = 5000;
+ break;
+ case TONE_RING_BACK:
+ toneType = ToneGenerator.TONE_SUP_RINGTONE;
+ toneVolume = RELATIVE_VOLUME_HIPRI;
+ toneLengthMillis = Integer.MAX_VALUE - TIMEOUT_BUFFER_MILLIS;
+ break;
+ case TONE_UNOBTAINABLE_NUMBER:
+ toneType = ToneGenerator.TONE_SUP_ERROR;
+ toneVolume = RELATIVE_VOLUME_HIPRI;
+ toneLengthMillis = 4000;
+ break;
+ case TONE_VOICE_PRIVACY:
+ // TODO: fill in.
+ throw new IllegalStateException("Voice privacy tone NYI.");
+ default:
+ throw new IllegalStateException("Bad toneId: " + mToneId);
+ }
+
+ int stream = AudioManager.STREAM_VOICE_CALL;
+ if (mCallAudioManager.isBluetoothAudioOn()) {
+ stream = AudioManager.STREAM_BLUETOOTH_SCO;
+ }
+
+ // If the ToneGenerator creation fails, just continue without it. It is a local audio
+ // signal, and is not as important.
+ try {
+ Log.v(this, "Creating generator");
+ toneGenerator = new ToneGenerator(stream, toneVolume);
+ } catch (RuntimeException e) {
+ Log.w(this, "Failed to create ToneGenerator.", e);
+ return;
+ }
+
+ // TODO: Certain CDMA tones need to check the ringer-volume state before
+ // playing. See CallNotifier.InCallTonePlayer.
+
+ // TODO: Some tones play through the end of a call so we need to inform
+ // CallAudioManager that we want focus the same way that Ringer does.
+
+ synchronized (this) {
+ if (mState != STATE_STOPPED) {
+ mState = STATE_ON;
+ toneGenerator.startTone(toneType);
+ try {
+ Log.v(this, "Starting tone %d...waiting for %d ms.", mToneId,
+ toneLengthMillis + TIMEOUT_BUFFER_MILLIS);
+ wait(toneLengthMillis + TIMEOUT_BUFFER_MILLIS);
+ } catch (InterruptedException e) {
+ Log.w(this, "wait interrupted", e);
+ }
+ }
+ }
+ mState = STATE_OFF;
+ } finally {
+ if (toneGenerator != null) {
+ toneGenerator.release();
+ }
+ cleanUpTonePlayer();
+ }
+ }
+
+ void startTone() {
+ ThreadUtil.checkOnMainThread();
+
+ sTonesPlaying++;
+ if (sTonesPlaying == 1) {
+ mCallAudioManager.setIsTonePlaying(true);
+ }
+
+ start();
+ }
+
+ /**
+ * Stops the tone.
+ */
+ void stopTone() {
+ synchronized (this) {
+ if (mState == STATE_ON) {
+ Log.d(this, "Stopping the tone %d.", mToneId);
+ notify();
+ }
+ mState = STATE_STOPPED;
+ }
+ }
+
+ private void cleanUpTonePlayer() {
+ // Release focus on the main thread.
+ mMainThreadHandler.post(new Runnable() {
+ @Override public void run() {
+ if (sTonesPlaying == 0) {
+ Log.wtf(this, "Over-releasing focus for tone player.");
+ } else if (--sTonesPlaying == 0) {
+ mCallAudioManager.setIsTonePlaying(false);
+ }
+ }
+ });
+ }
+}
diff --git a/src/com/android/server/telecom/Log.java b/src/com/android/server/telecom/Log.java
new file mode 100644
index 0000000..ff3c8ce
--- /dev/null
+++ b/src/com/android/server/telecom/Log.java
@@ -0,0 +1,211 @@
+/*
+ * 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;
+
+import android.net.Uri;
+import android.telecom.PhoneAccount;
+import android.telephony.PhoneNumberUtils;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.IllegalFormatException;
+import java.util.Locale;
+
+/**
+ * Manages logging for the entire module.
+ */
+public class Log {
+
+ // Generic tag for all In Call logging
+ private static final String TAG = "Telecom";
+
+ public static final boolean FORCE_LOGGING = false; /* STOP SHIP if true */
+ public static final boolean DEBUG = isLoggable(android.util.Log.DEBUG);
+ public static final boolean INFO = isLoggable(android.util.Log.INFO);
+ public static final boolean VERBOSE = isLoggable(android.util.Log.VERBOSE);
+ public static final boolean WARN = isLoggable(android.util.Log.WARN);
+ public static final boolean ERROR = isLoggable(android.util.Log.ERROR);
+
+ private Log() {}
+
+ public static boolean isLoggable(int level) {
+ return FORCE_LOGGING || android.util.Log.isLoggable(TAG, level);
+ }
+
+ public static void d(String prefix, String format, Object... args) {
+ if (DEBUG) {
+ android.util.Log.d(TAG, buildMessage(prefix, format, args));
+ }
+ }
+
+ public static void d(Object objectPrefix, String format, Object... args) {
+ if (DEBUG) {
+ android.util.Log.d(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args));
+ }
+ }
+
+ public static void i(String prefix, String format, Object... args) {
+ if (INFO) {
+ android.util.Log.i(TAG, buildMessage(prefix, format, args));
+ }
+ }
+
+ public static void i(Object objectPrefix, String format, Object... args) {
+ if (INFO) {
+ android.util.Log.i(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args));
+ }
+ }
+
+ public static void v(String prefix, String format, Object... args) {
+ if (VERBOSE) {
+ android.util.Log.v(TAG, buildMessage(prefix, format, args));
+ }
+ }
+
+ public static void v(Object objectPrefix, String format, Object... args) {
+ if (VERBOSE) {
+ android.util.Log.v(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args));
+ }
+ }
+
+ public static void w(String prefix, String format, Object... args) {
+ if (WARN) {
+ android.util.Log.w(TAG, buildMessage(prefix, format, args));
+ }
+ }
+
+ public static void w(Object objectPrefix, String format, Object... args) {
+ if (WARN) {
+ android.util.Log.w(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args));
+ }
+ }
+
+ public static void e(String prefix, Throwable tr, String format, Object... args) {
+ if (ERROR) {
+ android.util.Log.e(TAG, buildMessage(prefix, format, args), tr);
+ }
+ }
+
+ public static void e(Object objectPrefix, Throwable tr, String format, Object... args) {
+ if (ERROR) {
+ android.util.Log.e(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args),
+ tr);
+ }
+ }
+
+ public static void wtf(String prefix, Throwable tr, String format, Object... args) {
+ android.util.Log.wtf(TAG, buildMessage(prefix, format, args), tr);
+ }
+
+ public static void wtf(Object objectPrefix, Throwable tr, String format, Object... args) {
+ android.util.Log.wtf(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args),
+ tr);
+ }
+
+ public static void wtf(String prefix, String format, Object... args) {
+ String msg = buildMessage(prefix, format, args);
+ android.util.Log.wtf(TAG, msg, new IllegalStateException(msg));
+ }
+
+ public static void wtf(Object objectPrefix, String format, Object... args) {
+ String msg = buildMessage(getPrefixFromObject(objectPrefix), format, args);
+ android.util.Log.wtf(TAG, msg, new IllegalStateException(msg));
+ }
+
+ public static String piiHandle(Object pii) {
+ if (pii == null || VERBOSE) {
+ return String.valueOf(pii);
+ }
+
+ if (pii instanceof Uri) {
+ Uri uri = (Uri) pii;
+
+ // All Uri's which are not "tel" go through normal pii() method.
+ if (!PhoneAccount.SCHEME_TEL.equals(uri.getScheme())) {
+ return pii(pii);
+ } else {
+ pii = uri.getSchemeSpecificPart();
+ }
+ }
+
+ String originalString = String.valueOf(pii);
+ StringBuilder stringBuilder = new StringBuilder(originalString.length());
+ for (char c : originalString.toCharArray()) {
+ if (PhoneNumberUtils.isDialable(c)) {
+ stringBuilder.append('*');
+ } else {
+ stringBuilder.append(c);
+ }
+ }
+ return stringBuilder.toString();
+ }
+
+ /**
+ * Redact personally identifiable information for production users.
+ * If we are running in verbose mode, return the original string, otherwise
+ * return a SHA-1 hash of the input string.
+ */
+ public static String pii(Object pii) {
+ if (pii == null || VERBOSE) {
+ return String.valueOf(pii);
+ }
+ return "[" + secureHash(String.valueOf(pii).getBytes()) + "]";
+ }
+
+ private static String secureHash(byte[] input) {
+ MessageDigest messageDigest;
+ try {
+ messageDigest = MessageDigest.getInstance("SHA-1");
+ } catch (NoSuchAlgorithmException e) {
+ return null;
+ }
+ messageDigest.update(input);
+ byte[] result = messageDigest.digest();
+ return encodeHex(result);
+ }
+
+ private static String encodeHex(byte[] bytes) {
+ StringBuffer hex = new StringBuffer(bytes.length * 2);
+
+ for (int i = 0; i < bytes.length; i++) {
+ int byteIntValue = bytes[i] & 0xff;
+ if (byteIntValue < 0x10) {
+ hex.append("0");
+ }
+ hex.append(Integer.toString(byteIntValue, 16));
+ }
+
+ return hex.toString();
+ }
+
+ private static String getPrefixFromObject(Object obj) {
+ return obj == null ? "<null>" : obj.getClass().getSimpleName();
+ }
+
+ private static String buildMessage(String prefix, String format, Object... args) {
+ String msg;
+ try {
+ msg = (args == null || args.length == 0) ? format
+ : String.format(Locale.US, format, args);
+ } catch (IllegalFormatException ife) {
+ e("Log", ife, "IllegalFormatException: formatString='%s' numArgs=%d", format,
+ args.length);
+ msg = format + " (An error occurred while formatting the message.)";
+ }
+ return String.format(Locale.US, "%s: %s", prefix, msg);
+ }
+}
diff --git a/src/com/android/server/telecom/MissedCallNotifier.java b/src/com/android/server/telecom/MissedCallNotifier.java
new file mode 100644
index 0000000..553d2e1
--- /dev/null
+++ b/src/com/android/server/telecom/MissedCallNotifier.java
@@ -0,0 +1,322 @@
+/*
+ * 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;
+
+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.provider.CallLog;
+import android.provider.CallLog.Calls;
+import android.telecom.CallState;
+import android.telephony.DisconnectCause;
+import android.text.BidiFormatter;
+import android.text.TextDirectionHeuristics;
+import android.text.TextUtils;
+
+/**
+ * Creates a notification for calls that the user missed (neither answered nor rejected).
+ * TODO: Make TelephonyManager.clearMissedCalls call into this class.
+ * STOPSHIP: Resolve b/13769374 about moving this class to InCall.
+ */
+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 MISSED_CALL_NOTIFICATION_ID = 1;
+
+ private final Context mContext;
+ 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();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void onCallStateChanged(Call call, int oldState, int newState) {
+ if (oldState == CallState.RINGING && newState == CallState.DISCONNECTED &&
+ call.getDisconnectCause() == DisconnectCause.INCOMING_MISSED) {
+ showMissedCallNotification(call);
+ }
+ }
+
+ /** Clears missed call notification and marks the call log's missed calls as read. */
+ void clearMissedCalls() {
+ // 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.
+ */
+ private 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: <caller name || handle>
+ // More than 1 missed call: <number of calls> + "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.notify(MISSED_CALL_NOTIFICATION_ID, notification);
+ }
+
+ /** 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(
+ TelecomBroadcastReceiver.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(
+ TelecomBroadcastReceiver.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(
+ TelecomBroadcastReceiver.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 TelecomBroadcastReceiver}.
+ *
+ * @param action The intent action.
+ * @param data The intent data.
+ */
+ private PendingIntent createTelecomPendingIntent(String action, Uri data) {
+ Intent intent = new Intent(action, data, mContext, TelecomBroadcastReceiver.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
+ Uri handle = Uri.parse(cursor.getString(
+ cursor.getColumnIndexOrThrow(Calls.NUMBER)));
+ int presentation = cursor.getInt(cursor.getColumnIndexOrThrow(
+ Calls.NUMBER_PRESENTATION));
+
+ if (presentation != Calls.PRESENTATION_ALLOWED) {
+ handle = null;
+ }
+
+ // Convert the data to a call object
+ Call call = new Call(null, null, null, null, null, true, false);
+ call.setDisconnectCause(DisconnectCause.INCOMING_MISSED, "");
+ call.setState(CallState.DISCONNECTED);
+
+ // 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/src/com/android/server/telecom/MultiLineTitleEditTextPreference.java b/src/com/android/server/telecom/MultiLineTitleEditTextPreference.java
new file mode 100644
index 0000000..d94dc60
--- /dev/null
+++ b/src/com/android/server/telecom/MultiLineTitleEditTextPreference.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2011 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;
+import android.preference.EditTextPreference;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.TextView;
+
+/**
+ * Ultra-simple subclass of EditTextPreference that allows the "title" to wrap
+ * onto multiple lines.
+ *
+ * (By default, the title of an EditTextPreference is singleLine="true"; see
+ * preference_holo.xml under frameworks/base. But in the "Respond via SMS"
+ * settings UI we want titles to be multi-line, since the customized messages
+ * might be fairly long, and should be able to wrap.)
+ *
+ * TODO: This is pretty cumbersome; it would be nicer for the framework to
+ * either allow modifying the title's attributes in XML, or at least provide
+ * some way from Java (given an EditTextPreference) to reach inside and get a
+ * handle to the "title" TextView.
+ *
+ * TODO: Also, it would reduce clutter if this could be an inner class in
+ * RespondViaSmsManager.java, but then there would be no way to reference the
+ * class from XML. That's because
+ * <com.android.server.telecom.MultiLineTitleEditTextPreference ... />
+ * isn't valid XML syntax due to the "$" character. And Preference
+ * elements don't have a "class" attribute, so you can't do something like
+ * <view class="com.android.server.telecom.Foo$Bar"> as you can with regular views.
+ */
+public class MultiLineTitleEditTextPreference extends EditTextPreference {
+ public MultiLineTitleEditTextPreference(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public MultiLineTitleEditTextPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public MultiLineTitleEditTextPreference(Context context) {
+ super(context);
+ }
+
+ // The "title" TextView inside an EditTextPreference defaults to
+ // singleLine="true" (see preference_holo.xml under frameworks/base.)
+ // We override onBindView() purely to look up that TextView and call
+ // setSingleLine(false) on it.
+ @Override
+ protected void onBindView(View view) {
+ super.onBindView(view);
+
+ TextView textView = (TextView) view.findViewById(com.android.internal.R.id.title);
+ if (textView != null) {
+ textView.setSingleLine(false);
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java b/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
new file mode 100644
index 0000000..3fbe498
--- /dev/null
+++ b/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
@@ -0,0 +1,419 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.UserHandle;
+import android.telecom.GatewayInfo;
+import android.telecom.PhoneAccount;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
+import android.telephony.DisconnectCause;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+
+/**
+ * OutgoingCallIntentBroadcaster receives CALL and CALL_PRIVILEGED Intents, and broadcasts the
+ * ACTION_NEW_OUTGOING_CALL intent. ACTION_NEW_OUTGOING_CALL is an ordered broadcast intent which
+ * contains the phone number being dialed. Applications can use this intent to (1) see which numbers
+ * are being dialed, (2) redirect a call (change the number being dialed), or (3) prevent a call
+ * from being placed.
+ *
+ * After the other applications have had a chance to see the ACTION_NEW_OUTGOING_CALL intent, it
+ * finally reaches the {@link NewOutgoingCallBroadcastIntentReceiver}.
+ *
+ * Calls where no number is present (like for a CDMA "empty flash" or a nonexistent voicemail
+ * number) are exempt from being broadcast.
+ *
+ * Calls to emergency numbers are still broadcast for informative purposes. The call is placed
+ * prior to sending ACTION_NEW_OUTGOING_CALL and cannot be redirected nor prevented.
+ */
+class NewOutgoingCallIntentBroadcaster {
+ /** Required permission for any app that wants to consume ACTION_NEW_OUTGOING_CALL. */
+ private static final String PERMISSION = android.Manifest.permission.PROCESS_OUTGOING_CALLS;
+
+ private static final String EXTRA_ACTUAL_NUMBER_TO_DIAL =
+ "android.telecom.extra.ACTUAL_NUMBER_TO_DIAL";
+
+ /**
+ * Legacy string constants used to retrieve gateway provider extras from intents. These still
+ * need to be copied from the source call intent to the destination intent in order to
+ * support third party gateway providers that are still using old string constants in
+ * Telephony.
+ */
+ public static final String EXTRA_GATEWAY_PROVIDER_PACKAGE =
+ "com.android.phone.extra.GATEWAY_PROVIDER_PACKAGE";
+ public static final String EXTRA_GATEWAY_URI = "com.android.phone.extra.GATEWAY_URI";
+ public static final String EXTRA_GATEWAY_ORIGINAL_URI =
+ "com.android.phone.extra.GATEWAY_ORIGINAL_URI";
+
+ private final CallsManager mCallsManager;
+ private final Call mCall;
+ private final Intent mIntent;
+ /*
+ * Whether or not the outgoing call intent originated from the default phone application. If
+ * so, it will be allowed to make emergency calls, even with the ACTION_CALL intent.
+ */
+ private final boolean mIsDefaultOrSystemPhoneApp;
+
+ NewOutgoingCallIntentBroadcaster(CallsManager callsManager, Call call, Intent intent,
+ boolean isDefaultPhoneApp) {
+ mCallsManager = callsManager;
+ mCall = call;
+ mIntent = intent;
+ mIsDefaultOrSystemPhoneApp = isDefaultPhoneApp;
+ }
+
+ /**
+ * Processes the result of the outgoing call broadcast intent, and performs callbacks to
+ * the OutgoingCallIntentBroadcasterListener as necessary.
+ */
+ private class NewOutgoingCallBroadcastIntentReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.v(this, "onReceive: %s", intent);
+
+ // Once the NEW_OUTGOING_CALL broadcast is finished, the resultData is used as the
+ // actual number to call. (If null, no call will be placed.)
+ String resultNumber = getResultData();
+ Log.v(this, "- got number from resultData: %s", Log.pii(resultNumber));
+
+ boolean endEarly = false;
+ if (resultNumber == null) {
+ Log.v(this, "Call cancelled (null number), returning...");
+ endEarly = true;
+ } else if (PhoneNumberUtils.isPotentialLocalEmergencyNumber(context, resultNumber)) {
+ Log.w(this, "Cannot modify outgoing call to emergency number %s.", resultNumber);
+ endEarly = true;
+ }
+
+ if (endEarly) {
+ if (mCall != null) {
+ mCall.disconnect();
+ }
+ return;
+ }
+
+ Uri resultHandleUri = Uri.fromParts(PhoneNumberUtils.isUriNumber(resultNumber) ?
+ PhoneAccount.SCHEME_SIP : PhoneAccount.SCHEME_TEL, resultNumber, null);
+
+ Uri originalUri = mIntent.getData();
+
+ if (originalUri.getSchemeSpecificPart().equals(resultNumber)) {
+ Log.v(this, "Call number unmodified after new outgoing call intent broadcast.");
+ } else {
+ Log.v(this, "Retrieved modified handle after outgoing call intent broadcast: "
+ + "Original: %s, Modified: %s",
+ Log.pii(originalUri),
+ Log.pii(resultHandleUri));
+ }
+
+ GatewayInfo gatewayInfo = getGateWayInfoFromIntent(intent, resultHandleUri);
+ mCallsManager.placeOutgoingCall(mCall, resultHandleUri, gatewayInfo,
+ mIntent.getBooleanExtra(TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE,
+ false),
+ mIntent.getIntExtra(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+ VideoProfile.VideoState.AUDIO_ONLY));
+ }
+ }
+
+ /**
+ * Processes the supplied intent and starts the outgoing call broadcast process relevant to the
+ * intent.
+ *
+ * This method will handle three kinds of actions:
+ *
+ * - CALL (intent launched by all third party dialers)
+ * - CALL_PRIVILEGED (intent launched by system apps e.g. system Dialer, voice Dialer)
+ * - CALL_EMERGENCY (intent launched by lock screen emergency dialer)
+ *
+ * @return {@link CallActivity#OUTGOING_CALL_SUCCEEDED} if the call succeeded, and an
+ * appropriate {@link DisconnectCause} if the call did not, describing why it failed.
+ */
+ int processIntent() {
+ Log.v(this, "Processing call intent in OutgoingCallIntentBroadcaster.");
+
+ final Context context = TelecomApp.getInstance();
+
+ Intent intent = mIntent;
+ String action = intent.getAction();
+ final Uri handle = intent.getData();
+
+ if (handle == null) {
+ Log.w(this, "Empty handle obtained from the call intent.");
+ return DisconnectCause.INVALID_NUMBER;
+ }
+
+ boolean isVoicemailNumber = PhoneAccount.SCHEME_VOICEMAIL.equals(handle.getScheme());
+ if (isVoicemailNumber) {
+ if (Intent.ACTION_CALL.equals(action)) {
+ // Voicemail calls will be handled directly by the telephony connection manager
+ Log.i(this, "Placing call immediately instead of waiting for "
+ + " OutgoingCallBroadcastReceiver: %s", intent);
+
+ boolean speakerphoneOn = mIntent.getBooleanExtra(
+ TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, false);
+ mCallsManager.placeOutgoingCall(mCall, handle, null, speakerphoneOn,
+ VideoProfile.VideoState.AUDIO_ONLY);
+
+ return DisconnectCause.NOT_DISCONNECTED;
+ } else {
+ Log.i(this, "Unhandled intent %s. Ignoring and not placing call.", intent);
+ return DisconnectCause.OUTGOING_CANCELED;
+ }
+ }
+
+ String number = PhoneNumberUtils.getNumberFromIntent(intent, context);
+ if (TextUtils.isEmpty(number)) {
+ Log.w(this, "Empty number obtained from the call intent.");
+ return DisconnectCause.NO_PHONE_NUMBER_SUPPLIED;
+ }
+
+ boolean isUriNumber = PhoneNumberUtils.isUriNumber(number);
+ if (!isUriNumber) {
+ number = PhoneNumberUtils.convertKeypadLettersToDigits(number);
+ number = PhoneNumberUtils.stripSeparators(number);
+ }
+
+ final boolean isPotentialEmergencyNumber = isPotentialEmergencyNumber(context, number);
+ Log.v(this, "isPotentialEmergencyNumber = %s", isPotentialEmergencyNumber);
+
+ rewriteCallIntentAction(intent, isPotentialEmergencyNumber);
+ action = intent.getAction();
+ // True for certain types of numbers that are not intended to be intercepted or modified
+ // by third parties (e.g. emergency numbers).
+ boolean callImmediately = false;
+
+ if (Intent.ACTION_CALL.equals(action)) {
+ if (isPotentialEmergencyNumber) {
+ if (!mIsDefaultOrSystemPhoneApp) {
+ Log.w(this, "Cannot call potential emergency number %s with CALL Intent %s "
+ + "unless caller is system or default dialer.", number, intent);
+ launchSystemDialer(context, intent.getData());
+ return DisconnectCause.OUTGOING_CANCELED;
+ } else {
+ callImmediately = true;
+ }
+ }
+ } else if (Intent.ACTION_CALL_EMERGENCY.equals(action)) {
+ if (!isPotentialEmergencyNumber) {
+ Log.w(this, "Cannot call non-potential-emergency number %s with EMERGENCY_CALL "
+ + "Intent %s.", number, intent);
+ return DisconnectCause.OUTGOING_CANCELED;
+ }
+ callImmediately = true;
+ } else {
+ Log.w(this, "Unhandled Intent %s. Ignoring and not placing call.", intent);
+ return DisconnectCause.INVALID_NUMBER;
+ }
+
+ if (callImmediately) {
+ Log.i(this, "Placing call immediately instead of waiting for "
+ + " OutgoingCallBroadcastReceiver: %s", intent);
+ String scheme = isUriNumber ? PhoneAccount.SCHEME_SIP : PhoneAccount.SCHEME_TEL;
+ boolean speakerphoneOn = mIntent.getBooleanExtra(
+ TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, false);
+ int videoState = mIntent.getIntExtra(
+ TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+ VideoProfile.VideoState.AUDIO_ONLY);
+ mCallsManager.placeOutgoingCall(mCall, Uri.fromParts(scheme, number, null), null,
+ speakerphoneOn, videoState);
+
+ // Don't return but instead continue and send the ACTION_NEW_OUTGOING_CALL broadcast
+ // so that third parties can still inspect (but not intercept) the outgoing call. When
+ // the broadcast finally reaches the OutgoingCallBroadcastReceiver, we'll know not to
+ // initiate the call again because of the presence of the EXTRA_ALREADY_CALLED extra.
+ }
+
+ broadcastIntent(intent, number, context, !callImmediately);
+ return DisconnectCause.NOT_DISCONNECTED;
+ }
+
+ /**
+ * Sends a new outgoing call ordered broadcast so that third party apps can cancel the
+ * placement of the call or redirect it to a different number.
+ *
+ * @param originalCallIntent The original call intent.
+ * @param number Call number that was stored in the original call intent.
+ * @param context Valid context to send the ordered broadcast using.
+ * @param receiverRequired Whether or not the result from the ordered broadcast should be
+ * processed using a {@link NewOutgoingCallIntentBroadcaster}.
+ */
+ private void broadcastIntent(
+ Intent originalCallIntent,
+ String number,
+ Context context,
+ boolean receiverRequired) {
+ Intent broadcastIntent = new Intent(Intent.ACTION_NEW_OUTGOING_CALL);
+ if (number != null) {
+ broadcastIntent.putExtra(Intent.EXTRA_PHONE_NUMBER, number);
+ }
+
+ // Force receivers of this broadcast intent to run at foreground priority because we
+ // want to finish processing the broadcast intent as soon as possible.
+ broadcastIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+ Log.v(this, "Broadcasting intent: %s.", broadcastIntent);
+
+ checkAndCopyProviderExtras(originalCallIntent, broadcastIntent);
+
+ context.sendOrderedBroadcastAsUser(
+ broadcastIntent,
+ UserHandle.OWNER,
+ PERMISSION,
+ receiverRequired ? new NewOutgoingCallBroadcastIntentReceiver() : null,
+ null, // scheduler
+ Activity.RESULT_OK, // initialCode
+ number, // initialData: initial value for the result data (number to be modified)
+ null); // initialExtras
+ }
+
+ /**
+ * Copy all the expected extras set when a 3rd party gateway provider is to be used, from the
+ * source intent to the destination one.
+ *
+ * @param src Intent which may contain the provider's extras.
+ * @param dst Intent where a copy of the extras will be added if applicable.
+ */
+ public void checkAndCopyProviderExtras(Intent src, Intent dst) {
+ if (src == null) {
+ return;
+ }
+ if (hasGatewayProviderExtras(src)) {
+ dst.putExtra(EXTRA_GATEWAY_PROVIDER_PACKAGE,
+ src.getStringExtra(EXTRA_GATEWAY_PROVIDER_PACKAGE));
+ dst.putExtra(EXTRA_GATEWAY_URI,
+ src.getStringExtra(EXTRA_GATEWAY_URI));
+ Log.d(this, "Found and copied gateway provider extras to broadcast intent.");
+ return;
+ }
+
+ Log.d(this, "No provider extras found in call intent.");
+ }
+
+ /**
+ * Check if valid gateway provider information is stored as extras in the intent
+ *
+ * @param intent to check for
+ * @return true if the intent has all the gateway information extras needed.
+ */
+ private boolean hasGatewayProviderExtras(Intent intent) {
+ final String name = intent.getStringExtra(EXTRA_GATEWAY_PROVIDER_PACKAGE);
+ final String uriString = intent.getStringExtra(EXTRA_GATEWAY_URI);
+
+ return !TextUtils.isEmpty(name) && !TextUtils.isEmpty(uriString);
+ }
+
+ private static Uri getGatewayUriFromString(String gatewayUriString) {
+ return TextUtils.isEmpty(gatewayUriString) ? null : Uri.parse(gatewayUriString);
+ }
+
+ /**
+ * Extracts gateway provider information from a provided intent..
+ *
+ * @param intent to extract gateway provider information from.
+ * @param trueHandle The actual call handle that the user is trying to dial
+ * @return GatewayInfo object containing extracted gateway provider information as well as
+ * the actual handle the user is trying to dial.
+ */
+ public static GatewayInfo getGateWayInfoFromIntent(Intent intent, Uri trueHandle) {
+ if (intent == null) {
+ return null;
+ }
+
+ // Check if gateway extras are present.
+ String gatewayPackageName = intent.getStringExtra(EXTRA_GATEWAY_PROVIDER_PACKAGE);
+ Uri gatewayUri = getGatewayUriFromString(intent.getStringExtra(EXTRA_GATEWAY_URI));
+ if (!TextUtils.isEmpty(gatewayPackageName) && gatewayUri != null) {
+ return new GatewayInfo(gatewayPackageName, gatewayUri, trueHandle);
+ }
+
+ return null;
+ }
+
+ private void launchSystemDialer(Context context, Uri handle) {
+ Intent systemDialerIntent = new Intent();
+ final Resources resources = context.getResources();
+ systemDialerIntent.setClassName(
+ resources.getString(R.string.ui_default_package),
+ resources.getString(R.string.dialer_default_class));
+ systemDialerIntent.setAction(Intent.ACTION_DIAL);
+ systemDialerIntent.setData(handle);
+ systemDialerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ Log.v(this, "calling startActivity for default dialer: %s", systemDialerIntent);
+ context.startActivity(systemDialerIntent);
+ }
+
+ /**
+ * Check whether or not this is an emergency number, in order to enforce the restriction
+ * that only the CALL_PRIVILEGED and CALL_EMERGENCY intents are allowed to make emergency
+ * calls.
+ *
+ * To prevent malicious 3rd party apps from making emergency calls by passing in an
+ * "invalid" number like "9111234" (that isn't technically an emergency number but might
+ * still result in an emergency call with some networks), we use
+ * isPotentialLocalEmergencyNumber instead of isLocalEmergencyNumber.
+ *
+ * @param context Valid context
+ * @param number number to inspect in order to determine whether or not an emergency number
+ * is potentially being dialed
+ * @return True if the handle is potentially an emergency number.
+ */
+ private boolean isPotentialEmergencyNumber(Context context, String number) {
+ Log.v(this, "Checking restrictions for number : %s", Log.pii(number));
+ return (number != null) && PhoneNumberUtils.isPotentialLocalEmergencyNumber(context,number);
+ }
+
+ /**
+ * Given a call intent and whether or not the number to dial is an emergency number, rewrite
+ * the call intent action to an appropriate one.
+ *
+ * @param intent Intent to rewrite the action for
+ * @param isPotentialEmergencyNumber Whether or not the number is potentially an emergency
+ * number.
+ */
+ private void rewriteCallIntentAction(Intent intent, boolean isPotentialEmergencyNumber) {
+ if (CallActivity.class.getName().equals(intent.getComponent().getClassName())) {
+ // If we were launched directly from the CallActivity, not one of its more privileged
+ // aliases, then make sure that only the non-privileged actions are allowed.
+ if (!Intent.ACTION_CALL.equals(intent.getAction())) {
+ Log.w(this, "Attempt to deliver non-CALL action; forcing to CALL");
+ intent.setAction(Intent.ACTION_CALL);
+ }
+ }
+
+ String action = intent.getAction();
+
+ /* Change CALL_PRIVILEGED into CALL or CALL_EMERGENCY as needed. */
+ if (Intent.ACTION_CALL_PRIVILEGED.equals(action)) {
+ if (isPotentialEmergencyNumber) {
+ Log.i(this, "ACTION_CALL_PRIVILEGED is used while the number is a potential"
+ + " emergency number. Using ACTION_CALL_EMERGENCY as an action instead.");
+ action = Intent.ACTION_CALL_EMERGENCY;
+ } else {
+ action = Intent.ACTION_CALL;
+ }
+ Log.v(this, " - updating action from CALL_PRIVILEGED to %s", action);
+ intent.setAction(action);
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/PhoneAccountBroadcastReceiver.java b/src/com/android/server/telecom/PhoneAccountBroadcastReceiver.java
new file mode 100644
index 0000000..6152bef
--- /dev/null
+++ b/src/com/android/server/telecom/PhoneAccountBroadcastReceiver.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.telecom;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+
+import java.lang.String;
+
+/**
+ * Captures {@code android.intent.action.ACTION_PACKAGE_FULLY_REMOVED} intents and triggers the
+ * removal of associated {@link android.telecom.PhoneAccount}s via the
+ * {@link com.android.telecom.PhoneAccountRegistrar}.
+ * Note: This class listens for the {@code PACKAGE_FULLY_REMOVED} intent rather than
+ * {@code PACKAGE_REMOVED} as {@code PACKAGE_REMOVED} is triggered on re-installation of the same
+ * package, where {@code PACKAGE_FULLY_REMOVED} is triggered only when an application is completely
+ * uninstalled. This is desirable as we do not wish to un-register all
+ * {@link android.telecom.PhoneAccount}s associated with a package being re-installed to ensure
+ * the enabled state of the accounts is retained.
+ */
+public class PhoneAccountBroadcastReceiver extends BroadcastReceiver {
+ /**
+ * Receives the intents the class is configured to received.
+ *
+ * @param context The Context in which the receiver is running.
+ * @param intent The Intent being received.
+ */
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(intent.getAction())) {
+ Uri uri = intent.getData();
+ if (uri == null) {
+ return;
+ }
+
+ String packageName = uri.getSchemeSpecificPart();
+ handlePackageRemoved(context, packageName);
+ }
+ }
+
+ /**
+ * Handles the removal of a package by calling upon the {@link PhoneAccountRegistrar} to
+ * un-register any {@link android.telecom.PhoneAccount}s associated with the package.
+ *
+ * @param packageName The name of the removed package.
+ */
+ private void handlePackageRemoved(Context context, String packageName) {
+ TelecomApp telecomApp = TelecomApp.getInstance();
+ if (telecomApp == null) {
+ return;
+ }
+
+ PhoneAccountRegistrar registrar = telecomApp.getPhoneAccountRegistrar();
+ if (registrar == null) {
+ return;
+ }
+
+ registrar.clearAccounts(packageName);
+ }
+}
diff --git a/src/com/android/server/telecom/PhoneAccountRegistrar.java b/src/com/android/server/telecom/PhoneAccountRegistrar.java
new file mode 100644
index 0000000..31dd727
--- /dev/null
+++ b/src/com/android/server/telecom/PhoneAccountRegistrar.java
@@ -0,0 +1,1041 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.Manifest;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.provider.Settings;
+import android.telecom.ConnectionService;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.AtomicFile;
+import android.util.Xml;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.FastXmlSerializer;
+import com.android.internal.util.XmlUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.Integer;
+import java.lang.SecurityException;
+import java.lang.String;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Handles writing and reading PhoneAccountHandle registration entries. This is a simple verbatim
+ * delegate for all the account handling methods on {@link android.telecom.TelecomManager} as implemented in
+ * {@link TelecomServiceImpl}, with the notable exception that {@link TelecomServiceImpl} is
+ * responsible for security checking to make sure that the caller has proper authority over
+ * the {@code ComponentName}s they are declaring in their {@code PhoneAccountHandle}s.
+ */
+public final class PhoneAccountRegistrar {
+
+ public static final PhoneAccountHandle NO_ACCOUNT_SELECTED =
+ new PhoneAccountHandle(new ComponentName("null", "null"), "NO_ACCOUNT_SELECTED");
+
+ public abstract static class Listener {
+ public void onAccountsChanged(PhoneAccountRegistrar registrar) {}
+ public void onDefaultOutgoingChanged(PhoneAccountRegistrar registrar) {}
+ public void onSimCallManagerChanged(PhoneAccountRegistrar registrar) {}
+ }
+
+ private static final String FILE_NAME = "phone-account-registrar-state.xml";
+ @VisibleForTesting
+ public static final int EXPECTED_STATE_VERSION = 3;
+
+ /** Keep in sync with the same in SipSettings.java */
+ private static final String SIP_SHARED_PREFERENCES = "SIP_PREFERENCES";
+
+ private final List<Listener> mListeners = new CopyOnWriteArrayList<>();
+ private final AtomicFile mAtomicFile;
+ private final Context mContext;
+ private State mState;
+
+ public PhoneAccountRegistrar(Context context) {
+ this(context, FILE_NAME);
+ }
+
+ @VisibleForTesting
+ public PhoneAccountRegistrar(Context context, String fileName) {
+ // TODO: Change file location when Telecom is part of system
+ mAtomicFile = new AtomicFile(new File(context.getFilesDir(), fileName));
+ mState = new State();
+ mContext = context;
+ read();
+ }
+
+ /**
+ * Retrieves the default outgoing phone account supporting the specified uriScheme.
+ * @param uriScheme The URI scheme for the outgoing call.
+ * @return The {@link PhoneAccountHandle} to use.
+ */
+ public PhoneAccountHandle getDefaultOutgoingPhoneAccount(String uriScheme) {
+ final PhoneAccountHandle userSelected = getUserSelectedOutgoingPhoneAccount();
+
+ if (userSelected != null) {
+ // If there is a default PhoneAccount, ensure it supports calls to handles with the
+ // specified uriScheme.
+ final PhoneAccount userSelectedAccount = getPhoneAccount(userSelected);
+ if (userSelectedAccount.supportsUriScheme(uriScheme)) {
+ return userSelected;
+ }
+ }
+
+ List<PhoneAccountHandle> outgoing = getEnabledPhoneAccounts(uriScheme);
+ switch (outgoing.size()) {
+ case 0:
+ // There are no accounts, so there can be no default
+ return null;
+ case 1:
+ // There is only one account, which is by definition the default
+ return outgoing.get(0);
+ default:
+ // There are multiple accounts with no selected default
+ return null;
+ }
+ }
+
+ PhoneAccountHandle getUserSelectedOutgoingPhoneAccount() {
+ if (mState.defaultOutgoing != null) {
+ // Return the registered outgoing default iff it still exists (we keep a sticky
+ // default to survive account deletion and re-addition)
+ for (int i = 0; i < mState.accounts.size(); i++) {
+ if (mState.accounts.get(i).getAccountHandle().equals(mState.defaultOutgoing)) {
+ return mState.defaultOutgoing;
+ }
+ }
+ // At this point, there was a registered default but it has been deleted; proceed
+ // as though there were no default
+ }
+ return null;
+ }
+
+ public void setUserSelectedOutgoingPhoneAccount(PhoneAccountHandle accountHandle) {
+ if (accountHandle == null) {
+ // Asking to clear the default outgoing is a valid request
+ mState.defaultOutgoing = null;
+ } else {
+ boolean found = false;
+ for (PhoneAccount m : mState.accounts) {
+ if (Objects.equals(accountHandle, m.getAccountHandle())) {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ Log.w(this, "Trying to set nonexistent default outgoing %s",
+ accountHandle);
+ return;
+ }
+
+ if (!getPhoneAccount(accountHandle).hasCapabilities(
+ PhoneAccount.CAPABILITY_CALL_PROVIDER)) {
+ Log.w(this, "Trying to set non-call-provider default outgoing %s",
+ accountHandle);
+ return;
+ }
+
+ mState.defaultOutgoing = accountHandle;
+ }
+
+ write();
+ fireDefaultOutgoingChanged();
+ }
+
+ public void setSimCallManager(PhoneAccountHandle callManager) {
+ if (!isEnabledConnectionManager()) {
+ return;
+ }
+
+ if (callManager != null) {
+ PhoneAccount callManagerAccount = getPhoneAccount(callManager);
+ if (callManagerAccount == null) {
+ Log.d(this, "setSimCallManager: Nonexistent call manager: %s", callManager);
+ return;
+ } else if (!callManagerAccount.hasCapabilities(
+ PhoneAccount.CAPABILITY_CONNECTION_MANAGER)) {
+ Log.d(this, "setSimCallManager: Not a call manager: %s", callManagerAccount);
+ return;
+ }
+ } else {
+ callManager = NO_ACCOUNT_SELECTED;
+ }
+ mState.simCallManager = callManager;
+
+ write();
+ fireSimCallManagerChanged();
+ }
+
+ public PhoneAccountHandle getSimCallManager() {
+ if (!isEnabledConnectionManager()) {
+ return null;
+ }
+
+ if (mState.simCallManager != null) {
+ if (NO_ACCOUNT_SELECTED.equals(mState.simCallManager)) {
+ return null;
+ }
+ // Return the registered sim call manager iff it still exists (we keep a sticky
+ // setting to survive account deletion and re-addition)
+ for (int i = 0; i < mState.accounts.size(); i++) {
+ if (mState.accounts.get(i).getAccountHandle().equals(mState.simCallManager)) {
+ return mState.simCallManager;
+ }
+ }
+ }
+
+ // See if the OEM has specified a default one.
+ String defaultConnectionMgr =
+ mContext.getResources().getString(R.string.default_connection_manager_component);
+ if (!TextUtils.isEmpty(defaultConnectionMgr)) {
+ PackageManager pm = mContext.getPackageManager();
+
+ ComponentName componentName = ComponentName.unflattenFromString(defaultConnectionMgr);
+ Intent intent = new Intent(ConnectionService.SERVICE_INTERFACE);
+ intent.setComponent(componentName);
+
+ // Make sure that the component can be resolved.
+ List<ResolveInfo> resolveInfos = pm.queryIntentServices(intent, 0);
+ if (!resolveInfos.isEmpty()) {
+ // See if there is registered PhoneAccount by this component.
+ List<PhoneAccountHandle> handles = getAllPhoneAccountHandles();
+ for (PhoneAccountHandle handle : handles) {
+ if (componentName.equals(handle.getComponentName())) {
+ return handle;
+ }
+ }
+ Log.d(this, "%s does not have a PhoneAccount; not using as default", componentName);
+ } else {
+ Log.d(this, "%s could not be resolved; not using as default", componentName);
+ }
+ } else {
+ Log.v(this, "No default connection manager specified");
+ }
+
+ return null;
+ }
+
+ /**
+ * Retrieves a list of all {@link PhoneAccountHandle}s registered.
+ *
+ * @return The list of {@link PhoneAccountHandle}s.
+ */
+ public List<PhoneAccountHandle> getAllPhoneAccountHandles() {
+ List<PhoneAccountHandle> accountHandles = new ArrayList<>();
+ for (PhoneAccount m : mState.accounts) {
+ accountHandles.add(m.getAccountHandle());
+ }
+ return accountHandles;
+ }
+
+ public List<PhoneAccount> getAllPhoneAccounts() {
+ return new ArrayList<>(mState.accounts);
+ }
+
+ /**
+ * Determines the number of enabled and disabled {@link PhoneAccount}s.
+ *
+ * @return The number of enabled and disabled {@link PhoneAccount}s
+ */
+ public int getAllPhoneAccountsCount() {
+ return mState.accounts.size();
+ }
+
+ /**
+ * Retrieves a list of all enabled call provider phone accounts.
+ *
+ * @return The phone account handles.
+ */
+ public List<PhoneAccountHandle> getEnabledPhoneAccounts() {
+ return getPhoneAccountHandles(PhoneAccount.CAPABILITY_CALL_PROVIDER);
+ }
+
+ /**
+ * Retrieves a list of all enabled phone account call provider phone accounts supporting the
+ * specified URI scheme.
+ *
+ * @param uriScheme The URI scheme.
+ * @return The phone account handles.
+ */
+ public List<PhoneAccountHandle> getEnabledPhoneAccounts(String uriScheme) {
+ return getPhoneAccountHandles(PhoneAccount.CAPABILITY_CALL_PROVIDER, uriScheme,
+ false /* includeDisabled */);
+ }
+
+ /**
+ * Retrieves a list of all enabled phone account handles with the connection manager capability.
+ *
+ * @return The phone account handles.
+ */
+ public List<PhoneAccountHandle> getConnectionManagerPhoneAccounts() {
+ if (isEnabledConnectionManager()) {
+ return getPhoneAccountHandles(PhoneAccount.CAPABILITY_CONNECTION_MANAGER,
+ null /* supportedUriScheme */, false /* includeDisabled */);
+ }
+ return Collections.emptyList();
+ }
+
+ public PhoneAccount getPhoneAccount(PhoneAccountHandle handle) {
+ for (PhoneAccount m : mState.accounts) {
+ if (Objects.equals(handle, m.getAccountHandle())) {
+ return m;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Changes the enabled state of the {@link PhoneAccount} identified by a
+ * {@link PhoneAccountHandle}.
+ *
+ * @param handle The {@link PhoneAccountHandle}.
+ * @param isEnabled The new enabled state of the {@link PhoneAccount}.
+ */
+ public void setPhoneAccountEnabled(PhoneAccountHandle handle, boolean isEnabled) {
+ PhoneAccount existing = getPhoneAccount(handle);
+ if (existing.isEnabled() == isEnabled) {
+ return;
+ }
+
+ // Do not permit PhoneAccounts which are marked as always enabled to be disabled.
+ if (existing.hasCapabilities(PhoneAccount.CAPABILITY_ALWAYS_ENABLED)) {
+ return;
+ }
+
+ // If we are disabling the current default outgoing phone account or Sim call manager we
+ // need to null out those preferences.
+ if (!isEnabled) {
+ if (mState.defaultOutgoing != null && mState.defaultOutgoing.equals(handle)) {
+ setUserSelectedOutgoingPhoneAccount(null);
+ }
+
+ if (mState.simCallManager != null && mState.simCallManager.equals(handle)) {
+ setSimCallManager(null);
+ }
+ }
+
+ PhoneAccount.Builder builder = existing.toBuilder().setEnabled(isEnabled);
+ PhoneAccount replacement = builder.build();
+ addOrReplacePhoneAccount(replacement);
+
+ // Notify the package which registered this PhoneAccount of its new enabled state.
+ notifyPhoneAccountEnabledStateChanged(replacement.getAccountHandle(), isEnabled);
+ }
+
+ // TODO: Should we implement an artificial limit for # of accounts associated with a single
+ // ComponentName?
+ public void registerPhoneAccount(PhoneAccount account) {
+ // Enforce the requirement that a connection service for a phone account has the correct
+ // permission.
+ if (!phoneAccountHasPermission(account.getAccountHandle())) {
+ Log.w(this, "Phone account %s does not have BIND_CONNECTION_SERVICE permission.",
+ account.getAccountHandle());
+ throw new SecurityException(
+ "PhoneAccount connection service requires BIND_CONNECTION_SERVICE permission.");
+ }
+
+ // If there is an existing PhoneAccount already registered with this handle, copy its
+ // enabled state to the new phone account.
+ PhoneAccount existing = getPhoneAccount(account.getAccountHandle());
+ if (existing != null) {
+ account = account.toBuilder().setEnabled(existing.isEnabled()).build();
+ }
+
+ addOrReplacePhoneAccount(account);
+ }
+
+ /**
+ * Adds a {@code PhoneAccount}, replacing an existing one if found.
+ *
+ * @param account The {@code PhoneAccount} to add or replace.
+ */
+ private void addOrReplacePhoneAccount(PhoneAccount account) {
+ mState.accounts.add(account);
+ // Search for duplicates and remove any that are found.
+ for (int i = 0; i < mState.accounts.size() - 1; i++) {
+ if (Objects.equals(
+ account.getAccountHandle(), mState.accounts.get(i).getAccountHandle())) {
+ // replace existing entry.
+ mState.accounts.remove(i);
+ break;
+ }
+ }
+
+ write();
+ fireAccountsChanged();
+ }
+
+ public void unregisterPhoneAccount(PhoneAccountHandle accountHandle) {
+ for (int i = 0; i < mState.accounts.size(); i++) {
+ if (Objects.equals(accountHandle, mState.accounts.get(i).getAccountHandle())) {
+ mState.accounts.remove(i);
+ break;
+ }
+ }
+
+ write();
+ fireAccountsChanged();
+ }
+
+ /**
+ * Un-registers all phone accounts associated with a specified package.
+ *
+ * @param packageName The package for which phone accounts will be removed.
+ */
+ public void clearAccounts(String packageName) {
+ boolean accountsRemoved = false;
+ Iterator<PhoneAccount> it = mState.accounts.iterator();
+ while (it.hasNext()) {
+ PhoneAccount phoneAccount = it.next();
+ if (Objects.equals(
+ packageName,
+ phoneAccount.getAccountHandle().getComponentName().getPackageName())) {
+ Log.i(this, "Removing phone account " + phoneAccount.getLabel());
+ it.remove();
+ accountsRemoved = true;
+ }
+ }
+
+ if (accountsRemoved) {
+ write();
+ fireAccountsChanged();
+ }
+ }
+
+ public void addListener(Listener l) {
+ mListeners.add(l);
+ }
+
+ public void removeListener(Listener l) {
+ if (l != null) {
+ mListeners.remove(l);
+ }
+ }
+
+ private void fireAccountsChanged() {
+ for (Listener l : mListeners) {
+ l.onAccountsChanged(this);
+ }
+ }
+
+ private void fireDefaultOutgoingChanged() {
+ for (Listener l : mListeners) {
+ l.onDefaultOutgoingChanged(this);
+ }
+ }
+
+ private void fireSimCallManagerChanged() {
+ for (Listener l : mListeners) {
+ l.onSimCallManagerChanged(this);
+ }
+ }
+
+ private boolean isEnabledConnectionManager() {
+ return mContext.getResources().getBoolean(R.bool.connection_manager_enabled);
+ }
+
+ /**
+ * Determines if the connection service specified by a {@link PhoneAccountHandle} has the
+ * {@link Manifest.permission#BIND_CONNECTION_SERVICE} permission.
+ *
+ * @param phoneAccountHandle The phone account to check.
+ * @return {@code True} if the phone account has permission.
+ */
+ public boolean phoneAccountHasPermission(PhoneAccountHandle phoneAccountHandle) {
+ PackageManager packageManager = TelecomApp.getInstance().getPackageManager();
+ try {
+ ServiceInfo serviceInfo = packageManager.getServiceInfo(
+ phoneAccountHandle.getComponentName(), 0);
+
+ return serviceInfo.permission != null &&
+ serviceInfo.permission.equals(Manifest.permission.BIND_CONNECTION_SERVICE);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.w(this, "Name not found %s", e);
+ return false;
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Returns a list of phone account handles with the specified flag.
+ *
+ * @param flags Flags which the {@code PhoneAccount} must have.
+ */
+ private List<PhoneAccountHandle> getPhoneAccountHandles(int flags) {
+ return getPhoneAccountHandles(flags, null, false /* includeDisabled */);
+ }
+
+ /**
+ * Returns a list of phone account handles with the specified flag, supporting the specified
+ * URI scheme. By default, only enabled phone accounts are included, unless the
+ * {@code includeDisabled} parameter is set {@code true}.
+ *
+ * @param flags Flags which the {@code PhoneAccount} must have.
+ * @param uriScheme URI schemes the PhoneAccount must handle. {@code Null} bypasses the
+ * URI scheme check.
+ * @param includeDisabled When {@code true}, the list of phone accounts handles includes those
+ * which are marked as disabled.
+ */
+ private List<PhoneAccountHandle> getPhoneAccountHandles(int flags, String uriScheme,
+ boolean includeDisabled) {
+ List<PhoneAccountHandle> accountHandles = new ArrayList<>();
+ for (PhoneAccount m : mState.accounts) {
+ if ((includeDisabled || m.isEnabled()) && m.hasCapabilities(flags) &&
+ (uriScheme == null || m.supportsUriScheme(uriScheme))) {
+ accountHandles.add(m.getAccountHandle());
+ }
+ }
+ return accountHandles;
+ }
+
+ /**
+ * Notifies the package which registered a {@link PhoneAccount} that it has been enabled.
+ * Only broadcasts the intent if the package has a {@link android.content.BroadcastReceiver}
+ * registered for the intent.
+ *
+ * @param phoneAccountHandle The {@link PhoneAccountHandle} which has been enabled or disabled.
+ * @param isEnabled {@code True} if the {@link PhoneAccount} is enabled, false otherwise.
+ */
+ private void notifyPhoneAccountEnabledStateChanged(PhoneAccountHandle phoneAccountHandle,
+ boolean isEnabled) {
+ Intent intent;
+
+ if (isEnabled) {
+ intent = new Intent(TelecomManager.ACTION_PHONE_ACCOUNT_ENABLED);
+ } else {
+ intent = new Intent(TelecomManager.ACTION_PHONE_ACCOUNT_DISABLED);
+ }
+ intent.setPackage(phoneAccountHandle.getComponentName().getPackageName());
+ intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle);
+
+ if (isReceiverListening(intent)) {
+ Log.i(this, "notifyPhoneAccountEnabledState %s %s", phoneAccountHandle,
+ (isEnabled ? "enabled" : "disabled"));
+ mContext.sendBroadcast(intent);
+ }
+ }
+
+ /**
+ * Determines there is a {@link android.content.BroadcastReceiver} listening for an
+ * {@link Intent}.
+ *
+ * @param intent The {@link Intent}.
+ * @return {@code True} if there is a listener.
+ */
+ private boolean isReceiverListening(Intent intent) {
+ PackageManager pm = mContext.getPackageManager();
+ final List<ResolveInfo> activities = pm.queryBroadcastReceivers(intent, 0);
+ return !(activities.isEmpty());
+ }
+
+ /**
+ * The state of this {@code PhoneAccountRegistrar}.
+ */
+ @VisibleForTesting
+ public static class State {
+ /**
+ * The account selected by the user to be employed by default for making outgoing calls.
+ * If the user has not made such a selection, then this is null.
+ */
+ public PhoneAccountHandle defaultOutgoing = null;
+
+ /**
+ * A {@code PhoneAccount} having {@link PhoneAccount#CAPABILITY_CONNECTION_MANAGER} which
+ * manages and optimizes a user's PSTN SIM connections.
+ */
+ public PhoneAccountHandle simCallManager;
+
+ /**
+ * The complete list of {@code PhoneAccount}s known to the Telecom subsystem.
+ */
+ public final List<PhoneAccount> accounts = new ArrayList<>();
+
+ /**
+ * The version number of the State data.
+ */
+ public int versionNumber;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ //
+ // State management
+ //
+
+ private void write() {
+ final FileOutputStream os;
+ try {
+ os = mAtomicFile.startWrite();
+ boolean success = false;
+ try {
+ XmlSerializer serializer = new FastXmlSerializer();
+ serializer.setOutput(new BufferedOutputStream(os), "utf-8");
+ writeToXml(mState, serializer);
+ serializer.flush();
+ success = true;
+ } finally {
+ if (success) {
+ mAtomicFile.finishWrite(os);
+ } else {
+ mAtomicFile.failWrite(os);
+ }
+ }
+ } catch (IOException e) {
+ Log.e(this, e, "Writing state to XML file");
+ }
+ }
+
+ private void read() {
+ final InputStream is;
+ try {
+ is = mAtomicFile.openRead();
+ } catch (FileNotFoundException ex) {
+ return;
+ }
+
+ boolean versionChanged = false;
+
+ XmlPullParser parser;
+ try {
+ parser = Xml.newPullParser();
+ parser.setInput(new BufferedInputStream(is), null);
+ parser.nextTag();
+ mState = readFromXml(parser, mContext);
+ versionChanged = mState.versionNumber < EXPECTED_STATE_VERSION;
+
+ } catch (IOException | XmlPullParserException e) {
+ Log.e(this, e, "Reading state from XML file");
+ mState = new State();
+ } finally {
+ try {
+ is.close();
+ } catch (IOException e) {
+ Log.e(this, e, "Closing InputStream");
+ }
+ }
+
+ // If an upgrade occurred, write out the changed data.
+ if (versionChanged) {
+ write();
+ }
+ }
+
+ private static void writeToXml(State state, XmlSerializer serializer)
+ throws IOException {
+ sStateXml.writeToXml(state, serializer);
+ }
+
+ private static State readFromXml(XmlPullParser parser, Context context)
+ throws IOException, XmlPullParserException {
+ State s = sStateXml.readFromXml(parser, 0, context);
+ return s != null ? s : new State();
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ //
+ // XML serialization
+ //
+
+ @VisibleForTesting
+ public abstract static class XmlSerialization<T> {
+ private static final String LENGTH_ATTRIBUTE = "length";
+ private static final String VALUE_TAG = "value";
+
+ /**
+ * Write the supplied object to XML
+ */
+ public abstract void writeToXml(T o, XmlSerializer serializer)
+ throws IOException;
+
+ /**
+ * Read from the supplied XML into a new object, returning null in case of an
+ * unrecoverable schema mismatch or other data error. 'parser' must be already
+ * positioned at the first tag that is expected to have been emitted by this
+ * object's writeToXml(). This object tries to fail early without modifying
+ * 'parser' if it does not recognize the data it sees.
+ */
+ public abstract T readFromXml(XmlPullParser parser, int version, Context context)
+ throws IOException, XmlPullParserException;
+
+ protected void writeTextSafely(String tagName, Object value, XmlSerializer serializer)
+ throws IOException {
+ if (value != null) {
+ serializer.startTag(null, tagName);
+ serializer.text(Objects.toString(value));
+ serializer.endTag(null, tagName);
+ }
+ }
+
+ /**
+ * Serializes a string array.
+ *
+ * @param tagName The tag name for the string array.
+ * @param values The string values to serialize.
+ * @param serializer The serializer.
+ * @throws IOException
+ */
+ protected void writeStringList(String tagName, List<String> values,
+ XmlSerializer serializer)
+ throws IOException {
+
+ serializer.startTag(null, tagName);
+ if (values != null) {
+ serializer.attribute(null, LENGTH_ATTRIBUTE, Objects.toString(values.size()));
+ for (String toSerialize : values) {
+ serializer.startTag(null, VALUE_TAG);
+ if (toSerialize != null ){
+ serializer.text(toSerialize);
+ }
+ serializer.endTag(null, VALUE_TAG);
+ }
+ } else {
+ serializer.attribute(null, LENGTH_ATTRIBUTE, "0");
+ }
+ serializer.endTag(null, tagName);
+
+ }
+
+ /**
+ * Reads a string array from the XML parser.
+ *
+ * @param parser The XML parser.
+ * @return String array containing the parsed values.
+ * @throws IOException Exception related to IO.
+ * @throws XmlPullParserException Exception related to parsing.
+ */
+ protected List<String> readStringList(XmlPullParser parser)
+ throws IOException, XmlPullParserException {
+
+ int length = Integer.parseInt(parser.getAttributeValue(null, LENGTH_ATTRIBUTE));
+ List<String> arrayEntries = new ArrayList<String>(length);
+ String value = null;
+
+ if (length == 0) {
+ return arrayEntries;
+ }
+
+ int outerDepth = parser.getDepth();
+ while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+ if (parser.getName().equals(VALUE_TAG)) {
+ parser.next();
+ value = parser.getText();
+ arrayEntries.add(value);
+ }
+ }
+
+ return arrayEntries;
+ }
+ }
+
+ @VisibleForTesting
+ public static final XmlSerialization<State> sStateXml =
+ new XmlSerialization<State>() {
+ private static final String CLASS_STATE = "phone_account_registrar_state";
+ private static final String DEFAULT_OUTGOING = "default_outgoing";
+ private static final String SIM_CALL_MANAGER = "sim_call_manager";
+ private static final String ACCOUNTS = "accounts";
+ private static final String VERSION = "version";
+
+ @Override
+ public void writeToXml(State o, XmlSerializer serializer)
+ throws IOException {
+ if (o != null) {
+ serializer.startTag(null, CLASS_STATE);
+ serializer.attribute(null, VERSION, Objects.toString(EXPECTED_STATE_VERSION));
+
+ if (o.defaultOutgoing != null) {
+ serializer.startTag(null, DEFAULT_OUTGOING);
+ sPhoneAccountHandleXml.writeToXml(o.defaultOutgoing, serializer);
+ serializer.endTag(null, DEFAULT_OUTGOING);
+ }
+
+ if (o.simCallManager != null) {
+ serializer.startTag(null, SIM_CALL_MANAGER);
+ sPhoneAccountHandleXml.writeToXml(o.simCallManager, serializer);
+ serializer.endTag(null, SIM_CALL_MANAGER);
+ }
+
+ serializer.startTag(null, ACCOUNTS);
+ for (PhoneAccount m : o.accounts) {
+ sPhoneAccountXml.writeToXml(m, serializer);
+ }
+ serializer.endTag(null, ACCOUNTS);
+
+ serializer.endTag(null, CLASS_STATE);
+ }
+ }
+
+ @Override
+ public State readFromXml(XmlPullParser parser, int version, Context context)
+ throws IOException, XmlPullParserException {
+ if (parser.getName().equals(CLASS_STATE)) {
+ State s = new State();
+
+ String rawVersion = parser.getAttributeValue(null, VERSION);
+ s.versionNumber = TextUtils.isEmpty(rawVersion) ? 1 :
+ Integer.parseInt(rawVersion);
+
+ int outerDepth = parser.getDepth();
+ while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+ if (parser.getName().equals(DEFAULT_OUTGOING)) {
+ parser.nextTag();
+ s.defaultOutgoing = sPhoneAccountHandleXml.readFromXml(parser,
+ s.versionNumber, context);
+ } else if (parser.getName().equals(SIM_CALL_MANAGER)) {
+ parser.nextTag();
+ s.simCallManager = sPhoneAccountHandleXml.readFromXml(parser,
+ s.versionNumber, context);
+ } else if (parser.getName().equals(ACCOUNTS)) {
+ int accountsDepth = parser.getDepth();
+ while (XmlUtils.nextElementWithin(parser, accountsDepth)) {
+ PhoneAccount account = sPhoneAccountXml.readFromXml(parser,
+ s.versionNumber, context);
+
+ if (account != null && s.accounts != null) {
+ s.accounts.add(account);
+ }
+ }
+ }
+ }
+ return s;
+ }
+ return null;
+ }
+ };
+
+ @VisibleForTesting
+ public static final XmlSerialization<PhoneAccount> sPhoneAccountXml =
+ new XmlSerialization<PhoneAccount>() {
+ private static final String CLASS_PHONE_ACCOUNT = "phone_account";
+ private static final String ACCOUNT_HANDLE = "account_handle";
+ private static final String ADDRESS = "handle";
+ private static final String SUBSCRIPTION_ADDRESS = "subscription_number";
+ private static final String CAPABILITIES = "capabilities";
+ private static final String ICON_RES_ID = "icon_res_id";
+ private static final String LABEL = "label";
+ private static final String SHORT_DESCRIPTION = "short_description";
+ private static final String SUPPORTED_URI_SCHEMES = "supported_uri_schemes";
+ private static final String ENABLED = "enabled";
+ private static final String TRUE = "true";
+ private static final String FALSE = "false";
+
+ @Override
+ public void writeToXml(PhoneAccount o, XmlSerializer serializer)
+ throws IOException {
+ if (o != null) {
+ serializer.startTag(null, CLASS_PHONE_ACCOUNT);
+
+ if (o.getAccountHandle() != null) {
+ serializer.startTag(null, ACCOUNT_HANDLE);
+ sPhoneAccountHandleXml.writeToXml(o.getAccountHandle(), serializer);
+ serializer.endTag(null, ACCOUNT_HANDLE);
+ }
+
+ writeTextSafely(ADDRESS, o.getAddress(), serializer);
+ writeTextSafely(SUBSCRIPTION_ADDRESS, o.getSubscriptionAddress(), serializer);
+ writeTextSafely(CAPABILITIES, Integer.toString(o.getCapabilities()), serializer);
+ writeTextSafely(ICON_RES_ID, Integer.toString(o.getIconResId()), serializer);
+ writeTextSafely(LABEL, o.getLabel(), serializer);
+ writeTextSafely(SHORT_DESCRIPTION, o.getShortDescription(), serializer);
+ writeStringList(SUPPORTED_URI_SCHEMES, o.getSupportedUriSchemes(), serializer);
+ writeTextSafely(ENABLED, o.isEnabled() ? TRUE : FALSE, serializer);
+
+ serializer.endTag(null, CLASS_PHONE_ACCOUNT);
+ }
+ }
+
+ public PhoneAccount readFromXml(XmlPullParser parser, int version, Context context)
+ throws IOException, XmlPullParserException {
+ if (parser.getName().equals(CLASS_PHONE_ACCOUNT)) {
+ int outerDepth = parser.getDepth();
+ PhoneAccountHandle accountHandle = null;
+ Uri address = null;
+ Uri subscriptionAddress = null;
+ int capabilities = 0;
+ int iconResId = 0;
+ String label = null;
+ String shortDescription = null;
+ List<String> supportedUriSchemes = null;
+ boolean enabled = false;
+
+ while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+ if (parser.getName().equals(ACCOUNT_HANDLE)) {
+ parser.nextTag();
+ accountHandle = sPhoneAccountHandleXml.readFromXml(parser, version,
+ context);
+ } else if (parser.getName().equals(ADDRESS)) {
+ parser.next();
+ address = Uri.parse(parser.getText());
+ } else if (parser.getName().equals(SUBSCRIPTION_ADDRESS)) {
+ parser.next();
+ String nextText = parser.getText();
+ subscriptionAddress = nextText == null ? null : Uri.parse(nextText);
+ } else if (parser.getName().equals(CAPABILITIES)) {
+ parser.next();
+ capabilities = Integer.parseInt(parser.getText());
+ } else if (parser.getName().equals(ICON_RES_ID)) {
+ parser.next();
+ iconResId = Integer.parseInt(parser.getText());
+ } else if (parser.getName().equals(LABEL)) {
+ parser.next();
+ label = parser.getText();
+ } else if (parser.getName().equals(SHORT_DESCRIPTION)) {
+ parser.next();
+ shortDescription = parser.getText();
+ } else if (parser.getName().equals(SUPPORTED_URI_SCHEMES)) {
+ supportedUriSchemes = readStringList(parser);
+ } else if (parser.getName().equals(ENABLED)) {
+ parser.next();
+ enabled = parser.getText().equals(TRUE);
+ }
+ }
+
+ // Upgrade older phone accounts to specify the supported URI schemes.
+ if (version < 2) {
+ ComponentName sipComponentName = new ComponentName("com.android.phone",
+ "com.android.services.telephony.sip.SipConnectionService");
+
+ supportedUriSchemes = new ArrayList<>();
+
+ // Handle the SIP connection service.
+ // Check the system settings to see if it also should handle "tel" calls.
+ if (accountHandle.getComponentName().equals(sipComponentName)) {
+ boolean useSipForPstn = useSipForPstnCalls(context);
+ supportedUriSchemes.add(PhoneAccount.SCHEME_SIP);
+ if (useSipForPstn) {
+ supportedUriSchemes.add(PhoneAccount.SCHEME_TEL);
+ }
+ } else {
+ supportedUriSchemes.add(PhoneAccount.SCHEME_TEL);
+ supportedUriSchemes.add(PhoneAccount.SCHEME_VOICEMAIL);
+ }
+ }
+
+ // Prior to version 3, PhoneAccounts didn't include the enabled option. Enable
+ // all TelephonyConnectionService phone accounts by default.
+ if (version < 3) {
+ ComponentName telephonyComponentName = new ComponentName("com.android.phone",
+ "com.android.services.telephony.TelephonyConnectionService");
+
+ if (accountHandle.getComponentName().equals(telephonyComponentName)) {
+ enabled = true;
+ }
+ }
+
+ return PhoneAccount.builder(accountHandle, label)
+ .setAddress(address)
+ .setSubscriptionAddress(subscriptionAddress)
+ .setCapabilities(capabilities)
+ .setIconResId(iconResId)
+ .setShortDescription(shortDescription)
+ .setSupportedUriSchemes(supportedUriSchemes)
+ .setEnabled(enabled)
+ .build();
+ }
+ return null;
+ }
+
+ /**
+ * Determines if the SIP call settings specify to use SIP for all calls, including PSTN calls.
+ *
+ * @param context The context.
+ * @return {@code True} if SIP should be used for all calls.
+ */
+ private boolean useSipForPstnCalls(Context context) {
+ String option = Settings.System.getString(context.getContentResolver(),
+ Settings.System.SIP_CALL_OPTIONS);
+ option = (option != null) ? option : Settings.System.SIP_ADDRESS_ONLY;
+ return option.equals(Settings.System.SIP_ALWAYS);
+ }
+ };
+
+ @VisibleForTesting
+ public static final XmlSerialization<PhoneAccountHandle> sPhoneAccountHandleXml =
+ new XmlSerialization<PhoneAccountHandle>() {
+ private static final String CLASS_PHONE_ACCOUNT_HANDLE = "phone_account_handle";
+ private static final String COMPONENT_NAME = "component_name";
+ private static final String ID = "id";
+
+ @Override
+ public void writeToXml(PhoneAccountHandle o, XmlSerializer serializer)
+ throws IOException {
+ if (o != null) {
+ serializer.startTag(null, CLASS_PHONE_ACCOUNT_HANDLE);
+
+ if (o.getComponentName() != null) {
+ writeTextSafely(
+ COMPONENT_NAME, o.getComponentName().flattenToString(), serializer);
+ }
+
+ writeTextSafely(ID, o.getId(), serializer);
+
+ serializer.endTag(null, CLASS_PHONE_ACCOUNT_HANDLE);
+ }
+ }
+
+ @Override
+ public PhoneAccountHandle readFromXml(XmlPullParser parser, int version, Context context)
+ throws IOException, XmlPullParserException {
+ if (parser.getName().equals(CLASS_PHONE_ACCOUNT_HANDLE)) {
+ String componentNameString = null;
+ String idString = null;
+ int outerDepth = parser.getDepth();
+ while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+ if (parser.getName().equals(COMPONENT_NAME)) {
+ parser.next();
+ componentNameString = parser.getText();
+ } else if (parser.getName().equals(ID)) {
+ parser.next();
+ idString = parser.getText();
+ }
+ }
+ if (componentNameString != null) {
+ return new PhoneAccountHandle(
+ ComponentName.unflattenFromString(componentNameString),
+ idString);
+ }
+ }
+ return null;
+ }
+ };
+}
diff --git a/src/com/android/server/telecom/PhoneStateBroadcaster.java b/src/com/android/server/telecom/PhoneStateBroadcaster.java
new file mode 100644
index 0000000..d0d0b68
--- /dev/null
+++ b/src/com/android/server/telecom/PhoneStateBroadcaster.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.telecom.CallState;
+import android.telephony.TelephonyManager;
+
+import com.android.internal.telephony.ITelephonyRegistry;
+
+/**
+ * Send a {@link TelephonyManager#ACTION_PHONE_STATE_CHANGED} broadcast when the call state
+ * changes.
+ */
+final class PhoneStateBroadcaster extends CallsManagerListenerBase {
+
+ private final ITelephonyRegistry mRegistry;
+ private int mCurrentState = TelephonyManager.CALL_STATE_IDLE;
+
+ public PhoneStateBroadcaster() {
+ mRegistry = ITelephonyRegistry.Stub.asInterface(ServiceManager.getService(
+ "telephony.registry"));
+ if (mRegistry == null) {
+ Log.w(this, "TelephonyRegistry is null");
+ }
+ }
+
+ @Override
+ public void onCallStateChanged(Call call, int oldState, int newState) {
+ if ((newState == CallState.DIALING || newState == CallState.ACTIVE
+ || newState == CallState.ON_HOLD) && !CallsManager.getInstance().hasRingingCall()) {
+ /*
+ * EXTRA_STATE_RINGING takes precedence over EXTRA_STATE_OFFHOOK, so if there is
+ * already a ringing call, don't broadcast EXTRA_STATE_OFFHOOK.
+ */
+ sendPhoneStateChangedBroadcast(call, TelephonyManager.CALL_STATE_OFFHOOK);
+ }
+ }
+
+ @Override
+ public void onCallAdded(Call call) {
+ if (call.getState() == CallState.RINGING) {
+ sendPhoneStateChangedBroadcast(call, TelephonyManager.CALL_STATE_RINGING);
+ }
+ };
+
+ @Override
+ public void onCallRemoved(Call call) {
+ if (!CallsManager.getInstance().hasAnyCalls()) {
+ sendPhoneStateChangedBroadcast(call, TelephonyManager.CALL_STATE_IDLE);
+ }
+ }
+
+ private void sendPhoneStateChangedBroadcast(Call call, int phoneState) {
+ if (phoneState == mCurrentState) {
+ return;
+ }
+
+ mCurrentState = phoneState;
+
+ String callHandle = null;
+ if (call.getHandle() != null) {
+ callHandle = call.getHandle().getSchemeSpecificPart();
+ }
+
+ try {
+ if (mRegistry != null) {
+ mRegistry.notifyCallState(phoneState, callHandle);
+ Log.i(this, "Broadcasted state change: %s", mCurrentState);
+ }
+ } catch (RemoteException e) {
+ Log.w(this, "RemoteException when notifying TelephonyRegistry of call state change.");
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/ProximitySensorManager.java b/src/com/android/server/telecom/ProximitySensorManager.java
new file mode 100644
index 0000000..289366f
--- /dev/null
+++ b/src/com/android/server/telecom/ProximitySensorManager.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.telecom;
+
+import android.content.Context;
+import android.os.PowerManager;
+
+/**
+ * This class manages the proximity sensor and allows callers to turn it on and off.
+ */
+public class ProximitySensorManager extends CallsManagerListenerBase {
+ private static final String TAG = ProximitySensorManager.class.getSimpleName();
+
+ private final PowerManager.WakeLock mProximityWakeLock;
+
+ public ProximitySensorManager(Context context) {
+ PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+
+ if (pm.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
+ mProximityWakeLock = pm.newWakeLock(
+ PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG);
+ } else {
+ mProximityWakeLock = null;
+ }
+ Log.d(this, "onCreate: mProximityWakeLock: ", mProximityWakeLock);
+ }
+
+ @Override
+ public void onCallRemoved(Call call) {
+ if (CallsManager.getInstance().getCalls().isEmpty()) {
+ Log.v(this, "all calls removed, resetting proximity sensor to default state");
+ turnOff(true);
+ }
+ super.onCallRemoved(call);
+ }
+
+ /**
+ * Turn the proximity sensor on.
+ */
+ void turnOn() {
+ if (CallsManager.getInstance().getCalls().isEmpty()) {
+ Log.w(this, "Asking to turn on prox sensor without a call? I don't think so.");
+ return;
+ }
+
+ if (mProximityWakeLock == null) {
+ return;
+ }
+ if (!mProximityWakeLock.isHeld()) {
+ Log.i(this, "Acquiring proximity wake lock");
+ mProximityWakeLock.acquire();
+ } else {
+ Log.i(this, "Proximity wake lock already acquired");
+ }
+ }
+
+ /**
+ * Turn the proximity sensor off.
+ * @param screenOnImmediately
+ */
+ void turnOff(boolean screenOnImmediately) {
+ if (mProximityWakeLock == null) {
+ return;
+ }
+ if (mProximityWakeLock.isHeld()) {
+ Log.i(this, "Releasing proximity wake lock");
+ int flags =
+ (screenOnImmediately ? 0 : PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY);
+ mProximityWakeLock.release(flags);
+ } else {
+ Log.i(this, "Proximity wake lock already released");
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/QuickResponseUtils.java b/src/com/android/server/telecom/QuickResponseUtils.java
new file mode 100644
index 0000000..dad2907
--- /dev/null
+++ b/src/com/android/server/telecom/QuickResponseUtils.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+
+
+/**
+ * Utils class that exposes some helper routines to used to manage the QuickResponses
+ */
+public class QuickResponseUtils {
+ public static final String LOG_TAG = "QuickResponseUtils";
+
+ // SharedPreferences file name for our persistent settings.
+ public static final String SHARED_PREFERENCES_NAME = "respond_via_sms_prefs";
+ private static final String PACKAGE_NAME_TELEPHONY = "com.android.phone";
+
+ // Preference keys for the 4 "canned responses"; see RespondViaSmsManager$Settings.
+ // Since (for now at least) the number of messages is fixed at 4, and since
+ // SharedPreferences can't deal with arrays anyway, just store the messages
+ // as 4 separate strings.
+ public static final int NUM_CANNED_RESPONSES = 4;
+ public static final String KEY_CANNED_RESPONSE_PREF_1 = "canned_response_pref_1";
+ public static final String KEY_CANNED_RESPONSE_PREF_2 = "canned_response_pref_2";
+ public static final String KEY_CANNED_RESPONSE_PREF_3 = "canned_response_pref_3";
+ public static final String KEY_CANNED_RESPONSE_PREF_4 = "canned_response_pref_4";
+
+ /**
+ * As of L, QuickResponses were moved from Telephony to Telecom. Because of
+ * this, we need to make sure that we migrate any old QuickResponses to our
+ * current SharedPreferences. This is a lazy migration as it happens only when
+ * the QuickResponse settings are viewed or if they are queried via RespondViaSmsManager.
+ */
+ public static void maybeMigrateLegacyQuickResponses() {
+ // The algorithm will go as such:
+ // If Telecom QuickResponses exist, we will skip migration because this implies
+ // that a user has already specified their desired QuickResponses and have abandoned any
+ // older QuickResponses.
+ // Then, if Telephony QuickResponses exist, we will move those to Telecom.
+ // If neither exist, we'll populate Telecom with the default QuickResponses.
+ // This guarantees the caller that QuickResponses exist in SharedPreferences after this
+ // function is called.
+
+ Log.d(LOG_TAG, "maybeMigrateLegacyQuickResponses() - Starting");
+
+ final Context telecomContext = TelecomApp.getInstance();
+ final SharedPreferences prefs = telecomContext.getSharedPreferences(
+ SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
+ final Resources res = telecomContext.getResources();
+
+ final boolean responsesExist = prefs.contains(KEY_CANNED_RESPONSE_PREF_1);
+ if (responsesExist) {
+ // If one QuickResponse exists, they all exist.
+ Log.d(LOG_TAG, "maybeMigrateLegacyQuickResponses() - Telecom QuickResponses exist");
+ return;
+ }
+
+ // Grab the all the default QuickResponses from our resources.
+ String cannedResponse1 = res.getString(R.string.respond_via_sms_canned_response_1);
+ String cannedResponse2 = res.getString(R.string.respond_via_sms_canned_response_2);
+ String cannedResponse3 = res.getString(R.string.respond_via_sms_canned_response_3);
+ String cannedResponse4 = res.getString(R.string.respond_via_sms_canned_response_4);
+
+ Log.d(LOG_TAG, "maybeMigrateLegacyQuickResponses() - No local QuickResponses");
+
+ // We don't have local QuickResponses, let's see if they live in
+ // the Telephony package and we'll fall back on using our default values.
+ Context telephonyContext = null;
+ try {
+ telephonyContext = telecomContext.createPackageContext(PACKAGE_NAME_TELEPHONY, 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(LOG_TAG, e, "maybeMigrateLegacyQuickResponses() - Can't find Telephony package.");
+ }
+
+ // Read the old canned responses from the Telephony SharedPreference if possible.
+ if (telephonyContext != null) {
+ // Note that if any one QuickResponse does not exist, we'll use the default
+ // value to populate it.
+ Log.d(LOG_TAG, "maybeMigrateLegacyQuickResponses() - Using Telephony QuickResponses.");
+ final SharedPreferences oldPrefs = telephonyContext.getSharedPreferences(
+ SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
+ cannedResponse1 = oldPrefs.getString(KEY_CANNED_RESPONSE_PREF_1, cannedResponse1);
+ cannedResponse2 = oldPrefs.getString(KEY_CANNED_RESPONSE_PREF_2, cannedResponse2);
+ cannedResponse3 = oldPrefs.getString(KEY_CANNED_RESPONSE_PREF_3, cannedResponse3);
+ cannedResponse4 = oldPrefs.getString(KEY_CANNED_RESPONSE_PREF_4, cannedResponse4);
+ }
+
+ // Either way, write them back into Telecom SharedPreferences.
+ final SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(KEY_CANNED_RESPONSE_PREF_1, cannedResponse1);
+ editor.putString(KEY_CANNED_RESPONSE_PREF_2, cannedResponse2);
+ editor.putString(KEY_CANNED_RESPONSE_PREF_3, cannedResponse3);
+ editor.putString(KEY_CANNED_RESPONSE_PREF_4, cannedResponse4);
+ editor.commit();
+
+ Log.d(LOG_TAG, "maybeMigrateLegacyQuickResponses() - Done.");
+ return;
+ }
+}
diff --git a/src/com/android/server/telecom/README b/src/com/android/server/telecom/README
new file mode 100644
index 0000000..be202fe
--- /dev/null
+++ b/src/com/android/server/telecom/README
@@ -0,0 +1,2 @@
+Code to manage and handle phone calls etc, approximately migrated from
+/telephony etc.
\ No newline at end of file
diff --git a/src/com/android/server/telecom/RespondViaSmsManager.java b/src/com/android/server/telecom/RespondViaSmsManager.java
new file mode 100644
index 0000000..6c054e6
--- /dev/null
+++ b/src/com/android/server/telecom/RespondViaSmsManager.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2011 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 com.android.internal.os.SomeArgs;
+import com.android.internal.telephony.SmsApplication;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Message;
+import android.telecom.Response;
+import android.telephony.TelephonyManager;
+import android.widget.Toast;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Helper class to manage the "Respond via Message" feature for incoming calls.
+ */
+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 static final RespondViaSmsManager sInstance = new RespondViaSmsManager();
+
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_CANNED_TEXT_MESSAGES_READY:
+ SomeArgs args = (SomeArgs) msg.obj;
+ try {
+ Response<Void, List<String>> response =
+ (Response<Void, List<String>>) args.arg1;
+ List<String> textMessages =
+ (List<String>) args.arg2;
+ if (textMessages != null) {
+ response.onResult(null, textMessages);
+ } else {
+ response.onError(null, 0, null);
+ }
+ } finally {
+ args.recycle();
+ }
+ break;
+ case MSG_SHOW_SENT_TOAST:
+ showMessageSentToast((String) msg.obj);
+ break;
+ }
+ }
+ };
+
+ public static RespondViaSmsManager getInstance() { return sInstance; }
+
+ private RespondViaSmsManager() {}
+
+ /**
+ * Read the (customizable) canned responses from SharedPreferences,
+ * or from defaults if the user has never actually brought up
+ * the Settings UI.
+ *
+ * The interface of this method is asynchronous since it does disk I/O.
+ *
+ * @param response An object to receive an async reply, which will be called from
+ * the main thread.
+ */
+ public void loadCannedTextMessages(final Response<Void, List<String>> response) {
+ new Thread() {
+ @Override
+ public void run() {
+ Log.d(RespondViaSmsManager.this, "loadCannedResponses() starting");
+
+ // This function guarantees that QuickResponses will be in our
+ // SharedPreferences with the proper values considering there may be
+ // old QuickResponses in Telephony pre L.
+ QuickResponseUtils.maybeMigrateLegacyQuickResponses();
+
+ final SharedPreferences prefs = TelecomApp.getInstance().getSharedPreferences(
+ QuickResponseUtils.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
+ final Resources res = TelecomApp.getInstance().getInstance().getResources();
+
+ final ArrayList<String> textMessages = new ArrayList<>(
+ QuickResponseUtils.NUM_CANNED_RESPONSES);
+
+ // Note the default values here must agree with the corresponding
+ // android:defaultValue attributes in respond_via_sms_settings.xml.
+ textMessages.add(0, prefs.getString(QuickResponseUtils.KEY_CANNED_RESPONSE_PREF_1,
+ res.getString(R.string.respond_via_sms_canned_response_1)));
+ textMessages.add(1, prefs.getString(QuickResponseUtils.KEY_CANNED_RESPONSE_PREF_2,
+ res.getString(R.string.respond_via_sms_canned_response_2)));
+ textMessages.add(2, prefs.getString(QuickResponseUtils.KEY_CANNED_RESPONSE_PREF_3,
+ res.getString(R.string.respond_via_sms_canned_response_3)));
+ textMessages.add(3, prefs.getString(QuickResponseUtils.KEY_CANNED_RESPONSE_PREF_4,
+ res.getString(R.string.respond_via_sms_canned_response_4)));
+
+ Log.d(RespondViaSmsManager.this,
+ "loadCannedResponses() completed, found responses: %s",
+ textMessages.toString());
+
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = response;
+ args.arg2 = textMessages;
+ mHandler.obtainMessage(MSG_CANNED_TEXT_MESSAGES_READY, args).sendToTarget();
+ }
+ }.start();
+ }
+
+ @Override
+ public void onIncomingCallRejected(Call call, boolean rejectWithMessage, String textMessage) {
+ if (rejectWithMessage) {
+ rejectCallWithMessage(call.getHandle().getSchemeSpecificPart(), textMessage);
+ }
+ }
+
+ private void showMessageSentToast(final String phoneNumber) {
+ // ...and show a brief confirmation to the user (since
+ // otherwise it's hard to be sure that anything actually
+ // happened.)
+ final Resources res = TelecomApp.getInstance().getResources();
+ final String formatString = res.getString(
+ R.string.respond_via_sms_confirmation_format);
+ final String confirmationMsg = String.format(formatString, phoneNumber);
+ Toast.makeText(TelecomApp.getInstance(), confirmationMsg,
+ Toast.LENGTH_LONG).show();
+
+ // TODO: If the device is locked, this toast won't actually ever
+ // be visible! (That's because we're about to dismiss the call
+ // screen, which means that the device will return to the
+ // keyguard. But toasts aren't visible on top of the keyguard.)
+ // Possible fixes:
+ // (1) Is it possible to allow a specific Toast to be visible
+ // on top of the keyguard?
+ // (2) Artificially delay the dismissCallScreen() call by 3
+ // seconds to allow the toast to be seen?
+ // (3) Don't use a toast at all; instead use a transient state
+ // of the InCallScreen (perhaps via the InCallUiState
+ // progressIndication feature), and have that state be
+ // visible for 3 seconds before calling dismissCallScreen().
+ }
+
+ /**
+ * Reject the call with the specified message. If message is null this call is ignored.
+ */
+ private void rejectCallWithMessage(String phoneNumber, String textMessage) {
+ if (textMessage != null) {
+ final ComponentName component =
+ SmsApplication.getDefaultRespondViaMessageApplication(
+ TelecomApp.getInstance(), true /*updateIfNeeded*/);
+ if (component != null) {
+ // Build and send the intent
+ final Uri uri = Uri.fromParts(Constants.SCHEME_SMSTO, phoneNumber, null);
+ final Intent intent = new Intent(TelephonyManager.ACTION_RESPOND_VIA_MESSAGE, uri);
+ intent.putExtra(Intent.EXTRA_TEXT, textMessage);
+ mHandler.obtainMessage(MSG_SHOW_SENT_TOAST, phoneNumber).sendToTarget();
+ intent.setComponent(component);
+ TelecomApp.getInstance().startService(intent);
+ }
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/RespondViaSmsSettings.java b/src/com/android/server/telecom/RespondViaSmsSettings.java
new file mode 100644
index 0000000..23bf7b2
--- /dev/null
+++ b/src/com/android/server/telecom/RespondViaSmsSettings.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2011 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.app.ActionBar;
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.EditTextPreference;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.view.Menu;
+import android.view.MenuItem;
+
+/**
+ * Helper class to manage the "Respond via SMS Message" feature for incoming calls.
+ */
+public class RespondViaSmsSettings {
+ private static final String KEY_PREFERRED_PACKAGE = "preferred_package_pref";
+ private static final String KEY_INSTANT_TEXT_DEFAULT_COMPONENT = "instant_text_def_component";
+
+ // TODO: This class is newly copied into Telecom (com.android.server.telecom) from it previous
+ // location in Telephony (com.android.phone). User's preferences stored in the old location
+ // will be lost. We need code here to migrate KLP -> LMP settings values.
+
+ /**
+ * Settings activity under "Call settings" to let you manage the
+ * canned responses; see respond_via_sms_settings.xml
+ */
+ public static class Settings extends PreferenceActivity
+ implements Preference.OnPreferenceChangeListener {
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ Log.d(this, "Settings: onCreate()...");
+
+ // This function guarantees that QuickResponses will be in our
+ // SharedPreferences with the proper values considering there may be
+ // old QuickResponses in Telephony pre L.
+ QuickResponseUtils.maybeMigrateLegacyQuickResponses();
+
+ getPreferenceManager().setSharedPreferencesName(
+ QuickResponseUtils.SHARED_PREFERENCES_NAME);
+
+ // This preference screen is ultra-simple; it's just 4 plain
+ // <EditTextPreference>s, one for each of the 4 "canned responses".
+ //
+ // The only nontrivial thing we do here is copy the text value of
+ // each of those EditTextPreferences and use it as the preference's
+ // "title" as well, so that the user will immediately see all 4
+ // strings when they arrive here.
+ //
+ // Also, listen for change events (since we'll need to update the
+ // title any time the user edits one of the strings.)
+
+ addPreferencesFromResource(R.xml.respond_via_sms_settings);
+
+ EditTextPreference pref;
+ pref = (EditTextPreference) findPreference(
+ QuickResponseUtils.KEY_CANNED_RESPONSE_PREF_1);
+ pref.setTitle(pref.getText());
+ pref.setOnPreferenceChangeListener(this);
+
+ pref = (EditTextPreference) findPreference(
+ QuickResponseUtils.KEY_CANNED_RESPONSE_PREF_2);
+ pref.setTitle(pref.getText());
+ pref.setOnPreferenceChangeListener(this);
+
+ pref = (EditTextPreference) findPreference(
+ QuickResponseUtils.KEY_CANNED_RESPONSE_PREF_3);
+ pref.setTitle(pref.getText());
+ pref.setOnPreferenceChangeListener(this);
+
+ pref = (EditTextPreference) findPreference(
+ QuickResponseUtils.KEY_CANNED_RESPONSE_PREF_4);
+ pref.setTitle(pref.getText());
+ pref.setOnPreferenceChangeListener(this);
+
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ // android.R.id.home will be triggered in onOptionsItemSelected()
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+ }
+
+ // Preference.OnPreferenceChangeListener implementation
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ Log.d(this, "onPreferenceChange: key = %s", preference.getKey());
+ Log.d(this, " preference = '%s'", preference);
+ Log.d(this, " newValue = '%s'", newValue);
+
+ EditTextPreference pref = (EditTextPreference) preference;
+
+ // Copy the new text over to the title, just like in onCreate().
+ // (Watch out: onPreferenceChange() is called *before* the
+ // Preference itself gets updated, so we need to use newValue here
+ // rather than pref.getText().)
+ pref.setTitle((String) newValue);
+
+ return true; // means it's OK to update the state of the Preference with the new value
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ final int itemId = item.getItemId();
+ switch (itemId) {
+ case android.R.id.home:
+ goUpToTopLevelSetting(this);
+ return true;
+ case R.id.respond_via_message_reset:
+ // Reset the preferences settings
+ SharedPreferences prefs = getSharedPreferences(
+ QuickResponseUtils.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.remove(KEY_INSTANT_TEXT_DEFAULT_COMPONENT);
+ editor.apply();
+
+ return true;
+ default:
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.respond_via_message_settings_menu, menu);
+ return super.onCreateOptionsMenu(menu);
+ }
+ }
+
+ /**
+ * Finish current Activity and go up to the top level Settings.
+ */
+ public static void goUpToTopLevelSetting(Activity activity) {
+ activity.finish();
+ }
+}
diff --git a/src/com/android/server/telecom/RingbackPlayer.java b/src/com/android/server/telecom/RingbackPlayer.java
new file mode 100644
index 0000000..dc06c95
--- /dev/null
+++ b/src/com/android/server/telecom/RingbackPlayer.java
@@ -0,0 +1,142 @@
+/*
+ * 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;
+
+import android.telecom.CallState;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * Plays ringback tones. Ringback is different from other tones because it operates as the current
+ * audio for a call, whereas most tones play as simple timed events. This means ringback must be
+ * able to turn off and on as the user switches between calls. This is why it is implemented as its
+ * own class.
+ */
+class RingbackPlayer extends CallsManagerListenerBase {
+
+ private final CallsManager mCallsManager;
+
+ private final InCallTonePlayer.Factory mPlayerFactory;
+
+ /**
+ * The current call for which the ringback tone is being played.
+ */
+ private Call mCall;
+
+ /**
+ * The currently active player.
+ */
+ private InCallTonePlayer mTonePlayer;
+
+ RingbackPlayer(CallsManager callsManager, InCallTonePlayer.Factory playerFactory) {
+ mCallsManager = callsManager;
+ mPlayerFactory = playerFactory;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall) {
+ if (oldForegroundCall != null) {
+ stopRingbackForCall(oldForegroundCall);
+ }
+
+ if (shouldStartRinging(newForegroundCall)) {
+ startRingbackForCall(newForegroundCall);
+ }
+ }
+
+ @Override
+ public void onConnectionServiceChanged(
+ Call call,
+ ConnectionServiceWrapper oldService,
+ ConnectionServiceWrapper newService) {
+
+ // Treat as ending or begining dialing based on the state transition.
+ if (shouldStartRinging(call)) {
+ startRingbackForCall(call);
+ } else if (newService == null) {
+ stopRingbackForCall(call);
+ }
+ }
+
+ @Override
+ public void onRingbackRequested(Call call, boolean ignored) {
+ if (shouldStartRinging(call)) {
+ startRingbackForCall(call);
+ } else {
+ stopRingbackForCall(call);
+ }
+ }
+
+ /**
+ * Starts ringback for the specified dialing call as needed.
+ *
+ * @param call The call for which to ringback.
+ */
+ private void startRingbackForCall(Call call) {
+ Preconditions.checkState(call.getState() == CallState.DIALING);
+ ThreadUtil.checkOnMainThread();
+
+ if (mCall == call) {
+ Log.w(this, "Ignoring duplicate requests to ring for %s.", call);
+ return;
+ }
+
+ if (mCall != null) {
+ // We only get here for the foreground call so, there's no reason why there should
+ // exist a current dialing call.
+ Log.wtf(this, "Ringback player thinks there are two foreground-dialing calls.");
+ }
+
+ mCall = call;
+ if (mTonePlayer == null) {
+ Log.d(this, "Playing the ringback tone for %s.", call);
+ mTonePlayer = mPlayerFactory.createPlayer(InCallTonePlayer.TONE_RING_BACK);
+ mTonePlayer.startTone();
+ }
+ }
+
+ /**
+ * Stops the ringback for the specified dialing call as needed.
+ *
+ * @param call The call for which to stop ringback.
+ */
+ private void stopRingbackForCall(Call call) {
+ ThreadUtil.checkOnMainThread();
+
+ if (mCall == call) {
+ // The foreground call is no longer dialing or is no longer the foreground call. In
+ // either case, stop the ringback tone.
+ mCall = null;
+
+ if (mTonePlayer == null) {
+ Log.w(this, "No player found to stop.");
+ } else {
+ Log.i(this, "Stopping the ringback tone for %s.", call);
+ mTonePlayer.stopTone();
+ mTonePlayer = null;
+ }
+ }
+ }
+
+ private boolean shouldStartRinging(Call call) {
+ return call != null
+ && mCallsManager.getForegroundCall() == call
+ && call.getState() == CallState.DIALING
+ && call.isRingbackRequested();
+ }
+}
diff --git a/src/com/android/server/telecom/Ringer.java b/src/com/android/server/telecom/Ringer.java
new file mode 100644
index 0000000..34f6829
--- /dev/null
+++ b/src/com/android/server/telecom/Ringer.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.SystemVibrator;
+import android.os.Vibrator;
+import android.provider.Settings;
+import android.telecom.CallState;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Controls the ringtone player.
+ */
+final class Ringer extends CallsManagerListenerBase {
+ private static final long[] VIBRATION_PATTERN = new long[] {
+ 0, // No delay before starting
+ 1000, // How long to vibrate
+ 1000, // How long to wait before vibrating again
+ };
+
+ private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+ .build();
+
+ /** Indicate that we want the pattern to repeat at the step which turns on vibration. */
+ private static final int VIBRATION_PATTERN_REPEAT = 1;
+
+ private final AsyncRingtonePlayer mRingtonePlayer = new AsyncRingtonePlayer();
+
+ /**
+ * Used to keep ordering of unanswered incoming calls. There can easily exist multiple incoming
+ * calls and explicit ordering is useful for maintaining the proper state of the ringer.
+ */
+ private final List<Call> mRingingCalls = new LinkedList<>();
+
+ private final CallAudioManager mCallAudioManager;
+ private final CallsManager mCallsManager;
+ private final InCallTonePlayer.Factory mPlayerFactory;
+ private final Context mContext;
+ private final Vibrator mVibrator;
+
+ private InCallTonePlayer mCallWaitingPlayer;
+
+ /**
+ * Used to track the status of {@link #mVibrator} in the case of simultaneous incoming calls.
+ */
+ private boolean mIsVibrating = false;
+
+ /** Initializes the Ringer. */
+ Ringer(
+ CallAudioManager callAudioManager,
+ CallsManager callsManager,
+ InCallTonePlayer.Factory playerFactory,
+ Context context) {
+
+ mCallAudioManager = callAudioManager;
+ mCallsManager = callsManager;
+ mPlayerFactory = playerFactory;
+ mContext = context;
+ // We don't rely on getSystemService(Context.VIBRATOR_SERVICE) to make sure this
+ // vibrator object will be isolated from others.
+ mVibrator = new SystemVibrator(TelecomApp.getInstance());
+ }
+
+ @Override
+ public void onCallAdded(final Call call) {
+ if (call.isIncoming() && call.getState() == CallState.RINGING) {
+ if (mRingingCalls.contains(call)) {
+ Log.wtf(this, "New ringing call is already in list of unanswered calls");
+ }
+ mRingingCalls.add(call);
+ updateRinging();
+ }
+ }
+
+ @Override
+ public void onCallRemoved(Call call) {
+ removeFromUnansweredCall(call);
+ }
+
+ @Override
+ public void onCallStateChanged(Call call, int oldState, int newState) {
+ if (newState != CallState.RINGING) {
+ removeFromUnansweredCall(call);
+ }
+ }
+
+ @Override
+ public void onIncomingCallAnswered(Call call) {
+ onRespondedToIncomingCall(call);
+ }
+
+ @Override
+ public void onIncomingCallRejected(Call call, boolean rejectWithMessage, String textMessage) {
+ onRespondedToIncomingCall(call);
+ }
+
+ @Override
+ public void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall) {
+ if (mRingingCalls.contains(oldForegroundCall) ||
+ mRingingCalls.contains(newForegroundCall)) {
+ updateRinging();
+ }
+ }
+
+ /**
+ * Silences the ringer for any actively ringing calls.
+ */
+ void silence() {
+ // Remove all calls from the "ringing" set and then update the ringer.
+ mRingingCalls.clear();
+ updateRinging();
+ }
+
+ private void onRespondedToIncomingCall(Call call) {
+ // Only stop the ringer if this call is the top-most incoming call.
+ if (getTopMostUnansweredCall() == call) {
+ stopRinging();
+ stopCallWaiting();
+ }
+
+ // We do not remove the call from mRingingCalls until the call state changes from
+ // STATE_RINGING or the call is removed. see onCallStateChanged or onCallRemoved.
+ }
+
+ private Call getTopMostUnansweredCall() {
+ return mRingingCalls.isEmpty() ? null : mRingingCalls.get(0);
+ }
+
+ /**
+ * Removes the specified call from the list of unanswered incoming calls and updates the ringer
+ * based on the new state of {@link #mRingingCalls}. Safe to call with a call that is not
+ * present in the list of incoming calls.
+ */
+ private void removeFromUnansweredCall(Call call) {
+ mRingingCalls.remove(call);
+ updateRinging();
+ }
+
+ private void updateRinging() {
+ if (mRingingCalls.isEmpty()) {
+ stopRinging();
+ stopCallWaiting();
+ } else {
+ startRingingOrCallWaiting();
+ }
+ }
+
+ private void startRingingOrCallWaiting() {
+ Call foregroundCall = mCallsManager.getForegroundCall();
+ Log.v(this, "startRingingOrCallWaiting, foregroundCall: %s.", foregroundCall);
+
+ if (mRingingCalls.contains(foregroundCall)) {
+ // The foreground call is one of incoming calls so play the ringer out loud.
+ stopCallWaiting();
+
+ if (!shouldRingForContact(foregroundCall.getContactUri())) {
+ return;
+ }
+
+ AudioManager audioManager =
+ (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+ if (audioManager.getStreamVolume(AudioManager.STREAM_RING) > 0) {
+ Log.v(this, "startRingingOrCallWaiting");
+ mCallAudioManager.setIsRinging(true);
+
+ // Because we wait until a contact info query to complete before processing a
+ // call (for the purposes of direct-to-voicemail), the information about custom
+ // ringtones should be available by the time this code executes. We can safely
+ // request the custom ringtone from the call and expect it to be current.
+ mRingtonePlayer.play(foregroundCall.getRingtone());
+ } else {
+ Log.v(this, "startRingingOrCallWaiting, skipping because volume is 0");
+ }
+
+ if (shouldVibrate(TelecomApp.getInstance()) && !mIsVibrating) {
+ mVibrator.vibrate(VIBRATION_PATTERN, VIBRATION_PATTERN_REPEAT,
+ VIBRATION_ATTRIBUTES);
+ mIsVibrating = true;
+ }
+ } else {
+ Log.v(this, "Playing call-waiting tone.");
+
+ // All incoming calls are in background so play call waiting.
+ stopRinging();
+
+ if (mCallWaitingPlayer == null) {
+ mCallWaitingPlayer =
+ mPlayerFactory.createPlayer(InCallTonePlayer.TONE_CALL_WAITING);
+ mCallWaitingPlayer.startTone();
+ }
+ }
+ }
+
+ private boolean shouldRingForContact(Uri contactUri) {
+ final NotificationManager manager =
+ (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ final Bundle extras = new Bundle();
+ if (contactUri != null) {
+ extras.putStringArray(Notification.EXTRA_PEOPLE, new String[] {contactUri.toString()});
+ }
+ return manager.matchesCallFilter(extras);
+ }
+
+ private void stopRinging() {
+ Log.v(this, "stopRinging");
+
+ mRingtonePlayer.stop();
+
+ if (mIsVibrating) {
+ mVibrator.cancel();
+ mIsVibrating = false;
+ }
+
+ // Even though stop is asynchronous it's ok to update the audio manager. Things like audio
+ // focus are voluntary so releasing focus too early is not detrimental.
+ mCallAudioManager.setIsRinging(false);
+ }
+
+ private void stopCallWaiting() {
+ Log.v(this, "stop call waiting.");
+ if (mCallWaitingPlayer != null) {
+ mCallWaitingPlayer.stopTone();
+ mCallWaitingPlayer = null;
+ }
+ }
+
+ private boolean shouldVibrate(Context context) {
+ AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ int ringerMode = audioManager.getRingerMode();
+ if (getVibrateWhenRinging(context)) {
+ return ringerMode != AudioManager.RINGER_MODE_SILENT;
+ } else {
+ return ringerMode == AudioManager.RINGER_MODE_VIBRATE;
+ }
+ }
+
+ private boolean getVibrateWhenRinging(Context context) {
+ if (!mVibrator.hasVibrator()) {
+ return false;
+ }
+ return Settings.System.getInt(context.getContentResolver(),
+ Settings.System.VIBRATE_WHEN_RINGING, 0) != 0;
+ }
+}
diff --git a/src/com/android/server/telecom/ServiceBinder.java b/src/com/android/server/telecom/ServiceBinder.java
new file mode 100644
index 0000000..98e7bd0
--- /dev/null
+++ b/src/com/android/server/telecom/ServiceBinder.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.os.IInterface;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+
+import com.google.common.collect.Sets;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Abstract class to perform the work of binding and unbinding to the specified service interface.
+ * Subclasses supply the service intent and component name and this class will invoke protected
+ * methods when the class is bound, unbound, or upon failure.
+ */
+abstract class ServiceBinder<ServiceInterface extends IInterface> {
+
+ /**
+ * Callback to notify after a binding succeeds or fails.
+ */
+ interface BindCallback {
+ void onSuccess();
+ void onFailure();
+ }
+
+ /**
+ * Listener for bind events on ServiceBinder.
+ */
+ interface Listener<ServiceBinderClass extends ServiceBinder<?>> {
+ void onUnbind(ServiceBinderClass serviceBinder);
+ }
+
+ /**
+ * Helper class to perform on-demand binding.
+ */
+ final class Binder {
+ /**
+ * Performs an asynchronous bind to the service (only if not already bound) and executes the
+ * specified callback.
+ *
+ * @param callback The callback to notify of the binding's success or failure.
+ */
+ void bind(BindCallback callback) {
+ ThreadUtil.checkOnMainThread();
+ Log.d(ServiceBinder.this, "bind()");
+
+ // Reset any abort request if we're asked to bind again.
+ clearAbort();
+
+ if (!mCallbacks.isEmpty()) {
+ // Binding already in progress, append to the list of callbacks and bail out.
+ mCallbacks.add(callback);
+ return;
+ }
+
+ mCallbacks.add(callback);
+ if (mServiceConnection == null) {
+ Intent serviceIntent = new Intent(mServiceAction).setComponent(mComponentName);
+ ServiceConnection connection = new ServiceBinderConnection();
+
+ Log.d(ServiceBinder.this, "Binding to service with intent: %s", serviceIntent);
+ if (!mContext.bindService(serviceIntent, connection, Context.BIND_AUTO_CREATE)) {
+ handleFailedConnection();
+ return;
+ }
+ } else {
+ Log.d(ServiceBinder.this, "Service is already bound.");
+ Preconditions.checkNotNull(mBinder);
+ handleSuccessfulConnection();
+ }
+ }
+ }
+
+ private final class ServiceBinderConnection implements ServiceConnection {
+ @Override
+ public void onServiceConnected(ComponentName componentName, IBinder binder) {
+ ThreadUtil.checkOnMainThread();
+ Log.i(this, "Service bound %s", componentName);
+
+ // Unbind request was queued so unbind immediately.
+ if (mIsBindingAborted) {
+ clearAbort();
+ logServiceDisconnected("onServiceConnected");
+ mContext.unbindService(this);
+ handleFailedConnection();
+ return;
+ }
+
+ mServiceConnection = this;
+ setBinder(binder);
+ handleSuccessfulConnection();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName componentName) {
+ logServiceDisconnected("onServiceDisconnected");
+
+ mServiceConnection = null;
+ clearAbort();
+
+ handleServiceDisconnected();
+ }
+ }
+
+ /** The application context. */
+ private final Context mContext;
+
+ /** The intent action to use when binding through {@link Context#bindService}. */
+ private final String mServiceAction;
+
+ /** The component name of the service to bind to. */
+ private final ComponentName mComponentName;
+
+ /** The set of callbacks waiting for notification of the binding's success or failure. */
+ private final Set<BindCallback> mCallbacks = Sets.newHashSet();
+
+ /** Used to bind and unbind from the service. */
+ private ServiceConnection mServiceConnection;
+
+ /** The binder provided by {@link ServiceConnection#onServiceConnected} */
+ private IBinder mBinder;
+
+ private int mAssociatedCallCount = 0;
+
+ /**
+ * Indicates that an unbind request was made when the service was not yet bound. If the service
+ * successfully connects when this is true, it should be unbound immediately.
+ */
+ private boolean mIsBindingAborted;
+
+ /**
+ * Set of currently registered listeners.
+ * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
+ * load factor before resizing, 1 means we only expect a single thread to
+ * access the map so make only a single shard
+ */
+ private final Set<Listener> mListeners = Collections.newSetFromMap(
+ new ConcurrentHashMap<Listener, Boolean>(8, 0.9f, 1));
+
+ /**
+ * Persists the specified parameters and initializes the new instance.
+ *
+ * @param serviceAction The intent-action used with {@link Context#bindService}.
+ * @param componentName The component name of the service with which to bind.
+ */
+ protected ServiceBinder(String serviceAction, ComponentName componentName) {
+ Preconditions.checkState(!Strings.isNullOrEmpty(serviceAction));
+ Preconditions.checkNotNull(componentName);
+
+ mContext = TelecomApp.getInstance();
+ mServiceAction = serviceAction;
+ mComponentName = componentName;
+ }
+
+ final void incrementAssociatedCallCount() {
+ mAssociatedCallCount++;
+ Log.v(this, "Call count increment %d, %s", mAssociatedCallCount,
+ mComponentName.flattenToShortString());
+ }
+
+ final void decrementAssociatedCallCount() {
+ if (mAssociatedCallCount > 0) {
+ mAssociatedCallCount--;
+ Log.v(this, "Call count decrement %d, %s", mAssociatedCallCount,
+ mComponentName.flattenToShortString());
+
+ if (mAssociatedCallCount == 0) {
+ unbind();
+ }
+ } else {
+ Log.wtf(this, "%s: ignoring a request to decrement mAssociatedCallCount below zero",
+ mComponentName.getClassName());
+ }
+ }
+
+ final int getAssociatedCallCount() {
+ return mAssociatedCallCount;
+ }
+
+ /**
+ * Unbinds from the service if already bound, no-op otherwise.
+ */
+ final void unbind() {
+ ThreadUtil.checkOnMainThread();
+
+ if (mServiceConnection == null) {
+ // We're not yet bound, so queue up an abort request.
+ mIsBindingAborted = true;
+ } else {
+ logServiceDisconnected("unbind");
+ mContext.unbindService(mServiceConnection);
+ mServiceConnection = null;
+ setBinder(null);
+ }
+ }
+
+ final ComponentName getComponentName() {
+ return mComponentName;
+ }
+
+ final boolean isServiceValid(String actionName) {
+ if (mBinder == null) {
+ Log.w(this, "%s invoked while service is unbound", actionName);
+ return false;
+ }
+
+ return true;
+ }
+
+ final void addListener(Listener listener) {
+ mListeners.add(listener);
+ }
+
+ final void removeListener(Listener listener) {
+ if (listener != null) {
+ mListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Logs a standard message upon service disconnection. This method exists because there is no
+ * single method called whenever the service unbinds and we want to log the same string in all
+ * instances where that occurs. (Context.unbindService() does not cause onServiceDisconnected
+ * to execute).
+ *
+ * @param sourceTag Tag to disambiguate
+ */
+ private void logServiceDisconnected(String sourceTag) {
+ Log.i(this, "Service unbound %s, from %s.", mComponentName, sourceTag);
+ }
+
+ /**
+ * Notifies all the outstanding callbacks that the service is successfully bound. The list of
+ * outstanding callbacks is cleared afterwards.
+ */
+ private void handleSuccessfulConnection() {
+ for (BindCallback callback : mCallbacks) {
+ callback.onSuccess();
+ }
+ mCallbacks.clear();
+ }
+
+ /**
+ * Notifies all the outstanding callbacks that the service failed to bind. The list of
+ * outstanding callbacks is cleared afterwards.
+ */
+ private void handleFailedConnection() {
+ for (BindCallback callback : mCallbacks) {
+ callback.onFailure();
+ }
+ mCallbacks.clear();
+ }
+
+ /**
+ * Handles a service disconnection.
+ */
+ private void handleServiceDisconnected() {
+ setBinder(null);
+ }
+
+ private void clearAbort() {
+ mIsBindingAborted = false;
+ }
+
+ /**
+ * Sets the (private) binder and updates the child class.
+ *
+ * @param binder The new binder value.
+ */
+ private void setBinder(IBinder binder) {
+ if (mBinder != binder) {
+ mBinder = binder;
+
+ setServiceInterface(binder);
+
+ if (binder == null) {
+ for (Listener l : mListeners) {
+ l.onUnbind(this);
+ }
+ }
+ }
+ }
+
+ /**
+ * Sets the service interface after the service is bound or unbound.
+ *
+ * @param binder The actual bound service implementation.
+ */
+ protected abstract void setServiceInterface(IBinder binder);
+}
diff --git a/src/com/android/server/telecom/StatusBarNotifier.java b/src/com/android/server/telecom/StatusBarNotifier.java
new file mode 100644
index 0000000..14253a0
--- /dev/null
+++ b/src/com/android/server/telecom/StatusBarNotifier.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.app.StatusBarManager;
+import android.content.Context;
+
+/**
+ * Manages the special status bar notifications used by the phone app.
+ */
+final class StatusBarNotifier extends CallsManagerListenerBase {
+ private static final String SLOT_MUTE = "mute";
+ private static final String SLOT_SPEAKERPHONE = "speakerphone";
+
+ private final Context mContext;
+ private final CallsManager mCallsManager;
+ private final StatusBarManager mStatusBarManager;
+
+ private boolean mIsShowingMute;
+ private boolean mIsShowingSpeakerphone;
+
+ StatusBarNotifier(Context context, CallsManager callsManager) {
+ mContext = context;
+ mCallsManager = callsManager;
+ mStatusBarManager = (StatusBarManager) context.getSystemService(Context.STATUS_BAR_SERVICE);
+ }
+
+ /** ${inheritDoc} */
+ @Override
+ public void onCallRemoved(Call call) {
+ if (!mCallsManager.hasAnyCalls()) {
+ notifyMute(false);
+ notifySpeakerphone(false);
+ }
+ }
+
+ void notifyMute(boolean isMuted) {
+ // Never display anything if there are no calls.
+ if (!mCallsManager.hasAnyCalls()) {
+ isMuted = false;
+ }
+
+ if (mIsShowingMute == isMuted) {
+ return;
+ }
+
+ Log.d(this, "Mute status bar icon being set to %b", isMuted);
+
+ if (isMuted) {
+ mStatusBarManager.setIcon(
+ SLOT_MUTE,
+ android.R.drawable.stat_notify_call_mute,
+ 0, /* iconLevel */
+ mContext.getString(R.string.accessibility_call_muted));
+ } else {
+ mStatusBarManager.removeIcon(SLOT_MUTE);
+ }
+ mIsShowingMute = isMuted;
+ }
+
+ void notifySpeakerphone(boolean isSpeakerphone) {
+ // Never display anything if there are no calls.
+ if (!mCallsManager.hasAnyCalls()) {
+ isSpeakerphone = false;
+ }
+
+ if (mIsShowingSpeakerphone == isSpeakerphone) {
+ return;
+ }
+
+ Log.d(this, "Speakerphone status bar icon being set to %b", isSpeakerphone);
+
+ if (isSpeakerphone) {
+ mStatusBarManager.setIcon(
+ SLOT_SPEAKERPHONE,
+ android.R.drawable.stat_sys_speakerphone,
+ 0, /* iconLevel */
+ mContext.getString(R.string.accessibility_speakerphone_enabled));
+ } else {
+ mStatusBarManager.removeIcon(SLOT_SPEAKERPHONE);
+ }
+ mIsShowingSpeakerphone = isSpeakerphone;
+ }
+}
diff --git a/src/com/android/server/telecom/TelecomApp.java b/src/com/android/server/telecom/TelecomApp.java
new file mode 100644
index 0000000..c2e79eb
--- /dev/null
+++ b/src/com/android/server/telecom/TelecomApp.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.app.Application;
+import android.os.UserHandle;
+
+/**
+ * Top-level Application class for Telecom.
+ */
+public final class TelecomApp extends Application {
+
+ /** Singleton instance of TelecomApp. */
+ private static TelecomApp sInstance;
+
+ /**
+ * Missed call notifier. Exists here so that the instance can be shared with
+ * {@link TelecomBroadcastReceiver}.
+ */
+ private MissedCallNotifier mMissedCallNotifier;
+
+ /**
+ * Maintains the list of registered {@link android.telecom.PhoneAccountHandle}s.
+ */
+ private PhoneAccountRegistrar mPhoneAccountRegistrar;
+
+ /** {@inheritDoc} */
+ @Override public void onCreate() {
+ super.onCreate();
+ sInstance = this;
+
+ mMissedCallNotifier = new MissedCallNotifier(this);
+ mPhoneAccountRegistrar = new PhoneAccountRegistrar(this);
+
+ if (UserHandle.myUserId() == UserHandle.USER_OWNER) {
+ TelecomServiceImpl.init(mMissedCallNotifier, mPhoneAccountRegistrar);
+ }
+ }
+
+ public static TelecomApp getInstance() {
+ if (null == sInstance) {
+ throw new IllegalStateException("No TelecomApp running.");
+ }
+ return sInstance;
+ }
+
+ MissedCallNotifier getMissedCallNotifier() {
+ return mMissedCallNotifier;
+ }
+
+ PhoneAccountRegistrar getPhoneAccountRegistrar() {
+ return mPhoneAccountRegistrar;
+ }
+}
diff --git a/src/com/android/server/telecom/TelecomBroadcastReceiver.java b/src/com/android/server/telecom/TelecomBroadcastReceiver.java
new file mode 100644
index 0000000..36c23fc
--- /dev/null
+++ b/src/com/android/server/telecom/TelecomBroadcastReceiver.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+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 TelecomBroadcastReceiver extends BroadcastReceiver {
+ /** The action used to send SMS response for the missed call notification. */
+ 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 =
+ "com.android.server.telecom.ACTION_CALL_BACK_FROM_NOTIFICATION";
+
+ /** The action used to clear missed calls. */
+ static final String ACTION_CLEAR_MISSED_CALLS =
+ "com.android.server.telecom.ACTION_CLEAR_MISSED_CALLS";
+
+
+ /** {@inheritDoc} */
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+
+ Log.v(this, "Action received: %s.", action);
+
+ MissedCallNotifier missedCallNotifier = TelecomApp.getInstance().getMissedCallNotifier();
+
+ // Send an SMS from the missed call notification.
+ if (ACTION_SEND_SMS_FROM_NOTIFICATION.equals(action)) {
+ // Close the notification shade and the notification itself.
+ closeSystemDialogs(context);
+ missedCallNotifier.clearMissedCalls();
+
+ Intent callIntent = new Intent(Intent.ACTION_SENDTO, intent.getData());
+ callIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(callIntent);
+
+ // Call back recent caller from the missed call notification.
+ } else if (ACTION_CALL_BACK_FROM_NOTIFICATION.equals(action)) {
+ // Close the notification shade and the notification itself.
+ closeSystemDialogs(context);
+ missedCallNotifier.clearMissedCalls();
+
+ Intent callIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, intent.getData());
+ callIntent.setFlags(
+ Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+ context.startActivity(callIntent);
+
+ // Clear the missed call notification and call log entries.
+ } else if (ACTION_CLEAR_MISSED_CALLS.equals(action)) {
+ missedCallNotifier.clearMissedCalls();
+ }
+ }
+
+ /**
+ * Closes open system dialogs and the notification shade.
+ */
+ private void closeSystemDialogs(Context context) {
+ Intent intent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
+ context.sendBroadcastAsUser(intent, UserHandle.ALL);
+ }
+}
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
new file mode 100644
index 0000000..5bce513
--- /dev/null
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -0,0 +1,624 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.app.AppOpsManager;
+import android.content.ComponentName;
+import android.content.Context;
+
+import android.content.Intent;
+import android.content.res.Resources;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.telecom.CallState;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telephony.TelephonyManager;
+
+import com.android.internal.telecom.ITelecomService;
+
+import java.util.List;
+
+/**
+ * Implementation of the ITelecom interface.
+ */
+public class TelecomServiceImpl extends ITelecomService.Stub {
+ private static final String REGISTER_PROVIDER_OR_SUBSCRIPTION =
+ "com.android.server.telecom.permission.REGISTER_PROVIDER_OR_SUBSCRIPTION";
+
+ /** ${inheritDoc} */
+ @Override
+ public IBinder asBinder() {
+ return super.asBinder();
+ }
+
+ /**
+ * A request object for use with {@link MainThreadHandler}. Requesters should wait() on the
+ * request after sending. The main thread will notify the request when it is complete.
+ */
+ private static final class MainThreadRequest {
+ /** The result of the request that is run on the main thread */
+ public Object result;
+ }
+
+ /**
+ * A handler that processes messages on the main thread in the phone process. Since many
+ * of the Phone calls are not thread safe this is needed to shuttle the requests from the
+ * inbound binder threads to the main thread in the phone process.
+ */
+ private final class MainThreadHandler extends Handler {
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.obj instanceof MainThreadRequest) {
+ MainThreadRequest request = (MainThreadRequest) msg.obj;
+ Object result = null;
+ switch (msg.what) {
+ case MSG_SILENCE_RINGER:
+ mCallsManager.getRinger().silence();
+ break;
+ case MSG_SHOW_CALL_SCREEN:
+ mCallsManager.getInCallController().bringToForeground(msg.arg1 == 1);
+ break;
+ case MSG_END_CALL:
+ result = endCallInternal();
+ break;
+ case MSG_ACCEPT_RINGING_CALL:
+ acceptRingingCallInternal();
+ break;
+ case MSG_CANCEL_MISSED_CALLS_NOTIFICATION:
+ mMissedCallNotifier.clearMissedCalls();
+ break;
+ case MSG_IS_TTY_SUPPORTED:
+ result = mCallsManager.isTtySupported();
+ break;
+ case MSG_GET_CURRENT_TTY_MODE:
+ result = mCallsManager.getCurrentTtyMode();
+ break;
+ }
+
+ if (result != null) {
+ request.result = result;
+ synchronized(request) {
+ request.notifyAll();
+ }
+ }
+ }
+ }
+ }
+
+ /** Private constructor; @see init() */
+ private static final String TAG = TelecomServiceImpl.class.getSimpleName();
+
+ private static final String SERVICE_NAME = "telecom";
+
+ private static final int MSG_SILENCE_RINGER = 1;
+ private static final int MSG_SHOW_CALL_SCREEN = 2;
+ private static final int MSG_END_CALL = 3;
+ private static final int MSG_ACCEPT_RINGING_CALL = 4;
+ private static final int MSG_CANCEL_MISSED_CALLS_NOTIFICATION = 5;
+ private static final int MSG_IS_TTY_SUPPORTED = 6;
+ private static final int MSG_GET_CURRENT_TTY_MODE = 7;
+
+ /** The singleton instance. */
+ private static TelecomServiceImpl sInstance;
+
+ private final MainThreadHandler mMainThreadHandler = new MainThreadHandler();
+ private final CallsManager mCallsManager = CallsManager.getInstance();
+ private final MissedCallNotifier mMissedCallNotifier;
+ private final PhoneAccountRegistrar mPhoneAccountRegistrar;
+ private final AppOpsManager mAppOpsManager;
+
+ private TelecomServiceImpl(
+ MissedCallNotifier missedCallNotifier, PhoneAccountRegistrar phoneAccountRegistrar) {
+ mMissedCallNotifier = missedCallNotifier;
+ mPhoneAccountRegistrar = phoneAccountRegistrar;
+ mAppOpsManager =
+ (AppOpsManager) TelecomApp.getInstance().getSystemService(Context.APP_OPS_SERVICE);
+
+ publish();
+ }
+
+ /**
+ * Initialize the singleton TelecommServiceImpl instance.
+ * This is only done once, at startup, from TelecommApp.onCreate().
+ */
+ static TelecomServiceImpl init(
+ MissedCallNotifier missedCallNotifier, PhoneAccountRegistrar phoneAccountRegistrar) {
+ synchronized (TelecomServiceImpl.class) {
+ if (sInstance == null) {
+ sInstance = new TelecomServiceImpl(missedCallNotifier, phoneAccountRegistrar);
+ } else {
+ Log.wtf(TAG, "init() called multiple times! sInstance %s", sInstance);
+ }
+ return sInstance;
+ }
+ }
+
+ //
+ // Implementation of the ITelecomService interface.
+ //
+
+ @Override
+ public PhoneAccountHandle getDefaultOutgoingPhoneAccount(String uriScheme) {
+ try {
+ return mPhoneAccountRegistrar.getDefaultOutgoingPhoneAccount(uriScheme);
+ } catch (Exception e) {
+ Log.e(this, e, "getDefaultOutgoingPhoneAccount");
+ throw e;
+ }
+ }
+
+ @Override
+ public PhoneAccountHandle getUserSelectedOutgoingPhoneAccount() {
+ try {
+ return mPhoneAccountRegistrar.getUserSelectedOutgoingPhoneAccount();
+ } catch (Exception e) {
+ Log.e(this, e, "getUserSelectedOutgoingPhoneAccount");
+ throw e;
+ }
+ }
+
+ @Override
+ public void setUserSelectedOutgoingPhoneAccount(PhoneAccountHandle accountHandle) {
+ enforceModifyPermission();
+
+ try {
+ mPhoneAccountRegistrar.setUserSelectedOutgoingPhoneAccount(accountHandle);
+ } catch (Exception e) {
+ Log.e(this, e, "setUserSelectedOutgoingPhoneAccount");
+ throw e;
+ }
+ }
+
+ @Override
+ public List<PhoneAccountHandle> getEnabledPhoneAccounts() {
+ try {
+ return mPhoneAccountRegistrar.getEnabledPhoneAccounts();
+ } catch (Exception e) {
+ Log.e(this, e, "getEnabledPhoneAccounts");
+ throw e;
+ }
+ }
+
+ @Override
+ public List<PhoneAccountHandle> getPhoneAccountsSupportingScheme(String uriScheme) {
+ try {
+ return mPhoneAccountRegistrar.getEnabledPhoneAccounts(uriScheme);
+ } catch (Exception e) {
+ Log.e(this, e, "getPhoneAccountsSupportingScheme");
+ throw e;
+ }
+ }
+
+ @Override
+ public PhoneAccount getPhoneAccount(PhoneAccountHandle accountHandle) {
+ try {
+ return mPhoneAccountRegistrar.getPhoneAccount(accountHandle);
+ } catch (Exception e) {
+ Log.e(this, e, "getPhoneAccount %s", accountHandle);
+ throw e;
+ }
+ }
+
+ @Override
+ public int getAllPhoneAccountsCount() {
+ try {
+ return mPhoneAccountRegistrar.getAllPhoneAccountsCount();
+ } catch (Exception e) {
+ Log.e(this, e, "getAllPhoneAccountsCount");
+ throw e;
+ }
+ }
+
+ @Override
+ public List<PhoneAccount> getAllPhoneAccounts() {
+ try {
+ return mPhoneAccountRegistrar.getAllPhoneAccounts();
+ } catch (Exception e) {
+ Log.e(this, e, "getAllPhoneAccounts");
+ throw e;
+ }
+ }
+
+ @Override
+ public List<PhoneAccountHandle> getAllPhoneAccountHandles() {
+ try {
+ return mPhoneAccountRegistrar.getAllPhoneAccountHandles();
+ } catch (Exception e) {
+ Log.e(this, e, "getAllPhoneAccounts");
+ throw e;
+ }
+ }
+
+ @Override
+ public PhoneAccountHandle getSimCallManager() {
+ try {
+ return mPhoneAccountRegistrar.getSimCallManager();
+ } catch (Exception e) {
+ Log.e(this, e, "getSimCallManager");
+ throw e;
+ }
+ }
+
+ @Override
+ public void setSimCallManager(PhoneAccountHandle accountHandle) {
+ enforceModifyPermission();
+
+ try {
+ mPhoneAccountRegistrar.setSimCallManager(accountHandle);
+ } catch (Exception e) {
+ Log.e(this, e, "setSimCallManager");
+ throw e;
+ }
+ }
+
+ @Override
+ public List<PhoneAccountHandle> getSimCallManagers() {
+ try {
+ return mPhoneAccountRegistrar.getConnectionManagerPhoneAccounts();
+ } catch (Exception e) {
+ Log.e(this, e, "getSimCallManagers");
+ throw e;
+ }
+ }
+
+ @Override
+ public void registerPhoneAccount(PhoneAccount account) {
+ try {
+ enforceModifyPermissionOrCallingPackage(
+ account.getAccountHandle().getComponentName().getPackageName());
+ if (account.hasCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER) ||
+ account.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
+ enforceRegisterProviderOrSubscriptionPermission();
+ }
+
+ // If the account is marked as enabled or has CAPABILITY_ALWAYS_ENABLED set, check to
+ // ensure the caller has modify permission. If they do not, set the account to be
+ // disabled and remove CAPABILITY_ALWAYS_ENABLED.
+ if (account.isEnabled() ||
+ account.hasCapabilities(PhoneAccount.CAPABILITY_ALWAYS_ENABLED)) {
+ try {
+ enforceModifyPermission();
+ } catch (SecurityException e) {
+ // Caller does not have modify permission, so change account to disabled by
+ // default and remove the CAPABILITY_ALWAYS_ENABLED capability.
+ int capabilities = account.getCapabilities() &
+ ~PhoneAccount.CAPABILITY_ALWAYS_ENABLED;
+ account = account.toBuilder()
+ .setEnabled(false)
+ .setCapabilities(capabilities)
+ .build();
+ }
+ }
+
+ mPhoneAccountRegistrar.registerPhoneAccount(account);
+ } catch (Exception e) {
+ Log.e(this, e, "registerPhoneAccount %s", account);
+ throw e;
+ }
+ }
+
+ @Override
+ public void setPhoneAccountEnabled(PhoneAccountHandle account, boolean isEnabled) {
+ try {
+ enforceModifyPermission();
+ mPhoneAccountRegistrar.setPhoneAccountEnabled(account, isEnabled);
+ } catch (Exception e) {
+ Log.e(this, e, "setPhoneAccountEnabled %s %d", account, isEnabled ? 1 : 0);
+ throw e;
+ }
+ }
+
+ @Override
+ public void unregisterPhoneAccount(PhoneAccountHandle accountHandle) {
+ try {
+ enforceModifyPermissionOrCallingPackage(
+ accountHandle.getComponentName().getPackageName());
+ mPhoneAccountRegistrar.unregisterPhoneAccount(accountHandle);
+ } catch (Exception e) {
+ Log.e(this, e, "unregisterPhoneAccount %s", accountHandle);
+ throw e;
+ }
+ }
+
+ @Override
+ public void clearAccounts(String packageName) {
+ try {
+ enforceModifyPermissionOrCallingPackage(packageName);
+ mPhoneAccountRegistrar.clearAccounts(packageName);
+ } catch (Exception e) {
+ Log.e(this, e, "clearAccounts %s", packageName);
+ throw e;
+ }
+ }
+
+ /**
+ * @see android.telecom.TelecomManager#silenceRinger
+ */
+ @Override
+ public void silenceRinger() {
+ Log.d(this, "silenceRinger");
+ enforceModifyPermission();
+ sendRequestAsync(MSG_SILENCE_RINGER, 0);
+ }
+
+ /**
+ * @see android.telecom.TelecomManager#getDefaultPhoneApp
+ */
+ @Override
+ public ComponentName getDefaultPhoneApp() {
+ Resources resources = TelecomApp.getInstance().getResources();
+ return new ComponentName(
+ resources.getString(R.string.ui_default_package),
+ resources.getString(R.string.dialer_default_class));
+ }
+
+ /**
+ * @see android.telecom.TelecomManager#isInCall
+ */
+ @Override
+ 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().
+ return mCallsManager.hasAnyCalls();
+ }
+
+ /**
+ * @see android.telecom.TelecomManager#isRinging
+ */
+ @Override
+ public boolean isRinging() {
+ enforceReadPermission();
+ return mCallsManager.hasRingingCall();
+ }
+
+ /**
+ * @see android.telecom.TelecomManager#endCall
+ */
+ @Override
+ public boolean endCall() {
+ enforceModifyPermission();
+ return (boolean) sendRequest(MSG_END_CALL);
+ }
+
+ /**
+ * @see android.telecom.TelecomManager#acceptRingingCall
+ */
+ @Override
+ public void acceptRingingCall() {
+ enforceModifyPermission();
+ sendRequestAsync(MSG_ACCEPT_RINGING_CALL, 0);
+ }
+
+ /**
+ * @see android.telecom.TelecomManager#showInCallScreen
+ */
+ @Override
+ public void showInCallScreen(boolean showDialpad) {
+ enforceReadPermissionOrDefaultDialer();
+ sendRequestAsync(MSG_SHOW_CALL_SCREEN, showDialpad ? 1 : 0);
+ }
+
+ /**
+ * @see android.telecom.TelecomManager#cancelMissedCallsNotification
+ */
+ @Override
+ public void cancelMissedCallsNotification() {
+ enforceModifyPermissionOrDefaultDialer();
+ sendRequestAsync(MSG_CANCEL_MISSED_CALLS_NOTIFICATION, 0);
+ }
+
+ /**
+ * @see android.telecom.TelecomManager#handleMmi
+ */
+ @Override
+ public boolean handlePinMmi(String dialString) {
+ enforceModifyPermissionOrDefaultDialer();
+
+ // Switch identity so that TelephonyManager checks Telecom's permissions instead.
+ long token = Binder.clearCallingIdentity();
+ boolean retval = getTelephonyManager().handlePinMmi(dialString);
+ Binder.restoreCallingIdentity(token);
+
+ return retval;
+ }
+
+ /**
+ * @see android.telecom.TelecomManager#isTtySupported
+ */
+ @Override
+ public boolean isTtySupported() {
+ enforceReadPermission();
+ return (boolean) sendRequest(MSG_IS_TTY_SUPPORTED);
+ }
+
+ /**
+ * @see android.telecom.TelecomManager#getCurrentTtyMode
+ */
+ @Override
+ public int getCurrentTtyMode() {
+ enforceReadPermission();
+ return (int) sendRequest(MSG_GET_CURRENT_TTY_MODE);
+ }
+
+ /**
+ * @see android.telecom.TelecomManager#addNewIncomingCall
+ */
+ @Override
+ public void addNewIncomingCall(PhoneAccountHandle phoneAccountHandle, Bundle extras) {
+ if (phoneAccountHandle != null && phoneAccountHandle.getComponentName() != null) {
+ mAppOpsManager.checkPackage(
+ Binder.getCallingUid(), phoneAccountHandle.getComponentName().getPackageName());
+
+ Intent intent = new Intent(TelecomManager.ACTION_INCOMING_CALL);
+ intent.setPackage(TelecomApp.getInstance().getPackageName());
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle);
+ if (extras != null) {
+ intent.putExtra(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS, extras);
+ }
+
+ long token = Binder.clearCallingIdentity();
+ TelecomApp.getInstance().startActivityAsUser(intent, UserHandle.CURRENT);
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ //
+ // Supporting methods for the ITelecomService interface implementation.
+ //
+
+ private void acceptRingingCallInternal() {
+ Call call = mCallsManager.getFirstCallWithState(CallState.RINGING);
+ if (call != null) {
+ call.answer(call.getVideoState());
+ }
+ }
+
+ 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 = mCallsManager.getForegroundCall();
+ if (call == null) {
+ call = mCallsManager.getFirstCallWithState(
+ CallState.ACTIVE,
+ CallState.DIALING,
+ CallState.RINGING,
+ CallState.ON_HOLD);
+ }
+
+ if (call != null) {
+ if (call.getState() == CallState.RINGING) {
+ call.reject(false /* rejectWithMessage */, null);
+ } else {
+ call.disconnect();
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Make sure the caller has the MODIFY_PHONE_STATE permission.
+ *
+ * @throws SecurityException if the caller does not have the required permission
+ */
+ private void enforceModifyPermission() {
+ TelecomApp.getInstance().enforceCallingOrSelfPermission(
+ android.Manifest.permission.MODIFY_PHONE_STATE, null);
+ }
+
+ private void enforceModifyPermissionOrDefaultDialer() {
+ if (!isDefaultDialerCalling()) {
+ enforceModifyPermission();
+ }
+ }
+
+ private void enforceRegisterProviderOrSubscriptionPermission() {
+ TelecomApp.getInstance().enforceCallingOrSelfPermission(
+ REGISTER_PROVIDER_OR_SUBSCRIPTION, null);
+ }
+
+ private void enforceModifyPermissionOrCallingPackage(String packageName) {
+ // TODO: Use a new telecom permission for this instead of reusing modify.
+ try {
+ enforceModifyPermission();
+ } catch (SecurityException e) {
+ enforceCallingPackage(packageName);
+ }
+ }
+
+ private void enforceReadPermission() {
+ TelecomApp.getInstance().enforceCallingOrSelfPermission(
+ android.Manifest.permission.READ_PHONE_STATE, null);
+ }
+
+ private void enforceReadPermissionOrDefaultDialer() {
+ if (!isDefaultDialerCalling()) {
+ enforceReadPermission();
+ }
+ }
+
+ private void enforceCallingPackage(String packageName) {
+ mAppOpsManager.checkPackage(Binder.getCallingUid(), packageName);
+ }
+
+ private boolean isDefaultDialerCalling() {
+ ComponentName defaultDialerComponent = getDefaultPhoneApp();
+ if (defaultDialerComponent != null) {
+ try {
+ mAppOpsManager.checkPackage(
+ Binder.getCallingUid(), defaultDialerComponent.getPackageName());
+ return true;
+ } catch (SecurityException e) {
+ Log.e(TAG, e, "Could not get default dialer.");
+ }
+ }
+ return false;
+ }
+
+ private TelephonyManager getTelephonyManager() {
+ return (TelephonyManager)
+ TelecomApp.getInstance().getSystemService(Context.TELEPHONY_SERVICE);
+ }
+
+ private void publish() {
+ Log.d(this, "publish: %s", this);
+ ServiceManager.addService(SERVICE_NAME, this);
+ }
+
+ private MainThreadRequest sendRequestAsync(int command, int arg1) {
+ MainThreadRequest request = new MainThreadRequest();
+ mMainThreadHandler.obtainMessage(command, arg1, 0, request).sendToTarget();
+ return request;
+ }
+
+ /**
+ * Posts the specified command to be executed on the main thread, waits for the request to
+ * complete, and returns the result.
+ */
+ private Object sendRequest(int command) {
+ if (Looper.myLooper() == mMainThreadHandler.getLooper()) {
+ MainThreadRequest request = new MainThreadRequest();
+ mMainThreadHandler.handleMessage(mMainThreadHandler.obtainMessage(command, request));
+ return request.result;
+ } else {
+ MainThreadRequest request = sendRequestAsync(command, 0);
+
+ // Wait for the request to complete
+ synchronized (request) {
+ while (request.result == null) {
+ try {
+ request.wait();
+ } catch (InterruptedException e) {
+ // Do nothing, go back and wait until the request is complete
+ }
+ }
+ }
+ return request.result;
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/TelephonyUtil.java b/src/com/android/server/telecom/TelephonyUtil.java
new file mode 100644
index 0000000..29b6f89
--- /dev/null
+++ b/src/com/android/server/telecom/TelephonyUtil.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.net.Uri;
+import android.telephony.PhoneNumberUtils;
+
+/**
+ * Utilities to deal with the system telephony services. The system telephony services are treated
+ * differently from 3rd party services in some situations (emergency calls, audio focus, etc...).
+ */
+public final class TelephonyUtil {
+ private static final String TAG = TelephonyUtil.class.getSimpleName();
+
+ private static final String TELEPHONY_PACKAGE_NAME = "com.android.phone";
+
+ private static final String PSTN_CALL_SERVICE_CLASS_NAME =
+ "com.android.services.telephony.TelephonyConnectionService";
+
+ private TelephonyUtil() {}
+
+ static boolean isPstnComponentName(ComponentName componentName) {
+ final ComponentName pstnComponentName = new ComponentName(
+ TELEPHONY_PACKAGE_NAME, PSTN_CALL_SERVICE_CLASS_NAME);
+ return pstnComponentName.equals(componentName);
+ }
+
+ static boolean shouldProcessAsEmergency(Context context, Uri handle) {
+ return handle != null && PhoneNumberUtils.isPotentialLocalEmergencyNumber(
+ context, handle.getSchemeSpecificPart());
+ }
+}
diff --git a/src/com/android/server/telecom/ThreadUtil.java b/src/com/android/server/telecom/ThreadUtil.java
new file mode 100644
index 0000000..650e73f
--- /dev/null
+++ b/src/com/android/server/telecom/ThreadUtil.java
@@ -0,0 +1,53 @@
+/*
+ * 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;
+
+import android.os.Looper;
+
+/**
+ * Helper methods to deal with threading related tasks.
+ */
+public final class ThreadUtil {
+ private static final String TAG = ThreadUtil.class.getSimpleName();
+
+ private ThreadUtil() {}
+
+ /** @return whether this method is being called from the main (UI) thread. */
+ public static boolean isOnMainThread() {
+ return Looper.getMainLooper() == Looper.myLooper();
+ }
+
+ /**
+ * Checks that this is being executed on the main thread. If not, a message is logged at
+ * WTF-level priority.
+ */
+ public static void checkOnMainThread() {
+ if (!isOnMainThread()) {
+ Log.wtf(TAG, new IllegalStateException(), "Must be on the main thread!");
+ }
+ }
+
+ /**
+ * Checks that this is not being executed on the main thread. If so, a message is logged at
+ * WTF-level priority.
+ */
+ public static void checkNotOnMainThread() {
+ if (isOnMainThread()) {
+ Log.wtf(TAG, new IllegalStateException(), "Must not be on the main thread!");
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/Timeouts.java b/src/com/android/server/telecom/Timeouts.java
new file mode 100644
index 0000000..4434db4
--- /dev/null
+++ b/src/com/android/server/telecom/Timeouts.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.provider.Settings;
+
+/**
+ * A helper class which serves only to make it easier to lookup timeout values. This class should
+ * never be instantiated, and only accessed through the {@link #get(String, long)} method.
+ *
+ * These methods are safe to call from any thread, including the UI thread.
+ */
+public final class Timeouts {
+ /** A prefix to use for all keys so to not clobber the global namespace. */
+ private static final String PREFIX = "telecom.";
+
+ private Timeouts() {}
+
+ /**
+ * Returns the timeout value from Settings or the default value if it hasn't been changed. This
+ * method is safe to call from any thread, including the UI thread.
+ *
+ * @param key Settings key to retrieve.
+ * @param defaultValue Default value, in milliseconds.
+ * @return The timeout value from Settings or the default value if it hasn't been changed.
+ */
+ private static long get(String key, long defaultValue) {
+ return Settings.Secure.getLong(
+ TelecomApp.getInstance().getContentResolver(), PREFIX + key, defaultValue);
+ }
+
+ /**
+ * Returns the longest period, in milliseconds, to wait for the query for direct-to-voicemail
+ * to complete. If the query goes beyond this timeout, the incoming call screen is shown to the
+ * user.
+ */
+ public static long getDirectToVoicemailMillis() {
+ return get("direct_to_voicemail_ms", 500L);
+ }
+}
diff --git a/src/com/android/server/telecom/TtyManager.java b/src/com/android/server/telecom/TtyManager.java
new file mode 100644
index 0000000..945da5e
--- /dev/null
+++ b/src/com/android/server/telecom/TtyManager.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.telecom.TelecomManager;
+
+final class TtyManager implements WiredHeadsetManager.Listener {
+ private final TtyBroadcastReceiver mReceiver = new TtyBroadcastReceiver();
+ private final Context mContext;
+ private final WiredHeadsetManager mWiredHeadsetManager;
+ private int mPreferredTtyMode = TelecomManager.TTY_MODE_OFF;
+ private int mCurrentTtyMode = TelecomManager.TTY_MODE_OFF;
+
+ TtyManager(Context context, WiredHeadsetManager wiredHeadsetManager) {
+ mContext = context;
+ mWiredHeadsetManager = wiredHeadsetManager;
+ mWiredHeadsetManager.addListener(this);
+
+ mPreferredTtyMode = Settings.Secure.getInt(
+ mContext.getContentResolver(),
+ Settings.Secure.PREFERRED_TTY_MODE,
+ TelecomManager.TTY_MODE_OFF);
+
+ IntentFilter intentFilter = new IntentFilter(
+ TelecomManager.ACTION_TTY_PREFERRED_MODE_CHANGED);
+ mContext.registerReceiver(mReceiver, intentFilter);
+
+ updateCurrentTtyMode();
+ }
+
+ boolean isTtySupported() {
+ boolean isEnabled = mContext.getResources().getBoolean(R.bool.tty_enabled);
+ Log.v(this, "isTtySupported: %b", isEnabled);
+ return isEnabled;
+ }
+
+ int getCurrentTtyMode() {
+ return mCurrentTtyMode;
+ }
+
+ @Override
+ public void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn) {
+ Log.v(this, "onWiredHeadsetPluggedInChanged");
+ updateCurrentTtyMode();
+ }
+
+ private void updateCurrentTtyMode() {
+ int newTtyMode = TelecomManager.TTY_MODE_OFF;
+ if (isTtySupported() && mWiredHeadsetManager.isPluggedIn()) {
+ newTtyMode = mPreferredTtyMode;
+ }
+ Log.v(this, "updateCurrentTtyMode, %d -> %d", mCurrentTtyMode, newTtyMode);
+
+ if (mCurrentTtyMode != newTtyMode) {
+ mCurrentTtyMode = newTtyMode;
+ Intent ttyModeChanged = new Intent(TelecomManager.ACTION_CURRENT_TTY_MODE_CHANGED);
+ ttyModeChanged.putExtra(TelecomManager.EXTRA_CURRENT_TTY_MODE, mCurrentTtyMode);
+ mContext.sendBroadcastAsUser(ttyModeChanged, UserHandle.ALL);
+
+ updateAudioTtyMode();
+ }
+ }
+
+ private void updateAudioTtyMode() {
+ String audioTtyMode;
+ switch (mCurrentTtyMode) {
+ case TelecomManager.TTY_MODE_FULL:
+ audioTtyMode = "tty_full";
+ break;
+ case TelecomManager.TTY_MODE_VCO:
+ audioTtyMode = "tty_vco";
+ break;
+ case TelecomManager.TTY_MODE_HCO:
+ audioTtyMode = "tty_hco";
+ break;
+ case TelecomManager.TTY_MODE_OFF:
+ default:
+ audioTtyMode = "tty_off";
+ break;
+ }
+ Log.v(this, "updateAudioTtyMode, %s", audioTtyMode);
+
+ AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+ audioManager.setParameters("tty_mode=" + audioTtyMode);
+ }
+
+ private final class TtyBroadcastReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ Log.v(TtyManager.this, "onReceive, action: %s", action);
+ if (action.equals(TelecomManager.ACTION_TTY_PREFERRED_MODE_CHANGED)) {
+ int newPreferredTtyMode = intent.getIntExtra(
+ TelecomManager.EXTRA_TTY_PREFERRED_MODE, TelecomManager.TTY_MODE_OFF);
+ if (mPreferredTtyMode != newPreferredTtyMode) {
+ mPreferredTtyMode = newPreferredTtyMode;
+ updateCurrentTtyMode();
+ }
+ }
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/WiredHeadsetManager.java b/src/com/android/server/telecom/WiredHeadsetManager.java
new file mode 100644
index 0000000..8ce7b61
--- /dev/null
+++ b/src/com/android/server/telecom/WiredHeadsetManager.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/** Listens for and caches headset state. */
+class WiredHeadsetManager {
+ interface Listener {
+ void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn);
+ }
+
+ /** Receiver for wired headset plugged and unplugged events. */
+ private class WiredHeadsetBroadcastReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(Intent.ACTION_HEADSET_PLUG)) {
+ boolean isPluggedIn = intent.getIntExtra("state", 0) == 1;
+ Log.v(WiredHeadsetManager.this, "ACTION_HEADSET_PLUG event, plugged in: %b",
+ isPluggedIn);
+ onHeadsetPluggedInChanged(isPluggedIn);
+ }
+ }
+ }
+
+ private final WiredHeadsetBroadcastReceiver mReceiver;
+ private boolean mIsPluggedIn;
+ /**
+ * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
+ * load factor before resizing, 1 means we only expect a single thread to
+ * access the map so make only a single shard
+ */
+ private final Set<Listener> mListeners = Collections.newSetFromMap(
+ new ConcurrentHashMap<Listener, Boolean>(8, 0.9f, 1));
+
+ WiredHeadsetManager(Context context) {
+ mReceiver = new WiredHeadsetBroadcastReceiver();
+
+ AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ mIsPluggedIn = audioManager.isWiredHeadsetOn();
+
+ // Register for misc other intent broadcasts.
+ IntentFilter intentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
+ context.registerReceiver(mReceiver, intentFilter);
+ }
+
+ void addListener(Listener listener) {
+ mListeners.add(listener);
+ }
+
+ void removeListener(Listener listener) {
+ if (listener != null) {
+ mListeners.remove(listener);
+ }
+ }
+
+ boolean isPluggedIn() {
+ return mIsPluggedIn;
+ }
+
+ private void onHeadsetPluggedInChanged(boolean isPluggedIn) {
+ if (mIsPluggedIn != isPluggedIn) {
+ Log.v(this, "onHeadsetPluggedInChanged, mIsPluggedIn: %b -> %b", mIsPluggedIn,
+ isPluggedIn);
+ boolean oldIsPluggedIn = mIsPluggedIn;
+ mIsPluggedIn = isPluggedIn;
+ for (Listener listener : mListeners) {
+ listener.onWiredHeadsetPluggedInChanged(oldIsPluggedIn, mIsPluggedIn);
+ }
+ }
+ }
+}