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)62 public void notifyFeatureCreatedOrRemoved(ImsFeatureContainer connector) { 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); 77 } catch (RemoteException e) { 78 // This listener will eventually be caught and removed during stale checks. 79 } 80 }); 81 } 82 } 83 notifyStateChanged(int state)84 public void notifyStateChanged(int state) { 85 mExecutor.execute(() -> { 86 try { 87 mCallback.imsStatusChanged(state); 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 final @ImsFeature.FeatureType int imsFeatureType; 136 private final List<ListenerContainer> mListeners = new ArrayList<>(); 137 private ImsFeatureContainer mFeatureContainer; 138 private final Object mLock = new Object(); 139 140 UpdateMapper(int pId, @ImsFeature.FeatureType int t)141 public UpdateMapper(int pId, @ImsFeature.FeatureType int t) { 142 phoneId = pId; 143 imsFeatureType = t; 144 } 145 addFeatureContainer(ImsFeatureContainer c)146 public void addFeatureContainer(ImsFeatureContainer c) { 147 List<ListenerContainer> listeners; 148 synchronized (mLock) { 149 if (Objects.equals(c, mFeatureContainer)) return; 150 mFeatureContainer = c; 151 listeners = copyListenerList(mListeners); 152 } 153 listeners.forEach(l -> l.notifyFeatureCreatedOrRemoved(mFeatureContainer)); 154 } 155 removeFeatureContainer()156 public ImsFeatureContainer removeFeatureContainer() { 157 ImsFeatureContainer oldContainer; 158 List<ListenerContainer> listeners; 159 synchronized (mLock) { 160 if (mFeatureContainer == null) return null; 161 oldContainer = mFeatureContainer; 162 mFeatureContainer = null; 163 listeners = copyListenerList(mListeners); 164 } 165 listeners.forEach(l -> l.notifyFeatureCreatedOrRemoved(mFeatureContainer)); 166 return oldContainer; 167 } 168 getFeatureContainer()169 public ImsFeatureContainer getFeatureContainer() { 170 synchronized(mLock) { 171 return mFeatureContainer; 172 } 173 } 174 addListener(ListenerContainer c)175 public void addListener(ListenerContainer c) { 176 ImsFeatureContainer featureContainer; 177 synchronized (mLock) { 178 removeStaleListeners(); 179 if (mListeners.contains(c)) { 180 return; 181 } 182 featureContainer = mFeatureContainer; 183 mListeners.add(c); 184 } 185 // Do not call back until the feature container has been set. 186 if (featureContainer != null) { 187 c.notifyFeatureCreatedOrRemoved(featureContainer); 188 } 189 } 190 removeListener(IImsServiceFeatureCallback callback)191 public void removeListener(IImsServiceFeatureCallback callback) { 192 synchronized (mLock) { 193 removeStaleListeners(); 194 List<ListenerContainer> oldListeners = mListeners.stream() 195 .filter((c) -> Objects.equals(c.mCallback, callback)) 196 .collect(Collectors.toList()); 197 mListeners.removeAll(oldListeners); 198 } 199 } 200 notifyStateUpdated(int newState)201 public void notifyStateUpdated(int newState) { 202 ImsFeatureContainer featureContainer; 203 List<ListenerContainer> listeners; 204 synchronized (mLock) { 205 removeStaleListeners(); 206 featureContainer = mFeatureContainer; 207 listeners = copyListenerList(mListeners); 208 if (mFeatureContainer != null) { 209 if (mFeatureContainer.getState() != newState) { 210 mFeatureContainer.setState(newState); 211 } 212 } 213 } 214 // Only update if the feature container is set. 215 if (featureContainer != null) { 216 listeners.forEach(l -> l.notifyStateChanged(newState)); 217 } 218 } 219 notifyUpdateCapabilities(long caps)220 public void notifyUpdateCapabilities(long caps) { 221 ImsFeatureContainer featureContainer; 222 List<ListenerContainer> listeners; 223 synchronized (mLock) { 224 removeStaleListeners(); 225 featureContainer = mFeatureContainer; 226 listeners = copyListenerList(mListeners); 227 if (mFeatureContainer != null) { 228 if (mFeatureContainer.getCapabilities() != caps) { 229 mFeatureContainer.setCapabilities(caps); 230 } 231 } 232 } 233 // Only update if the feature container is set. 234 if (featureContainer != null) { 235 listeners.forEach(l -> l.notifyUpdateCapabilties(caps)); 236 } 237 } 238 239 @GuardedBy("mLock") removeStaleListeners()240 private void removeStaleListeners() { 241 List<ListenerContainer> staleListeners = mListeners.stream().filter( 242 ListenerContainer::isStale) 243 .collect(Collectors.toList()); 244 mListeners.removeAll(staleListeners); 245 } 246 247 @Override toString()248 public String toString() { 249 synchronized (mLock) { 250 return "UpdateMapper{" + "phoneId=" + phoneId + ", type=" 251 + ImsFeature.FEATURE_LOG_MAP.get(imsFeatureType) + ", container=" 252 + mFeatureContainer + '}'; 253 } 254 } 255 256 copyListenerList(List<ListenerContainer> listeners)257 private List<ListenerContainer> copyListenerList(List<ListenerContainer> listeners) { 258 return new ArrayList<>(listeners); 259 } 260 } 261 262 private final List<UpdateMapper> mFeatures = new ArrayList<>(); 263 private final LocalLog mLocalLog = new LocalLog(50 /*lines*/); 264 ImsFeatureBinderRepository()265 public ImsFeatureBinderRepository() { 266 logInfoLineLocked(-1, "FeatureConnectionRepository - created"); 267 } 268 269 /** 270 * Get the Container for a specific ImsFeature now if it exists. 271 * 272 * @param phoneId The phone ID that the connection is related to. 273 * @param type The ImsFeature type to get the cotnainr for (MMTEL/RCS). 274 * @return The Container containing the requested ImsFeature if it exists. 275 */ getIfExists( int phoneId, @ImsFeature.FeatureType int type)276 public Optional<ImsFeatureContainer> getIfExists( 277 int phoneId, @ImsFeature.FeatureType int type) { 278 if (type < 0 || type >= ImsFeature.FEATURE_MAX) { 279 throw new IllegalArgumentException("Incorrect feature type"); 280 } 281 UpdateMapper m; 282 m = getUpdateMapper(phoneId, type); 283 ImsFeatureContainer c = m.getFeatureContainer(); 284 logVerboseLineLocked(phoneId, "getIfExists, type= " + ImsFeature.FEATURE_LOG_MAP.get(type) 285 + ", result= " + c); 286 return Optional.ofNullable(c); 287 } 288 289 /** 290 * Register a callback that will receive updates when the requested ImsFeature type becomes 291 * available or unavailable for the specified phone ID. 292 * <p> 293 * This callback will not be called the first time until there is a valid ImsFeature. 294 * @param phoneId The phone ID that the connection will be related to. 295 * @param type The ImsFeature type to get (MMTEL/RCS). 296 * @param callback The callback that will be used to notify when the callback is 297 * available/unavailable. 298 * @param executor The executor that the callback will be run on. 299 */ registerForConnectionUpdates(int phoneId, @ImsFeature.FeatureType int type, @NonNull IImsServiceFeatureCallback callback, @NonNull Executor executor)300 public void registerForConnectionUpdates(int phoneId, 301 @ImsFeature.FeatureType int type, @NonNull IImsServiceFeatureCallback callback, 302 @NonNull Executor executor) { 303 if (type < 0 || type >= ImsFeature.FEATURE_MAX || callback == null || executor == null) { 304 throw new IllegalArgumentException("One or more invalid arguments have been passed in"); 305 } 306 ListenerContainer container = new ListenerContainer(callback, executor); 307 logInfoLineLocked(phoneId, "registerForConnectionUpdates, type= " 308 + ImsFeature.FEATURE_LOG_MAP.get(type) +", conn= " + container); 309 UpdateMapper m = getUpdateMapper(phoneId, type); 310 m.addListener(container); 311 } 312 313 /** 314 * Unregister for updates on a previously registered callback. 315 * 316 * @param callback The callback to unregister. 317 */ unregisterForConnectionUpdates(@onNull IImsServiceFeatureCallback callback)318 public void unregisterForConnectionUpdates(@NonNull IImsServiceFeatureCallback callback) { 319 if (callback == null) { 320 throw new IllegalArgumentException("this method does not accept null arguments"); 321 } 322 logInfoLineLocked(-1, "unregisterForConnectionUpdates, callback= " + callback); 323 synchronized (mFeatures) { 324 for (UpdateMapper m : mFeatures) { 325 // warning: no callbacks should be called while holding locks 326 m.removeListener(callback); 327 } 328 } 329 } 330 331 /** 332 * Add a Container containing the IBinder interfaces associated with a specific ImsFeature type 333 * (MMTEL/RCS). If one already exists, it will be replaced. This will notify listeners of the 334 * change. 335 * @param phoneId The phone ID associated with this Container. 336 * @param type The ImsFeature type to get (MMTEL/RCS). 337 * @param newConnection A Container containing the IBinder interface connections associated with 338 * the ImsFeature type. 339 */ addConnection(int phoneId, @ImsFeature.FeatureType int type, @Nullable ImsFeatureContainer newConnection)340 public void addConnection(int phoneId, @ImsFeature.FeatureType int type, 341 @Nullable ImsFeatureContainer newConnection) { 342 if (type < 0 || type >= ImsFeature.FEATURE_MAX) { 343 throw new IllegalArgumentException("The type must valid"); 344 } 345 logInfoLineLocked(phoneId, "addConnection, type=" + ImsFeature.FEATURE_LOG_MAP.get(type) 346 + ", conn=" + newConnection); 347 UpdateMapper m = getUpdateMapper(phoneId, type); 348 m.addFeatureContainer(newConnection); 349 } 350 351 /** 352 * Remove the IBinder Container associated with a specific ImsService type. Listeners will be 353 * notified of this change. 354 * @param phoneId The phone ID associated with this connection. 355 * @param type The ImsFeature type to get (MMTEL/RCS). 356 */ removeConnection(int phoneId, @ImsFeature.FeatureType int type)357 public ImsFeatureContainer removeConnection(int phoneId, @ImsFeature.FeatureType int type) { 358 if (type < 0 || type >= ImsFeature.FEATURE_MAX) { 359 throw new IllegalArgumentException("The type must valid"); 360 } 361 logInfoLineLocked(phoneId, "removeConnection, type=" 362 + ImsFeature.FEATURE_LOG_MAP.get(type)); 363 UpdateMapper m = getUpdateMapper(phoneId, type); 364 return m.removeFeatureContainer(); 365 } 366 367 /** 368 * Notify listeners that the state of a specific ImsFeature that this repository is 369 * tracking has changed. Listeners will be notified of the change in the ImsFeature's state. 370 * @param phoneId The phoneId of the feature that has changed state. 371 * @param type The ImsFeature type to get (MMTEL/RCS). 372 * @param state The new state of the ImsFeature 373 */ notifyFeatureStateChanged(int phoneId, @ImsFeature.FeatureType int type, @ImsFeature.ImsState int state)374 public void notifyFeatureStateChanged(int phoneId, @ImsFeature.FeatureType int type, 375 @ImsFeature.ImsState int state) { 376 logInfoLineLocked(phoneId, "notifyFeatureStateChanged, type=" 377 + ImsFeature.FEATURE_LOG_MAP.get(type) + ", state=" 378 + ImsFeature.STATE_LOG_MAP.get(state)); 379 UpdateMapper m = getUpdateMapper(phoneId, type); 380 m.notifyStateUpdated(state); 381 } 382 383 /** 384 * Notify listeners that the capabilities of a specific ImsFeature that this repository is 385 * tracking has changed. Listeners will be notified of the change in the ImsFeature's 386 * capabilities. 387 * @param phoneId The phoneId of the feature that has changed capabilities. 388 * @param type The ImsFeature type to get (MMTEL/RCS). 389 * @param capabilities The new capabilities of the ImsFeature 390 */ notifyFeatureCapabilitiesChanged(int phoneId, @ImsFeature.FeatureType int type, @ImsService.ImsServiceCapability long capabilities)391 public void notifyFeatureCapabilitiesChanged(int phoneId, @ImsFeature.FeatureType int type, 392 @ImsService.ImsServiceCapability long capabilities) { 393 logInfoLineLocked(phoneId, "notifyFeatureCapabilitiesChanged, type=" 394 + ImsFeature.FEATURE_LOG_MAP.get(type) + ", caps=" 395 + ImsService.getCapabilitiesString(capabilities)); 396 UpdateMapper m = getUpdateMapper(phoneId, type); 397 m.notifyUpdateCapabilities(capabilities); 398 } 399 400 /** 401 * Prints the dump of log events that have occurred on this repository. 402 */ dump(PrintWriter printWriter)403 public void dump(PrintWriter printWriter) { 404 synchronized (mLocalLog) { 405 mLocalLog.dump(printWriter); 406 } 407 } 408 getUpdateMapper(int phoneId, int type)409 private UpdateMapper getUpdateMapper(int phoneId, int type) { 410 synchronized (mFeatures) { 411 UpdateMapper mapper = mFeatures.stream() 412 .filter((c) -> ((c.phoneId == phoneId) && (c.imsFeatureType == type))) 413 .findFirst().orElse(null); 414 if (mapper == null) { 415 mapper = new UpdateMapper(phoneId, type); 416 mFeatures.add(mapper); 417 } 418 return mapper; 419 } 420 } 421 logVerboseLineLocked(int phoneId, String log)422 private void logVerboseLineLocked(int phoneId, String log) { 423 if (!Log.isLoggable(TAG, Log.VERBOSE)) return; 424 final String phoneIdPrefix = "[" + phoneId + "] "; 425 Log.v(TAG, phoneIdPrefix + log); 426 synchronized (mLocalLog) { 427 mLocalLog.log(phoneIdPrefix + log); 428 } 429 } 430 logInfoLineLocked(int phoneId, String log)431 private void logInfoLineLocked(int phoneId, String log) { 432 final String phoneIdPrefix = "[" + phoneId + "] "; 433 Log.i(TAG, phoneIdPrefix + log); 434 synchronized (mLocalLog) { 435 mLocalLog.log(phoneIdPrefix + log); 436 } 437 } 438 } 439