|  | /* | 
|  | * 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; | 
|  | } | 
|  | } | 
|  | } |