/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.car.dialer.telecom; import android.content.Context; import android.database.Cursor; import android.provider.CallLog; import android.telecom.Call; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Log; import android.view.KeyEvent; import com.android.car.dialer.ClassFactory; import com.android.car.dialer.R; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Comparator; import java.util.List; /** * The entry point for all interactions between UI and telecom. */ public abstract class UiCallManager { private static String TAG = "Em.TelecomMgr"; private static Object sInstanceLock = new Object(); private static UiCallManager sInstance; private long mLastPlacedCallTimeMs = 0; // Rate limit how often you can place outgoing calls. private static final long MIN_TIME_BETWEEN_CALLS_MS = 3000; private static final List sCallStateRank = new ArrayList<>(); protected Context mContext; protected TelephonyManager mTelephonyManager; static { // States should be added from lowest rank to highest sCallStateRank.add(Call.STATE_DISCONNECTED); sCallStateRank.add(Call.STATE_DISCONNECTING); sCallStateRank.add(Call.STATE_NEW); sCallStateRank.add(Call.STATE_CONNECTING); sCallStateRank.add(Call.STATE_SELECT_PHONE_ACCOUNT); sCallStateRank.add(Call.STATE_HOLDING); sCallStateRank.add(Call.STATE_ACTIVE); sCallStateRank.add(Call.STATE_DIALING); sCallStateRank.add(Call.STATE_RINGING); } public static UiCallManager getInstance(Context context) { synchronized (sInstanceLock) { if (sInstance == null) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Creating an instance of CarTelecomManager"); } sInstance = ClassFactory.getFactory().createCarTelecomManager(); sInstance.setUp(context.getApplicationContext()); } } return sInstance; } protected UiCallManager() {} protected void setUp(Context context) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "SetUp"); } mContext = context; mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); } public abstract void tearDown(); public abstract void addListener(CallListener listener); public abstract void removeListener(CallListener listener); protected abstract void placeCall(String number); public abstract void answerCall(UiCall call); public abstract void rejectCall(UiCall call, boolean rejectWithMessage, String textMessage); public abstract void disconnectCall(UiCall call); public abstract List getCalls(); public abstract boolean getMuted(); public abstract void setMuted(boolean muted); public abstract int getSupportedAudioRouteMask(); public abstract int getAudioRoute(); public abstract void setAudioRoute(int audioRoute); public abstract void holdCall(UiCall call); public abstract void unholdCall(UiCall call); public abstract void playDtmfTone(UiCall call, char digit); public abstract void stopDtmfTone(UiCall call); public abstract void postDialContinue(UiCall call, boolean proceed); public abstract void conference(UiCall call, UiCall otherCall); public abstract void splitFromConference(UiCall call); public static class CallListener { @SuppressWarnings("unused") public void dispatchPhoneKeyEvent(KeyEvent event) {} @SuppressWarnings("unused") public void onAudioStateChanged(boolean isMuted, int route, int supportedRouteMask) {} @SuppressWarnings("unused") public void onCallAdded(UiCall call) {} @SuppressWarnings("unused") public void onStateChanged(UiCall call, int state) {} @SuppressWarnings("unused") public void onCallUpdated(UiCall call) {} @SuppressWarnings("unused") public void onCallRemoved(UiCall call) {} } /** Returns a first call that matches at least one provided call state */ public UiCall getCallWithState(int... callStates) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "getCallWithState: " + callStates); } for (UiCall call : getCalls()) { for (int callState : callStates) { if (call.getState() == callState) { return call; } } } return null; } public UiCall getPrimaryCall() { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "getPrimaryCall"); } List calls = getCalls(); if (calls.isEmpty()) { return null; } Collections.sort(calls, getCallComparator()); UiCall uiCall = calls.get(0); if (uiCall.hasParent()) { return null; } return uiCall; } public UiCall getSecondaryCall() { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "getSecondaryCall"); } List calls = getCalls(); if (calls.size() < 2) { return null; } Collections.sort(calls, getCallComparator()); UiCall uiCall = calls.get(1); if (uiCall.hasParent()) { return null; } return uiCall; } public static final int CAN_PLACE_CALL_RESULT_OK = 0; public static final int CAN_PLACE_CALL_RESULT_NETWORK_UNAVAILABLE = 1; public static final int CAN_PLACE_CALL_RESULT_HFP_UNAVAILABLE = 2; public static final int CAN_PLACE_CALL_RESULT_AIRPLANE_MODE = 3; public int getCanPlaceCallStatus(String number, boolean bluetoothRequired) { // TODO(b/26191392): figure out the logic for projected and embedded modes return CAN_PLACE_CALL_RESULT_OK; } public String getFailToPlaceCallMessage(int canPlaceCallResult) { switch (canPlaceCallResult) { case CAN_PLACE_CALL_RESULT_OK: return ""; case CAN_PLACE_CALL_RESULT_HFP_UNAVAILABLE: return mContext.getString(R.string.error_no_hfp); case CAN_PLACE_CALL_RESULT_AIRPLANE_MODE: return mContext.getString(R.string.error_airplane_mode); case CAN_PLACE_CALL_RESULT_NETWORK_UNAVAILABLE: default: return mContext.getString(R.string.error_network_not_available); } } /** Places call only if there's no outgoing call right now */ public void safePlaceCall(String number, boolean bluetoothRequired) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "safePlaceCall: " + number); } int placeCallStatus = getCanPlaceCallStatus(number, bluetoothRequired); if (placeCallStatus != CAN_PLACE_CALL_RESULT_OK) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Unable to place a call: " + placeCallStatus); } return; } UiCall outgoingCall = getCallWithState( Call.STATE_CONNECTING, Call.STATE_NEW, Call.STATE_DIALING); if (outgoingCall == null) { long now = Calendar.getInstance().getTimeInMillis(); if (now - mLastPlacedCallTimeMs > MIN_TIME_BETWEEN_CALLS_MS) { placeCall(number); mLastPlacedCallTimeMs = now; } else { if (Log.isLoggable(TAG, Log.INFO)) { Log.i(TAG, "You have to wait " + MIN_TIME_BETWEEN_CALLS_MS + "ms between making calls"); } } } } public void callVoicemail() { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "callVoicemail"); } String voicemailNumber = TelecomUtils.getVoicemailNumber(mContext); if (TextUtils.isEmpty(voicemailNumber)) { Log.w(TAG, "Unable to get voicemail number."); return; } safePlaceCall(voicemailNumber, false); } /** * Returns the call types for the given number of items in the cursor. *

* It uses the next {@code count} rows in the cursor to extract the types. *

* Its position in the cursor is unchanged by this function. */ public int[] getCallTypes(Cursor cursor, int count) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "getCallTypes: cursor: " + cursor + ", count: " + count); } int position = cursor.getPosition(); int[] callTypes = new int[count]; String voicemailNumber = mTelephonyManager.getVoiceMailNumber(); int column; for (int index = 0; index < count; ++index) { column = cursor.getColumnIndex(CallLog.Calls.NUMBER); String phoneNumber = cursor.getString(column); if (phoneNumber != null && phoneNumber.equals(voicemailNumber)) { callTypes[index] = PhoneLoader.VOICEMAIL_TYPE; } else { column = cursor.getColumnIndex(CallLog.Calls.TYPE); callTypes[index] = cursor.getInt(column); } cursor.moveToNext(); } cursor.moveToPosition(position); return callTypes; } private static Comparator getCallComparator() { return new Comparator() { @Override public int compare(UiCall call, UiCall otherCall) { if (call.hasParent() && !otherCall.hasParent()) { return 1; } else if (!call.hasParent() && otherCall.hasParent()) { return -1; } int carCallRank = sCallStateRank.indexOf(call.getState()); int otherCarCallRank = sCallStateRank.indexOf(otherCall.getState()); return otherCarCallRank - carCallRank; } }; } }