1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.telephony; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.annotation.SdkConstant; 22 import android.annotation.SystemApi; 23 import android.annotation.TestApi; 24 import android.content.ComponentName; 25 import android.content.Context; 26 import android.content.ServiceConnection; 27 import android.os.IBinder; 28 import android.os.RemoteException; 29 import android.telephony.mbms.GroupCall; 30 import android.telephony.mbms.GroupCallCallback; 31 import android.telephony.mbms.InternalGroupCallCallback; 32 import android.telephony.mbms.InternalGroupCallSessionCallback; 33 import android.telephony.mbms.MbmsErrors; 34 import android.telephony.mbms.MbmsGroupCallSessionCallback; 35 import android.telephony.mbms.MbmsUtils; 36 import android.telephony.mbms.vendor.IMbmsGroupCallService; 37 import android.util.ArraySet; 38 import android.util.Log; 39 40 import java.util.List; 41 import java.util.Set; 42 import java.util.concurrent.Executor; 43 import java.util.concurrent.atomic.AtomicBoolean; 44 import java.util.concurrent.atomic.AtomicReference; 45 46 /** 47 * This class provides functionality for accessing group call functionality over MBMS. 48 */ 49 public class MbmsGroupCallSession implements AutoCloseable { 50 private static final String LOG_TAG = "MbmsGroupCallSession"; 51 52 /** 53 * Service action which must be handled by the middleware implementing the MBMS group call 54 * interface. 55 * @hide 56 */ 57 @SystemApi 58 @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) 59 public static final String MBMS_GROUP_CALL_SERVICE_ACTION = 60 "android.telephony.action.EmbmsGroupCall"; 61 62 /** 63 * Metadata key that specifies the component name of the service to bind to for group calls. 64 * @hide 65 */ 66 @TestApi 67 public static final String MBMS_GROUP_CALL_SERVICE_OVERRIDE_METADATA = 68 "mbms-group-call-service-override"; 69 70 private static AtomicBoolean sIsInitialized = new AtomicBoolean(false); 71 72 private AtomicReference<IMbmsGroupCallService> mService = new AtomicReference<>(null); 73 private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() { 74 @Override 75 public void binderDied() { 76 sIsInitialized.set(false); 77 mInternalCallback.onError(MbmsErrors.ERROR_MIDDLEWARE_LOST, 78 "Received death notification"); 79 } 80 }; 81 82 private InternalGroupCallSessionCallback mInternalCallback; 83 private Set<GroupCall> mKnownActiveGroupCalls = new ArraySet<>(); 84 85 private final Context mContext; 86 private int mSubscriptionId; 87 88 /** @hide */ MbmsGroupCallSession(Context context, Executor executor, int subscriptionId, MbmsGroupCallSessionCallback callback)89 private MbmsGroupCallSession(Context context, Executor executor, int subscriptionId, 90 MbmsGroupCallSessionCallback callback) { 91 mContext = context; 92 mSubscriptionId = subscriptionId; 93 mInternalCallback = new InternalGroupCallSessionCallback(callback, executor); 94 } 95 96 /** 97 * Create a new {@link MbmsGroupCallSession} using the given subscription ID. 98 * 99 * You may only have one instance of {@link MbmsGroupCallSession} per UID. If you call this 100 * method while there is an active instance of {@link MbmsGroupCallSession} in your process 101 * (in other words, one that has not had {@link #close()} called on it), this method will 102 * throw an {@link IllegalStateException}. If you call this method in a different process 103 * running under the same UID, an error will be indicated via 104 * {@link MbmsGroupCallSessionCallback#onError(int, String)}. 105 * 106 * Note that initialization may fail asynchronously. If you wish to try again after you 107 * receive such an asynchronous error, you must call {@link #close()} on the instance of 108 * {@link MbmsGroupCallSession} that you received before calling this method again. 109 * 110 * @param context The {@link Context} to use. 111 * @param subscriptionId The subscription ID to use. 112 * @param executor The executor on which you wish to execute callbacks. 113 * @param callback A callback object on which you wish to receive results of asynchronous 114 * operations. 115 * @return An instance of {@link MbmsGroupCallSession}, or null if an error occurred. 116 */ create(@onNull Context context, int subscriptionId, @NonNull Executor executor, final @NonNull MbmsGroupCallSessionCallback callback)117 public static @Nullable MbmsGroupCallSession create(@NonNull Context context, 118 int subscriptionId, @NonNull Executor executor, 119 final @NonNull MbmsGroupCallSessionCallback callback) { 120 if (!sIsInitialized.compareAndSet(false, true)) { 121 throw new IllegalStateException("Cannot create two instances of MbmsGroupCallSession"); 122 } 123 MbmsGroupCallSession session = new MbmsGroupCallSession(context, executor, 124 subscriptionId, callback); 125 126 final int result = session.bindAndInitialize(); 127 if (result != MbmsErrors.SUCCESS) { 128 sIsInitialized.set(false); 129 executor.execute(new Runnable() { 130 @Override 131 public void run() { 132 callback.onError(result, null); 133 } 134 }); 135 return null; 136 } 137 return session; 138 } 139 140 /** 141 * Create a new {@link MbmsGroupCallSession} using the system default data subscription ID. 142 * See {@link #create(Context, int, Executor, MbmsGroupCallSessionCallback)}. 143 */ create(@onNull Context context, @NonNull Executor executor, @NonNull MbmsGroupCallSessionCallback callback)144 public static @Nullable MbmsGroupCallSession create(@NonNull Context context, 145 @NonNull Executor executor, @NonNull MbmsGroupCallSessionCallback callback) { 146 return create(context, SubscriptionManager.getDefaultSubscriptionId(), executor, callback); 147 } 148 149 /** 150 * Terminates this instance. Also terminates 151 * any group calls spawned from this instance as if 152 * {@link GroupCall#close()} had been called on them. After this method returns, 153 * no further callbacks originating from the middleware will be enqueued on the provided 154 * instance of {@link MbmsGroupCallSessionCallback}, but callbacks that have already been 155 * enqueued will still be delivered. 156 * 157 * It is safe to call {@link #create(Context, int, Executor, MbmsGroupCallSessionCallback)} to 158 * obtain another instance of {@link MbmsGroupCallSession} immediately after this method 159 * returns. 160 * 161 * May throw an {@link IllegalStateException} 162 */ close()163 public void close() { 164 try { 165 IMbmsGroupCallService groupCallService = mService.get(); 166 if (groupCallService == null) { 167 // Ignore and return, assume already disposed. 168 return; 169 } 170 groupCallService.dispose(mSubscriptionId); 171 for (GroupCall s : mKnownActiveGroupCalls) { 172 s.getCallback().stop(); 173 } 174 mKnownActiveGroupCalls.clear(); 175 } catch (RemoteException e) { 176 // Ignore for now 177 } finally { 178 mService.set(null); 179 sIsInitialized.set(false); 180 mInternalCallback.stop(); 181 } 182 } 183 184 /** 185 * Starts the requested group call, reporting status to the indicated callback. 186 * Returns an object used to control that call. 187 * 188 * May throw an {@link IllegalArgumentException} or an {@link IllegalStateException} 189 * 190 * Asynchronous errors through the callback include any of the errors in 191 * {@link MbmsErrors.GeneralErrors}. 192 * 193 * @param tmgi The TMGI, an identifier for the group call you want to join. 194 * @param saiList A list of SAIs for the group call that should be negotiated separately with 195 * the carrier. 196 * @param frequencyList A lost of frequencies for the group call that should be negotiated 197 * separately with the carrier. 198 * @param executor The executor on which you wish to execute callbacks for this stream. 199 * @param callback The callback that you want to receive information about the call on. 200 * @return An instance of {@link GroupCall} through which the call can be controlled. 201 * May be {@code null} if an error occurred. 202 */ startGroupCall(long tmgi, @NonNull List<Integer> saiList, @NonNull List<Integer> frequencyList, @NonNull Executor executor, @NonNull GroupCallCallback callback)203 public @Nullable GroupCall startGroupCall(long tmgi, @NonNull List<Integer> saiList, 204 @NonNull List<Integer> frequencyList, @NonNull Executor executor, 205 @NonNull GroupCallCallback callback) { 206 IMbmsGroupCallService groupCallService = mService.get(); 207 if (groupCallService == null) { 208 throw new IllegalStateException("Middleware not yet bound"); 209 } 210 211 InternalGroupCallCallback serviceCallback = new InternalGroupCallCallback( 212 callback, executor); 213 214 GroupCall serviceForApp = new GroupCall(mSubscriptionId, 215 groupCallService, this, tmgi, serviceCallback); 216 mKnownActiveGroupCalls.add(serviceForApp); 217 218 try { 219 int returnCode = groupCallService.startGroupCall( 220 mSubscriptionId, tmgi, saiList, frequencyList, serviceCallback); 221 if (returnCode == MbmsErrors.UNKNOWN) { 222 // Unbind and throw an obvious error 223 close(); 224 throw new IllegalStateException("Middleware must not return an unknown error code"); 225 } 226 if (returnCode != MbmsErrors.SUCCESS) { 227 mInternalCallback.onError(returnCode, null); 228 return null; 229 } 230 } catch (RemoteException e) { 231 Log.w(LOG_TAG, "Remote process died"); 232 mService.set(null); 233 sIsInitialized.set(false); 234 mInternalCallback.onError(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); 235 return null; 236 } 237 238 return serviceForApp; 239 } 240 241 /** @hide */ onGroupCallStopped(GroupCall service)242 public void onGroupCallStopped(GroupCall service) { 243 mKnownActiveGroupCalls.remove(service); 244 } 245 bindAndInitialize()246 private int bindAndInitialize() { 247 return MbmsUtils.startBinding(mContext, MBMS_GROUP_CALL_SERVICE_ACTION, 248 new ServiceConnection() { 249 @Override 250 public void onServiceConnected(ComponentName name, IBinder service) { 251 IMbmsGroupCallService groupCallService = 252 IMbmsGroupCallService.Stub.asInterface(service); 253 int result; 254 try { 255 result = groupCallService.initialize(mInternalCallback, 256 mSubscriptionId); 257 } catch (RemoteException e) { 258 Log.e(LOG_TAG, "Service died before initialization"); 259 mInternalCallback.onError( 260 MbmsErrors.InitializationErrors.ERROR_UNABLE_TO_INITIALIZE, 261 e.toString()); 262 sIsInitialized.set(false); 263 return; 264 } catch (RuntimeException e) { 265 Log.e(LOG_TAG, "Runtime exception during initialization"); 266 mInternalCallback.onError( 267 MbmsErrors.InitializationErrors.ERROR_UNABLE_TO_INITIALIZE, 268 e.toString()); 269 sIsInitialized.set(false); 270 return; 271 } 272 if (result == MbmsErrors.UNKNOWN) { 273 // Unbind and throw an obvious error 274 close(); 275 throw new IllegalStateException("Middleware must not return" 276 + " an unknown error code"); 277 } 278 if (result != MbmsErrors.SUCCESS) { 279 mInternalCallback.onError(result, 280 "Error returned during initialization"); 281 sIsInitialized.set(false); 282 return; 283 } 284 try { 285 groupCallService.asBinder().linkToDeath(mDeathRecipient, 0); 286 } catch (RemoteException e) { 287 mInternalCallback.onError(MbmsErrors.ERROR_MIDDLEWARE_LOST, 288 "Middleware lost during initialization"); 289 sIsInitialized.set(false); 290 return; 291 } 292 mService.set(groupCallService); 293 } 294 295 @Override 296 public void onServiceDisconnected(ComponentName name) { 297 sIsInitialized.set(false); 298 mService.set(null); 299 } 300 }); 301 } 302 } 303