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