• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
handleNfwNotification(NfwNotification nfwNotification)440     private void handleNfwNotification(NfwNotification nfwNotification) {
441         if (DEBUG) Log.d(TAG, "Non-framework location access notification: " + nfwNotification);
442 
443         if (nfwNotification.isEmergencyRequestNotification()) {
444             handleEmergencyNfwNotification(nfwNotification);
445             return;
446         }
447 
448         final String proxyAppPkgName = nfwNotification.mProxyAppPackageName;
449         final ProxyAppState proxyAppState = mProxyAppsState.get(proxyAppPkgName);
450         final boolean isLocationRequestAccepted = nfwNotification.isRequestAccepted();
451         final boolean isPermissionMismatched = isPermissionMismatched(proxyAppState,
452                 nfwNotification);
453         logEvent(nfwNotification, isPermissionMismatched);
454 
455         if (!nfwNotification.isRequestAttributedToProxyApp()) {
456             // Handle cases where GNSS HAL implementation correctly rejected NFW location request.
457             // 1. GNSS HAL implementation doesn't provide location to any NFW location use cases.
458             //    There is no Location Attribution App configured in the framework.
459             // 2. GNSS HAL implementation doesn't provide location to some NFW location use cases.
460             //    Location Attribution Apps are configured only for the supported NFW location
461             //    use cases. All other use cases which are not supported (and always rejected) by
462             //    the GNSS HAL implementation will have proxyAppPackageName set to empty string.
463             if (!isLocationRequestAccepted) {
464                 if (DEBUG) {
465                     Log.d(TAG, "Non-framework location request rejected. ProxyAppPackageName field"
466                             + " is not set in the notification: " + nfwNotification + ". Number of"
467                             + " configured proxy apps: " + mProxyAppsState.size());
468                 }
469                 return;
470             }
471 
472             Log.e(TAG, "ProxyAppPackageName field is not set. AppOps service not notified"
473                     + " for notification: " + nfwNotification);
474             return;
475         }
476 
477         if (proxyAppState == null) {
478             Log.w(TAG, "Could not find proxy app " + proxyAppPkgName + " in the value specified for"
479                     + " config parameter: " + GnssConfiguration.CONFIG_NFW_PROXY_APPS
480                     + ". AppOps service not notified for notification: " + nfwNotification);
481             return;
482         }
483 
484         // Display location icon attributed to this proxy app.
485         final ApplicationInfo proxyAppInfo = getProxyAppInfo(proxyAppPkgName);
486         if (proxyAppInfo == null) {
487             Log.e(TAG, "Proxy app " + proxyAppPkgName + " is not found. AppOps service not "
488                     + "notified for notification: " + nfwNotification);
489             return;
490         }
491 
492         if (nfwNotification.isLocationProvided()) {
493             showLocationIcon(proxyAppState, nfwNotification, proxyAppInfo.uid, proxyAppPkgName);
494             mAppOps.noteOpNoThrow(AppOpsManager.OP_FINE_LOCATION, proxyAppInfo.uid,
495                     proxyAppPkgName);
496         }
497 
498         // Log proxy app permission mismatch between framework and GNSS HAL.
499         if (isPermissionMismatched) {
500             Log.w(TAG, "Permission mismatch. Proxy app " + proxyAppPkgName
501                     + " location permission is set to " + proxyAppState.mHasLocationPermission
502                     + " and GNSS HAL enabled is set to " + mIsGpsEnabled
503                     + " but GNSS non-framework location access response type is "
504                     + nfwNotification.getResponseTypeAsString() + " for notification: "
505                     + nfwNotification);
506         }
507     }
508 
isPermissionMismatched(ProxyAppState proxyAppState, NfwNotification nfwNotification)509     private boolean isPermissionMismatched(ProxyAppState proxyAppState,
510             NfwNotification nfwNotification) {
511         // Non-framework non-emergency location requests must be accepted only when IGnss.hal
512         // is enabled and the proxy app has location permission.
513         final boolean isLocationRequestAccepted = nfwNotification.isRequestAccepted();
514         return (proxyAppState == null || !mIsGpsEnabled) ? isLocationRequestAccepted
515                 : (proxyAppState.mHasLocationPermission != isLocationRequestAccepted);
516     }
517 
showLocationIcon(ProxyAppState proxyAppState, NfwNotification nfwNotification, int uid, String proxyAppPkgName)518     private void showLocationIcon(ProxyAppState proxyAppState, NfwNotification nfwNotification,
519             int uid, String proxyAppPkgName) {
520         // If we receive a new NfwNotification before the location icon is turned off for the
521         // previous notification, update the timer to extend the location icon display duration.
522         final boolean isLocationIconOn = proxyAppState.mIsLocationIconOn;
523         if (!isLocationIconOn) {
524             if (!updateLocationIcon(/* displayLocationIcon = */ true, uid, proxyAppPkgName)) {
525                 Log.w(TAG, "Failed to show Location icon for notification: " + nfwNotification);
526                 return;
527             }
528             proxyAppState.mIsLocationIconOn = true;
529         } else {
530             // Extend timer by canceling the current one and starting a new one.
531             mHandler.removeCallbacksAndMessages(proxyAppState);
532         }
533 
534         // Start timer to turn off location icon. proxyAppState is used as a token to cancel timer.
535         if (DEBUG) {
536             Log.d(TAG, "Location icon on. " + (isLocationIconOn ? "Extending" : "Setting")
537                     + " icon display timer. Uid: " + uid + ", proxyAppPkgName: " + proxyAppPkgName);
538         }
539         if (!mHandler.postDelayed(() -> handleLocationIconTimeout(proxyAppPkgName),
540                 /* token = */ proxyAppState, LOCATION_ICON_DISPLAY_DURATION_MILLIS)) {
541             clearLocationIcon(proxyAppState, uid, proxyAppPkgName);
542             Log.w(TAG, "Failed to show location icon for the full duration for notification: "
543                     + nfwNotification);
544         }
545     }
546 
handleLocationIconTimeout(String proxyAppPkgName)547     private void handleLocationIconTimeout(String proxyAppPkgName) {
548         // Get uid again instead of using the one provided in startOp() call as the app could have
549         // been uninstalled and reinstalled during the timeout duration (unlikely in real world).
550         final ApplicationInfo proxyAppInfo = getProxyAppInfo(proxyAppPkgName);
551         if (proxyAppInfo != null) {
552             clearLocationIcon(mProxyAppsState.get(proxyAppPkgName), proxyAppInfo.uid,
553                     proxyAppPkgName);
554         }
555     }
556 
clearLocationIcon(@ullable ProxyAppState proxyAppState, int uid, String proxyAppPkgName)557     private void clearLocationIcon(@Nullable ProxyAppState proxyAppState, int uid,
558             String proxyAppPkgName) {
559         updateLocationIcon(/* displayLocationIcon = */ false, uid, proxyAppPkgName);
560         if (proxyAppState != null) proxyAppState.mIsLocationIconOn = false;
561         if (DEBUG) {
562             Log.d(TAG, "Location icon off. Uid: " + uid + ", proxyAppPkgName: " + proxyAppPkgName);
563         }
564     }
565 
updateLocationIcon(boolean displayLocationIcon, int uid, String proxyAppPkgName)566     private boolean updateLocationIcon(boolean displayLocationIcon, int uid,
567             String proxyAppPkgName) {
568         if (displayLocationIcon) {
569             // Need two calls to startOp() here with different op code so that the proxy app shows
570             // up in the recent location requests page and also the location icon gets displayed.
571             if (mAppOps.startOpNoThrow(AppOpsManager.OP_MONITOR_LOCATION, uid,
572                     proxyAppPkgName) != AppOpsManager.MODE_ALLOWED) {
573                 return false;
574             }
575             if (mAppOps.startOpNoThrow(AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION, uid,
576                     proxyAppPkgName) != AppOpsManager.MODE_ALLOWED) {
577                 mAppOps.finishOp(AppOpsManager.OP_MONITOR_LOCATION, uid, proxyAppPkgName);
578                 return false;
579             }
580         } else {
581             mAppOps.finishOp(AppOpsManager.OP_MONITOR_LOCATION, uid, proxyAppPkgName);
582             mAppOps.finishOp(AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION, uid, proxyAppPkgName);
583         }
584         return true;
585     }
586 
handleEmergencyNfwNotification(NfwNotification nfwNotification)587     private void handleEmergencyNfwNotification(NfwNotification nfwNotification) {
588         boolean isPermissionMismatched = false;
589         if (!nfwNotification.isRequestAccepted()) {
590             Log.e(TAG, "Emergency non-framework location request incorrectly rejected."
591                     + " Notification: " + nfwNotification);
592             isPermissionMismatched = true;
593         }
594 
595         if (!mNiHandler.getInEmergency(EMERGENCY_EXTENSION_FOR_MISMATCH)) {
596             Log.w(TAG, "Emergency state mismatch. Device currently not in user initiated emergency"
597                     + " session. Notification: " + nfwNotification);
598             isPermissionMismatched = true;
599         }
600 
601         logEvent(nfwNotification, isPermissionMismatched);
602 
603         if (nfwNotification.isLocationProvided()) {
604             postEmergencyLocationUserNotification(nfwNotification);
605         }
606     }
607 
postEmergencyLocationUserNotification(NfwNotification nfwNotification)608     private void postEmergencyLocationUserNotification(NfwNotification nfwNotification) {
609         // Emulate deprecated IGnssNi.hal user notification of emergency NI requests.
610         NotificationManager notificationManager = (NotificationManager) mContext
611                 .getSystemService(Context.NOTIFICATION_SERVICE);
612         if (notificationManager == null) {
613             Log.w(TAG, "Could not notify user of emergency location request. Notification: "
614                     + nfwNotification);
615             return;
616         }
617 
618         notificationManager.notifyAsUser(/* tag= */ null, /* notificationId= */ 0,
619                 createEmergencyLocationUserNotification(mContext), UserHandle.ALL);
620     }
621 
createEmergencyLocationUserNotification(Context context)622     private static Notification createEmergencyLocationUserNotification(Context context) {
623         // NOTE: Do not reuse the returned notification object as it will not reflect
624         //       changes to notification text when the system language is changed.
625         final String firstLineText = context.getString(R.string.gpsNotifTitle);
626         final String secondLineText = context.getString(R.string.global_action_emergency);
627         final String accessibilityServicesText = firstLineText + " (" + secondLineText + ")";
628         return new Notification.Builder(context, SystemNotificationChannels.NETWORK_STATUS)
629                 .setSmallIcon(com.android.internal.R.drawable.stat_sys_gps_on)
630                 .setWhen(0)
631                 .setOngoing(false)
632                 .setAutoCancel(true)
633                 .setColor(context.getColor(
634                         com.android.internal.R.color.system_notification_accent_color))
635                 .setDefaults(0)
636                 .setTicker(accessibilityServicesText)
637                 .setContentTitle(firstLineText)
638                 .setContentText(secondLineText)
639                 .build();
640     }
641 
logEvent(NfwNotification notification, boolean isPermissionMismatched)642     private void logEvent(NfwNotification notification, boolean isPermissionMismatched) {
643         FrameworkStatsLog.write(FrameworkStatsLog.GNSS_NFW_NOTIFICATION_REPORTED,
644                 notification.mProxyAppPackageName,
645                 notification.mProtocolStack,
646                 notification.mOtherProtocolStackName,
647                 notification.mRequestor,
648                 notification.mRequestorId,
649                 notification.mResponseType,
650                 notification.mInEmergencyMode,
651                 notification.mIsCachedLocation,
652                 isPermissionMismatched);
653     }
654 
runOnHandler(Runnable event)655     private void runOnHandler(Runnable event) {
656         // Hold a wake lock until this message is delivered.
657         // Note that this assumes the message will not be removed from the queue before
658         // it is handled (otherwise the wake lock would be leaked).
659         mWakeLock.acquire(WAKELOCK_TIMEOUT_MILLIS);
660         if (!mHandler.post(runEventAndReleaseWakeLock(event))) {
661             mWakeLock.release();
662         }
663     }
664 
runEventAndReleaseWakeLock(Runnable event)665     private Runnable runEventAndReleaseWakeLock(Runnable event) {
666         return () -> {
667             try {
668                 event.run();
669             } finally {
670                 mWakeLock.release();
671             }
672         };
673     }
674 
675     private native boolean native_enable_nfw_location_access(String[] proxyApps);
676 }
677