1 /*
2  * Copyright 2014, 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 
17 package com.android.server.telecom;
18 
19 import android.content.Context;
20 import android.os.UserHandle;
21 import android.telecom.DisconnectCause;
22 import android.telecom.ParcelableConnection;
23 import android.telecom.PhoneAccount;
24 import android.telecom.PhoneAccountHandle;
25 
26 // TODO: Needed for move to system service: import com.android.internal.R;
27 
28 import com.android.internal.annotations.VisibleForTesting;
29 
30 import java.util.ArrayList;
31 import java.util.Collection;
32 import java.util.HashSet;
33 import java.util.Iterator;
34 import java.util.List;
35 import java.util.Objects;
36 
37 /**
38  * This class creates connections to place new outgoing calls or to attach to an existing incoming
39  * call. In either case, this class cycles through a set of connection services until:
40  *   - a connection service returns a newly created connection in which case the call is displayed
41  *     to the user
42  *   - a connection service cancels the process, in which case the call is aborted
43  */
44 @VisibleForTesting
45 public class CreateConnectionProcessor implements CreateConnectionResponse {
46 
47     // Describes information required to attempt to make a phone call
48     private static class CallAttemptRecord {
49         // The PhoneAccount describing the target connection service which we will
50         // contact in order to process an attempt
51         public final PhoneAccountHandle connectionManagerPhoneAccount;
52         // The PhoneAccount which we will tell the target connection service to use
53         // for attempting to make the actual phone call
54         public final PhoneAccountHandle targetPhoneAccount;
55 
CallAttemptRecord( PhoneAccountHandle connectionManagerPhoneAccount, PhoneAccountHandle targetPhoneAccount)56         public CallAttemptRecord(
57                 PhoneAccountHandle connectionManagerPhoneAccount,
58                 PhoneAccountHandle targetPhoneAccount) {
59             this.connectionManagerPhoneAccount = connectionManagerPhoneAccount;
60             this.targetPhoneAccount = targetPhoneAccount;
61         }
62 
63         @Override
toString()64         public String toString() {
65             return "CallAttemptRecord("
66                     + Objects.toString(connectionManagerPhoneAccount) + ","
67                     + Objects.toString(targetPhoneAccount) + ")";
68         }
69 
70         /**
71          * Determines if this instance of {@code CallAttemptRecord} has the same underlying
72          * {@code PhoneAccountHandle}s as another instance.
73          *
74          * @param obj The other instance to compare against.
75          * @return {@code True} if the {@code CallAttemptRecord}s are equal.
76          */
77         @Override
equals(Object obj)78         public boolean equals(Object obj) {
79             if (obj instanceof CallAttemptRecord) {
80                 CallAttemptRecord other = (CallAttemptRecord) obj;
81                 return Objects.equals(connectionManagerPhoneAccount,
82                         other.connectionManagerPhoneAccount) &&
83                         Objects.equals(targetPhoneAccount, other.targetPhoneAccount);
84             }
85             return false;
86         }
87     }
88 
89     private final Call mCall;
90     private final ConnectionServiceRepository mRepository;
91     private List<CallAttemptRecord> mAttemptRecords;
92     private Iterator<CallAttemptRecord> mAttemptRecordIterator;
93     private CreateConnectionResponse mCallResponse;
94     private DisconnectCause mLastErrorDisconnectCause;
95     private final PhoneAccountRegistrar mPhoneAccountRegistrar;
96     private final Context mContext;
97     private CreateConnectionTimeout mTimeout;
98     private ConnectionServiceWrapper mService;
99 
100     @VisibleForTesting
CreateConnectionProcessor( Call call, ConnectionServiceRepository repository, CreateConnectionResponse response, PhoneAccountRegistrar phoneAccountRegistrar, Context context)101     public CreateConnectionProcessor(
102             Call call, ConnectionServiceRepository repository, CreateConnectionResponse response,
103             PhoneAccountRegistrar phoneAccountRegistrar, Context context) {
104         Log.v(this, "CreateConnectionProcessor created for Call = %s", call);
105         mCall = call;
106         mRepository = repository;
107         mCallResponse = response;
108         mPhoneAccountRegistrar = phoneAccountRegistrar;
109         mContext = context;
110     }
111 
isProcessingComplete()112     boolean isProcessingComplete() {
113         return mCallResponse == null;
114     }
115 
isCallTimedOut()116     boolean isCallTimedOut() {
117         return mTimeout != null && mTimeout.isCallTimedOut();
118     }
119 
120     @VisibleForTesting
process()121     public void process() {
122         Log.v(this, "process");
123         clearTimeout();
124         mAttemptRecords = new ArrayList<>();
125         if (mCall.getTargetPhoneAccount() != null) {
126             mAttemptRecords.add(new CallAttemptRecord(
127                     mCall.getTargetPhoneAccount(), mCall.getTargetPhoneAccount()));
128         }
129         adjustAttemptsForConnectionManager();
130         adjustAttemptsForEmergency();
131         mAttemptRecordIterator = mAttemptRecords.iterator();
132         attemptNextPhoneAccount();
133     }
134 
hasMorePhoneAccounts()135     boolean hasMorePhoneAccounts() {
136         return mAttemptRecordIterator.hasNext();
137     }
138 
continueProcessingIfPossible(CreateConnectionResponse response, DisconnectCause disconnectCause)139     void continueProcessingIfPossible(CreateConnectionResponse response,
140             DisconnectCause disconnectCause) {
141         Log.v(this, "continueProcessingIfPossible");
142         mCallResponse = response;
143         mLastErrorDisconnectCause = disconnectCause;
144         attemptNextPhoneAccount();
145     }
146 
abort()147     void abort() {
148         Log.v(this, "abort");
149 
150         // Clear the response first to prevent attemptNextConnectionService from attempting any
151         // more services.
152         CreateConnectionResponse response = mCallResponse;
153         mCallResponse = null;
154         clearTimeout();
155 
156         ConnectionServiceWrapper service = mCall.getConnectionService();
157         if (service != null) {
158             service.abort(mCall);
159             mCall.clearConnectionService();
160         }
161         if (response != null) {
162             response.handleCreateConnectionFailure(new DisconnectCause(DisconnectCause.LOCAL));
163         }
164     }
165 
attemptNextPhoneAccount()166     private void attemptNextPhoneAccount() {
167         Log.v(this, "attemptNextPhoneAccount");
168         CallAttemptRecord attempt = null;
169         if (mAttemptRecordIterator.hasNext()) {
170             attempt = mAttemptRecordIterator.next();
171 
172             if (!mPhoneAccountRegistrar.phoneAccountRequiresBindPermission(
173                     attempt.connectionManagerPhoneAccount)) {
174                 Log.w(this,
175                         "Connection mgr does not have BIND_TELECOM_CONNECTION_SERVICE for "
176                                 + "attempt: %s", attempt);
177                 attemptNextPhoneAccount();
178                 return;
179             }
180 
181             // If the target PhoneAccount differs from the ConnectionManager phone acount, ensure it
182             // also requires the BIND_TELECOM_CONNECTION_SERVICE permission.
183             if (!attempt.connectionManagerPhoneAccount.equals(attempt.targetPhoneAccount) &&
184                     !mPhoneAccountRegistrar.phoneAccountRequiresBindPermission(
185                             attempt.targetPhoneAccount)) {
186                 Log.w(this,
187                         "Target PhoneAccount does not have BIND_TELECOM_CONNECTION_SERVICE for "
188                                 + "attempt: %s", attempt);
189                 attemptNextPhoneAccount();
190                 return;
191             }
192         }
193 
194         if (mCallResponse != null && attempt != null) {
195             Log.i(this, "Trying attempt %s", attempt);
196             PhoneAccountHandle phoneAccount = attempt.connectionManagerPhoneAccount;
197             mService = mRepository.getService(phoneAccount.getComponentName(),
198                     phoneAccount.getUserHandle());
199             if (mService == null) {
200                 Log.i(this, "Found no connection service for attempt %s", attempt);
201                 attemptNextPhoneAccount();
202             } else {
203                 mCall.setConnectionManagerPhoneAccount(attempt.connectionManagerPhoneAccount);
204                 mCall.setTargetPhoneAccount(attempt.targetPhoneAccount);
205                 mCall.setConnectionService(mService);
206                 setTimeoutIfNeeded(mService, attempt);
207 
208                 mService.createConnection(mCall, this);
209             }
210         } else {
211             Log.v(this, "attemptNextPhoneAccount, no more accounts, failing");
212             DisconnectCause disconnectCause = mLastErrorDisconnectCause != null ?
213                     mLastErrorDisconnectCause : new DisconnectCause(DisconnectCause.ERROR);
214             notifyCallConnectionFailure(disconnectCause);
215         }
216     }
217 
setTimeoutIfNeeded(ConnectionServiceWrapper service, CallAttemptRecord attempt)218     private void setTimeoutIfNeeded(ConnectionServiceWrapper service, CallAttemptRecord attempt) {
219         clearTimeout();
220 
221         CreateConnectionTimeout timeout = new CreateConnectionTimeout(
222                 mContext, mPhoneAccountRegistrar, service, mCall);
223         if (timeout.isTimeoutNeededForCall(getConnectionServices(mAttemptRecords),
224                 attempt.connectionManagerPhoneAccount)) {
225             mTimeout = timeout;
226             timeout.registerTimeout();
227         }
228     }
229 
clearTimeout()230     private void clearTimeout() {
231         if (mTimeout != null) {
232             mTimeout.unregisterTimeout();
233             mTimeout = null;
234         }
235     }
236 
shouldSetConnectionManager()237     private boolean shouldSetConnectionManager() {
238         if (mAttemptRecords.size() == 0) {
239             return false;
240         }
241 
242         if (mAttemptRecords.size() > 1) {
243             Log.d(this, "shouldSetConnectionManager, error, mAttemptRecords should not have more "
244                     + "than 1 record");
245             return false;
246         }
247 
248         PhoneAccountHandle connectionManager =
249                 mPhoneAccountRegistrar.getSimCallManagerFromCall(mCall);
250         if (connectionManager == null) {
251             return false;
252         }
253 
254         PhoneAccountHandle targetPhoneAccountHandle = mAttemptRecords.get(0).targetPhoneAccount;
255         if (Objects.equals(connectionManager, targetPhoneAccountHandle)) {
256             return false;
257         }
258 
259         // Connection managers are only allowed to manage SIM subscriptions.
260         // TODO: Should this really be checking the "calling user" test for phone account?
261         PhoneAccount targetPhoneAccount = mPhoneAccountRegistrar
262                 .getPhoneAccountUnchecked(targetPhoneAccountHandle);
263         if (targetPhoneAccount == null) {
264             Log.d(this, "shouldSetConnectionManager, phone account not found");
265             return false;
266         }
267         boolean isSimSubscription = (targetPhoneAccount.getCapabilities() &
268                 PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION) != 0;
269         if (!isSimSubscription) {
270             return false;
271         }
272 
273         return true;
274     }
275 
276     // If there exists a registered connection manager then use it.
adjustAttemptsForConnectionManager()277     private void adjustAttemptsForConnectionManager() {
278         if (shouldSetConnectionManager()) {
279             CallAttemptRecord record = new CallAttemptRecord(
280                     mPhoneAccountRegistrar.getSimCallManagerFromCall(mCall),
281                     mAttemptRecords.get(0).targetPhoneAccount);
282             Log.v(this, "setConnectionManager, changing %s -> %s", mAttemptRecords.get(0), record);
283             mAttemptRecords.add(0, record);
284         } else {
285             Log.v(this, "setConnectionManager, not changing");
286         }
287     }
288 
289     // If we are possibly attempting to call a local emergency number, ensure that the
290     // plain PSTN connection services are listed, and nothing else.
adjustAttemptsForEmergency()291     private void adjustAttemptsForEmergency() {
292         if (mCall.isEmergencyCall()) {
293             Log.i(this, "Emergency number detected");
294             mAttemptRecords.clear();
295             // Phone accounts in profile do not handle emergency call, use phone accounts in
296             // current user.
297             List<PhoneAccount> allAccounts = mPhoneAccountRegistrar
298                     .getAllPhoneAccountsOfCurrentUser();
299 
300             if (allAccounts.isEmpty()) {
301                 // If the list of phone accounts is empty at this point, it means Telephony hasn't
302                 // registered any phone accounts yet. Add a fallback emergency phone account so
303                 // that emergency calls can still go through. We create a new ArrayLists here just
304                 // in case the implementation of PhoneAccountRegistrar ever returns an unmodifiable
305                 // list.
306                 allAccounts = new ArrayList<PhoneAccount>();
307                 allAccounts.add(TelephonyUtil.getDefaultEmergencyPhoneAccount());
308             }
309 
310             // First, add SIM phone accounts which can place emergency calls.
311             for (PhoneAccount phoneAccount : allAccounts) {
312                 if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS) &&
313                         phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
314                     Log.i(this, "Will try PSTN account %s for emergency",
315                             phoneAccount.getAccountHandle());
316                     mAttemptRecords.add(
317                             new CallAttemptRecord(
318                                     phoneAccount.getAccountHandle(),
319                                     phoneAccount.getAccountHandle()));
320                 }
321             }
322 
323             // Next, add the connection manager account as a backup if it can place emergency calls.
324             PhoneAccountHandle callManagerHandle =
325                     mPhoneAccountRegistrar.getSimCallManagerOfCurrentUser();
326             if (callManagerHandle != null) {
327                 // TODO: Should this really be checking the "calling user" test for phone account?
328                 PhoneAccount callManager = mPhoneAccountRegistrar
329                         .getPhoneAccountUnchecked(callManagerHandle);
330                 if (callManager != null && callManager.hasCapabilities(
331                         PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS)) {
332                     CallAttemptRecord callAttemptRecord = new CallAttemptRecord(callManagerHandle,
333                             mPhoneAccountRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser(
334                                     mCall.getHandle().getScheme()));
335                     if (!mAttemptRecords.contains(callAttemptRecord)) {
336                         Log.i(this, "Will try Connection Manager account %s for emergency",
337                                 callManager);
338                         mAttemptRecords.add(callAttemptRecord);
339                     }
340                 }
341             }
342         }
343     }
344 
345     /** Returns all connection services used by the call attempt records. */
getConnectionServices( List<CallAttemptRecord> records)346     private static Collection<PhoneAccountHandle> getConnectionServices(
347             List<CallAttemptRecord> records) {
348         HashSet<PhoneAccountHandle> result = new HashSet<>();
349         for (CallAttemptRecord record : records) {
350             result.add(record.connectionManagerPhoneAccount);
351         }
352         return result;
353     }
354 
355 
notifyCallConnectionFailure(DisconnectCause errorDisconnectCause)356     private void notifyCallConnectionFailure(DisconnectCause errorDisconnectCause) {
357         if (mCallResponse != null) {
358             clearTimeout();
359             mCallResponse.handleCreateConnectionFailure(errorDisconnectCause);
360             mCallResponse = null;
361             mCall.clearConnectionService();
362         }
363     }
364 
365     @Override
handleCreateConnectionSuccess( CallIdMapper idMapper, ParcelableConnection connection)366     public void handleCreateConnectionSuccess(
367             CallIdMapper idMapper,
368             ParcelableConnection connection) {
369         if (mCallResponse == null) {
370             // Nobody is listening for this connection attempt any longer; ask the responsible
371             // ConnectionService to tear down any resources associated with the call
372             mService.abort(mCall);
373         } else {
374             // Success -- share the good news and remember that we are no longer interested
375             // in hearing about any more attempts
376             mCallResponse.handleCreateConnectionSuccess(idMapper, connection);
377             mCallResponse = null;
378             // If there's a timeout running then don't clear it. The timeout can be triggered
379             // after the call has successfully been created but before it has become active.
380         }
381     }
382 
shouldFailCallIfConnectionManagerFails(DisconnectCause cause)383     private boolean shouldFailCallIfConnectionManagerFails(DisconnectCause cause) {
384         // Connection Manager does not exist or does not match registered Connection Manager
385         // Since Connection manager is a proxy for SIM, fall back to SIM
386         PhoneAccountHandle handle = mCall.getConnectionManagerPhoneAccount();
387         if (handle == null || !handle.equals(mPhoneAccountRegistrar.getSimCallManagerFromCall(
388                 mCall))) {
389             return false;
390         }
391 
392         // The Call's Connection Service does not exist
393         ConnectionServiceWrapper connectionManager = mCall.getConnectionService();
394         if (connectionManager == null) {
395             return true;
396         }
397 
398         // In this case, fall back to a sim because connection manager declined
399         if (cause.getCode() == DisconnectCause.CONNECTION_MANAGER_NOT_SUPPORTED) {
400             Log.d(CreateConnectionProcessor.this, "Connection manager declined to handle the "
401                     + "call, falling back to not using a connection manager");
402             return false;
403         }
404 
405         if (!connectionManager.isServiceValid("createConnection")) {
406             Log.d(CreateConnectionProcessor.this, "Connection manager unbound while trying "
407                     + "create a connection, falling back to not using a connection manager");
408             return false;
409         }
410 
411         // Do not fall back from connection manager and simply fail call if the failure reason is
412         // other
413         Log.d(CreateConnectionProcessor.this, "Connection Manager denied call with the following " +
414                 "error: " + cause.getReason() + ". Not falling back to SIM.");
415         return true;
416     }
417 
418     @Override
handleCreateConnectionFailure(DisconnectCause errorDisconnectCause)419     public void handleCreateConnectionFailure(DisconnectCause errorDisconnectCause) {
420         // Failure of some sort; record the reasons for failure and try again if possible
421         Log.d(CreateConnectionProcessor.this, "Connection failed: (%s)", errorDisconnectCause);
422         if(shouldFailCallIfConnectionManagerFails(errorDisconnectCause)){
423             notifyCallConnectionFailure(errorDisconnectCause);
424             return;
425         }
426         mLastErrorDisconnectCause = errorDisconnectCause;
427         attemptNextPhoneAccount();
428     }
429 }
430