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