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.annotation.IntDef;
20 import android.content.Context;
21 import android.content.pm.PackageManager;
22 import android.os.RemoteException;
23 import android.telephony.ims.ImsReasonInfo;
24 import android.telephony.ims.ImsService;
25 import android.telephony.ims.feature.ImsFeature;
26 
27 import com.android.ims.internal.IImsServiceFeatureCallback;
28 import com.android.internal.annotations.VisibleForTesting;
29 import com.android.telephony.Rlog;
30 
31 import java.lang.annotation.Retention;
32 import java.lang.annotation.RetentionPolicy;
33 import java.util.ArrayList;
34 import java.util.List;
35 import java.util.concurrent.Executor;
36 
37 /**
38  * Helper class for managing a connection to the ImsFeature manager.
39  */
40 public class FeatureConnector<U extends FeatureUpdates> {
41     private static final String TAG = "FeatureConnector";
42     private static final boolean DBG = false;
43 
44     /**
45      * This Connection has become unavailable due to the ImsService being disconnected due to
46      * an event such as SIM Swap, carrier configuration change, etc...
47      *
48      * {@link Listener#connectionReady}  will be called when a new Manager is available.
49      */
50     public static final int UNAVAILABLE_REASON_DISCONNECTED = 0;
51 
52     /**
53      * This Connection has become unavailable due to the ImsService moving to the NOT_READY state.
54      *
55      * {@link Listener#connectionReady}  will be called when the manager moves back to ready.
56      */
57     public static final int UNAVAILABLE_REASON_NOT_READY = 1;
58 
59     /**
60      * IMS is not supported on this device. This should be considered a permanent error and
61      * a Manager will never become available.
62      */
63     public static final int UNAVAILABLE_REASON_IMS_UNSUPPORTED = 2;
64 
65     /**
66      * The server of this information has crashed or otherwise generated an error that will require
67      * a retry to connect. This is rare, however in this case, {@link #disconnect()} and
68      * {@link #connect()} will need to be called again to recreate the connection with the server.
69      * <p>
70      * Only applicable if this is used outside of the server's own process.
71      */
72     public static final int UNAVAILABLE_REASON_SERVER_UNAVAILABLE = 3;
73 
74     /**
75      * @hide
76      */
77     @Retention(RetentionPolicy.SOURCE)
78     @IntDef(prefix = "UNAVAILABLE_REASON_", value = {
79             UNAVAILABLE_REASON_DISCONNECTED,
80             UNAVAILABLE_REASON_NOT_READY,
81             UNAVAILABLE_REASON_IMS_UNSUPPORTED,
82             UNAVAILABLE_REASON_SERVER_UNAVAILABLE
83     })
84     public @interface UnavailableReason {}
85 
86     /**
87      * Factory used to create a new instance of the manager that this FeatureConnector is waiting
88      * to connect the FeatureConnection to.
89      * @param <U> The Manager that this FeatureConnector has been created for.
90      */
91     public interface ManagerFactory<U extends FeatureUpdates> {
92         /**
93          * Create a manager instance, which will connect to the FeatureConnection.
94          */
createManager(Context context, int phoneId)95         U createManager(Context context, int phoneId);
96     }
97 
98     /**
99      * Listener interface used by Listeners of FeatureConnector that are waiting for a Manager
100      * interface for a specific ImsFeature.
101      * @param <U> The Manager that the listener is listening for.
102      */
103     public interface Listener<U extends FeatureUpdates> {
104         /**
105          * ImsFeature manager is connected to the underlying IMS implementation.
106          */
connectionReady(U manager, int subId)107         void connectionReady(U manager, int subId) throws ImsException;
108 
109         /**
110          * The underlying IMS implementation is unavailable and can not be used to communicate.
111          */
connectionUnavailable(@navailableReason int reason)112         void connectionUnavailable(@UnavailableReason int reason);
113     }
114 
115     private final IImsServiceFeatureCallback mCallback = new IImsServiceFeatureCallback.Stub() {
116 
117         @Override
118         public void imsFeatureCreated(ImsFeatureContainer c, int subId) {
119             log("imsFeatureCreated: " + c + ", subId: " + subId);
120             synchronized (mLock) {
121                 mManager.associate(c, subId);
122                 mManager.updateFeatureCapabilities(c.getCapabilities());
123                 mDisconnectedReason = null;
124             }
125             // Notifies executor, so notify outside of lock
126             imsStatusChanged(c.getState(), subId);
127         }
128 
129         @Override
130         public void imsFeatureRemoved(@UnavailableReason int reason) {
131             log("imsFeatureRemoved: reason=" + reason);
132             synchronized (mLock) {
133                 // only generate new events if the disconnect event isn't the same as before, except
134                 // for UNAVAILABLE_REASON_SERVER_UNAVAILABLE, which indicates a local issue and
135                 // each event is actionable.
136                 if (mDisconnectedReason != null
137                         && (mDisconnectedReason == reason
138                         && mDisconnectedReason != UNAVAILABLE_REASON_SERVER_UNAVAILABLE)) {
139                     log("imsFeatureRemoved: ignore");
140                     return;
141                 }
142                 mDisconnectedReason = reason;
143                 // Ensure that we set ready state back to false so that we do not miss setting ready
144                 // later if the initial state when recreated is READY.
145                 mLastReadyState = false;
146             }
147             // Allow the listener to do cleanup while the connection still potentially valid (unless
148             // the process crashed).
149             mExecutor.execute(() -> mListener.connectionUnavailable(reason));
150             mManager.invalidate();
151         }
152 
153         @Override
154         public void imsStatusChanged(int status, int subId) {
155             log("imsStatusChanged: status=" + ImsFeature.STATE_LOG_MAP.get(status));
156             final U manager;
157             final boolean isReady;
158             synchronized (mLock) {
159                 if (mDisconnectedReason != null) {
160                     log("imsStatusChanged: ignore");
161                     return;
162                 }
163                 mManager.updateFeatureState(status);
164                 manager = mManager;
165                 isReady = mReadyFilter.contains(status);
166                 boolean didReadyChange = isReady ^ mLastReadyState;
167                 mLastReadyState = isReady;
168                 if (!didReadyChange) {
169                     log("imsStatusChanged: ready didn't change, ignore");
170                     return;
171                 }
172             }
173             mExecutor.execute(() -> {
174                 try {
175                     if (isReady) {
176                         notifyReady(manager, subId);
177                     } else {
178                         notifyNotReady();
179                     }
180                 } catch (ImsException e) {
181                     if (e.getCode()
182                             == ImsReasonInfo.CODE_LOCAL_IMS_NOT_SUPPORTED_ON_DEVICE) {
183                         mListener.connectionUnavailable(UNAVAILABLE_REASON_IMS_UNSUPPORTED);
184                     } else {
185                         notifyNotReady();
186                     }
187                 }
188             });
189         }
190 
191         @Override
192         public void updateCapabilities(long caps) {
193             log("updateCapabilities: capabilities=" + ImsService.getCapabilitiesString(caps));
194             synchronized (mLock) {
195                 if (mDisconnectedReason != null) {
196                     log("updateCapabilities: ignore");
197                     return;
198                 }
199                 mManager.updateFeatureCapabilities(caps);
200             }
201         }
202     };
203 
204     private final int mPhoneId;
205     private final Context mContext;
206     private final ManagerFactory<U> mFactory;
207     private final Listener<U> mListener;
208     private final Executor mExecutor;
209     private final Object mLock = new Object();
210     private final String mLogPrefix;
211     // A List of integers, each corresponding to an ImsFeature.ImsState, that the FeatureConnector
212     // will use to call Listener#connectionReady when the ImsFeature that this connector is waiting
213     // for changes into one of the states in this list.
214     private final List<Integer> mReadyFilter = new ArrayList<>();
215 
216     private U mManager;
217     // Start in disconnected state;
218     private Integer mDisconnectedReason = UNAVAILABLE_REASON_DISCONNECTED;
219     // Stop redundant connectionAvailable if the ready filter contains multiple states.
220     // Also, do not send the first unavailable until after we have moved to available once.
221     private boolean mLastReadyState = false;
222 
223 
224 
225     @VisibleForTesting
FeatureConnector(Context context, int phoneId, ManagerFactory<U> factory, String logPrefix, List<Integer> readyFilter, Listener<U> listener, Executor executor)226     public FeatureConnector(Context context, int phoneId, ManagerFactory<U> factory,
227             String logPrefix, List<Integer> readyFilter, Listener<U> listener, Executor executor) {
228         mContext = context;
229         mPhoneId = phoneId;
230         mFactory = factory;
231         mLogPrefix = logPrefix;
232         mReadyFilter.addAll(readyFilter);
233         mListener = listener;
234         mExecutor = executor;
235     }
236 
237     /**
238      * Start the creation of a connection to the underlying ImsService implementation. When the
239      * service is connected, {@link FeatureConnector.Listener#connectionReady} will be
240      * called with an active instance.
241      *
242      * If this device does not support an ImsStack (i.e. doesn't support
243      * {@link PackageManager#FEATURE_TELEPHONY_IMS} feature), this method will do nothing.
244      */
connect()245     public void connect() {
246         if (DBG) log("connect");
247         if (!isSupported()) {
248             mExecutor.execute(() -> mListener.connectionUnavailable(
249                     UNAVAILABLE_REASON_IMS_UNSUPPORTED));
250             logw("connect: not supported.");
251             return;
252         }
253         synchronized (mLock) {
254             if (mManager == null) {
255                 mManager = mFactory.createManager(mContext, mPhoneId);
256             }
257         }
258         mManager.registerFeatureCallback(mPhoneId, mCallback);
259     }
260 
261     // Check if this ImsFeature is supported or not.
isSupported()262     private boolean isSupported() {
263         return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY_IMS);
264     }
265 
266     /**
267      * Disconnect from the ImsService Implementation and clean up. When this is complete,
268      * {@link FeatureConnector.Listener#connectionUnavailable(int)} will be called one last time.
269      */
disconnect()270     public void disconnect() {
271         if (DBG) log("disconnect");
272         final U manager;
273         synchronized (mLock) {
274             manager = mManager;
275         }
276         if (manager == null) return;
277 
278         manager.unregisterFeatureCallback(mCallback);
279         try {
280             mCallback.imsFeatureRemoved(UNAVAILABLE_REASON_DISCONNECTED);
281         } catch (RemoteException ignore) {} // local call
282     }
283 
284     // Should be called on executor
notifyReady(U manager, int subId)285     private void notifyReady(U manager, int subId) throws ImsException {
286         try {
287             if (DBG) log("notifyReady");
288             mListener.connectionReady(manager, subId);
289         }
290         catch (ImsException e) {
291             if(DBG) log("notifyReady exception: " + e.getMessage());
292             throw e;
293         }
294     }
295 
296     // Should be called on executor.
notifyNotReady()297     private void notifyNotReady() {
298         if (DBG) log("notifyNotReady");
299         mListener.connectionUnavailable(UNAVAILABLE_REASON_NOT_READY);
300     }
301 
log(String message)302     private void log(String message) {
303         Rlog.d(TAG, "[" + mLogPrefix + ", " + mPhoneId + "] " + message);
304     }
305 
logw(String message)306     private void logw(String message) {
307         Rlog.w(TAG, "[" + mLogPrefix + ", " + mPhoneId + "] " + message);
308     }
309 }
310