/* * Copyright (C) 2017 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.rtt; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE; import static com.android.server.wifi.WifiSettingsConfigStore.WIFI_VERBOSE_LOGGING_ENABLED; import android.annotation.NonNull; import android.app.ActivityManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.location.LocationManager; import android.net.MacAddress; import android.net.wifi.aware.IWifiAwareMacAddressProvider; import android.net.wifi.aware.WifiAwareManager; import android.net.wifi.rtt.IRttCallback; import android.net.wifi.rtt.IWifiRttManager; import android.net.wifi.rtt.RangingRequest; import android.net.wifi.rtt.RangingResult; import android.net.wifi.rtt.RangingResultCallback; import android.net.wifi.rtt.ResponderConfig; import android.net.wifi.rtt.ResponderLocation; import android.net.wifi.rtt.WifiRttManager; import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.ParcelFileDescriptor; import android.os.PowerManager; import android.os.RemoteException; import android.os.UserHandle; import android.os.WorkSource; import android.os.WorkSource.WorkChain; import android.util.Log; import android.util.SparseIntArray; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.WakeupMessage; import com.android.modules.utils.BasicShellCommandHandler; import com.android.server.wifi.Clock; import com.android.server.wifi.WifiSettingsConfigStore; import com.android.server.wifi.proto.nano.WifiMetricsProto; import com.android.server.wifi.util.WifiPermissionsUtil; import com.android.wifi.resources.R; import org.json.JSONException; import org.json.JSONObject; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Map; /** * Implementation of the IWifiRttManager AIDL interface and of the RttService state manager. */ public class RttServiceImpl extends IWifiRttManager.Stub { private static final String TAG = "RttServiceImpl"; private static final boolean VDBG = false; // STOPSHIP if true private boolean mDbg = false; private final Context mContext; private final RttShellCommand mShellCommand; private Clock mClock; private WifiAwareManager mAwareManager; private RttNative mRttNative; private RttMetrics mRttMetrics; private WifiPermissionsUtil mWifiPermissionsUtil; private ActivityManager mActivityManager; private PowerManager mPowerManager; private int mBackgroundProcessExecGapMs; private long mLastRequestTimestamp; private RttServiceSynchronized mRttServiceSynchronized; /* package */ static final String HAL_RANGING_TIMEOUT_TAG = TAG + " HAL Ranging Timeout"; @VisibleForTesting public static final long HAL_RANGING_TIMEOUT_MS = 5_000; // 5 sec @VisibleForTesting public static final long HAL_AWARE_RANGING_TIMEOUT_MS = 10_000; // 10 sec // Default value for RTT background throttling interval. private static final long DEFAULT_BACKGROUND_PROCESS_EXEC_GAP_MS = 1_800_000; // 30 min // arbitrary, larger than anything reasonable /* package */ static final int MAX_QUEUED_PER_UID = 20; public RttServiceImpl(Context context) { mContext = context; mShellCommand = new RttShellCommand(); mShellCommand.reset(); } /* * Shell command: adb shell cmd wifirtt ... */ // If set to 0: normal behavior, if set to 1: do not allow any caller (including system // callers) privileged API access private static final String CONTROL_PARAM_OVERRIDE_ASSUME_NO_PRIVILEGE_NAME = "override_assume_no_privilege"; private static final int CONTROL_PARAM_OVERRIDE_ASSUME_NO_PRIVILEGE_DEFAULT = 0; private class RttShellCommand extends BasicShellCommandHandler { private Map mControlParams = new HashMap<>(); @Override public int onCommand(String cmd) { final int uid = Binder.getCallingUid(); if (uid != 0) { throw new SecurityException( "Uid " + uid + " does not have access to wifirtt commands"); } final PrintWriter pw = getErrPrintWriter(); try { if ("reset".equals(cmd)) { reset(); return 0; } else if ("get".equals(cmd)) { String name = getNextArgRequired(); if (!mControlParams.containsKey(name)) { pw.println("Unknown parameter name -- '" + name + "'"); return -1; } getOutPrintWriter().println(mControlParams.get(name)); return 0; } else if ("set".equals(cmd)) { String name = getNextArgRequired(); String valueStr = getNextArgRequired(); if (!mControlParams.containsKey(name)) { pw.println("Unknown parameter name -- '" + name + "'"); return -1; } try { mControlParams.put(name, Integer.valueOf(valueStr)); return 0; } catch (NumberFormatException e) { pw.println("Can't convert value to integer -- '" + valueStr + "'"); return -1; } } else if ("get_capabilities".equals(cmd)) { RttNative.Capabilities cap = mRttNative.getRttCapabilities(); JSONObject j = new JSONObject(); if (cap != null) { try { j.put("rttOneSidedSupported", cap.oneSidedRttSupported); j.put("rttFtmSupported", cap.rttFtmSupported); j.put("lciSupported", cap.lciSupported); j.put("lcrSupported", cap.lcrSupported); j.put("responderSupported", cap.responderSupported); j.put("mcVersion", cap.mcVersion); } catch (JSONException e) { Log.e(TAG, "onCommand: get_capabilities e=" + e); } } getOutPrintWriter().println(j.toString()); return 0; } else { handleDefaultCommands(cmd); } } catch (Exception e) { pw.println("Exception: " + e); } return -1; } @Override public void onHelp() { final PrintWriter pw = getOutPrintWriter(); pw.println("Wi-Fi RTT (wifirt) commands:"); pw.println(" help"); pw.println(" Print this help text."); pw.println(" reset"); pw.println(" Reset parameters to default values."); pw.println(" get_capabilities: prints out the RTT capabilities as a JSON string"); pw.println(" get "); pw.println(" Get the value of the control parameter."); pw.println(" set "); pw.println(" Set the value of the control parameter."); pw.println(" Control parameters:"); for (String name : mControlParams.keySet()) { pw.println(" " + name); } pw.println(); } public int getControlParam(String name) { if (mControlParams.containsKey(name)) { return mControlParams.get(name); } Log.wtf(TAG, "getControlParam for unknown variable: " + name); return 0; } public void reset() { mControlParams.put(CONTROL_PARAM_OVERRIDE_ASSUME_NO_PRIVILEGE_NAME, CONTROL_PARAM_OVERRIDE_ASSUME_NO_PRIVILEGE_DEFAULT); } } /* * INITIALIZATION */ /** * Initializes the RTT service (usually with objects from an injector). * * @param looper The looper on which to synchronize operations. * @param clock A mockable clock. * @param awareManager The Wi-Fi Aware service (binder) if supported on the system. * @param rttNative The Native interface to the HAL. * @param rttMetrics The Wi-Fi RTT metrics object. * @param wifiPermissionsUtil Utility for permission checks. * @param settingsConfigStore Used for retrieving verbose logging level. */ public void start(Looper looper, Clock clock, WifiAwareManager awareManager, RttNative rttNative, RttMetrics rttMetrics, WifiPermissionsUtil wifiPermissionsUtil, WifiSettingsConfigStore settingsConfigStore) { mClock = clock; mAwareManager = awareManager; mRttNative = rttNative; mRttMetrics = rttMetrics; mWifiPermissionsUtil = wifiPermissionsUtil; mRttServiceSynchronized = new RttServiceSynchronized(looper, rttNative); mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); mPowerManager = mContext.getSystemService(PowerManager.class); mRttServiceSynchronized.mHandler.post(() -> { IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED); mContext.registerReceiver(new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (mDbg) Log.v(TAG, "BroadcastReceiver: action=" + action); if (PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED.equals(action)) { if (mPowerManager.isDeviceIdleMode()) { disable(); } else { enableIfPossible(); } } } }, intentFilter); settingsConfigStore.registerChangeListener( WIFI_VERBOSE_LOGGING_ENABLED, (key, newValue) -> enableVerboseLogging(newValue), mRttServiceSynchronized.mHandler); enableVerboseLogging(settingsConfigStore.get(WIFI_VERBOSE_LOGGING_ENABLED)); mBackgroundProcessExecGapMs = mContext.getResources().getInteger( R.integer.config_wifiRttBackgroundExecGapMs); intentFilter = new IntentFilter(); intentFilter.addAction(LocationManager.MODE_CHANGED_ACTION); mContext.registerReceiver(new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (mDbg) Log.v(TAG, "onReceive: MODE_CHANGED_ACTION: intent=" + intent); if (mWifiPermissionsUtil.isLocationModeEnabled()) { enableIfPossible(); } else { disable(); } } }, intentFilter); rttNative.start(mRttServiceSynchronized.mHandler); }); } private void enableVerboseLogging(boolean verbose) { mDbg = verbose; if (VDBG) { mDbg = true; // just override } mRttNative.mDbg = mDbg; mRttMetrics.mDbg = mDbg; } /* * ASYNCHRONOUS DOMAIN - can be called from different threads! */ /** * Proxy for the final native call of the parent class. Enables mocking of * the function. */ public int getMockableCallingUid() { return getCallingUid(); } /** * Enable the API if possible: broadcast notification & start launching any queued requests * * If possible: * - RTT HAL is available * - Not in Idle mode * - Location Mode allows Wi-Fi based locationing */ public void enableIfPossible() { boolean isAvailable = isAvailable(); if (VDBG) Log.v(TAG, "enableIfPossible: isAvailable=" + isAvailable); if (!isAvailable) { return; } sendRttStateChangedBroadcast(true); mRttServiceSynchronized.mHandler.post(() -> { // queue should be empty at this point (but this call allows validation) mRttServiceSynchronized.executeNextRangingRequestIfPossible(false); }); } /** * Disable the API: * - Clean-up (fail) pending requests * - Broadcast notification */ public void disable() { if (VDBG) Log.v(TAG, "disable"); sendRttStateChangedBroadcast(false); mRttServiceSynchronized.mHandler.post(() -> { mRttServiceSynchronized.cleanUpOnDisable(); }); } @Override public int handleShellCommand(@NonNull ParcelFileDescriptor in, @NonNull ParcelFileDescriptor out, @NonNull ParcelFileDescriptor err, @NonNull String[] args) { return mShellCommand.exec( this, in.getFileDescriptor(), out.getFileDescriptor(), err.getFileDescriptor(), args); } /** * Binder interface API to indicate whether the API is currently available. This requires an * immediate asynchronous response. */ @Override public boolean isAvailable() { long ident = Binder.clearCallingIdentity(); try { return mRttNative != null && mRttNative.isReady() && !mPowerManager.isDeviceIdleMode() && mWifiPermissionsUtil.isLocationModeEnabled(); } finally { Binder.restoreCallingIdentity(ident); } } /** * Binder interface API to start a ranging operation. Called on binder thread, operations needs * to be posted to handler thread. */ @Override public void startRanging(IBinder binder, String callingPackage, String callingFeatureId, WorkSource workSource, RangingRequest request, IRttCallback callback) throws RemoteException { if (VDBG) { Log.v(TAG, "startRanging: binder=" + binder + ", callingPackage=" + callingPackage + ", workSource=" + workSource + ", request=" + request + ", callback=" + callback); } // verify arguments if (binder == null) { throw new IllegalArgumentException("Binder must not be null"); } if (request == null || request.mRttPeers == null || request.mRttPeers.size() == 0) { throw new IllegalArgumentException("Request must not be null or empty"); } for (ResponderConfig responder : request.mRttPeers) { if (responder == null) { throw new IllegalArgumentException("Request must not contain null Responders"); } } if (callback == null) { throw new IllegalArgumentException("Callback must not be null"); } request.enforceValidity(mAwareManager != null); if (!isAvailable()) { try { mRttMetrics.recordOverallStatus( WifiMetricsProto.WifiRttLog.OVERALL_RTT_NOT_AVAILABLE); callback.onRangingFailure(RangingResultCallback.STATUS_CODE_FAIL_RTT_NOT_AVAILABLE); } catch (RemoteException e) { Log.e(TAG, "startRanging: disabled, callback failed -- " + e); } return; } final int uid = getMockableCallingUid(); // permission checks enforceAccessPermission(); enforceChangePermission(); mWifiPermissionsUtil.checkPackage(uid, callingPackage); mWifiPermissionsUtil.enforceFineLocationPermission(callingPackage, callingFeatureId, uid); final WorkSource ws; if (workSource != null) { enforceLocationHardware(); // We only care about UIDs in the incoming worksources and not their associated // tags. Clear names so that other operations involving wakesources become simpler. ws = workSource.withoutNames(); } else { ws = null; } boolean isCalledFromPrivilegedContext = checkLocationHardware() && mShellCommand.getControlParam( CONTROL_PARAM_OVERRIDE_ASSUME_NO_PRIVILEGE_NAME) == 0; // register for binder death IBinder.DeathRecipient dr = new IBinder.DeathRecipient() { @Override public void binderDied() { if (mDbg) Log.v(TAG, "binderDied: uid=" + uid); binder.unlinkToDeath(this, 0); mRttServiceSynchronized.mHandler.post(() -> { mRttServiceSynchronized.cleanUpClientRequests(uid, null); }); } }; try { binder.linkToDeath(dr, 0); } catch (RemoteException e) { Log.e(TAG, "Error on linkToDeath - " + e); return; } mRttServiceSynchronized.mHandler.post(() -> { WorkSource sourceToUse = ws; if (ws == null || ws.isEmpty()) { sourceToUse = new WorkSource(uid); } mRttServiceSynchronized.queueRangingRequest(uid, sourceToUse, binder, dr, callingPackage, callingFeatureId, request, callback, isCalledFromPrivilegedContext); }); } @Override public void cancelRanging(WorkSource workSource) throws RemoteException { if (VDBG) Log.v(TAG, "cancelRanging: workSource=" + workSource); enforceLocationHardware(); // We only care about UIDs in the incoming worksources and not their associated // tags. Clear names so that other operations involving wakesources become simpler. final WorkSource ws = (workSource != null) ? workSource.withoutNames() : null; if (ws == null || ws.isEmpty()) { Log.e(TAG, "cancelRanging: invalid work-source -- " + ws); return; } mRttServiceSynchronized.mHandler.post(() -> { mRttServiceSynchronized.cleanUpClientRequests(0, ws); }); } /** * Called by HAL to report ranging results. Called on HAL thread - needs to post to local * thread. */ public void onRangingResults(int cmdId, List results) { if (VDBG) Log.v(TAG, "onRangingResults: cmdId=" + cmdId); mRttServiceSynchronized.mHandler.post(() -> { mRttServiceSynchronized.onRangingResults(cmdId, results); }); } private void enforceAccessPermission() { mContext.enforceCallingOrSelfPermission(android.Manifest.permission.ACCESS_WIFI_STATE, TAG); } private void enforceChangePermission() { mContext.enforceCallingOrSelfPermission(android.Manifest.permission.CHANGE_WIFI_STATE, TAG); } private void enforceLocationHardware() { mContext.enforceCallingOrSelfPermission(android.Manifest.permission.LOCATION_HARDWARE, TAG); } private boolean checkLocationHardware() { return mContext.checkCallingOrSelfPermission(android.Manifest.permission.LOCATION_HARDWARE) == PackageManager.PERMISSION_GRANTED; } private void sendRttStateChangedBroadcast(boolean enabled) { if (VDBG) Log.v(TAG, "sendRttStateChangedBroadcast: enabled=" + enabled); final Intent intent = new Intent(WifiRttManager.ACTION_WIFI_RTT_STATE_CHANGED); intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); mContext.sendBroadcastAsUser(intent, UserHandle.ALL); } @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (mContext.checkCallingOrSelfPermission( android.Manifest.permission.DUMP) != PackageManager.PERMISSION_GRANTED) { pw.println("Permission Denial: can't dump RttService from pid=" + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()); return; } pw.println("Wi-Fi RTT Service"); mRttServiceSynchronized.dump(fd, pw, args); } /* * SYNCHRONIZED DOMAIN */ /** * RTT service implementation - synchronized on a single thread. All commands should be posted * to the exposed handler. */ private class RttServiceSynchronized { public Handler mHandler; private RttNative mRttNative; private int mNextCommandId = 1000; private Map mRttRequesterInfo = new HashMap<>(); private List mRttRequestQueue = new LinkedList<>(); private WakeupMessage mRangingTimeoutMessage = null; RttServiceSynchronized(Looper looper, RttNative rttNative) { mRttNative = rttNative; mHandler = new Handler(looper); mRangingTimeoutMessage = new WakeupMessage(mContext, mHandler, HAL_RANGING_TIMEOUT_TAG, () -> { timeoutRangingRequest(); }); } private void cancelRanging(RttRequestInfo rri) { ArrayList macAddresses = new ArrayList<>(); for (ResponderConfig peer : rri.request.mRttPeers) { macAddresses.add(peer.macAddress.toByteArray()); } mRttNative.rangeCancel(rri.cmdId, macAddresses); } private void cleanUpOnDisable() { if (VDBG) Log.v(TAG, "RttServiceSynchronized.cleanUpOnDisable"); for (RttRequestInfo rri : mRttRequestQueue) { try { if (rri.dispatchedToNative) { // may not be necessary in some cases (e.g. Wi-Fi disable may already clear // up active RTT), but in other cases will be needed (doze disabling RTT // but Wi-Fi still up). Doesn't hurt - worst case will fail. cancelRanging(rri); } mRttMetrics.recordOverallStatus( WifiMetricsProto.WifiRttLog.OVERALL_RTT_NOT_AVAILABLE); rri.callback.onRangingFailure( RangingResultCallback.STATUS_CODE_FAIL_RTT_NOT_AVAILABLE); } catch (RemoteException e) { Log.e(TAG, "RttServiceSynchronized.startRanging: disabled, callback failed -- " + e); } rri.binder.unlinkToDeath(rri.dr, 0); } mRttRequestQueue.clear(); mRangingTimeoutMessage.cancel(); } /** * Remove entries related to the specified client and cancel any dispatched to HAL * requests. Expected to provide either the UID or the WorkSource (the other will be 0 or * null respectively). * * A workSource specification will be cleared from the requested workSource and the request * cancelled only if there are no remaining uids in the work-source. */ private void cleanUpClientRequests(int uid, WorkSource workSource) { if (VDBG) { Log.v(TAG, "RttServiceSynchronized.cleanUpOnClientDeath: uid=" + uid + ", workSource=" + workSource + ", mRttRequestQueue=" + mRttRequestQueue); } boolean dispatchedRequestAborted = false; ListIterator it = mRttRequestQueue.listIterator(); while (it.hasNext()) { RttRequestInfo rri = it.next(); boolean match = rri.uid == uid; // original UID will never be 0 if (rri.workSource != null && workSource != null) { rri.workSource.remove(workSource); if (rri.workSource.isEmpty()) { match = true; } } if (match) { if (!rri.dispatchedToNative) { it.remove(); rri.binder.unlinkToDeath(rri.dr, 0); } else { dispatchedRequestAborted = true; Log.d(TAG, "Client death - cancelling RTT operation in progress: cmdId=" + rri.cmdId); mRangingTimeoutMessage.cancel(); cancelRanging(rri); } } } if (VDBG) { Log.v(TAG, "RttServiceSynchronized.cleanUpOnClientDeath: uid=" + uid + ", dispatchedRequestAborted=" + dispatchedRequestAborted + ", after cleanup - mRttRequestQueue=" + mRttRequestQueue); } if (dispatchedRequestAborted) { executeNextRangingRequestIfPossible(true); } } private void timeoutRangingRequest() { if (VDBG) { Log.v(TAG, "RttServiceSynchronized.timeoutRangingRequest mRttRequestQueue=" + mRttRequestQueue); } if (mRttRequestQueue.size() == 0) { Log.w(TAG, "RttServiceSynchronized.timeoutRangingRequest: but nothing in queue!?"); return; } RttRequestInfo rri = mRttRequestQueue.get(0); if (!rri.dispatchedToNative) { Log.w(TAG, "RttServiceSynchronized.timeoutRangingRequest: command not dispatched " + "to native!?"); return; } cancelRanging(rri); try { mRttMetrics.recordOverallStatus(WifiMetricsProto.WifiRttLog.OVERALL_TIMEOUT); rri.callback.onRangingFailure(RangingResultCallback.STATUS_CODE_FAIL); } catch (RemoteException e) { Log.e(TAG, "RttServiceSynchronized.timeoutRangingRequest: callback failed: " + e); } executeNextRangingRequestIfPossible(true); } private void queueRangingRequest(int uid, WorkSource workSource, IBinder binder, IBinder.DeathRecipient dr, String callingPackage, String callingFeatureId, RangingRequest request, IRttCallback callback, boolean isCalledFromPrivilegedContext) { mRttMetrics.recordRequest(workSource, request); if (isRequestorSpamming(workSource)) { Log.w(TAG, "Work source " + workSource + " is spamming, dropping request: " + request); binder.unlinkToDeath(dr, 0); try { mRttMetrics.recordOverallStatus(WifiMetricsProto.WifiRttLog.OVERALL_THROTTLE); callback.onRangingFailure(RangingResultCallback.STATUS_CODE_FAIL); } catch (RemoteException e) { Log.e(TAG, "RttServiceSynchronized.queueRangingRequest: spamming, callback " + "failed -- " + e); } return; } RttRequestInfo newRequest = new RttRequestInfo(); newRequest.uid = uid; newRequest.workSource = workSource; newRequest.binder = binder; newRequest.dr = dr; newRequest.callingPackage = callingPackage; newRequest.callingFeatureId = callingFeatureId; newRequest.request = request; newRequest.callback = callback; newRequest.isCalledFromPrivilegedContext = isCalledFromPrivilegedContext; mRttRequestQueue.add(newRequest); if (VDBG) { Log.v(TAG, "RttServiceSynchronized.queueRangingRequest: newRequest=" + newRequest); } executeNextRangingRequestIfPossible(false); } private boolean isRequestorSpamming(WorkSource ws) { if (VDBG) Log.v(TAG, "isRequestorSpamming: ws" + ws); SparseIntArray counts = new SparseIntArray(); for (RttRequestInfo rri : mRttRequestQueue) { for (int i = 0; i < rri.workSource.size(); ++i) { int uid = rri.workSource.getUid(i); counts.put(uid, counts.get(uid) + 1); } final List workChains = rri.workSource.getWorkChains(); if (workChains != null) { for (int i = 0; i < workChains.size(); ++i) { final int uid = workChains.get(i).getAttributionUid(); counts.put(uid, counts.get(uid) + 1); } } } for (int i = 0; i < ws.size(); ++i) { if (counts.get(ws.getUid(i)) < MAX_QUEUED_PER_UID) { return false; } } final List workChains = ws.getWorkChains(); if (workChains != null) { for (int i = 0; i < workChains.size(); ++i) { final int uid = workChains.get(i).getAttributionUid(); if (counts.get(uid) < MAX_QUEUED_PER_UID) { return false; } } } if (mDbg) { Log.v(TAG, "isRequestorSpamming: ws=" + ws + ", someone is spamming: " + counts); } return true; } private void executeNextRangingRequestIfPossible(boolean popFirst) { if (VDBG) Log.v(TAG, "executeNextRangingRequestIfPossible: popFirst=" + popFirst); if (popFirst) { if (mRttRequestQueue.size() == 0) { Log.w(TAG, "executeNextRangingRequestIfPossible: pop requested - but empty " + "queue!? Ignoring pop."); } else { RttRequestInfo topOfQueueRequest = mRttRequestQueue.remove(0); topOfQueueRequest.binder.unlinkToDeath(topOfQueueRequest.dr, 0); } } if (mRttRequestQueue.size() == 0) { if (VDBG) Log.v(TAG, "executeNextRangingRequestIfPossible: no requests pending"); return; } // if top of list is in progress then do nothing RttRequestInfo nextRequest = mRttRequestQueue.get(0); if (nextRequest.peerHandlesTranslated || nextRequest.dispatchedToNative) { if (VDBG) { Log.v(TAG, "executeNextRangingRequestIfPossible: called but a command is " + "executing. topOfQueue=" + nextRequest); } return; } startRanging(nextRequest); } private void startRanging(RttRequestInfo nextRequest) { if (VDBG) { Log.v(TAG, "RttServiceSynchronized.startRanging: nextRequest=" + nextRequest); } if (!isAvailable()) { Log.d(TAG, "RttServiceSynchronized.startRanging: disabled"); try { mRttMetrics.recordOverallStatus( WifiMetricsProto.WifiRttLog.OVERALL_RTT_NOT_AVAILABLE); nextRequest.callback.onRangingFailure( RangingResultCallback.STATUS_CODE_FAIL_RTT_NOT_AVAILABLE); } catch (RemoteException e) { Log.e(TAG, "RttServiceSynchronized.startRanging: disabled, callback failed -- " + e); executeNextRangingRequestIfPossible(true); return; } } if (processAwarePeerHandles(nextRequest)) { if (VDBG) { Log.v(TAG, "RttServiceSynchronized.startRanging: deferring due to PeerHandle " + "Aware requests"); } return; } if (!preExecThrottleCheck(nextRequest.workSource)) { Log.w(TAG, "RttServiceSynchronized.startRanging: execution throttled - nextRequest=" + nextRequest + ", mRttRequesterInfo=" + mRttRequesterInfo); try { mRttMetrics.recordOverallStatus(WifiMetricsProto.WifiRttLog.OVERALL_THROTTLE); nextRequest.callback.onRangingFailure(RangingResultCallback.STATUS_CODE_FAIL); } catch (RemoteException e) { Log.e(TAG, "RttServiceSynchronized.startRanging: throttled, callback failed -- " + e); } executeNextRangingRequestIfPossible(true); return; } nextRequest.cmdId = mNextCommandId++; mLastRequestTimestamp = mClock.getWallClockMillis(); if (mRttNative.rangeRequest(nextRequest.cmdId, nextRequest.request, nextRequest.isCalledFromPrivilegedContext)) { long timeout = HAL_RANGING_TIMEOUT_MS; for (ResponderConfig responderConfig : nextRequest.request.mRttPeers) { if (responderConfig.responderType == ResponderConfig.RESPONDER_AWARE) { timeout = HAL_AWARE_RANGING_TIMEOUT_MS; break; } } mRangingTimeoutMessage.schedule(mClock.getElapsedSinceBootMillis() + timeout); } else { Log.w(TAG, "RttServiceSynchronized.startRanging: native rangeRequest call failed"); try { mRttMetrics.recordOverallStatus( WifiMetricsProto.WifiRttLog.OVERALL_HAL_FAILURE); nextRequest.callback.onRangingFailure(RangingResultCallback.STATUS_CODE_FAIL); } catch (RemoteException e) { Log.e(TAG, "RttServiceSynchronized.startRanging: HAL request failed, callback " + "failed -- " + e); } executeNextRangingRequestIfPossible(true); } nextRequest.dispatchedToNative = true; } /** * Perform pre-execution throttling checks: * - If all uids in ws are in background then check last execution and block if request is * more frequent than permitted * - If executing (i.e. permitted) then update execution time * * Returns true to permit execution, false to abort it. */ private boolean preExecThrottleCheck(WorkSource ws) { if (VDBG) Log.v(TAG, "preExecThrottleCheck: ws=" + ws); // are all UIDs running in the background or is at least 1 in the foreground? boolean allUidsInBackground = true; for (int i = 0; i < ws.size(); ++i) { int uidImportance = mActivityManager.getUidImportance(ws.getUid(i)); if (VDBG) { Log.v(TAG, "preExecThrottleCheck: uid=" + ws.getUid(i) + " -> importance=" + uidImportance); } if (uidImportance <= IMPORTANCE_FOREGROUND_SERVICE) { allUidsInBackground = false; break; } } final List workChains = ws.getWorkChains(); if (allUidsInBackground && workChains != null) { for (int i = 0; i < workChains.size(); ++i) { final WorkChain wc = workChains.get(i); int uidImportance = mActivityManager.getUidImportance(wc.getAttributionUid()); if (VDBG) { Log.v(TAG, "preExecThrottleCheck: workChain=" + wc + " -> importance=" + uidImportance); } if (uidImportance <= IMPORTANCE_FOREGROUND_SERVICE) { allUidsInBackground = false; break; } } } // if all UIDs are in background then check timestamp since last execution and see if // any is permitted (infrequent enough) boolean allowExecution = false; long mostRecentExecutionPermitted = mClock.getElapsedSinceBootMillis() - mBackgroundProcessExecGapMs; if (allUidsInBackground) { for (int i = 0; i < ws.size(); ++i) { RttRequesterInfo info = mRttRequesterInfo.get(ws.getUid(i)); if (info == null || info.lastRangingExecuted < mostRecentExecutionPermitted) { allowExecution = true; break; } } if (workChains != null & !allowExecution) { for (int i = 0; i < workChains.size(); ++i) { final WorkChain wc = workChains.get(i); RttRequesterInfo info = mRttRequesterInfo.get(wc.getAttributionUid()); if (info == null || info.lastRangingExecuted < mostRecentExecutionPermitted) { allowExecution = true; break; } } } } else { allowExecution = true; } // update exec time if (allowExecution) { for (int i = 0; i < ws.size(); ++i) { RttRequesterInfo info = mRttRequesterInfo.get(ws.getUid(i)); if (info == null) { info = new RttRequesterInfo(); mRttRequesterInfo.put(ws.getUid(i), info); } info.lastRangingExecuted = mClock.getElapsedSinceBootMillis(); } if (workChains != null) { for (int i = 0; i < workChains.size(); ++i) { final WorkChain wc = workChains.get(i); RttRequesterInfo info = mRttRequesterInfo.get(wc.getAttributionUid()); if (info == null) { info = new RttRequesterInfo(); mRttRequesterInfo.put(wc.getAttributionUid(), info); } info.lastRangingExecuted = mClock.getElapsedSinceBootMillis(); } } } return allowExecution; } /** * Check request for any PeerHandle Aware requests. If there are any: issue requests to * translate the peer ID to a MAC address and abort current execution of the range request. * The request will be re-attempted when response is received. * * In cases of failure: pop the current request and execute the next one. Failures: * - Not able to connect to remote service (unlikely) * - Request already processed: but we're missing information * * @return true if need to abort execution, false otherwise. */ private boolean processAwarePeerHandles(RttRequestInfo request) { List peerIdsNeedingTranslation = new ArrayList<>(); for (ResponderConfig rttPeer : request.request.mRttPeers) { if (rttPeer.peerHandle != null && rttPeer.macAddress == null) { peerIdsNeedingTranslation.add(rttPeer.peerHandle.peerId); } } if (peerIdsNeedingTranslation.size() == 0) { return false; } if (request.peerHandlesTranslated) { Log.w(TAG, "processAwarePeerHandles: request=" + request + ": PeerHandles translated - but information still missing!?"); try { mRttMetrics.recordOverallStatus( WifiMetricsProto.WifiRttLog.OVERALL_AWARE_TRANSLATION_FAILURE); request.callback.onRangingFailure(RangingResultCallback.STATUS_CODE_FAIL); } catch (RemoteException e) { Log.e(TAG, "processAwarePeerHandles: onRangingResults failure -- " + e); } executeNextRangingRequestIfPossible(true); return true; // an abort because we removed request and are executing next one } request.peerHandlesTranslated = true; mAwareManager.requestMacAddresses(request.uid, peerIdsNeedingTranslation, new IWifiAwareMacAddressProvider.Stub() { @Override public void macAddress(Map peerIdToMacMap) { // ASYNC DOMAIN mHandler.post(() -> { // BACK TO SYNC DOMAIN processReceivedAwarePeerMacAddresses(request, peerIdToMacMap); }); } }); return true; // a deferral } private void processReceivedAwarePeerMacAddresses(RttRequestInfo request, Map peerIdToMacMap) { if (VDBG) { Log.v(TAG, "processReceivedAwarePeerMacAddresses: request=" + request + ", peerIdToMacMap=" + peerIdToMacMap); } RangingRequest.Builder newRequestBuilder = new RangingRequest.Builder(); for (ResponderConfig rttPeer : request.request.mRttPeers) { if (rttPeer.peerHandle != null && rttPeer.macAddress == null) { byte[] mac = peerIdToMacMap.get(rttPeer.peerHandle.peerId); if (mac == null || mac.length != 6) { Log.e(TAG, "processReceivedAwarePeerMacAddresses: received an invalid MAC " + "address for peerId=" + rttPeer.peerHandle.peerId); continue; } newRequestBuilder.addResponder(new ResponderConfig( MacAddress.fromBytes(mac), rttPeer.peerHandle, rttPeer.responderType, rttPeer.supports80211mc, rttPeer.channelWidth, rttPeer.frequency, rttPeer.centerFreq0, rttPeer.centerFreq1, rttPeer.preamble)); } else { newRequestBuilder.addResponder(rttPeer); } } request.request = newRequestBuilder.build(); // run request again startRanging(request); } private void onRangingResults(int cmdId, List results) { if (mRttRequestQueue.size() == 0) { Log.e(TAG, "RttServiceSynchronized.onRangingResults: no current RTT request " + "pending!?"); return; } mRangingTimeoutMessage.cancel(); RttRequestInfo topOfQueueRequest = mRttRequestQueue.get(0); if (VDBG) { Log.v(TAG, "RttServiceSynchronized.onRangingResults: cmdId=" + cmdId + ", topOfQueueRequest=" + topOfQueueRequest + ", results=" + Arrays.toString(results.toArray())); } if (topOfQueueRequest.cmdId != cmdId) { Log.e(TAG, "RttServiceSynchronized.onRangingResults: cmdId=" + cmdId + ", does not match pending RTT request cmdId=" + topOfQueueRequest.cmdId); return; } boolean permissionGranted = mWifiPermissionsUtil.checkCallersLocationPermission( topOfQueueRequest.callingPackage, topOfQueueRequest.callingFeatureId, topOfQueueRequest.uid, /* coarseForTargetSdkLessThanQ */ false, null) && mWifiPermissionsUtil.isLocationModeEnabled(); try { if (permissionGranted) { List finalResults = postProcessResults(topOfQueueRequest.request, results, topOfQueueRequest.isCalledFromPrivilegedContext); mRttMetrics.recordOverallStatus(WifiMetricsProto.WifiRttLog.OVERALL_SUCCESS); mRttMetrics.recordResult(topOfQueueRequest.request, results, (int) (mClock.getWallClockMillis() - mLastRequestTimestamp)); if (VDBG) { Log.v(TAG, "RttServiceSynchronized.onRangingResults: finalResults=" + finalResults); } topOfQueueRequest.callback.onRangingResults(finalResults); } else { Log.w(TAG, "RttServiceSynchronized.onRangingResults: location permission " + "revoked - not forwarding results"); mRttMetrics.recordOverallStatus( WifiMetricsProto.WifiRttLog.OVERALL_LOCATION_PERMISSION_MISSING); topOfQueueRequest.callback.onRangingFailure( RangingResultCallback.STATUS_CODE_FAIL); } } catch (RemoteException e) { Log.e(TAG, "RttServiceSynchronized.onRangingResults: callback exception -- " + e); } executeNextRangingRequestIfPossible(true); } /* * Post process the results: * - For requests without results: add FAILED results * - For Aware requests using PeerHandle: replace MAC address with PeerHandle * - Effectively: throws away results which don't match requests */ private List postProcessResults(RangingRequest request, List results, boolean isCalledFromPrivilegedContext) { Map resultEntries = new HashMap<>(); for (RangingResult result : results) { resultEntries.put(result.getMacAddress(), result); } List finalResults = new ArrayList<>(request.mRttPeers.size()); for (ResponderConfig peer : request.mRttPeers) { RangingResult resultForRequest = resultEntries.get(peer.macAddress); if (resultForRequest == null || resultForRequest.getStatus() != RttNative.FRAMEWORK_RTT_STATUS_SUCCESS) { if (mDbg) { Log.v(TAG, "postProcessResults: missing=" + peer.macAddress); } int errorCode = RangingResult.STATUS_FAIL; if (peer.peerHandle == null) { finalResults.add( new RangingResult(errorCode, peer.macAddress, 0, 0, 0, 0, 0, null, null, null, 0, false)); } else { finalResults.add( new RangingResult(errorCode, peer.peerHandle, 0, 0, 0, 0, 0, null, null, null, 0)); } } else { int status = RangingResult.STATUS_SUCCESS; // Clear LCI and LCR data if the location data should not be retransmitted, // has a retention expiration time, contains no useful data, or did not parse, // or the caller is not in a privileged context. byte[] lci = resultForRequest.getLci(); byte[] lcr = resultForRequest.getLcr(); ResponderLocation responderLocation = resultForRequest.getUnverifiedResponderLocation(); if (responderLocation == null || !isCalledFromPrivilegedContext) { lci = null; lcr = null; } // Create external result with external RangResultStatus, cleared LCI and LCR. if (peer.peerHandle == null) { finalResults.add(new RangingResult( status, peer.macAddress, resultForRequest.mDistanceMm, resultForRequest.mDistanceStdDevMm, resultForRequest.mRssi, resultForRequest.mNumAttemptedMeasurements, resultForRequest.mNumSuccessfulMeasurements, lci, lcr, responderLocation, resultForRequest.mTimestamp, resultForRequest.mIs80211mcMeasurement)); } else { finalResults.add(new RangingResult( status, peer.peerHandle, resultForRequest.mDistanceMm, resultForRequest.mDistanceStdDevMm, resultForRequest.mRssi, resultForRequest.mNumAttemptedMeasurements, resultForRequest.mNumSuccessfulMeasurements, lci, lcr, responderLocation, resultForRequest.mTimestamp)); } } } return finalResults; } // dump call (asynchronous most likely) protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println(" mNextCommandId: " + mNextCommandId); pw.println(" mRttRequesterInfo: " + mRttRequesterInfo); pw.println(" mRttRequestQueue: " + mRttRequestQueue); pw.println(" mRangingTimeoutMessage: " + mRangingTimeoutMessage); mRttMetrics.dump(fd, pw, args); mRttNative.dump(fd, pw, args); } } private static class RttRequestInfo { public int uid; public WorkSource workSource; public IBinder binder; public IBinder.DeathRecipient dr; public String callingPackage; public String callingFeatureId; public RangingRequest request; public IRttCallback callback; public boolean isCalledFromPrivilegedContext; public int cmdId = 0; // uninitialized cmdId value public boolean dispatchedToNative = false; public boolean peerHandlesTranslated = false; @Override public String toString() { return new StringBuilder("RttRequestInfo: uid=").append(uid).append( ", workSource=").append(workSource).append(", binder=").append(binder).append( ", dr=").append(dr).append(", callingPackage=").append(callingPackage).append( ", callingFeatureId=").append(callingFeatureId).append(", request=").append( request.toString()).append(", callback=").append(callback).append( ", cmdId=").append(cmdId).append(", peerHandlesTranslated=").append( peerHandlesTranslated).append(", isCalledFromPrivilegedContext=").append( isCalledFromPrivilegedContext).toString(); } } private static class RttRequesterInfo { public long lastRangingExecuted; @Override public String toString() { return new StringBuilder("RttRequesterInfo: lastRangingExecuted=").append( lastRangingExecuted).toString(); } } }