blob: 5869f432ae0f89a2a23036f1a819b72cd2cf25c4 [file] [log] [blame]
/*
* Copyright (C) 2022 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 com.android.server.wifi;
import static com.android.server.wifi.HalDeviceManager.HDM_CREATE_IFACE_AP;
import static com.android.server.wifi.HalDeviceManager.HDM_CREATE_IFACE_AP_BRIDGE;
import static com.android.server.wifi.HalDeviceManager.HDM_CREATE_IFACE_NAN;
import static com.android.server.wifi.HalDeviceManager.HDM_CREATE_IFACE_P2P;
import static com.android.server.wifi.HalDeviceManager.HDM_CREATE_IFACE_STA;
import android.annotation.IntDef;
import android.content.res.Resources;
import android.net.wifi.WifiContext;
import android.os.Message;
import android.os.WorkSource;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
import android.util.Pair;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
import com.android.server.wifi.util.WaitingState;
import com.android.wifi.resources.R;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
/**
* Displays dialogs asking the user to approve or reject interface priority decisions.
*/
public class InterfaceConflictManager {
private static final String TAG = "InterfaceConflictManager";
private boolean mVerboseLoggingEnabled = false;
private final WifiContext mContext;
private final FrameworkFacade mFrameworkFacade;
private final HalDeviceManager mHdm;
private final WifiThreadRunner mThreadRunner;
private final WifiDialogManager mWifiDialogManager;
private final Resources mResources;
private final boolean mUserApprovalNeeded;
private final Set<String> mUserApprovalExemptedPackages;
private boolean mUserApprovalNeededOverride = false;
private boolean mUserApprovalNeededOverrideValue = false;
private Object mLock = new Object();
private boolean mUserApprovalPending = false;
private String mUserApprovalPendingTag = null;
private boolean mUserJustApproved = false;
private static final String MESSAGE_BUNDLE_KEY_PENDING_USER = "pending_user_decision";
public InterfaceConflictManager(WifiContext wifiContext, FrameworkFacade frameworkFacade,
HalDeviceManager hdm, WifiThreadRunner threadRunner,
WifiDialogManager wifiDialogManager) {
mContext = wifiContext;
mFrameworkFacade = frameworkFacade;
mHdm = hdm;
mThreadRunner = threadRunner;
mWifiDialogManager = wifiDialogManager;
mResources = mContext.getResources();
mUserApprovalNeeded = mResources.getBoolean(
R.bool.config_wifiUserApprovalRequiredForD2dInterfacePriority);
String[] packageList = mResources.getStringArray(
R.array.config_wifiExcludedFromUserApprovalForD2dInterfacePriority);
mUserApprovalExemptedPackages =
(packageList == null || packageList.length == 0) ? Collections.emptySet()
: new ArraySet<>(packageList);
}
/**
* Enable verbose logging.
*/
public void enableVerboseLogging(boolean verboseEnabled) {
mVerboseLoggingEnabled = verboseEnabled;
}
/**
* Returns an indication as to whether user approval is needed for this specific request. User
* approval is controlled by:
* - A global overlay `config_wifiUserApprovalRequiredForD2dInterfacePriority`
* - An exemption list overlay `config_wifiExcludedFromUserApprovalForD2dInterfacePriority`
* which is a list of packages which are *exempted* from user approval
* - A shell command which can be used to override
*
* @param requestorWs The WorkSource of the requestor - used to determine whether it is exempted
* from user approval. All requesting packages must be exempted for the
* dialog to NOT be displayed.
*/
private boolean isUserApprovalNeeded(WorkSource requestorWs) {
if (mUserApprovalNeededOverride) return mUserApprovalNeededOverrideValue;
if (!mUserApprovalNeeded || mUserApprovalExemptedPackages.isEmpty()) {
return mUserApprovalNeeded;
}
for (int i = 0; i < requestorWs.size(); ++i) {
if (!mUserApprovalExemptedPackages.contains(requestorWs.getPackageName(i))) {
return true;
}
}
return false; // all packages of the requestor are excluded
}
/**
* Override (potentially) the user approval needed device configuration. Intended for debugging
* via the shell command.
*
* @param override Enable overriding the default.
* @param overrideValue The actual override value (i.e. disable or enable).
*/
public void setUserApprovalNeededOverride(boolean override, boolean overrideValue) {
if (mVerboseLoggingEnabled) {
Log.d(TAG, "setUserApprovalNeededOverride: override=" + override + ", overrideValue="
+ overrideValue);
}
mUserApprovalNeededOverride = override;
mUserApprovalNeededOverrideValue = overrideValue;
}
/**
* Return values for {@link #manageInterfaceConflictForStateMachine}
*/
// Caller should continue and execute command: no need for user approval, or user approval
// already granted, or command bound to fail so just fail through the normal path
public static final int ICM_EXECUTE_COMMAND = 0;
// Caller should skip executing the command for now (do not defer it - already done!). The user
// was asked for permission and the command will be executed again when we get a response.
public static final int ICM_SKIP_COMMAND_WAIT_FOR_USER = 1;
// Caller should abort the command and execute whatever failure code is necessary - this
// command was rejected by the user or we cannot ask the user since there's a pending user
// request.
public static final int ICM_ABORT_COMMAND = 2;
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = {"ICM_"}, value = {
ICM_EXECUTE_COMMAND,
ICM_SKIP_COMMAND_WAIT_FOR_USER,
ICM_ABORT_COMMAND
})
@interface IcmResult {}
/**
* Manages interface conflicts for a State Machine based caller. Possible scenarios:
* - New request:
* - ok to proceed inline (i.e. caller can just proceed normally - no conflict)
* [nop]
* - need to request user approval (there's conflict, caller need to wait for user response)
* [msg get tagged + deferred, transition to waiting state]
* - Previously executed command (i.e. already asked the user)
* - user rejected request
* [discard request, execute any necessary error callbacks]
* - user approved request
* [~nop (i.e. proceed)]
* - Busy asking approval for another request:
* - If from another caller: reject
* - If from the same caller: defer the caller (possibly will be approved when gets to ask
* again).
*
* Synchronization:
* - Multiple threads accessing this method will be blocked until the processing of the other
* thread is done. The "processing" is simply the decision making - i.e. not the waiting for
* user response.
* - If a user response is pending then subsequent requests are auto-rejected if they require
* user approval. Note that this will result in race condition if this approval changes
* the conditions for the user approval request: e.g. it may increase the impact of a user
* approval (w/o telling the user) or it may be rejected even if approved by the user (if
* the newly allocated interface now has higher priority).
*
* @param tag Tag of the caller for logging
* @param msg The command which needs to be evaluated or executed for user approval
* @param stateMachine The source state machine
* @param waitingState The {@link WaitingState} added to the above state machine
* @param targetState The target state to transition to on user response
* @param createIfaceType The interface which needs to be created
* @param requestorWs The requestor WorkSource
*
* @return ICM_EXECUTE_COMMAND caller should execute the command,
* ICM_SKIP_COMMAND_WAIT_FOR_USER caller should skip the command (for now),
* ICM_ABORT_COMMAND caller should abort this command and execute whatever failure code is
* necessary.
*/
public @IcmResult int manageInterfaceConflictForStateMachine(String tag, Message msg,
StateMachine stateMachine, WaitingState waitingState, State targetState,
@HalDeviceManager.HdmIfaceTypeForCreation int createIfaceType, WorkSource requestorWs) {
synchronized (mLock) {
if (mUserApprovalPending && !TextUtils.equals(tag, mUserApprovalPendingTag)) {
Log.w(TAG, tag + ": rejected since there's a pending user approval for "
+ mUserApprovalPendingTag);
return ICM_ABORT_COMMAND; // caller should not proceed with operation
}
// is this a command which was waiting for a user decision?
boolean isReexecutedCommand = msg.getData().getBoolean(
MESSAGE_BUNDLE_KEY_PENDING_USER, false);
if (isReexecutedCommand) {
mUserApprovalPending = false;
mUserApprovalPendingTag = null;
if (mVerboseLoggingEnabled) {
Log.d(TAG, tag + ": Re-executing a command with user approval result - "
+ mUserJustApproved);
}
return mUserJustApproved ? ICM_EXECUTE_COMMAND : ICM_ABORT_COMMAND;
}
if (mUserApprovalPending) {
Log.w(TAG, tag
+ ": trying for another potentially waiting operation - but should be"
+ " in a waiting state!?");
stateMachine.deferMessage(msg);
return ICM_SKIP_COMMAND_WAIT_FOR_USER; // same effect
}
if (!isUserApprovalNeeded(requestorWs)) return ICM_EXECUTE_COMMAND;
List<Pair<Integer, WorkSource>> impact = mHdm.reportImpactToCreateIface(createIfaceType,
false, requestorWs);
if (mVerboseLoggingEnabled) {
Log.d(TAG, tag + ": Asking user about creating the interface, impact=" + impact);
}
if (impact == null || impact.isEmpty()) {
Log.d(TAG, tag
+ ": Either can't create interface or can w/o sid-effects - proceeding");
return ICM_EXECUTE_COMMAND;
}
displayUserApprovalDialog(createIfaceType, requestorWs, impact,
(result) -> {
if (mVerboseLoggingEnabled) {
Log.d(TAG, tag + ": User response to creating " + getInterfaceName(
createIfaceType) + ": " + result);
}
mUserJustApproved = result;
waitingState.sendTransitionStateCommand(targetState);
});
// defer message to have it executed again automatically when switching
// states - want to do it now so that it will be at the top of the queue
// when we switch back. Will need to skip it if the user rejected it!
msg.getData().putBoolean(MESSAGE_BUNDLE_KEY_PENDING_USER, true);
stateMachine.deferMessage(msg);
stateMachine.transitionTo(waitingState);
mUserApprovalPending = true;
mUserApprovalPendingTag = tag;
return ICM_SKIP_COMMAND_WAIT_FOR_USER;
}
}
/**
* Trigger a dialog which requests user approval to resolve an interface priority confict.
*
* @param createIfaceType The interface to be created.
* @param requestorWs The WorkSource of the requesting application.
* @param impact The impact of creating this interface (a list of interfaces to be deleted and
* their corresponding impacted WorkSources).
* @param handleResult A Consumer to execute with results.
*/
private void displayUserApprovalDialog(
@HalDeviceManager.HdmIfaceTypeForCreation int createIfaceType,
WorkSource requestorWs,
List<Pair<Integer, WorkSource>> impact,
Consumer<Boolean> handleResult) {
if (mVerboseLoggingEnabled) {
Log.d(TAG, "displayUserApprovalDialog: createIfaceType=" + createIfaceType
+ ", requestorWs=" + requestorWs + ", impact=" + impact);
}
CharSequence requestorAppName = mFrameworkFacade.getAppName(mContext,
requestorWs.getPackageName(0), requestorWs.getUid(0));
String requestedInterface = getInterfaceName(createIfaceType);
Set<String> impactedPackagesSet = new HashSet<>();
for (Pair<Integer, WorkSource> detail : impact) {
for (int j = 0; j < detail.second.size(); ++j) {
impactedPackagesSet.add(
mFrameworkFacade.getAppName(mContext, detail.second.getPackageName(j),
detail.second.getUid(j)).toString());
}
}
String impactedPackages = TextUtils.join(", ", impactedPackagesSet);
mWifiDialogManager.createSimpleDialog(
mResources.getString(R.string.wifi_interface_priority_title, requestorAppName),
impactedPackagesSet.size() == 1 ? mResources.getString(
R.string.wifi_interface_priority_message, requestorAppName,
requestedInterface, impactedPackages)
: mResources.getString(R.string.wifi_interface_priority_message_plural,
requestorAppName, requestedInterface, impactedPackages),
mResources.getString(R.string.wifi_interface_priority_approve),
mResources.getString(R.string.wifi_interface_priority_reject),
null,
new WifiDialogManager.SimpleDialogCallback() {
@Override
public void onPositiveButtonClicked() {
if (mVerboseLoggingEnabled) {
Log.d(TAG, "User approved request for " + getInterfaceName(
createIfaceType));
}
handleResult.accept(true);
}
@Override
public void onNegativeButtonClicked() {
if (mVerboseLoggingEnabled) {
Log.d(TAG, "User rejected request for " + getInterfaceName(
createIfaceType));
}
handleResult.accept(false);
}
@Override
public void onNeutralButtonClicked() {
onNegativeButtonClicked();
}
@Override
public void onCancelled() {
onNegativeButtonClicked();
}
}, mThreadRunner).launchDialog();
}
private String getInterfaceName(@HalDeviceManager.HdmIfaceTypeForCreation int createIfaceType) {
switch (createIfaceType) {
case HDM_CREATE_IFACE_STA:
return "STA";
case HDM_CREATE_IFACE_AP:
return "AP";
case HDM_CREATE_IFACE_AP_BRIDGE:
return "AP";
case HDM_CREATE_IFACE_P2P:
return "Wi-Fi Direct";
case HDM_CREATE_IFACE_NAN:
return "Wi-Fi Aware";
}
return "Unknown";
}
/**
* Dump the internal state of the class.
*/
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println("dump of " + TAG + ":");
pw.println(" mUserApprovalNeeded=" + mUserApprovalNeeded);
pw.println(" mUserApprovalNeededOverride=" + mUserApprovalNeededOverride);
pw.println(" mUserApprovalNeededOverrideValue=" + mUserApprovalNeededOverrideValue);
pw.println(" mUserApprovalPending=" + mUserApprovalPending);
pw.println(" mUserApprovalPendingTag=" + mUserApprovalPendingTag);
pw.println(" mUserJustApproved=" + mUserJustApproved);
}
}