1 /*
2  * Copyright (c) 2019 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 
17 package com.android.ims;
18 
19 import android.content.Context;
20 import android.content.pm.PackageManager;
21 import android.os.Handler;
22 import android.os.Looper;
23 import android.telephony.ims.ImsReasonInfo;
24 import android.telephony.ims.feature.ImsFeature;
25 
26 import com.android.internal.annotations.VisibleForTesting;
27 import com.android.internal.telephony.util.HandlerExecutor;
28 import com.android.telephony.Rlog;
29 
30 import java.util.concurrent.Executor;
31 
32 /**
33  * Helper class for managing a connection to the ImsFeature manager.
34  */
35 public class FeatureConnector<T extends IFeatureConnector> extends Handler {
36     private static final String TAG = "FeatureConnector";
37     private static final boolean DBG = false;
38 
39     // Initial condition for ims connection retry.
40     private static final int IMS_RETRY_STARTING_TIMEOUT_MS = 500; // ms
41 
42     // Ceiling bitshift amount for service query timeout, calculated as:
43     // 2^mImsServiceRetryCount * IMS_RETRY_STARTING_TIMEOUT_MS, where
44     // mImsServiceRetryCount ∊ [0, CEILING_SERVICE_RETRY_COUNT].
45     private static final int CEILING_SERVICE_RETRY_COUNT = 6;
46 
47     public interface Listener<T> {
48         /**
49          * Get ImsFeature manager instance
50          */
getFeatureManager()51         T getFeatureManager();
52 
53         /**
54          * ImsFeature manager is connected to the underlying IMS implementation.
55          */
connectionReady(T manager)56         void connectionReady(T manager) throws ImsException;
57 
58         /**
59          * The underlying IMS implementation is unavailable and can not be used to communicate.
60          */
connectionUnavailable()61         void connectionUnavailable();
62     }
63 
64     public interface RetryTimeout {
get()65         int get();
66     }
67 
68     protected final int mPhoneId;
69     protected final Context mContext;
70     protected final Executor mExecutor;
71     protected final Object mLock = new Object();
72     protected final String mLogPrefix;
73 
74     @VisibleForTesting
75     public Listener<T> mListener;
76 
77     // The IMS feature manager which interacts with ImsService
78     @VisibleForTesting
79     public T mManager;
80 
81     protected int mRetryCount = 0;
82 
83     @VisibleForTesting
84     public RetryTimeout mRetryTimeout = () -> {
85         synchronized (mLock) {
86             int timeout = (1 << mRetryCount) * IMS_RETRY_STARTING_TIMEOUT_MS;
87             if (mRetryCount <= CEILING_SERVICE_RETRY_COUNT) {
88                 mRetryCount++;
89             }
90             return timeout;
91         }
92     };
93 
FeatureConnector(Context context, int phoneId, Listener<T> listener, String logPrefix)94     public FeatureConnector(Context context, int phoneId, Listener<T> listener,
95             String logPrefix) {
96         mContext = context;
97         mPhoneId = phoneId;
98         mListener = listener;
99         mExecutor = new HandlerExecutor(this);
100         mLogPrefix = logPrefix;
101     }
102 
103     @VisibleForTesting
FeatureConnector(Context context, int phoneId, Listener<T> listener, Executor executor, String logPrefix)104     public FeatureConnector(Context context, int phoneId, Listener<T> listener,
105             Executor executor, String logPrefix) {
106         mContext = context;
107         mPhoneId = phoneId;
108         mListener= listener;
109         mExecutor = executor;
110         mLogPrefix = logPrefix;
111     }
112 
113     @VisibleForTesting
FeatureConnector(Context context, int phoneId, Listener<T> listener, Executor executor, Looper looper)114     public FeatureConnector(Context context, int phoneId, Listener<T> listener,
115             Executor executor, Looper looper) {
116         super(looper);
117         mContext = context;
118         mPhoneId = phoneId;
119         mListener= listener;
120         mExecutor = executor;
121         mLogPrefix = "?";
122     }
123 
124     /**
125      * Start the creation of a connection to the underlying ImsService implementation. When the
126      * service is connected, {@link FeatureConnector.Listener#connectionReady(Object)} will be
127      * called with an active instance.
128      *
129      * If this device does not support an ImsStack (i.e. doesn't support
130      * {@link PackageManager#FEATURE_TELEPHONY_IMS} feature), this method will do nothing.
131      */
connect()132     public void connect() {
133         if (DBG) log("connect");
134         if (!isSupported()) {
135             logw("connect: not supported.");
136             return;
137         }
138         mRetryCount = 0;
139 
140         // Send a message to connect to the Ims Service and open a connection through
141         // getImsService().
142         post(mGetServiceRunnable);
143     }
144 
145     // Check if this ImsFeature is supported or not.
isSupported()146     private boolean isSupported() {
147         return ImsManager.isImsSupportedOnDevice(mContext);
148     }
149 
150     /**
151      * Disconnect from the ImsService Implementation and clean up. When this is complete,
152      * {@link FeatureConnector.Listener#connectionUnavailable()} will be called one last time.
153      */
disconnect()154     public void disconnect() {
155         if (DBG) log("disconnect");
156         removeCallbacks(mGetServiceRunnable);
157         synchronized (mLock) {
158             if (mManager != null) {
159                 mManager.removeNotifyStatusChangedCallback(mNotifyStatusChangedCallback);
160             }
161         }
162         notifyNotReady();
163     }
164 
165     private final Runnable mGetServiceRunnable = () -> {
166         try {
167             createImsService();
168         } catch (android.telephony.ims.ImsException e) {
169             int errorCode = e.getCode();
170             if (DBG) logw("Create IMS service error: " + errorCode);
171             if (android.telephony.ims.ImsException.CODE_ERROR_UNSUPPORTED_OPERATION != errorCode) {
172                 // Retry when error is not CODE_ERROR_UNSUPPORTED_OPERATION
173                 retryGetImsService();
174             }
175         }
176     };
177 
178     @VisibleForTesting
createImsService()179     public void createImsService() throws android.telephony.ims.ImsException {
180         synchronized (mLock) {
181             if (DBG) log("createImsService");
182             mManager = mListener.getFeatureManager();
183             // Adding to set, will be safe adding multiple times. If the ImsService is not
184             // active yet, this method will throw an ImsException.
185             mManager.addNotifyStatusChangedCallbackIfAvailable(mNotifyStatusChangedCallback);
186         }
187         // Wait for ImsService.STATE_READY to start listening for calls.
188         // Call the callback right away for compatibility with older devices that do not use
189         // states.
190         mNotifyStatusChangedCallback.notifyStateChanged();
191     }
192 
193     /**
194      * Remove callback and re-running mGetServiceRunnable
195      */
retryGetImsService()196     public void retryGetImsService() {
197         if (mManager != null) {
198             // remove callback so we do not receive updates from old ImsServiceProxy when
199             // switching between ImsServices.
200             mManager.removeNotifyStatusChangedCallback(mNotifyStatusChangedCallback);
201             //Leave mImsManager as null, then CallStateException will be thrown when dialing
202             mManager = null;
203         }
204 
205         // Exponential backoff during retry, limited to 32 seconds.
206         removeCallbacks(mGetServiceRunnable);
207         int timeout = mRetryTimeout.get();
208         postDelayed(mGetServiceRunnable, timeout);
209         if (DBG) log("retryGetImsService: unavailable, retrying in " + timeout + " ms");
210     }
211 
212     // Callback fires when IMS Feature changes state
213     public FeatureConnection.IFeatureUpdate mNotifyStatusChangedCallback =
214             new FeatureConnection.IFeatureUpdate() {
215                 @Override
216                 public void notifyStateChanged() {
217                     mExecutor.execute(() -> {
218                         try {
219                             int status = ImsFeature.STATE_UNAVAILABLE;
220                             synchronized (mLock) {
221                                 if (mManager != null) {
222                                     status = mManager.getImsServiceState();
223                                 }
224                             }
225                             switch (status) {
226                                 case ImsFeature.STATE_READY: {
227                                     notifyReady();
228                                     break;
229                                 }
230                                 case ImsFeature.STATE_INITIALIZING:
231                                     // fall through
232                                 case ImsFeature.STATE_UNAVAILABLE: {
233                                     notifyNotReady();
234                                     break;
235                                 }
236                                 default: {
237                                     logw("Unexpected State! " + status);
238                                 }
239                             }
240                         } catch (ImsException e) {
241                             // Could not get the ImsService, retry!
242                             notifyNotReady();
243                             retryGetImsService();
244                         }
245                     });
246                 }
247 
248                 @Override
249                 public void notifyUnavailable() {
250                     mExecutor.execute(() -> {
251                         notifyNotReady();
252                         retryGetImsService();
253                     });
254                 }
255             };
256 
notifyReady()257     private void notifyReady() throws ImsException {
258         T manager;
259         synchronized (mLock) {
260             manager = mManager;
261         }
262         try {
263             if (DBG) log("notifyReady");
264             mListener.connectionReady(manager);
265         }
266         catch (ImsException e) {
267             if(DBG) log("notifyReady exception: " + e.getMessage());
268             throw e;
269         }
270         // Only reset retry count if connectionReady does not generate an ImsException/
271         synchronized (mLock) {
272             mRetryCount = 0;
273         }
274     }
275 
notifyNotReady()276     protected void notifyNotReady() {
277         if (DBG) log("notifyNotReady");
278         mListener.connectionUnavailable();
279     }
280 
log(String message)281     private final void log(String message) {
282         Rlog.d(TAG, "[" + mLogPrefix + ", " + mPhoneId + "] " + message);
283     }
284 
logw(String message)285     private final void logw(String message) {
286         Rlog.w(TAG, "[" + mLogPrefix + ", " + mPhoneId + "] " + message);
287     }
288 }
289