1 /* 2 * Copyright (C) 2020 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.location.gnss; 18 19 import android.annotation.Nullable; 20 import android.annotation.SuppressLint; 21 import android.app.AppOpsManager; 22 import android.app.Notification; 23 import android.app.NotificationManager; 24 import android.content.BroadcastReceiver; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.IntentFilter; 28 import android.content.pm.ApplicationInfo; 29 import android.content.pm.PackageManager; 30 import android.os.Handler; 31 import android.os.Looper; 32 import android.os.PowerManager; 33 import android.os.UserHandle; 34 import android.text.TextUtils; 35 import android.util.ArrayMap; 36 import android.util.Log; 37 38 import com.android.internal.R; 39 import com.android.internal.location.GpsNetInitiatedHandler; 40 import com.android.internal.notification.SystemNotificationChannels; 41 import com.android.internal.util.FrameworkStatsLog; 42 43 import java.util.Arrays; 44 import java.util.List; 45 import java.util.Map; 46 47 /** 48 * Handles GNSS non-framework location access user visibility and control. 49 * 50 * The state of the GnssVisibilityControl object must be accessed/modified through the Handler 51 * thread only. 52 */ 53 class GnssVisibilityControl { 54 private static final String TAG = "GnssVisibilityControl"; 55 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 56 57 private static final String LOCATION_PERMISSION_NAME = 58 "android.permission.ACCESS_FINE_LOCATION"; 59 60 private static final String[] NO_LOCATION_ENABLED_PROXY_APPS = new String[0]; 61 62 // Max wait time for synchronous method onGpsEnabledChanged() to run. 63 private static final long ON_GPS_ENABLED_CHANGED_TIMEOUT_MILLIS = 3 * 1000; 64 65 // How long to display location icon for each non-framework non-emergency location request. 66 private static final long LOCATION_ICON_DISPLAY_DURATION_MILLIS = 5 * 1000; 67 68 // Wakelocks 69 private static final String WAKELOCK_KEY = TAG; 70 private static final long WAKELOCK_TIMEOUT_MILLIS = 60 * 1000; 71 private static final long EMERGENCY_EXTENSION_FOR_MISMATCH = 128 * 1000; 72 private final PowerManager.WakeLock mWakeLock; 73 74 private final AppOpsManager mAppOps; 75 private final PackageManager mPackageManager; 76 77 private final Handler mHandler; 78 private final Context mContext; 79 private final GpsNetInitiatedHandler mNiHandler; 80 81 private boolean mIsGpsEnabled; 82 83 private static final class ProxyAppState { 84 private boolean mHasLocationPermission; 85 private boolean mIsLocationIconOn; 86 ProxyAppState(boolean hasLocationPermission)87 private ProxyAppState(boolean hasLocationPermission) { 88 mHasLocationPermission = hasLocationPermission; 89 } 90 } 91 92 // Number of non-framework location access proxy apps is expected to be small (< 5). 93 private static final int ARRAY_MAP_INITIAL_CAPACITY_PROXY_APPS_STATE = 5; 94 private ArrayMap<String, ProxyAppState> mProxyAppsState = new ArrayMap<>( 95 ARRAY_MAP_INITIAL_CAPACITY_PROXY_APPS_STATE); 96 97 private PackageManager.OnPermissionsChangedListener mOnPermissionsChangedListener = 98 uid -> runOnHandler(() -> handlePermissionsChanged(uid)); 99 GnssVisibilityControl(Context context, Looper looper, GpsNetInitiatedHandler niHandler)100 GnssVisibilityControl(Context context, Looper looper, GpsNetInitiatedHandler niHandler) { 101 mContext = context; 102 PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); 103 mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_KEY); 104 mHandler = new Handler(looper); 105 mNiHandler = niHandler; 106 mAppOps = mContext.getSystemService(AppOpsManager.class); 107 mPackageManager = mContext.getPackageManager(); 108 109 // Complete initialization as the first event to run in mHandler thread. After that, 110 // all object state read/update events run in the mHandler thread. 111 runOnHandler(this::handleInitialize); 112 } 113 onGpsEnabledChanged(boolean isEnabled)114 void onGpsEnabledChanged(boolean isEnabled) { 115 // The GnssLocationProvider's methods: handleEnable() calls this method after native_init() 116 // and handleDisable() calls this method before native_cleanup(). This method must be 117 // executed synchronously so that the NFW location access permissions are disabled in 118 // the HAL before native_cleanup() method is called. 119 // 120 // NOTE: Since improper use of runWithScissors() method can result in deadlocks, the method 121 // doc recommends limiting its use to cases where some initialization steps need to be 122 // executed in sequence before continuing which fits this scenario. 123 if (mHandler.runWithScissors(() -> handleGpsEnabledChanged(isEnabled), 124 ON_GPS_ENABLED_CHANGED_TIMEOUT_MILLIS)) { 125 return; 126 } 127 128 // After timeout, the method remains posted in the queue and hence future enable/disable 129 // calls to this method will all get executed in the correct sequence. But this timeout 130 // situation should not even arise because runWithScissors() will run in the caller's 131 // thread without blocking as it is the same thread as mHandler's thread. 132 if (!isEnabled) { 133 Log.w(TAG, "Native call to disable non-framework location access in GNSS HAL may" 134 + " get executed after native_cleanup()."); 135 } 136 } 137 reportNfwNotification(String proxyAppPackageName, byte protocolStack, String otherProtocolStackName, byte requestor, String requestorId, byte responseType, boolean inEmergencyMode, boolean isCachedLocation)138 void reportNfwNotification(String proxyAppPackageName, byte protocolStack, 139 String otherProtocolStackName, byte requestor, String requestorId, byte responseType, 140 boolean inEmergencyMode, boolean isCachedLocation) { 141 runOnHandler(() -> handleNfwNotification( 142 new NfwNotification(proxyAppPackageName, protocolStack, otherProtocolStackName, 143 requestor, requestorId, responseType, inEmergencyMode, isCachedLocation))); 144 } 145 onConfigurationUpdated(GnssConfiguration configuration)146 void onConfigurationUpdated(GnssConfiguration configuration) { 147 // The configuration object must be accessed only in the caller thread and not in mHandler. 148 List<String> nfwLocationAccessProxyApps = configuration.getProxyApps(); 149 runOnHandler(() -> handleUpdateProxyApps(nfwLocationAccessProxyApps)); 150 } 151 handleInitialize()152 private void handleInitialize() { 153 listenForProxyAppsPackageUpdates(); 154 } 155 listenForProxyAppsPackageUpdates()156 private void listenForProxyAppsPackageUpdates() { 157 // Listen for proxy apps package installation, removal events. 158 IntentFilter intentFilter = new IntentFilter(); 159 intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); 160 intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); 161 intentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED); 162 intentFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); 163 intentFilter.addDataScheme("package"); 164 mContext.registerReceiverAsUser(new BroadcastReceiver() { 165 @Override 166 public void onReceive(Context context, Intent intent) { 167 String action = intent.getAction(); 168 if (action == null) { 169 return; 170 } 171 172 switch (action) { 173 case Intent.ACTION_PACKAGE_ADDED: 174 case Intent.ACTION_PACKAGE_REMOVED: 175 case Intent.ACTION_PACKAGE_REPLACED: 176 case Intent.ACTION_PACKAGE_CHANGED: 177 String pkgName = intent.getData().getEncodedSchemeSpecificPart(); 178 handleProxyAppPackageUpdate(pkgName, action); 179 break; 180 } 181 } 182 }, UserHandle.ALL, intentFilter, null, mHandler); 183 } 184 handleProxyAppPackageUpdate(String pkgName, String action)185 private void handleProxyAppPackageUpdate(String pkgName, String action) { 186 final ProxyAppState proxyAppState = mProxyAppsState.get(pkgName); 187 if (proxyAppState == null) { 188 return; // ignore, pkgName is not one of the proxy apps in our list. 189 } 190 191 if (DEBUG) Log.d(TAG, "Proxy app " + pkgName + " package changed: " + action); 192 final boolean updatedLocationPermission = shouldEnableLocationPermissionInGnssHal(pkgName); 193 if (proxyAppState.mHasLocationPermission != updatedLocationPermission) { 194 // Permission changed. So, update the GNSS HAL with the updated list. 195 Log.i(TAG, "Proxy app " + pkgName + " location permission changed." 196 + " IsLocationPermissionEnabled: " + updatedLocationPermission); 197 proxyAppState.mHasLocationPermission = updatedLocationPermission; 198 updateNfwLocationAccessProxyAppsInGnssHal(); 199 } 200 } 201 handleUpdateProxyApps(List<String> nfwLocationAccessProxyApps)202 private void handleUpdateProxyApps(List<String> nfwLocationAccessProxyApps) { 203 if (!isProxyAppListUpdated(nfwLocationAccessProxyApps)) { 204 return; 205 } 206 207 if (nfwLocationAccessProxyApps.isEmpty()) { 208 // Stop listening for app permission changes. Clear the app list in GNSS HAL. 209 if (!mProxyAppsState.isEmpty()) { 210 mPackageManager.removeOnPermissionsChangeListener(mOnPermissionsChangedListener); 211 resetProxyAppsState(); 212 updateNfwLocationAccessProxyAppsInGnssHal(); 213 } 214 return; 215 } 216 217 if (mProxyAppsState.isEmpty()) { 218 mPackageManager.addOnPermissionsChangeListener(mOnPermissionsChangedListener); 219 } else { 220 resetProxyAppsState(); 221 } 222 223 for (String proxyAppPkgName : nfwLocationAccessProxyApps) { 224 ProxyAppState proxyAppState = new ProxyAppState(shouldEnableLocationPermissionInGnssHal( 225 proxyAppPkgName)); 226 mProxyAppsState.put(proxyAppPkgName, proxyAppState); 227 } 228 229 updateNfwLocationAccessProxyAppsInGnssHal(); 230 } 231 resetProxyAppsState()232 private void resetProxyAppsState() { 233 // Clear location icons displayed. 234 for (Map.Entry<String, ProxyAppState> entry : mProxyAppsState.entrySet()) { 235 ProxyAppState proxyAppState = entry.getValue(); 236 if (!proxyAppState.mIsLocationIconOn) { 237 continue; 238 } 239 240 mHandler.removeCallbacksAndMessages(proxyAppState); 241 final ApplicationInfo proxyAppInfo = getProxyAppInfo(entry.getKey()); 242 if (proxyAppInfo != null) { 243 clearLocationIcon(proxyAppState, proxyAppInfo.uid, entry.getKey()); 244 } 245 } 246 mProxyAppsState.clear(); 247 } 248 isProxyAppListUpdated(List<String> nfwLocationAccessProxyApps)249 private boolean isProxyAppListUpdated(List<String> nfwLocationAccessProxyApps) { 250 if (nfwLocationAccessProxyApps.size() != mProxyAppsState.size()) { 251 return true; 252 } 253 254 for (String nfwLocationAccessProxyApp : nfwLocationAccessProxyApps) { 255 if (!mProxyAppsState.containsKey(nfwLocationAccessProxyApp)) { 256 return true; 257 } 258 } 259 return false; 260 } 261 handleGpsEnabledChanged(boolean isGpsEnabled)262 private void handleGpsEnabledChanged(boolean isGpsEnabled) { 263 if (DEBUG) { 264 Log.d(TAG, "handleGpsEnabledChanged, mIsGpsEnabled: " + mIsGpsEnabled 265 + ", isGpsEnabled: " + isGpsEnabled); 266 } 267 268 // The proxy app list in the GNSS HAL needs to be configured if it restarts after 269 // a crash. So, update HAL irrespective of the previous GPS enabled state. 270 mIsGpsEnabled = isGpsEnabled; 271 if (!mIsGpsEnabled) { 272 disableNfwLocationAccess(); 273 return; 274 } 275 276 setNfwLocationAccessProxyAppsInGnssHal(getLocationPermissionEnabledProxyApps()); 277 } 278 disableNfwLocationAccess()279 private void disableNfwLocationAccess() { 280 setNfwLocationAccessProxyAppsInGnssHal(NO_LOCATION_ENABLED_PROXY_APPS); 281 } 282 283 // Represents NfwNotification structure in IGnssVisibilityControlCallback.hal 284 private static class NfwNotification { 285 // These must match with NfwResponseType enum in IGnssVisibilityControlCallback.hal. 286 private static final byte NFW_RESPONSE_TYPE_REJECTED = 0; 287 private static final byte NFW_RESPONSE_TYPE_ACCEPTED_NO_LOCATION_PROVIDED = 1; 288 private static final byte NFW_RESPONSE_TYPE_ACCEPTED_LOCATION_PROVIDED = 2; 289 290 private final String mProxyAppPackageName; 291 private final byte mProtocolStack; 292 private final String mOtherProtocolStackName; 293 private final byte mRequestor; 294 private final String mRequestorId; 295 private final byte mResponseType; 296 private final boolean mInEmergencyMode; 297 private final boolean mIsCachedLocation; 298 NfwNotification(String proxyAppPackageName, byte protocolStack, String otherProtocolStackName, byte requestor, String requestorId, byte responseType, boolean inEmergencyMode, boolean isCachedLocation)299 private NfwNotification(String proxyAppPackageName, byte protocolStack, 300 String otherProtocolStackName, byte requestor, String requestorId, 301 byte responseType, boolean inEmergencyMode, boolean isCachedLocation) { 302 mProxyAppPackageName = proxyAppPackageName; 303 mProtocolStack = protocolStack; 304 mOtherProtocolStackName = otherProtocolStackName; 305 mRequestor = requestor; 306 mRequestorId = requestorId; 307 mResponseType = responseType; 308 mInEmergencyMode = inEmergencyMode; 309 mIsCachedLocation = isCachedLocation; 310 } 311 312 @SuppressLint("DefaultLocale") toString()313 public String toString() { 314 return String.format( 315 "{proxyAppPackageName: %s, protocolStack: %d, otherProtocolStackName: %s, " 316 + "requestor: %d, requestorId: %s, responseType: %s, inEmergencyMode:" 317 + " %b, isCachedLocation: %b}", 318 mProxyAppPackageName, mProtocolStack, mOtherProtocolStackName, mRequestor, 319 mRequestorId, getResponseTypeAsString(), mInEmergencyMode, mIsCachedLocation); 320 } 321 getResponseTypeAsString()322 private String getResponseTypeAsString() { 323 switch (mResponseType) { 324 case NFW_RESPONSE_TYPE_REJECTED: 325 return "REJECTED"; 326 case NFW_RESPONSE_TYPE_ACCEPTED_NO_LOCATION_PROVIDED: 327 return "ACCEPTED_NO_LOCATION_PROVIDED"; 328 case NFW_RESPONSE_TYPE_ACCEPTED_LOCATION_PROVIDED: 329 return "ACCEPTED_LOCATION_PROVIDED"; 330 default: 331 return "<Unknown>"; 332 } 333 } 334 isRequestAccepted()335 private boolean isRequestAccepted() { 336 return mResponseType != NfwNotification.NFW_RESPONSE_TYPE_REJECTED; 337 } 338 isLocationProvided()339 private boolean isLocationProvided() { 340 return mResponseType == NfwNotification.NFW_RESPONSE_TYPE_ACCEPTED_LOCATION_PROVIDED; 341 } 342 isRequestAttributedToProxyApp()343 private boolean isRequestAttributedToProxyApp() { 344 return !TextUtils.isEmpty(mProxyAppPackageName); 345 } 346 isEmergencyRequestNotification()347 private boolean isEmergencyRequestNotification() { 348 return mInEmergencyMode && !isRequestAttributedToProxyApp(); 349 } 350 } 351 handlePermissionsChanged(int uid)352 private void handlePermissionsChanged(int uid) { 353 if (mProxyAppsState.isEmpty()) { 354 return; 355 } 356 357 for (Map.Entry<String, ProxyAppState> entry : mProxyAppsState.entrySet()) { 358 final String proxyAppPkgName = entry.getKey(); 359 final ApplicationInfo proxyAppInfo = getProxyAppInfo(proxyAppPkgName); 360 if (proxyAppInfo == null || proxyAppInfo.uid != uid) { 361 continue; 362 } 363 364 final boolean isLocationPermissionEnabled = shouldEnableLocationPermissionInGnssHal( 365 proxyAppPkgName); 366 ProxyAppState proxyAppState = entry.getValue(); 367 if (isLocationPermissionEnabled != proxyAppState.mHasLocationPermission) { 368 Log.i(TAG, "Proxy app " + proxyAppPkgName + " location permission changed." 369 + " IsLocationPermissionEnabled: " + isLocationPermissionEnabled); 370 proxyAppState.mHasLocationPermission = isLocationPermissionEnabled; 371 updateNfwLocationAccessProxyAppsInGnssHal(); 372 } 373 return; 374 } 375 } 376 getProxyAppInfo(String proxyAppPkgName)377 private ApplicationInfo getProxyAppInfo(String proxyAppPkgName) { 378 try { 379 return mPackageManager.getApplicationInfo(proxyAppPkgName, 0); 380 } catch (PackageManager.NameNotFoundException e) { 381 if (DEBUG) Log.d(TAG, "Proxy app " + proxyAppPkgName + " is not found."); 382 return null; 383 } 384 } 385 shouldEnableLocationPermissionInGnssHal(String proxyAppPkgName)386 private boolean shouldEnableLocationPermissionInGnssHal(String proxyAppPkgName) { 387 return isProxyAppInstalled(proxyAppPkgName) && hasLocationPermission(proxyAppPkgName); 388 } 389 isProxyAppInstalled(String pkgName)390 private boolean isProxyAppInstalled(String pkgName) { 391 ApplicationInfo proxyAppInfo = getProxyAppInfo(pkgName); 392 return (proxyAppInfo != null) && proxyAppInfo.enabled; 393 } 394 hasLocationPermission(String pkgName)395 private boolean hasLocationPermission(String pkgName) { 396 return mPackageManager.checkPermission(LOCATION_PERMISSION_NAME, pkgName) 397 == PackageManager.PERMISSION_GRANTED; 398 } 399 updateNfwLocationAccessProxyAppsInGnssHal()400 private void updateNfwLocationAccessProxyAppsInGnssHal() { 401 if (!mIsGpsEnabled) { 402 return; // Keep non-framework location access disabled. 403 } 404 setNfwLocationAccessProxyAppsInGnssHal(getLocationPermissionEnabledProxyApps()); 405 } 406 setNfwLocationAccessProxyAppsInGnssHal( String[] locationPermissionEnabledProxyApps)407 private void setNfwLocationAccessProxyAppsInGnssHal( 408 String[] locationPermissionEnabledProxyApps) { 409 final String proxyAppsStr = Arrays.toString(locationPermissionEnabledProxyApps); 410 Log.i(TAG, "Updating non-framework location access proxy apps in the GNSS HAL to: " 411 + proxyAppsStr); 412 boolean result = native_enable_nfw_location_access(locationPermissionEnabledProxyApps); 413 if (!result) { 414 Log.e(TAG, "Failed to update non-framework location access proxy apps in the" 415 + " GNSS HAL to: " + proxyAppsStr); 416 } 417 } 418 getLocationPermissionEnabledProxyApps()419 private String[] getLocationPermissionEnabledProxyApps() { 420 // Get a count of proxy apps with location permission enabled for array creation size. 421 int countLocationPermissionEnabledProxyApps = 0; 422 for (ProxyAppState proxyAppState : mProxyAppsState.values()) { 423 if (proxyAppState.mHasLocationPermission) { 424 ++countLocationPermissionEnabledProxyApps; 425 } 426 } 427 428 int i = 0; 429 String[] locationPermissionEnabledProxyApps = 430 new String[countLocationPermissionEnabledProxyApps]; 431 for (Map.Entry<String, ProxyAppState> entry : mProxyAppsState.entrySet()) { 432 final String proxyApp = entry.getKey(); 433 if (entry.getValue().mHasLocationPermission) { 434 locationPermissionEnabledProxyApps[i++] = proxyApp; 435 } 436 } 437 return locationPermissionEnabledProxyApps; 438 } 439 hasLocationPermissionEnabledProxyApps()440 public boolean hasLocationPermissionEnabledProxyApps() { 441 return getLocationPermissionEnabledProxyApps().length > 0; 442 } 443 handleNfwNotification(NfwNotification nfwNotification)444 private void handleNfwNotification(NfwNotification nfwNotification) { 445 if (DEBUG) Log.d(TAG, "Non-framework location access notification: " + nfwNotification); 446 447 if (nfwNotification.isEmergencyRequestNotification()) { 448 handleEmergencyNfwNotification(nfwNotification); 449 return; 450 } 451 452 final String proxyAppPkgName = nfwNotification.mProxyAppPackageName; 453 final ProxyAppState proxyAppState = mProxyAppsState.get(proxyAppPkgName); 454 final boolean isLocationRequestAccepted = nfwNotification.isRequestAccepted(); 455 final boolean isPermissionMismatched = isPermissionMismatched(proxyAppState, 456 nfwNotification); 457 logEvent(nfwNotification, isPermissionMismatched); 458 459 if (!nfwNotification.isRequestAttributedToProxyApp()) { 460 // Handle cases where GNSS HAL implementation correctly rejected NFW location request. 461 // 1. GNSS HAL implementation doesn't provide location to any NFW location use cases. 462 // There is no Location Attribution App configured in the framework. 463 // 2. GNSS HAL implementation doesn't provide location to some NFW location use cases. 464 // Location Attribution Apps are configured only for the supported NFW location 465 // use cases. All other use cases which are not supported (and always rejected) by 466 // the GNSS HAL implementation will have proxyAppPackageName set to empty string. 467 if (!isLocationRequestAccepted) { 468 if (DEBUG) { 469 Log.d(TAG, "Non-framework location request rejected. ProxyAppPackageName field" 470 + " is not set in the notification: " + nfwNotification + ". Number of" 471 + " configured proxy apps: " + mProxyAppsState.size()); 472 } 473 return; 474 } 475 476 Log.e(TAG, "ProxyAppPackageName field is not set. AppOps service not notified" 477 + " for notification: " + nfwNotification); 478 return; 479 } 480 481 if (proxyAppState == null) { 482 Log.w(TAG, "Could not find proxy app " + proxyAppPkgName + " in the value specified for" 483 + " config parameter: " + GnssConfiguration.CONFIG_NFW_PROXY_APPS 484 + ". AppOps service not notified for notification: " + nfwNotification); 485 return; 486 } 487 488 // Display location icon attributed to this proxy app. 489 final ApplicationInfo proxyAppInfo = getProxyAppInfo(proxyAppPkgName); 490 if (proxyAppInfo == null) { 491 Log.e(TAG, "Proxy app " + proxyAppPkgName + " is not found. AppOps service not " 492 + "notified for notification: " + nfwNotification); 493 return; 494 } 495 496 if (nfwNotification.isLocationProvided()) { 497 showLocationIcon(proxyAppState, nfwNotification, proxyAppInfo.uid, proxyAppPkgName); 498 mAppOps.noteOpNoThrow(AppOpsManager.OP_FINE_LOCATION, proxyAppInfo.uid, 499 proxyAppPkgName); 500 } 501 502 // Log proxy app permission mismatch between framework and GNSS HAL. 503 if (isPermissionMismatched) { 504 Log.w(TAG, "Permission mismatch. Proxy app " + proxyAppPkgName 505 + " location permission is set to " + proxyAppState.mHasLocationPermission 506 + " and GNSS HAL enabled is set to " + mIsGpsEnabled 507 + " but GNSS non-framework location access response type is " 508 + nfwNotification.getResponseTypeAsString() + " for notification: " 509 + nfwNotification); 510 } 511 } 512 isPermissionMismatched(ProxyAppState proxyAppState, NfwNotification nfwNotification)513 private boolean isPermissionMismatched(ProxyAppState proxyAppState, 514 NfwNotification nfwNotification) { 515 // Non-framework non-emergency location requests must be accepted only when IGnss.hal 516 // is enabled and the proxy app has location permission. 517 final boolean isLocationRequestAccepted = nfwNotification.isRequestAccepted(); 518 return (proxyAppState == null || !mIsGpsEnabled) ? isLocationRequestAccepted 519 : (proxyAppState.mHasLocationPermission != isLocationRequestAccepted); 520 } 521 showLocationIcon(ProxyAppState proxyAppState, NfwNotification nfwNotification, int uid, String proxyAppPkgName)522 private void showLocationIcon(ProxyAppState proxyAppState, NfwNotification nfwNotification, 523 int uid, String proxyAppPkgName) { 524 // If we receive a new NfwNotification before the location icon is turned off for the 525 // previous notification, update the timer to extend the location icon display duration. 526 final boolean isLocationIconOn = proxyAppState.mIsLocationIconOn; 527 if (!isLocationIconOn) { 528 if (!updateLocationIcon(/* displayLocationIcon = */ true, uid, proxyAppPkgName)) { 529 Log.w(TAG, "Failed to show Location icon for notification: " + nfwNotification); 530 return; 531 } 532 proxyAppState.mIsLocationIconOn = true; 533 } else { 534 // Extend timer by canceling the current one and starting a new one. 535 mHandler.removeCallbacksAndMessages(proxyAppState); 536 } 537 538 // Start timer to turn off location icon. proxyAppState is used as a token to cancel timer. 539 if (DEBUG) { 540 Log.d(TAG, "Location icon on. " + (isLocationIconOn ? "Extending" : "Setting") 541 + " icon display timer. Uid: " + uid + ", proxyAppPkgName: " + proxyAppPkgName); 542 } 543 if (!mHandler.postDelayed(() -> handleLocationIconTimeout(proxyAppPkgName), 544 /* token = */ proxyAppState, LOCATION_ICON_DISPLAY_DURATION_MILLIS)) { 545 clearLocationIcon(proxyAppState, uid, proxyAppPkgName); 546 Log.w(TAG, "Failed to show location icon for the full duration for notification: " 547 + nfwNotification); 548 } 549 } 550 handleLocationIconTimeout(String proxyAppPkgName)551 private void handleLocationIconTimeout(String proxyAppPkgName) { 552 // Get uid again instead of using the one provided in startOp() call as the app could have 553 // been uninstalled and reinstalled during the timeout duration (unlikely in real world). 554 final ApplicationInfo proxyAppInfo = getProxyAppInfo(proxyAppPkgName); 555 if (proxyAppInfo != null) { 556 clearLocationIcon(mProxyAppsState.get(proxyAppPkgName), proxyAppInfo.uid, 557 proxyAppPkgName); 558 } 559 } 560 clearLocationIcon(@ullable ProxyAppState proxyAppState, int uid, String proxyAppPkgName)561 private void clearLocationIcon(@Nullable ProxyAppState proxyAppState, int uid, 562 String proxyAppPkgName) { 563 updateLocationIcon(/* displayLocationIcon = */ false, uid, proxyAppPkgName); 564 if (proxyAppState != null) proxyAppState.mIsLocationIconOn = false; 565 if (DEBUG) { 566 Log.d(TAG, "Location icon off. Uid: " + uid + ", proxyAppPkgName: " + proxyAppPkgName); 567 } 568 } 569 updateLocationIcon(boolean displayLocationIcon, int uid, String proxyAppPkgName)570 private boolean updateLocationIcon(boolean displayLocationIcon, int uid, 571 String proxyAppPkgName) { 572 if (displayLocationIcon) { 573 // Need two calls to startOp() here with different op code so that the proxy app shows 574 // up in the recent location requests page and also the location icon gets displayed. 575 if (mAppOps.startOpNoThrow(AppOpsManager.OP_MONITOR_LOCATION, uid, 576 proxyAppPkgName) != AppOpsManager.MODE_ALLOWED) { 577 return false; 578 } 579 if (mAppOps.startOpNoThrow(AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION, uid, 580 proxyAppPkgName) != AppOpsManager.MODE_ALLOWED) { 581 mAppOps.finishOp(AppOpsManager.OP_MONITOR_LOCATION, uid, proxyAppPkgName); 582 return false; 583 } 584 } else { 585 mAppOps.finishOp(AppOpsManager.OP_MONITOR_LOCATION, uid, proxyAppPkgName); 586 mAppOps.finishOp(AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION, uid, proxyAppPkgName); 587 } 588 return true; 589 } 590 handleEmergencyNfwNotification(NfwNotification nfwNotification)591 private void handleEmergencyNfwNotification(NfwNotification nfwNotification) { 592 boolean isPermissionMismatched = false; 593 if (!nfwNotification.isRequestAccepted()) { 594 Log.e(TAG, "Emergency non-framework location request incorrectly rejected." 595 + " Notification: " + nfwNotification); 596 isPermissionMismatched = true; 597 } 598 599 if (!mNiHandler.getInEmergency(EMERGENCY_EXTENSION_FOR_MISMATCH)) { 600 Log.w(TAG, "Emergency state mismatch. Device currently not in user initiated emergency" 601 + " session. Notification: " + nfwNotification); 602 isPermissionMismatched = true; 603 } 604 605 logEvent(nfwNotification, isPermissionMismatched); 606 607 if (nfwNotification.isLocationProvided()) { 608 postEmergencyLocationUserNotification(nfwNotification); 609 } 610 } 611 postEmergencyLocationUserNotification(NfwNotification nfwNotification)612 private void postEmergencyLocationUserNotification(NfwNotification nfwNotification) { 613 // Emulate deprecated IGnssNi.hal user notification of emergency NI requests. 614 NotificationManager notificationManager = (NotificationManager) mContext 615 .getSystemService(Context.NOTIFICATION_SERVICE); 616 if (notificationManager == null) { 617 Log.w(TAG, "Could not notify user of emergency location request. Notification: " 618 + nfwNotification); 619 return; 620 } 621 622 notificationManager.notifyAsUser(/* tag= */ null, /* notificationId= */ 0, 623 createEmergencyLocationUserNotification(mContext), UserHandle.ALL); 624 } 625 createEmergencyLocationUserNotification(Context context)626 private static Notification createEmergencyLocationUserNotification(Context context) { 627 // NOTE: Do not reuse the returned notification object as it will not reflect 628 // changes to notification text when the system language is changed. 629 final String firstLineText = context.getString(R.string.gpsNotifTitle); 630 final String secondLineText = context.getString(R.string.global_action_emergency); 631 final String accessibilityServicesText = firstLineText + " (" + secondLineText + ")"; 632 return new Notification.Builder(context, SystemNotificationChannels.NETWORK_STATUS) 633 .setSmallIcon(com.android.internal.R.drawable.stat_sys_gps_on) 634 .setWhen(0) 635 .setOngoing(false) 636 .setAutoCancel(true) 637 .setColor(context.getColor( 638 com.android.internal.R.color.system_notification_accent_color)) 639 .setDefaults(0) 640 .setTicker(accessibilityServicesText) 641 .setContentTitle(firstLineText) 642 .setContentText(secondLineText) 643 .build(); 644 } 645 logEvent(NfwNotification notification, boolean isPermissionMismatched)646 private void logEvent(NfwNotification notification, boolean isPermissionMismatched) { 647 FrameworkStatsLog.write(FrameworkStatsLog.GNSS_NFW_NOTIFICATION_REPORTED, 648 notification.mProxyAppPackageName, 649 notification.mProtocolStack, 650 notification.mOtherProtocolStackName, 651 notification.mRequestor, 652 notification.mRequestorId, 653 notification.mResponseType, 654 notification.mInEmergencyMode, 655 notification.mIsCachedLocation, 656 isPermissionMismatched); 657 } 658 runOnHandler(Runnable event)659 private void runOnHandler(Runnable event) { 660 // Hold a wake lock until this message is delivered. 661 // Note that this assumes the message will not be removed from the queue before 662 // it is handled (otherwise the wake lock would be leaked). 663 mWakeLock.acquire(WAKELOCK_TIMEOUT_MILLIS); 664 if (!mHandler.post(runEventAndReleaseWakeLock(event))) { 665 mWakeLock.release(); 666 } 667 } 668 runEventAndReleaseWakeLock(Runnable event)669 private Runnable runEventAndReleaseWakeLock(Runnable event) { 670 return () -> { 671 try { 672 event.run(); 673 } finally { 674 mWakeLock.release(); 675 } 676 }; 677 } 678 679 private native boolean native_enable_nfw_location_access(String[] proxyApps); 680 } 681