• 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.annotation.Nullable;
19 import android.bluetooth.BluetoothDevice;
20 import android.car.Car;
21 import android.car.CarProjectionManager;
22 import android.car.projection.ProjectionStatus;
23 import android.content.Context;
24 import android.net.Uri;
25 import android.os.Bundle;
26 import android.os.Parcelable;
27 import android.telecom.Call;
28 import android.telecom.PhoneAccount;
29 import android.telecom.PhoneAccountHandle;
30 import android.telecom.TelecomManager;
31 
32 import com.android.car.dialer.log.L;
33 import com.android.internal.annotations.VisibleForTesting;
34 
35 import java.util.Collections;
36 import java.util.List;
37 
38 class ProjectionCallHandler implements InCallServiceImpl.ActiveCallListChangedCallback,
39         CarProjectionManager.ProjectionStatusListener {
40     private static final String TAG = "CD.ProjectionCallHandler";
41 
42     @VisibleForTesting static final String HFP_CLIENT_SCHEME = "hfpc";
43     @VisibleForTesting static final String PROJECTION_STATUS_EXTRA_HANDLES_PHONE_UI =
44             "android.car.projection.HANDLES_PHONE_UI";
45     @VisibleForTesting static final String PROJECTION_STATUS_EXTRA_DEVICE_STATE =
46             "android.car.projection.DEVICE_STATE";
47 
48     private final CarProjectionManager mCarProjectionManager;
49     private final TelecomManager mTelecomManager;
50 
51     private int mProjectionState = ProjectionStatus.PROJECTION_STATE_INACTIVE;
52     private List<ProjectionStatus> mProjectionDetails = Collections.emptyList();
53 
ProjectionCallHandler(Context context)54     ProjectionCallHandler(Context context) {
55         this(context.getSystemService(TelecomManager.class),
56                 (CarProjectionManager)
57                         Car.createCar(context).getCarManager(Car.PROJECTION_SERVICE));
58     }
59 
60     @VisibleForTesting
ProjectionCallHandler(TelecomManager telecomManager, CarProjectionManager projectionManager)61     ProjectionCallHandler(TelecomManager telecomManager, CarProjectionManager projectionManager) {
62         mTelecomManager = telecomManager;
63         mCarProjectionManager = projectionManager;
64     }
65 
start()66     void start() {
67         mCarProjectionManager.registerProjectionStatusListener(this);
68     }
69 
stop()70     void stop() {
71         mCarProjectionManager.unregisterProjectionStatusListener(this);
72     }
73 
74     @Override
onProjectionStatusChanged( int state, String packageName, List<ProjectionStatus> details)75     public void onProjectionStatusChanged(
76             int state, String packageName, List<ProjectionStatus> details) {
77         mProjectionState = state;
78         mProjectionDetails = details;
79     }
80 
81     @Override
onTelecomCallAdded(Call telecomCall)82     public boolean onTelecomCallAdded(Call telecomCall) {
83         L.d(TAG, "onTelecomCallAdded(%s)", telecomCall);
84         if (mProjectionState != ProjectionStatus.PROJECTION_STATE_ACTIVE_BACKGROUND
85                 && mProjectionState != ProjectionStatus.PROJECTION_STATE_ACTIVE_FOREGROUND) {
86             // Nothing's actively projecting, so no need to even check anything else.
87             return false;
88         }
89 
90         if (mTelecomManager.isInEmergencyCall()) {
91             L.i(TAG, "Not suppressing UI for projection - in emergency call");
92             return false;
93         }
94 
95         String bluetoothAddress = getHfpBluetoothAddressForCall(telecomCall);
96         if (bluetoothAddress == null) {
97             // Not an HFP call, so don't suppress UI.
98             return false;
99         }
100 
101         return shouldSuppressCallUiForBluetoothDevice(bluetoothAddress);
102     }
103 
104     @Override
onTelecomCallRemoved(Call telecomCall)105     public boolean onTelecomCallRemoved(Call telecomCall) {
106         return false;
107     }
108 
109     @Nullable
getHfpBluetoothAddressForCall(Call call)110     private String getHfpBluetoothAddressForCall(Call call) {
111         Call.Details details = call.getDetails();
112         if (details == null) {
113             return null;
114         }
115 
116         PhoneAccountHandle accountHandle = details.getAccountHandle();
117         PhoneAccount account = mTelecomManager.getPhoneAccount(accountHandle);
118         if (account == null) {
119             return null;
120         }
121 
122         Uri address = account.getAddress();
123         if (address == null || !HFP_CLIENT_SCHEME.equals(address.getScheme())) {
124             return null;
125         }
126 
127         return address.getSchemeSpecificPart();
128     }
129 
shouldSuppressCallUiForBluetoothDevice(String bluetoothAddress)130     private boolean shouldSuppressCallUiForBluetoothDevice(String bluetoothAddress) {
131         L.d(TAG, "shouldSuppressCallUiFor(%s)", bluetoothAddress);
132         for (ProjectionStatus status : mProjectionDetails) {
133             if (!status.isActive()) {
134                 // Don't suppress UI for packages that aren't actively projecting.
135                 L.d(TAG, "skip non-projecting package %s", status.getPackageName());
136                 continue;
137             }
138 
139             Bundle appExtras = status.getExtras();
140             if (!appExtras.getBoolean(PROJECTION_STATUS_EXTRA_HANDLES_PHONE_UI, true)) {
141                 // Don't suppress UI for apps that say they don't handle phone UI.
142                 continue;
143             }
144 
145             for (ProjectionStatus.MobileDevice device : status.getConnectedMobileDevices()) {
146                 if (!device.isProjecting()) {
147                     // Don't suppress UI for devices that aren't foreground.
148                     L.d(TAG, "skip non-projecting device %s", device.getName());
149                     continue;
150                 }
151 
152                 Bundle extras = device.getExtras();
153                 if (extras.getInt(PROJECTION_STATUS_EXTRA_DEVICE_STATE,
154                         ProjectionStatus.PROJECTION_STATE_ACTIVE_FOREGROUND)
155                         != ProjectionStatus.PROJECTION_STATE_ACTIVE_FOREGROUND) {
156                     L.d(TAG, "skip device %s - not foreground", device.getName());
157                     continue;
158                 }
159 
160                 Parcelable projectingBluetoothDevice =
161                         extras.getParcelable(BluetoothDevice.EXTRA_DEVICE);
162 
163                 L.d(TAG, "Device %s has BT device %s", device.getName(), projectingBluetoothDevice);
164 
165                 if (projectingBluetoothDevice == null) {
166                     L.i(TAG, "Suppressing in-call UI - device %s is projecting, and does not "
167                             + "specify a Bluetooth address", device);
168                     return true;
169                 } else if (!(projectingBluetoothDevice instanceof BluetoothDevice)) {
170                     L.e(TAG, "Device %s has bad EXTRA_DEVICE value %s - treating as unspecified",
171                             device, projectingBluetoothDevice);
172                     return true;
173                 } else if (bluetoothAddress.equals(
174                         ((BluetoothDevice) projectingBluetoothDevice).getAddress())) {
175                     L.i(TAG, "Suppressing in-call UI - device %s is projecting, and call is coming "
176                             + "from device's Bluetooth address %s", device, bluetoothAddress);
177                     return true;
178                 }
179             }
180         }
181 
182         // No projecting apps want to suppress this device, so let it through.
183         return false;
184     }
185 }
186