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.hardware.gnss.visibility_control.V1_0.IGnssVisibilityControlCallback; 31 import android.location.LocationManager; 32 import android.os.Handler; 33 import android.os.Looper; 34 import android.os.PowerManager; 35 import android.os.UserHandle; 36 import android.text.TextUtils; 37 import android.util.ArrayMap; 38 import android.util.Log; 39 40 import com.android.internal.R; 41 import com.android.internal.location.GpsNetInitiatedHandler; 42 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; 43 import com.android.internal.notification.SystemNotificationChannels; 44 import com.android.internal.util.FrameworkStatsLog; 45 46 import java.util.Arrays; 47 import java.util.List; 48 import java.util.Map; 49 import java.util.Objects; 50 51 /** 52 * Handles GNSS non-framework location access user visibility and control. 53 * 54 * The state of the GnssVisibilityControl object must be accessed/modified through the Handler 55 * thread only. 56 */ 57 class GnssVisibilityControl { 58 private static final String TAG = "GnssVisibilityControl"; 59 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 60 61 private static final String LOCATION_PERMISSION_NAME = 62 "android.permission.ACCESS_FINE_LOCATION"; 63 64 private static final String[] NO_LOCATION_ENABLED_PROXY_APPS = new String[0]; 65 66 // Max wait time for synchronous method onGpsEnabledChanged() to run. 67 private static final long ON_GPS_ENABLED_CHANGED_TIMEOUT_MILLIS = 3 * 1000; 68 69 // How long to display location icon for each non-framework non-emergency location request. 70 private static final long LOCATION_ICON_DISPLAY_DURATION_MILLIS = 5 * 1000; 71 72 // Wakelocks 73 private static final String WAKELOCK_KEY = TAG; 74 private static final long WAKELOCK_TIMEOUT_MILLIS = 60 * 1000; 75 private final PowerManager.WakeLock mWakeLock; 76 77 private final AppOpsManager mAppOps; 78 private final PackageManager mPackageManager; 79 80 private final Handler mHandler; 81 private final Context mContext; 82 private final GpsNetInitiatedHandler mNiHandler; 83 84 private boolean mIsGpsEnabled; 85 86 private static final class ProxyAppState { 87 private boolean mHasLocationPermission; 88 private boolean mIsLocationIconOn; 89 ProxyAppState(boolean hasLocationPermission)90 private ProxyAppState(boolean hasLocationPermission) { 91 mHasLocationPermission = hasLocationPermission; 92 } 93 } 94 95 // Number of non-framework location access proxy apps is expected to be small (< 5). 96 private static final int ARRAY_MAP_INITIAL_CAPACITY_PROXY_APPS_STATE = 5; 97 private ArrayMap<String, ProxyAppState> mProxyAppsState = new ArrayMap<>( 98 ARRAY_MAP_INITIAL_CAPACITY_PROXY_APPS_STATE); 99 100 private PackageManager.OnPermissionsChangedListener mOnPermissionsChangedListener = 101 uid -> runOnHandler(() -> handlePermissionsChanged(uid)); 102 GnssVisibilityControl(Context context, Looper looper, GpsNetInitiatedHandler niHandler)103 GnssVisibilityControl(Context context, Looper looper, GpsNetInitiatedHandler niHandler) { 104 mContext = context; 105 PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); 106 mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_KEY); 107 mHandler = new Handler(looper); 108 mNiHandler = niHandler; 109 mAppOps = mContext.getSystemService(AppOpsManager.class); 110 mPackageManager = mContext.getPackageManager(); 111 112 // Complete initialization as the first event to run in mHandler thread. After that, 113 // all object state read/update events run in the mHandler thread. 114 runOnHandler(this::handleInitialize); 115 } 116 onGpsEnabledChanged(boolean isEnabled)117 void onGpsEnabledChanged(boolean isEnabled) { 118 // The GnssLocationProvider's methods: handleEnable() calls this method after native_init() 119 // and handleDisable() calls this method before native_cleanup(). This method must be 120 // executed synchronously so that the NFW location access permissions are disabled in 121 // the HAL before native_cleanup() method is called. 122 // 123 // NOTE: Since improper use of runWithScissors() method can result in deadlocks, the method 124 // doc recommends limiting its use to cases where some initialization steps need to be 125 // executed in sequence before continuing which fits this scenario. 126 if (mHandler.runWithScissors(() -> handleGpsEnabledChanged(isEnabled), 127 ON_GPS_ENABLED_CHANGED_TIMEOUT_MILLIS)) { 128 return; 129 } 130 131 // After timeout, the method remains posted in the queue and hence future enable/disable 132 // calls to this method will all get executed in the correct sequence. But this timeout 133 // situation should not even arise because runWithScissors() will run in the caller's 134 // thread without blocking as it is the same thread as mHandler's thread. 135 if (!isEnabled) { 136 Log.w(TAG, "Native call to disable non-framework location access in GNSS HAL may" 137 + " get executed after native_cleanup()."); 138 } 139 } 140 reportNfwNotification(String proxyAppPackageName, byte protocolStack, String otherProtocolStackName, byte requestor, String requestorId, byte responseType, boolean inEmergencyMode, boolean isCachedLocation)141 void reportNfwNotification(String proxyAppPackageName, byte protocolStack, 142 String otherProtocolStackName, byte requestor, String requestorId, byte responseType, 143 boolean inEmergencyMode, boolean isCachedLocation) { 144 runOnHandler(() -> handleNfwNotification( 145 new NfwNotification(proxyAppPackageName, protocolStack, otherProtocolStackName, 146 requestor, requestorId, responseType, inEmergencyMode, isCachedLocation))); 147 } 148 onConfigurationUpdated(GnssConfiguration configuration)149 void onConfigurationUpdated(GnssConfiguration configuration) { 150 // The configuration object must be accessed only in the caller thread and not in mHandler. 151 List<String> nfwLocationAccessProxyApps = configuration.getProxyApps(); 152 runOnHandler(() -> handleUpdateProxyApps(nfwLocationAccessProxyApps)); 153 } 154 handleInitialize()155 private void handleInitialize() { 156 listenForProxyAppsPackageUpdates(); 157 } 158 listenForProxyAppsPackageUpdates()159 private void listenForProxyAppsPackageUpdates() { 160 // Listen for proxy apps package installation, removal events. 161 IntentFilter intentFilter = new IntentFilter(); 162 intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); 163 intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); 164 intentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED); 165 intentFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); 166 intentFilter.addDataScheme("package"); 167 mContext.registerReceiverAsUser(new BroadcastReceiver() { 168 @Override 169 public void onReceive(Context context, Intent intent) { 170 String action = intent.getAction(); 171 if (action == null) { 172 return; 173 } 174 175 switch (action) { 176 case Intent.ACTION_PACKAGE_ADDED: 177 case Intent.ACTION_PACKAGE_REMOVED: 178 case Intent.ACTION_PACKAGE_REPLACED: 179 case Intent.ACTION_PACKAGE_CHANGED: 180 String pkgName = intent.getData().getEncodedSchemeSpecificPart(); 181 handleProxyAppPackageUpdate(pkgName, action); 182 break; 183 } 184 } 185 }, UserHandle.ALL, intentFilter, null, mHandler); 186 } 187 handleProxyAppPackageUpdate(String pkgName, String action)188 private void handleProxyAppPackageUpdate(String pkgName, String action) { 189 final ProxyAppState proxyAppState = mProxyAppsState.get(pkgName); 190 if (proxyAppState == null) { 191 return; // ignore, pkgName is not one of the proxy apps in our list. 192 } 193 194 if (DEBUG) Log.d(TAG, "Proxy app " + pkgName + " package changed: " + action); 195 final boolean updatedLocationPermission = shouldEnableLocationPermissionInGnssHal(pkgName); 196 if (proxyAppState.mHasLocationPermission != updatedLocationPermission) { 197 // Permission changed. So, update the GNSS HAL with the updated list. 198 Log.i(TAG, "Proxy app " + pkgName + " location permission changed." 199 + " IsLocationPermissionEnabled: " + updatedLocationPermission); 200 proxyAppState.mHasLocationPermission = updatedLocationPermission; 201 updateNfwLocationAccessProxyAppsInGnssHal(); 202 } 203 } 204 handleUpdateProxyApps(List<String> nfwLocationAccessProxyApps)205 private void handleUpdateProxyApps(List<String> nfwLocationAccessProxyApps) { 206 if (!isProxyAppListUpdated(nfwLocationAccessProxyApps)) { 207 return; 208 } 209 210 if (nfwLocationAccessProxyApps.isEmpty()) { 211 // Stop listening for app permission changes. Clear the app list in GNSS HAL. 212 if (!mProxyAppsState.isEmpty()) { 213 mPackageManager.removeOnPermissionsChangeListener(mOnPermissionsChangedListener); 214 resetProxyAppsState(); 215 updateNfwLocationAccessProxyAppsInGnssHal(); 216 } 217 return; 218 } 219 220 if (mProxyAppsState.isEmpty()) { 221 mPackageManager.addOnPermissionsChangeListener(mOnPermissionsChangedListener); 222 } else { 223 resetProxyAppsState(); 224 } 225 226 for (String proxyAppPkgName : nfwLocationAccessProxyApps) { 227 ProxyAppState proxyAppState = new ProxyAppState(shouldEnableLocationPermissionInGnssHal( 228 proxyAppPkgName)); 229 mProxyAppsState.put(proxyAppPkgName, proxyAppState); 230 } 231 232 updateNfwLocationAccessProxyAppsInGnssHal(); 233 } 234 resetProxyAppsState()235 private void resetProxyAppsState() { 236 // Clear location icons displayed. 237 for (Map.Entry<String, ProxyAppState> entry : mProxyAppsState.entrySet()) { 238 ProxyAppState proxyAppState = entry.getValue(); 239 if (!proxyAppState.mIsLocationIconOn) { 240 continue; 241 } 242 243 mHandler.removeCallbacksAndMessages(proxyAppState); 244 final ApplicationInfo proxyAppInfo = getProxyAppInfo(entry.getKey()); 245 if (proxyAppInfo != null) { 246 clearLocationIcon(proxyAppState, proxyAppInfo.uid, entry.getKey()); 247 } 248 } 249 mProxyAppsState.clear(); 250 } 251 isProxyAppListUpdated(List<String> nfwLocationAccessProxyApps)252 private boolean isProxyAppListUpdated(List<String> nfwLocationAccessProxyApps) { 253 if (nfwLocationAccessProxyApps.size() != mProxyAppsState.size()) { 254 return true; 255 } 256 257 for (String nfwLocationAccessProxyApp : nfwLocationAccessProxyApps) { 258 if (!mProxyAppsState.containsKey(nfwLocationAccessProxyApp)) { 259 return true; 260 } 261 } 262 return false; 263 } 264 handleGpsEnabledChanged(boolean isGpsEnabled)265 private void handleGpsEnabledChanged(boolean isGpsEnabled) { 266 if (DEBUG) { 267 Log.d(TAG, "handleGpsEnabledChanged, mIsGpsEnabled: " + mIsGpsEnabled 268 + ", isGpsEnabled: " + isGpsEnabled); 269 } 270 271 // The proxy app list in the GNSS HAL needs to be configured if it restarts after 272 // a crash. So, update HAL irrespective of the previous GPS enabled state. 273 mIsGpsEnabled = isGpsEnabled; 274 if (!mIsGpsEnabled) { 275 disableNfwLocationAccess(); 276 return; 277 } 278 279 setNfwLocationAccessProxyAppsInGnssHal(getLocationPermissionEnabledProxyApps()); 280 } 281 disableNfwLocationAccess()282 private void disableNfwLocationAccess() { 283 setNfwLocationAccessProxyAppsInGnssHal(NO_LOCATION_ENABLED_PROXY_APPS); 284 } 285 286 // Represents NfwNotification structure in IGnssVisibilityControlCallback.hal 287 private static class NfwNotification { 288 289 // These must match with NfwResponseType enum in IGnssVisibilityControlCallback.hal 290 static final byte NFW_RESPONSE_TYPE_REJECTED = 0; 291 static final byte NFW_RESPONSE_TYPE_ACCEPTED_NO_LOCATION_PROVIDED = 1; 292 static final byte NFW_RESPONSE_TYPE_ACCEPTED_LOCATION_PROVIDED = 2; 293 294 final String mProxyAppPackageName; 295 final byte mProtocolStack; 296 final String mOtherProtocolStackName; 297 final byte mRequestor; 298 final String mRequestorId; 299 final byte mResponseType; 300 final boolean mInEmergencyMode; 301 final boolean mIsCachedLocation; 302 NfwNotification(String proxyAppPackageName, byte protocolStack, String otherProtocolStackName, byte requestor, String requestorId, byte responseType, boolean inEmergencyMode, boolean isCachedLocation)303 NfwNotification(String proxyAppPackageName, byte protocolStack, 304 String otherProtocolStackName, byte requestor, String requestorId, 305 byte responseType, boolean inEmergencyMode, boolean isCachedLocation) { 306 mProxyAppPackageName = proxyAppPackageName; 307 mProtocolStack = protocolStack; 308 mOtherProtocolStackName = otherProtocolStackName; 309 mRequestor = requestor; 310 mRequestorId = requestorId; 311 mResponseType = responseType; 312 mInEmergencyMode = inEmergencyMode; 313 mIsCachedLocation = isCachedLocation; 314 } 315 316 @SuppressLint("DefaultLocale") toString()317 public String toString() { 318 return String.format( 319 "{proxyAppPackageName: %s, protocolStack: %d, otherProtocolStackName: %s, " 320 + "requestor: %d, requestorId: %s, responseType: %s, inEmergencyMode:" 321 + " %b, isCachedLocation: %b}", 322 mProxyAppPackageName, mProtocolStack, mOtherProtocolStackName, mRequestor, 323 mRequestorId, getResponseTypeAsString(), mInEmergencyMode, mIsCachedLocation); 324 } 325 getResponseTypeAsString()326 private String getResponseTypeAsString() { 327 switch (mResponseType) { 328 case NFW_RESPONSE_TYPE_REJECTED: 329 return "REJECTED"; 330 case NFW_RESPONSE_TYPE_ACCEPTED_NO_LOCATION_PROVIDED: 331 return "ACCEPTED_NO_LOCATION_PROVIDED"; 332 case NFW_RESPONSE_TYPE_ACCEPTED_LOCATION_PROVIDED: 333 return "ACCEPTED_LOCATION_PROVIDED"; 334 default: 335 return "<Unknown>"; 336 } 337 } 338 isRequestAccepted()339 private boolean isRequestAccepted() { 340 return mResponseType != NfwNotification.NFW_RESPONSE_TYPE_REJECTED; 341 } 342 isLocationProvided()343 private boolean isLocationProvided() { 344 return mResponseType == NfwNotification.NFW_RESPONSE_TYPE_ACCEPTED_LOCATION_PROVIDED; 345 } 346 isRequestAttributedToProxyApp()347 private boolean isRequestAttributedToProxyApp() { 348 return !TextUtils.isEmpty(mProxyAppPackageName); 349 } 350 isEmergencyRequestNotification()351 private boolean isEmergencyRequestNotification() { 352 return mInEmergencyMode && !isRequestAttributedToProxyApp(); 353 } 354 } 355 handlePermissionsChanged(int uid)356 private void handlePermissionsChanged(int uid) { 357 if (mProxyAppsState.isEmpty()) { 358 return; 359 } 360 361 for (Map.Entry<String, ProxyAppState> entry : mProxyAppsState.entrySet()) { 362 final String proxyAppPkgName = entry.getKey(); 363 final ApplicationInfo proxyAppInfo = getProxyAppInfo(proxyAppPkgName); 364 if (proxyAppInfo == null || proxyAppInfo.uid != uid) { 365 continue; 366 } 367 368 final boolean isLocationPermissionEnabled = shouldEnableLocationPermissionInGnssHal( 369 proxyAppPkgName); 370 ProxyAppState proxyAppState = entry.getValue(); 371 if (isLocationPermissionEnabled != proxyAppState.mHasLocationPermission) { 372 Log.i(TAG, "Proxy app " + proxyAppPkgName + " location permission changed." 373 + " IsLocationPermissionEnabled: " + isLocationPermissionEnabled); 374 proxyAppState.mHasLocationPermission = isLocationPermissionEnabled; 375 updateNfwLocationAccessProxyAppsInGnssHal(); 376 } 377 return; 378 } 379 } 380 getProxyAppInfo(String proxyAppPkgName)381 private ApplicationInfo getProxyAppInfo(String proxyAppPkgName) { 382 try { 383 return mPackageManager.getApplicationInfo(proxyAppPkgName, 0); 384 } catch (PackageManager.NameNotFoundException e) { 385 if (DEBUG) Log.d(TAG, "Proxy app " + proxyAppPkgName + " is not found."); 386 return null; 387 } 388 } 389 shouldEnableLocationPermissionInGnssHal(String proxyAppPkgName)390 private boolean shouldEnableLocationPermissionInGnssHal(String proxyAppPkgName) { 391 return isProxyAppInstalled(proxyAppPkgName) && hasLocationPermission(proxyAppPkgName); 392 } 393 isProxyAppInstalled(String pkgName)394 private boolean isProxyAppInstalled(String pkgName) { 395 ApplicationInfo proxyAppInfo = getProxyAppInfo(pkgName); 396 return (proxyAppInfo != null) && proxyAppInfo.enabled; 397 } 398 hasLocationPermission(String pkgName)399 private boolean hasLocationPermission(String pkgName) { 400 return mPackageManager.checkPermission(LOCATION_PERMISSION_NAME, pkgName) 401 == PackageManager.PERMISSION_GRANTED; 402 } 403 updateNfwLocationAccessProxyAppsInGnssHal()404 private void updateNfwLocationAccessProxyAppsInGnssHal() { 405 if (!mIsGpsEnabled) { 406 return; // Keep non-framework location access disabled. 407 } 408 setNfwLocationAccessProxyAppsInGnssHal(getLocationPermissionEnabledProxyApps()); 409 } 410 setNfwLocationAccessProxyAppsInGnssHal( String[] locationPermissionEnabledProxyApps)411 private void setNfwLocationAccessProxyAppsInGnssHal( 412 String[] locationPermissionEnabledProxyApps) { 413 final String proxyAppsStr = Arrays.toString(locationPermissionEnabledProxyApps); 414 Log.i(TAG, "Updating non-framework location access proxy apps in the GNSS HAL to: " 415 + proxyAppsStr); 416 boolean result = native_enable_nfw_location_access(locationPermissionEnabledProxyApps); 417 if (!result) { 418 Log.e(TAG, "Failed to update non-framework location access proxy apps in the" 419 + " GNSS HAL to: " + proxyAppsStr); 420 } 421 } 422 getLocationPermissionEnabledProxyApps()423 private String[] getLocationPermissionEnabledProxyApps() { 424 // Get a count of proxy apps with location permission enabled for array creation size. 425 int countLocationPermissionEnabledProxyApps = 0; 426 for (ProxyAppState proxyAppState : mProxyAppsState.values()) { 427 if (proxyAppState.mHasLocationPermission) { 428 ++countLocationPermissionEnabledProxyApps; 429 } 430 } 431 432 int i = 0; 433 String[] locationPermissionEnabledProxyApps = 434 new String[countLocationPermissionEnabledProxyApps]; 435 for (Map.Entry<String, ProxyAppState> entry : mProxyAppsState.entrySet()) { 436 final String proxyApp = entry.getKey(); 437 if (entry.getValue().mHasLocationPermission) { 438 locationPermissionEnabledProxyApps[i++] = proxyApp; 439 } 440 } 441 return locationPermissionEnabledProxyApps; 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 sendHighPowerMonitoringBroadcast(); 589 return true; 590 } 591 sendHighPowerMonitoringBroadcast()592 private void sendHighPowerMonitoringBroadcast() { 593 // Send an intent to notify that a high power request has been added/removed so that 594 // the SystemUi checks the state of AppOps and updates the location icon accordingly. 595 Intent intent = new Intent(LocationManager.HIGH_POWER_REQUEST_CHANGE_ACTION); 596 mContext.sendBroadcastAsUser(intent, UserHandle.ALL); 597 } 598 handleEmergencyNfwNotification(NfwNotification nfwNotification)599 private void handleEmergencyNfwNotification(NfwNotification nfwNotification) { 600 boolean isPermissionMismatched = false; 601 if (!nfwNotification.isRequestAccepted()) { 602 Log.e(TAG, "Emergency non-framework location request incorrectly rejected." 603 + " Notification: " + nfwNotification); 604 isPermissionMismatched = true; 605 } 606 607 if (!mNiHandler.getInEmergency()) { 608 Log.w(TAG, "Emergency state mismatch. Device currently not in user initiated emergency" 609 + " session. Notification: " + nfwNotification); 610 isPermissionMismatched = true; 611 } 612 613 logEvent(nfwNotification, isPermissionMismatched); 614 615 if (nfwNotification.isLocationProvided()) { 616 displayNfwNotification(nfwNotification); 617 } 618 } 619 displayNfwNotification(NfwNotification nfwNotification)620 private void displayNfwNotification(NfwNotification nfwNotification) { 621 NotificationManager notificationManager = Objects.requireNonNull( 622 mContext.getSystemService(NotificationManager.class)); 623 624 String title = mContext.getString(R.string.gnss_nfw_notification_title); 625 String message; 626 if (nfwNotification.mRequestor == IGnssVisibilityControlCallback.NfwRequestor.CARRIER) { 627 message = mContext.getString(R.string.gnss_nfw_notification_message_carrier); 628 } else { 629 message = mContext.getString(R.string.gnss_nfw_notification_message_oem); 630 } 631 632 Notification.Builder builder = new Notification.Builder(mContext, 633 SystemNotificationChannels.NETWORK_STATUS) 634 .setSmallIcon(R.drawable.stat_sys_gps_on) 635 .setCategory(Notification.CATEGORY_SYSTEM) 636 .setVisibility(Notification.VISIBILITY_SECRET) 637 .setContentTitle(title) 638 .setTicker(title) 639 .setContentText(message) 640 .setStyle(new Notification.BigTextStyle().bigText(message)) 641 .setAutoCancel(true) 642 .setColor(mContext.getColor(R.color.system_notification_accent_color)) 643 .setWhen(System.currentTimeMillis()) 644 .setShowWhen(true) 645 .setDefaults(0); 646 647 notificationManager.notify(SystemMessage.NOTE_GNSS_NFW_LOCATION_ACCESS, builder.build()); 648 } 649 logEvent(NfwNotification notification, boolean isPermissionMismatched)650 private void logEvent(NfwNotification notification, boolean isPermissionMismatched) { 651 FrameworkStatsLog.write(FrameworkStatsLog.GNSS_NFW_NOTIFICATION_REPORTED, 652 notification.mProxyAppPackageName, 653 notification.mProtocolStack, 654 notification.mOtherProtocolStackName, 655 notification.mRequestor, 656 notification.mRequestorId, 657 notification.mResponseType, 658 notification.mInEmergencyMode, 659 notification.mIsCachedLocation, 660 isPermissionMismatched); 661 } 662 runOnHandler(Runnable event)663 private void runOnHandler(Runnable event) { 664 // Hold a wake lock until this message is delivered. 665 // Note that this assumes the message will not be removed from the queue before 666 // it is handled (otherwise the wake lock would be leaked). 667 mWakeLock.acquire(WAKELOCK_TIMEOUT_MILLIS); 668 if (!mHandler.post(runEventAndReleaseWakeLock(event))) { 669 mWakeLock.release(); 670 } 671 } 672 runEventAndReleaseWakeLock(Runnable event)673 private Runnable runEventAndReleaseWakeLock(Runnable event) { 674 return () -> { 675 try { 676 event.run(); 677 } finally { 678 mWakeLock.release(); 679 } 680 }; 681 } 682 683 private native boolean native_enable_nfw_location_access(String[] proxyApps); 684 } 685