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 ServiceConnection mServiceConnection; 84 private Set<GroupCall> mKnownActiveGroupCalls = new ArraySet<>(); 85 86 private final Context mContext; 87 private int mSubscriptionId; 88 89 /** @hide */ MbmsGroupCallSession(Context context, Executor executor, int subscriptionId, MbmsGroupCallSessionCallback callback)90 private MbmsGroupCallSession(Context context, Executor executor, int subscriptionId, 91 MbmsGroupCallSessionCallback callback) { 92 mContext = context; 93 mSubscriptionId = subscriptionId; 94 mInternalCallback = new InternalGroupCallSessionCallback(callback, executor); 95 } 96 97 /** 98 * Create a new {@link MbmsGroupCallSession} using the given subscription ID. 99 * 100 * You may only have one instance of {@link MbmsGroupCallSession} per UID. If you call this 101 * method while there is an active instance of {@link MbmsGroupCallSession} in your process 102 * (in other words, one that has not had {@link #close()} called on it), this method will 103 * throw an {@link IllegalStateException}. If you call this method in a different process 104 * running under the same UID, an error will be indicated via 105 * {@link MbmsGroupCallSessionCallback#onError(int, String)}. 106 * 107 * Note that initialization may fail asynchronously. If you wish to try again after you 108 * receive such an asynchronous error, you must call {@link #close()} on the instance of 109 * {@link MbmsGroupCallSession} that you received before calling this method again. 110 * 111 * @param context The {@link Context} to use. 112 * @param subscriptionId The subscription ID to use. 113 * @param executor The executor on which you wish to execute callbacks. 114 * @param callback A callback object on which you wish to receive results of asynchronous 115 * operations. 116 * @return An instance of {@link MbmsGroupCallSession}, or null if an error occurred. 117 */ create(@onNull Context context, int subscriptionId, @NonNull Executor executor, final @NonNull MbmsGroupCallSessionCallback callback)118 public static @Nullable MbmsGroupCallSession create(@NonNull Context context, 119 int subscriptionId, @NonNull Executor executor, 120 final @NonNull MbmsGroupCallSessionCallback callback) { 121 if (!sIsInitialized.compareAndSet(false, true)) { 122 throw new IllegalStateException("Cannot create two instances of MbmsGroupCallSession"); 123 } 124 MbmsGroupCallSession session = new MbmsGroupCallSession(context, executor, 125 subscriptionId, callback); 126 127 final int result = session.bindAndInitialize(); 128 if (result != MbmsErrors.SUCCESS) { 129 sIsInitialized.set(false); 130 executor.execute(new Runnable() { 131 @Override 132 public void run() { 133 callback.onError(result, null); 134 } 135 }); 136 return null; 137 } 138 return session; 139 } 140 141 /** 142 * Create a new {@link MbmsGroupCallSession} using the system default data subscription ID. 143 * See {@link #create(Context, int, Executor, MbmsGroupCallSessionCallback)}. 144 */ create(@onNull Context context, @NonNull Executor executor, @NonNull MbmsGroupCallSessionCallback callback)145 public static @Nullable MbmsGroupCallSession create(@NonNull Context context, 146 @NonNull Executor executor, @NonNull MbmsGroupCallSessionCallback callback) { 147 return create(context, SubscriptionManager.getDefaultSubscriptionId(), executor, callback); 148 } 149 150 /** 151 * Terminates this instance. Also terminates 152 * any group calls spawned from this instance as if 153 * {@link GroupCall#close()} had been called on them. After this method returns, 154 * no further callbacks originating from the middleware will be enqueued on the provided 155 * instance of {@link MbmsGroupCallSessionCallback}, but callbacks that have already been 156 * enqueued will still be delivered. 157 * 158 * It is safe to call {@link #create(Context, int, Executor, MbmsGroupCallSessionCallback)} to 159 * obtain another instance of {@link MbmsGroupCallSession} immediately after this method 160 * returns. 161 * 162 * May throw an {@link IllegalStateException} 163 */ close()164 public void close() { 165 try { 166 IMbmsGroupCallService groupCallService = mService.get(); 167 if (groupCallService == null || mServiceConnection == null) { 168 // Ignore and return, assume already disposed. 169 return; 170 } 171 groupCallService.dispose(mSubscriptionId); 172 for (GroupCall s : mKnownActiveGroupCalls) { 173 s.getCallback().stop(); 174 } 175 mKnownActiveGroupCalls.clear(); 176 mContext.unbindService(mServiceConnection); 177 } catch (RemoteException e) { 178 // Ignore for now 179 } finally { 180 mService.set(null); 181 sIsInitialized.set(false); 182 mServiceConnection = null; 183 mInternalCallback.stop(); 184 } 185 } 186 187 /** 188 * Starts the requested group call, reporting status to the indicated callback. 189 * Returns an object used to control that call. 190 * 191 * May throw an {@link IllegalArgumentException} or an {@link IllegalStateException} 192 * 193 * Asynchronous errors through the callback include any of the errors in 194 * {@link MbmsErrors.GeneralErrors}. 195 * 196 * @param tmgi The TMGI, an identifier for the group call you want to join. 197 * @param saiList A list of SAIs for the group call that should be negotiated separately with 198 * the carrier. 199 * @param frequencyList A lost of frequencies for the group call that should be negotiated 200 * separately with the carrier. 201 * @param executor The executor on which you wish to execute callbacks for this stream. 202 * @param callback The callback that you want to receive information about the call on. 203 * @return An instance of {@link GroupCall} through which the call can be controlled. 204 * May be {@code null} if an error occurred. 205 */ startGroupCall(long tmgi, @NonNull List<Integer> saiList, @NonNull List<Integer> frequencyList, @NonNull Executor executor, @NonNull GroupCallCallback callback)206 public @Nullable GroupCall startGroupCall(long tmgi, @NonNull List<Integer> saiList, 207 @NonNull List<Integer> frequencyList, @NonNull Executor executor, 208 @NonNull GroupCallCallback callback) { 209 IMbmsGroupCallService groupCallService = mService.get(); 210 if (groupCallService == null) { 211 throw new IllegalStateException("Middleware not yet bound"); 212 } 213 214 InternalGroupCallCallback serviceCallback = new InternalGroupCallCallback( 215 callback, executor); 216 217 GroupCall serviceForApp = new GroupCall(mSubscriptionId, 218 groupCallService, this, tmgi, serviceCallback); 219 mKnownActiveGroupCalls.add(serviceForApp); 220 221 try { 222 int returnCode = groupCallService.startGroupCall( 223 mSubscriptionId, tmgi, saiList, frequencyList, serviceCallback); 224 if (returnCode == MbmsErrors.UNKNOWN) { 225 // Unbind and throw an obvious error 226 close(); 227 throw new IllegalStateException("Middleware must not return an unknown error code"); 228 } 229 if (returnCode != MbmsErrors.SUCCESS) { 230 mInternalCallback.onError(returnCode, null); 231 return null; 232 } 233 } catch (RemoteException e) { 234 Log.w(LOG_TAG, "Remote process died"); 235 mService.set(null); 236 sIsInitialized.set(false); 237 mInternalCallback.onError(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); 238 return null; 239 } 240 241 return serviceForApp; 242 } 243 244 /** @hide */ onGroupCallStopped(GroupCall service)245 public void onGroupCallStopped(GroupCall service) { 246 mKnownActiveGroupCalls.remove(service); 247 } 248 bindAndInitialize()249 private int bindAndInitialize() { 250 mServiceConnection = new ServiceConnection() { 251 @Override 252 public void onServiceConnected(ComponentName name, IBinder service) { 253 IMbmsGroupCallService groupCallService = 254 IMbmsGroupCallService.Stub.asInterface(service); 255 int result; 256 try { 257 result = groupCallService.initialize(mInternalCallback, 258 mSubscriptionId); 259 } catch (RemoteException e) { 260 Log.e(LOG_TAG, "Service died before initialization"); 261 mInternalCallback.onError( 262 MbmsErrors.InitializationErrors.ERROR_UNABLE_TO_INITIALIZE, 263 e.toString()); 264 sIsInitialized.set(false); 265 return; 266 } catch (RuntimeException e) { 267 Log.e(LOG_TAG, "Runtime exception during initialization"); 268 mInternalCallback.onError( 269 MbmsErrors.InitializationErrors.ERROR_UNABLE_TO_INITIALIZE, 270 e.toString()); 271 sIsInitialized.set(false); 272 return; 273 } 274 if (result == MbmsErrors.UNKNOWN) { 275 // Unbind and throw an obvious error 276 close(); 277 throw new IllegalStateException("Middleware must not return" 278 + " an unknown error code"); 279 } 280 if (result != MbmsErrors.SUCCESS) { 281 mInternalCallback.onError(result, 282 "Error returned during initialization"); 283 sIsInitialized.set(false); 284 return; 285 } 286 try { 287 groupCallService.asBinder().linkToDeath(mDeathRecipient, 0); 288 } catch (RemoteException e) { 289 mInternalCallback.onError(MbmsErrors.ERROR_MIDDLEWARE_LOST, 290 "Middleware lost during initialization"); 291 sIsInitialized.set(false); 292 return; 293 } 294 mService.set(groupCallService); 295 } 296 297 @Override 298 public void onServiceDisconnected(ComponentName name) { 299 sIsInitialized.set(false); 300 mService.set(null); 301 } 302 303 @Override 304 public void onNullBinding(ComponentName name) { 305 Log.w(LOG_TAG, "bindAndInitialize: Remote service returned null"); 306 mInternalCallback.onError(MbmsErrors.ERROR_MIDDLEWARE_LOST, 307 "Middleware service binding returned null"); 308 sIsInitialized.set(false); 309 mService.set(null); 310 mContext.unbindService(this); 311 } 312 }; 313 return MbmsUtils.startBinding(mContext, MBMS_GROUP_CALL_SERVICE_ACTION, mServiceConnection); 314 } 315 } 316