| /* |
| * Copyright (C) 2023 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.net.wifi.sharedconnectivity.app; |
| |
| import android.annotation.CallbackExecutor; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.RequiresPermission; |
| import android.annotation.SuppressLint; |
| import android.annotation.SystemApi; |
| import android.annotation.TestApi; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.ServiceConnection; |
| import android.content.res.Resources; |
| import android.net.wifi.sharedconnectivity.service.ISharedConnectivityCallback; |
| import android.net.wifi.sharedconnectivity.service.ISharedConnectivityService; |
| import android.net.wifi.sharedconnectivity.service.SharedConnectivityService; |
| import android.os.Binder; |
| import android.os.IBinder; |
| import android.os.IInterface; |
| import android.os.RemoteException; |
| import android.text.TextUtils; |
| import android.util.Log; |
| |
| import com.android.internal.R; |
| import com.android.internal.annotations.GuardedBy; |
| |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.concurrent.Executor; |
| |
| /** |
| * This class is the library used by consumers of Shared Connectivity data to bind to the service, |
| * receive callbacks from, and send user actions to the service. |
| * |
| * A client must register at least one callback so that the manager will bind to the service. Once |
| * all callbacks are unregistered, the manager will unbind from the service. When the client no |
| * longer needs Shared Connectivity data, the client must unregister. |
| * |
| * The methods {@link #connectHotspotNetwork}, {@link #disconnectHotspotNetwork}, |
| * {@link #connectKnownNetwork} and {@link #forgetKnownNetwork} are not valid and will return false |
| * and getter methods will fail and return null if not called between |
| * {@link SharedConnectivityClientCallback#onServiceConnected()} |
| * and {@link SharedConnectivityClientCallback#onServiceDisconnected()} or if |
| * {@link SharedConnectivityClientCallback#onRegisterCallbackFailed} was called. |
| * |
| * @hide |
| */ |
| @SystemApi |
| public class SharedConnectivityManager { |
| private static final String TAG = SharedConnectivityManager.class.getSimpleName(); |
| private static final boolean DEBUG = true; |
| |
| private static final class SharedConnectivityCallbackProxy extends |
| ISharedConnectivityCallback.Stub { |
| private final Executor mExecutor; |
| private final SharedConnectivityClientCallback mCallback; |
| |
| SharedConnectivityCallbackProxy( |
| @NonNull @CallbackExecutor Executor executor, |
| @NonNull SharedConnectivityClientCallback callback) { |
| mExecutor = executor; |
| mCallback = callback; |
| } |
| |
| public void onHotspotNetworksUpdated(@NonNull List<HotspotNetwork> networks) { |
| if (mCallback != null) { |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| mExecutor.execute(() -> mCallback.onHotspotNetworksUpdated(networks)); |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| } |
| |
| @Override |
| public void onKnownNetworksUpdated(@NonNull List<KnownNetwork> networks) { |
| if (mCallback != null) { |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| mExecutor.execute(() -> mCallback.onKnownNetworksUpdated(networks)); |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| } |
| |
| @Override |
| public void onSharedConnectivitySettingsChanged( |
| @NonNull SharedConnectivitySettingsState state) { |
| if (mCallback != null) { |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| mExecutor.execute(() -> mCallback.onSharedConnectivitySettingsChanged(state)); |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| } |
| |
| public void onHotspotNetworkConnectionStatusChanged( |
| @NonNull HotspotNetworkConnectionStatus status) { |
| if (mCallback != null) { |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| mExecutor.execute(() -> |
| mCallback.onHotspotNetworkConnectionStatusChanged(status)); |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| } |
| |
| @Override |
| public void onKnownNetworkConnectionStatusChanged( |
| @NonNull KnownNetworkConnectionStatus status) { |
| if (mCallback != null) { |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| mExecutor.execute(() -> |
| mCallback.onKnownNetworkConnectionStatusChanged(status)); |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| } |
| } |
| |
| private ISharedConnectivityService mService; |
| @GuardedBy("mProxyDataLock") |
| private final Map<SharedConnectivityClientCallback, SharedConnectivityCallbackProxy> |
| mProxyMap = new HashMap<>(); |
| @GuardedBy("mProxyDataLock") |
| private final Map<SharedConnectivityClientCallback, SharedConnectivityCallbackProxy> |
| mCallbackProxyCache = new HashMap<>(); |
| // Makes sure mProxyMap and mCallbackProxyCache are locked together when one of them is used. |
| private final Object mProxyDataLock = new Object(); |
| private final Context mContext; |
| private final String mServicePackageName; |
| private final String mIntentAction; |
| private ServiceConnection mServiceConnection; |
| |
| /** |
| * Creates a new instance of {@link SharedConnectivityManager}. |
| * |
| * @return An instance of {@link SharedConnectivityManager} or null if the shared connectivity |
| * service is not found. |
| * @hide |
| */ |
| @Nullable |
| public static SharedConnectivityManager create(@NonNull Context context) { |
| Resources resources = context.getResources(); |
| try { |
| String servicePackageName = resources.getString( |
| R.string.config_sharedConnectivityServicePackage); |
| String serviceIntentAction = resources.getString( |
| R.string.config_sharedConnectivityServiceIntentAction); |
| if (TextUtils.isEmpty(servicePackageName) || TextUtils.isEmpty(serviceIntentAction)) { |
| Log.e(TAG, "To support shared connectivity service on this device, the" |
| + " service's package name and intent action strings must not be empty"); |
| return null; |
| } |
| return new SharedConnectivityManager(context, servicePackageName, serviceIntentAction); |
| } catch (Resources.NotFoundException e) { |
| Log.e(TAG, "To support shared connectivity service on this device, the service's" |
| + " package name and intent action strings must be defined"); |
| } |
| return null; |
| } |
| |
| /** |
| * @hide |
| */ |
| @SuppressLint("ManagerLookup") |
| @TestApi |
| @Nullable |
| public static SharedConnectivityManager create(@NonNull Context context, |
| @NonNull String servicePackageName, @NonNull String serviceIntentAction) { |
| return new SharedConnectivityManager(context, servicePackageName, serviceIntentAction); |
| } |
| |
| private SharedConnectivityManager(@NonNull Context context, String servicePackageName, |
| String serviceIntentAction) { |
| mContext = context; |
| mServicePackageName = servicePackageName; |
| mIntentAction = serviceIntentAction; |
| } |
| |
| private void bind() { |
| mServiceConnection = new ServiceConnection() { |
| @Override |
| public void onServiceConnected(ComponentName name, IBinder service) { |
| mService = ISharedConnectivityService.Stub.asInterface(service); |
| synchronized (mProxyDataLock) { |
| if (!mCallbackProxyCache.isEmpty()) { |
| mCallbackProxyCache.keySet().forEach(callback -> |
| registerCallbackInternal( |
| callback, mCallbackProxyCache.get(callback))); |
| mCallbackProxyCache.clear(); |
| } |
| } |
| } |
| |
| @Override |
| public void onServiceDisconnected(ComponentName name) { |
| if (DEBUG) Log.i(TAG, "onServiceDisconnected"); |
| mService = null; |
| synchronized (mProxyDataLock) { |
| if (!mCallbackProxyCache.isEmpty()) { |
| mCallbackProxyCache.keySet().forEach( |
| SharedConnectivityClientCallback::onServiceDisconnected); |
| mCallbackProxyCache.clear(); |
| } |
| if (!mProxyMap.isEmpty()) { |
| mProxyMap.keySet().forEach( |
| SharedConnectivityClientCallback::onServiceDisconnected); |
| mProxyMap.clear(); |
| } |
| } |
| } |
| }; |
| |
| mContext.bindService( |
| new Intent().setPackage(mServicePackageName).setAction(mIntentAction), |
| mServiceConnection, Context.BIND_AUTO_CREATE); |
| } |
| |
| private void registerCallbackInternal(SharedConnectivityClientCallback callback, |
| SharedConnectivityCallbackProxy proxy) { |
| try { |
| mService.registerCallback(proxy); |
| synchronized (mProxyDataLock) { |
| mProxyMap.put(callback, proxy); |
| } |
| callback.onServiceConnected(); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Exception in registerCallback", e); |
| callback.onRegisterCallbackFailed(e); |
| } |
| } |
| |
| /** |
| * @hide |
| */ |
| @TestApi |
| public void setService(@Nullable IInterface service) { |
| mService = (ISharedConnectivityService) service; |
| } |
| |
| /** |
| * @hide |
| */ |
| @TestApi |
| @Nullable |
| public ServiceConnection getServiceConnection() { |
| return mServiceConnection; |
| } |
| |
| private void unbind() { |
| if (mServiceConnection != null) { |
| mContext.unbindService(mServiceConnection); |
| mServiceConnection = null; |
| } |
| } |
| |
| /** |
| * Registers a callback for receiving updates to the list of Hotspot Networks, Known Networks, |
| * shared connectivity settings state, hotspot network connection status and known network |
| * connection status. |
| * Automatically binds to implementation of {@link SharedConnectivityService} specified in |
| * the device overlay when the first callback is registered. |
| * The {@link SharedConnectivityClientCallback#onRegisterCallbackFailed} will be called if the |
| * registration failed. |
| * |
| * @param executor The Executor used to invoke the callback. |
| * @param callback The callback of type {@link SharedConnectivityClientCallback} that is invoked |
| * when the service updates its data. |
| */ |
| @RequiresPermission(anyOf = {android.Manifest.permission.NETWORK_SETTINGS, |
| android.Manifest.permission.NETWORK_SETUP_WIZARD}) |
| public void registerCallback(@NonNull @CallbackExecutor Executor executor, |
| @NonNull SharedConnectivityClientCallback callback) { |
| Objects.requireNonNull(executor, "executor cannot be null"); |
| Objects.requireNonNull(callback, "callback cannot be null"); |
| |
| if (mProxyMap.containsKey(callback) || mCallbackProxyCache.containsKey(callback)) { |
| Log.e(TAG, "Callback already registered"); |
| callback.onRegisterCallbackFailed(new IllegalStateException( |
| "Callback already registered")); |
| return; |
| } |
| |
| SharedConnectivityCallbackProxy proxy = |
| new SharedConnectivityCallbackProxy(executor, callback); |
| if (mService == null) { |
| boolean shouldBind; |
| synchronized (mProxyDataLock) { |
| // Size can be 1 in different cases of register/unregister sequences. If size is 0 |
| // Bind never happened or unbind was called. |
| shouldBind = mCallbackProxyCache.size() == 0; |
| mCallbackProxyCache.put(callback, proxy); |
| } |
| if (shouldBind) { |
| bind(); |
| } |
| return; |
| } |
| registerCallbackInternal(callback, proxy); |
| } |
| |
| /** |
| * Unregisters a callback. |
| * Unbinds from {@link SharedConnectivityService} when no more callbacks are registered. |
| * |
| * @return Returns true if the callback was successfully unregistered, false otherwise. |
| */ |
| @RequiresPermission(anyOf = {android.Manifest.permission.NETWORK_SETTINGS, |
| android.Manifest.permission.NETWORK_SETUP_WIZARD}) |
| public boolean unregisterCallback( |
| @NonNull SharedConnectivityClientCallback callback) { |
| Objects.requireNonNull(callback, "callback cannot be null"); |
| |
| if (!mProxyMap.containsKey(callback) && !mCallbackProxyCache.containsKey(callback)) { |
| Log.e(TAG, "Callback not found, cannot unregister"); |
| return false; |
| } |
| |
| if (mService == null) { |
| boolean shouldUnbind; |
| synchronized (mProxyDataLock) { |
| mCallbackProxyCache.remove(callback); |
| // Connection was never established, so all registered callbacks are in the cache. |
| shouldUnbind = mCallbackProxyCache.isEmpty(); |
| } |
| if (shouldUnbind) { |
| unbind(); |
| } |
| return true; |
| } |
| |
| try { |
| boolean shouldUnbind; |
| synchronized (mProxyDataLock) { |
| mService.unregisterCallback(mProxyMap.get(callback)); |
| mProxyMap.remove(callback); |
| shouldUnbind = mProxyMap.isEmpty(); |
| } |
| if (shouldUnbind) { |
| unbind(); |
| } |
| } catch (RemoteException e) { |
| Log.e(TAG, "Exception in unregisterCallback", e); |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * Send command to the implementation of {@link SharedConnectivityService} requesting connection |
| * to the specified Hotspot Network. |
| * |
| * @param network {@link HotspotNetwork} object representing the network the user has requested |
| * a connection to. |
| * @return Returns true if the service received the command. Does not guarantee that the |
| * connection was successful. |
| */ |
| @RequiresPermission(anyOf = {android.Manifest.permission.NETWORK_SETTINGS, |
| android.Manifest.permission.NETWORK_SETUP_WIZARD}) |
| public boolean connectHotspotNetwork(@NonNull HotspotNetwork network) { |
| Objects.requireNonNull(network, "Hotspot network cannot be null"); |
| |
| if (mService == null) { |
| return false; |
| } |
| |
| try { |
| mService.connectHotspotNetwork(network); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Exception in connectHotspotNetwork", e); |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * Send command to the implementation of {@link SharedConnectivityService} requesting |
| * disconnection from the active Hotspot Network. |
| * |
| * @param network {@link HotspotNetwork} object representing the network the user has requested |
| * to disconnect from. |
| * @return Returns true if the service received the command. Does not guarantee that the |
| * disconnection was successful. |
| */ |
| @RequiresPermission(anyOf = {android.Manifest.permission.NETWORK_SETTINGS, |
| android.Manifest.permission.NETWORK_SETUP_WIZARD}) |
| public boolean disconnectHotspotNetwork(@NonNull HotspotNetwork network) { |
| if (mService == null) { |
| return false; |
| } |
| |
| try { |
| mService.disconnectHotspotNetwork(network); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Exception in disconnectHotspotNetwork", e); |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * Send command to the implementation of {@link SharedConnectivityService} requesting connection |
| * to the specified Known Network. |
| * |
| * @param network {@link KnownNetwork} object representing the network the user has requested |
| * a connection to. |
| * @return Returns true if the service received the command. Does not guarantee that the |
| * connection was successful. |
| */ |
| @RequiresPermission(anyOf = {android.Manifest.permission.NETWORK_SETTINGS, |
| android.Manifest.permission.NETWORK_SETUP_WIZARD}) |
| public boolean connectKnownNetwork(@NonNull KnownNetwork network) { |
| Objects.requireNonNull(network, "Known network cannot be null"); |
| |
| if (mService == null) { |
| return false; |
| } |
| |
| try { |
| mService.connectKnownNetwork(network); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Exception in connectKnownNetwork", e); |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * Send command to the implementation of {@link SharedConnectivityService} requesting removal of |
| * the specified Known Network from the list of Known Networks. |
| * |
| * @return Returns true if the service received the command. Does not guarantee that the |
| * forget action was successful. |
| */ |
| @RequiresPermission(anyOf = {android.Manifest.permission.NETWORK_SETTINGS, |
| android.Manifest.permission.NETWORK_SETUP_WIZARD}) |
| public boolean forgetKnownNetwork(@NonNull KnownNetwork network) { |
| Objects.requireNonNull(network, "Known network cannot be null"); |
| |
| if (mService == null) { |
| return false; |
| } |
| |
| try { |
| mService.forgetKnownNetwork(network); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Exception in forgetKnownNetwork", e); |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * Gets the list of hotspot networks the user can select to connect to. |
| * |
| * @return Returns a {@link List} of {@link HotspotNetwork} objects, null on failure. |
| */ |
| @RequiresPermission(anyOf = {android.Manifest.permission.NETWORK_SETTINGS, |
| android.Manifest.permission.NETWORK_SETUP_WIZARD}) |
| @SuppressWarnings("NullableCollection") |
| @Nullable |
| public List<HotspotNetwork> getHotspotNetworks() { |
| if (mService == null) { |
| return null; |
| } |
| |
| try { |
| return mService.getHotspotNetworks(); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Exception in getHotspotNetworks", e); |
| } |
| return null; |
| } |
| |
| /** |
| * Gets the list of known networks the user can select to connect to. |
| * |
| * @return Returns a {@link List} of {@link KnownNetwork} objects, null on failure. |
| */ |
| @RequiresPermission(anyOf = {android.Manifest.permission.NETWORK_SETTINGS, |
| android.Manifest.permission.NETWORK_SETUP_WIZARD}) |
| @SuppressWarnings("NullableCollection") |
| @Nullable |
| public List<KnownNetwork> getKnownNetworks() { |
| if (mService == null) { |
| return null; |
| } |
| |
| try { |
| return mService.getKnownNetworks(); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Exception in getKnownNetworks", e); |
| } |
| return null; |
| } |
| |
| /** |
| * Gets the shared connectivity settings state. |
| * |
| * @return Returns a {@link SharedConnectivitySettingsState} object with the state, null on |
| * failure. |
| */ |
| @RequiresPermission(anyOf = {android.Manifest.permission.NETWORK_SETTINGS, |
| android.Manifest.permission.NETWORK_SETUP_WIZARD}) |
| @Nullable |
| public SharedConnectivitySettingsState getSettingsState() { |
| if (mService == null) { |
| return null; |
| } |
| |
| try { |
| return mService.getSettingsState(); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Exception in getSettingsState", e); |
| } |
| return null; |
| } |
| |
| /** |
| * Gets the connection status of the hotspot network the user selected to connect to. |
| * |
| * @return Returns a {@link HotspotNetworkConnectionStatus} object with the connection status, |
| * null on failure. If no connection is active the status will be |
| * {@link HotspotNetworkConnectionStatus#CONNECTION_STATUS_UNKNOWN}. |
| */ |
| @RequiresPermission(anyOf = {android.Manifest.permission.NETWORK_SETTINGS, |
| android.Manifest.permission.NETWORK_SETUP_WIZARD}) |
| @Nullable |
| public HotspotNetworkConnectionStatus getHotspotNetworkConnectionStatus() { |
| if (mService == null) { |
| return null; |
| } |
| |
| try { |
| return mService.getHotspotNetworkConnectionStatus(); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Exception in getHotspotNetworkConnectionStatus", e); |
| } |
| return null; |
| } |
| |
| /** |
| * Gets the connection status of the known network the user selected to connect to. |
| * |
| * @return Returns a {@link KnownNetworkConnectionStatus} object with the connection status, |
| * null on failure. If no connection is active the status will be |
| * {@link KnownNetworkConnectionStatus#CONNECTION_STATUS_UNKNOWN}. |
| */ |
| @RequiresPermission(anyOf = {android.Manifest.permission.NETWORK_SETTINGS, |
| android.Manifest.permission.NETWORK_SETUP_WIZARD}) |
| @Nullable |
| public KnownNetworkConnectionStatus getKnownNetworkConnectionStatus() { |
| if (mService == null) { |
| return null; |
| } |
| |
| try { |
| return mService.getKnownNetworkConnectionStatus(); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Exception in getKnownNetworkConnectionStatus", e); |
| } |
| return null; |
| } |
| } |