1 /*
2  * Copyright (C) 2023 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.net.wifi.sharedconnectivity.app;
18 
19 import android.annotation.CallbackExecutor;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.annotation.RequiresPermission;
23 import android.annotation.SuppressLint;
24 import android.annotation.SystemApi;
25 import android.annotation.TestApi;
26 import android.content.BroadcastReceiver;
27 import android.content.ComponentName;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.IntentFilter;
31 import android.content.ServiceConnection;
32 import android.content.res.Resources;
33 import android.net.wifi.sharedconnectivity.service.ISharedConnectivityCallback;
34 import android.net.wifi.sharedconnectivity.service.ISharedConnectivityService;
35 import android.net.wifi.sharedconnectivity.service.SharedConnectivityService;
36 import android.os.Binder;
37 import android.os.IBinder;
38 import android.os.IInterface;
39 import android.os.RemoteException;
40 import android.os.UserManager;
41 import android.text.TextUtils;
42 import android.util.Log;
43 
44 import com.android.internal.R;
45 import com.android.internal.annotations.GuardedBy;
46 
47 import java.util.HashMap;
48 import java.util.List;
49 import java.util.Map;
50 import java.util.Objects;
51 import java.util.concurrent.Executor;
52 
53 /**
54  * This class is the library used by consumers of Shared Connectivity data to bind to the service,
55  * receive callbacks from, and send user actions to the service.
56  *
57  * A client must register at least one callback so that the manager will bind to the service. Once
58  * all callbacks are unregistered, the manager will unbind from the service. When the client no
59  * longer needs Shared Connectivity data, the client must unregister.
60  *
61  * The methods {@link #connectHotspotNetwork}, {@link #disconnectHotspotNetwork},
62  * {@link #connectKnownNetwork} and {@link #forgetKnownNetwork} are not valid and will return false
63  * and getter methods will fail and return null if not called between
64  * {@link SharedConnectivityClientCallback#onServiceConnected()}
65  * and {@link SharedConnectivityClientCallback#onServiceDisconnected()} or if
66  * {@link SharedConnectivityClientCallback#onRegisterCallbackFailed} was called.
67  *
68  * @hide
69  */
70 @SystemApi
71 public class SharedConnectivityManager {
72     private static final String TAG = SharedConnectivityManager.class.getSimpleName();
73     private static final boolean DEBUG = false;
74 
75     private static final class SharedConnectivityCallbackProxy extends
76             ISharedConnectivityCallback.Stub {
77         private final Executor mExecutor;
78         private final SharedConnectivityClientCallback mCallback;
79 
SharedConnectivityCallbackProxy( @onNull @allbackExecutor Executor executor, @NonNull SharedConnectivityClientCallback callback)80         SharedConnectivityCallbackProxy(
81                 @NonNull @CallbackExecutor Executor executor,
82                 @NonNull SharedConnectivityClientCallback callback) {
83             mExecutor = executor;
84             mCallback = callback;
85         }
86 
87         @Override
onServiceConnected()88         public void onServiceConnected() {
89             if (mCallback != null) {
90                 final long token = Binder.clearCallingIdentity();
91                 try {
92                     mExecutor.execute(() -> mCallback.onServiceConnected());
93                 } finally {
94                     Binder.restoreCallingIdentity(token);
95                 }
96             }
97         }
98 
99         @Override
onHotspotNetworksUpdated(@onNull List<HotspotNetwork> networks)100         public void onHotspotNetworksUpdated(@NonNull List<HotspotNetwork> networks) {
101             if (mCallback != null) {
102                 final long token = Binder.clearCallingIdentity();
103                 try {
104                     mExecutor.execute(() -> mCallback.onHotspotNetworksUpdated(networks));
105                 } finally {
106                     Binder.restoreCallingIdentity(token);
107                 }
108             }
109         }
110 
111         @Override
onKnownNetworksUpdated(@onNull List<KnownNetwork> networks)112         public void onKnownNetworksUpdated(@NonNull List<KnownNetwork> networks) {
113             if (mCallback != null) {
114                 final long token = Binder.clearCallingIdentity();
115                 try {
116                     mExecutor.execute(() -> mCallback.onKnownNetworksUpdated(networks));
117                 } finally {
118                     Binder.restoreCallingIdentity(token);
119                 }
120             }
121         }
122 
123         @Override
onSharedConnectivitySettingsChanged( @onNull SharedConnectivitySettingsState state)124         public void onSharedConnectivitySettingsChanged(
125                 @NonNull SharedConnectivitySettingsState state) {
126             if (mCallback != null) {
127                 final long token = Binder.clearCallingIdentity();
128                 try {
129                     mExecutor.execute(() -> mCallback.onSharedConnectivitySettingsChanged(state));
130                 } finally {
131                     Binder.restoreCallingIdentity(token);
132                 }
133             }
134         }
135 
136         @Override
onHotspotNetworkConnectionStatusChanged( @onNull HotspotNetworkConnectionStatus status)137         public void onHotspotNetworkConnectionStatusChanged(
138                 @NonNull HotspotNetworkConnectionStatus status) {
139             if (mCallback != null) {
140                 final long token = Binder.clearCallingIdentity();
141                 try {
142                     mExecutor.execute(() ->
143                             mCallback.onHotspotNetworkConnectionStatusChanged(status));
144                 } finally {
145                     Binder.restoreCallingIdentity(token);
146                 }
147             }
148         }
149 
150         @Override
onKnownNetworkConnectionStatusChanged( @onNull KnownNetworkConnectionStatus status)151         public void onKnownNetworkConnectionStatusChanged(
152                 @NonNull KnownNetworkConnectionStatus status) {
153             if (mCallback != null) {
154                 final long token = Binder.clearCallingIdentity();
155                 try {
156                     mExecutor.execute(() ->
157                             mCallback.onKnownNetworkConnectionStatusChanged(status));
158                 } finally {
159                     Binder.restoreCallingIdentity(token);
160                 }
161             }
162         }
163     }
164 
165     private ISharedConnectivityService mService;
166     @GuardedBy("mProxyDataLock")
167     private final Map<SharedConnectivityClientCallback, SharedConnectivityCallbackProxy>
168             mProxyMap = new HashMap<>();
169     @GuardedBy("mProxyDataLock")
170     private final Map<SharedConnectivityClientCallback, SharedConnectivityCallbackProxy>
171             mCallbackProxyCache = new HashMap<>();
172     // Makes sure mProxyMap and mCallbackProxyCache are locked together when one of them is used.
173     private final Object mProxyDataLock = new Object();
174     private final Context mContext;
175     private final String mServicePackageName;
176     private final String mIntentAction;
177     private ServiceConnection mServiceConnection;
178     private UserManager mUserManager;
179 
180     /**
181      * Creates a new instance of {@link SharedConnectivityManager}.
182      *
183      * @return An instance of {@link SharedConnectivityManager} or null if the shared connectivity
184      * service is not found.
185      * @hide
186      */
187     @Nullable
create(@onNull Context context)188     public static SharedConnectivityManager create(@NonNull Context context) {
189         Resources resources = context.getResources();
190         try {
191             String servicePackageName = resources.getString(
192                     R.string.config_sharedConnectivityServicePackage);
193             String serviceIntentAction = resources.getString(
194                     R.string.config_sharedConnectivityServiceIntentAction);
195             if (TextUtils.isEmpty(servicePackageName) || TextUtils.isEmpty(serviceIntentAction)) {
196                 Log.e(TAG, "To support shared connectivity service on this device, the"
197                         + " service's package name and intent action strings must not be empty");
198                 return null;
199             }
200             return new SharedConnectivityManager(context, servicePackageName, serviceIntentAction);
201         } catch (Resources.NotFoundException e) {
202             Log.e(TAG, "To support shared connectivity service on this device, the service's"
203                     + " package name and intent action strings must be defined");
204         }
205         return null;
206     }
207 
208     /**
209      * @hide
210      */
211     @SuppressLint("ManagerLookup")
212     @TestApi
213     @Nullable
create(@onNull Context context, @NonNull String servicePackageName, @NonNull String serviceIntentAction)214     public static SharedConnectivityManager create(@NonNull Context context,
215             @NonNull String servicePackageName, @NonNull String serviceIntentAction) {
216         return new SharedConnectivityManager(context, servicePackageName, serviceIntentAction);
217     }
218 
SharedConnectivityManager(@onNull Context context, String servicePackageName, String serviceIntentAction)219     private SharedConnectivityManager(@NonNull Context context, String servicePackageName,
220             String serviceIntentAction) {
221         mContext = context;
222         mServicePackageName = servicePackageName;
223         mIntentAction = serviceIntentAction;
224         mUserManager = context.getSystemService(UserManager.class);
225     }
226 
bind()227     private void bind() {
228         mServiceConnection = new ServiceConnection() {
229             @Override
230             public void onServiceConnected(ComponentName name, IBinder service) {
231                 if (DEBUG) Log.i(TAG, "onServiceConnected");
232                 mService = ISharedConnectivityService.Stub.asInterface(service);
233                 synchronized (mProxyDataLock) {
234                     if (!mCallbackProxyCache.isEmpty()) {
235                         mCallbackProxyCache.keySet().forEach(callback ->
236                                 registerCallbackInternal(
237                                         callback, mCallbackProxyCache.get(callback)));
238                         mCallbackProxyCache.clear();
239                     }
240                 }
241             }
242 
243             @Override
244             public void onServiceDisconnected(ComponentName name) {
245                 if (DEBUG) Log.i(TAG, "onServiceDisconnected");
246                 mService = null;
247                 synchronized (mProxyDataLock) {
248                     if (!mCallbackProxyCache.isEmpty()) {
249                         mCallbackProxyCache.keySet().forEach(
250                                 SharedConnectivityClientCallback::onServiceDisconnected);
251                         mCallbackProxyCache.clear();
252                     }
253                     if (!mProxyMap.isEmpty()) {
254                         mProxyMap.keySet().forEach(
255                                 SharedConnectivityClientCallback::onServiceDisconnected);
256                         mProxyMap.clear();
257                     }
258                 }
259             }
260         };
261 
262         boolean result = mContext.bindService(
263                 new Intent().setPackage(mServicePackageName).setAction(mIntentAction),
264                 mServiceConnection, Context.BIND_AUTO_CREATE);
265         if (!result) {
266             if (DEBUG) Log.i(TAG, "bindService failed");
267             mServiceConnection = null;
268             if (mUserManager != null && !mUserManager.isUserUnlocked()) {  // In direct boot mode
269                 IntentFilter intentFilter = new IntentFilter();
270                 intentFilter.addAction(Intent.ACTION_USER_UNLOCKED);
271                 mContext.registerReceiver(mBroadcastReceiver, intentFilter);
272             } else {
273                 synchronized (mProxyDataLock) {
274                     if (!mCallbackProxyCache.isEmpty()) {
275                         mCallbackProxyCache.keySet().forEach(
276                                 callback -> callback.onRegisterCallbackFailed(
277                                         new IllegalStateException(
278                                                 "Failed to bind after user unlock")));
279                         mCallbackProxyCache.clear();
280                     }
281                 }
282             }
283         }
284     }
285 
286     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
287         @Override
288         public void onReceive(Context context, Intent intent) {
289             context.unregisterReceiver(mBroadcastReceiver);
290             bind();
291         }
292     };
293 
294     /**
295      * @hide
296      */
297     @TestApi
298     @NonNull
getBroadcastReceiver()299     public BroadcastReceiver getBroadcastReceiver() {
300         return mBroadcastReceiver;
301     }
302 
registerCallbackInternal(SharedConnectivityClientCallback callback, SharedConnectivityCallbackProxy proxy)303     private void registerCallbackInternal(SharedConnectivityClientCallback callback,
304             SharedConnectivityCallbackProxy proxy) {
305         try {
306             mService.registerCallback(proxy);
307             synchronized (mProxyDataLock) {
308                 mProxyMap.put(callback, proxy);
309             }
310         } catch (RemoteException e) {
311             Log.e(TAG, "Exception in registerCallback", e);
312             callback.onRegisterCallbackFailed(e);
313         }
314     }
315 
316     /**
317      * @hide
318      */
319     @TestApi
setService(@ullable IInterface service)320     public void setService(@Nullable IInterface service) {
321         mService = (ISharedConnectivityService) service;
322     }
323 
324     /**
325      * @hide
326      */
327     @TestApi
328     @Nullable
getServiceConnection()329     public ServiceConnection getServiceConnection() {
330         return mServiceConnection;
331     }
332 
unbind()333     private void unbind() {
334         if (mServiceConnection != null) {
335             mContext.unbindService(mServiceConnection);
336             mServiceConnection = null;
337             mService = null;
338         }
339     }
340 
341     /**
342      * Registers a callback for receiving updates to the list of Hotspot Networks, Known Networks,
343      * shared connectivity settings state, hotspot network connection status and known network
344      * connection status.
345      * Automatically binds to implementation of {@link SharedConnectivityService} specified in
346      * the device overlay when the first callback is registered.
347      * The {@link SharedConnectivityClientCallback#onRegisterCallbackFailed} will be called if the
348      * registration failed.
349      *
350      * @param executor The Executor used to invoke the callback.
351      * @param callback The callback of type {@link SharedConnectivityClientCallback} that is invoked
352      *                 when the service updates its data.
353      */
354     @RequiresPermission(anyOf = {android.Manifest.permission.NETWORK_SETTINGS,
355             android.Manifest.permission.NETWORK_SETUP_WIZARD})
registerCallback(@onNull @allbackExecutor Executor executor, @NonNull SharedConnectivityClientCallback callback)356     public void registerCallback(@NonNull @CallbackExecutor Executor executor,
357             @NonNull SharedConnectivityClientCallback callback) {
358         Objects.requireNonNull(executor, "executor cannot be null");
359         Objects.requireNonNull(callback, "callback cannot be null");
360 
361         if (mProxyMap.containsKey(callback) || mCallbackProxyCache.containsKey(callback)) {
362             Log.e(TAG, "Callback already registered");
363             callback.onRegisterCallbackFailed(new IllegalStateException(
364                     "Callback already registered"));
365             return;
366         }
367 
368         SharedConnectivityCallbackProxy proxy =
369                 new SharedConnectivityCallbackProxy(executor, callback);
370         if (mService == null) {
371             boolean shouldBind;
372             synchronized (mProxyDataLock) {
373                 // Size can be 1 in different cases of register/unregister sequences. If size is 0
374                 // Bind never happened or unbind was called.
375                 shouldBind = mCallbackProxyCache.size() == 0;
376                 mCallbackProxyCache.put(callback, proxy);
377             }
378             if (shouldBind) {
379                 bind();
380             }
381             return;
382         }
383         registerCallbackInternal(callback, proxy);
384     }
385 
386     /**
387      * Unregisters a callback.
388      * Unbinds from {@link SharedConnectivityService} when no more callbacks are registered.
389      *
390      * @return Returns true if the callback was successfully unregistered, false otherwise.
391      */
392     @RequiresPermission(anyOf = {android.Manifest.permission.NETWORK_SETTINGS,
393             android.Manifest.permission.NETWORK_SETUP_WIZARD})
unregisterCallback( @onNull SharedConnectivityClientCallback callback)394     public boolean unregisterCallback(
395             @NonNull SharedConnectivityClientCallback callback) {
396         Objects.requireNonNull(callback, "callback cannot be null");
397 
398         if (!mProxyMap.containsKey(callback) && !mCallbackProxyCache.containsKey(callback)) {
399             Log.e(TAG, "Callback not found, cannot unregister");
400             return false;
401         }
402 
403         // Try to unregister the broadcast receiver to guard against memory leaks.
404         try {
405             mContext.unregisterReceiver(mBroadcastReceiver);
406         } catch (IllegalArgumentException e) {
407             // This is fine, it means the receiver was never registered or was already unregistered.
408         }
409 
410         if (mService == null) {
411             boolean shouldUnbind;
412             synchronized (mProxyDataLock) {
413                 mCallbackProxyCache.remove(callback);
414                 // Connection was never established, so all registered callbacks are in the cache.
415                 shouldUnbind = mCallbackProxyCache.isEmpty();
416             }
417             if (shouldUnbind) {
418                 unbind();
419             }
420             return true;
421         }
422 
423         try {
424             boolean shouldUnbind;
425             synchronized (mProxyDataLock) {
426                 mService.unregisterCallback(mProxyMap.get(callback));
427                 mProxyMap.remove(callback);
428                 shouldUnbind = mProxyMap.isEmpty();
429             }
430             if (shouldUnbind) {
431                 unbind();
432             }
433         } catch (RemoteException e) {
434             Log.e(TAG, "Exception in unregisterCallback", e);
435             return false;
436         }
437         return true;
438     }
439 
440     /**
441      * Send command to the implementation of {@link SharedConnectivityService} requesting connection
442      * to the specified Hotspot Network.
443      *
444      * @param network {@link HotspotNetwork} object representing the network the user has requested
445      *                a connection to.
446      * @return Returns true if the service received the command. Does not guarantee that the
447      * connection was successful.
448      */
449     @RequiresPermission(anyOf = {android.Manifest.permission.NETWORK_SETTINGS,
450             android.Manifest.permission.NETWORK_SETUP_WIZARD})
connectHotspotNetwork(@onNull HotspotNetwork network)451     public boolean connectHotspotNetwork(@NonNull HotspotNetwork network) {
452         Objects.requireNonNull(network, "Hotspot network cannot be null");
453 
454         if (mService == null) {
455             return false;
456         }
457 
458         try {
459             mService.connectHotspotNetwork(network);
460         } catch (RemoteException e) {
461             Log.e(TAG, "Exception in connectHotspotNetwork", e);
462             return false;
463         }
464         return true;
465     }
466 
467     /**
468      * Send command to the implementation of {@link SharedConnectivityService} requesting
469      * disconnection from the active Hotspot Network.
470      *
471      * @param network {@link HotspotNetwork} object representing the network the user has requested
472      *                to disconnect from.
473      * @return Returns true if the service received the command. Does not guarantee that the
474      * disconnection was successful.
475      */
476     @RequiresPermission(anyOf = {android.Manifest.permission.NETWORK_SETTINGS,
477             android.Manifest.permission.NETWORK_SETUP_WIZARD})
disconnectHotspotNetwork(@onNull HotspotNetwork network)478     public boolean disconnectHotspotNetwork(@NonNull HotspotNetwork network) {
479         if (mService == null) {
480             return false;
481         }
482 
483         try {
484             mService.disconnectHotspotNetwork(network);
485         } catch (RemoteException e) {
486             Log.e(TAG, "Exception in disconnectHotspotNetwork", e);
487             return false;
488         }
489         return true;
490     }
491 
492     /**
493      * Send command to the implementation of {@link SharedConnectivityService} requesting connection
494      * to the specified Known Network.
495      *
496      * @param network {@link KnownNetwork} object representing the network the user has requested
497      *                a connection to.
498      * @return Returns true if the service received the command. Does not guarantee that the
499      * connection was successful.
500      */
501     @RequiresPermission(anyOf = {android.Manifest.permission.NETWORK_SETTINGS,
502             android.Manifest.permission.NETWORK_SETUP_WIZARD})
connectKnownNetwork(@onNull KnownNetwork network)503     public boolean connectKnownNetwork(@NonNull KnownNetwork network) {
504         Objects.requireNonNull(network, "Known network cannot be null");
505 
506         if (mService == null) {
507             return false;
508         }
509 
510         try {
511             mService.connectKnownNetwork(network);
512         } catch (RemoteException e) {
513             Log.e(TAG, "Exception in connectKnownNetwork", e);
514             return false;
515         }
516         return true;
517     }
518 
519     /**
520      * Send command to the implementation of {@link SharedConnectivityService} requesting removal of
521      * the specified Known Network from the list of Known Networks.
522      *
523      * @return Returns true if the service received the command. Does not guarantee that the
524      * forget action was successful.
525      */
526     @RequiresPermission(anyOf = {android.Manifest.permission.NETWORK_SETTINGS,
527             android.Manifest.permission.NETWORK_SETUP_WIZARD})
forgetKnownNetwork(@onNull KnownNetwork network)528     public boolean forgetKnownNetwork(@NonNull KnownNetwork network) {
529         Objects.requireNonNull(network, "Known network cannot be null");
530 
531         if (mService == null) {
532             return false;
533         }
534 
535         try {
536             mService.forgetKnownNetwork(network);
537         } catch (RemoteException e) {
538             Log.e(TAG, "Exception in forgetKnownNetwork", e);
539             return false;
540         }
541         return true;
542     }
543 
544     /**
545      * Gets the list of hotspot networks the user can select to connect to.
546      *
547      * @return Returns a {@link List} of {@link HotspotNetwork} objects, null on failure.
548      */
549     @RequiresPermission(anyOf = {android.Manifest.permission.NETWORK_SETTINGS,
550             android.Manifest.permission.NETWORK_SETUP_WIZARD})
551     @SuppressWarnings("NullableCollection")
552     @Nullable
getHotspotNetworks()553     public List<HotspotNetwork> getHotspotNetworks() {
554         if (mService == null) {
555             return null;
556         }
557 
558         try {
559             return mService.getHotspotNetworks();
560         } catch (RemoteException e) {
561             Log.e(TAG, "Exception in getHotspotNetworks", e);
562         }
563         return null;
564     }
565 
566     /**
567      * Gets the list of known networks the user can select to connect to.
568      *
569      * @return Returns a {@link List} of {@link KnownNetwork} objects, null on failure.
570      */
571     @RequiresPermission(anyOf = {android.Manifest.permission.NETWORK_SETTINGS,
572             android.Manifest.permission.NETWORK_SETUP_WIZARD})
573     @SuppressWarnings("NullableCollection")
574     @Nullable
getKnownNetworks()575     public List<KnownNetwork> getKnownNetworks() {
576         if (mService == null) {
577             return null;
578         }
579 
580         try {
581             return mService.getKnownNetworks();
582         } catch (RemoteException e) {
583             Log.e(TAG, "Exception in getKnownNetworks", e);
584         }
585         return null;
586     }
587 
588     /**
589      * Gets the shared connectivity settings state.
590      *
591      * @return Returns a {@link SharedConnectivitySettingsState} object with the state, null on
592      * failure.
593      */
594     @RequiresPermission(anyOf = {android.Manifest.permission.NETWORK_SETTINGS,
595             android.Manifest.permission.NETWORK_SETUP_WIZARD})
596     @Nullable
getSettingsState()597     public SharedConnectivitySettingsState getSettingsState() {
598         if (mService == null) {
599             return null;
600         }
601 
602         try {
603             return mService.getSettingsState();
604         } catch (RemoteException e) {
605             Log.e(TAG, "Exception in getSettingsState", e);
606         }
607         return null;
608     }
609 
610     /**
611      * Gets the connection status of the hotspot network the user selected to connect to.
612      *
613      * @return Returns a {@link HotspotNetworkConnectionStatus} object with the connection status,
614      * null on failure. If no connection is active the status will be
615      * {@link HotspotNetworkConnectionStatus#CONNECTION_STATUS_UNKNOWN}.
616      */
617     @RequiresPermission(anyOf = {android.Manifest.permission.NETWORK_SETTINGS,
618             android.Manifest.permission.NETWORK_SETUP_WIZARD})
619     @Nullable
getHotspotNetworkConnectionStatus()620     public HotspotNetworkConnectionStatus getHotspotNetworkConnectionStatus() {
621         if (mService == null) {
622             return null;
623         }
624 
625         try {
626             return mService.getHotspotNetworkConnectionStatus();
627         } catch (RemoteException e) {
628             Log.e(TAG, "Exception in getHotspotNetworkConnectionStatus", e);
629         }
630         return null;
631     }
632 
633     /**
634      * Gets the connection status of the known network the user selected to connect to.
635      *
636      * @return Returns a {@link KnownNetworkConnectionStatus} object with the connection status,
637      * null on failure. If no connection is active the status will be
638      * {@link KnownNetworkConnectionStatus#CONNECTION_STATUS_UNKNOWN}.
639      */
640     @RequiresPermission(anyOf = {android.Manifest.permission.NETWORK_SETTINGS,
641             android.Manifest.permission.NETWORK_SETUP_WIZARD})
642     @Nullable
getKnownNetworkConnectionStatus()643     public KnownNetworkConnectionStatus getKnownNetworkConnectionStatus() {
644         if (mService == null) {
645             return null;
646         }
647 
648         try {
649             return mService.getKnownNetworkConnectionStatus();
650         } catch (RemoteException e) {
651             Log.e(TAG, "Exception in getKnownNetworkConnectionStatus", e);
652         }
653         return null;
654     }
655 }
656