blob: feef0497c152bc3bf1afef6c527abf66a24ed6ce [file] [log] [blame]
/*
* 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;
}
}