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.os.UserHandle;
21import android.telecom.DisconnectCause;
22import android.telecom.ParcelableConnection;
23import android.telecom.PhoneAccount;
24import android.telecom.PhoneAccountHandle;
25
26// TODO: Needed for move to system service: import com.android.internal.R;
27
28import com.android.internal.annotations.VisibleForTesting;
29
30import java.util.ArrayList;
31import java.util.Collection;
32import java.util.HashSet;
33import java.util.Iterator;
34import java.util.List;
35import 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
45public 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
56        public CallAttemptRecord(
57                PhoneAccountHandle connectionManagerPhoneAccount,
58                PhoneAccountHandle targetPhoneAccount) {
59            this.connectionManagerPhoneAccount = connectionManagerPhoneAccount;
60            this.targetPhoneAccount = targetPhoneAccount;
61        }
62
63        @Override
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
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    private int mConnectionAttempt;
100
101    @VisibleForTesting
102    public 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        mCallResponse = response;
109        mPhoneAccountRegistrar = phoneAccountRegistrar;
110        mContext = context;
111        mConnectionAttempt = 0;
112    }
113
114    boolean isProcessingComplete() {
115        return mCallResponse == null;
116    }
117
118    boolean isCallTimedOut() {
119        return mTimeout != null && mTimeout.isCallTimedOut();
120    }
121
122    public int getConnectionAttempt() {
123        return mConnectionAttempt;
124    }
125
126    @VisibleForTesting
127    public void process() {
128        Log.v(this, "process");
129        clearTimeout();
130        mAttemptRecords = new ArrayList<>();
131        if (mCall.getTargetPhoneAccount() != null) {
132            mAttemptRecords.add(new CallAttemptRecord(
133                    mCall.getTargetPhoneAccount(), mCall.getTargetPhoneAccount()));
134        }
135        adjustAttemptsForConnectionManager();
136        adjustAttemptsForEmergency(mCall.getTargetPhoneAccount());
137        mAttemptRecordIterator = mAttemptRecords.iterator();
138        attemptNextPhoneAccount();
139    }
140
141    boolean hasMorePhoneAccounts() {
142        return mAttemptRecordIterator.hasNext();
143    }
144
145    void continueProcessingIfPossible(CreateConnectionResponse response,
146            DisconnectCause disconnectCause) {
147        Log.v(this, "continueProcessingIfPossible");
148        mCallResponse = response;
149        mLastErrorDisconnectCause = disconnectCause;
150        attemptNextPhoneAccount();
151    }
152
153    void abort() {
154        Log.v(this, "abort");
155
156        // Clear the response first to prevent attemptNextConnectionService from attempting any
157        // more services.
158        CreateConnectionResponse response = mCallResponse;
159        mCallResponse = null;
160        clearTimeout();
161
162        ConnectionServiceWrapper service = mCall.getConnectionService();
163        if (service != null) {
164            service.abort(mCall);
165            mCall.clearConnectionService();
166        }
167        if (response != null) {
168            response.handleCreateConnectionFailure(new DisconnectCause(DisconnectCause.LOCAL));
169        }
170    }
171
172    private void attemptNextPhoneAccount() {
173        Log.v(this, "attemptNextPhoneAccount");
174        CallAttemptRecord attempt = null;
175        if (mAttemptRecordIterator.hasNext()) {
176            attempt = mAttemptRecordIterator.next();
177
178            if (!mPhoneAccountRegistrar.phoneAccountRequiresBindPermission(
179                    attempt.connectionManagerPhoneAccount)) {
180                Log.w(this,
181                        "Connection mgr does not have BIND_TELECOM_CONNECTION_SERVICE for "
182                                + "attempt: %s", attempt);
183                attemptNextPhoneAccount();
184                return;
185            }
186
187            // If the target PhoneAccount differs from the ConnectionManager phone acount, ensure it
188            // also requires the BIND_TELECOM_CONNECTION_SERVICE permission.
189            if (!attempt.connectionManagerPhoneAccount.equals(attempt.targetPhoneAccount) &&
190                    !mPhoneAccountRegistrar.phoneAccountRequiresBindPermission(
191                            attempt.targetPhoneAccount)) {
192                Log.w(this,
193                        "Target PhoneAccount does not have BIND_TELECOM_CONNECTION_SERVICE for "
194                                + "attempt: %s", attempt);
195                attemptNextPhoneAccount();
196                return;
197            }
198        }
199
200        if (mCallResponse != null && attempt != null) {
201            Log.i(this, "Trying attempt %s", attempt);
202            PhoneAccountHandle phoneAccount = attempt.connectionManagerPhoneAccount;
203            mService = mRepository.getService(phoneAccount.getComponentName(),
204                    phoneAccount.getUserHandle());
205            if (mService == null) {
206                Log.i(this, "Found no connection service for attempt %s", attempt);
207                attemptNextPhoneAccount();
208            } else {
209                mConnectionAttempt++;
210                mCall.setConnectionManagerPhoneAccount(attempt.connectionManagerPhoneAccount);
211                mCall.setTargetPhoneAccount(attempt.targetPhoneAccount);
212                mCall.setConnectionService(mService);
213                setTimeoutIfNeeded(mService, attempt);
214
215                mService.createConnection(mCall, this);
216            }
217        } else {
218            Log.v(this, "attemptNextPhoneAccount, no more accounts, failing");
219            DisconnectCause disconnectCause = mLastErrorDisconnectCause != null ?
220                    mLastErrorDisconnectCause : new DisconnectCause(DisconnectCause.ERROR);
221            notifyCallConnectionFailure(disconnectCause);
222        }
223    }
224
225    private void setTimeoutIfNeeded(ConnectionServiceWrapper service, CallAttemptRecord attempt) {
226        clearTimeout();
227
228        CreateConnectionTimeout timeout = new CreateConnectionTimeout(
229                mContext, mPhoneAccountRegistrar, service, mCall);
230        if (timeout.isTimeoutNeededForCall(getConnectionServices(mAttemptRecords),
231                attempt.connectionManagerPhoneAccount)) {
232            mTimeout = timeout;
233            timeout.registerTimeout();
234        }
235    }
236
237    private void clearTimeout() {
238        if (mTimeout != null) {
239            mTimeout.unregisterTimeout();
240            mTimeout = null;
241        }
242    }
243
244    private boolean shouldSetConnectionManager() {
245        if (mAttemptRecords.size() == 0) {
246            return false;
247        }
248
249        if (mAttemptRecords.size() > 1) {
250            Log.d(this, "shouldSetConnectionManager, error, mAttemptRecords should not have more "
251                    + "than 1 record");
252            return false;
253        }
254
255        PhoneAccountHandle connectionManager =
256                mPhoneAccountRegistrar.getSimCallManagerFromCall(mCall);
257        if (connectionManager == null) {
258            return false;
259        }
260
261        PhoneAccountHandle targetPhoneAccountHandle = mAttemptRecords.get(0).targetPhoneAccount;
262        if (Objects.equals(connectionManager, targetPhoneAccountHandle)) {
263            return false;
264        }
265
266        // Connection managers are only allowed to manage SIM subscriptions.
267        // TODO: Should this really be checking the "calling user" test for phone account?
268        PhoneAccount targetPhoneAccount = mPhoneAccountRegistrar
269                .getPhoneAccountUnchecked(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.getSimCallManagerFromCall(mCall),
288                    mAttemptRecords.get(0).targetPhoneAccount);
289            Log.v(this, "setConnectionManager, changing %s -> %s", mAttemptRecords.get(0), record);
290            mAttemptRecords.add(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(PhoneAccountHandle preferredPAH) {
299        if (mCall.isEmergencyCall()) {
300            Log.i(this, "Emergency number detected");
301            mAttemptRecords.clear();
302            // Phone accounts in profile do not handle emergency call, use phone accounts in
303            // current user.
304            List<PhoneAccount> allAccounts = mPhoneAccountRegistrar
305                    .getAllPhoneAccountsOfCurrentUser();
306
307            if (allAccounts.isEmpty()) {
308                // If the list of phone accounts is empty at this point, it means Telephony hasn't
309                // registered any phone accounts yet. Add a fallback emergency phone account so
310                // that emergency calls can still go through. We create a new ArrayLists here just
311                // in case the implementation of PhoneAccountRegistrar ever returns an unmodifiable
312                // list.
313                allAccounts = new ArrayList<PhoneAccount>();
314                allAccounts.add(TelephonyUtil.getDefaultEmergencyPhoneAccount());
315            }
316
317            // First, possibly add the SIM phone account that the user prefers
318            PhoneAccount preferredPA = mPhoneAccountRegistrar.getPhoneAccountUnchecked(
319                    preferredPAH);
320            if (preferredPA != null &&
321                    preferredPA.hasCapabilities(PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS) &&
322                    preferredPA.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
323                Log.i(this, "Will try PSTN account %s for emergency",
324                        preferredPA.getAccountHandle());
325                mAttemptRecords.add(new CallAttemptRecord(preferredPAH, preferredPAH));
326            }
327
328            // Next, add all SIM phone accounts which can place emergency calls.
329            TelephonyUtil.sortSimPhoneAccounts(mContext, allAccounts);
330            for (PhoneAccount phoneAccount : allAccounts) {
331                if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS) &&
332                        phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
333                    PhoneAccountHandle phoneAccountHandle = phoneAccount.getAccountHandle();
334                    // Don't add the preferred account since it has already been added previously.
335                    if (!phoneAccountHandle.equals(preferredPAH)) {
336                        Log.i(this, "Will try PSTN account %s for emergency", phoneAccountHandle);
337                        mAttemptRecords.add(new CallAttemptRecord(phoneAccountHandle,
338                                phoneAccountHandle));
339                    }
340                }
341            }
342
343            // Next, add the connection manager account as a backup if it can place emergency calls.
344            PhoneAccountHandle callManagerHandle =
345                    mPhoneAccountRegistrar.getSimCallManagerOfCurrentUser();
346            if (callManagerHandle != null) {
347                // TODO: Should this really be checking the "calling user" test for phone account?
348                PhoneAccount callManager = mPhoneAccountRegistrar
349                        .getPhoneAccountUnchecked(callManagerHandle);
350                if (callManager != null && callManager.hasCapabilities(
351                        PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS)) {
352                    CallAttemptRecord callAttemptRecord = new CallAttemptRecord(callManagerHandle,
353                            mPhoneAccountRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser(
354                                    mCall.getHandle().getScheme()));
355                    if (!mAttemptRecords.contains(callAttemptRecord)) {
356                        Log.i(this, "Will try Connection Manager account %s for emergency",
357                                callManager);
358                        mAttemptRecords.add(callAttemptRecord);
359                    }
360                }
361            }
362        }
363    }
364
365    /** Returns all connection services used by the call attempt records. */
366    private static Collection<PhoneAccountHandle> getConnectionServices(
367            List<CallAttemptRecord> records) {
368        HashSet<PhoneAccountHandle> result = new HashSet<>();
369        for (CallAttemptRecord record : records) {
370            result.add(record.connectionManagerPhoneAccount);
371        }
372        return result;
373    }
374
375
376    private void notifyCallConnectionFailure(DisconnectCause errorDisconnectCause) {
377        if (mCallResponse != null) {
378            clearTimeout();
379            mCallResponse.handleCreateConnectionFailure(errorDisconnectCause);
380            mCallResponse = null;
381            mCall.clearConnectionService();
382        }
383    }
384
385    @Override
386    public void handleCreateConnectionSuccess(
387            CallIdMapper idMapper,
388            ParcelableConnection connection) {
389        if (mCallResponse == null) {
390            // Nobody is listening for this connection attempt any longer; ask the responsible
391            // ConnectionService to tear down any resources associated with the call
392            mService.abort(mCall);
393        } else {
394            // Success -- share the good news and remember that we are no longer interested
395            // in hearing about any more attempts
396            mCallResponse.handleCreateConnectionSuccess(idMapper, connection);
397            mCallResponse = null;
398            // If there's a timeout running then don't clear it. The timeout can be triggered
399            // after the call has successfully been created but before it has become active.
400        }
401    }
402
403    private boolean shouldFailCallIfConnectionManagerFails(DisconnectCause cause) {
404        // Connection Manager does not exist or does not match registered Connection Manager
405        // Since Connection manager is a proxy for SIM, fall back to SIM
406        PhoneAccountHandle handle = mCall.getConnectionManagerPhoneAccount();
407        if (handle == null || !handle.equals(mPhoneAccountRegistrar.getSimCallManagerFromCall(
408                mCall))) {
409            return false;
410        }
411
412        // The Call's Connection Service does not exist
413        ConnectionServiceWrapper connectionManager = mCall.getConnectionService();
414        if (connectionManager == null) {
415            return true;
416        }
417
418        // In this case, fall back to a sim because connection manager declined
419        if (cause.getCode() == DisconnectCause.CONNECTION_MANAGER_NOT_SUPPORTED) {
420            Log.d(CreateConnectionProcessor.this, "Connection manager declined to handle the "
421                    + "call, falling back to not using a connection manager");
422            return false;
423        }
424
425        if (!connectionManager.isServiceValid("createConnection")) {
426            Log.d(CreateConnectionProcessor.this, "Connection manager unbound while trying "
427                    + "create a connection, falling back to not using a connection manager");
428            return false;
429        }
430
431        // Do not fall back from connection manager and simply fail call if the failure reason is
432        // other
433        Log.d(CreateConnectionProcessor.this, "Connection Manager denied call with the following " +
434                "error: " + cause.getReason() + ". Not falling back to SIM.");
435        return true;
436    }
437
438    @Override
439    public void handleCreateConnectionFailure(DisconnectCause errorDisconnectCause) {
440        // Failure of some sort; record the reasons for failure and try again if possible
441        Log.d(CreateConnectionProcessor.this, "Connection failed: (%s)", errorDisconnectCause);
442        if (shouldFailCallIfConnectionManagerFails(errorDisconnectCause)) {
443            notifyCallConnectionFailure(errorDisconnectCause);
444            return;
445        }
446        mLastErrorDisconnectCause = errorDisconnectCause;
447        attemptNextPhoneAccount();
448    }
449}
450