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