1 /*
2  * Copyright (C) 2020 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.NonNull;
20 import android.annotation.Nullable;
21 import android.os.RemoteException;
22 import android.telephony.ims.ImsService;
23 import android.telephony.ims.feature.ImsFeature;
24 import android.util.LocalLog;
25 import android.util.Log;
26 
27 import com.android.ims.internal.IImsServiceFeatureCallback;
28 import com.android.internal.annotations.GuardedBy;
29 
30 import java.io.PrintWriter;
31 import java.util.ArrayList;
32 import java.util.List;
33 import java.util.Objects;
34 import java.util.Optional;
35 import java.util.concurrent.Executor;
36 import java.util.stream.Collectors;
37 
38 /**
39  * A repository of ImsFeature connections made available by an ImsService once it has been
40  * successfully bound.
41  *
42  * Provides the ability for listeners to register callbacks and the repository notify registered
43  * listeners when a connection has been created/removed for a specific connection type.
44  */
45 public class ImsFeatureBinderRepository {
46 
47     private static final String TAG = "ImsFeatureBinderRepo";
48 
49     /**
50      * Internal class representing a listener that is listening for changes to specific
51      * ImsFeature instances.
52      */
53     private static class ListenerContainer {
54         private final IImsServiceFeatureCallback mCallback;
55         private final Executor mExecutor;
56 
ListenerContainer(@onNull IImsServiceFeatureCallback c, @NonNull Executor e)57         public ListenerContainer(@NonNull IImsServiceFeatureCallback c, @NonNull Executor e) {
58             mCallback = c;
59             mExecutor = e;
60         }
61 
notifyFeatureCreatedOrRemoved(ImsFeatureContainer connector, int subId)62         public void notifyFeatureCreatedOrRemoved(ImsFeatureContainer connector, int subId) {
63             if (connector == null) {
64                 mExecutor.execute(() -> {
65                     try {
66                         mCallback.imsFeatureRemoved(
67                                 FeatureConnector.UNAVAILABLE_REASON_DISCONNECTED);
68                     } catch (RemoteException e) {
69                         // This listener will eventually be caught and removed during stale checks.
70                     }
71                 });
72             }
73             else {
74                 mExecutor.execute(() -> {
75                     try {
76                         mCallback.imsFeatureCreated(connector, subId);
77                     } catch (RemoteException e) {
78                         // This listener will eventually be caught and removed during stale checks.
79                     }
80                 });
81             }
82         }
83 
notifyStateChanged(int state, int subId)84         public void notifyStateChanged(int state, int subId) {
85             mExecutor.execute(() -> {
86                 try {
87                     mCallback.imsStatusChanged(state, subId);
88                 } catch (RemoteException e) {
89                     // This listener will eventually be caught and removed during stale checks.
90                 }
91             });
92         }
93 
notifyUpdateCapabilties(long caps)94         public void notifyUpdateCapabilties(long caps) {
95             mExecutor.execute(() -> {
96                 try {
97                     mCallback.updateCapabilities(caps);
98                 } catch (RemoteException e) {
99                     // This listener will eventually be caught and removed during stale checks.
100                 }
101             });
102         }
103 
isStale()104         public boolean isStale() {
105             return !mCallback.asBinder().isBinderAlive();
106         }
107 
108         @Override
equals(Object o)109         public boolean equals(Object o) {
110             if (this == o) return true;
111             if (o == null || getClass() != o.getClass()) return false;
112             ListenerContainer that = (ListenerContainer) o;
113             // Do not count executor for equality.
114             return mCallback.equals(that.mCallback);
115         }
116 
117         @Override
hashCode()118         public int hashCode() {
119             // Do not use executor for hash.
120             return Objects.hash(mCallback);
121         }
122 
123         @Override
toString()124         public String toString() {
125             return "ListenerContainer{" + "cb=" + mCallback + '}';
126         }
127     }
128 
129     /**
130      * Contains the mapping from ImsFeature type (MMTEL/RCS) to List of listeners listening for
131      * updates to the ImsFeature instance contained in the ImsFeatureContainer.
132      */
133     private static final class UpdateMapper {
134         public final int phoneId;
135         public int subId;
136         public final @ImsFeature.FeatureType int imsFeatureType;
137         private final List<ListenerContainer> mListeners = new ArrayList<>();
138         private ImsFeatureContainer mFeatureContainer;
139         private final Object mLock = new Object();
140 
141 
UpdateMapper(int pId, @ImsFeature.FeatureType int t)142         public UpdateMapper(int pId, @ImsFeature.FeatureType int t) {
143             phoneId = pId;
144             imsFeatureType = t;
145         }
146 
addFeatureContainer(ImsFeatureContainer c)147         public void addFeatureContainer(ImsFeatureContainer c) {
148             List<ListenerContainer> listeners;
149             synchronized (mLock) {
150                 if (Objects.equals(c, mFeatureContainer)) return;
151                 mFeatureContainer = c;
152                 listeners = copyListenerList(mListeners);
153             }
154             listeners.forEach(l -> l.notifyFeatureCreatedOrRemoved(mFeatureContainer, subId));
155         }
156 
removeFeatureContainer()157         public ImsFeatureContainer removeFeatureContainer() {
158             ImsFeatureContainer oldContainer;
159             List<ListenerContainer> listeners;
160             synchronized (mLock) {
161                 if (mFeatureContainer == null) return null;
162                 oldContainer = mFeatureContainer;
163                 mFeatureContainer = null;
164                 listeners = copyListenerList(mListeners);
165             }
166             listeners.forEach(l -> l.notifyFeatureCreatedOrRemoved(mFeatureContainer, subId));
167             return oldContainer;
168         }
169 
getFeatureContainer()170         public ImsFeatureContainer getFeatureContainer() {
171             synchronized(mLock) {
172                 return mFeatureContainer;
173             }
174         }
175 
addListener(ListenerContainer c)176         public void addListener(ListenerContainer c) {
177             ImsFeatureContainer featureContainer;
178             synchronized (mLock) {
179                 removeStaleListeners();
180                 if (mListeners.contains(c)) {
181                     return;
182                 }
183                 featureContainer = mFeatureContainer;
184                 mListeners.add(c);
185             }
186             // Do not call back until the feature container has been set.
187             if (featureContainer != null) {
188                 c.notifyFeatureCreatedOrRemoved(featureContainer, subId);
189             }
190         }
191 
removeListener(IImsServiceFeatureCallback callback)192         public void removeListener(IImsServiceFeatureCallback callback) {
193             synchronized (mLock) {
194                 removeStaleListeners();
195                 List<ListenerContainer> oldListeners = mListeners.stream()
196                         .filter((c) -> Objects.equals(c.mCallback, callback))
197                         .collect(Collectors.toList());
198                 mListeners.removeAll(oldListeners);
199             }
200         }
201 
notifyStateUpdated(int newState)202         public void notifyStateUpdated(int newState) {
203             ImsFeatureContainer featureContainer;
204             List<ListenerContainer> listeners;
205             synchronized (mLock) {
206                 removeStaleListeners();
207                 featureContainer = mFeatureContainer;
208                 listeners = copyListenerList(mListeners);
209                 if (mFeatureContainer != null) {
210                     if (mFeatureContainer.getState() != newState) {
211                         mFeatureContainer.setState(newState);
212                     }
213                 }
214             }
215             // Only update if the feature container is set.
216             if (featureContainer != null) {
217                 listeners.forEach(l -> l.notifyStateChanged(newState, subId));
218             }
219         }
220 
notifyUpdateCapabilities(long caps)221         public void notifyUpdateCapabilities(long caps) {
222             ImsFeatureContainer featureContainer;
223             List<ListenerContainer> listeners;
224             synchronized (mLock) {
225                 removeStaleListeners();
226                 featureContainer = mFeatureContainer;
227                 listeners = copyListenerList(mListeners);
228                 if (mFeatureContainer != null) {
229                     if (mFeatureContainer.getCapabilities() != caps) {
230                         mFeatureContainer.setCapabilities(caps);
231                     }
232                 }
233             }
234             // Only update if the feature container is set.
235             if (featureContainer != null) {
236                 listeners.forEach(l -> l.notifyUpdateCapabilties(caps));
237             }
238         }
239 
updateSubId(int newSubId)240         public void updateSubId(int newSubId) {
241             subId = newSubId;
242         }
243 
244         @GuardedBy("mLock")
removeStaleListeners()245         private void removeStaleListeners() {
246             List<ListenerContainer> staleListeners = mListeners.stream().filter(
247                     ListenerContainer::isStale)
248                     .collect(Collectors.toList());
249             mListeners.removeAll(staleListeners);
250         }
251 
252         @Override
toString()253         public String toString() {
254             synchronized (mLock) {
255                 return "UpdateMapper{" + "phoneId=" + phoneId + ", type="
256                         + ImsFeature.FEATURE_LOG_MAP.get(imsFeatureType) + ", container="
257                         + mFeatureContainer + '}';
258             }
259         }
260 
261 
copyListenerList(List<ListenerContainer> listeners)262         private List<ListenerContainer> copyListenerList(List<ListenerContainer> listeners) {
263             return new ArrayList<>(listeners);
264         }
265     }
266 
267     private final List<UpdateMapper> mFeatures = new ArrayList<>();
268     private final LocalLog mLocalLog = new LocalLog(50 /*lines*/);
269 
ImsFeatureBinderRepository()270     public ImsFeatureBinderRepository() {
271         logInfoLineLocked(-1, "FeatureConnectionRepository - created");
272     }
273 
274     /**
275      * Get the Container for a specific ImsFeature now if it exists.
276      *
277      * @param phoneId The phone ID that the connection is related to.
278      * @param type The ImsFeature type to get the cotnainr for (MMTEL/RCS).
279      * @return The Container containing the requested ImsFeature if it exists.
280      */
getIfExists( int phoneId, @ImsFeature.FeatureType int type)281     public Optional<ImsFeatureContainer> getIfExists(
282             int phoneId, @ImsFeature.FeatureType int type) {
283         if (type < 0 || type >= ImsFeature.FEATURE_MAX) {
284             throw new IllegalArgumentException("Incorrect feature type");
285         }
286         UpdateMapper m;
287         m = getUpdateMapper(phoneId, type);
288         ImsFeatureContainer c = m.getFeatureContainer();
289         logVerboseLineLocked(phoneId, "getIfExists, type= " + ImsFeature.FEATURE_LOG_MAP.get(type)
290                 + ", result= " + c);
291         return Optional.ofNullable(c);
292     }
293 
294     /**
295      * Register a callback that will receive updates when the requested ImsFeature type becomes
296      * available or unavailable for the specified phone ID.
297      * <p>
298      * This callback will not be called the first time until there is a valid ImsFeature.
299      * @param phoneId The phone ID that the connection will be related to.
300      * @param type The ImsFeature type to get (MMTEL/RCS).
301      * @param callback The callback that will be used to notify when the callback is
302      *                 available/unavailable.
303      * @param executor The executor that the callback will be run on.
304      */
registerForConnectionUpdates(int phoneId, @ImsFeature.FeatureType int type, @NonNull IImsServiceFeatureCallback callback, @NonNull Executor executor)305     public void registerForConnectionUpdates(int phoneId,
306             @ImsFeature.FeatureType int type, @NonNull IImsServiceFeatureCallback callback,
307             @NonNull Executor executor) {
308         if (type < 0 || type >= ImsFeature.FEATURE_MAX || callback == null || executor == null) {
309             throw new IllegalArgumentException("One or more invalid arguments have been passed in");
310         }
311         ListenerContainer container = new ListenerContainer(callback, executor);
312         logInfoLineLocked(phoneId, "registerForConnectionUpdates, type= "
313                 + ImsFeature.FEATURE_LOG_MAP.get(type) +", conn= " + container);
314         UpdateMapper m = getUpdateMapper(phoneId, type);
315         m.addListener(container);
316     }
317 
318     /**
319      * Unregister for updates on a previously registered callback.
320      *
321      * @param callback The callback to unregister.
322      */
unregisterForConnectionUpdates(@onNull IImsServiceFeatureCallback callback)323     public void unregisterForConnectionUpdates(@NonNull IImsServiceFeatureCallback callback) {
324         if (callback == null) {
325             throw new IllegalArgumentException("this method does not accept null arguments");
326         }
327         logInfoLineLocked(-1, "unregisterForConnectionUpdates, callback= " + callback);
328         synchronized (mFeatures) {
329             for (UpdateMapper m : mFeatures) {
330                 // warning: no callbacks should be called while holding locks
331                 m.removeListener(callback);
332             }
333         }
334     }
335 
336     /**
337      * Add a Container containing the IBinder interfaces associated with a specific ImsFeature type
338      * (MMTEL/RCS). If one already exists, it will be replaced. This will notify listeners of the
339      * change.
340      * @param phoneId The phone ID associated with this Container.
341      * @param type The ImsFeature type to get (MMTEL/RCS).
342      * @param newConnection A Container containing the IBinder interface connections associated with
343      *                      the ImsFeature type.
344      */
addConnection(int phoneId, int subId, @ImsFeature.FeatureType int type, @Nullable ImsFeatureContainer newConnection)345     public void addConnection(int phoneId, int subId, @ImsFeature.FeatureType int type,
346             @Nullable ImsFeatureContainer newConnection) {
347         if (type < 0 || type >= ImsFeature.FEATURE_MAX) {
348             throw new IllegalArgumentException("The type must valid");
349         }
350         logInfoLineLocked(phoneId, "addConnection, subId=" + subId + ", type="
351                 + ImsFeature.FEATURE_LOG_MAP.get(type) + ", conn=" + newConnection);
352         UpdateMapper m = getUpdateMapper(phoneId, type);
353         m.updateSubId(subId);
354         m.addFeatureContainer(newConnection);
355     }
356 
357     /**
358      * Remove the IBinder Container associated with a specific ImsService type. Listeners will be
359      * notified of this change.
360      * @param phoneId The phone ID associated with this connection.
361      * @param type The ImsFeature type to get (MMTEL/RCS).
362      */
removeConnection(int phoneId, @ImsFeature.FeatureType int type)363     public ImsFeatureContainer removeConnection(int phoneId, @ImsFeature.FeatureType int type) {
364         if (type < 0 || type >= ImsFeature.FEATURE_MAX) {
365             throw new IllegalArgumentException("The type must valid");
366         }
367         logInfoLineLocked(phoneId, "removeConnection, type="
368                 + ImsFeature.FEATURE_LOG_MAP.get(type));
369         UpdateMapper m = getUpdateMapper(phoneId, type);
370         return m.removeFeatureContainer();
371     }
372 
373     /**
374      * Notify listeners that the state of a specific ImsFeature that this repository is
375      * tracking has changed. Listeners will be notified of the change in the ImsFeature's state.
376      * @param phoneId The phoneId of the feature that has changed state.
377      * @param type The ImsFeature type to get (MMTEL/RCS).
378      * @param state The new state of the ImsFeature
379      */
notifyFeatureStateChanged(int phoneId, @ImsFeature.FeatureType int type, @ImsFeature.ImsState int state)380     public void notifyFeatureStateChanged(int phoneId, @ImsFeature.FeatureType int type,
381             @ImsFeature.ImsState int state) {
382         logInfoLineLocked(phoneId, "notifyFeatureStateChanged, type="
383                 + ImsFeature.FEATURE_LOG_MAP.get(type) + ", state="
384                 + ImsFeature.STATE_LOG_MAP.get(state));
385         UpdateMapper m = getUpdateMapper(phoneId, type);
386         m.notifyStateUpdated(state);
387     }
388 
389     /**
390      *  Notify listeners that the capabilities of a specific ImsFeature that this repository is
391      * tracking has changed. Listeners will be notified of the change in the ImsFeature's
392      * capabilities.
393      * @param phoneId The phoneId of the feature that has changed capabilities.
394      * @param type The ImsFeature type to get (MMTEL/RCS).
395      * @param capabilities The new capabilities of the ImsFeature
396      */
notifyFeatureCapabilitiesChanged(int phoneId, @ImsFeature.FeatureType int type, @ImsService.ImsServiceCapability long capabilities)397     public void notifyFeatureCapabilitiesChanged(int phoneId, @ImsFeature.FeatureType int type,
398             @ImsService.ImsServiceCapability long capabilities) {
399         logInfoLineLocked(phoneId, "notifyFeatureCapabilitiesChanged, type="
400                 + ImsFeature.FEATURE_LOG_MAP.get(type) + ", caps="
401                 + ImsService.getCapabilitiesString(capabilities));
402         UpdateMapper m = getUpdateMapper(phoneId, type);
403         m.notifyUpdateCapabilities(capabilities);
404     }
405 
406     /**
407      * Prints the dump of log events that have occurred on this repository.
408      */
dump(PrintWriter printWriter)409     public void dump(PrintWriter printWriter) {
410         synchronized (mLocalLog) {
411             mLocalLog.dump(printWriter);
412         }
413     }
414 
getUpdateMapper(int phoneId, int type)415     private UpdateMapper getUpdateMapper(int phoneId, int type) {
416         synchronized (mFeatures) {
417             UpdateMapper mapper = mFeatures.stream()
418                     .filter((c) -> ((c.phoneId == phoneId) && (c.imsFeatureType == type)))
419                     .findFirst().orElse(null);
420             if (mapper == null) {
421                 mapper = new UpdateMapper(phoneId, type);
422                 mFeatures.add(mapper);
423             }
424             return mapper;
425         }
426     }
427 
logVerboseLineLocked(int phoneId, String log)428     private void logVerboseLineLocked(int phoneId, String log) {
429         if (!Log.isLoggable(TAG, Log.VERBOSE)) return;
430         final String phoneIdPrefix = "[" + phoneId + "] ";
431         Log.v(TAG, phoneIdPrefix + log);
432         synchronized (mLocalLog) {
433             mLocalLog.log(phoneIdPrefix + log);
434         }
435     }
436 
logInfoLineLocked(int phoneId, String log)437     private void logInfoLineLocked(int phoneId, String log) {
438         final String phoneIdPrefix = "[" + phoneId + "] ";
439         Log.i(TAG, phoneIdPrefix + log);
440         synchronized (mLocalLog) {
441             mLocalLog.log(phoneIdPrefix + log);
442         }
443     }
444 }
445