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