316 lines
13 KiB
Java
316 lines
13 KiB
Java
/*
|
|
* 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 ServiceConnection mServiceConnection;
|
|
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 || 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<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() {
|
|
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);
|
|
}
|
|
}
|