blob: 2b5fc7989d8b98b93bbb89509570db4b8b2566e3 [file] [log] [blame]
Anil Admal94ec76a2019-01-15 09:42:01 -08001/*
2 * Copyright (C) 2019 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.location;
18
Anil Admal74de73e2019-05-14 19:14:23 -070019import android.annotation.Nullable;
Anil Admal94ec76a2019-01-15 09:42:01 -080020import android.annotation.SuppressLint;
21import android.app.AppOpsManager;
Anil Admal61da5f02019-05-17 09:04:13 -070022import android.app.Notification;
23import android.app.NotificationManager;
24import android.app.PendingIntent;
Anil Admal204e22e2019-01-22 15:14:17 -080025import android.content.BroadcastReceiver;
Anil Admal94ec76a2019-01-15 09:42:01 -080026import android.content.Context;
27import android.content.Intent;
Anil Admal204e22e2019-01-22 15:14:17 -080028import android.content.IntentFilter;
Anil Admal4b6a8302019-05-02 18:38:39 -070029import android.content.pm.ApplicationInfo;
Anil Admal94ec76a2019-01-15 09:42:01 -080030import android.content.pm.PackageManager;
Anil Admal74de73e2019-05-14 19:14:23 -070031import android.location.LocationManager;
Anil Admal94ec76a2019-01-15 09:42:01 -080032import android.os.Handler;
33import android.os.Looper;
34import android.os.PowerManager;
35import android.os.UserHandle;
36import android.text.TextUtils;
Anil Admalc25a2712019-02-15 16:48:48 -080037import android.util.ArrayMap;
Anil Admal94ec76a2019-01-15 09:42:01 -080038import android.util.Log;
39
Anil Admal8a246a22019-05-05 00:34:55 -070040import com.android.internal.R;
41import com.android.internal.location.GpsNetInitiatedHandler;
Anil Admal61da5f02019-05-17 09:04:13 -070042import com.android.internal.notification.SystemNotificationChannels;
Muhammad Qureshi6f207102020-01-28 10:37:41 -080043import com.android.internal.util.FrameworkStatsLog;
Anil Admal8a246a22019-05-05 00:34:55 -070044
Anil Admal94ec76a2019-01-15 09:42:01 -080045import java.util.Arrays;
Anil Admal94ec76a2019-01-15 09:42:01 -080046import java.util.List;
47import java.util.Map;
48
49/**
50 * Handles GNSS non-framework location access user visibility and control.
51 *
52 * The state of the GnssVisibilityControl object must be accessed/modified through the Handler
53 * thread only.
54 */
55class GnssVisibilityControl {
56 private static final String TAG = "GnssVisibilityControl";
57 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
58
Anil Admal94ec76a2019-01-15 09:42:01 -080059 private static final String LOCATION_PERMISSION_NAME =
60 "android.permission.ACCESS_FINE_LOCATION";
61
Anil Admal204e22e2019-01-22 15:14:17 -080062 private static final String[] NO_LOCATION_ENABLED_PROXY_APPS = new String[0];
63
Anil Admal138cdc32019-04-16 10:07:43 -070064 // Max wait time for synchronous method onGpsEnabledChanged() to run.
65 private static final long ON_GPS_ENABLED_CHANGED_TIMEOUT_MILLIS = 3 * 1000;
66
Anil Admal74de73e2019-05-14 19:14:23 -070067 // How long to display location icon for each non-framework non-emergency location request.
68 private static final long LOCATION_ICON_DISPLAY_DURATION_MILLIS = 5 * 1000;
69
Anil Admal94ec76a2019-01-15 09:42:01 -080070 // Wakelocks
71 private static final String WAKELOCK_KEY = TAG;
72 private static final long WAKELOCK_TIMEOUT_MILLIS = 60 * 1000;
73 private final PowerManager.WakeLock mWakeLock;
74
75 private final AppOpsManager mAppOps;
76 private final PackageManager mPackageManager;
77
78 private final Handler mHandler;
79 private final Context mContext;
Anil Admal8a246a22019-05-05 00:34:55 -070080 private final GpsNetInitiatedHandler mNiHandler;
Anil Admal94ec76a2019-01-15 09:42:01 -080081
Anil Admal138cdc32019-04-16 10:07:43 -070082 private boolean mIsGpsEnabled;
Anil Admal204e22e2019-01-22 15:14:17 -080083
Anil Admal74de73e2019-05-14 19:14:23 -070084 private static final class ProxyAppState {
85 private boolean mHasLocationPermission;
86 private boolean mIsLocationIconOn;
87
88 private ProxyAppState(boolean hasLocationPermission) {
89 mHasLocationPermission = hasLocationPermission;
90 }
91 }
92
Anil Admal94ec76a2019-01-15 09:42:01 -080093 // Number of non-framework location access proxy apps is expected to be small (< 5).
Anil Admal74de73e2019-05-14 19:14:23 -070094 private static final int ARRAY_MAP_INITIAL_CAPACITY_PROXY_APPS_STATE = 5;
95 private ArrayMap<String, ProxyAppState> mProxyAppsState = new ArrayMap<>(
96 ARRAY_MAP_INITIAL_CAPACITY_PROXY_APPS_STATE);
Anil Admal94ec76a2019-01-15 09:42:01 -080097
98 private PackageManager.OnPermissionsChangedListener mOnPermissionsChangedListener =
Anil Admal204e22e2019-01-22 15:14:17 -080099 uid -> runOnHandler(() -> handlePermissionsChanged(uid));
Anil Admal94ec76a2019-01-15 09:42:01 -0800100
Anil Admal8a246a22019-05-05 00:34:55 -0700101 GnssVisibilityControl(Context context, Looper looper, GpsNetInitiatedHandler niHandler) {
Anil Admal94ec76a2019-01-15 09:42:01 -0800102 mContext = context;
103 PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
104 mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_KEY);
105 mHandler = new Handler(looper);
Anil Admal8a246a22019-05-05 00:34:55 -0700106 mNiHandler = niHandler;
Anil Admal94ec76a2019-01-15 09:42:01 -0800107 mAppOps = mContext.getSystemService(AppOpsManager.class);
108 mPackageManager = mContext.getPackageManager();
Anil Admal204e22e2019-01-22 15:14:17 -0800109
Anil Admal316f9482019-02-12 18:57:18 -0800110 // Complete initialization as the first event to run in mHandler thread. After that,
111 // all object state read/update events run in the mHandler thread.
112 runOnHandler(this::handleInitialize);
Anil Admal94ec76a2019-01-15 09:42:01 -0800113 }
114
Anil Admal138cdc32019-04-16 10:07:43 -0700115 void onGpsEnabledChanged(boolean isEnabled) {
116 // The GnssLocationProvider's methods: handleEnable() calls this method after native_init()
117 // and handleDisable() calls this method before native_cleanup(). This method must be
118 // executed synchronously so that the NFW location access permissions are disabled in
119 // the HAL before native_cleanup() method is called.
120 //
121 // NOTE: Since improper use of runWithScissors() method can result in deadlocks, the method
122 // doc recommends limiting its use to cases where some initialization steps need to be
123 // executed in sequence before continuing which fits this scenario.
124 if (mHandler.runWithScissors(() -> handleGpsEnabledChanged(isEnabled),
125 ON_GPS_ENABLED_CHANGED_TIMEOUT_MILLIS)) {
126 return;
127 }
128
129 // After timeout, the method remains posted in the queue and hence future enable/disable
130 // calls to this method will all get executed in the correct sequence. But this timeout
131 // situation should not even arise because runWithScissors() will run in the caller's
132 // thread without blocking as it is the same thread as mHandler's thread.
133 if (!isEnabled) {
134 Log.w(TAG, "Native call to disable non-framework location access in GNSS HAL may"
135 + " get executed after native_cleanup().");
136 }
137 }
138
Anil Admal94ec76a2019-01-15 09:42:01 -0800139 void reportNfwNotification(String proxyAppPackageName, byte protocolStack,
140 String otherProtocolStackName, byte requestor, String requestorId, byte responseType,
141 boolean inEmergencyMode, boolean isCachedLocation) {
Anil Admal204e22e2019-01-22 15:14:17 -0800142 runOnHandler(() -> handleNfwNotification(
Anil Admal94ec76a2019-01-15 09:42:01 -0800143 new NfwNotification(proxyAppPackageName, protocolStack, otherProtocolStackName,
144 requestor, requestorId, responseType, inEmergencyMode, isCachedLocation)));
145 }
146
Anil Admale1539e82019-05-09 15:05:04 -0700147 void onConfigurationUpdated(GnssConfiguration configuration) {
148 // The configuration object must be accessed only in the caller thread and not in mHandler.
149 List<String> nfwLocationAccessProxyApps = configuration.getProxyApps();
Anil Admal61da5f02019-05-17 09:04:13 -0700150 runOnHandler(() -> handleUpdateProxyApps(nfwLocationAccessProxyApps));
Anil Admalbefd4832019-05-09 19:58:13 -0700151 }
152
Anil Admal316f9482019-02-12 18:57:18 -0800153 private void handleInitialize() {
Anil Admal316f9482019-02-12 18:57:18 -0800154 listenForProxyAppsPackageUpdates();
Anil Admal316f9482019-02-12 18:57:18 -0800155 }
156
Anil Admal204e22e2019-01-22 15:14:17 -0800157 private void listenForProxyAppsPackageUpdates() {
Anil Admal316f9482019-02-12 18:57:18 -0800158 // Listen for proxy apps package installation, removal events.
Anil Admal204e22e2019-01-22 15:14:17 -0800159 IntentFilter intentFilter = new IntentFilter();
160 intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
161 intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
162 intentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED);
Anil Admal4b6a8302019-05-02 18:38:39 -0700163 intentFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
Anil Admal204e22e2019-01-22 15:14:17 -0800164 intentFilter.addDataScheme("package");
165 mContext.registerReceiverAsUser(new BroadcastReceiver() {
166 @Override
167 public void onReceive(Context context, Intent intent) {
168 String action = intent.getAction();
169 if (action == null) {
170 return;
171 }
172
173 switch (action) {
174 case Intent.ACTION_PACKAGE_ADDED:
175 case Intent.ACTION_PACKAGE_REMOVED:
176 case Intent.ACTION_PACKAGE_REPLACED:
Anil Admal4b6a8302019-05-02 18:38:39 -0700177 case Intent.ACTION_PACKAGE_CHANGED:
Anil Admal204e22e2019-01-22 15:14:17 -0800178 String pkgName = intent.getData().getEncodedSchemeSpecificPart();
179 handleProxyAppPackageUpdate(pkgName, action);
180 break;
181 }
182 }
183 }, UserHandle.ALL, intentFilter, null, mHandler);
184 }
185
186 private void handleProxyAppPackageUpdate(String pkgName, String action) {
Anil Admal74de73e2019-05-14 19:14:23 -0700187 final ProxyAppState proxyAppState = mProxyAppsState.get(pkgName);
188 if (proxyAppState == null) {
Anil Admal316f9482019-02-12 18:57:18 -0800189 return; // ignore, pkgName is not one of the proxy apps in our list.
Anil Admal204e22e2019-01-22 15:14:17 -0800190 }
191
Anil Admal4b6a8302019-05-02 18:38:39 -0700192 if (DEBUG) Log.d(TAG, "Proxy app " + pkgName + " package changed: " + action);
193 final boolean updatedLocationPermission = shouldEnableLocationPermissionInGnssHal(pkgName);
Anil Admal74de73e2019-05-14 19:14:23 -0700194 if (proxyAppState.mHasLocationPermission != updatedLocationPermission) {
Anil Admal204e22e2019-01-22 15:14:17 -0800195 // Permission changed. So, update the GNSS HAL with the updated list.
Anil Admal4b6a8302019-05-02 18:38:39 -0700196 Log.i(TAG, "Proxy app " + pkgName + " location permission changed."
197 + " IsLocationPermissionEnabled: " + updatedLocationPermission);
Anil Admal74de73e2019-05-14 19:14:23 -0700198 proxyAppState.mHasLocationPermission = updatedLocationPermission;
Anil Admal204e22e2019-01-22 15:14:17 -0800199 updateNfwLocationAccessProxyAppsInGnssHal();
200 }
201 }
202
203 private void handleUpdateProxyApps(List<String> nfwLocationAccessProxyApps) {
204 if (!isProxyAppListUpdated(nfwLocationAccessProxyApps)) {
205 return;
206 }
207
Anil Admal94ec76a2019-01-15 09:42:01 -0800208 if (nfwLocationAccessProxyApps.isEmpty()) {
209 // Stop listening for app permission changes. Clear the app list in GNSS HAL.
Anil Admal74de73e2019-05-14 19:14:23 -0700210 if (!mProxyAppsState.isEmpty()) {
Anil Admal94ec76a2019-01-15 09:42:01 -0800211 mPackageManager.removeOnPermissionsChangeListener(mOnPermissionsChangedListener);
Anil Admal74de73e2019-05-14 19:14:23 -0700212 resetProxyAppsState();
Anil Admal94ec76a2019-01-15 09:42:01 -0800213 updateNfwLocationAccessProxyAppsInGnssHal();
214 }
215 return;
216 }
217
Anil Admal74de73e2019-05-14 19:14:23 -0700218 if (mProxyAppsState.isEmpty()) {
Anil Admal94ec76a2019-01-15 09:42:01 -0800219 mPackageManager.addOnPermissionsChangeListener(mOnPermissionsChangedListener);
220 } else {
Anil Admal74de73e2019-05-14 19:14:23 -0700221 resetProxyAppsState();
Anil Admal94ec76a2019-01-15 09:42:01 -0800222 }
223
Anil Admal4b6a8302019-05-02 18:38:39 -0700224 for (String proxyAppPkgName : nfwLocationAccessProxyApps) {
Anil Admal74de73e2019-05-14 19:14:23 -0700225 ProxyAppState proxyAppState = new ProxyAppState(shouldEnableLocationPermissionInGnssHal(
226 proxyAppPkgName));
227 mProxyAppsState.put(proxyAppPkgName, proxyAppState);
Anil Admal94ec76a2019-01-15 09:42:01 -0800228 }
229
230 updateNfwLocationAccessProxyAppsInGnssHal();
231 }
232
Anil Admal74de73e2019-05-14 19:14:23 -0700233 private void resetProxyAppsState() {
234 // Clear location icons displayed.
235 for (Map.Entry<String, ProxyAppState> entry : mProxyAppsState.entrySet()) {
236 ProxyAppState proxyAppState = entry.getValue();
237 if (!proxyAppState.mIsLocationIconOn) {
238 continue;
239 }
240
241 mHandler.removeCallbacksAndMessages(proxyAppState);
242 final ApplicationInfo proxyAppInfo = getProxyAppInfo(entry.getKey());
243 if (proxyAppInfo != null) {
244 clearLocationIcon(proxyAppState, proxyAppInfo.uid, entry.getKey());
245 }
246 }
247 mProxyAppsState.clear();
248 }
249
Anil Admal204e22e2019-01-22 15:14:17 -0800250 private boolean isProxyAppListUpdated(List<String> nfwLocationAccessProxyApps) {
Anil Admal74de73e2019-05-14 19:14:23 -0700251 if (nfwLocationAccessProxyApps.size() != mProxyAppsState.size()) {
Anil Admal204e22e2019-01-22 15:14:17 -0800252 return true;
253 }
254
255 for (String nfwLocationAccessProxyApp : nfwLocationAccessProxyApps) {
Anil Admal74de73e2019-05-14 19:14:23 -0700256 if (!mProxyAppsState.containsKey(nfwLocationAccessProxyApp)) {
Anil Admal204e22e2019-01-22 15:14:17 -0800257 return true;
258 }
259 }
Anil Admal204e22e2019-01-22 15:14:17 -0800260 return false;
261 }
262
Anil Admal8b83dc12019-06-12 13:55:42 -0700263 private void handleGpsEnabledChanged(boolean isGpsEnabled) {
264 if (DEBUG) {
265 Log.d(TAG, "handleGpsEnabledChanged, mIsGpsEnabled: " + mIsGpsEnabled
266 + ", isGpsEnabled: " + isGpsEnabled);
Anil Admal316f9482019-02-12 18:57:18 -0800267 }
268
Anil Admal8b83dc12019-06-12 13:55:42 -0700269 // The proxy app list in the GNSS HAL needs to be configured if it restarts after
270 // a crash. So, update HAL irrespective of the previous GPS enabled state.
271 mIsGpsEnabled = isGpsEnabled;
Anil Admal138cdc32019-04-16 10:07:43 -0700272 if (!mIsGpsEnabled) {
Anil Admal316f9482019-02-12 18:57:18 -0800273 disableNfwLocationAccess();
274 return;
275 }
276
Anil Admal8b83dc12019-06-12 13:55:42 -0700277 setNfwLocationAccessProxyAppsInGnssHal(getLocationPermissionEnabledProxyApps());
Anil Admal316f9482019-02-12 18:57:18 -0800278 }
279
280 private void disableNfwLocationAccess() {
281 setNfwLocationAccessProxyAppsInGnssHal(NO_LOCATION_ENABLED_PROXY_APPS);
Anil Admal204e22e2019-01-22 15:14:17 -0800282 }
283
Anil Admal94ec76a2019-01-15 09:42:01 -0800284 // Represents NfwNotification structure in IGnssVisibilityControlCallback.hal
285 private static class NfwNotification {
Anil Admalc25a2712019-02-15 16:48:48 -0800286 // These must match with NfwResponseType enum in IGnssVisibilityControlCallback.hal.
Anil Admal94ec76a2019-01-15 09:42:01 -0800287 private static final byte NFW_RESPONSE_TYPE_REJECTED = 0;
288 private static final byte NFW_RESPONSE_TYPE_ACCEPTED_NO_LOCATION_PROVIDED = 1;
289 private static final byte NFW_RESPONSE_TYPE_ACCEPTED_LOCATION_PROVIDED = 2;
290
291 private final String mProxyAppPackageName;
292 private final byte mProtocolStack;
293 private final String mOtherProtocolStackName;
294 private final byte mRequestor;
295 private final String mRequestorId;
296 private final byte mResponseType;
297 private final boolean mInEmergencyMode;
298 private final boolean mIsCachedLocation;
299
Anil Admal204e22e2019-01-22 15:14:17 -0800300 private NfwNotification(String proxyAppPackageName, byte protocolStack,
Anil Admal94ec76a2019-01-15 09:42:01 -0800301 String otherProtocolStackName, byte requestor, String requestorId,
302 byte responseType, boolean inEmergencyMode, boolean isCachedLocation) {
303 mProxyAppPackageName = proxyAppPackageName;
304 mProtocolStack = protocolStack;
305 mOtherProtocolStackName = otherProtocolStackName;
306 mRequestor = requestor;
307 mRequestorId = requestorId;
308 mResponseType = responseType;
309 mInEmergencyMode = inEmergencyMode;
310 mIsCachedLocation = isCachedLocation;
311 }
312
Anil Admal94ec76a2019-01-15 09:42:01 -0800313 @SuppressLint("DefaultLocale")
314 public String toString() {
315 return String.format(
Anil Admalc25a2712019-02-15 16:48:48 -0800316 "{proxyAppPackageName: %s, protocolStack: %d, otherProtocolStackName: %s, "
317 + "requestor: %d, requestorId: %s, responseType: %s, inEmergencyMode:"
318 + " %b, isCachedLocation: %b}",
319 mProxyAppPackageName, mProtocolStack, mOtherProtocolStackName, mRequestor,
320 mRequestorId, getResponseTypeAsString(), mInEmergencyMode, mIsCachedLocation);
Anil Admal94ec76a2019-01-15 09:42:01 -0800321 }
322
Anil Admal204e22e2019-01-22 15:14:17 -0800323 private String getResponseTypeAsString() {
Anil Admal94ec76a2019-01-15 09:42:01 -0800324 switch (mResponseType) {
325 case NFW_RESPONSE_TYPE_REJECTED:
326 return "REJECTED";
327 case NFW_RESPONSE_TYPE_ACCEPTED_NO_LOCATION_PROVIDED:
328 return "ACCEPTED_NO_LOCATION_PROVIDED";
329 case NFW_RESPONSE_TYPE_ACCEPTED_LOCATION_PROVIDED:
330 return "ACCEPTED_LOCATION_PROVIDED";
331 default:
332 return "<Unknown>";
333 }
334 }
Anil Admalfd9bb8e2019-04-17 18:08:46 -0700335
336 private boolean isRequestAccepted() {
337 return mResponseType != NfwNotification.NFW_RESPONSE_TYPE_REJECTED;
338 }
339
Anil Admal8a246a22019-05-05 00:34:55 -0700340 private boolean isLocationProvided() {
341 return mResponseType == NfwNotification.NFW_RESPONSE_TYPE_ACCEPTED_LOCATION_PROVIDED;
342 }
343
Anil Admalfd9bb8e2019-04-17 18:08:46 -0700344 private boolean isRequestAttributedToProxyApp() {
345 return !TextUtils.isEmpty(mProxyAppPackageName);
346 }
347
348 private boolean isEmergencyRequestNotification() {
349 return mInEmergencyMode && !isRequestAttributedToProxyApp();
350 }
Anil Admal94ec76a2019-01-15 09:42:01 -0800351 }
352
353 private void handlePermissionsChanged(int uid) {
Anil Admal74de73e2019-05-14 19:14:23 -0700354 if (mProxyAppsState.isEmpty()) {
Anil Admal94ec76a2019-01-15 09:42:01 -0800355 return;
356 }
357
Anil Admal74de73e2019-05-14 19:14:23 -0700358 for (Map.Entry<String, ProxyAppState> entry : mProxyAppsState.entrySet()) {
Anil Admal4b6a8302019-05-02 18:38:39 -0700359 final String proxyAppPkgName = entry.getKey();
360 final ApplicationInfo proxyAppInfo = getProxyAppInfo(proxyAppPkgName);
361 if (proxyAppInfo == null || proxyAppInfo.uid != uid) {
Anil Admal94ec76a2019-01-15 09:42:01 -0800362 continue;
363 }
364
Anil Admal4b6a8302019-05-02 18:38:39 -0700365 final boolean isLocationPermissionEnabled = shouldEnableLocationPermissionInGnssHal(
366 proxyAppPkgName);
Anil Admal74de73e2019-05-14 19:14:23 -0700367 ProxyAppState proxyAppState = entry.getValue();
368 if (isLocationPermissionEnabled != proxyAppState.mHasLocationPermission) {
Anil Admal4b6a8302019-05-02 18:38:39 -0700369 Log.i(TAG, "Proxy app " + proxyAppPkgName + " location permission changed."
370 + " IsLocationPermissionEnabled: " + isLocationPermissionEnabled);
Anil Admal74de73e2019-05-14 19:14:23 -0700371 proxyAppState.mHasLocationPermission = isLocationPermissionEnabled;
Anil Admal94ec76a2019-01-15 09:42:01 -0800372 updateNfwLocationAccessProxyAppsInGnssHal();
Anil Admal94ec76a2019-01-15 09:42:01 -0800373 }
Anil Admal4b6a8302019-05-02 18:38:39 -0700374 return;
Anil Admal94ec76a2019-01-15 09:42:01 -0800375 }
376 }
377
Anil Admal4b6a8302019-05-02 18:38:39 -0700378 private ApplicationInfo getProxyAppInfo(String proxyAppPkgName) {
Anil Admal94ec76a2019-01-15 09:42:01 -0800379 try {
Anil Admal4b6a8302019-05-02 18:38:39 -0700380 return mPackageManager.getApplicationInfo(proxyAppPkgName, 0);
Anil Admal94ec76a2019-01-15 09:42:01 -0800381 } catch (PackageManager.NameNotFoundException e) {
Anil Admal4b6a8302019-05-02 18:38:39 -0700382 if (DEBUG) Log.d(TAG, "Proxy app " + proxyAppPkgName + " is not found.");
Anil Admal94ec76a2019-01-15 09:42:01 -0800383 return null;
384 }
385 }
386
Anil Admal4b6a8302019-05-02 18:38:39 -0700387 private boolean shouldEnableLocationPermissionInGnssHal(String proxyAppPkgName) {
388 return isProxyAppInstalled(proxyAppPkgName) && hasLocationPermission(proxyAppPkgName);
389 }
390
391 private boolean isProxyAppInstalled(String pkgName) {
392 ApplicationInfo proxyAppInfo = getProxyAppInfo(pkgName);
393 return (proxyAppInfo != null) && proxyAppInfo.enabled;
394 }
395
Anil Admal94ec76a2019-01-15 09:42:01 -0800396 private boolean hasLocationPermission(String pkgName) {
397 return mPackageManager.checkPermission(LOCATION_PERMISSION_NAME, pkgName)
398 == PackageManager.PERMISSION_GRANTED;
399 }
400
401 private void updateNfwLocationAccessProxyAppsInGnssHal() {
Anil Admal138cdc32019-04-16 10:07:43 -0700402 if (!mIsGpsEnabled) {
Anil Admal316f9482019-02-12 18:57:18 -0800403 return; // Keep non-framework location access disabled.
404 }
405 setNfwLocationAccessProxyAppsInGnssHal(getLocationPermissionEnabledProxyApps());
406 }
407
408 private void setNfwLocationAccessProxyAppsInGnssHal(
409 String[] locationPermissionEnabledProxyApps) {
Anil Admal204e22e2019-01-22 15:14:17 -0800410 final String proxyAppsStr = Arrays.toString(locationPermissionEnabledProxyApps);
411 Log.i(TAG, "Updating non-framework location access proxy apps in the GNSS HAL to: "
412 + proxyAppsStr);
413 boolean result = native_enable_nfw_location_access(locationPermissionEnabledProxyApps);
414 if (!result) {
415 Log.e(TAG, "Failed to update non-framework location access proxy apps in the"
416 + " GNSS HAL to: " + proxyAppsStr);
417 }
418 }
419
Anil Admal204e22e2019-01-22 15:14:17 -0800420 private String[] getLocationPermissionEnabledProxyApps() {
Anil Admal316f9482019-02-12 18:57:18 -0800421 // Get a count of proxy apps with location permission enabled for array creation size.
Anil Admal94ec76a2019-01-15 09:42:01 -0800422 int countLocationPermissionEnabledProxyApps = 0;
Anil Admal74de73e2019-05-14 19:14:23 -0700423 for (ProxyAppState proxyAppState : mProxyAppsState.values()) {
424 if (proxyAppState.mHasLocationPermission) {
Anil Admal94ec76a2019-01-15 09:42:01 -0800425 ++countLocationPermissionEnabledProxyApps;
426 }
427 }
428
429 int i = 0;
430 String[] locationPermissionEnabledProxyApps =
431 new String[countLocationPermissionEnabledProxyApps];
Anil Admal74de73e2019-05-14 19:14:23 -0700432 for (Map.Entry<String, ProxyAppState> entry : mProxyAppsState.entrySet()) {
Anil Admal94ec76a2019-01-15 09:42:01 -0800433 final String proxyApp = entry.getKey();
Anil Admal74de73e2019-05-14 19:14:23 -0700434 if (entry.getValue().mHasLocationPermission) {
Anil Admal94ec76a2019-01-15 09:42:01 -0800435 locationPermissionEnabledProxyApps[i++] = proxyApp;
436 }
437 }
Anil Admal204e22e2019-01-22 15:14:17 -0800438 return locationPermissionEnabledProxyApps;
Anil Admal94ec76a2019-01-15 09:42:01 -0800439 }
440
441 private void handleNfwNotification(NfwNotification nfwNotification) {
Anil Admalc25a2712019-02-15 16:48:48 -0800442 if (DEBUG) Log.d(TAG, "Non-framework location access notification: " + nfwNotification);
Anil Admal94ec76a2019-01-15 09:42:01 -0800443
Anil Admalfd9bb8e2019-04-17 18:08:46 -0700444 if (nfwNotification.isEmergencyRequestNotification()) {
445 handleEmergencyNfwNotification(nfwNotification);
446 return;
Yu-Han Yang14d5fb42019-01-16 12:42:59 -0800447 }
Anil Admalfd9bb8e2019-04-17 18:08:46 -0700448
Anil Admal4b6a8302019-05-02 18:38:39 -0700449 final String proxyAppPkgName = nfwNotification.mProxyAppPackageName;
Anil Admal74de73e2019-05-14 19:14:23 -0700450 final ProxyAppState proxyAppState = mProxyAppsState.get(proxyAppPkgName);
Anil Admalfd9bb8e2019-04-17 18:08:46 -0700451 final boolean isLocationRequestAccepted = nfwNotification.isRequestAccepted();
Anil Admal27caa742019-05-30 14:44:52 -0700452 final boolean isPermissionMismatched = isPermissionMismatched(proxyAppState,
453 nfwNotification);
Yu-Han Yang14d5fb42019-01-16 12:42:59 -0800454 logEvent(nfwNotification, isPermissionMismatched);
455
Anil Admalfd9bb8e2019-04-17 18:08:46 -0700456 if (!nfwNotification.isRequestAttributedToProxyApp()) {
457 // Handle cases where GNSS HAL implementation correctly rejected NFW location request.
458 // 1. GNSS HAL implementation doesn't provide location to any NFW location use cases.
459 // There is no Location Attribution App configured in the framework.
460 // 2. GNSS HAL implementation doesn't provide location to some NFW location use cases.
461 // Location Attribution Apps are configured only for the supported NFW location
462 // use cases. All other use cases which are not supported (and always rejected) by
463 // the GNSS HAL implementation will have proxyAppPackageName set to empty string.
464 if (!isLocationRequestAccepted) {
465 if (DEBUG) {
466 Log.d(TAG, "Non-framework location request rejected. ProxyAppPackageName field"
467 + " is not set in the notification: " + nfwNotification + ". Number of"
Anil Admal74de73e2019-05-14 19:14:23 -0700468 + " configured proxy apps: " + mProxyAppsState.size());
Anil Admalfd9bb8e2019-04-17 18:08:46 -0700469 }
470 return;
471 }
472
Anil Admal8a246a22019-05-05 00:34:55 -0700473 Log.e(TAG, "ProxyAppPackageName field is not set. AppOps service not notified"
474 + " for notification: " + nfwNotification);
Anil Admal94ec76a2019-01-15 09:42:01 -0800475 return;
476 }
477
Anil Admal74de73e2019-05-14 19:14:23 -0700478 if (proxyAppState == null) {
Anil Admal8a246a22019-05-05 00:34:55 -0700479 Log.w(TAG, "Could not find proxy app " + proxyAppPkgName + " in the value specified for"
480 + " config parameter: " + GnssConfiguration.CONFIG_NFW_PROXY_APPS
481 + ". AppOps service not notified for notification: " + nfwNotification);
Anil Admal94ec76a2019-01-15 09:42:01 -0800482 return;
483 }
484
485 // Display location icon attributed to this proxy app.
Anil Admal4b6a8302019-05-02 18:38:39 -0700486 final ApplicationInfo proxyAppInfo = getProxyAppInfo(proxyAppPkgName);
487 if (proxyAppInfo == null) {
488 Log.e(TAG, "Proxy app " + proxyAppPkgName + " is not found. AppOps service not "
Anil Admal8a246a22019-05-05 00:34:55 -0700489 + "notified for notification: " + nfwNotification);
Anil Admalc25a2712019-02-15 16:48:48 -0800490 return;
491 }
Anil Admal4b6a8302019-05-02 18:38:39 -0700492
Anil Admal74de73e2019-05-14 19:14:23 -0700493 if (nfwNotification.isLocationProvided()) {
494 showLocationIcon(proxyAppState, nfwNotification, proxyAppInfo.uid, proxyAppPkgName);
495 mAppOps.noteOpNoThrow(AppOpsManager.OP_FINE_LOCATION, proxyAppInfo.uid,
496 proxyAppPkgName);
497 }
Anil Admal94ec76a2019-01-15 09:42:01 -0800498
499 // Log proxy app permission mismatch between framework and GNSS HAL.
Yu-Han Yang14d5fb42019-01-16 12:42:59 -0800500 if (isPermissionMismatched) {
Anil Admal27caa742019-05-30 14:44:52 -0700501 Log.w(TAG, "Permission mismatch. Proxy app " + proxyAppPkgName
Anil Admal74de73e2019-05-14 19:14:23 -0700502 + " location permission is set to " + proxyAppState.mHasLocationPermission
Anil Admal27caa742019-05-30 14:44:52 -0700503 + " and GNSS HAL enabled is set to " + mIsGpsEnabled
Anil Admal94ec76a2019-01-15 09:42:01 -0800504 + " but GNSS non-framework location access response type is "
Anil Admalc25a2712019-02-15 16:48:48 -0800505 + nfwNotification.getResponseTypeAsString() + " for notification: "
506 + nfwNotification);
Anil Admal94ec76a2019-01-15 09:42:01 -0800507 }
508 }
509
Anil Admal27caa742019-05-30 14:44:52 -0700510 private boolean isPermissionMismatched(ProxyAppState proxyAppState,
511 NfwNotification nfwNotification) {
512 // Non-framework non-emergency location requests must be accepted only when IGnss.hal
513 // is enabled and the proxy app has location permission.
514 final boolean isLocationRequestAccepted = nfwNotification.isRequestAccepted();
515 return (proxyAppState == null || !mIsGpsEnabled) ? isLocationRequestAccepted
516 : (proxyAppState.mHasLocationPermission != isLocationRequestAccepted);
517 }
518
Anil Admal74de73e2019-05-14 19:14:23 -0700519 private void showLocationIcon(ProxyAppState proxyAppState, NfwNotification nfwNotification,
520 int uid, String proxyAppPkgName) {
521 // If we receive a new NfwNotification before the location icon is turned off for the
522 // previous notification, update the timer to extend the location icon display duration.
523 final boolean isLocationIconOn = proxyAppState.mIsLocationIconOn;
524 if (!isLocationIconOn) {
525 if (!updateLocationIcon(/* displayLocationIcon = */ true, uid, proxyAppPkgName)) {
526 Log.w(TAG, "Failed to show Location icon for notification: " + nfwNotification);
527 return;
528 }
529 proxyAppState.mIsLocationIconOn = true;
530 } else {
531 // Extend timer by canceling the current one and starting a new one.
532 mHandler.removeCallbacksAndMessages(proxyAppState);
533 }
534
535 // Start timer to turn off location icon. proxyAppState is used as a token to cancel timer.
536 if (DEBUG) {
537 Log.d(TAG, "Location icon on. " + (isLocationIconOn ? "Extending" : "Setting")
538 + " icon display timer. Uid: " + uid + ", proxyAppPkgName: " + proxyAppPkgName);
539 }
540 if (!mHandler.postDelayed(() -> handleLocationIconTimeout(proxyAppPkgName),
541 /* token = */ proxyAppState, LOCATION_ICON_DISPLAY_DURATION_MILLIS)) {
542 clearLocationIcon(proxyAppState, uid, proxyAppPkgName);
543 Log.w(TAG, "Failed to show location icon for the full duration for notification: "
544 + nfwNotification);
545 }
546 }
547
548 private void handleLocationIconTimeout(String proxyAppPkgName) {
549 // Get uid again instead of using the one provided in startOp() call as the app could have
550 // been uninstalled and reinstalled during the timeout duration (unlikely in real world).
551 final ApplicationInfo proxyAppInfo = getProxyAppInfo(proxyAppPkgName);
552 if (proxyAppInfo != null) {
553 clearLocationIcon(mProxyAppsState.get(proxyAppPkgName), proxyAppInfo.uid,
554 proxyAppPkgName);
555 }
556 }
557
558 private void clearLocationIcon(@Nullable ProxyAppState proxyAppState, int uid,
559 String proxyAppPkgName) {
560 updateLocationIcon(/* displayLocationIcon = */ false, uid, proxyAppPkgName);
561 if (proxyAppState != null) proxyAppState.mIsLocationIconOn = false;
562 if (DEBUG) {
563 Log.d(TAG, "Location icon off. Uid: " + uid + ", proxyAppPkgName: " + proxyAppPkgName);
564 }
565 }
566
567 private boolean updateLocationIcon(boolean displayLocationIcon, int uid,
568 String proxyAppPkgName) {
569 if (displayLocationIcon) {
570 // Need two calls to startOp() here with different op code so that the proxy app shows
571 // up in the recent location requests page and also the location icon gets displayed.
572 if (mAppOps.startOpNoThrow(AppOpsManager.OP_MONITOR_LOCATION, uid,
573 proxyAppPkgName) != AppOpsManager.MODE_ALLOWED) {
574 return false;
575 }
576 if (mAppOps.startOpNoThrow(AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION, uid,
577 proxyAppPkgName) != AppOpsManager.MODE_ALLOWED) {
578 mAppOps.finishOp(AppOpsManager.OP_MONITOR_LOCATION, uid, proxyAppPkgName);
579 return false;
580 }
581 } else {
582 mAppOps.finishOp(AppOpsManager.OP_MONITOR_LOCATION, uid, proxyAppPkgName);
583 mAppOps.finishOp(AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION, uid, proxyAppPkgName);
584 }
585 sendHighPowerMonitoringBroadcast();
586 return true;
587 }
588
589 private void sendHighPowerMonitoringBroadcast() {
590 // Send an intent to notify that a high power request has been added/removed so that
591 // the SystemUi checks the state of AppOps and updates the location icon accordingly.
592 Intent intent = new Intent(LocationManager.HIGH_POWER_REQUEST_CHANGE_ACTION);
593 mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
594 }
595
Anil Admalfd9bb8e2019-04-17 18:08:46 -0700596 private void handleEmergencyNfwNotification(NfwNotification nfwNotification) {
Anil Admal8a246a22019-05-05 00:34:55 -0700597 boolean isPermissionMismatched = false;
598 if (!nfwNotification.isRequestAccepted()) {
Anil Admalfd9bb8e2019-04-17 18:08:46 -0700599 Log.e(TAG, "Emergency non-framework location request incorrectly rejected."
600 + " Notification: " + nfwNotification);
Anil Admal8a246a22019-05-05 00:34:55 -0700601 isPermissionMismatched = true;
Anil Admalfd9bb8e2019-04-17 18:08:46 -0700602 }
Anil Admal8a246a22019-05-05 00:34:55 -0700603
604 if (!mNiHandler.getInEmergency()) {
605 Log.w(TAG, "Emergency state mismatch. Device currently not in user initiated emergency"
606 + " session. Notification: " + nfwNotification);
607 isPermissionMismatched = true;
608 }
609
Anil Admalfd9bb8e2019-04-17 18:08:46 -0700610 logEvent(nfwNotification, isPermissionMismatched);
Anil Admal8a246a22019-05-05 00:34:55 -0700611
Anil Admal61da5f02019-05-17 09:04:13 -0700612 if (nfwNotification.isLocationProvided()) {
613 postEmergencyLocationUserNotification(nfwNotification);
Anil Admal8a246a22019-05-05 00:34:55 -0700614 }
Anil Admalfd9bb8e2019-04-17 18:08:46 -0700615 }
616
Anil Admal61da5f02019-05-17 09:04:13 -0700617 private void postEmergencyLocationUserNotification(NfwNotification nfwNotification) {
618 // Emulate deprecated IGnssNi.hal user notification of emergency NI requests.
619 NotificationManager notificationManager = (NotificationManager) mContext
620 .getSystemService(Context.NOTIFICATION_SERVICE);
621 if (notificationManager == null) {
622 Log.w(TAG, "Could not notify user of emergency location request. Notification: "
623 + nfwNotification);
624 return;
625 }
626
627 notificationManager.notifyAsUser(/* tag= */ null, /* notificationId= */ 0,
Anil Admal6acd4d72019-06-11 14:37:31 -0700628 createEmergencyLocationUserNotification(mContext), UserHandle.ALL);
Anil Admal61da5f02019-05-17 09:04:13 -0700629 }
630
631 private static Notification createEmergencyLocationUserNotification(Context context) {
Anil Admal6acd4d72019-06-11 14:37:31 -0700632 // NOTE: Do not reuse the returned notification object as it will not reflect
633 // changes to notification text when the system language is changed.
634 final String firstLineText = context.getString(R.string.gpsNotifTitle);
635 final String secondLineText = context.getString(R.string.global_action_emergency);
636 final String accessibilityServicesText = firstLineText + " (" + secondLineText + ")";
Anil Admal61da5f02019-05-17 09:04:13 -0700637 return new Notification.Builder(context, SystemNotificationChannels.NETWORK_ALERTS)
638 .setSmallIcon(com.android.internal.R.drawable.stat_sys_gps_on)
639 .setWhen(0)
Yu-Han Yang1f503e42019-12-05 14:07:23 -0800640 .setOngoing(false)
Anil Admal61da5f02019-05-17 09:04:13 -0700641 .setAutoCancel(true)
642 .setColor(context.getColor(
643 com.android.internal.R.color.system_notification_accent_color))
644 .setDefaults(0)
645 .setTicker(accessibilityServicesText)
646 .setContentTitle(firstLineText)
647 .setContentText(secondLineText)
648 .setContentIntent(PendingIntent.getBroadcast(context, 0, new Intent(), 0))
649 .build();
650 }
651
Yu-Han Yang14d5fb42019-01-16 12:42:59 -0800652 private void logEvent(NfwNotification notification, boolean isPermissionMismatched) {
Muhammad Qureshi6f207102020-01-28 10:37:41 -0800653 FrameworkStatsLog.write(FrameworkStatsLog.GNSS_NFW_NOTIFICATION_REPORTED,
Yu-Han Yang14d5fb42019-01-16 12:42:59 -0800654 notification.mProxyAppPackageName,
655 notification.mProtocolStack,
656 notification.mOtherProtocolStackName,
657 notification.mRequestor,
658 notification.mRequestorId,
659 notification.mResponseType,
660 notification.mInEmergencyMode,
661 notification.mIsCachedLocation,
662 isPermissionMismatched);
663 }
664
Anil Admal204e22e2019-01-22 15:14:17 -0800665 private void runOnHandler(Runnable event) {
Anil Admal94ec76a2019-01-15 09:42:01 -0800666 // Hold a wake lock until this message is delivered.
667 // Note that this assumes the message will not be removed from the queue before
668 // it is handled (otherwise the wake lock would be leaked).
669 mWakeLock.acquire(WAKELOCK_TIMEOUT_MILLIS);
670 if (!mHandler.post(runEventAndReleaseWakeLock(event))) {
671 mWakeLock.release();
672 }
673 }
674
675 private Runnable runEventAndReleaseWakeLock(Runnable event) {
676 return () -> {
677 try {
678 event.run();
679 } finally {
680 mWakeLock.release();
681 }
682 };
683 }
684
685 private native boolean native_enable_nfw_location_access(String[] proxyApps);
686}