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 handleNfwNotification(NfwNotification nfwNotification)440 private void handleNfwNotification(NfwNotification nfwNotification) { 441 if (DEBUG) Log.d(TAG, "Non-framework location access notification: " + nfwNotification); 442 443 if (nfwNotification.isEmergencyRequestNotification()) { 444 handleEmergencyNfwNotification(nfwNotification); 445 return; 446 } 447 448 final String proxyAppPkgName = nfwNotification.mProxyAppPackageName; 449 final ProxyAppState proxyAppState = mProxyAppsState.get(proxyAppPkgName); 450 final boolean isLocationRequestAccepted = nfwNotification.isRequestAccepted(); 451 final boolean isPermissionMismatched = isPermissionMismatched(proxyAppState, 452 nfwNotification); 453 logEvent(nfwNotification, isPermissionMismatched); 454 455 if (!nfwNotification.isRequestAttributedToProxyApp()) { 456 // Handle cases where GNSS HAL implementation correctly rejected NFW location request. 457 // 1. GNSS HAL implementation doesn't provide location to any NFW location use cases. 458 // There is no Location Attribution App configured in the framework. 459 // 2. GNSS HAL implementation doesn't provide location to some NFW location use cases. 460 // Location Attribution Apps are configured only for the supported NFW location 461 // use cases. All other use cases which are not supported (and always rejected) by 462 // the GNSS HAL implementation will have proxyAppPackageName set to empty string. 463 if (!isLocationRequestAccepted) { 464 if (DEBUG) { 465 Log.d(TAG, "Non-framework location request rejected. ProxyAppPackageName field" 466 + " is not set in the notification: " + nfwNotification + ". Number of" 467 + " configured proxy apps: " + mProxyAppsState.size()); 468 } 469 return; 470 } 471 472 Log.e(TAG, "ProxyAppPackageName field is not set. AppOps service not notified" 473 + " for notification: " + nfwNotification); 474 return; 475 } 476 477 if (proxyAppState == null) { 478 Log.w(TAG, "Could not find proxy app " + proxyAppPkgName + " in the value specified for" 479 + " config parameter: " + GnssConfiguration.CONFIG_NFW_PROXY_APPS 480 + ". AppOps service not notified for notification: " + nfwNotification); 481 return; 482 } 483 484 // Display location icon attributed to this proxy app. 485 final ApplicationInfo proxyAppInfo = getProxyAppInfo(proxyAppPkgName); 486 if (proxyAppInfo == null) { 487 Log.e(TAG, "Proxy app " + proxyAppPkgName + " is not found. AppOps service not " 488 + "notified for notification: " + nfwNotification); 489 return; 490 } 491 492 if (nfwNotification.isLocationProvided()) { 493 showLocationIcon(proxyAppState, nfwNotification, proxyAppInfo.uid, proxyAppPkgName); 494 mAppOps.noteOpNoThrow(AppOpsManager.OP_FINE_LOCATION, proxyAppInfo.uid, 495 proxyAppPkgName); 496 } 497 498 // Log proxy app permission mismatch between framework and GNSS HAL. 499 if (isPermissionMismatched) { 500 Log.w(TAG, "Permission mismatch. Proxy app " + proxyAppPkgName 501 + " location permission is set to " + proxyAppState.mHasLocationPermission 502 + " and GNSS HAL enabled is set to " + mIsGpsEnabled 503 + " but GNSS non-framework location access response type is " 504 + nfwNotification.getResponseTypeAsString() + " for notification: " 505 + nfwNotification); 506 } 507 } 508 isPermissionMismatched(ProxyAppState proxyAppState, NfwNotification nfwNotification)509 private boolean isPermissionMismatched(ProxyAppState proxyAppState, 510 NfwNotification nfwNotification) { 511 // Non-framework non-emergency location requests must be accepted only when IGnss.hal 512 // is enabled and the proxy app has location permission. 513 final boolean isLocationRequestAccepted = nfwNotification.isRequestAccepted(); 514 return (proxyAppState == null || !mIsGpsEnabled) ? isLocationRequestAccepted 515 : (proxyAppState.mHasLocationPermission != isLocationRequestAccepted); 516 } 517 showLocationIcon(ProxyAppState proxyAppState, NfwNotification nfwNotification, int uid, String proxyAppPkgName)518 private void showLocationIcon(ProxyAppState proxyAppState, NfwNotification nfwNotification, 519 int uid, String proxyAppPkgName) { 520 // If we receive a new NfwNotification before the location icon is turned off for the 521 // previous notification, update the timer to extend the location icon display duration. 522 final boolean isLocationIconOn = proxyAppState.mIsLocationIconOn; 523 if (!isLocationIconOn) { 524 if (!updateLocationIcon(/* displayLocationIcon = */ true, uid, proxyAppPkgName)) { 525 Log.w(TAG, "Failed to show Location icon for notification: " + nfwNotification); 526 return; 527 } 528 proxyAppState.mIsLocationIconOn = true; 529 } else { 530 // Extend timer by canceling the current one and starting a new one. 531 mHandler.removeCallbacksAndMessages(proxyAppState); 532 } 533 534 // Start timer to turn off location icon. proxyAppState is used as a token to cancel timer. 535 if (DEBUG) { 536 Log.d(TAG, "Location icon on. " + (isLocationIconOn ? "Extending" : "Setting") 537 + " icon display timer. Uid: " + uid + ", proxyAppPkgName: " + proxyAppPkgName); 538 } 539 if (!mHandler.postDelayed(() -> handleLocationIconTimeout(proxyAppPkgName), 540 /* token = */ proxyAppState, LOCATION_ICON_DISPLAY_DURATION_MILLIS)) { 541 clearLocationIcon(proxyAppState, uid, proxyAppPkgName); 542 Log.w(TAG, "Failed to show location icon for the full duration for notification: " 543 + nfwNotification); 544 } 545 } 546 handleLocationIconTimeout(String proxyAppPkgName)547 private void handleLocationIconTimeout(String proxyAppPkgName) { 548 // Get uid again instead of using the one provided in startOp() call as the app could have 549 // been uninstalled and reinstalled during the timeout duration (unlikely in real world). 550 final ApplicationInfo proxyAppInfo = getProxyAppInfo(proxyAppPkgName); 551 if (proxyAppInfo != null) { 552 clearLocationIcon(mProxyAppsState.get(proxyAppPkgName), proxyAppInfo.uid, 553 proxyAppPkgName); 554 } 555 } 556 clearLocationIcon(@ullable ProxyAppState proxyAppState, int uid, String proxyAppPkgName)557 private void clearLocationIcon(@Nullable ProxyAppState proxyAppState, int uid, 558 String proxyAppPkgName) { 559 updateLocationIcon(/* displayLocationIcon = */ false, uid, proxyAppPkgName); 560 if (proxyAppState != null) proxyAppState.mIsLocationIconOn = false; 561 if (DEBUG) { 562 Log.d(TAG, "Location icon off. Uid: " + uid + ", proxyAppPkgName: " + proxyAppPkgName); 563 } 564 } 565 updateLocationIcon(boolean displayLocationIcon, int uid, String proxyAppPkgName)566 private boolean updateLocationIcon(boolean displayLocationIcon, int uid, 567 String proxyAppPkgName) { 568 if (displayLocationIcon) { 569 // Need two calls to startOp() here with different op code so that the proxy app shows 570 // up in the recent location requests page and also the location icon gets displayed. 571 if (mAppOps.startOpNoThrow(AppOpsManager.OP_MONITOR_LOCATION, uid, 572 proxyAppPkgName) != AppOpsManager.MODE_ALLOWED) { 573 return false; 574 } 575 if (mAppOps.startOpNoThrow(AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION, uid, 576 proxyAppPkgName) != AppOpsManager.MODE_ALLOWED) { 577 mAppOps.finishOp(AppOpsManager.OP_MONITOR_LOCATION, uid, proxyAppPkgName); 578 return false; 579 } 580 } else { 581 mAppOps.finishOp(AppOpsManager.OP_MONITOR_LOCATION, uid, proxyAppPkgName); 582 mAppOps.finishOp(AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION, uid, proxyAppPkgName); 583 } 584 return true; 585 } 586 handleEmergencyNfwNotification(NfwNotification nfwNotification)587 private void handleEmergencyNfwNotification(NfwNotification nfwNotification) { 588 boolean isPermissionMismatched = false; 589 if (!nfwNotification.isRequestAccepted()) { 590 Log.e(TAG, "Emergency non-framework location request incorrectly rejected." 591 + " Notification: " + nfwNotification); 592 isPermissionMismatched = true; 593 } 594 595 if (!mNiHandler.getInEmergency(EMERGENCY_EXTENSION_FOR_MISMATCH)) { 596 Log.w(TAG, "Emergency state mismatch. Device currently not in user initiated emergency" 597 + " session. Notification: " + nfwNotification); 598 isPermissionMismatched = true; 599 } 600 601 logEvent(nfwNotification, isPermissionMismatched); 602 603 if (nfwNotification.isLocationProvided()) { 604 postEmergencyLocationUserNotification(nfwNotification); 605 } 606 } 607 postEmergencyLocationUserNotification(NfwNotification nfwNotification)608 private void postEmergencyLocationUserNotification(NfwNotification nfwNotification) { 609 // Emulate deprecated IGnssNi.hal user notification of emergency NI requests. 610 NotificationManager notificationManager = (NotificationManager) mContext 611 .getSystemService(Context.NOTIFICATION_SERVICE); 612 if (notificationManager == null) { 613 Log.w(TAG, "Could not notify user of emergency location request. Notification: " 614 + nfwNotification); 615 return; 616 } 617 618 notificationManager.notifyAsUser(/* tag= */ null, /* notificationId= */ 0, 619 createEmergencyLocationUserNotification(mContext), UserHandle.ALL); 620 } 621 createEmergencyLocationUserNotification(Context context)622 private static Notification createEmergencyLocationUserNotification(Context context) { 623 // NOTE: Do not reuse the returned notification object as it will not reflect 624 // changes to notification text when the system language is changed. 625 final String firstLineText = context.getString(R.string.gpsNotifTitle); 626 final String secondLineText = context.getString(R.string.global_action_emergency); 627 final String accessibilityServicesText = firstLineText + " (" + secondLineText + ")"; 628 return new Notification.Builder(context, SystemNotificationChannels.NETWORK_STATUS) 629 .setSmallIcon(com.android.internal.R.drawable.stat_sys_gps_on) 630 .setWhen(0) 631 .setOngoing(false) 632 .setAutoCancel(true) 633 .setColor(context.getColor( 634 com.android.internal.R.color.system_notification_accent_color)) 635 .setDefaults(0) 636 .setTicker(accessibilityServicesText) 637 .setContentTitle(firstLineText) 638 .setContentText(secondLineText) 639 .build(); 640 } 641 logEvent(NfwNotification notification, boolean isPermissionMismatched)642 private void logEvent(NfwNotification notification, boolean isPermissionMismatched) { 643 FrameworkStatsLog.write(FrameworkStatsLog.GNSS_NFW_NOTIFICATION_REPORTED, 644 notification.mProxyAppPackageName, 645 notification.mProtocolStack, 646 notification.mOtherProtocolStackName, 647 notification.mRequestor, 648 notification.mRequestorId, 649 notification.mResponseType, 650 notification.mInEmergencyMode, 651 notification.mIsCachedLocation, 652 isPermissionMismatched); 653 } 654 runOnHandler(Runnable event)655 private void runOnHandler(Runnable event) { 656 // Hold a wake lock until this message is delivered. 657 // Note that this assumes the message will not be removed from the queue before 658 // it is handled (otherwise the wake lock would be leaked). 659 mWakeLock.acquire(WAKELOCK_TIMEOUT_MILLIS); 660 if (!mHandler.post(runEventAndReleaseWakeLock(event))) { 661 mWakeLock.release(); 662 } 663 } 664 runEventAndReleaseWakeLock(Runnable event)665 private Runnable runEventAndReleaseWakeLock(Runnable event) { 666 return () -> { 667 try { 668 event.run(); 669 } finally { 670 mWakeLock.release(); 671 } 672 }; 673 } 674 675 private native boolean native_enable_nfw_location_access(String[] proxyApps); 676 } 677