/* * 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.Manifest.permission.UWB_RANGING; import static android.permission.PermissionManager.PERMISSION_GRANTED; import static java.lang.Math.toRadians; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.AlarmManager; import android.content.ApexEnvironment; import android.content.AttributionSource; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.database.ContentObserver; import android.location.Geocoder; import android.net.Uri; import android.os.Binder; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Process; import android.os.SystemClock; import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.permission.PermissionManager; import android.provider.Settings; import android.util.AtomicFile; import android.util.Log; import com.android.server.uwb.advertisement.UwbAdvertiseManager; import com.android.server.uwb.correction.UwbFilterEngine; import com.android.server.uwb.correction.filtering.IFilter; import com.android.server.uwb.correction.filtering.MedAvgFilter; import com.android.server.uwb.correction.filtering.MedAvgRotationFilter; import com.android.server.uwb.correction.filtering.PositionFilterImpl; import com.android.server.uwb.correction.pose.GyroPoseSource; import com.android.server.uwb.correction.pose.IPoseSource; import com.android.server.uwb.correction.pose.IntegPoseSource; import com.android.server.uwb.correction.pose.RotationPoseSource; import com.android.server.uwb.correction.pose.SixDofPoseSource; import com.android.server.uwb.correction.primers.AoaPrimer; import com.android.server.uwb.correction.primers.BackAzimuthPrimer; import com.android.server.uwb.correction.primers.ElevationPrimer; import com.android.server.uwb.correction.primers.FovPrimer; import com.android.server.uwb.data.ServiceProfileData; import com.android.server.uwb.jni.NativeUwbManager; import com.android.server.uwb.multchip.UwbMultichipData; import com.android.server.uwb.pm.ProfileManager; import com.android.uwb.flags.FeatureFlags; import java.io.File; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.locks.ReentrantLock; /** * To be used for dependency injection (especially helps mocking static dependencies). */ public class UwbInjector { private static final String TAG = "UwbInjector"; private static final String APEX_NAME = "com.android.uwb"; private static final String VENDOR_SERVICE_NAME = "uwb_vendor"; private static final String BOOT_DEFAULT_UWB_COUNTRY_CODE = "ro.boot.uwbcountrycode"; /** * The path where the Uwb apex is mounted. * Current value = "/apex/com.android.uwb" */ private static final String UWB_APEX_PATH = new File("/apex", APEX_NAME).getAbsolutePath(); private static final int APP_INFO_FLAGS_SYSTEM_APP = ApplicationInfo.FLAG_SYSTEM | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP; private final UwbContext mContext; private final Looper mLooper; private final PermissionManager mPermissionManager; private final UserManager mUserManager; private final UwbConfigStore mUwbConfigStore; private final ProfileManager mProfileManager; private final UwbSettingsStore mUwbSettingsStore; private final NativeUwbManager mNativeUwbManager; private final UwbCountryCode mUwbCountryCode; private final UciLogModeStore mUciLogModeStore; private final UwbServiceCore mUwbService; private final UwbMetrics mUwbMetrics; private final DeviceConfigFacade mDeviceConfigFacade; private final UwbMultichipData mUwbMultichipData; private final SystemBuildProperties mSystemBuildProperties; private final UwbDiagnostics mUwbDiagnostics; private IPoseSource mDefaultPoseSource; private final ReentrantLock mPoseLock = new ReentrantLock(); private int mPoseSourceRefCount = 0; private final UwbSessionManager mUwbSessionManager; private final FeatureFlags mFeatureFlags; public UwbInjector(@NonNull UwbContext context) { // Create UWB service thread. HandlerThread uwbHandlerThread = new HandlerThread("UwbService"); uwbHandlerThread.start(); mLooper = uwbHandlerThread.getLooper(); mContext = context; mPermissionManager = context.getSystemService(PermissionManager.class); mUserManager = mContext.getSystemService(UserManager.class); mUwbConfigStore = new UwbConfigStore(context, new Handler(mLooper), this, UwbConfigStore.createSharedFiles()); mProfileManager = new ProfileManager(context, new Handler(mLooper), mUwbConfigStore, this); mUwbSettingsStore = new UwbSettingsStore( context, new Handler(mLooper), new AtomicFile(new File(getDeviceProtectedDataDir(), UwbSettingsStore.FILE_NAME)), this); mUwbMultichipData = new UwbMultichipData(mContext); mUciLogModeStore = new UciLogModeStore(mUwbSettingsStore); mNativeUwbManager = new NativeUwbManager(this, mUciLogModeStore, mUwbMultichipData); mUwbCountryCode = new UwbCountryCode(mContext, mNativeUwbManager, new Handler(mLooper), this); mUwbMetrics = new UwbMetrics(this); mDeviceConfigFacade = new DeviceConfigFacade(new Handler(mLooper), mContext); UwbConfigurationManager uwbConfigurationManager = new UwbConfigurationManager(mNativeUwbManager, this); UwbSessionNotificationManager uwbSessionNotificationManager = new UwbSessionNotificationManager(this); UwbAdvertiseManager uwbAdvertiseManager = new UwbAdvertiseManager(this, mDeviceConfigFacade); mUwbSessionManager = new UwbSessionManager(uwbConfigurationManager, mNativeUwbManager, mUwbMetrics, uwbAdvertiseManager, uwbSessionNotificationManager, this, mContext.getSystemService(AlarmManager.class), mContext.getSystemService(ActivityManager.class), mLooper); mUwbService = new UwbServiceCore(mContext, mNativeUwbManager, mUwbMetrics, mUwbCountryCode, mUwbSessionManager, uwbConfigurationManager, this, mLooper); mSystemBuildProperties = new SystemBuildProperties(); mUwbDiagnostics = new UwbDiagnostics(mContext, this, mSystemBuildProperties); mFeatureFlags = new com.android.uwb.flags.FeatureFlagsImpl(); } public FeatureFlags getFeatureFlags() { return mFeatureFlags; } public Looper getUwbServiceLooper() { return mLooper; } public UserManager getUserManager() { return mUserManager; } /** * Construct an instance of {@link ServiceProfileData}. */ public ServiceProfileData makeServiceProfileData(ServiceProfileData.DataSource dataSource) { return new ServiceProfileData(dataSource); } public ProfileManager getProfileManager() { return mProfileManager; } public UwbConfigStore getUwbConfigStore() { return mUwbConfigStore; } public UwbSettingsStore getUwbSettingsStore() { return mUwbSettingsStore; } public NativeUwbManager getNativeUwbManager() { return mNativeUwbManager; } public UwbCountryCode getUwbCountryCode() { return mUwbCountryCode; } public UciLogModeStore getUciLogModeStore() { return mUciLogModeStore; } public UwbMetrics getUwbMetrics() { return mUwbMetrics; } public DeviceConfigFacade getDeviceConfigFacade() { return mDeviceConfigFacade; } public UwbMultichipData getMultichipData() { return mUwbMultichipData; } public UwbServiceCore getUwbServiceCore() { return mUwbService; } public UwbDiagnostics getUwbDiagnostics() { return mUwbDiagnostics; } public UwbSessionManager getUwbSessionManager() { return mUwbSessionManager; } /** * Create a UwbShellCommand instance. */ public UwbShellCommand makeUwbShellCommand(UwbServiceImpl uwbService) { return new UwbShellCommand(this, uwbService, mContext); } /** * Creates a Geocoder. */ @Nullable public Geocoder makeGeocoder() { return new Geocoder(mContext); } /** * Returns whether geocoder is supported on this device or not. */ public boolean isGeocoderPresent() { return Geocoder.isPresent(); } /** * Throws security exception if the UWB_RANGING permission is not granted for the calling app. * *

Should be used in situations where the app op should not be noted. */ public void enforceUwbRangingPermissionForPreflight( @NonNull AttributionSource attributionSource) { if (!attributionSource.checkCallingUid()) { throw new SecurityException("Invalid attribution source " + attributionSource + ", callingUid: " + Binder.getCallingUid()); } int permissionCheckResult = mPermissionManager.checkPermissionForPreflight( UWB_RANGING, attributionSource); if (permissionCheckResult != PERMISSION_GRANTED) { throw new SecurityException("Caller does not hold UWB_RANGING permission"); } } /** * Returns true if the UWB_RANGING permission is granted for the calling app. * *

Used for checking permission before first data delivery for the session. */ public boolean checkUwbRangingPermissionForStartDataDelivery( @NonNull AttributionSource attributionSource, @NonNull String message) { int permissionCheckResult = mPermissionManager.checkPermissionForStartDataDelivery( UWB_RANGING, attributionSource, message); return permissionCheckResult == PERMISSION_GRANTED; } /** Indicate permission manager that the ranging session is done or stopped. */ public void finishUwbRangingPermissionForDataDelivery( @NonNull AttributionSource attributionSource) { mPermissionManager.finishDataDelivery(UWB_RANGING, attributionSource); } /** * Get device protected storage dir for the UWB apex. */ @NonNull public static File getDeviceProtectedDataDir() { return ApexEnvironment.getApexEnvironment(APEX_NAME).getDeviceProtectedDataDir(); } /** * Get integer value from Settings. * * @throws Settings.SettingNotFoundException */ public int getGlobalSettingsInt(@NonNull String key) throws Settings.SettingNotFoundException { return Settings.Global.getInt(mContext.getContentResolver(), key); } /** * Get integer value from Settings. */ public int getGlobalSettingsInt(@NonNull String key, int defValue) { return Settings.Global.getInt(mContext.getContentResolver(), key, defValue); } /** * Get string value from Settings. */ @Nullable public String getGlobalSettingsString(@NonNull String key) { return Settings.Global.getString(mContext.getContentResolver(), key); } /** * Helper method for classes to register a ContentObserver * {@see ContentResolver#registerContentObserver(Uri,boolean,ContentObserver)}. */ public void registerContentObserver(Uri uri, boolean notifyForDescendants, ContentObserver contentObserver) { mContext.getContentResolver().registerContentObserver(uri, notifyForDescendants, contentObserver); } /** * Helper method for classes to unregister a ContentObserver * {@see ContentResolver#unregisterContentObserver(ContentObserver)}. */ public void unregisterContentObserver(ContentObserver contentObserver) { mContext.getContentResolver().unregisterContentObserver(contentObserver); } /** * Uwb user specific folder. */ public static File getCredentialProtectedDataDirForUser(int userId) { return ApexEnvironment.getApexEnvironment(APEX_NAME) .getCredentialProtectedDataDirForUser(UserHandle.of(userId)); } /** * Returns true if the app is in the Uwb apex, false otherwise. * Checks if the app's path starts with "/apex/com.android.uwb". */ public static boolean isAppInUwbApex(ApplicationInfo appInfo) { return appInfo.sourceDir.startsWith(UWB_APEX_PATH); } /** * Get the current time of the clock in milliseconds. * * @return Current time in milliseconds. */ public long getWallClockMillis() { return System.currentTimeMillis(); } /** * Returns milliseconds since boot, including time spent in sleep. * * @return Current time since boot in milliseconds. */ public long getElapsedSinceBootMillis() { return SystemClock.elapsedRealtime(); } /** * Returns nanoseconds since boot, including time spent in sleep. * * @return Current time since boot in milliseconds. */ public long getElapsedSinceBootNanos() { return SystemClock.elapsedRealtimeNanos(); } /** * Is this a valid country code * * @param countryCode A 2-Character alphanumeric country code. * @return true if the countryCode is valid, false otherwise. */ private static boolean isValidCountryCode(String countryCode) { return countryCode != null && countryCode.length() == 2 && countryCode.chars().allMatch(Character::isLetterOrDigit); } /** * Default country code stored in system property * * @return Country code if available, null otherwise. */ public String getOemDefaultCountryCode() { String country = SystemProperties.get(BOOT_DEFAULT_UWB_COUNTRY_CODE); return isValidCountryCode(country) ? country.toUpperCase(Locale.US) : null; } /** * Helper method creating a context based on the app's uid (to deal with multi user scenarios) */ @Nullable private Context createPackageContextAsUser(int uid) { Context userContext; try { userContext = mContext.createPackageContextAsUser(mContext.getPackageName(), 0, UserHandle.getUserHandleForUid(uid)); } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "Unknown package name"); return null; } if (userContext == null) { Log.e(TAG, "Unable to retrieve user context for " + uid); return null; } return userContext; } /** Helper method to check if the app is a system app. */ public boolean isSystemApp(int uid, @NonNull String packageName) { try { ApplicationInfo info = createPackageContextAsUser(uid) .getPackageManager() .getApplicationInfo(packageName, 0); return (info.flags & APP_INFO_FLAGS_SYSTEM_APP) != 0; } catch (PackageManager.NameNotFoundException e) { // In case of exception, assume unknown app (more strict checking) // Note: This case will never happen since checkPackage is // called to verify validity before checking App's version. Log.e(TAG, "Failed to get the app info", e); } return false; } /** Whether the uid is signed with the same key as the platform. */ public boolean isAppSignedWithPlatformKey(int uid) { return mContext.getPackageManager().checkSignatures(uid, Process.SYSTEM_UID) == PackageManager.SIGNATURE_MATCH; } private static Map sOverridePackageImportance = new HashMap(); public void setOverridePackageImportance(String packageName, int importance) { sOverridePackageImportance.put(packageName, importance); } public void resetOverridePackageImportance(String packageName) { sOverridePackageImportance.remove(packageName); } /** Helper method to retrieve app importance. */ private int getPackageImportance(int uid, @NonNull String packageName) { if (sOverridePackageImportance.containsKey(packageName)) { Log.w(TAG, "Overriding package importance for testing"); return sOverridePackageImportance.get(packageName); } try { return createPackageContextAsUser(uid) .getSystemService(ActivityManager.class) .getPackageImportance(packageName); } catch (SecurityException e) { Log.e(TAG, "Failed to retrieve the app importance", e); return ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE; } } /** Helper method to check if the app is from foreground app/service. */ public static boolean isForegroundAppOrServiceImportance(int importance) { return importance <= ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE; } /** Helper method to check if the app is from foreground app/service. */ public boolean isForegroundAppOrService(int uid, @NonNull String packageName) { long identity = Binder.clearCallingIdentity(); try { return isForegroundAppOrServiceImportance(getPackageImportance(uid, packageName)); } catch (SecurityException e) { Log.e(TAG, "Failed to retrieve the app importance", e); return false; } finally { Binder.restoreCallingIdentity(identity); } } /* Helps to mock the executor for tests */ public int runTaskOnSingleThreadExecutor(FutureTask task, int timeoutMs) throws InterruptedException, TimeoutException, ExecutionException { ExecutorService executor = Executors.newSingleThreadExecutor(); executor.submit(task); try { return task.get(timeoutMs, TimeUnit.MILLISECONDS); } catch (TimeoutException e) { executor.shutdownNow(); throw e; } } public boolean isMulticastListNtfV2Supported() { return mContext.getResources().getBoolean( com.android.uwb.resources.R.bool.is_multicast_list_update_ntf_v2_supported); } public boolean isMulticastListRspV2Supported() { return mContext.getResources().getBoolean( com.android.uwb.resources.R.bool.is_multicast_list_update_rsp_v2_supported); } /** * Gets the configured pose source, which is reference counted. If there are no references * to the pose source, one will be created based on the device configuration. This may * @return A shared or new pose source, or null if one is not configured or available. */ public IPoseSource acquirePoseSource() { mPoseLock.lock(); try { // Keep our ref counts accurate because isEnableFilters can change at runtime. mPoseSourceRefCount++; if (!getDeviceConfigFacade().isEnableFilters()) { return null; } if (mDefaultPoseSource != null) { // Already have a pose source. return mDefaultPoseSource; } switch (mDeviceConfigFacade.getPoseSourceType()) { case NONE: mDefaultPoseSource = null; break; case ROTATION_VECTOR: mDefaultPoseSource = new RotationPoseSource(mContext, 100); break; case GYRO: mDefaultPoseSource = new GyroPoseSource(mContext, 100); break; case SIXDOF: mDefaultPoseSource = new SixDofPoseSource(mContext, 100); break; case DOUBLE_INTEGRATE: mDefaultPoseSource = new IntegPoseSource(mContext, 100); break; } return mDefaultPoseSource; } catch (Exception ex) { Log.e(TAG, "Unable to create the configured UWB pose source: " + ex.getMessage()); mPoseSourceRefCount--; return null; } finally { mPoseLock.unlock(); } } /** * Decrements the reference counts to the default pose source, and closes the source once the * count reaches zero. This must be called once for each time acquirePoseSource() is called. */ public void releasePoseSource() { mPoseLock.lock(); try { // Keep our ref counts accurate because isEnableFilters can change at runtime. --mPoseSourceRefCount; if (mPoseSourceRefCount <= 0 && mDefaultPoseSource != null) { mDefaultPoseSource.close(); mDefaultPoseSource = null; } } finally { mPoseLock.unlock(); } } /** * Creates a filter engine using the default pose source. A default pose source must first be * acquired with {@link #acquirePoseSource()}. * * @return A fully configured filter engine, or null if filtering is disabled. */ public UwbFilterEngine createFilterEngine(IPoseSource poseSource) { DeviceConfigFacade cfg = getDeviceConfigFacade(); if (!cfg.isEnableFilters()) { return null; } // This could go wrong if the config flags or overlay have bad values. try { IFilter azimuthFilter = new MedAvgRotationFilter( cfg.getFilterAngleWindow(), cfg.getFilterAngleInliersPercent() / 100f); IFilter elevationFilter = new MedAvgRotationFilter( cfg.getFilterAngleWindow(), cfg.getFilterAngleInliersPercent() / 100f); IFilter distanceFilter = new MedAvgFilter( cfg.getFilterDistanceWindow(), cfg.getFilterDistanceInliersPercent() / 100f); PositionFilterImpl posFilter = new PositionFilterImpl( azimuthFilter, elevationFilter, distanceFilter); UwbFilterEngine.Builder builder = new UwbFilterEngine.Builder().setFilter(posFilter); if (poseSource != null) { builder.setPoseSource(poseSource); } // Order is important. if (cfg.isEnablePrimerEstElevation()) { builder.addPrimer(new ElevationPrimer()); } // AoAPrimer requires an elevation estimation in order to convert to spherical coords. if (cfg.isEnablePrimerAoA()) { builder.addPrimer(new AoaPrimer()); } // Fov requires an elevation and a spherical coord. if (cfg.isEnablePrimerFov()) { builder.addPrimer(new FovPrimer((float) toRadians(cfg.getPrimerFovDegree()))); } // Back azimuth detection requires true spherical. if (cfg.isEnableBackAzimuth()) { builder.addPrimer(new BackAzimuthPrimer( cfg.getFrontAzimuthRadiansPerSecond(), cfg.getBackAzimuthRadiansPerSecond(), cfg.getBackAzimuthWindow(), cfg.isEnableBackAzimuthMasking(), cfg.getMirrorScoreStdRadians(), cfg.getBackNoiseInfluenceCoeff())); } return builder.build(); } catch (Exception ex) { Log.e(TAG, "Unable to create UWB filter engine: " + ex.getMessage()); return null; } } }