CreateConnectionProcessor.java revision 8de76915ea2772faeb41705aaaeb65f5b3478ac4
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.CallState;
21import android.telecom.DisconnectCause;
22import android.telecom.ParcelableConnection;
23import android.telecom.Phone;
24import android.telecom.PhoneAccount;
25import android.telecom.PhoneAccountHandle;
26import android.telephony.TelephonyManager;
27import android.telephony.PhoneStateListener;
28import android.telephony.ServiceState;
29
30// TODO: Needed for move to system service: import com.android.internal.R;
31
32import java.util.ArrayList;
33import java.util.Collection;
34import java.util.HashSet;
35import java.util.Iterator;
36import java.util.List;
37import java.util.Set;
38import 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 */
47final 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
58        public CallAttemptRecord(
59                PhoneAccountHandle connectionManagerPhoneAccount,
60                PhoneAccountHandle targetPhoneAccount) {
61            this.connectionManagerPhoneAccount = connectionManagerPhoneAccount;
62            this.targetPhoneAccount = targetPhoneAccount;
63        }
64
65        @Override
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
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
102    CreateConnectionProcessor(
103            Call call, ConnectionServiceRepository repository, CreateConnectionResponse response,
104            PhoneAccountRegistrar phoneAccountRegistrar, Context context) {
105        Log.v(this, "CreateConnectionProcessor created for Call = %s", call);
106        mCall = call;
107        mRepository = repository;
108        mResponse = response;
109        mPhoneAccountRegistrar = phoneAccountRegistrar;
110        mContext = context;
111    }
112
113    boolean isProcessingComplete() {
114        return mResponse == null;
115    }
116
117    boolean isCallTimedOut() {
118        return mTimeout != null && mTimeout.isCallTimedOut();
119    }
120
121    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
135    boolean hasMorePhoneAccounts() {
136        return mAttemptRecordIterator.hasNext();
137    }
138
139    void continueProcessingIfPossible(CreateConnectionResponse response,
140            DisconnectCause disconnectCause) {
141        Log.v(this, "continueProcessingIfPossible");
142        mResponse = response;
143        mLastErrorDisconnectCause = disconnectCause;
144        attemptNextPhoneAccount();
145    }
146
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 = mResponse;
153        mResponse = 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
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.phoneAccountHasPermission(
173                    attempt.connectionManagerPhoneAccount)) {
174                Log.w(this,
175                        "Connection mgr does not have BIND_CONNECTION_SERVICE for attempt: %s",
176                        attempt);
177                attemptNextPhoneAccount();
178                return;
179            }
180
181            // If the target PhoneAccount differs from the ConnectionManager phone acount, ensure it
182            // also has BIND_CONNECTION_SERVICE permission.
183            if (!attempt.connectionManagerPhoneAccount.equals(attempt.targetPhoneAccount) &&
184                    !mPhoneAccountRegistrar.phoneAccountHasPermission(attempt.targetPhoneAccount)) {
185                Log.w(this,
186                        "Target PhoneAccount does not have BIND_CONNECTION_SERVICE for attempt: %s",
187                        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                Log.i(this, "Attempting to call from %s", service.getComponentName());
210                service.createConnection(mCall, new Response(service));
211            }
212        } else {
213            Log.v(this, "attemptNextPhoneAccount, no more accounts, failing");
214            if (mResponse != null) {
215                clearTimeout();
216                mResponse.handleCreateConnectionFailure(mLastErrorDisconnectCause != null ?
217                        mLastErrorDisconnectCause : new DisconnectCause(DisconnectCause.ERROR));
218                mResponse = null;
219                mCall.clearConnectionService();
220            }
221        }
222    }
223
224    private void setTimeoutIfNeeded(ConnectionServiceWrapper service, CallAttemptRecord attempt) {
225        clearTimeout();
226
227        CreateConnectionTimeout timeout = new CreateConnectionTimeout(
228                mContext, mPhoneAccountRegistrar, service, mCall);
229        if (timeout.isTimeoutNeededForCall(getConnectionServices(mAttemptRecords),
230                attempt.connectionManagerPhoneAccount)) {
231            mTimeout = timeout;
232            timeout.registerTimeout();
233        }
234    }
235
236    private void clearTimeout() {
237        if (mTimeout != null) {
238            mTimeout.unregisterTimeout();
239            mTimeout = null;
240        }
241    }
242
243    private boolean shouldSetConnectionManager() {
244        if (!mShouldUseConnectionManager) {
245            return false;
246        }
247
248        if (mAttemptRecords.size() == 0) {
249            return false;
250        }
251
252        if (mAttemptRecords.size() > 1) {
253            Log.d(this, "shouldSetConnectionManager, error, mAttemptRecords should not have more "
254                    + "than 1 record");
255            return false;
256        }
257
258        PhoneAccountHandle connectionManager = mPhoneAccountRegistrar.getSimCallManager();
259        if (connectionManager == null) {
260            return false;
261        }
262
263        PhoneAccountHandle targetPhoneAccountHandle = mAttemptRecords.get(0).targetPhoneAccount;
264        if (Objects.equals(connectionManager, targetPhoneAccountHandle)) {
265            return false;
266        }
267
268        // Connection managers are only allowed to manage SIM subscriptions.
269        PhoneAccount targetPhoneAccount = mPhoneAccountRegistrar.getPhoneAccount(
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 (TelephonyUtil.shouldProcessAsEmergency(mContext, mCall.getHandle())) {
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                PhoneAccount callManager = mPhoneAccountRegistrar
333                        .getPhoneAccount(callManagerHandle);
334                if (callManager != null && callManager.hasCapabilities(
335                        PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS)) {
336                    CallAttemptRecord callAttemptRecord = new CallAttemptRecord(callManagerHandle,
337                            mPhoneAccountRegistrar.
338                                    getDefaultOutgoingPhoneAccount(mCall.getHandle().getScheme())
339                    );
340
341                    if (!mAttemptRecords.contains(callAttemptRecord)) {
342                        Log.i(this, "Will try Connection Manager account %s for emergency",
343                                callManager);
344                        mAttemptRecords.add(callAttemptRecord);
345                    }
346                }
347            }
348        }
349    }
350
351    /** Returns all connection services used by the call attempt records. */
352    private static Collection<PhoneAccountHandle> getConnectionServices(
353            List<CallAttemptRecord> records) {
354        HashSet<PhoneAccountHandle> result = new HashSet<>();
355        for (CallAttemptRecord record : records) {
356            result.add(record.connectionManagerPhoneAccount);
357        }
358        return result;
359    }
360
361    private class Response implements CreateConnectionResponse {
362        private final ConnectionServiceWrapper mService;
363
364        Response(ConnectionServiceWrapper service) {
365            mService = service;
366        }
367
368        @Override
369        public void handleCreateConnectionSuccess(
370                CallIdMapper idMapper,
371                ParcelableConnection connection) {
372            if (mResponse == null) {
373                // Nobody is listening for this connection attempt any longer; ask the responsible
374                // ConnectionService to tear down any resources associated with the call
375                mService.abort(mCall);
376            } else {
377                // Success -- share the good news and remember that we are no longer interested
378                // in hearing about any more attempts
379                mResponse.handleCreateConnectionSuccess(idMapper, connection);
380                mResponse = null;
381                // If there's a timeout running then don't clear it. The timeout can be triggered
382                // after the call has successfully been created but before it has become active.
383            }
384        }
385
386        private boolean shouldFallbackToNoConnectionManager(DisconnectCause cause) {
387            PhoneAccountHandle handle = mCall.getConnectionManagerPhoneAccount();
388            if (handle == null || !handle.equals(mPhoneAccountRegistrar.getSimCallManager())) {
389                return false;
390            }
391
392            ConnectionServiceWrapper connectionManager = mCall.getConnectionService();
393            if (connectionManager == null) {
394                return false;
395            }
396
397            if (cause.getCode() == DisconnectCause.CONNECTION_MANAGER_NOT_SUPPORTED) {
398                Log.d(CreateConnectionProcessor.this, "Connection manager declined to handle the "
399                        + "call, falling back to not using a connection manager");
400                return true;
401            }
402
403            if (!connectionManager.isServiceValid("createConnection")) {
404                Log.d(CreateConnectionProcessor.this, "Connection manager unbound while trying "
405                        + "create a connection, falling back to not using a connection manager");
406                return true;
407            }
408
409            return false;
410        }
411
412        @Override
413        public void handleCreateConnectionFailure(DisconnectCause errorDisconnectCause) {
414            // Failure of some sort; record the reasons for failure and try again if possible
415            Log.d(CreateConnectionProcessor.this, "Connection failed: (%s)", errorDisconnectCause);
416            mLastErrorDisconnectCause = errorDisconnectCause;
417            if (shouldFallbackToNoConnectionManager(errorDisconnectCause)) {
418                mShouldUseConnectionManager = false;
419                // Restart from the beginning.
420                process();
421            } else {
422                attemptNextPhoneAccount();
423            }
424        }
425    }
426}
427