• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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