blob: 3124d57243613244ae0848ae5363f96e2628178a [file] [log] [blame]
/*
* Copyright (C) 2021 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.uwb;
import static android.uwb.UwbAddress.SHORT_ADDRESS_BYTE_LENGTH;
import static com.google.uwb.support.ccc.CccParams.CHAPS_PER_SLOT_3;
import static com.google.uwb.support.ccc.CccParams.HOPPING_CONFIG_MODE_ADAPTIVE;
import static com.google.uwb.support.ccc.CccParams.HOPPING_CONFIG_MODE_CONTINUOUS;
import static com.google.uwb.support.ccc.CccParams.HOPPING_SEQUENCE_AES;
import static com.google.uwb.support.ccc.CccParams.HOPPING_SEQUENCE_DEFAULT;
import static com.google.uwb.support.ccc.CccParams.PULSE_SHAPE_SYMMETRICAL_ROOT_RAISED_COSINE;
import static com.google.uwb.support.ccc.CccParams.SLOTS_PER_ROUND_6;
import static com.google.uwb.support.ccc.CccParams.UWB_CHANNEL_9;
import static com.google.uwb.support.fira.FiraParams.AOA_RESULT_REQUEST_MODE_NO_AOA_REPORT;
import static com.google.uwb.support.fira.FiraParams.AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS;
import static com.google.uwb.support.fira.FiraParams.AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS_AZIMUTH_ONLY;
import static com.google.uwb.support.fira.FiraParams.AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS_ELEVATION_ONLY;
import static com.google.uwb.support.fira.FiraParams.AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS_INTERLEAVED;
import static com.google.uwb.support.fira.FiraParams.HOPPING_MODE_DISABLE;
import static com.google.uwb.support.fira.FiraParams.MULTICAST_LIST_UPDATE_ACTION_ADD;
import static com.google.uwb.support.fira.FiraParams.MULTICAST_LIST_UPDATE_ACTION_DELETE;
import static com.google.uwb.support.fira.FiraParams.MULTI_NODE_MODE_ONE_TO_MANY;
import static com.google.uwb.support.fira.FiraParams.MULTI_NODE_MODE_UNICAST;
import static com.google.uwb.support.fira.FiraParams.RANGING_DEVICE_ROLE_INITIATOR;
import static com.google.uwb.support.fira.FiraParams.RANGING_DEVICE_ROLE_RESPONDER;
import static com.google.uwb.support.fira.FiraParams.RANGING_DEVICE_TYPE_CONTROLEE;
import static com.google.uwb.support.fira.FiraParams.RANGING_DEVICE_TYPE_CONTROLLER;
import static com.google.uwb.support.fira.FiraParams.RANGING_ROUND_USAGE_DS_TWR_DEFERRED_MODE;
import static com.google.uwb.support.fira.FiraParams.RANGING_ROUND_USAGE_DS_TWR_NON_DEFERRED_MODE;
import static com.google.uwb.support.fira.FiraParams.RANGING_ROUND_USAGE_SS_TWR_DEFERRED_MODE;
import static com.google.uwb.support.fira.FiraParams.RANGING_ROUND_USAGE_SS_TWR_NON_DEFERRED_MODE;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.annotation.NonNull;
import android.content.AttributionSource;
import android.content.Context;
import android.os.Binder;
import android.os.PersistableBundle;
import android.os.Process;
import android.os.RemoteException;
import android.util.ArrayMap;
import android.util.Pair;
import android.uwb.IUwbRangingCallbacks;
import android.uwb.RangingReport;
import android.uwb.SessionHandle;
import android.uwb.UwbAddress;
import android.uwb.UwbManager;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.BasicShellCommandHandler;
import com.android.server.uwb.jni.NativeUwbManager;
import com.android.server.uwb.util.ArrayUtils;
import com.google.uwb.support.base.Params;
import com.google.uwb.support.ccc.CccOpenRangingParams;
import com.google.uwb.support.ccc.CccParams;
import com.google.uwb.support.ccc.CccPulseShapeCombo;
import com.google.uwb.support.ccc.CccStartRangingParams;
import com.google.uwb.support.fira.FiraOpenSessionParams;
import com.google.uwb.support.fira.FiraParams;
import com.google.uwb.support.fira.FiraRangingReconfigureParams;
import java.io.PrintWriter;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
/**
* Interprets and executes 'adb shell cmd uwb [args]'.
*
* To add new commands:
* - onCommand: Add a case "<command>" execute. Return a 0
* if command executed successfully.
* - onHelp: add a description string.
*
* Permissions: currently root permission is required for some commands. Others will
* enforce the corresponding API permissions.
*/
public class UwbShellCommand extends BasicShellCommandHandler {
@VisibleForTesting
public static String SHELL_PACKAGE_NAME = "com.android.shell";
private static final long RANGE_CTL_TIMEOUT_MILLIS = 10_000;
// These don't require root access.
// However, these do perform permission checks in the corresponding UwbService methods.
private static final String[] NON_PRIVILEGED_COMMANDS = {
"help",
"status",
"get-country-code",
"enable-uwb",
"disable-uwb",
"start-fira-ranging-session",
"start-ccc-ranging-session",
"reconfigure-fira-ranging-session",
"get-ranging-session-reports",
"get-all-ranging-session-reports",
"stop-ranging-session",
"stop-all-ranging-sessions",
"get-specification-info",
};
@VisibleForTesting
public static final FiraOpenSessionParams.Builder DEFAULT_FIRA_OPEN_SESSION_PARAMS =
new FiraOpenSessionParams.Builder()
.setProtocolVersion(FiraParams.PROTOCOL_VERSION_1_1)
.setSessionId(1)
.setChannelNumber(9)
.setDeviceType(RANGING_DEVICE_TYPE_CONTROLLER)
.setDeviceRole(RANGING_DEVICE_ROLE_INITIATOR)
.setDeviceAddress(UwbAddress.fromBytes(new byte[] { 0x4, 0x6}))
.setDestAddressList(Arrays.asList(UwbAddress.fromBytes(new byte[] { 0x4, 0x6})))
.setMultiNodeMode(MULTI_NODE_MODE_UNICAST)
.setRangingRoundUsage(RANGING_ROUND_USAGE_DS_TWR_DEFERRED_MODE)
.setVendorId(new byte[]{0x5, 0x78})
.setStaticStsIV(new byte[]{0x1a, 0x55, 0x77, 0x47, 0x7e, 0x7d});
@VisibleForTesting
public static final CccOpenRangingParams.Builder DEFAULT_CCC_OPEN_RANGING_PARAMS =
new CccOpenRangingParams.Builder()
.setProtocolVersion(CccParams.PROTOCOL_VERSION_1_0)
.setUwbConfig(CccParams.UWB_CONFIG_0)
.setPulseShapeCombo(
new CccPulseShapeCombo(
PULSE_SHAPE_SYMMETRICAL_ROOT_RAISED_COSINE,
PULSE_SHAPE_SYMMETRICAL_ROOT_RAISED_COSINE))
.setSessionId(1)
.setRanMultiplier(4)
.setChannel(UWB_CHANNEL_9)
.setNumChapsPerSlot(CHAPS_PER_SLOT_3)
.setNumResponderNodes(1)
.setNumSlotsPerRound(SLOTS_PER_ROUND_6)
.setSyncCodeIndex(1)
.setHoppingConfigMode(HOPPING_MODE_DISABLE)
.setHoppingSequence(HOPPING_SEQUENCE_DEFAULT);
private static final Map<Integer, SessionInfo> sSessionIdToInfo = new ArrayMap<>();
private static int sSessionHandleIdNext = 0;
private final UwbServiceImpl mUwbService;
private final UwbCountryCode mUwbCountryCode;
private final NativeUwbManager mNativeUwbManager;
private final Context mContext;
UwbShellCommand(UwbInjector uwbInjector, UwbServiceImpl uwbService, Context context) {
mUwbService = uwbService;
mContext = context;
mUwbCountryCode = uwbInjector.getUwbCountryCode();
mNativeUwbManager = uwbInjector.getNativeUwbManager();
}
private static String bundleToString(@Nullable PersistableBundle bundle) {
if (bundle != null) {
// Need to defuse any local bundles before printing. Use isEmpty() triggers unparcel.
bundle.isEmpty();
return bundle.toString();
} else {
return "null";
}
}
private static final class UwbRangingCallbacks extends IUwbRangingCallbacks.Stub {
private final SessionInfo mSessionInfo;
private final PrintWriter mPw;
private final CompletableFuture mRangingOpenedFuture;
private final CompletableFuture mRangingStartedFuture;
private final CompletableFuture mRangingStoppedFuture;
private final CompletableFuture mRangingClosedFuture;
private final CompletableFuture mRangingReconfiguredFuture;
UwbRangingCallbacks(@NonNull SessionInfo sessionInfo, @NonNull PrintWriter pw,
@NonNull CompletableFuture rangingOpenedFuture,
@NonNull CompletableFuture rangingStartedFuture,
@NonNull CompletableFuture rangingStoppedFuture,
@NonNull CompletableFuture rangingClosedFuture,
@NonNull CompletableFuture rangingReconfiguredFuture) {
mSessionInfo = sessionInfo;
mPw = pw;
mRangingOpenedFuture = rangingOpenedFuture;
mRangingStartedFuture = rangingStartedFuture;
mRangingStoppedFuture = rangingStoppedFuture;
mRangingClosedFuture = rangingClosedFuture;
mRangingReconfiguredFuture = rangingReconfiguredFuture;
}
public void onRangingOpened(SessionHandle sessionHandle) {
mPw.println("Ranging session opened");
mRangingOpenedFuture.complete(true);
}
public void onRangingOpenFailed(SessionHandle sessionHandle, int reason,
PersistableBundle params) {
mPw.println("Ranging session open failed with reason: " + reason + " and params: "
+ bundleToString(params));
mRangingOpenedFuture.complete(false);
}
public void onRangingStarted(SessionHandle sessionHandle, PersistableBundle params) {
mPw.println("Ranging session started with params: " + bundleToString(params));
mRangingStartedFuture.complete(true);
}
public void onRangingStartFailed(SessionHandle sessionHandle, int reason,
PersistableBundle params) {
mPw.println("Ranging session start failed with reason: " + reason + " and params: "
+ bundleToString(params));
mRangingStartedFuture.complete(false);
}
public void onRangingReconfigured(SessionHandle sessionHandle, PersistableBundle params) {
mPw.println("Ranging reconfigured with params: " + bundleToString(params));
mRangingReconfiguredFuture.complete(true);
}
public void onRangingReconfigureFailed(SessionHandle sessionHandle, int reason,
PersistableBundle params) {
mPw.println("Ranging reconfigure failed with reason: " + reason + " and params: "
+ bundleToString(params));
mRangingReconfiguredFuture.complete(true);
}
public void onRangingStopped(SessionHandle sessionHandle, int reason,
PersistableBundle params) {
mPw.println("Ranging session stopped with reason: " + reason + " and params: "
+ bundleToString(params));
mRangingStoppedFuture.complete(true);
}
public void onRangingStopFailed(SessionHandle sessionHandle, int reason,
PersistableBundle params) {
mPw.println("Ranging session stop failed with reason: " + reason + " and params: "
+ bundleToString(params));
mRangingStoppedFuture.complete(false);
}
public void onRangingClosed(SessionHandle sessionHandle, int reason,
PersistableBundle params) {
mPw.println("Ranging session closed with reason: " + reason + " and params: "
+ bundleToString(params));
sSessionIdToInfo.remove(mSessionInfo.sessionId);
mRangingClosedFuture.complete(true);
}
public void onRangingResult(SessionHandle sessionHandle, RangingReport rangingReport) {
mPw.println("Ranging Result: " + rangingReport);
mSessionInfo.addRangingReport(rangingReport);
}
public void onControleeAdded(SessionHandle sessionHandle, PersistableBundle params) {}
public void onControleeAddFailed(SessionHandle sessionHandle, int reason,
PersistableBundle params) {}
public void onControleeRemoved(SessionHandle sessionHandle, PersistableBundle params) {}
public void onControleeRemoveFailed(SessionHandle sessionHandle, int reason,
PersistableBundle params) {}
public void onRangingPaused(SessionHandle sessionHandle, PersistableBundle params) {}
public void onRangingPauseFailed(SessionHandle sessionHandle, int reason,
PersistableBundle params) {}
public void onRangingResumed(SessionHandle sessionHandle, PersistableBundle params) {}
public void onRangingResumeFailed(SessionHandle sessionHandle, int reason,
PersistableBundle params) {}
public void onDataSent(SessionHandle sessionHandle, UwbAddress uwbAddress,
PersistableBundle params) {}
public void onDataSendFailed(SessionHandle sessionHandle, UwbAddress uwbAddress, int reason,
PersistableBundle params) {}
public void onDataReceived(SessionHandle sessionHandle, UwbAddress uwbAddress,
PersistableBundle params, byte[] data) {}
public void onDataReceiveFailed(SessionHandle sessionHandle, UwbAddress uwbAddress,
int reason, PersistableBundle params) {}
public void onServiceDiscovered(SessionHandle sessionHandle, PersistableBundle params) {}
public void onServiceConnected(SessionHandle sessionHandle, PersistableBundle params) {}
}
private class SessionInfo {
private static final int LAST_NUM_RANGING_REPORTS = 20;
public final SessionHandle sessionHandle;
public final int sessionId;
public final Params openRangingParams;
public final UwbRangingCallbacks uwbRangingCbs;
public final ArrayDeque<RangingReport> lastRangingReports =
new ArrayDeque<>(LAST_NUM_RANGING_REPORTS);
public final CompletableFuture<Boolean> rangingOpenedFuture = new CompletableFuture<>();
public final CompletableFuture<Boolean> rangingStartedFuture = new CompletableFuture<>();
public final CompletableFuture<Boolean> rangingStoppedFuture = new CompletableFuture<>();
public final CompletableFuture<Boolean> rangingClosedFuture = new CompletableFuture<>();
public final CompletableFuture<Boolean> rangingReconfiguredFuture =
new CompletableFuture<>();
SessionInfo(int sessionId, int sSessionHandleIdNext, @NonNull Params openRangingParams,
@NonNull PrintWriter pw) {
this.sessionId = sessionId;
sessionHandle = new SessionHandle(sSessionHandleIdNext);
this.openRangingParams = openRangingParams;
uwbRangingCbs = new UwbRangingCallbacks(this, pw, rangingOpenedFuture,
rangingStartedFuture, rangingStoppedFuture, rangingClosedFuture,
rangingReconfiguredFuture);
}
public void addRangingReport(@NonNull RangingReport rangingReport) {
if (lastRangingReports.size() == LAST_NUM_RANGING_REPORTS) {
lastRangingReports.remove();
}
lastRangingReports.add(rangingReport);
}
}
private Pair<FiraOpenSessionParams, Boolean> buildFiraOpenSessionParams() {
FiraOpenSessionParams.Builder builder =
new FiraOpenSessionParams.Builder(DEFAULT_FIRA_OPEN_SESSION_PARAMS);
boolean shouldBlockCall = false;
String option = getNextOption();
while (option != null) {
if (option.equals("-b")) {
shouldBlockCall = true;
}
if (option.equals("-i")) {
builder.setSessionId(Integer.parseInt(getNextArgRequired()));
}
if (option.equals("-c")) {
builder.setChannelNumber(Integer.parseInt(getNextArgRequired()));
}
if (option.equals("-t")) {
String type = getNextArgRequired();
if (type.equals("controller")) {
builder.setDeviceType(RANGING_DEVICE_TYPE_CONTROLLER);
} else if (type.equals("controlee")) {
builder.setDeviceType(RANGING_DEVICE_TYPE_CONTROLEE);
} else {
throw new IllegalArgumentException("Unknown device type: " + type);
}
}
if (option.equals("-r")) {
String role = getNextArgRequired();
if (role.equals("initiator")) {
builder.setDeviceType(RANGING_DEVICE_ROLE_INITIATOR);
} else if (role.equals("responder")) {
builder.setDeviceType(RANGING_DEVICE_ROLE_RESPONDER);
} else {
throw new IllegalArgumentException("Unknown device role: " + role);
}
}
if (option.equals("-a")) {
builder.setDeviceAddress(
UwbAddress.fromBytes(
ByteBuffer.allocate(SHORT_ADDRESS_BYTE_LENGTH)
.putShort(Short.parseShort(getNextArgRequired()))
.array()));
}
if (option.equals("-d")) {
String[] destAddressesString = getNextArgRequired().split(",");
List<UwbAddress> destAddresses = new ArrayList<>();
for (String destAddressString : destAddressesString) {
destAddresses.add(UwbAddress.fromBytes(
ByteBuffer.allocate(SHORT_ADDRESS_BYTE_LENGTH)
.putShort(Short.parseShort(destAddressString))
.array()));
}
builder.setDestAddressList(destAddresses);
builder.setMultiNodeMode(destAddresses.size() > 1
? MULTI_NODE_MODE_ONE_TO_MANY
: MULTI_NODE_MODE_UNICAST);
}
if (option.equals("-u")) {
String usage = getNextArgRequired();
if (usage.equals("ds-twr")) {
builder.setRangingRoundUsage(RANGING_ROUND_USAGE_DS_TWR_DEFERRED_MODE);
} else if (usage.equals("ss-twr")) {
builder.setRangingRoundUsage(RANGING_ROUND_USAGE_SS_TWR_DEFERRED_MODE);
} else if (usage.equals("ds-twr-non-deferred")) {
builder.setRangingRoundUsage(RANGING_ROUND_USAGE_DS_TWR_NON_DEFERRED_MODE);
} else if (usage.equals("ss-twr-non-deferred")) {
builder.setRangingRoundUsage(RANGING_ROUND_USAGE_SS_TWR_NON_DEFERRED_MODE);
} else {
throw new IllegalArgumentException("Unknown round usage: " + usage);
}
}
if (option.equals("-z")) {
String[] interleaveRatioString = getNextArgRequired().split(",");
if (interleaveRatioString.length != 3) {
throw new IllegalArgumentException("Unexpected interleaving ratio: "
+ Arrays.toString(interleaveRatioString)
+ " expected to be <numRange, numAoaAzimuth, numAoaElevation>");
}
int numOfRangeMsrmts = Integer.parseInt(interleaveRatioString[0]);
int numOfAoaAzimuthMrmts = Integer.parseInt(interleaveRatioString[1]);
int numOfAoaElevationMrmts = Integer.parseInt(interleaveRatioString[2]);
// Set to interleaving mode
builder.setAoaResultRequest(AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS_INTERLEAVED);
builder.setMeasurementFocusRatio(
numOfRangeMsrmts,
numOfAoaAzimuthMrmts,
numOfAoaElevationMrmts);
}
if (option.equals("-e")) {
String aoaType = getNextArgRequired();
if (aoaType.equals("none")) {
builder.setAoaResultRequest(AOA_RESULT_REQUEST_MODE_NO_AOA_REPORT);
} else if (aoaType.equals("enabled")) {
builder.setAoaResultRequest(AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS);
} else if (aoaType.equals("azimuth-only")) {
builder.setAoaResultRequest(
AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS_AZIMUTH_ONLY);
} else if (aoaType.equals("elevation-only")) {
builder.setAoaResultRequest(
AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS_ELEVATION_ONLY);
} else {
throw new IllegalArgumentException("Unknown aoa type: " + aoaType);
}
}
option = getNextOption();
}
// TODO: Add remaining params if needed.
return Pair.create(builder.build(), shouldBlockCall);
}
private void startFiraRangingSession(PrintWriter pw) throws Exception {
Pair<FiraOpenSessionParams, Boolean> firaOpenSessionParams = buildFiraOpenSessionParams();
startRangingSession(
firaOpenSessionParams.first, null, firaOpenSessionParams.first.getSessionId(),
firaOpenSessionParams.second, pw);
}
private Pair<CccOpenRangingParams, Boolean> buildCccOpenRangingParams() {
CccOpenRangingParams.Builder builder =
new CccOpenRangingParams.Builder(DEFAULT_CCC_OPEN_RANGING_PARAMS);
boolean shouldBlockCall = false;
String option = getNextOption();
while (option != null) {
if (option.equals("-b")) {
shouldBlockCall = true;
}
if (option.equals("-u")) {
builder.setUwbConfig(Integer.parseInt(getNextArgRequired()));
}
if (option.equals("-p")) {
String[] pulseComboString = getNextArgRequired().split(",");
if (pulseComboString.length != 2) {
throw new IllegalArgumentException("Erroneous pulse combo: "
+ Arrays.toString(pulseComboString));
}
builder.setPulseShapeCombo(new CccPulseShapeCombo(
Integer.parseInt(pulseComboString[0]),
Integer.parseInt(pulseComboString[1])));
}
if (option.equals("-i")) {
builder.setSessionId(Integer.parseInt(getNextArgRequired()));
}
if (option.equals("-r")) {
builder.setRanMultiplier(Integer.parseInt(getNextArgRequired()));
}
if (option.equals("-c")) {
builder.setChannel(Integer.parseInt(getNextArgRequired()));
}
if (option.equals("-p")) {
builder.setNumChapsPerSlot(Integer.parseInt(getNextArgRequired()));
}
if (option.equals("-n")) {
builder.setNumResponderNodes(Integer.parseInt(getNextArgRequired()));
}
if (option.equals("-o")) {
builder.setNumSlotsPerRound(Integer.parseInt(getNextArgRequired()));
}
if (option.equals("-s")) {
builder.setSyncCodeIndex(Integer.parseInt(getNextArgRequired()));
}
if (option.equals("-h")) {
String hoppingConfigMode = getNextArgRequired();
if (hoppingConfigMode.equals("none")) {
builder.setHoppingConfigMode(HOPPING_MODE_DISABLE);
} else if (hoppingConfigMode.equals("continuous")) {
builder.setHoppingConfigMode(HOPPING_CONFIG_MODE_CONTINUOUS);
} else if (hoppingConfigMode.equals("adaptive")) {
builder.setHoppingConfigMode(HOPPING_CONFIG_MODE_ADAPTIVE);
} else {
throw new IllegalArgumentException("Unknown hopping config mode: "
+ hoppingConfigMode);
}
}
if (option.equals("-a")) {
String hoppingSequence = getNextArgRequired();
if (hoppingSequence.equals("default")) {
builder.setHoppingSequence(HOPPING_SEQUENCE_DEFAULT);
} else if (hoppingSequence.equals("aes")) {
builder.setHoppingConfigMode(HOPPING_SEQUENCE_AES);
} else {
throw new IllegalArgumentException("Unknown hopping sequence: "
+ hoppingSequence);
}
}
option = getNextOption();
}
// TODO: Add remaining params if needed.
return Pair.create(builder.build(), shouldBlockCall);
}
private void startCccRangingSession(PrintWriter pw) throws Exception {
Pair<CccOpenRangingParams, Boolean> cccOpenRangingParamsAndBlocking =
buildCccOpenRangingParams();
CccOpenRangingParams cccOpenRangingParams = cccOpenRangingParamsAndBlocking.first;
CccStartRangingParams cccStartRangingParams = new CccStartRangingParams.Builder()
.setSessionId(cccOpenRangingParams.getSessionId())
.setRanMultiplier(cccOpenRangingParams.getRanMultiplier())
.build();
startRangingSession(
cccOpenRangingParams, cccStartRangingParams, cccOpenRangingParams.getSessionId(),
cccOpenRangingParamsAndBlocking.second, pw);
}
private void startRangingSession(@NonNull Params openRangingSessionParams,
@Nullable Params startRangingSessionParams, int sessionId,
boolean shouldBlockCall, @NonNull PrintWriter pw) throws Exception {
if (sSessionIdToInfo.containsKey(sessionId)) {
pw.println("Session with session ID: " + sessionId
+ " already ongoing. Stop that session before you start a new session");
return;
}
SessionInfo sessionInfo =
new SessionInfo(sessionId, sSessionHandleIdNext++, openRangingSessionParams, pw);
mUwbService.openRanging(
new AttributionSource.Builder(Process.SHELL_UID)
.setPackageName(SHELL_PACKAGE_NAME)
.build(),
sessionInfo.sessionHandle,
sessionInfo.uwbRangingCbs,
openRangingSessionParams.toBundle(),
null);
boolean openCompleted = false;
try {
openCompleted = sessionInfo.rangingOpenedFuture.get(
RANGE_CTL_TIMEOUT_MILLIS, MILLISECONDS);
} catch (InterruptedException | CancellationException | TimeoutException
| ExecutionException e) {
}
if (!openCompleted) {
pw.println("Failed to open ranging session. Aborting!");
return;
}
pw.println("Ranging session opened with params: "
+ bundleToString(openRangingSessionParams.toBundle()));
mUwbService.startRanging(
sessionInfo.sessionHandle,
startRangingSessionParams != null
? startRangingSessionParams.toBundle()
: new PersistableBundle());
boolean startCompleted = false;
try {
startCompleted = sessionInfo.rangingStartedFuture.get(
RANGE_CTL_TIMEOUT_MILLIS, MILLISECONDS);
} catch (InterruptedException | CancellationException | TimeoutException
| ExecutionException e) {
}
if (!startCompleted) {
pw.println("Failed to start ranging session. Aborting!");
return;
}
pw.println("Ranging session started for sessionId: " + sessionId);
sSessionIdToInfo.put(sessionId, sessionInfo);
while (shouldBlockCall) {
Thread.sleep(RANGE_CTL_TIMEOUT_MILLIS);
}
}
private void stopRangingSession(PrintWriter pw) throws RemoteException {
int sessionId = Integer.parseInt(getNextArgRequired());
stopRangingSession(pw, sessionId);
}
private void stopRangingSession(PrintWriter pw, int sessionId) throws RemoteException {
SessionInfo sessionInfo = sSessionIdToInfo.get(sessionId);
if (sessionInfo == null) {
pw.println("No active session with session ID: " + sessionId + " found");
return;
}
mUwbService.stopRanging(sessionInfo.sessionHandle);
boolean stopCompleted = false;
try {
stopCompleted = sessionInfo.rangingStoppedFuture.get(
RANGE_CTL_TIMEOUT_MILLIS, MILLISECONDS);
} catch (InterruptedException | CancellationException | TimeoutException
| ExecutionException e) {
}
if (!stopCompleted) {
pw.println("Failed to stop ranging session. Aborting!");
return;
}
pw.println("Ranging session stopped");
mUwbService.closeRanging(sessionInfo.sessionHandle);
boolean closeCompleted = false;
try {
closeCompleted = sessionInfo.rangingClosedFuture.get(
RANGE_CTL_TIMEOUT_MILLIS, MILLISECONDS);
} catch (InterruptedException | CancellationException | TimeoutException
| ExecutionException e) {
}
if (!closeCompleted) {
pw.println("Failed to close ranging session. Aborting!");
return;
}
pw.println("Ranging session closed");
}
private FiraRangingReconfigureParams buildFiraReconfigureParams() {
FiraRangingReconfigureParams.Builder builder =
new FiraRangingReconfigureParams.Builder();
// defaults
builder.setAction(MULTICAST_LIST_UPDATE_ACTION_ADD);
String option = getNextOption();
while (option != null) {
if (option.equals("-a")) {
String action = getNextArgRequired();
if (action.equals("add")) {
builder.setAction(MULTICAST_LIST_UPDATE_ACTION_ADD);
} else if (action.equals("delete")) {
builder.setAction(MULTICAST_LIST_UPDATE_ACTION_DELETE);
} else {
throw new IllegalArgumentException("Unexpected action " + action);
}
}
if (option.equals("-d")) {
String[] destAddressesString = getNextArgRequired().split(",");
List<UwbAddress> destAddresses = new ArrayList<>();
for (String destAddressString : destAddressesString) {
destAddresses.add(UwbAddress.fromBytes(
ByteBuffer.allocate(SHORT_ADDRESS_BYTE_LENGTH)
.putShort(Short.parseShort(destAddressString))
.array()));
}
builder.setAddressList(destAddresses.toArray(new UwbAddress[0]));
}
if (option.equals("-s")) {
String[] subSessionIdsString = getNextArgRequired().split(",");
List<Integer> subSessionIds = new ArrayList<>();
for (String subSessionIdString : subSessionIdsString) {
subSessionIds.add(Integer.parseInt(subSessionIdString));
}
builder.setSubSessionIdList(subSessionIds.stream().mapToInt(s -> s).toArray());
}
option = getNextOption();
}
// TODO: Add remaining params if needed.
return builder.build();
}
private void reconfigureFiraRangingSession(PrintWriter pw) throws RemoteException {
int sessionId = Integer.parseInt(getNextArgRequired());
SessionInfo sessionInfo = sSessionIdToInfo.get(sessionId);
if (sessionInfo == null) {
pw.println("No active session with session ID: " + sessionId + " found");
return;
}
FiraRangingReconfigureParams params = buildFiraReconfigureParams();
mUwbService.reconfigureRanging(sessionInfo.sessionHandle, params.toBundle());
boolean reconfigureCompleted = false;
try {
reconfigureCompleted = sessionInfo.rangingClosedFuture.get(
RANGE_CTL_TIMEOUT_MILLIS, MILLISECONDS);
} catch (InterruptedException | CancellationException | TimeoutException
| ExecutionException e) {
}
if (!reconfigureCompleted) {
pw.println("Failed to reconfigure ranging session. Aborting!");
return;
}
pw.println("Ranging session reconfigured");
}
@Override
public int onCommand(String cmd) {
// Treat no command as help command.
if (cmd == null || cmd.equals("")) {
cmd = "help";
}
// Explicit exclusion from root permission
if (ArrayUtils.indexOf(NON_PRIVILEGED_COMMANDS, cmd) == -1) {
final int uid = Binder.getCallingUid();
if (uid != Process.ROOT_UID) {
throw new SecurityException(
"Uid " + uid + " does not have access to " + cmd + " uwb command "
+ "(or such command doesn't exist)");
}
}
final PrintWriter pw = getOutPrintWriter();
try {
switch (cmd) {
case "force-country-code": {
boolean enabled = getNextArgRequiredTrueOrFalse("enabled", "disabled");
if (enabled) {
String countryCode = getNextArgRequired();
if (!UwbCountryCode.isValid(countryCode)) {
pw.println("Invalid argument: Country code must be a 2-Character"
+ " alphanumeric code. But got countryCode " + countryCode
+ " instead");
return -1;
}
mUwbCountryCode.setOverrideCountryCode(countryCode);
return 0;
} else {
mUwbCountryCode.clearOverrideCountryCode();
return 0;
}
}
case "get-country-code":
pw.println("Uwb Country Code = " + mUwbCountryCode.getCountryCode());
return 0;
case "status":
printStatus(pw);
return 0;
case "enable-uwb":
mUwbService.setEnabled(true);
return 0;
case "disable-uwb":
mUwbService.setEnabled(false);
return 0;
case "start-fira-ranging-session":
startFiraRangingSession(pw);
return 0;
case "start-ccc-ranging-session":
startCccRangingSession(pw);
return 0;
case "reconfigure-fira-ranging-session":
reconfigureFiraRangingSession(pw);
return 0;
case "get-ranging-session-reports": {
int sessionId = Integer.parseInt(getNextArgRequired());
SessionInfo sessionInfo = sSessionIdToInfo.get(sessionId);
if (sessionInfo == null) {
pw.println("No active session with session ID: " + sessionId + " found");
return -1;
}
pw.println("Last Ranging results:");
for (RangingReport rangingReport : sessionInfo.lastRangingReports) {
pw.println(rangingReport);
}
return 0;
}
case "get-all-ranging-session-reports": {
for (SessionInfo sessionInfo: sSessionIdToInfo.values()) {
pw.println("Last Ranging results for sessionId " + sessionInfo.sessionId
+ ":");
for (RangingReport rangingReport : sessionInfo.lastRangingReports) {
pw.println(rangingReport);
}
}
return 0;
}
case "stop-ranging-session":
stopRangingSession(pw);
return 0;
case "stop-all-ranging-sessions": {
for (int sessionId : sSessionIdToInfo.keySet()) {
stopRangingSession(pw, sessionId);
}
return 0;
}
case "get-specification-info": {
PersistableBundle bundle = mUwbService.getSpecificationInfo(null);
PersistableBundle fira_bundle = bundle.getPersistableBundle(
FiraParams.PROTOCOL_NAME);
PersistableBundle ccc_bundle = bundle.getPersistableBundle(
CccParams.PROTOCOL_NAME);
pw.println("FIRA Specification info: " + bundleToString(fira_bundle));
pw.println("CCC Specification info: " + bundleToString(ccc_bundle));
return 0;
}
case "get-power-stats": {
pw.println(mNativeUwbManager.getPowerStats());
return 0;
}
default:
return handleDefaultCommands(cmd);
}
} catch (IllegalArgumentException e) {
pw.println("Invalid args for " + cmd + ": ");
e.printStackTrace(pw);
return -1;
} catch (Exception e) {
pw.println("Exception while executing UwbShellCommand" + cmd + ": ");
e.printStackTrace(pw);
return -1;
}
}
private boolean getNextArgRequiredTrueOrFalse(String trueString, String falseString)
throws IllegalArgumentException {
String nextArg = getNextArgRequired();
if (trueString.equals(nextArg)) {
return true;
} else if (falseString.equals(nextArg)) {
return false;
} else {
throw new IllegalArgumentException("Expected '" + trueString + "' or '" + falseString
+ "' as next arg but got '" + nextArg + "'");
}
}
private void printStatus(PrintWriter pw) throws RemoteException {
boolean uwbEnabled =
mUwbService.getAdapterState() != UwbManager.AdapterStateCallback.STATE_DISABLED;
pw.println("Uwb is " + (uwbEnabled ? "enabled" : "disabled"));
}
private void onHelpNonPrivileged(PrintWriter pw) {
pw.println(" status");
pw.println(" Gets status of UWB stack");
pw.println(" get-country-code");
pw.println(" Gets country code as a two-letter string");
pw.println(" enable-uwb");
pw.println(" Toggle UWB on");
pw.println(" disable-uwb");
pw.println(" Toggle UWB off");
pw.println(" start-fira-ranging-session"
+ " [-b](blocking call)"
+ " [-i <sessionId>](session-id)"
+ " [-c <channel>](channel)"
+ " [-t controller|controlee](device-type)"
+ " [-r initiator|responder](device-role)"
+ " [-a <deviceAddress>](device-address)"
+ " [-d <destAddress-1, destAddress-2,...>](dest-addresses)"
+ " [-u ds-twr|ss-twr|ds-twr-non-deferred|ss-twr-non-deferred](round-usage)"
+ " [-z <numRangeMrmts, numAoaAzimuthMrmts, numAoaElevationMrmts>"
+ "(interleaving-ratio)"
+ " [-e none|enabled|azimuth-only|elevation-only](aoa type)");
pw.println(" Starts a FIRA ranging session with the provided params."
+ " Note: default behavior is to cache the latest ranging reports which can be"
+ " retrieved using |get-ranging-session-reports|");
pw.println(" start-ccc-ranging-session"
+ " [-b](blocking call)"
+ " Ranging reports will be displayed on screen)"
+ " [-u 0|1](uwb-config)"
+ " [-p <tx>,<rx>](pulse-shape-combo)"
+ " [-i <sessionId>](session-id)"
+ " [-r <ran_multiplier>](ran-multiplier)"
+ " [-c <channel>](channel)"
+ " [-p <num-chaps-per-slot>](num-chaps-per-slot)"
+ " [-n <num-responder-nodes>](num-responder-nodes)"
+ " [-o <num-slots-per-round>](num-slots-per-round)"
+ " [-s <sync-code-index>](sync-code-index)"
+ " [-h none|continuous|adaptive](hopping-config-mode)"
+ " [-a default|aes](hopping-sequence)");
pw.println(" Starts a CCC ranging session with the provided params."
+ " Note: default behavior is to cache the latest ranging reports which can be"
+ " retrieved using |get-ranging-session-reports|");
pw.println(" reconfigure-fira-ranging-session"
+ " <sessionId>"
+ " [-a add|delete](action)"
+ " [-d <destAddress-1, destAddress-2,...>](dest-addresses)"
+ " [-s <subSessionId-1, subSessionId-2,...>](sub-sessionIds)");
pw.println(" get-ranging-session-reports <sessionId>");
pw.println(" Displays latest cached ranging reports for an ongoing ranging session");
pw.println(" get-all-ranging-session-reports");
pw.println(" Displays latest cached ranging reports for all ongoing ranging session");
pw.println(" stop-ranging-session <sessionId>");
pw.println(" Stops an ongoing ranging session");
pw.println(" stop-all-ranging-sessions");
pw.println(" Stops all ongoing ranging sessions");
pw.println(" get-specification-info");
pw.println(" Gets specification info from uwb chip");
}
private void onHelpPrivileged(PrintWriter pw) {
pw.println(" force-country-code enabled <two-letter code> | disabled ");
pw.println(" Sets country code to <two-letter code> or left for normal value");
pw.println(" get-power-stats");
pw.println(" Get power stats");
}
@Override
public void onHelp() {
final PrintWriter pw = getOutPrintWriter();
pw.println("UWB (ultra wide-band) commands:");
pw.println(" help or -h");
pw.println(" Print this help text.");
onHelpNonPrivileged(pw);
if (Binder.getCallingUid() == Process.ROOT_UID) {
onHelpPrivileged(pw);
}
pw.println();
}
@VisibleForTesting
public void reset() {
sSessionHandleIdNext = 0;
sSessionIdToInfo.clear();
}
}