• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 import javax.inject.Inject;
40 import javax.inject.Singleton;
41 
42 import dagger.hilt.android.qualifiers.ApplicationContext;
43 
44 @Singleton
45 class ProjectionCallHandler implements InCallServiceImpl.ActiveCallListChangedCallback,
46         CarProjectionManager.ProjectionStatusListener {
47     private static final String TAG = "CD.ProjectionCallHandler";
48 
49     @VisibleForTesting static final String HFP_CLIENT_SCHEME = "hfpc";
50     @VisibleForTesting static final String PROJECTION_STATUS_EXTRA_HANDLES_PHONE_UI =
51             "android.car.projection.HANDLES_PHONE_UI";
52     @VisibleForTesting static final String PROJECTION_STATUS_EXTRA_DEVICE_STATE =
53             "android.car.projection.DEVICE_STATE";
54 
55     private final Context mContext;
56     private final TelecomManager mTelecomManager;
57     private final CarProjectionManagerProvider mCarProjectionManagerProvider;
58     private Car mCar;
59     private CarProjectionManager mCarProjectionManager;
60 
61     private int mProjectionState = ProjectionStatus.PROJECTION_STATE_INACTIVE;
62     private List<ProjectionStatus> mProjectionDetails = Collections.emptyList();
63 
64     @Inject
ProjectionCallHandler(@pplicationContext Context context, TelecomManager telecomManager)65     ProjectionCallHandler(@ApplicationContext Context context, TelecomManager telecomManager) {
66         this(context, telecomManager,
67                 car -> (CarProjectionManager) car.getCarManager(Car.PROJECTION_SERVICE));
68     }
69 
70     @VisibleForTesting
ProjectionCallHandler(Context context, TelecomManager telecomManager, CarProjectionManagerProvider projectionManagerProvider)71     ProjectionCallHandler(Context context, TelecomManager telecomManager,
72             CarProjectionManagerProvider projectionManagerProvider) {
73         mContext = context;
74         mTelecomManager = telecomManager;
75         mCarProjectionManagerProvider = projectionManagerProvider;
76     }
77 
start()78     void start() {
79         if (mCar == null) {
80             mCar = Car.createCar(mContext);
81         }
82         if (mCarProjectionManager == null) {
83             mCarProjectionManager = mCarProjectionManagerProvider.getCarProjectionManager(mCar);
84             mCarProjectionManager.registerProjectionStatusListener(this);
85         }
86     }
87 
stop()88     void stop() {
89         if (mCarProjectionManager != null) {
90             mCarProjectionManager.unregisterProjectionStatusListener(this);
91             mCarProjectionManager = null;
92         }
93         if (mCar != null) {
94             mCar.disconnect();
95             mCar = null;
96         }
97     }
98 
99     @Override
onProjectionStatusChanged( int state, String packageName, List<ProjectionStatus> details)100     public void onProjectionStatusChanged(
101             int state, String packageName, List<ProjectionStatus> details) {
102         mProjectionState = state;
103         mProjectionDetails = details;
104     }
105 
106     @Override
onTelecomCallAdded(Call telecomCall)107     public boolean onTelecomCallAdded(Call telecomCall) {
108         L.d(TAG, "onTelecomCallAdded(%s)", telecomCall);
109         if (mProjectionState != ProjectionStatus.PROJECTION_STATE_ACTIVE_FOREGROUND) {
110             // Nothing's actively projecting in the foreground, so no need to even check anything
111             // else.
112             return false;
113         }
114 
115         if (mTelecomManager.isInEmergencyCall()) {
116             L.i(TAG, "Not suppressing UI for projection - in emergency call");
117             return false;
118         }
119 
120         String bluetoothAddress = getHfpBluetoothAddressForCall(telecomCall);
121         if (bluetoothAddress == null) {
122             // Not an HFP call, so don't suppress UI.
123             return false;
124         }
125 
126         return shouldSuppressCallUiForBluetoothDevice(bluetoothAddress);
127     }
128 
129     @Override
onTelecomCallRemoved(Call telecomCall)130     public boolean onTelecomCallRemoved(Call telecomCall) {
131         return false;
132     }
133 
134     @Nullable
getHfpBluetoothAddressForCall(Call call)135     private String getHfpBluetoothAddressForCall(Call call) {
136         Call.Details details = call.getDetails();
137         if (details == null) {
138             return null;
139         }
140 
141         PhoneAccountHandle accountHandle = details.getAccountHandle();
142         PhoneAccount account = mTelecomManager.getPhoneAccount(accountHandle);
143         if (account == null) {
144             return null;
145         }
146 
147         Uri address = account.getAddress();
148         if (address == null || !HFP_CLIENT_SCHEME.equals(address.getScheme())) {
149             return null;
150         }
151 
152         return address.getSchemeSpecificPart();
153     }
154 
shouldSuppressCallUiForBluetoothDevice(String bluetoothAddress)155     private boolean shouldSuppressCallUiForBluetoothDevice(String bluetoothAddress) {
156         L.d(TAG, "shouldSuppressCallUiFor(%s)", bluetoothAddress);
157         for (ProjectionStatus status : mProjectionDetails) {
158             if (!status.isActive()) {
159                 // Don't suppress UI for packages that aren't actively projecting.
160                 L.d(TAG, "skip non-projecting package %s", status.getPackageName());
161                 continue;
162             }
163 
164             Bundle appExtras = status.getExtras();
165             if (!appExtras.getBoolean(PROJECTION_STATUS_EXTRA_HANDLES_PHONE_UI, true)) {
166                 // Don't suppress UI for apps that say they don't handle phone UI.
167                 continue;
168             }
169 
170             for (ProjectionStatus.MobileDevice device : status.getConnectedMobileDevices()) {
171                 if (!device.isProjecting()) {
172                     // Don't suppress UI for devices that aren't foreground.
173                     L.d(TAG, "skip non-projecting device %s", device.getName());
174                     continue;
175                 }
176 
177                 Bundle extras = device.getExtras();
178                 if (extras.getInt(PROJECTION_STATUS_EXTRA_DEVICE_STATE,
179                         ProjectionStatus.PROJECTION_STATE_ACTIVE_FOREGROUND)
180                         != ProjectionStatus.PROJECTION_STATE_ACTIVE_FOREGROUND) {
181                     L.d(TAG, "skip device %s - not foreground", device.getName());
182                     continue;
183                 }
184 
185                 Parcelable projectingBluetoothDevice =
186                         extras.getParcelable(BluetoothDevice.EXTRA_DEVICE);
187 
188                 L.d(TAG, "Device %s has BT device %s", device.getName(), projectingBluetoothDevice);
189 
190                 if (projectingBluetoothDevice == null) {
191                     L.i(TAG, "Suppressing in-call UI - device %s is projecting, and does not "
192                             + "specify a Bluetooth address", device);
193                     return true;
194                 } else if (!(projectingBluetoothDevice instanceof BluetoothDevice)) {
195                     L.e(TAG, "Device %s has bad EXTRA_DEVICE value %s - treating as unspecified",
196                             device, projectingBluetoothDevice);
197                     return true;
198                 } else if (bluetoothAddress.equals(
199                         ((BluetoothDevice) projectingBluetoothDevice).getAddress())) {
200                     L.i(TAG, "Suppressing in-call UI - device %s is projecting, and call is coming "
201                             + "from device's Bluetooth address %s", device, bluetoothAddress);
202                     return true;
203                 }
204             }
205         }
206 
207         // No projecting apps want to suppress this device, so let it through.
208         return false;
209     }
210 
211     interface CarProjectionManagerProvider {
getCarProjectionManager(Car car)212         CarProjectionManager getCarProjectionManager(Car car);
213     }
214 }
215