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