blob: 0f38473ef11ad4fadb7ef4dba6568cc833cace1b [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.location;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.app.AppOpsManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.location.LocationManager;
import android.os.Handler;
import android.os.Looper;
import android.os.PowerManager;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
import android.util.StatsLog;
import com.android.internal.R;
import com.android.internal.location.GpsNetInitiatedHandler;
import com.android.internal.notification.SystemNotificationChannels;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* Handles GNSS non-framework location access user visibility and control.
*
* The state of the GnssVisibilityControl object must be accessed/modified through the Handler
* thread only.
*/
class GnssVisibilityControl {
private static final String TAG = "GnssVisibilityControl";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private static final String LOCATION_PERMISSION_NAME =
"android.permission.ACCESS_FINE_LOCATION";
private static final String[] NO_LOCATION_ENABLED_PROXY_APPS = new String[0];
// Max wait time for synchronous method onGpsEnabledChanged() to run.
private static final long ON_GPS_ENABLED_CHANGED_TIMEOUT_MILLIS = 3 * 1000;
// How long to display location icon for each non-framework non-emergency location request.
private static final long LOCATION_ICON_DISPLAY_DURATION_MILLIS = 5 * 1000;
// Wakelocks
private static final String WAKELOCK_KEY = TAG;
private static final long WAKELOCK_TIMEOUT_MILLIS = 60 * 1000;
private final PowerManager.WakeLock mWakeLock;
private final AppOpsManager mAppOps;
private final PackageManager mPackageManager;
private final Handler mHandler;
private final Context mContext;
private final GpsNetInitiatedHandler mNiHandler;
private boolean mIsGpsEnabled;
private static final class ProxyAppState {
private boolean mHasLocationPermission;
private boolean mIsLocationIconOn;
private ProxyAppState(boolean hasLocationPermission) {
mHasLocationPermission = hasLocationPermission;
}
}
// Number of non-framework location access proxy apps is expected to be small (< 5).
private static final int ARRAY_MAP_INITIAL_CAPACITY_PROXY_APPS_STATE = 5;
private ArrayMap<String, ProxyAppState> mProxyAppsState = new ArrayMap<>(
ARRAY_MAP_INITIAL_CAPACITY_PROXY_APPS_STATE);
private PackageManager.OnPermissionsChangedListener mOnPermissionsChangedListener =
uid -> runOnHandler(() -> handlePermissionsChanged(uid));
GnssVisibilityControl(Context context, Looper looper, GpsNetInitiatedHandler niHandler) {
mContext = context;
PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_KEY);
mHandler = new Handler(looper);
mNiHandler = niHandler;
mAppOps = mContext.getSystemService(AppOpsManager.class);
mPackageManager = mContext.getPackageManager();
// Complete initialization as the first event to run in mHandler thread. After that,
// all object state read/update events run in the mHandler thread.
runOnHandler(this::handleInitialize);
}
void onGpsEnabledChanged(boolean isEnabled) {
// The GnssLocationProvider's methods: handleEnable() calls this method after native_init()
// and handleDisable() calls this method before native_cleanup(). This method must be
// executed synchronously so that the NFW location access permissions are disabled in
// the HAL before native_cleanup() method is called.
//
// NOTE: Since improper use of runWithScissors() method can result in deadlocks, the method
// doc recommends limiting its use to cases where some initialization steps need to be
// executed in sequence before continuing which fits this scenario.
if (mHandler.runWithScissors(() -> handleGpsEnabledChanged(isEnabled),
ON_GPS_ENABLED_CHANGED_TIMEOUT_MILLIS)) {
return;
}
// After timeout, the method remains posted in the queue and hence future enable/disable
// calls to this method will all get executed in the correct sequence. But this timeout
// situation should not even arise because runWithScissors() will run in the caller's
// thread without blocking as it is the same thread as mHandler's thread.
if (!isEnabled) {
Log.w(TAG, "Native call to disable non-framework location access in GNSS HAL may"
+ " get executed after native_cleanup().");
}
}
void reportNfwNotification(String proxyAppPackageName, byte protocolStack,
String otherProtocolStackName, byte requestor, String requestorId, byte responseType,
boolean inEmergencyMode, boolean isCachedLocation) {
runOnHandler(() -> handleNfwNotification(
new NfwNotification(proxyAppPackageName, protocolStack, otherProtocolStackName,
requestor, requestorId, responseType, inEmergencyMode, isCachedLocation)));
}
void onConfigurationUpdated(GnssConfiguration configuration) {
// The configuration object must be accessed only in the caller thread and not in mHandler.
List<String> nfwLocationAccessProxyApps = configuration.getProxyApps();
runOnHandler(() -> handleUpdateProxyApps(nfwLocationAccessProxyApps));
}
private void handleInitialize() {
listenForProxyAppsPackageUpdates();
}
private void listenForProxyAppsPackageUpdates() {
// Listen for proxy apps package installation, removal events.
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
intentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED);
intentFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
intentFilter.addDataScheme("package");
mContext.registerReceiverAsUser(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action == null) {
return;
}
switch (action) {
case Intent.ACTION_PACKAGE_ADDED:
case Intent.ACTION_PACKAGE_REMOVED:
case Intent.ACTION_PACKAGE_REPLACED:
case Intent.ACTION_PACKAGE_CHANGED:
String pkgName = intent.getData().getEncodedSchemeSpecificPart();
handleProxyAppPackageUpdate(pkgName, action);
break;
}
}
}, UserHandle.ALL, intentFilter, null, mHandler);
}
private void handleProxyAppPackageUpdate(String pkgName, String action) {
final ProxyAppState proxyAppState = mProxyAppsState.get(pkgName);
if (proxyAppState == null) {
return; // ignore, pkgName is not one of the proxy apps in our list.
}
if (DEBUG) Log.d(TAG, "Proxy app " + pkgName + " package changed: " + action);
final boolean updatedLocationPermission = shouldEnableLocationPermissionInGnssHal(pkgName);
if (proxyAppState.mHasLocationPermission != updatedLocationPermission) {
// Permission changed. So, update the GNSS HAL with the updated list.
Log.i(TAG, "Proxy app " + pkgName + " location permission changed."
+ " IsLocationPermissionEnabled: " + updatedLocationPermission);
proxyAppState.mHasLocationPermission = updatedLocationPermission;
updateNfwLocationAccessProxyAppsInGnssHal();
}
}
private void handleUpdateProxyApps(List<String> nfwLocationAccessProxyApps) {
if (!isProxyAppListUpdated(nfwLocationAccessProxyApps)) {
return;
}
if (nfwLocationAccessProxyApps.isEmpty()) {
// Stop listening for app permission changes. Clear the app list in GNSS HAL.
if (!mProxyAppsState.isEmpty()) {
mPackageManager.removeOnPermissionsChangeListener(mOnPermissionsChangedListener);
resetProxyAppsState();
updateNfwLocationAccessProxyAppsInGnssHal();
}
return;
}
if (mProxyAppsState.isEmpty()) {
mPackageManager.addOnPermissionsChangeListener(mOnPermissionsChangedListener);
} else {
resetProxyAppsState();
}
for (String proxyAppPkgName : nfwLocationAccessProxyApps) {
ProxyAppState proxyAppState = new ProxyAppState(shouldEnableLocationPermissionInGnssHal(
proxyAppPkgName));
mProxyAppsState.put(proxyAppPkgName, proxyAppState);
}
updateNfwLocationAccessProxyAppsInGnssHal();
}
private void resetProxyAppsState() {
// Clear location icons displayed.
for (Map.Entry<String, ProxyAppState> entry : mProxyAppsState.entrySet()) {
ProxyAppState proxyAppState = entry.getValue();
if (!proxyAppState.mIsLocationIconOn) {
continue;
}
mHandler.removeCallbacksAndMessages(proxyAppState);
final ApplicationInfo proxyAppInfo = getProxyAppInfo(entry.getKey());
if (proxyAppInfo != null) {
clearLocationIcon(proxyAppState, proxyAppInfo.uid, entry.getKey());
}
}
mProxyAppsState.clear();
}
private boolean isProxyAppListUpdated(List<String> nfwLocationAccessProxyApps) {
if (nfwLocationAccessProxyApps.size() != mProxyAppsState.size()) {
return true;
}
for (String nfwLocationAccessProxyApp : nfwLocationAccessProxyApps) {
if (!mProxyAppsState.containsKey(nfwLocationAccessProxyApp)) {
return true;
}
}
return false;
}
private void handleGpsEnabledChanged(boolean isGpsEnabled) {
if (DEBUG) {
Log.d(TAG, "handleGpsEnabledChanged, mIsGpsEnabled: " + mIsGpsEnabled
+ ", isGpsEnabled: " + isGpsEnabled);
}
// The proxy app list in the GNSS HAL needs to be configured if it restarts after
// a crash. So, update HAL irrespective of the previous GPS enabled state.
mIsGpsEnabled = isGpsEnabled;
if (!mIsGpsEnabled) {
disableNfwLocationAccess();
return;
}
setNfwLocationAccessProxyAppsInGnssHal(getLocationPermissionEnabledProxyApps());
}
private void disableNfwLocationAccess() {
setNfwLocationAccessProxyAppsInGnssHal(NO_LOCATION_ENABLED_PROXY_APPS);
}
// Represents NfwNotification structure in IGnssVisibilityControlCallback.hal
private static class NfwNotification {
// These must match with NfwResponseType enum in IGnssVisibilityControlCallback.hal.
private static final byte NFW_RESPONSE_TYPE_REJECTED = 0;
private static final byte NFW_RESPONSE_TYPE_ACCEPTED_NO_LOCATION_PROVIDED = 1;
private static final byte NFW_RESPONSE_TYPE_ACCEPTED_LOCATION_PROVIDED = 2;
private final String mProxyAppPackageName;
private final byte mProtocolStack;
private final String mOtherProtocolStackName;
private final byte mRequestor;
private final String mRequestorId;
private final byte mResponseType;
private final boolean mInEmergencyMode;
private final boolean mIsCachedLocation;
private NfwNotification(String proxyAppPackageName, byte protocolStack,
String otherProtocolStackName, byte requestor, String requestorId,
byte responseType, boolean inEmergencyMode, boolean isCachedLocation) {
mProxyAppPackageName = proxyAppPackageName;
mProtocolStack = protocolStack;
mOtherProtocolStackName = otherProtocolStackName;
mRequestor = requestor;
mRequestorId = requestorId;
mResponseType = responseType;
mInEmergencyMode = inEmergencyMode;
mIsCachedLocation = isCachedLocation;
}
@SuppressLint("DefaultLocale")
public String toString() {
return String.format(
"{proxyAppPackageName: %s, protocolStack: %d, otherProtocolStackName: %s, "
+ "requestor: %d, requestorId: %s, responseType: %s, inEmergencyMode:"
+ " %b, isCachedLocation: %b}",
mProxyAppPackageName, mProtocolStack, mOtherProtocolStackName, mRequestor,
mRequestorId, getResponseTypeAsString(), mInEmergencyMode, mIsCachedLocation);
}
private String getResponseTypeAsString() {
switch (mResponseType) {
case NFW_RESPONSE_TYPE_REJECTED:
return "REJECTED";
case NFW_RESPONSE_TYPE_ACCEPTED_NO_LOCATION_PROVIDED:
return "ACCEPTED_NO_LOCATION_PROVIDED";
case NFW_RESPONSE_TYPE_ACCEPTED_LOCATION_PROVIDED:
return "ACCEPTED_LOCATION_PROVIDED";
default:
return "<Unknown>";
}
}
private boolean isRequestAccepted() {
return mResponseType != NfwNotification.NFW_RESPONSE_TYPE_REJECTED;
}
private boolean isLocationProvided() {
return mResponseType == NfwNotification.NFW_RESPONSE_TYPE_ACCEPTED_LOCATION_PROVIDED;
}
private boolean isRequestAttributedToProxyApp() {
return !TextUtils.isEmpty(mProxyAppPackageName);
}
private boolean isEmergencyRequestNotification() {
return mInEmergencyMode && !isRequestAttributedToProxyApp();
}
}
private void handlePermissionsChanged(int uid) {
if (mProxyAppsState.isEmpty()) {
return;
}
for (Map.Entry<String, ProxyAppState> entry : mProxyAppsState.entrySet()) {
final String proxyAppPkgName = entry.getKey();
final ApplicationInfo proxyAppInfo = getProxyAppInfo(proxyAppPkgName);
if (proxyAppInfo == null || proxyAppInfo.uid != uid) {
continue;
}
final boolean isLocationPermissionEnabled = shouldEnableLocationPermissionInGnssHal(
proxyAppPkgName);
ProxyAppState proxyAppState = entry.getValue();
if (isLocationPermissionEnabled != proxyAppState.mHasLocationPermission) {
Log.i(TAG, "Proxy app " + proxyAppPkgName + " location permission changed."
+ " IsLocationPermissionEnabled: " + isLocationPermissionEnabled);
proxyAppState.mHasLocationPermission = isLocationPermissionEnabled;
updateNfwLocationAccessProxyAppsInGnssHal();
}
return;
}
}
private ApplicationInfo getProxyAppInfo(String proxyAppPkgName) {
try {
return mPackageManager.getApplicationInfo(proxyAppPkgName, 0);
} catch (PackageManager.NameNotFoundException e) {
if (DEBUG) Log.d(TAG, "Proxy app " + proxyAppPkgName + " is not found.");
return null;
}
}
private boolean shouldEnableLocationPermissionInGnssHal(String proxyAppPkgName) {
return isProxyAppInstalled(proxyAppPkgName) && hasLocationPermission(proxyAppPkgName);
}
private boolean isProxyAppInstalled(String pkgName) {
ApplicationInfo proxyAppInfo = getProxyAppInfo(pkgName);
return (proxyAppInfo != null) && proxyAppInfo.enabled;
}
private boolean hasLocationPermission(String pkgName) {
return mPackageManager.checkPermission(LOCATION_PERMISSION_NAME, pkgName)
== PackageManager.PERMISSION_GRANTED;
}
private void updateNfwLocationAccessProxyAppsInGnssHal() {
if (!mIsGpsEnabled) {
return; // Keep non-framework location access disabled.
}
setNfwLocationAccessProxyAppsInGnssHal(getLocationPermissionEnabledProxyApps());
}
private void setNfwLocationAccessProxyAppsInGnssHal(
String[] locationPermissionEnabledProxyApps) {
final String proxyAppsStr = Arrays.toString(locationPermissionEnabledProxyApps);
Log.i(TAG, "Updating non-framework location access proxy apps in the GNSS HAL to: "
+ proxyAppsStr);
boolean result = native_enable_nfw_location_access(locationPermissionEnabledProxyApps);
if (!result) {
Log.e(TAG, "Failed to update non-framework location access proxy apps in the"
+ " GNSS HAL to: " + proxyAppsStr);
}
}
private String[] getLocationPermissionEnabledProxyApps() {
// Get a count of proxy apps with location permission enabled for array creation size.
int countLocationPermissionEnabledProxyApps = 0;
for (ProxyAppState proxyAppState : mProxyAppsState.values()) {
if (proxyAppState.mHasLocationPermission) {
++countLocationPermissionEnabledProxyApps;
}
}
int i = 0;
String[] locationPermissionEnabledProxyApps =
new String[countLocationPermissionEnabledProxyApps];
for (Map.Entry<String, ProxyAppState> entry : mProxyAppsState.entrySet()) {
final String proxyApp = entry.getKey();
if (entry.getValue().mHasLocationPermission) {
locationPermissionEnabledProxyApps[i++] = proxyApp;
}
}
return locationPermissionEnabledProxyApps;
}
private void handleNfwNotification(NfwNotification nfwNotification) {
if (DEBUG) Log.d(TAG, "Non-framework location access notification: " + nfwNotification);
if (nfwNotification.isEmergencyRequestNotification()) {
handleEmergencyNfwNotification(nfwNotification);
return;
}
final String proxyAppPkgName = nfwNotification.mProxyAppPackageName;
final ProxyAppState proxyAppState = mProxyAppsState.get(proxyAppPkgName);
final boolean isLocationRequestAccepted = nfwNotification.isRequestAccepted();
final boolean isPermissionMismatched = isPermissionMismatched(proxyAppState,
nfwNotification);
logEvent(nfwNotification, isPermissionMismatched);
if (!nfwNotification.isRequestAttributedToProxyApp()) {
// Handle cases where GNSS HAL implementation correctly rejected NFW location request.
// 1. GNSS HAL implementation doesn't provide location to any NFW location use cases.
// There is no Location Attribution App configured in the framework.
// 2. GNSS HAL implementation doesn't provide location to some NFW location use cases.
// Location Attribution Apps are configured only for the supported NFW location
// use cases. All other use cases which are not supported (and always rejected) by
// the GNSS HAL implementation will have proxyAppPackageName set to empty string.
if (!isLocationRequestAccepted) {
if (DEBUG) {
Log.d(TAG, "Non-framework location request rejected. ProxyAppPackageName field"
+ " is not set in the notification: " + nfwNotification + ". Number of"
+ " configured proxy apps: " + mProxyAppsState.size());
}
return;
}
Log.e(TAG, "ProxyAppPackageName field is not set. AppOps service not notified"
+ " for notification: " + nfwNotification);
return;
}
if (proxyAppState == null) {
Log.w(TAG, "Could not find proxy app " + proxyAppPkgName + " in the value specified for"
+ " config parameter: " + GnssConfiguration.CONFIG_NFW_PROXY_APPS
+ ". AppOps service not notified for notification: " + nfwNotification);
return;
}
// Display location icon attributed to this proxy app.
final ApplicationInfo proxyAppInfo = getProxyAppInfo(proxyAppPkgName);
if (proxyAppInfo == null) {
Log.e(TAG, "Proxy app " + proxyAppPkgName + " is not found. AppOps service not "
+ "notified for notification: " + nfwNotification);
return;
}
if (nfwNotification.isLocationProvided()) {
showLocationIcon(proxyAppState, nfwNotification, proxyAppInfo.uid, proxyAppPkgName);
mAppOps.noteOpNoThrow(AppOpsManager.OP_FINE_LOCATION, proxyAppInfo.uid,
proxyAppPkgName);
}
// Log proxy app permission mismatch between framework and GNSS HAL.
if (isPermissionMismatched) {
Log.w(TAG, "Permission mismatch. Proxy app " + proxyAppPkgName
+ " location permission is set to " + proxyAppState.mHasLocationPermission
+ " and GNSS HAL enabled is set to " + mIsGpsEnabled
+ " but GNSS non-framework location access response type is "
+ nfwNotification.getResponseTypeAsString() + " for notification: "
+ nfwNotification);
}
}
private boolean isPermissionMismatched(ProxyAppState proxyAppState,
NfwNotification nfwNotification) {
// Non-framework non-emergency location requests must be accepted only when IGnss.hal
// is enabled and the proxy app has location permission.
final boolean isLocationRequestAccepted = nfwNotification.isRequestAccepted();
return (proxyAppState == null || !mIsGpsEnabled) ? isLocationRequestAccepted
: (proxyAppState.mHasLocationPermission != isLocationRequestAccepted);
}
private void showLocationIcon(ProxyAppState proxyAppState, NfwNotification nfwNotification,
int uid, String proxyAppPkgName) {
// If we receive a new NfwNotification before the location icon is turned off for the
// previous notification, update the timer to extend the location icon display duration.
final boolean isLocationIconOn = proxyAppState.mIsLocationIconOn;
if (!isLocationIconOn) {
if (!updateLocationIcon(/* displayLocationIcon = */ true, uid, proxyAppPkgName)) {
Log.w(TAG, "Failed to show Location icon for notification: " + nfwNotification);
return;
}
proxyAppState.mIsLocationIconOn = true;
} else {
// Extend timer by canceling the current one and starting a new one.
mHandler.removeCallbacksAndMessages(proxyAppState);
}
// Start timer to turn off location icon. proxyAppState is used as a token to cancel timer.
if (DEBUG) {
Log.d(TAG, "Location icon on. " + (isLocationIconOn ? "Extending" : "Setting")
+ " icon display timer. Uid: " + uid + ", proxyAppPkgName: " + proxyAppPkgName);
}
if (!mHandler.postDelayed(() -> handleLocationIconTimeout(proxyAppPkgName),
/* token = */ proxyAppState, LOCATION_ICON_DISPLAY_DURATION_MILLIS)) {
clearLocationIcon(proxyAppState, uid, proxyAppPkgName);
Log.w(TAG, "Failed to show location icon for the full duration for notification: "
+ nfwNotification);
}
}
private void handleLocationIconTimeout(String proxyAppPkgName) {
// Get uid again instead of using the one provided in startOp() call as the app could have
// been uninstalled and reinstalled during the timeout duration (unlikely in real world).
final ApplicationInfo proxyAppInfo = getProxyAppInfo(proxyAppPkgName);
if (proxyAppInfo != null) {
clearLocationIcon(mProxyAppsState.get(proxyAppPkgName), proxyAppInfo.uid,
proxyAppPkgName);
}
}
private void clearLocationIcon(@Nullable ProxyAppState proxyAppState, int uid,
String proxyAppPkgName) {
updateLocationIcon(/* displayLocationIcon = */ false, uid, proxyAppPkgName);
if (proxyAppState != null) proxyAppState.mIsLocationIconOn = false;
if (DEBUG) {
Log.d(TAG, "Location icon off. Uid: " + uid + ", proxyAppPkgName: " + proxyAppPkgName);
}
}
private boolean updateLocationIcon(boolean displayLocationIcon, int uid,
String proxyAppPkgName) {
if (displayLocationIcon) {
// Need two calls to startOp() here with different op code so that the proxy app shows
// up in the recent location requests page and also the location icon gets displayed.
if (mAppOps.startOpNoThrow(AppOpsManager.OP_MONITOR_LOCATION, uid,
proxyAppPkgName) != AppOpsManager.MODE_ALLOWED) {
return false;
}
if (mAppOps.startOpNoThrow(AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION, uid,
proxyAppPkgName) != AppOpsManager.MODE_ALLOWED) {
mAppOps.finishOp(AppOpsManager.OP_MONITOR_LOCATION, uid, proxyAppPkgName);
return false;
}
} else {
mAppOps.finishOp(AppOpsManager.OP_MONITOR_LOCATION, uid, proxyAppPkgName);
mAppOps.finishOp(AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION, uid, proxyAppPkgName);
}
sendHighPowerMonitoringBroadcast();
return true;
}
private void sendHighPowerMonitoringBroadcast() {
// Send an intent to notify that a high power request has been added/removed so that
// the SystemUi checks the state of AppOps and updates the location icon accordingly.
Intent intent = new Intent(LocationManager.HIGH_POWER_REQUEST_CHANGE_ACTION);
mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
}
private void handleEmergencyNfwNotification(NfwNotification nfwNotification) {
boolean isPermissionMismatched = false;
if (!nfwNotification.isRequestAccepted()) {
Log.e(TAG, "Emergency non-framework location request incorrectly rejected."
+ " Notification: " + nfwNotification);
isPermissionMismatched = true;
}
if (!mNiHandler.getInEmergency()) {
Log.w(TAG, "Emergency state mismatch. Device currently not in user initiated emergency"
+ " session. Notification: " + nfwNotification);
isPermissionMismatched = true;
}
logEvent(nfwNotification, isPermissionMismatched);
if (nfwNotification.isLocationProvided()) {
postEmergencyLocationUserNotification(nfwNotification);
}
}
private void postEmergencyLocationUserNotification(NfwNotification nfwNotification) {
// Emulate deprecated IGnssNi.hal user notification of emergency NI requests.
NotificationManager notificationManager = (NotificationManager) mContext
.getSystemService(Context.NOTIFICATION_SERVICE);
if (notificationManager == null) {
Log.w(TAG, "Could not notify user of emergency location request. Notification: "
+ nfwNotification);
return;
}
notificationManager.notifyAsUser(/* tag= */ null, /* notificationId= */ 0,
createEmergencyLocationUserNotification(mContext), UserHandle.ALL);
}
private static Notification createEmergencyLocationUserNotification(Context context) {
// NOTE: Do not reuse the returned notification object as it will not reflect
// changes to notification text when the system language is changed.
final String firstLineText = context.getString(R.string.gpsNotifTitle);
final String secondLineText = context.getString(R.string.global_action_emergency);
final String accessibilityServicesText = firstLineText + " (" + secondLineText + ")";
return new Notification.Builder(context, SystemNotificationChannels.NETWORK_ALERTS)
.setSmallIcon(com.android.internal.R.drawable.stat_sys_gps_on)
.setWhen(0)
.setOngoing(false)
.setAutoCancel(true)
.setColor(context.getColor(
com.android.internal.R.color.system_notification_accent_color))
.setDefaults(0)
.setTicker(accessibilityServicesText)
.setContentTitle(firstLineText)
.setContentText(secondLineText)
.build();
}
private void logEvent(NfwNotification notification, boolean isPermissionMismatched) {
StatsLog.write(StatsLog.GNSS_NFW_NOTIFICATION_REPORTED,
notification.mProxyAppPackageName,
notification.mProtocolStack,
notification.mOtherProtocolStackName,
notification.mRequestor,
notification.mRequestorId,
notification.mResponseType,
notification.mInEmergencyMode,
notification.mIsCachedLocation,
isPermissionMismatched);
}
private void runOnHandler(Runnable event) {
// Hold a wake lock until this message is delivered.
// Note that this assumes the message will not be removed from the queue before
// it is handled (otherwise the wake lock would be leaked).
mWakeLock.acquire(WAKELOCK_TIMEOUT_MILLIS);
if (!mHandler.post(runEventAndReleaseWakeLock(event))) {
mWakeLock.release();
}
}
private Runnable runEventAndReleaseWakeLock(Runnable event) {
return () -> {
try {
event.run();
} finally {
mWakeLock.release();
}
};
}
private native boolean native_enable_nfw_location_access(String[] proxyApps);
}