blob: f1be31fa5477ce1f882b0736ac874b5ab01824e1 [file] [log] [blame]
/*
* 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);
}
});
}
}