CreateConnectionProcessor.java revision 6ffe531cc43849b5ca22a23647f5e729fae01c25
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.phoneAccountRequiresBindPermission(
172                    attempt.connectionManagerPhoneAccount)) {
173                Log.w(this,
174                        "Connection mgr does not have BIND_TELECOM_CONNECTION_SERVICE for "
175                                + "attempt: %s", attempt);
176                attemptNextPhoneAccount();
177                return;
178            }
179
180            // If the target PhoneAccount differs from the ConnectionManager phone acount, ensure it
181            // also requires the BIND_TELECOM_CONNECTION_SERVICE permission.
182            if (!attempt.connectionManagerPhoneAccount.equals(attempt.targetPhoneAccount) &&
183                    !mPhoneAccountRegistrar.phoneAccountRequiresBindPermission(
184                            attempt.targetPhoneAccount)) {
185                Log.w(this,
186                        "Target PhoneAccount does not have BIND_TELECOM_CONNECTION_SERVICE for "
187                                + "attempt: %s", attempt);
188                attemptNextPhoneAccount();
189                return;
190            }
191        }
192
193        if (mResponse != null && attempt != null) {
194            Log.i(this, "Trying attempt %s", attempt);
195            PhoneAccountHandle phoneAccount = attempt.connectionManagerPhoneAccount;
196            ConnectionServiceWrapper service =
197                    mRepository.getService(
198                            phoneAccount.getComponentName(),
199                            phoneAccount.getUserHandle());
200            if (service == null) {
201                Log.i(this, "Found no connection service for attempt %s", attempt);
202                attemptNextPhoneAccount();
203            } else {
204                mCall.setConnectionManagerPhoneAccount(attempt.connectionManagerPhoneAccount);
205                mCall.setTargetPhoneAccount(attempt.targetPhoneAccount);
206                mCall.setConnectionService(service);
207                setTimeoutIfNeeded(service, attempt);
208
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        // TODO: Should this really be checking the "calling user" test for phone account?
269        PhoneAccount targetPhoneAccount = mPhoneAccountRegistrar.getPhoneAccountCheckCallingUser(
270                targetPhoneAccountHandle);
271        if (targetPhoneAccount == null) {
272            Log.d(this, "shouldSetConnectionManager, phone account not found");
273            return false;
274        }
275        boolean isSimSubscription = (targetPhoneAccount.getCapabilities() &
276                PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION) != 0;
277        if (!isSimSubscription) {
278            return false;
279        }
280
281        return true;
282    }
283
284    // If there exists a registered connection manager then use it.
285    private void adjustAttemptsForConnectionManager() {
286        if (shouldSetConnectionManager()) {
287            CallAttemptRecord record = new CallAttemptRecord(
288                    mPhoneAccountRegistrar.getSimCallManager(),
289                    mAttemptRecords.get(0).targetPhoneAccount);
290            Log.v(this, "setConnectionManager, changing %s -> %s", mAttemptRecords.get(0), record);
291            mAttemptRecords.set(0, record);
292        } else {
293            Log.v(this, "setConnectionManager, not changing");
294        }
295    }
296
297    // If we are possibly attempting to call a local emergency number, ensure that the
298    // plain PSTN connection services are listed, and nothing else.
299    private void adjustAttemptsForEmergency()  {
300        if (mCall.isEmergencyCall()) {
301            Log.i(this, "Emergency number detected");
302            mAttemptRecords.clear();
303            List<PhoneAccount> allAccounts = mPhoneAccountRegistrar.getAllPhoneAccounts();
304
305            if (allAccounts.isEmpty()) {
306                // If the list of phone accounts is empty at this point, it means Telephony hasn't
307                // registered any phone accounts yet. Add a fallback emergency phone account so
308                // that emergency calls can still go through. We create a new ArrayLists here just
309                // in case the implementation of PhoneAccountRegistrar ever returns an unmodifiable
310                // list.
311                allAccounts = new ArrayList<PhoneAccount>();
312                allAccounts.add(TelephonyUtil.getDefaultEmergencyPhoneAccount());
313            }
314
315
316            // First, add SIM phone accounts which can place emergency calls.
317            for (PhoneAccount phoneAccount : allAccounts) {
318                if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS) &&
319                        phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
320                    Log.i(this, "Will try PSTN account %s for emergency",
321                            phoneAccount.getAccountHandle());
322                    mAttemptRecords.add(
323                            new CallAttemptRecord(
324                                    phoneAccount.getAccountHandle(),
325                                    phoneAccount.getAccountHandle()));
326                }
327            }
328
329            // Next, add the connection manager account as a backup if it can place emergency calls.
330            PhoneAccountHandle callManagerHandle = mPhoneAccountRegistrar.getSimCallManager();
331            if (mShouldUseConnectionManager && callManagerHandle != null) {
332                // TODO: Should this really be checking the "calling user" test for phone account?
333                PhoneAccount callManager = mPhoneAccountRegistrar
334                        .getPhoneAccountCheckCallingUser(callManagerHandle);
335                if (callManager != null && callManager.hasCapabilities(
336                        PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS)) {
337                    CallAttemptRecord callAttemptRecord = new CallAttemptRecord(callManagerHandle,
338                            mPhoneAccountRegistrar.
339                                    getOutgoingPhoneAccountForScheme(mCall.getHandle().getScheme())
340                    );
341
342                    if (!mAttemptRecords.contains(callAttemptRecord)) {
343                        Log.i(this, "Will try Connection Manager account %s for emergency",
344                                callManager);
345                        mAttemptRecords.add(callAttemptRecord);
346                    }
347                }
348            }
349        }
350    }
351
352    /** Returns all connection services used by the call attempt records. */
353    private static Collection<PhoneAccountHandle> getConnectionServices(
354            List<CallAttemptRecord> records) {
355        HashSet<PhoneAccountHandle> result = new HashSet<>();
356        for (CallAttemptRecord record : records) {
357            result.add(record.connectionManagerPhoneAccount);
358        }
359        return result;
360    }
361
362    private class Response implements CreateConnectionResponse {
363        private final ConnectionServiceWrapper mService;
364
365        Response(ConnectionServiceWrapper service) {
366            mService = service;
367        }
368
369        @Override
370        public void handleCreateConnectionSuccess(
371                CallIdMapper idMapper,
372                ParcelableConnection connection) {
373            if (mResponse == null) {
374                // Nobody is listening for this connection attempt any longer; ask the responsible
375                // ConnectionService to tear down any resources associated with the call
376                mService.abort(mCall);
377            } else {
378                // Success -- share the good news and remember that we are no longer interested
379                // in hearing about any more attempts
380                mResponse.handleCreateConnectionSuccess(idMapper, connection);
381                mResponse = null;
382                // If there's a timeout running then don't clear it. The timeout can be triggered
383                // after the call has successfully been created but before it has become active.
384            }
385        }
386
387        private boolean shouldFallbackToNoConnectionManager(DisconnectCause cause) {
388            PhoneAccountHandle handle = mCall.getConnectionManagerPhoneAccount();
389            if (handle == null || !handle.equals(mPhoneAccountRegistrar.getSimCallManager())) {
390                return false;
391            }
392
393            ConnectionServiceWrapper connectionManager = mCall.getConnectionService();
394            if (connectionManager == null) {
395                return false;
396            }
397
398            if (cause.getCode() == DisconnectCause.CONNECTION_MANAGER_NOT_SUPPORTED) {
399                Log.d(CreateConnectionProcessor.this, "Connection manager declined to handle the "
400                        + "call, falling back to not using a connection manager");
401                return true;
402            }
403
404            if (!connectionManager.isServiceValid("createConnection")) {
405                Log.d(CreateConnectionProcessor.this, "Connection manager unbound while trying "
406                        + "create a connection, falling back to not using a connection manager");
407                return true;
408            }
409
410            return false;
411        }
412
413        @Override
414        public void handleCreateConnectionFailure(DisconnectCause errorDisconnectCause) {
415            // Failure of some sort; record the reasons for failure and try again if possible
416            Log.d(CreateConnectionProcessor.this, "Connection failed: (%s)", errorDisconnectCause);
417            mLastErrorDisconnectCause = errorDisconnectCause;
418            if (shouldFallbackToNoConnectionManager(errorDisconnectCause)) {
419                mShouldUseConnectionManager = false;
420                // Restart from the beginning.
421                process();
422            } else {
423                attemptNextPhoneAccount();
424            }
425        }
426    }
427}
428