|  | /* | 
|  | * 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<IMbmsGroupCallService> 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 Set<GroupCall> 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) { | 
|  | // Ignore and return, assume already disposed. | 
|  | return; | 
|  | } | 
|  | groupCallService.dispose(mSubscriptionId); | 
|  | for (GroupCall s : mKnownActiveGroupCalls) { | 
|  | s.getCallback().stop(); | 
|  | } | 
|  | mKnownActiveGroupCalls.clear(); | 
|  | } catch (RemoteException e) { | 
|  | // Ignore for now | 
|  | } finally { | 
|  | mService.set(null); | 
|  | sIsInitialized.set(false); | 
|  | 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<Integer> saiList, | 
|  | @NonNull List<Integer> 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() { | 
|  | return MbmsUtils.startBinding(mContext, MBMS_GROUP_CALL_SERVICE_ACTION, | 
|  | 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); | 
|  | } | 
|  | }); | 
|  | } | 
|  | } |