aboutsummaryrefslogtreecommitdiff
path: root/src/java/com/android/ims/FeatureConnector.java
blob: e7c1c74ade82c184ebb139cbcc4f4846ce3575c5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
/*
 * Copyright (c) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.ims;

import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Handler;
import android.os.Looper;
import android.telephony.ims.ImsReasonInfo;
import android.telephony.ims.feature.ImsFeature;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.util.HandlerExecutor;
import com.android.telephony.Rlog;

import java.util.concurrent.Executor;

/**
 * Helper class for managing a connection to the ImsFeature manager.
 */
public class FeatureConnector<T extends IFeatureConnector> extends Handler {
    private static final String TAG = "FeatureConnector";
    private static final boolean DBG = false;

    // Initial condition for ims connection retry.
    private static final int IMS_RETRY_STARTING_TIMEOUT_MS = 500; // ms

    // Ceiling bitshift amount for service query timeout, calculated as:
    // 2^mImsServiceRetryCount * IMS_RETRY_STARTING_TIMEOUT_MS, where
    // mImsServiceRetryCount ∊ [0, CEILING_SERVICE_RETRY_COUNT].
    private static final int CEILING_SERVICE_RETRY_COUNT = 6;

    public interface Listener<T> {
        /**
         * Get ImsFeature manager instance
         */
        T getFeatureManager();

        /**
         * ImsFeature manager is connected to the underlying IMS implementation.
         */
        void connectionReady(T manager) throws ImsException;

        /**
         * The underlying IMS implementation is unavailable and can not be used to communicate.
         */
        void connectionUnavailable();
    }

    public interface RetryTimeout {
        int get();
    }

    protected final int mPhoneId;
    protected final Context mContext;
    protected final Executor mExecutor;
    protected final Object mLock = new Object();
    protected final String mLogPrefix;

    @VisibleForTesting
    public Listener<T> mListener;

    // The IMS feature manager which interacts with ImsService
    @VisibleForTesting
    public T mManager;

    protected int mRetryCount = 0;

    @VisibleForTesting
    public RetryTimeout mRetryTimeout = () -> {
        synchronized (mLock) {
            int timeout = (1 << mRetryCount) * IMS_RETRY_STARTING_TIMEOUT_MS;
            if (mRetryCount <= CEILING_SERVICE_RETRY_COUNT) {
                mRetryCount++;
            }
            return timeout;
        }
    };

    public FeatureConnector(Context context, int phoneId, Listener<T> listener,
            String logPrefix) {
        mContext = context;
        mPhoneId = phoneId;
        mListener = listener;
        mExecutor = new HandlerExecutor(this);
        mLogPrefix = logPrefix;
    }

    @VisibleForTesting
    public FeatureConnector(Context context, int phoneId, Listener<T> listener,
            Executor executor, String logPrefix) {
        mContext = context;
        mPhoneId = phoneId;
        mListener= listener;
        mExecutor = executor;
        mLogPrefix = logPrefix;
    }

    @VisibleForTesting
    public FeatureConnector(Context context, int phoneId, Listener<T> listener,
            Executor executor, Looper looper) {
        super(looper);
        mContext = context;
        mPhoneId = phoneId;
        mListener= listener;
        mExecutor = executor;
        mLogPrefix = "?";
    }

    /**
     * Start the creation of a connection to the underlying ImsService implementation. When the
     * service is connected, {@link FeatureConnector.Listener#connectionReady(Object)} will be
     * called with an active instance.
     *
     * If this device does not support an ImsStack (i.e. doesn't support
     * {@link PackageManager#FEATURE_TELEPHONY_IMS} feature), this method will do nothing.
     */
    public void connect() {
        if (DBG) log("connect");
        if (!isSupported()) {
            logw("connect: not supported.");
            return;
        }
        mRetryCount = 0;

        // Send a message to connect to the Ims Service and open a connection through
        // getImsService().
        post(mGetServiceRunnable);
    }

    // Check if this ImsFeature is supported or not.
    private boolean isSupported() {
        return ImsManager.isImsSupportedOnDevice(mContext);
    }

    /**
     * Disconnect from the ImsService Implementation and clean up. When this is complete,
     * {@link FeatureConnector.Listener#connectionUnavailable()} will be called one last time.
     */
    public void disconnect() {
        if (DBG) log("disconnect");
        removeCallbacks(mGetServiceRunnable);
        synchronized (mLock) {
            if (mManager != null) {
                mManager.removeNotifyStatusChangedCallback(mNotifyStatusChangedCallback);
            }
        }
        notifyNotReady();
    }

    private final Runnable mGetServiceRunnable = () -> {
        try {
            createImsService();
        } catch (android.telephony.ims.ImsException e) {
            int errorCode = e.getCode();
            if (DBG) logw("Create IMS service error: " + errorCode);
            if (android.telephony.ims.ImsException.CODE_ERROR_UNSUPPORTED_OPERATION != errorCode) {
                // Retry when error is not CODE_ERROR_UNSUPPORTED_OPERATION
                retryGetImsService();
            }
        }
    };

    @VisibleForTesting
    public void createImsService() throws android.telephony.ims.ImsException {
        synchronized (mLock) {
            if (DBG) log("createImsService");
            mManager = mListener.getFeatureManager();
            // Adding to set, will be safe adding multiple times. If the ImsService is not
            // active yet, this method will throw an ImsException.
            mManager.addNotifyStatusChangedCallbackIfAvailable(mNotifyStatusChangedCallback);
        }
        // Wait for ImsService.STATE_READY to start listening for calls.
        // Call the callback right away for compatibility with older devices that do not use
        // states.
        mNotifyStatusChangedCallback.notifyStateChanged();
    }

    /**
     * Remove callback and re-running mGetServiceRunnable
     */
    public void retryGetImsService() {
        if (mManager != null) {
            // remove callback so we do not receive updates from old ImsServiceProxy when
            // switching between ImsServices.
            mManager.removeNotifyStatusChangedCallback(mNotifyStatusChangedCallback);
            //Leave mImsManager as null, then CallStateException will be thrown when dialing
            mManager = null;
        }

        // Exponential backoff during retry, limited to 32 seconds.
        removeCallbacks(mGetServiceRunnable);
        int timeout = mRetryTimeout.get();
        postDelayed(mGetServiceRunnable, timeout);
        if (DBG) log("retryGetImsService: unavailable, retrying in " + timeout + " ms");
    }

    // Callback fires when IMS Feature changes state
    public FeatureConnection.IFeatureUpdate mNotifyStatusChangedCallback =
            new FeatureConnection.IFeatureUpdate() {
                @Override
                public void notifyStateChanged() {
                    mExecutor.execute(() -> {
                        try {
                            int status = ImsFeature.STATE_UNAVAILABLE;
                            synchronized (mLock) {
                                if (mManager != null) {
                                    status = mManager.getImsServiceState();
                                }
                            }
                            switch (status) {
                                case ImsFeature.STATE_READY: {
                                    notifyReady();
                                    break;
                                }
                                case ImsFeature.STATE_INITIALIZING:
                                    // fall through
                                case ImsFeature.STATE_UNAVAILABLE: {
                                    notifyNotReady();
                                    break;
                                }
                                default: {
                                    logw("Unexpected State! " + status);
                                }
                            }
                        } catch (ImsException e) {
                            // Could not get the ImsService, retry!
                            notifyNotReady();
                            retryGetImsService();
                        }
                    });
                }

                @Override
                public void notifyUnavailable() {
                    mExecutor.execute(() -> {
                        notifyNotReady();
                        retryGetImsService();
                    });
                }
            };

    private void notifyReady() throws ImsException {
        T manager;
        synchronized (mLock) {
            manager = mManager;
        }
        try {
            if (DBG) log("notifyReady");
            mListener.connectionReady(manager);
        }
        catch (ImsException e) {
            if(DBG) log("notifyReady exception: " + e.getMessage());
            throw e;
        }
        // Only reset retry count if connectionReady does not generate an ImsException/
        synchronized (mLock) {
            mRetryCount = 0;
        }
    }

    protected void notifyNotReady() {
        if (DBG) log("notifyNotReady");
        mListener.connectionUnavailable();
    }

    private final void log(String message) {
        Rlog.d(TAG, "[" + mLogPrefix + ", " + mPhoneId + "] " + message);
    }

    private final void logw(String message) {
        Rlog.w(TAG, "[" + mLogPrefix + ", " + mPhoneId + "] " + message);
    }
}