• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.uwb;
18 
19 import static android.Manifest.permission.UWB_RANGING;
20 import static android.permission.PermissionManager.PERMISSION_GRANTED;
21 
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.app.ActivityManager;
25 import android.app.AlarmManager;
26 import android.content.ApexEnvironment;
27 import android.content.AttributionSource;
28 import android.content.Context;
29 import android.content.pm.ApplicationInfo;
30 import android.content.pm.PackageManager;
31 import android.database.ContentObserver;
32 import android.location.Geocoder;
33 import android.net.Uri;
34 import android.os.Binder;
35 import android.os.Handler;
36 import android.os.HandlerThread;
37 import android.os.Looper;
38 import android.os.Process;
39 import android.os.SystemClock;
40 import android.os.SystemProperties;
41 import android.os.UserHandle;
42 import android.os.UserManager;
43 import android.permission.PermissionManager;
44 import android.provider.Settings;
45 import android.util.AtomicFile;
46 import android.util.Log;
47 
48 import com.android.server.uwb.advertisement.UwbAdvertiseManager;
49 import com.android.server.uwb.correction.UwbFilterEngine;
50 import com.android.server.uwb.correction.filtering.IFilter;
51 import com.android.server.uwb.correction.filtering.MedAvgFilter;
52 import com.android.server.uwb.correction.filtering.MedAvgRotationFilter;
53 import com.android.server.uwb.correction.filtering.PositionFilterImpl;
54 import com.android.server.uwb.correction.pose.GyroPoseSource;
55 import com.android.server.uwb.correction.pose.IPoseSource;
56 import com.android.server.uwb.correction.pose.IntegPoseSource;
57 import com.android.server.uwb.correction.pose.RotationPoseSource;
58 import com.android.server.uwb.correction.pose.SixDofPoseSource;
59 import com.android.server.uwb.correction.primers.AoaPrimer;
60 import com.android.server.uwb.correction.primers.BackAzimuthPrimer;
61 import com.android.server.uwb.correction.primers.ElevationPrimer;
62 import com.android.server.uwb.correction.primers.FovPrimer;
63 import com.android.server.uwb.data.ServiceProfileData;
64 import com.android.server.uwb.jni.NativeUwbManager;
65 import com.android.server.uwb.multchip.UwbMultichipData;
66 import com.android.server.uwb.pm.ProfileManager;
67 
68 import java.io.File;
69 import java.util.HashMap;
70 import java.util.Locale;
71 import java.util.Map;
72 import java.util.concurrent.ExecutionException;
73 import java.util.concurrent.ExecutorService;
74 import java.util.concurrent.Executors;
75 import java.util.concurrent.FutureTask;
76 import java.util.concurrent.TimeUnit;
77 import java.util.concurrent.TimeoutException;
78 import java.util.concurrent.locks.ReentrantLock;
79 
80 /**
81  * To be used for dependency injection (especially helps mocking static dependencies).
82  */
83 public class UwbInjector {
84     private static final String TAG = "UwbInjector";
85     private static final String APEX_NAME = "com.android.uwb";
86     private static final String VENDOR_SERVICE_NAME = "uwb_vendor";
87     private static final String BOOT_DEFAULT_UWB_COUNTRY_CODE = "ro.boot.uwbcountrycode";
88 
89     /**
90      * The path where the Uwb apex is mounted.
91      * Current value = "/apex/com.android.uwb"
92      */
93     private static final String UWB_APEX_PATH =
94             new File("/apex", APEX_NAME).getAbsolutePath();
95     private static final int APP_INFO_FLAGS_SYSTEM_APP =
96             ApplicationInfo.FLAG_SYSTEM | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP;
97 
98     private final UwbContext mContext;
99     private final Looper mLooper;
100     private final PermissionManager mPermissionManager;
101     private final UserManager mUserManager;
102     private final UwbConfigStore mUwbConfigStore;
103     private final ProfileManager mProfileManager;
104     private final UwbSettingsStore mUwbSettingsStore;
105     private final NativeUwbManager mNativeUwbManager;
106     private final UwbCountryCode mUwbCountryCode;
107     private final UciLogModeStore mUciLogModeStore;
108     private final UwbServiceCore mUwbService;
109     private final UwbMetrics mUwbMetrics;
110     private final DeviceConfigFacade mDeviceConfigFacade;
111     private final UwbMultichipData mUwbMultichipData;
112     private final SystemBuildProperties mSystemBuildProperties;
113     private final UwbDiagnostics mUwbDiagnostics;
114     private IPoseSource mDefaultPoseSource;
115     private final ReentrantLock mPoseLock = new ReentrantLock();
116     private int mPoseSourceRefCount = 0;
117 
118     private final UwbSessionManager mUwbSessionManager;
119 
UwbInjector(@onNull UwbContext context)120     public UwbInjector(@NonNull UwbContext context) {
121         // Create UWB service thread.
122         HandlerThread uwbHandlerThread = new HandlerThread("UwbService");
123         uwbHandlerThread.start();
124         mLooper = uwbHandlerThread.getLooper();
125 
126         mContext = context;
127         mPermissionManager = context.getSystemService(PermissionManager.class);
128         mUserManager = mContext.getSystemService(UserManager.class);
129         mUwbConfigStore = new UwbConfigStore(context, new Handler(mLooper), this,
130                 UwbConfigStore.createSharedFiles());
131         mProfileManager = new ProfileManager(context, new Handler(mLooper),
132                 mUwbConfigStore, this);
133         mUwbSettingsStore = new UwbSettingsStore(
134                 context, new Handler(mLooper),
135                 new AtomicFile(new File(getDeviceProtectedDataDir(),
136                         UwbSettingsStore.FILE_NAME)), this);
137         mUwbMultichipData = new UwbMultichipData(mContext);
138         mUciLogModeStore = new UciLogModeStore(mUwbSettingsStore);
139         mNativeUwbManager = new NativeUwbManager(this, mUciLogModeStore, mUwbMultichipData);
140         mUwbCountryCode =
141                 new UwbCountryCode(mContext, mNativeUwbManager, new Handler(mLooper), this);
142         mUwbMetrics = new UwbMetrics(this);
143         mDeviceConfigFacade = new DeviceConfigFacade(new Handler(mLooper), mContext);
144         UwbConfigurationManager uwbConfigurationManager =
145                 new UwbConfigurationManager(mNativeUwbManager);
146         UwbSessionNotificationManager uwbSessionNotificationManager =
147                 new UwbSessionNotificationManager(this);
148         UwbAdvertiseManager uwbAdvertiseManager = new UwbAdvertiseManager(this,
149                 mDeviceConfigFacade);
150         mUwbSessionManager =
151                 new UwbSessionManager(uwbConfigurationManager, mNativeUwbManager, mUwbMetrics,
152                         uwbAdvertiseManager, uwbSessionNotificationManager, this,
153                         mContext.getSystemService(AlarmManager.class),
154                         mContext.getSystemService(ActivityManager.class),
155                         mLooper);
156         mUwbService = new UwbServiceCore(mContext, mNativeUwbManager, mUwbMetrics,
157                 mUwbCountryCode, mUwbSessionManager, uwbConfigurationManager, this, mLooper);
158         mSystemBuildProperties = new SystemBuildProperties();
159         mUwbDiagnostics = new UwbDiagnostics(mContext, this, mSystemBuildProperties);
160     }
161 
getUwbServiceLooper()162     public Looper getUwbServiceLooper() {
163         return mLooper;
164     }
165 
getUserManager()166     public UserManager getUserManager() {
167         return mUserManager;
168     }
169 
170     /**
171      * Construct an instance of {@link ServiceProfileData}.
172      */
makeServiceProfileData(ServiceProfileData.DataSource dataSource)173     public ServiceProfileData makeServiceProfileData(ServiceProfileData.DataSource dataSource) {
174         return new ServiceProfileData(dataSource);
175     }
176 
getProfileManager()177     public ProfileManager getProfileManager() {
178         return mProfileManager;
179     }
180 
getUwbConfigStore()181     public UwbConfigStore getUwbConfigStore() {
182         return mUwbConfigStore;
183     }
184 
getUwbSettingsStore()185     public UwbSettingsStore getUwbSettingsStore() {
186         return mUwbSettingsStore;
187     }
188 
getNativeUwbManager()189     public NativeUwbManager getNativeUwbManager() {
190         return mNativeUwbManager;
191     }
192 
getUwbCountryCode()193     public UwbCountryCode getUwbCountryCode() {
194         return mUwbCountryCode;
195     }
196 
getUciLogModeStore()197     public UciLogModeStore getUciLogModeStore() {
198         return mUciLogModeStore;
199     }
200 
getUwbMetrics()201     public UwbMetrics getUwbMetrics() {
202         return mUwbMetrics;
203     }
204 
getDeviceConfigFacade()205     public DeviceConfigFacade getDeviceConfigFacade() {
206         return mDeviceConfigFacade;
207     }
208 
getMultichipData()209     public UwbMultichipData getMultichipData() {
210         return mUwbMultichipData;
211     }
212 
getUwbServiceCore()213     public UwbServiceCore getUwbServiceCore() {
214         return mUwbService;
215     }
216 
getUwbDiagnostics()217     public UwbDiagnostics getUwbDiagnostics() {
218         return mUwbDiagnostics;
219     }
220 
getUwbSessionManager()221     public UwbSessionManager getUwbSessionManager() {
222         return mUwbSessionManager;
223     }
224 
225     /**
226      * Create a UwbShellCommand instance.
227      */
makeUwbShellCommand(UwbServiceImpl uwbService)228     public UwbShellCommand makeUwbShellCommand(UwbServiceImpl uwbService) {
229         return new UwbShellCommand(this, uwbService, mContext);
230     }
231 
232     /**
233      * Creates a Geocoder.
234      */
235     @Nullable
makeGeocoder()236     public Geocoder makeGeocoder() {
237         return new Geocoder(mContext);
238     }
239 
240     /**
241      * Returns whether geocoder is supported on this device or not.
242      */
isGeocoderPresent()243     public boolean isGeocoderPresent() {
244         return Geocoder.isPresent();
245     }
246 
247     /**
248      * Throws security exception if the UWB_RANGING permission is not granted for the calling app.
249      *
250      * <p>Should be used in situations where the app op should not be noted.
251      */
enforceUwbRangingPermissionForPreflight( @onNull AttributionSource attributionSource)252     public void enforceUwbRangingPermissionForPreflight(
253             @NonNull AttributionSource attributionSource) {
254         if (!attributionSource.checkCallingUid()) {
255             throw new SecurityException("Invalid attribution source " + attributionSource
256                     + ", callingUid: " + Binder.getCallingUid());
257         }
258         int permissionCheckResult = mPermissionManager.checkPermissionForPreflight(
259                 UWB_RANGING, attributionSource);
260         if (permissionCheckResult != PERMISSION_GRANTED) {
261             throw new SecurityException("Caller does not hold UWB_RANGING permission");
262         }
263     }
264 
265     /**
266      * Returns true if the UWB_RANGING permission is granted for the calling app.
267      *
268      * <p>Should be used in situations where data will be delivered and hence the app op should
269      * be noted.
270      */
checkUwbRangingPermissionForDataDelivery( @onNull AttributionSource attributionSource, @NonNull String message)271     public boolean checkUwbRangingPermissionForDataDelivery(
272             @NonNull AttributionSource attributionSource, @NonNull String message) {
273         int permissionCheckResult = mPermissionManager.checkPermissionForDataDelivery(
274                 UWB_RANGING, attributionSource, message);
275         return permissionCheckResult == PERMISSION_GRANTED;
276     }
277 
278     /**
279      * Get device protected storage dir for the UWB apex.
280      */
281     @NonNull
getDeviceProtectedDataDir()282     public static File getDeviceProtectedDataDir() {
283         return ApexEnvironment.getApexEnvironment(APEX_NAME).getDeviceProtectedDataDir();
284     }
285 
286     /**
287      * Get integer value from Settings.
288      *
289      * @throws Settings.SettingNotFoundException
290      */
getGlobalSettingsInt(@onNull String key)291     public int getGlobalSettingsInt(@NonNull String key) throws Settings.SettingNotFoundException {
292         return Settings.Global.getInt(mContext.getContentResolver(), key);
293     }
294 
295     /**
296      * Get integer value from Settings.
297      */
getGlobalSettingsInt(@onNull String key, int defValue)298     public int getGlobalSettingsInt(@NonNull String key, int defValue) {
299         return Settings.Global.getInt(mContext.getContentResolver(), key, defValue);
300     }
301 
302     /**
303      * Get string value from Settings.
304      */
305     @Nullable
getGlobalSettingsString(@onNull String key)306     public String getGlobalSettingsString(@NonNull String key) {
307         return Settings.Global.getString(mContext.getContentResolver(), key);
308     }
309 
310     /**
311      * Helper method for classes to register a ContentObserver
312      * {@see ContentResolver#registerContentObserver(Uri,boolean,ContentObserver)}.
313      */
registerContentObserver(Uri uri, boolean notifyForDescendants, ContentObserver contentObserver)314     public void registerContentObserver(Uri uri, boolean notifyForDescendants,
315             ContentObserver contentObserver) {
316         mContext.getContentResolver().registerContentObserver(uri, notifyForDescendants,
317                 contentObserver);
318     }
319 
320     /**
321      * Helper method for classes to unregister a ContentObserver
322      * {@see ContentResolver#unregisterContentObserver(ContentObserver)}.
323      */
unregisterContentObserver(ContentObserver contentObserver)324     public void unregisterContentObserver(ContentObserver contentObserver) {
325         mContext.getContentResolver().unregisterContentObserver(contentObserver);
326     }
327 
328     /**
329      * Uwb user specific folder.
330      */
getCredentialProtectedDataDirForUser(int userId)331     public static File getCredentialProtectedDataDirForUser(int userId) {
332         return ApexEnvironment.getApexEnvironment(APEX_NAME)
333                 .getCredentialProtectedDataDirForUser(UserHandle.of(userId));
334     }
335 
336     /**
337      * Returns true if the app is in the Uwb apex, false otherwise.
338      * Checks if the app's path starts with "/apex/com.android.uwb".
339      */
isAppInUwbApex(ApplicationInfo appInfo)340     public static boolean isAppInUwbApex(ApplicationInfo appInfo) {
341         return appInfo.sourceDir.startsWith(UWB_APEX_PATH);
342     }
343 
344     /**
345      * Get the current time of the clock in milliseconds.
346      *
347      * @return Current time in milliseconds.
348      */
getWallClockMillis()349     public long getWallClockMillis() {
350         return System.currentTimeMillis();
351     }
352 
353     /**
354      * Returns milliseconds since boot, including time spent in sleep.
355      *
356      * @return Current time since boot in milliseconds.
357      */
getElapsedSinceBootMillis()358     public long getElapsedSinceBootMillis() {
359         return SystemClock.elapsedRealtime();
360     }
361 
362     /**
363      * Returns nanoseconds since boot, including time spent in sleep.
364      *
365      * @return Current time since boot in milliseconds.
366      */
getElapsedSinceBootNanos()367     public long getElapsedSinceBootNanos() {
368         return SystemClock.elapsedRealtimeNanos();
369     }
370 
371     /**
372      * Is this a valid country code
373      *
374      * @param countryCode A 2-Character alphanumeric country code.
375      * @return true if the countryCode is valid, false otherwise.
376      */
isValidCountryCode(String countryCode)377     private static boolean isValidCountryCode(String countryCode) {
378         return countryCode != null && countryCode.length() == 2
379                 && countryCode.chars().allMatch(Character::isLetterOrDigit);
380     }
381 
382     /**
383      * Default country code stored in system property
384      *
385      * @return Country code if available, null otherwise.
386      */
getOemDefaultCountryCode()387     public String getOemDefaultCountryCode() {
388         String country = SystemProperties.get(BOOT_DEFAULT_UWB_COUNTRY_CODE);
389         return isValidCountryCode(country) ? country.toUpperCase(Locale.US) : null;
390     }
391 
392     /**
393      * Helper method creating a context based on the app's uid (to deal with multi user scenarios)
394      */
395     @Nullable
createPackageContextAsUser(int uid)396     private Context createPackageContextAsUser(int uid) {
397         Context userContext;
398         try {
399             userContext = mContext.createPackageContextAsUser(mContext.getPackageName(), 0,
400                     UserHandle.getUserHandleForUid(uid));
401         } catch (PackageManager.NameNotFoundException e) {
402             Log.e(TAG, "Unknown package name");
403             return null;
404         }
405         if (userContext == null) {
406             Log.e(TAG, "Unable to retrieve user context for " + uid);
407             return null;
408         }
409         return userContext;
410     }
411 
412     /** Helper method to check if the app is a system app. */
isSystemApp(int uid, @NonNull String packageName)413     public boolean isSystemApp(int uid, @NonNull String packageName) {
414         try {
415             ApplicationInfo info = createPackageContextAsUser(uid)
416                     .getPackageManager()
417                     .getApplicationInfo(packageName, 0);
418             return (info.flags & APP_INFO_FLAGS_SYSTEM_APP) != 0;
419         } catch (PackageManager.NameNotFoundException e) {
420             // In case of exception, assume unknown app (more strict checking)
421             // Note: This case will never happen since checkPackage is
422             // called to verify validity before checking App's version.
423             Log.e(TAG, "Failed to get the app info", e);
424         }
425         return false;
426     }
427 
428     /** Whether the uid is signed with the same key as the platform. */
isAppSignedWithPlatformKey(int uid)429     public boolean isAppSignedWithPlatformKey(int uid) {
430         return mContext.getPackageManager().checkSignatures(uid, Process.SYSTEM_UID)
431                 == PackageManager.SIGNATURE_MATCH;
432     }
433 
434     private static Map<String, Integer> sOverridePackageImportance = new HashMap();
setOverridePackageImportance(String packageName, int importance)435     public void setOverridePackageImportance(String packageName, int importance) {
436         sOverridePackageImportance.put(packageName, importance);
437     }
resetOverridePackageImportance(String packageName)438     public void resetOverridePackageImportance(String packageName) {
439         sOverridePackageImportance.remove(packageName);
440     }
441 
442     /** Helper method to retrieve app importance. */
getPackageImportance(int uid, @NonNull String packageName)443     private int getPackageImportance(int uid, @NonNull String packageName) {
444         if (sOverridePackageImportance.containsKey(packageName)) {
445             Log.w(TAG, "Overriding package importance for testing");
446             return sOverridePackageImportance.get(packageName);
447         }
448         try {
449             return createPackageContextAsUser(uid)
450                     .getSystemService(ActivityManager.class)
451                     .getPackageImportance(packageName);
452         } catch (SecurityException e) {
453             Log.e(TAG, "Failed to retrieve the app importance", e);
454             return ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE;
455         }
456     }
457 
458     /** Helper method to check if the app is from foreground app/service. */
isForegroundAppOrServiceImportance(int importance)459     public static boolean isForegroundAppOrServiceImportance(int importance) {
460         return importance <= ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE;
461     }
462 
463     /** Helper method to check if the app is from foreground app/service. */
isForegroundAppOrService(int uid, @NonNull String packageName)464     public boolean isForegroundAppOrService(int uid, @NonNull String packageName) {
465         try {
466             return isForegroundAppOrServiceImportance(getPackageImportance(uid, packageName));
467         } catch (SecurityException e) {
468             Log.e(TAG, "Failed to retrieve the app importance", e);
469             return false;
470         }
471     }
472 
473     /* Helps to mock the executor for tests */
runTaskOnSingleThreadExecutor(FutureTask<Integer> task, int timeoutMs)474     public int runTaskOnSingleThreadExecutor(FutureTask<Integer> task, int timeoutMs)
475             throws InterruptedException, TimeoutException, ExecutionException {
476         ExecutorService executor = Executors.newSingleThreadExecutor();
477         executor.submit(task);
478         try {
479             return task.get(timeoutMs, TimeUnit.MILLISECONDS);
480         } catch (TimeoutException e) {
481             executor.shutdownNow();
482             throw e;
483         }
484     }
485 
486     /**
487      * Gets the configured pose source, which is reference counted. If there are no references
488      * to the pose source, one will be created based on the device configuration. This may
489      * @return A shared or new pose source, or null if one is not configured or available.
490      */
acquirePoseSource()491     public IPoseSource acquirePoseSource() {
492         mPoseLock.lock();
493         try {
494             // Keep our ref counts accurate because isEnableFilters can change at runtime.
495             mPoseSourceRefCount++;
496 
497             if (!getDeviceConfigFacade().isEnableFilters()) {
498                 return null;
499             }
500 
501             if (mDefaultPoseSource != null) {
502                 // Already have a pose source.
503                 return mDefaultPoseSource;
504             }
505 
506             switch (mDeviceConfigFacade.getPoseSourceType()) {
507                 case NONE:
508                     mDefaultPoseSource = null;
509                     break;
510                 case ROTATION_VECTOR:
511                     mDefaultPoseSource = new RotationPoseSource(mContext, 100);
512                     break;
513                 case GYRO:
514                     mDefaultPoseSource = new GyroPoseSource(mContext, 100);
515                     break;
516                 case SIXDOF:
517                     mDefaultPoseSource = new SixDofPoseSource(mContext, 100);
518                     break;
519                 case DOUBLE_INTEGRATE:
520                     mDefaultPoseSource = new IntegPoseSource(mContext, 100);
521                     break;
522             }
523 
524             return mDefaultPoseSource;
525         } catch (Exception ex) {
526             Log.e(TAG, "Unable to create the configured UWB pose source: "
527                     + ex.getMessage());
528             mPoseSourceRefCount--;
529             return null;
530         } finally {
531             mPoseLock.unlock();
532         }
533     }
534 
535     /**
536      * Decrements the reference counts to the default pose source, and closes the source once the
537      * count reaches zero. This must be called once for each time acquirePoseSource() is called.
538      */
releasePoseSource()539     public void releasePoseSource() {
540         mPoseLock.lock();
541         try {
542             // Keep our ref counts accurate because isEnableFilters can change at runtime.
543             --mPoseSourceRefCount;
544             if (mPoseSourceRefCount <= 0 && mDefaultPoseSource != null) {
545                 mDefaultPoseSource.close();
546                 mDefaultPoseSource = null;
547             }
548         } finally {
549             mPoseLock.unlock();
550         }
551     }
552 
553     /**
554      * Creates a filter engine using the default pose source. A default pose source must first be
555      * acquired with {@link #acquirePoseSource()}.
556      *
557      * @return A fully configured filter engine, or null if filtering is disabled.
558      */
createFilterEngine(IPoseSource poseSource)559     public UwbFilterEngine createFilterEngine(IPoseSource poseSource) {
560         DeviceConfigFacade cfg = getDeviceConfigFacade();
561         if (!cfg.isEnableFilters()) {
562             return null;
563         }
564 
565         // This could go wrong if the config flags or overlay have bad values.
566         try {
567             IFilter azimuthFilter = new MedAvgRotationFilter(
568                     cfg.getFilterAngleWindow(),
569                     cfg.getFilterAngleInliersPercent() / 100f);
570             IFilter elevationFilter = new MedAvgRotationFilter(
571                     cfg.getFilterAngleWindow(),
572                     cfg.getFilterAngleInliersPercent() / 100f);
573             IFilter distanceFilter = new MedAvgFilter(
574                     cfg.getFilterDistanceWindow(),
575                     cfg.getFilterDistanceInliersPercent() / 100f);
576 
577             PositionFilterImpl posFilter = new PositionFilterImpl(
578                     azimuthFilter,
579                     elevationFilter,
580                     distanceFilter);
581 
582             UwbFilterEngine.Builder builder = new UwbFilterEngine.Builder().setFilter(posFilter);
583 
584             if (poseSource != null) {
585                 builder.setPoseSource(poseSource);
586             }
587 
588             // Order is important.
589             if (cfg.isEnablePrimerEstElevation()) {
590                 builder.addPrimer(new ElevationPrimer());
591             }
592 
593             // AoAPrimer requires an elevation estimation in order to convert to spherical coords.
594             if (cfg.isEnablePrimerAoA()) {
595                 builder.addPrimer(new AoaPrimer());
596             }
597 
598             // Fov requires an elevation and a spherical coord.
599             if (cfg.isEnablePrimerFov()) {
600                 builder.addPrimer(new FovPrimer(cfg.getPrimerFovDegree()));
601             }
602 
603             // Back azimuth detection requires true spherical.
604             if (cfg.isEnableBackAzimuth()) {
605                 builder.addPrimer(new BackAzimuthPrimer(
606                         cfg.getFrontAzimuthRadiansPerSecond(),
607                         cfg.getBackAzimuthRadiansPerSecond(),
608                         cfg.getBackAzimuthWindow(),
609                         cfg.isEnableBackAzimuthMasking(),
610                         cfg.getMirrorScoreStdRadians(),
611                         cfg.getBackNoiseInfluenceCoeff()));
612             }
613 
614             return builder.build();
615         } catch (Exception ex) {
616             Log.e(TAG, "Unable to create UWB filter engine: " + ex.getMessage());
617             return null;
618         }
619     }
620 }
621