/* * Copyright (C) 2018 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 android.telephony; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SdkConstant; import android.annotation.SystemApi; import android.annotation.TestApi; import android.content.ComponentName; import android.content.Context; import android.content.ServiceConnection; import android.os.IBinder; import android.os.RemoteException; import android.telephony.mbms.GroupCall; import android.telephony.mbms.GroupCallCallback; import android.telephony.mbms.InternalGroupCallCallback; import android.telephony.mbms.InternalGroupCallSessionCallback; import android.telephony.mbms.MbmsErrors; import android.telephony.mbms.MbmsGroupCallSessionCallback; import android.telephony.mbms.MbmsUtils; import android.telephony.mbms.vendor.IMbmsGroupCallService; import android.util.ArraySet; import android.util.Log; import java.util.List; import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; /** * This class provides functionality for accessing group call functionality over MBMS. */ public class MbmsGroupCallSession implements AutoCloseable { private static final String LOG_TAG = "MbmsGroupCallSession"; /** * Service action which must be handled by the middleware implementing the MBMS group call * interface. * @hide */ @SystemApi @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) public static final String MBMS_GROUP_CALL_SERVICE_ACTION = "android.telephony.action.EmbmsGroupCall"; /** * Metadata key that specifies the component name of the service to bind to for group calls. * @hide */ @TestApi public static final String MBMS_GROUP_CALL_SERVICE_OVERRIDE_METADATA = "mbms-group-call-service-override"; private static AtomicBoolean sIsInitialized = new AtomicBoolean(false); private AtomicReference mService = new AtomicReference<>(null); private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() { @Override public void binderDied() { sIsInitialized.set(false); mInternalCallback.onError(MbmsErrors.ERROR_MIDDLEWARE_LOST, "Received death notification"); } }; private InternalGroupCallSessionCallback mInternalCallback; private ServiceConnection mServiceConnection; private Set mKnownActiveGroupCalls = new ArraySet<>(); private final Context mContext; private int mSubscriptionId; /** @hide */ private MbmsGroupCallSession(Context context, Executor executor, int subscriptionId, MbmsGroupCallSessionCallback callback) { mContext = context; mSubscriptionId = subscriptionId; mInternalCallback = new InternalGroupCallSessionCallback(callback, executor); } /** * Create a new {@link MbmsGroupCallSession} using the given subscription ID. * * You may only have one instance of {@link MbmsGroupCallSession} per UID. If you call this * method while there is an active instance of {@link MbmsGroupCallSession} in your process * (in other words, one that has not had {@link #close()} called on it), this method will * throw an {@link IllegalStateException}. If you call this method in a different process * running under the same UID, an error will be indicated via * {@link MbmsGroupCallSessionCallback#onError(int, String)}. * * Note that initialization may fail asynchronously. If you wish to try again after you * receive such an asynchronous error, you must call {@link #close()} on the instance of * {@link MbmsGroupCallSession} that you received before calling this method again. * * @param context The {@link Context} to use. * @param subscriptionId The subscription ID to use. * @param executor The executor on which you wish to execute callbacks. * @param callback A callback object on which you wish to receive results of asynchronous * operations. * @return An instance of {@link MbmsGroupCallSession}, or null if an error occurred. */ public static @Nullable MbmsGroupCallSession create(@NonNull Context context, int subscriptionId, @NonNull Executor executor, final @NonNull MbmsGroupCallSessionCallback callback) { if (!sIsInitialized.compareAndSet(false, true)) { throw new IllegalStateException("Cannot create two instances of MbmsGroupCallSession"); } MbmsGroupCallSession session = new MbmsGroupCallSession(context, executor, subscriptionId, callback); final int result = session.bindAndInitialize(); if (result != MbmsErrors.SUCCESS) { sIsInitialized.set(false); executor.execute(new Runnable() { @Override public void run() { callback.onError(result, null); } }); return null; } return session; } /** * Create a new {@link MbmsGroupCallSession} using the system default data subscription ID. * See {@link #create(Context, int, Executor, MbmsGroupCallSessionCallback)}. */ public static @Nullable MbmsGroupCallSession create(@NonNull Context context, @NonNull Executor executor, @NonNull MbmsGroupCallSessionCallback callback) { return create(context, SubscriptionManager.getDefaultSubscriptionId(), executor, callback); } /** * Terminates this instance. Also terminates * any group calls spawned from this instance as if * {@link GroupCall#close()} had been called on them. After this method returns, * no further callbacks originating from the middleware will be enqueued on the provided * instance of {@link MbmsGroupCallSessionCallback}, but callbacks that have already been * enqueued will still be delivered. * * It is safe to call {@link #create(Context, int, Executor, MbmsGroupCallSessionCallback)} to * obtain another instance of {@link MbmsGroupCallSession} immediately after this method * returns. * * May throw an {@link IllegalStateException} */ public void close() { try { IMbmsGroupCallService groupCallService = mService.get(); if (groupCallService == null || mServiceConnection == null) { // Ignore and return, assume already disposed. return; } groupCallService.dispose(mSubscriptionId); for (GroupCall s : mKnownActiveGroupCalls) { s.getCallback().stop(); } mKnownActiveGroupCalls.clear(); mContext.unbindService(mServiceConnection); } catch (RemoteException e) { // Ignore for now } finally { mService.set(null); sIsInitialized.set(false); mServiceConnection = null; mInternalCallback.stop(); } } /** * Starts the requested group call, reporting status to the indicated callback. * Returns an object used to control that call. * * May throw an {@link IllegalArgumentException} or an {@link IllegalStateException} * * Asynchronous errors through the callback include any of the errors in * {@link MbmsErrors.GeneralErrors}. * * @param tmgi The TMGI, an identifier for the group call you want to join. * @param saiList A list of SAIs for the group call that should be negotiated separately with * the carrier. * @param frequencyList A lost of frequencies for the group call that should be negotiated * separately with the carrier. * @param executor The executor on which you wish to execute callbacks for this stream. * @param callback The callback that you want to receive information about the call on. * @return An instance of {@link GroupCall} through which the call can be controlled. * May be {@code null} if an error occurred. */ public @Nullable GroupCall startGroupCall(long tmgi, @NonNull List saiList, @NonNull List frequencyList, @NonNull Executor executor, @NonNull GroupCallCallback callback) { IMbmsGroupCallService groupCallService = mService.get(); if (groupCallService == null) { throw new IllegalStateException("Middleware not yet bound"); } InternalGroupCallCallback serviceCallback = new InternalGroupCallCallback( callback, executor); GroupCall serviceForApp = new GroupCall(mSubscriptionId, groupCallService, this, tmgi, serviceCallback); mKnownActiveGroupCalls.add(serviceForApp); try { int returnCode = groupCallService.startGroupCall( mSubscriptionId, tmgi, saiList, frequencyList, serviceCallback); if (returnCode == MbmsErrors.UNKNOWN) { // Unbind and throw an obvious error close(); throw new IllegalStateException("Middleware must not return an unknown error code"); } if (returnCode != MbmsErrors.SUCCESS) { mInternalCallback.onError(returnCode, null); return null; } } catch (RemoteException e) { Log.w(LOG_TAG, "Remote process died"); mService.set(null); sIsInitialized.set(false); mInternalCallback.onError(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); return null; } return serviceForApp; } /** @hide */ public void onGroupCallStopped(GroupCall service) { mKnownActiveGroupCalls.remove(service); } private int bindAndInitialize() { mServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { IMbmsGroupCallService groupCallService = IMbmsGroupCallService.Stub.asInterface(service); int result; try { result = groupCallService.initialize(mInternalCallback, mSubscriptionId); } catch (RemoteException e) { Log.e(LOG_TAG, "Service died before initialization"); mInternalCallback.onError( MbmsErrors.InitializationErrors.ERROR_UNABLE_TO_INITIALIZE, e.toString()); sIsInitialized.set(false); return; } catch (RuntimeException e) { Log.e(LOG_TAG, "Runtime exception during initialization"); mInternalCallback.onError( MbmsErrors.InitializationErrors.ERROR_UNABLE_TO_INITIALIZE, e.toString()); sIsInitialized.set(false); return; } if (result == MbmsErrors.UNKNOWN) { // Unbind and throw an obvious error close(); throw new IllegalStateException("Middleware must not return" + " an unknown error code"); } if (result != MbmsErrors.SUCCESS) { mInternalCallback.onError(result, "Error returned during initialization"); sIsInitialized.set(false); return; } try { groupCallService.asBinder().linkToDeath(mDeathRecipient, 0); } catch (RemoteException e) { mInternalCallback.onError(MbmsErrors.ERROR_MIDDLEWARE_LOST, "Middleware lost during initialization"); sIsInitialized.set(false); return; } mService.set(groupCallService); } @Override public void onServiceDisconnected(ComponentName name) { sIsInitialized.set(false); mService.set(null); } @Override public void onNullBinding(ComponentName name) { Log.w(LOG_TAG, "bindAndInitialize: Remote service returned null"); mInternalCallback.onError(MbmsErrors.ERROR_MIDDLEWARE_LOST, "Middleware service binding returned null"); sIsInitialized.set(false); mService.set(null); mContext.unbindService(this); } }; return MbmsUtils.startBinding(mContext, MBMS_GROUP_CALL_SERVICE_ACTION, mServiceConnection); } }