blob: fc8f977396722c56efa96acb3c9797282588f636 [file] [log] [blame]
/*
* 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<Integer> 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<UiCall> 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<UiCall> 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<UiCall> 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.
* <p/>
* It uses the next {@code count} rows in the cursor to extract the types.
* <p/>
* 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<UiCall> getCallComparator() {
return new Comparator<UiCall>() {
@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;
}
};
}
}