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