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