| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.ims; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.os.RemoteException; |
| import android.telephony.ims.ImsService; |
| import android.telephony.ims.feature.ImsFeature; |
| import android.util.LocalLog; |
| import android.util.Log; |
| |
| import com.android.ims.internal.IImsServiceFeatureCallback; |
| import com.android.internal.annotations.GuardedBy; |
| |
| import java.io.PrintWriter; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.Optional; |
| import java.util.concurrent.Executor; |
| import java.util.stream.Collectors; |
| |
| /** |
| * A repository of ImsFeature connections made available by an ImsService once it has been |
| * successfully bound. |
| * |
| * Provides the ability for listeners to register callbacks and the repository notify registered |
| * listeners when a connection has been created/removed for a specific connection type. |
| */ |
| public class ImsFeatureBinderRepository { |
| |
| private static final String TAG = "ImsFeatureBinderRepo"; |
| |
| /** |
| * Internal class representing a listener that is listening for changes to specific |
| * ImsFeature instances. |
| */ |
| private static class ListenerContainer { |
| private final IImsServiceFeatureCallback mCallback; |
| private final Executor mExecutor; |
| |
| public ListenerContainer(@NonNull IImsServiceFeatureCallback c, @NonNull Executor e) { |
| mCallback = c; |
| mExecutor = e; |
| } |
| |
| public void notifyFeatureCreatedOrRemoved(ImsFeatureContainer connector, int subId) { |
| if (connector == null) { |
| mExecutor.execute(() -> { |
| try { |
| mCallback.imsFeatureRemoved( |
| FeatureConnector.UNAVAILABLE_REASON_DISCONNECTED); |
| } catch (RemoteException e) { |
| // This listener will eventually be caught and removed during stale checks. |
| } |
| }); |
| } |
| else { |
| mExecutor.execute(() -> { |
| try { |
| mCallback.imsFeatureCreated(connector, subId); |
| } catch (RemoteException e) { |
| // This listener will eventually be caught and removed during stale checks. |
| } |
| }); |
| } |
| } |
| |
| public void notifyStateChanged(int state, int subId) { |
| mExecutor.execute(() -> { |
| try { |
| mCallback.imsStatusChanged(state, subId); |
| } catch (RemoteException e) { |
| // This listener will eventually be caught and removed during stale checks. |
| } |
| }); |
| } |
| |
| public void notifyUpdateCapabilties(long caps) { |
| mExecutor.execute(() -> { |
| try { |
| mCallback.updateCapabilities(caps); |
| } catch (RemoteException e) { |
| // This listener will eventually be caught and removed during stale checks. |
| } |
| }); |
| } |
| |
| public boolean isStale() { |
| return !mCallback.asBinder().isBinderAlive(); |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) return true; |
| if (o == null || getClass() != o.getClass()) return false; |
| ListenerContainer that = (ListenerContainer) o; |
| // Do not count executor for equality. |
| return mCallback.equals(that.mCallback); |
| } |
| |
| @Override |
| public int hashCode() { |
| // Do not use executor for hash. |
| return Objects.hash(mCallback); |
| } |
| |
| @Override |
| public String toString() { |
| return "ListenerContainer{" + "cb=" + mCallback + '}'; |
| } |
| } |
| |
| /** |
| * Contains the mapping from ImsFeature type (MMTEL/RCS) to List of listeners listening for |
| * updates to the ImsFeature instance contained in the ImsFeatureContainer. |
| */ |
| private static final class UpdateMapper { |
| public final int phoneId; |
| public int subId; |
| public final @ImsFeature.FeatureType int imsFeatureType; |
| private final List<ListenerContainer> mListeners = new ArrayList<>(); |
| private ImsFeatureContainer mFeatureContainer; |
| private final Object mLock = new Object(); |
| |
| |
| public UpdateMapper(int pId, @ImsFeature.FeatureType int t) { |
| phoneId = pId; |
| imsFeatureType = t; |
| } |
| |
| public void addFeatureContainer(ImsFeatureContainer c) { |
| List<ListenerContainer> listeners; |
| synchronized (mLock) { |
| if (Objects.equals(c, mFeatureContainer)) return; |
| mFeatureContainer = c; |
| listeners = copyListenerList(mListeners); |
| } |
| listeners.forEach(l -> l.notifyFeatureCreatedOrRemoved(mFeatureContainer, subId)); |
| } |
| |
| public ImsFeatureContainer removeFeatureContainer() { |
| ImsFeatureContainer oldContainer; |
| List<ListenerContainer> listeners; |
| synchronized (mLock) { |
| if (mFeatureContainer == null) return null; |
| oldContainer = mFeatureContainer; |
| mFeatureContainer = null; |
| listeners = copyListenerList(mListeners); |
| } |
| listeners.forEach(l -> l.notifyFeatureCreatedOrRemoved(mFeatureContainer, subId)); |
| return oldContainer; |
| } |
| |
| public ImsFeatureContainer getFeatureContainer() { |
| synchronized(mLock) { |
| return mFeatureContainer; |
| } |
| } |
| |
| public void addListener(ListenerContainer c) { |
| ImsFeatureContainer featureContainer; |
| synchronized (mLock) { |
| removeStaleListeners(); |
| if (mListeners.contains(c)) { |
| return; |
| } |
| featureContainer = mFeatureContainer; |
| mListeners.add(c); |
| } |
| // Do not call back until the feature container has been set. |
| if (featureContainer != null) { |
| c.notifyFeatureCreatedOrRemoved(featureContainer, subId); |
| } |
| } |
| |
| public void removeListener(IImsServiceFeatureCallback callback) { |
| synchronized (mLock) { |
| removeStaleListeners(); |
| List<ListenerContainer> oldListeners = mListeners.stream() |
| .filter((c) -> Objects.equals(c.mCallback, callback)) |
| .collect(Collectors.toList()); |
| mListeners.removeAll(oldListeners); |
| } |
| } |
| |
| public void notifyStateUpdated(int newState) { |
| ImsFeatureContainer featureContainer; |
| List<ListenerContainer> listeners; |
| synchronized (mLock) { |
| removeStaleListeners(); |
| featureContainer = mFeatureContainer; |
| listeners = copyListenerList(mListeners); |
| if (mFeatureContainer != null) { |
| if (mFeatureContainer.getState() != newState) { |
| mFeatureContainer.setState(newState); |
| } |
| } |
| } |
| // Only update if the feature container is set. |
| if (featureContainer != null) { |
| listeners.forEach(l -> l.notifyStateChanged(newState, subId)); |
| } |
| } |
| |
| public void notifyUpdateCapabilities(long caps) { |
| ImsFeatureContainer featureContainer; |
| List<ListenerContainer> listeners; |
| synchronized (mLock) { |
| removeStaleListeners(); |
| featureContainer = mFeatureContainer; |
| listeners = copyListenerList(mListeners); |
| if (mFeatureContainer != null) { |
| if (mFeatureContainer.getCapabilities() != caps) { |
| mFeatureContainer.setCapabilities(caps); |
| } |
| } |
| } |
| // Only update if the feature container is set. |
| if (featureContainer != null) { |
| listeners.forEach(l -> l.notifyUpdateCapabilties(caps)); |
| } |
| } |
| |
| public void updateSubId(int newSubId) { |
| subId = newSubId; |
| } |
| |
| @GuardedBy("mLock") |
| private void removeStaleListeners() { |
| List<ListenerContainer> staleListeners = mListeners.stream().filter( |
| ListenerContainer::isStale) |
| .collect(Collectors.toList()); |
| mListeners.removeAll(staleListeners); |
| } |
| |
| @Override |
| public String toString() { |
| synchronized (mLock) { |
| return "UpdateMapper{" + "phoneId=" + phoneId + ", type=" |
| + ImsFeature.FEATURE_LOG_MAP.get(imsFeatureType) + ", container=" |
| + mFeatureContainer + '}'; |
| } |
| } |
| |
| |
| private List<ListenerContainer> copyListenerList(List<ListenerContainer> listeners) { |
| return new ArrayList<>(listeners); |
| } |
| } |
| |
| private final List<UpdateMapper> mFeatures = new ArrayList<>(); |
| private final LocalLog mLocalLog = new LocalLog(50 /*lines*/); |
| |
| public ImsFeatureBinderRepository() { |
| logInfoLineLocked(-1, "FeatureConnectionRepository - created"); |
| } |
| |
| /** |
| * Get the Container for a specific ImsFeature now if it exists. |
| * |
| * @param phoneId The phone ID that the connection is related to. |
| * @param type The ImsFeature type to get the cotnainr for (MMTEL/RCS). |
| * @return The Container containing the requested ImsFeature if it exists. |
| */ |
| public Optional<ImsFeatureContainer> getIfExists( |
| int phoneId, @ImsFeature.FeatureType int type) { |
| if (type < 0 || type >= ImsFeature.FEATURE_MAX) { |
| throw new IllegalArgumentException("Incorrect feature type"); |
| } |
| UpdateMapper m; |
| m = getUpdateMapper(phoneId, type); |
| ImsFeatureContainer c = m.getFeatureContainer(); |
| logVerboseLineLocked(phoneId, "getIfExists, type= " + ImsFeature.FEATURE_LOG_MAP.get(type) |
| + ", result= " + c); |
| return Optional.ofNullable(c); |
| } |
| |
| /** |
| * Register a callback that will receive updates when the requested ImsFeature type becomes |
| * available or unavailable for the specified phone ID. |
| * <p> |
| * This callback will not be called the first time until there is a valid ImsFeature. |
| * @param phoneId The phone ID that the connection will be related to. |
| * @param type The ImsFeature type to get (MMTEL/RCS). |
| * @param callback The callback that will be used to notify when the callback is |
| * available/unavailable. |
| * @param executor The executor that the callback will be run on. |
| */ |
| public void registerForConnectionUpdates(int phoneId, |
| @ImsFeature.FeatureType int type, @NonNull IImsServiceFeatureCallback callback, |
| @NonNull Executor executor) { |
| if (type < 0 || type >= ImsFeature.FEATURE_MAX || callback == null || executor == null) { |
| throw new IllegalArgumentException("One or more invalid arguments have been passed in"); |
| } |
| ListenerContainer container = new ListenerContainer(callback, executor); |
| logInfoLineLocked(phoneId, "registerForConnectionUpdates, type= " |
| + ImsFeature.FEATURE_LOG_MAP.get(type) +", conn= " + container); |
| UpdateMapper m = getUpdateMapper(phoneId, type); |
| m.addListener(container); |
| } |
| |
| /** |
| * Unregister for updates on a previously registered callback. |
| * |
| * @param callback The callback to unregister. |
| */ |
| public void unregisterForConnectionUpdates(@NonNull IImsServiceFeatureCallback callback) { |
| if (callback == null) { |
| throw new IllegalArgumentException("this method does not accept null arguments"); |
| } |
| logInfoLineLocked(-1, "unregisterForConnectionUpdates, callback= " + callback); |
| synchronized (mFeatures) { |
| for (UpdateMapper m : mFeatures) { |
| // warning: no callbacks should be called while holding locks |
| m.removeListener(callback); |
| } |
| } |
| } |
| |
| /** |
| * Add a Container containing the IBinder interfaces associated with a specific ImsFeature type |
| * (MMTEL/RCS). If one already exists, it will be replaced. This will notify listeners of the |
| * change. |
| * @param phoneId The phone ID associated with this Container. |
| * @param type The ImsFeature type to get (MMTEL/RCS). |
| * @param newConnection A Container containing the IBinder interface connections associated with |
| * the ImsFeature type. |
| */ |
| public void addConnection(int phoneId, int subId, @ImsFeature.FeatureType int type, |
| @Nullable ImsFeatureContainer newConnection) { |
| if (type < 0 || type >= ImsFeature.FEATURE_MAX) { |
| throw new IllegalArgumentException("The type must valid"); |
| } |
| logInfoLineLocked(phoneId, "addConnection, subId=" + subId + ", type=" |
| + ImsFeature.FEATURE_LOG_MAP.get(type) + ", conn=" + newConnection); |
| UpdateMapper m = getUpdateMapper(phoneId, type); |
| m.updateSubId(subId); |
| m.addFeatureContainer(newConnection); |
| } |
| |
| /** |
| * Remove the IBinder Container associated with a specific ImsService type. Listeners will be |
| * notified of this change. |
| * @param phoneId The phone ID associated with this connection. |
| * @param type The ImsFeature type to get (MMTEL/RCS). |
| */ |
| public ImsFeatureContainer removeConnection(int phoneId, @ImsFeature.FeatureType int type) { |
| if (type < 0 || type >= ImsFeature.FEATURE_MAX) { |
| throw new IllegalArgumentException("The type must valid"); |
| } |
| logInfoLineLocked(phoneId, "removeConnection, type=" |
| + ImsFeature.FEATURE_LOG_MAP.get(type)); |
| UpdateMapper m = getUpdateMapper(phoneId, type); |
| return m.removeFeatureContainer(); |
| } |
| |
| /** |
| * Notify listeners that the state of a specific ImsFeature that this repository is |
| * tracking has changed. Listeners will be notified of the change in the ImsFeature's state. |
| * @param phoneId The phoneId of the feature that has changed state. |
| * @param type The ImsFeature type to get (MMTEL/RCS). |
| * @param state The new state of the ImsFeature |
| */ |
| public void notifyFeatureStateChanged(int phoneId, @ImsFeature.FeatureType int type, |
| @ImsFeature.ImsState int state) { |
| logInfoLineLocked(phoneId, "notifyFeatureStateChanged, type=" |
| + ImsFeature.FEATURE_LOG_MAP.get(type) + ", state=" |
| + ImsFeature.STATE_LOG_MAP.get(state)); |
| UpdateMapper m = getUpdateMapper(phoneId, type); |
| m.notifyStateUpdated(state); |
| } |
| |
| /** |
| * Notify listeners that the capabilities of a specific ImsFeature that this repository is |
| * tracking has changed. Listeners will be notified of the change in the ImsFeature's |
| * capabilities. |
| * @param phoneId The phoneId of the feature that has changed capabilities. |
| * @param type The ImsFeature type to get (MMTEL/RCS). |
| * @param capabilities The new capabilities of the ImsFeature |
| */ |
| public void notifyFeatureCapabilitiesChanged(int phoneId, @ImsFeature.FeatureType int type, |
| @ImsService.ImsServiceCapability long capabilities) { |
| logInfoLineLocked(phoneId, "notifyFeatureCapabilitiesChanged, type=" |
| + ImsFeature.FEATURE_LOG_MAP.get(type) + ", caps=" |
| + ImsService.getCapabilitiesString(capabilities)); |
| UpdateMapper m = getUpdateMapper(phoneId, type); |
| m.notifyUpdateCapabilities(capabilities); |
| } |
| |
| /** |
| * Prints the dump of log events that have occurred on this repository. |
| */ |
| public void dump(PrintWriter printWriter) { |
| synchronized (mLocalLog) { |
| mLocalLog.dump(printWriter); |
| } |
| } |
| |
| private UpdateMapper getUpdateMapper(int phoneId, int type) { |
| synchronized (mFeatures) { |
| UpdateMapper mapper = mFeatures.stream() |
| .filter((c) -> ((c.phoneId == phoneId) && (c.imsFeatureType == type))) |
| .findFirst().orElse(null); |
| if (mapper == null) { |
| mapper = new UpdateMapper(phoneId, type); |
| mFeatures.add(mapper); |
| } |
| return mapper; |
| } |
| } |
| |
| private void logVerboseLineLocked(int phoneId, String log) { |
| if (!Log.isLoggable(TAG, Log.VERBOSE)) return; |
| final String phoneIdPrefix = "[" + phoneId + "] "; |
| Log.v(TAG, phoneIdPrefix + log); |
| synchronized (mLocalLog) { |
| mLocalLog.log(phoneIdPrefix + log); |
| } |
| } |
| |
| private void logInfoLineLocked(int phoneId, String log) { |
| final String phoneIdPrefix = "[" + phoneId + "] "; |
| Log.i(TAG, phoneIdPrefix + log); |
| synchronized (mLocalLog) { |
| mLocalLog.log(phoneIdPrefix + log); |
| } |
| } |
| } |