CreateConnectionProcessor.java revision 92694519535c54f542b4ef3973e9c1934f2feeff
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
17package com.android.server.telecom;
18
19import android.content.Context;
20import android.telecom.DisconnectCause;
21import android.telecom.ParcelableConnection;
22import android.telecom.Phone;
23import android.telecom.PhoneAccount;
24import android.telecom.PhoneAccountHandle;
25import android.telephony.TelephonyManager;
26import android.telephony.PhoneStateListener;
27import android.telephony.ServiceState;
28
29// TODO: Needed for move to system service: import com.android.internal.R;
30
31import java.util.ArrayList;
32import java.util.Collection;
33import java.util.HashSet;
34import java.util.Iterator;
35import java.util.List;
36import java.util.Set;
37import java.util.Objects;
38
39/**
40 * This class creates connections to place new outgoing calls or to attach to an existing incoming
41 * call. In either case, this class cycles through a set of connection services until:
42 *   - a connection service returns a newly created connection in which case the call is displayed
43 *     to the user
44 *   - a connection service cancels the process, in which case the call is aborted
45 */
46final class CreateConnectionProcessor {
47
48    // Describes information required to attempt to make a phone call
49    private static class CallAttemptRecord {
50        // The PhoneAccount describing the target connection service which we will
51        // contact in order to process an attempt
52        public final PhoneAccountHandle connectionManagerPhoneAccount;
53        // The PhoneAccount which we will tell the target connection service to use
54        // for attempting to make the actual phone call
55        public final PhoneAccountHandle targetPhoneAccount;
56
57        public CallAttemptRecord(
58                PhoneAccountHandle connectionManagerPhoneAccount,
59                PhoneAccountHandle targetPhoneAccount) {
60            this.connectionManagerPhoneAccount = connectionManagerPhoneAccount;
61            this.targetPhoneAccount = targetPhoneAccount;
62        }
63
64        @Override
65        public String toString() {
66            return "CallAttemptRecord("
67                    + Objects.toString(connectionManagerPhoneAccount) + ","
68                    + Objects.toString(targetPhoneAccount) + ")";
69        }
70
71        /**
72         * Determines if this instance of {@code CallAttemptRecord} has the same underlying
73         * {@code PhoneAccountHandle}s as another instance.
74         *
75         * @param obj The other instance to compare against.
76         * @return {@code True} if the {@code CallAttemptRecord}s are equal.
77         */
78        @Override
79        public boolean equals(Object obj) {
80            if (obj instanceof CallAttemptRecord) {
81                CallAttemptRecord other = (CallAttemptRecord) obj;
82                return Objects.equals(connectionManagerPhoneAccount,
83                        other.connectionManagerPhoneAccount) &&
84                        Objects.equals(targetPhoneAccount, other.targetPhoneAccount);
85            }
86            return false;
87        }
88    }
89
90    private final Call mCall;
91    private final ConnectionServiceRepository mRepository;
92    private List<CallAttemptRecord> mAttemptRecords;
93    private Iterator<CallAttemptRecord> mAttemptRecordIterator;
94    private CreateConnectionResponse mResponse;
95    private DisconnectCause mLastErrorDisconnectCause;
96    private final PhoneAccountRegistrar mPhoneAccountRegistrar;
97    private final Context mContext;
98    private boolean mShouldUseConnectionManager = true;
99    private CreateConnectionTimeout mTimeout;
100
101    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        mResponse = response;
108        mPhoneAccountRegistrar = phoneAccountRegistrar;
109        mContext = context;
110    }
111
112    boolean isProcessingComplete() {
113        return mResponse == null;
114    }
115
116    boolean isCallTimedOut() {
117        return mTimeout != null && mTimeout.isCallTimedOut();
118    }
119
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
134    boolean hasMorePhoneAccounts() {
135        return mAttemptRecordIterator.hasNext();
136    }
137
138    void continueProcessingIfPossible(CreateConnectionResponse response,
139            DisconnectCause disconnectCause) {
140        Log.v(this, "continueProcessingIfPossible");
141        mResponse = response;
142        mLastErrorDisconnectCause = disconnectCause;
143        attemptNextPhoneAccount();
144    }
145
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
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
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
235    private void clearTimeout() {
236        if (mTimeout != null) {
237            mTimeout.unregisterTimeout();
238            mTimeout = null;
239        }
240    }
241
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        if (targetPhoneAccount == null) {
271            Log.d(this, "shouldSetConnectionManager, phone account not found");
272            return false;
273        }
274        boolean isSimSubscription = (targetPhoneAccount.getCapabilities() &
275                PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION) != 0;
276        if (!isSimSubscription) {
277            return false;
278        }
279
280        return true;
281    }
282
283    // If there exists a registered connection manager then use it.
284    private void adjustAttemptsForConnectionManager() {
285        if (shouldSetConnectionManager()) {
286            CallAttemptRecord record = new CallAttemptRecord(
287                    mPhoneAccountRegistrar.getSimCallManager(),
288                    mAttemptRecords.get(0).targetPhoneAccount);
289            Log.v(this, "setConnectionManager, changing %s -> %s", mAttemptRecords.get(0), record);
290            mAttemptRecords.set(0, record);
291        } else {
292            Log.v(this, "setConnectionManager, not changing");
293        }
294    }
295
296    // If we are possibly attempting to call a local emergency number, ensure that the
297    // plain PSTN connection services are listed, and nothing else.
298    private void adjustAttemptsForEmergency()  {
299        if (TelephonyUtil.shouldProcessAsEmergency(mContext, mCall.getHandle())) {
300            Log.i(this, "Emergency number detected");
301            mAttemptRecords.clear();
302            List<PhoneAccount> allAccounts = mPhoneAccountRegistrar.getAllPhoneAccounts();
303
304            if (allAccounts.isEmpty()) {
305                // If the list of phone accounts is empty at this point, it means Telephony hasn't
306                // registered any phone accounts yet. Add a fallback emergency phone account so
307                // that emergency calls can still go through. We create a new ArrayLists here just
308                // in case the implementation of PhoneAccountRegistrar ever returns an unmodifiable
309                // list.
310                allAccounts = new ArrayList<PhoneAccount>();
311                allAccounts.add(TelephonyUtil.getDefaultEmergencyPhoneAccount());
312            }
313
314
315            // First, add SIM phone accounts which can place emergency calls.
316            for (PhoneAccount phoneAccount : allAccounts) {
317                if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS) &&
318                        phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
319                    Log.i(this, "Will try PSTN account %s for emergency",
320                            phoneAccount.getAccountHandle());
321                    mAttemptRecords.add(
322                            new CallAttemptRecord(
323                                    phoneAccount.getAccountHandle(),
324                                    phoneAccount.getAccountHandle()));
325                }
326            }
327
328            // Next, add the connection manager account as a backup if it can place emergency calls.
329            PhoneAccountHandle callManagerHandle = mPhoneAccountRegistrar.getSimCallManager();
330            if (mShouldUseConnectionManager && callManagerHandle != null) {
331                PhoneAccount callManager = mPhoneAccountRegistrar
332                        .getPhoneAccount(callManagerHandle);
333                if (callManager != null && callManager.hasCapabilities(
334                        PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS)) {
335                    CallAttemptRecord callAttemptRecord = new CallAttemptRecord(callManagerHandle,
336                            mPhoneAccountRegistrar.
337                                    getDefaultOutgoingPhoneAccount(mCall.getHandle().getScheme())
338                    );
339
340                    if (!mAttemptRecords.contains(callAttemptRecord)) {
341                        Log.i(this, "Will try Connection Manager account %s for emergency",
342                                callManager);
343                        mAttemptRecords.add(callAttemptRecord);
344                    }
345                }
346            }
347        }
348    }
349
350    /** Returns all connection services used by the call attempt records. */
351    private static Collection<PhoneAccountHandle> getConnectionServices(
352            List<CallAttemptRecord> records) {
353        HashSet<PhoneAccountHandle> result = new HashSet<>();
354        for (CallAttemptRecord record : records) {
355            result.add(record.connectionManagerPhoneAccount);
356        }
357        return result;
358    }
359
360    private class Response implements CreateConnectionResponse {
361        private final ConnectionServiceWrapper mService;
362
363        Response(ConnectionServiceWrapper service) {
364            mService = service;
365        }
366
367        @Override
368        public void handleCreateConnectionSuccess(
369                CallIdMapper idMapper,
370                ParcelableConnection connection) {
371            if (mResponse == null) {
372                // Nobody is listening for this connection attempt any longer; ask the responsible
373                // ConnectionService to tear down any resources associated with the call
374                mService.abort(mCall);
375            } else {
376                // Success -- share the good news and remember that we are no longer interested
377                // in hearing about any more attempts
378                mResponse.handleCreateConnectionSuccess(idMapper, connection);
379                mResponse = null;
380                // If there's a timeout running then don't clear it. The timeout can be triggered
381                // after the call has successfully been created but before it has become active.
382            }
383        }
384
385        private boolean shouldFallbackToNoConnectionManager(DisconnectCause cause) {
386            PhoneAccountHandle handle = mCall.getConnectionManagerPhoneAccount();
387            if (handle == null || !handle.equals(mPhoneAccountRegistrar.getSimCallManager())) {
388                return false;
389            }
390
391            ConnectionServiceWrapper connectionManager = mCall.getConnectionService();
392            if (connectionManager == null) {
393                return false;
394            }
395
396            if (cause.getCode() == DisconnectCause.CONNECTION_MANAGER_NOT_SUPPORTED) {
397                Log.d(CreateConnectionProcessor.this, "Connection manager declined to handle the "
398                        + "call, falling back to not using a connection manager");
399                return true;
400            }
401
402            if (!connectionManager.isServiceValid("createConnection")) {
403                Log.d(CreateConnectionProcessor.this, "Connection manager unbound while trying "
404                        + "create a connection, falling back to not using a connection manager");
405                return true;
406            }
407
408            return false;
409        }
410
411        @Override
412        public void handleCreateConnectionFailure(DisconnectCause errorDisconnectCause) {
413            // Failure of some sort; record the reasons for failure and try again if possible
414            Log.d(CreateConnectionProcessor.this, "Connection failed: (%s)", errorDisconnectCause);
415            mLastErrorDisconnectCause = errorDisconnectCause;
416            if (shouldFallbackToNoConnectionManager(errorDisconnectCause)) {
417                mShouldUseConnectionManager = false;
418                // Restart from the beginning.
419                process();
420            } else {
421                attemptNextPhoneAccount();
422            }
423        }
424    }
425}
426