1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.car.dialer.telecom;
17 
18 import android.bluetooth.BluetoothDevice;
19 import android.car.Car;
20 import android.car.CarProjectionManager;
21 import android.car.projection.ProjectionStatus;
22 import android.content.Context;
23 import android.net.Uri;
24 import android.os.Bundle;
25 import android.os.Parcelable;
26 import android.telecom.Call;
27 import android.telecom.PhoneAccount;
28 import android.telecom.PhoneAccountHandle;
29 import android.telecom.TelecomManager;
30 
31 import androidx.annotation.Nullable;
32 import androidx.annotation.VisibleForTesting;
33 
34 import com.android.car.dialer.log.L;
35 
36 import java.util.Collections;
37 import java.util.List;
38 
39 class ProjectionCallHandler implements InCallServiceImpl.ActiveCallListChangedCallback,
40         CarProjectionManager.ProjectionStatusListener {
41     private static final String TAG = "CD.ProjectionCallHandler";
42 
43     @VisibleForTesting static final String HFP_CLIENT_SCHEME = "hfpc";
44     @VisibleForTesting static final String PROJECTION_STATUS_EXTRA_HANDLES_PHONE_UI =
45             "android.car.projection.HANDLES_PHONE_UI";
46     @VisibleForTesting static final String PROJECTION_STATUS_EXTRA_DEVICE_STATE =
47             "android.car.projection.DEVICE_STATE";
48 
49     private final Context mContext;
50     private final TelecomManager mTelecomManager;
51     private final CarProjectionManagerProvider mCarProjectionManagerProvider;
52     private Car mCar;
53     private CarProjectionManager mCarProjectionManager;
54 
55     private int mProjectionState = ProjectionStatus.PROJECTION_STATE_INACTIVE;
56     private List<ProjectionStatus> mProjectionDetails = Collections.emptyList();
57 
ProjectionCallHandler(Context context)58     ProjectionCallHandler(Context context) {
59         this(context, context.getSystemService(TelecomManager.class),
60                 car -> (CarProjectionManager) car.getCarManager(Car.PROJECTION_SERVICE));
61     }
62 
63     @VisibleForTesting
ProjectionCallHandler(Context context, TelecomManager telecomManager, CarProjectionManagerProvider projectionManagerProvider)64     ProjectionCallHandler(Context context, TelecomManager telecomManager,
65             CarProjectionManagerProvider projectionManagerProvider) {
66         mContext = context;
67         mTelecomManager = telecomManager;
68         mCarProjectionManagerProvider = projectionManagerProvider;
69     }
70 
start()71     void start() {
72         if (mCar == null) {
73             mCar = Car.createCar(mContext);
74         }
75         if (mCarProjectionManager == null) {
76             mCarProjectionManager = mCarProjectionManagerProvider.getCarProjectionManager(mCar);
77             mCarProjectionManager.registerProjectionStatusListener(this);
78         }
79     }
80 
stop()81     void stop() {
82         if (mCarProjectionManager != null) {
83             mCarProjectionManager.unregisterProjectionStatusListener(this);
84             mCarProjectionManager = null;
85         }
86         if (mCar != null) {
87             mCar.disconnect();
88             mCar = null;
89         }
90     }
91 
92     @Override
onProjectionStatusChanged( int state, String packageName, List<ProjectionStatus> details)93     public void onProjectionStatusChanged(
94             int state, String packageName, List<ProjectionStatus> details) {
95         mProjectionState = state;
96         mProjectionDetails = details;
97     }
98 
99     @Override
onTelecomCallAdded(Call telecomCall)100     public boolean onTelecomCallAdded(Call telecomCall) {
101         L.d(TAG, "onTelecomCallAdded(%s)", telecomCall);
102         if (mProjectionState != ProjectionStatus.PROJECTION_STATE_ACTIVE_BACKGROUND
103                 && mProjectionState != ProjectionStatus.PROJECTION_STATE_ACTIVE_FOREGROUND) {
104             // Nothing's actively projecting, so no need to even check anything else.
105             return false;
106         }
107 
108         if (mTelecomManager.isInEmergencyCall()) {
109             L.i(TAG, "Not suppressing UI for projection - in emergency call");
110             return false;
111         }
112 
113         String bluetoothAddress = getHfpBluetoothAddressForCall(telecomCall);
114         if (bluetoothAddress == null) {
115             // Not an HFP call, so don't suppress UI.
116             return false;
117         }
118 
119         return shouldSuppressCallUiForBluetoothDevice(bluetoothAddress);
120     }
121 
122     @Override
onTelecomCallRemoved(Call telecomCall)123     public boolean onTelecomCallRemoved(Call telecomCall) {
124         return false;
125     }
126 
127     @Nullable
getHfpBluetoothAddressForCall(Call call)128     private String getHfpBluetoothAddressForCall(Call call) {
129         Call.Details details = call.getDetails();
130         if (details == null) {
131             return null;
132         }
133 
134         PhoneAccountHandle accountHandle = details.getAccountHandle();
135         PhoneAccount account = mTelecomManager.getPhoneAccount(accountHandle);
136         if (account == null) {
137             return null;
138         }
139 
140         Uri address = account.getAddress();
141         if (address == null || !HFP_CLIENT_SCHEME.equals(address.getScheme())) {
142             return null;
143         }
144 
145         return address.getSchemeSpecificPart();
146     }
147 
shouldSuppressCallUiForBluetoothDevice(String bluetoothAddress)148     private boolean shouldSuppressCallUiForBluetoothDevice(String bluetoothAddress) {
149         L.d(TAG, "shouldSuppressCallUiFor(%s)", bluetoothAddress);
150         for (ProjectionStatus status : mProjectionDetails) {
151             if (!status.isActive()) {
152                 // Don't suppress UI for packages that aren't actively projecting.
153                 L.d(TAG, "skip non-projecting package %s", status.getPackageName());
154                 continue;
155             }
156 
157             Bundle appExtras = status.getExtras();
158             if (!appExtras.getBoolean(PROJECTION_STATUS_EXTRA_HANDLES_PHONE_UI, true)) {
159                 // Don't suppress UI for apps that say they don't handle phone UI.
160                 continue;
161             }
162 
163             for (ProjectionStatus.MobileDevice device : status.getConnectedMobileDevices()) {
164                 if (!device.isProjecting()) {
165                     // Don't suppress UI for devices that aren't foreground.
166                     L.d(TAG, "skip non-projecting device %s", device.getName());
167                     continue;
168                 }
169 
170                 Bundle extras = device.getExtras();
171                 if (extras.getInt(PROJECTION_STATUS_EXTRA_DEVICE_STATE,
172                         ProjectionStatus.PROJECTION_STATE_ACTIVE_FOREGROUND)
173                         != ProjectionStatus.PROJECTION_STATE_ACTIVE_FOREGROUND) {
174                     L.d(TAG, "skip device %s - not foreground", device.getName());
175                     continue;
176                 }
177 
178                 Parcelable projectingBluetoothDevice =
179                         extras.getParcelable(BluetoothDevice.EXTRA_DEVICE);
180 
181                 L.d(TAG, "Device %s has BT device %s", device.getName(), projectingBluetoothDevice);
182 
183                 if (projectingBluetoothDevice == null) {
184                     L.i(TAG, "Suppressing in-call UI - device %s is projecting, and does not "
185                             + "specify a Bluetooth address", device);
186                     return true;
187                 } else if (!(projectingBluetoothDevice instanceof BluetoothDevice)) {
188                     L.e(TAG, "Device %s has bad EXTRA_DEVICE value %s - treating as unspecified",
189                             device, projectingBluetoothDevice);
190                     return true;
191                 } else if (bluetoothAddress.equals(
192                         ((BluetoothDevice) projectingBluetoothDevice).getAddress())) {
193                     L.i(TAG, "Suppressing in-call UI - device %s is projecting, and call is coming "
194                             + "from device's Bluetooth address %s", device, bluetoothAddress);
195                     return true;
196                 }
197             }
198         }
199 
200         // No projecting apps want to suppress this device, so let it through.
201         return false;
202     }
203 
204     interface CarProjectionManagerProvider {
getCarProjectionManager(Car car)205         CarProjectionManager getCarProjectionManager(Car car);
206     }
207 }
208