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