| /* |
| * 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.phone; |
| |
| 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.os.Handler; |
| import android.os.IBinder; |
| import android.os.Message; |
| import android.os.PowerManager; |
| import android.os.RemoteException; |
| import android.os.SystemClock; |
| import android.os.SystemProperties; |
| import android.text.TextUtils; |
| import android.util.Log; |
| |
| import com.android.internal.telephony.Connection; |
| import com.android.internal.telephony.Connection.PostDialState; |
| import com.android.phone.AudioRouter.AudioModeListener; |
| import com.android.phone.NotificationMgr.StatusBarHelper; |
| import com.android.services.telephony.common.AudioMode; |
| import com.android.services.telephony.common.Call; |
| import com.android.services.telephony.common.ICallHandlerService; |
| import com.google.common.collect.Lists; |
| |
| import java.util.List; |
| |
| /** |
| * This class is responsible for passing through call state changes to the CallHandlerService. |
| */ |
| public class CallHandlerServiceProxy extends Handler |
| implements CallModeler.Listener, AudioModeListener { |
| |
| private static final String TAG = CallHandlerServiceProxy.class.getSimpleName(); |
| private static final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt( |
| "ro.debuggable", 0) == 1); |
| |
| public static final int RETRY_DELAY_MILLIS = 2000; |
| private static final int BIND_RETRY_MSG = 1; |
| private static final int MAX_RETRY_COUNT = 5; |
| |
| private AudioRouter mAudioRouter; |
| private CallCommandService mCallCommandService; |
| private CallModeler mCallModeler; |
| private Context mContext; |
| private boolean mFullUpdateOnConnect; |
| |
| private ICallHandlerService mCallHandlerServiceGuarded; // Guarded by mServiceAndQueueLock |
| // Single queue to guarantee ordering |
| private List<QueueParams> mQueue; // Guarded by mServiceAndQueueLock |
| |
| private final Object mServiceAndQueueLock = new Object(); |
| private int mBindRetryCount = 0; |
| |
| @Override |
| public void handleMessage(Message msg) { |
| super.handleMessage(msg); |
| |
| switch (msg.what) { |
| case BIND_RETRY_MSG: |
| setupServiceConnection(); |
| break; |
| } |
| } |
| |
| public CallHandlerServiceProxy(Context context, CallModeler callModeler, |
| CallCommandService callCommandService, AudioRouter audioRouter) { |
| if (DBG) { |
| Log.d(TAG, "init CallHandlerServiceProxy"); |
| } |
| mContext = context; |
| mCallCommandService = callCommandService; |
| mCallModeler = callModeler; |
| mAudioRouter = audioRouter; |
| |
| mAudioRouter.addAudioModeListener(this); |
| mCallModeler.addListener(this); |
| } |
| |
| @Override |
| public void onDisconnect(Call call) { |
| // Wake up in case the screen was off. |
| wakeUpScreen(); |
| synchronized (mServiceAndQueueLock) { |
| if (mCallHandlerServiceGuarded == null) { |
| if (DBG) { |
| Log.d(TAG, "CallHandlerService not connected. Enqueue disconnect"); |
| } |
| enqueueDisconnect(call); |
| setupServiceConnection(); |
| return; |
| } |
| } |
| processDisconnect(call); |
| } |
| |
| private void wakeUpScreen() { |
| Log.d(TAG, "wakeUpScreen()"); |
| final PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); |
| pm.wakeUp(SystemClock.uptimeMillis()); |
| } |
| |
| private void processDisconnect(Call call) { |
| try { |
| if (DBG) { |
| Log.d(TAG, "onDisconnect: " + call); |
| } |
| synchronized (mServiceAndQueueLock) { |
| if (mCallHandlerServiceGuarded != null) { |
| mCallHandlerServiceGuarded.onDisconnect(call); |
| } |
| } |
| if (!mCallModeler.hasLiveCall()) { |
| unbind(); |
| } |
| } catch (Exception e) { |
| Log.e(TAG, "Remote exception handling onDisconnect ", e); |
| } |
| } |
| |
| @Override |
| public void onIncoming(Call call) { |
| synchronized (mServiceAndQueueLock) { |
| if (mCallHandlerServiceGuarded == null) { |
| if (DBG) { |
| Log.d(TAG, "CallHandlerService not connected. Enqueue incoming."); |
| } |
| enqueueIncoming(call); |
| setupServiceConnection(); |
| return; |
| } |
| } |
| processIncoming(call); |
| } |
| |
| private void processIncoming(Call call) { |
| if (DBG) { |
| Log.d(TAG, "onIncoming: " + call); |
| } |
| try { |
| // TODO: check RespondViaSmsManager.allowRespondViaSmsForCall() |
| // must refactor call method to accept proper call object. |
| synchronized (mServiceAndQueueLock) { |
| if (mCallHandlerServiceGuarded != null) { |
| mCallHandlerServiceGuarded.onIncoming(call, |
| RejectWithTextMessageManager.loadCannedResponses()); |
| } |
| } |
| } catch (Exception e) { |
| Log.e(TAG, "Remote exception handling onUpdate", e); |
| } |
| } |
| |
| @Override |
| public void onUpdate(List<Call> calls) { |
| synchronized (mServiceAndQueueLock) { |
| if (mCallHandlerServiceGuarded == null) { |
| if (DBG) { |
| Log.d(TAG, "CallHandlerService not connected. Enqueue update."); |
| } |
| enqueueUpdate(calls); |
| setupServiceConnection(); |
| return; |
| } |
| } |
| processUpdate(calls); |
| } |
| |
| private void processUpdate(List<Call> calls) { |
| if (DBG) { |
| Log.d(TAG, "onUpdate: " + calls.toString()); |
| } |
| try { |
| synchronized (mServiceAndQueueLock) { |
| if (mCallHandlerServiceGuarded != null) { |
| mCallHandlerServiceGuarded.onUpdate(calls); |
| } |
| } |
| if (!mCallModeler.hasLiveCall()) { |
| // TODO: unbinding happens in both onUpdate and onDisconnect because the ordering |
| // is not deterministic. Unbinding in both ensures that the service is unbound. |
| // But it also makes this in-efficient because we are unbinding twice, which leads |
| // to the CallHandlerService performing onCreate() and onDestroy() twice for each |
| // disconnect. |
| unbind(); |
| } |
| } catch (Exception e) { |
| Log.e(TAG, "Remote exception handling onUpdate", e); |
| } |
| } |
| |
| |
| @Override |
| public void onPostDialAction(Connection.PostDialState state, int callId, String remainingChars, |
| char currentChar) { |
| if (state != PostDialState.WAIT) return; |
| try { |
| synchronized (mServiceAndQueueLock) { |
| if (mCallHandlerServiceGuarded == null) { |
| if (DBG) { |
| Log.d(TAG, "CallHandlerService not conneccted. Skipping " |
| + "onPostDialWait()."); |
| } |
| return; |
| } |
| } |
| |
| mCallHandlerServiceGuarded.onPostDialWait(callId, remainingChars); |
| } catch (Exception e) { |
| Log.e(TAG, "Remote exception handling onUpdate", e); |
| } |
| } |
| |
| @Override |
| public void onAudioModeChange(int newMode, boolean muted) { |
| try { |
| synchronized (mServiceAndQueueLock) { |
| if (mCallHandlerServiceGuarded == null) { |
| if (DBG) { |
| Log.d(TAG, "CallHandlerService not conneccted. Skipping " |
| + "onAudioModeChange()."); |
| } |
| return; |
| } |
| } |
| |
| // Just do a simple log for now. |
| Log.i(TAG, "Updating with new audio mode: " + AudioMode.toString(newMode) + |
| " with mute " + muted); |
| |
| mCallHandlerServiceGuarded.onAudioModeChange(newMode, muted); |
| } catch (Exception e) { |
| Log.e(TAG, "Remote exception handling onAudioModeChange", e); |
| } |
| } |
| |
| @Override |
| public void onSupportedAudioModeChange(int modeMask) { |
| try { |
| synchronized (mServiceAndQueueLock) { |
| if (mCallHandlerServiceGuarded == null) { |
| if (DBG) { |
| Log.d(TAG, "CallHandlerService not conneccted. Skipping" |
| + "onSupportedAudioModeChange()."); |
| } |
| return; |
| } |
| } |
| |
| if (DBG) { |
| Log.d(TAG, "onSupportAudioModeChange: " + AudioMode.toString(modeMask)); |
| } |
| |
| mCallHandlerServiceGuarded.onSupportedAudioModeChange(modeMask); |
| } catch (Exception e) { |
| Log.e(TAG, "Remote exception handling onAudioModeChange", e); |
| } |
| |
| } |
| |
| private ServiceConnection mConnection = null; |
| |
| private class InCallServiceConnection implements ServiceConnection { |
| @Override public void onServiceConnected (ComponentName className, IBinder service){ |
| if (DBG) { |
| Log.d(TAG, "Service Connected"); |
| } |
| onCallHandlerServiceConnected(ICallHandlerService.Stub.asInterface(service)); |
| mBindRetryCount = 0; |
| } |
| |
| @Override public void onServiceDisconnected (ComponentName className){ |
| Log.i(TAG, "Disconnected from UI service."); |
| synchronized (mServiceAndQueueLock) { |
| // Technically, unbindService is un-necessary since the framework will schedule and |
| // restart the crashed service. But there is a exponential backoff for the restart. |
| // Unbind explicitly and setup again to avoid the backoff since it's important to |
| // always have an in call ui. |
| unbind(); |
| |
| reconnectOnRemainingCalls(); |
| } |
| } |
| } |
| |
| public void bringToForeground(boolean showDialpad) { |
| // only support this call if the service is already connected. |
| synchronized (mServiceAndQueueLock) { |
| if (mCallHandlerServiceGuarded != null && mCallModeler.hasLiveCall()) { |
| try { |
| if (DBG) Log.d(TAG, "bringToForeground: " + showDialpad); |
| mCallHandlerServiceGuarded.bringToForeground(showDialpad); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Exception handling bringToForeground", e); |
| } |
| } |
| } |
| } |
| |
| private static Intent getInCallServiceIntent(Context context) { |
| final Intent serviceIntent = new Intent(ICallHandlerService.class.getName()); |
| final ComponentName component = new ComponentName(context.getResources().getString( |
| R.string.ui_default_package), context.getResources().getString( |
| R.string.incall_default_class)); |
| serviceIntent.setComponent(component); |
| return serviceIntent; |
| } |
| |
| /** |
| * Sets up the connection with ICallHandlerService |
| */ |
| private void setupServiceConnection() { |
| if (!PhoneGlobals.sVoiceCapable) { |
| return; |
| } |
| |
| final Intent serviceIntent = getInCallServiceIntent(mContext); |
| if (DBG) { |
| Log.d(TAG, "binding to service " + serviceIntent); |
| } |
| |
| synchronized (mServiceAndQueueLock) { |
| if (mConnection == null) { |
| mConnection = new InCallServiceConnection(); |
| |
| final PackageManager packageManger = mContext.getPackageManager(); |
| final List<ResolveInfo> services = packageManger.queryIntentServices(serviceIntent, |
| 0); |
| |
| ServiceInfo serviceInfo = null; |
| |
| for (int i = 0; i < services.size(); i++) { |
| final ResolveInfo info = services.get(i); |
| if (info.serviceInfo != null) { |
| if (Manifest.permission.BIND_CALL_SERVICE.equals( |
| info.serviceInfo.permission)) { |
| serviceInfo = info.serviceInfo; |
| break; |
| } |
| } |
| } |
| |
| if (serviceInfo == null) { |
| // Service not found, retry again after some delay |
| // This can happen if the service is being installed by the package manager. |
| // Between deletes and installs, bindService could get a silent service not |
| // found error. |
| mBindRetryCount++; |
| if (mBindRetryCount < MAX_RETRY_COUNT) { |
| Log.w(TAG, "InCallUI service not found. " + serviceIntent |
| + ". This happens if the service is being installed and should be" |
| + " transient. Retrying" + RETRY_DELAY_MILLIS + " ms."); |
| sendMessageDelayed(Message.obtain(this, BIND_RETRY_MSG), |
| RETRY_DELAY_MILLIS); |
| } else { |
| Log.e(TAG, "Tried to bind to in-call UI " + MAX_RETRY_COUNT + " times." |
| + " Giving up."); |
| } |
| return; |
| } |
| |
| // Bind to the first service that has a permission |
| // TODO: Add UI to allow us to select between services |
| |
| serviceIntent.setComponent(new ComponentName(serviceInfo.packageName, |
| serviceInfo.name)); |
| if (DBG) { |
| Log.d(TAG, "binding to service " + serviceIntent); |
| } |
| if (!mContext.bindService(serviceIntent, mConnection, Context.BIND_AUTO_CREATE)) { |
| // This happens when the in-call package is in the middle of being |
| // installed. |
| // Delay the retry. |
| mBindRetryCount++; |
| if (mBindRetryCount < MAX_RETRY_COUNT) { |
| Log.e(TAG, "bindService failed on " + serviceIntent + ". Retrying in " |
| + RETRY_DELAY_MILLIS + " ms."); |
| sendMessageDelayed(Message.obtain(this, BIND_RETRY_MSG), |
| RETRY_DELAY_MILLIS); |
| } else { |
| Log.wtf(TAG, "Tried to bind to in-call UI " + MAX_RETRY_COUNT + " times." |
| + " Giving up."); |
| } |
| } |
| |
| } else { |
| Log.d(TAG, "Service connection to in call service already started."); |
| } |
| } |
| } |
| |
| private void unbind() { |
| synchronized (mServiceAndQueueLock) { |
| // On unbind, reenable the notification shade and navigation bar just in case the |
| // in-call UI crashed on an incoming call. |
| final StatusBarHelper statusBarHelper = PhoneGlobals.getInstance().notificationMgr. |
| statusBarHelper; |
| statusBarHelper.enableSystemBarNavigation(true); |
| statusBarHelper.enableExpandedView(true); |
| if (mCallHandlerServiceGuarded != null) { |
| Log.d(TAG, "Unbinding service."); |
| mCallHandlerServiceGuarded = null; |
| mContext.unbindService(mConnection); |
| } |
| mConnection = null; |
| } |
| } |
| |
| /** |
| * Called when the in-call UI service is connected. Send command interface to in-call. |
| */ |
| private void onCallHandlerServiceConnected(ICallHandlerService callHandlerService) { |
| |
| synchronized (mServiceAndQueueLock) { |
| mCallHandlerServiceGuarded = callHandlerService; |
| |
| // Before we send any updates, we need to set up the initial service calls. |
| makeInitialServiceCalls(); |
| |
| processQueue(); |
| |
| if (mFullUpdateOnConnect) { |
| mFullUpdateOnConnect = false; |
| onUpdate(mCallModeler.getFullList()); |
| } |
| } |
| } |
| |
| /** |
| * Checks to see if there are any live calls left, and if so, try reconnecting the UI. |
| */ |
| private void reconnectOnRemainingCalls() { |
| if (mCallModeler.hasLiveCall()) { |
| mFullUpdateOnConnect = true; |
| setupServiceConnection(); |
| } |
| } |
| |
| /** |
| * Makes initial service calls to set up callcommandservice and audio modes. |
| */ |
| private void makeInitialServiceCalls() { |
| try { |
| mCallHandlerServiceGuarded.startCallService(mCallCommandService); |
| |
| onSupportedAudioModeChange(mAudioRouter.getSupportedAudioModes()); |
| onAudioModeChange(mAudioRouter.getAudioMode(), mAudioRouter.getMute()); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Remote exception calling CallHandlerService::setCallCommandService", e); |
| } |
| } |
| |
| private List<QueueParams> getQueue() { |
| if (mQueue == null) { |
| mQueue = Lists.newArrayList(); |
| } |
| return mQueue; |
| } |
| |
| private void enqueueDisconnect(Call call) { |
| getQueue().add(new QueueParams(QueueParams.METHOD_DISCONNECT, new Call(call))); |
| } |
| |
| private void enqueueIncoming(Call call) { |
| getQueue().add(new QueueParams(QueueParams.METHOD_INCOMING, new Call(call))); |
| } |
| |
| private void enqueueUpdate(List<Call> calls) { |
| final List<Call> copy = Lists.newArrayList(); |
| for (Call call : calls) { |
| copy.add(new Call(call)); |
| } |
| getQueue().add(new QueueParams(QueueParams.METHOD_UPDATE, copy)); |
| } |
| |
| private void processQueue() { |
| synchronized (mServiceAndQueueLock) { |
| if (mQueue != null) { |
| for (QueueParams params : mQueue) { |
| switch (params.mMethod) { |
| case QueueParams.METHOD_INCOMING: |
| processIncoming((Call) params.mArg); |
| break; |
| case QueueParams.METHOD_UPDATE: |
| processUpdate((List<Call>) params.mArg); |
| break; |
| case QueueParams.METHOD_DISCONNECT: |
| processDisconnect((Call) params.mArg); |
| break; |
| default: |
| throw new IllegalArgumentException("Method type " + params.mMethod + |
| " not recognized."); |
| } |
| } |
| mQueue.clear(); |
| mQueue = null; |
| } |
| } |
| } |
| |
| /** |
| * Holds method parameters. |
| */ |
| private static class QueueParams { |
| private static final int METHOD_INCOMING = 1; |
| private static final int METHOD_UPDATE = 2; |
| private static final int METHOD_DISCONNECT = 3; |
| |
| private final int mMethod; |
| private final Object mArg; |
| |
| private QueueParams(int method, Object arg) { |
| mMethod = method; |
| this.mArg = arg; |
| } |
| } |
| } |