blob: 75fd7dca2a91025d861c9f9fe71de2ee6b69180d [file] [log] [blame]
Anil Admal94ec76a2019-01-15 09:42:01 -08001/*
Sasha Kuznetsov94bb0092020-03-26 12:08:17 -07002 * Copyright (C) 2020 The Android Open Source Project
Anil Admal94ec76a2019-01-15 09:42:01 -08003 *
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
Sasha Kuznetsov94bb0092020-03-26 12:08:17 -070017package com.android.server.location.gnss;
Anil Admal94ec76a2019-01-15 09:42:01 -080018
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;
Anil Admal204e22e2019-01-22 15:14:17 -080024import android.content.BroadcastReceiver;
Anil Admal94ec76a2019-01-15 09:42:01 -080025import android.content.Context;
26import android.content.Intent;
Anil Admal204e22e2019-01-22 15:14:17 -080027import android.content.IntentFilter;
Anil Admal4b6a8302019-05-02 18:38:39 -070028import android.content.pm.ApplicationInfo;
Anil Admal94ec76a2019-01-15 09:42:01 -080029import android.content.pm.PackageManager;
Soonil Nagarkarbfef65a2020-04-16 14:18:12 -070030import android.hardware.gnss.visibility_control.V1_0.IGnssVisibilityControlCallback;
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;
Soonil Nagarkarbfef65a2020-04-16 14:18:12 -070042import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
Anil Admal61da5f02019-05-17 09:04:13 -070043import com.android.internal.notification.SystemNotificationChannels;
Muhammad Qureshi6f207102020-01-28 10:37:41 -080044import com.android.internal.util.FrameworkStatsLog;
Anil Admal8a246a22019-05-05 00:34:55 -070045
Anil Admal94ec76a2019-01-15 09:42:01 -080046import java.util.Arrays;
Anil Admal94ec76a2019-01-15 09:42:01 -080047import java.util.List;
48import java.util.Map;
Soonil Nagarkarbfef65a2020-04-16 14:18:12 -070049import java.util.Objects;
Anil Admal94ec76a2019-01-15 09:42:01 -080050
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 */
57class GnssVisibilityControl {
58 private static final String TAG = "GnssVisibilityControl";
59 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
60
Anil Admal94ec76a2019-01-15 09:42:01 -080061 private static final String LOCATION_PERMISSION_NAME =
62 "android.permission.ACCESS_FINE_LOCATION";
63
Anil Admal204e22e2019-01-22 15:14:17 -080064 private static final String[] NO_LOCATION_ENABLED_PROXY_APPS = new String[0];
65
Anil Admal138cdc32019-04-16 10:07:43 -070066 // Max wait time for synchronous method onGpsEnabledChanged() to run.
67 private static final long ON_GPS_ENABLED_CHANGED_TIMEOUT_MILLIS = 3 * 1000;
68
Anil Admal74de73e2019-05-14 19:14:23 -070069 // 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
Anil Admal94ec76a2019-01-15 09:42:01 -080072 // 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;
Anil Admal8a246a22019-05-05 00:34:55 -070082 private final GpsNetInitiatedHandler mNiHandler;
Anil Admal94ec76a2019-01-15 09:42:01 -080083
Anil Admal138cdc32019-04-16 10:07:43 -070084 private boolean mIsGpsEnabled;
Anil Admal204e22e2019-01-22 15:14:17 -080085
Anil Admal74de73e2019-05-14 19:14:23 -070086 private static final class ProxyAppState {
87 private boolean mHasLocationPermission;
88 private boolean mIsLocationIconOn;
89
90 private ProxyAppState(boolean hasLocationPermission) {
91 mHasLocationPermission = hasLocationPermission;
92 }
93 }
94
Anil Admal94ec76a2019-01-15 09:42:01 -080095 // Number of non-framework location access proxy apps is expected to be small (< 5).
Anil Admal74de73e2019-05-14 19:14:23 -070096 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);
Anil Admal94ec76a2019-01-15 09:42:01 -080099
100 private PackageManager.OnPermissionsChangedListener mOnPermissionsChangedListener =
Anil Admal204e22e2019-01-22 15:14:17 -0800101 uid -> runOnHandler(() -> handlePermissionsChanged(uid));
Anil Admal94ec76a2019-01-15 09:42:01 -0800102
Anil Admal8a246a22019-05-05 00:34:55 -0700103 GnssVisibilityControl(Context context, Looper looper, GpsNetInitiatedHandler niHandler) {
Anil Admal94ec76a2019-01-15 09:42:01 -0800104 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);
Anil Admal8a246a22019-05-05 00:34:55 -0700108 mNiHandler = niHandler;
Anil Admal94ec76a2019-01-15 09:42:01 -0800109 mAppOps = mContext.getSystemService(AppOpsManager.class);
110 mPackageManager = mContext.getPackageManager();
Anil Admal204e22e2019-01-22 15:14:17 -0800111
Anil Admal316f9482019-02-12 18:57:18 -0800112 // 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);
Anil Admal94ec76a2019-01-15 09:42:01 -0800115 }
116
Anil Admal138cdc32019-04-16 10:07:43 -0700117 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
Anil Admal94ec76a2019-01-15 09:42:01 -0800141 void reportNfwNotification(String proxyAppPackageName, byte protocolStack,
142 String otherProtocolStackName, byte requestor, String requestorId, byte responseType,
143 boolean inEmergencyMode, boolean isCachedLocation) {
Anil Admal204e22e2019-01-22 15:14:17 -0800144 runOnHandler(() -> handleNfwNotification(
Anil Admal94ec76a2019-01-15 09:42:01 -0800145 new NfwNotification(proxyAppPackageName, protocolStack, otherProtocolStackName,
146 requestor, requestorId, responseType, inEmergencyMode, isCachedLocation)));
147 }
148
Anil Admale1539e82019-05-09 15:05:04 -0700149 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();
Anil Admal61da5f02019-05-17 09:04:13 -0700152 runOnHandler(() -> handleUpdateProxyApps(nfwLocationAccessProxyApps));
Anil Admalbefd4832019-05-09 19:58:13 -0700153 }
154
Anil Admal316f9482019-02-12 18:57:18 -0800155 private void handleInitialize() {
Anil Admal316f9482019-02-12 18:57:18 -0800156 listenForProxyAppsPackageUpdates();
Anil Admal316f9482019-02-12 18:57:18 -0800157 }
158
Anil Admal204e22e2019-01-22 15:14:17 -0800159 private void listenForProxyAppsPackageUpdates() {
Anil Admal316f9482019-02-12 18:57:18 -0800160 // Listen for proxy apps package installation, removal events.
Anil Admal204e22e2019-01-22 15:14:17 -0800161 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);
Anil Admal4b6a8302019-05-02 18:38:39 -0700165 intentFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
Anil Admal204e22e2019-01-22 15:14:17 -0800166 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:
Anil Admal4b6a8302019-05-02 18:38:39 -0700179 case Intent.ACTION_PACKAGE_CHANGED:
Anil Admal204e22e2019-01-22 15:14:17 -0800180 String pkgName = intent.getData().getEncodedSchemeSpecificPart();
181 handleProxyAppPackageUpdate(pkgName, action);
182 break;
183 }
184 }
185 }, UserHandle.ALL, intentFilter, null, mHandler);
186 }
187
188 private void handleProxyAppPackageUpdate(String pkgName, String action) {
Anil Admal74de73e2019-05-14 19:14:23 -0700189 final ProxyAppState proxyAppState = mProxyAppsState.get(pkgName);
190 if (proxyAppState == null) {
Anil Admal316f9482019-02-12 18:57:18 -0800191 return; // ignore, pkgName is not one of the proxy apps in our list.
Anil Admal204e22e2019-01-22 15:14:17 -0800192 }
193
Anil Admal4b6a8302019-05-02 18:38:39 -0700194 if (DEBUG) Log.d(TAG, "Proxy app " + pkgName + " package changed: " + action);
195 final boolean updatedLocationPermission = shouldEnableLocationPermissionInGnssHal(pkgName);
Anil Admal74de73e2019-05-14 19:14:23 -0700196 if (proxyAppState.mHasLocationPermission != updatedLocationPermission) {
Anil Admal204e22e2019-01-22 15:14:17 -0800197 // Permission changed. So, update the GNSS HAL with the updated list.
Anil Admal4b6a8302019-05-02 18:38:39 -0700198 Log.i(TAG, "Proxy app " + pkgName + " location permission changed."
199 + " IsLocationPermissionEnabled: " + updatedLocationPermission);
Anil Admal74de73e2019-05-14 19:14:23 -0700200 proxyAppState.mHasLocationPermission = updatedLocationPermission;
Anil Admal204e22e2019-01-22 15:14:17 -0800201 updateNfwLocationAccessProxyAppsInGnssHal();
202 }
203 }
204
205 private void handleUpdateProxyApps(List<String> nfwLocationAccessProxyApps) {
206 if (!isProxyAppListUpdated(nfwLocationAccessProxyApps)) {
207 return;
208 }
209
Anil Admal94ec76a2019-01-15 09:42:01 -0800210 if (nfwLocationAccessProxyApps.isEmpty()) {
211 // Stop listening for app permission changes. Clear the app list in GNSS HAL.
Anil Admal74de73e2019-05-14 19:14:23 -0700212 if (!mProxyAppsState.isEmpty()) {
Anil Admal94ec76a2019-01-15 09:42:01 -0800213 mPackageManager.removeOnPermissionsChangeListener(mOnPermissionsChangedListener);
Anil Admal74de73e2019-05-14 19:14:23 -0700214 resetProxyAppsState();
Anil Admal94ec76a2019-01-15 09:42:01 -0800215 updateNfwLocationAccessProxyAppsInGnssHal();
216 }
217 return;
218 }
219
Anil Admal74de73e2019-05-14 19:14:23 -0700220 if (mProxyAppsState.isEmpty()) {
Anil Admal94ec76a2019-01-15 09:42:01 -0800221 mPackageManager.addOnPermissionsChangeListener(mOnPermissionsChangedListener);
222 } else {
Anil Admal74de73e2019-05-14 19:14:23 -0700223 resetProxyAppsState();
Anil Admal94ec76a2019-01-15 09:42:01 -0800224 }
225
Anil Admal4b6a8302019-05-02 18:38:39 -0700226 for (String proxyAppPkgName : nfwLocationAccessProxyApps) {
Anil Admal74de73e2019-05-14 19:14:23 -0700227 ProxyAppState proxyAppState = new ProxyAppState(shouldEnableLocationPermissionInGnssHal(
228 proxyAppPkgName));
229 mProxyAppsState.put(proxyAppPkgName, proxyAppState);
Anil Admal94ec76a2019-01-15 09:42:01 -0800230 }
231
232 updateNfwLocationAccessProxyAppsInGnssHal();
233 }
234
Anil Admal74de73e2019-05-14 19:14:23 -0700235 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
Anil Admal204e22e2019-01-22 15:14:17 -0800252 private boolean isProxyAppListUpdated(List<String> nfwLocationAccessProxyApps) {
Anil Admal74de73e2019-05-14 19:14:23 -0700253 if (nfwLocationAccessProxyApps.size() != mProxyAppsState.size()) {
Anil Admal204e22e2019-01-22 15:14:17 -0800254 return true;
255 }
256
257 for (String nfwLocationAccessProxyApp : nfwLocationAccessProxyApps) {
Anil Admal74de73e2019-05-14 19:14:23 -0700258 if (!mProxyAppsState.containsKey(nfwLocationAccessProxyApp)) {
Anil Admal204e22e2019-01-22 15:14:17 -0800259 return true;
260 }
261 }
Anil Admal204e22e2019-01-22 15:14:17 -0800262 return false;
263 }
264
Anil Admal8b83dc12019-06-12 13:55:42 -0700265 private void handleGpsEnabledChanged(boolean isGpsEnabled) {
266 if (DEBUG) {
267 Log.d(TAG, "handleGpsEnabledChanged, mIsGpsEnabled: " + mIsGpsEnabled
268 + ", isGpsEnabled: " + isGpsEnabled);
Anil Admal316f9482019-02-12 18:57:18 -0800269 }
270
Anil Admal8b83dc12019-06-12 13:55:42 -0700271 // 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;
Anil Admal138cdc32019-04-16 10:07:43 -0700274 if (!mIsGpsEnabled) {
Anil Admal316f9482019-02-12 18:57:18 -0800275 disableNfwLocationAccess();
276 return;
277 }
278
Anil Admal8b83dc12019-06-12 13:55:42 -0700279 setNfwLocationAccessProxyAppsInGnssHal(getLocationPermissionEnabledProxyApps());
Anil Admal316f9482019-02-12 18:57:18 -0800280 }
281
282 private void disableNfwLocationAccess() {
283 setNfwLocationAccessProxyAppsInGnssHal(NO_LOCATION_ENABLED_PROXY_APPS);
Anil Admal204e22e2019-01-22 15:14:17 -0800284 }
285
Anil Admal94ec76a2019-01-15 09:42:01 -0800286 // Represents NfwNotification structure in IGnssVisibilityControlCallback.hal
287 private static class NfwNotification {
Anil Admal94ec76a2019-01-15 09:42:01 -0800288
Soonil Nagarkarbfef65a2020-04-16 14:18:12 -0700289 // 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;
Anil Admal94ec76a2019-01-15 09:42:01 -0800293
Soonil Nagarkarbfef65a2020-04-16 14:18:12 -0700294 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
303 NfwNotification(String proxyAppPackageName, byte protocolStack,
Anil Admal94ec76a2019-01-15 09:42:01 -0800304 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
Anil Admal94ec76a2019-01-15 09:42:01 -0800316 @SuppressLint("DefaultLocale")
317 public String toString() {
318 return String.format(
Anil Admalc25a2712019-02-15 16:48:48 -0800319 "{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);
Anil Admal94ec76a2019-01-15 09:42:01 -0800324 }
325
Anil Admal204e22e2019-01-22 15:14:17 -0800326 private String getResponseTypeAsString() {
Anil Admal94ec76a2019-01-15 09:42:01 -0800327 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 }
Anil Admalfd9bb8e2019-04-17 18:08:46 -0700338
339 private boolean isRequestAccepted() {
340 return mResponseType != NfwNotification.NFW_RESPONSE_TYPE_REJECTED;
341 }
342
Anil Admal8a246a22019-05-05 00:34:55 -0700343 private boolean isLocationProvided() {
344 return mResponseType == NfwNotification.NFW_RESPONSE_TYPE_ACCEPTED_LOCATION_PROVIDED;
345 }
346
Anil Admalfd9bb8e2019-04-17 18:08:46 -0700347 private boolean isRequestAttributedToProxyApp() {
348 return !TextUtils.isEmpty(mProxyAppPackageName);
349 }
350
351 private boolean isEmergencyRequestNotification() {
352 return mInEmergencyMode && !isRequestAttributedToProxyApp();
353 }
Anil Admal94ec76a2019-01-15 09:42:01 -0800354 }
355
356 private void handlePermissionsChanged(int uid) {
Anil Admal74de73e2019-05-14 19:14:23 -0700357 if (mProxyAppsState.isEmpty()) {
Anil Admal94ec76a2019-01-15 09:42:01 -0800358 return;
359 }
360
Anil Admal74de73e2019-05-14 19:14:23 -0700361 for (Map.Entry<String, ProxyAppState> entry : mProxyAppsState.entrySet()) {
Anil Admal4b6a8302019-05-02 18:38:39 -0700362 final String proxyAppPkgName = entry.getKey();
363 final ApplicationInfo proxyAppInfo = getProxyAppInfo(proxyAppPkgName);
364 if (proxyAppInfo == null || proxyAppInfo.uid != uid) {
Anil Admal94ec76a2019-01-15 09:42:01 -0800365 continue;
366 }
367
Anil Admal4b6a8302019-05-02 18:38:39 -0700368 final boolean isLocationPermissionEnabled = shouldEnableLocationPermissionInGnssHal(
369 proxyAppPkgName);
Anil Admal74de73e2019-05-14 19:14:23 -0700370 ProxyAppState proxyAppState = entry.getValue();
371 if (isLocationPermissionEnabled != proxyAppState.mHasLocationPermission) {
Anil Admal4b6a8302019-05-02 18:38:39 -0700372 Log.i(TAG, "Proxy app " + proxyAppPkgName + " location permission changed."
373 + " IsLocationPermissionEnabled: " + isLocationPermissionEnabled);
Anil Admal74de73e2019-05-14 19:14:23 -0700374 proxyAppState.mHasLocationPermission = isLocationPermissionEnabled;
Anil Admal94ec76a2019-01-15 09:42:01 -0800375 updateNfwLocationAccessProxyAppsInGnssHal();
Anil Admal94ec76a2019-01-15 09:42:01 -0800376 }
Anil Admal4b6a8302019-05-02 18:38:39 -0700377 return;
Anil Admal94ec76a2019-01-15 09:42:01 -0800378 }
379 }
380
Anil Admal4b6a8302019-05-02 18:38:39 -0700381 private ApplicationInfo getProxyAppInfo(String proxyAppPkgName) {
Anil Admal94ec76a2019-01-15 09:42:01 -0800382 try {
Anil Admal4b6a8302019-05-02 18:38:39 -0700383 return mPackageManager.getApplicationInfo(proxyAppPkgName, 0);
Anil Admal94ec76a2019-01-15 09:42:01 -0800384 } catch (PackageManager.NameNotFoundException e) {
Anil Admal4b6a8302019-05-02 18:38:39 -0700385 if (DEBUG) Log.d(TAG, "Proxy app " + proxyAppPkgName + " is not found.");
Anil Admal94ec76a2019-01-15 09:42:01 -0800386 return null;
387 }
388 }
389
Anil Admal4b6a8302019-05-02 18:38:39 -0700390 private boolean shouldEnableLocationPermissionInGnssHal(String proxyAppPkgName) {
391 return isProxyAppInstalled(proxyAppPkgName) && hasLocationPermission(proxyAppPkgName);
392 }
393
394 private boolean isProxyAppInstalled(String pkgName) {
395 ApplicationInfo proxyAppInfo = getProxyAppInfo(pkgName);
396 return (proxyAppInfo != null) && proxyAppInfo.enabled;
397 }
398
Anil Admal94ec76a2019-01-15 09:42:01 -0800399 private boolean hasLocationPermission(String pkgName) {
400 return mPackageManager.checkPermission(LOCATION_PERMISSION_NAME, pkgName)
401 == PackageManager.PERMISSION_GRANTED;
402 }
403
404 private void updateNfwLocationAccessProxyAppsInGnssHal() {
Anil Admal138cdc32019-04-16 10:07:43 -0700405 if (!mIsGpsEnabled) {
Anil Admal316f9482019-02-12 18:57:18 -0800406 return; // Keep non-framework location access disabled.
407 }
408 setNfwLocationAccessProxyAppsInGnssHal(getLocationPermissionEnabledProxyApps());
409 }
410
411 private void setNfwLocationAccessProxyAppsInGnssHal(
412 String[] locationPermissionEnabledProxyApps) {
Anil Admal204e22e2019-01-22 15:14:17 -0800413 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
Anil Admal204e22e2019-01-22 15:14:17 -0800423 private String[] getLocationPermissionEnabledProxyApps() {
Anil Admal316f9482019-02-12 18:57:18 -0800424 // Get a count of proxy apps with location permission enabled for array creation size.
Anil Admal94ec76a2019-01-15 09:42:01 -0800425 int countLocationPermissionEnabledProxyApps = 0;
Anil Admal74de73e2019-05-14 19:14:23 -0700426 for (ProxyAppState proxyAppState : mProxyAppsState.values()) {
427 if (proxyAppState.mHasLocationPermission) {
Anil Admal94ec76a2019-01-15 09:42:01 -0800428 ++countLocationPermissionEnabledProxyApps;
429 }
430 }
431
432 int i = 0;
433 String[] locationPermissionEnabledProxyApps =
434 new String[countLocationPermissionEnabledProxyApps];
Anil Admal74de73e2019-05-14 19:14:23 -0700435 for (Map.Entry<String, ProxyAppState> entry : mProxyAppsState.entrySet()) {
Anil Admal94ec76a2019-01-15 09:42:01 -0800436 final String proxyApp = entry.getKey();
Anil Admal74de73e2019-05-14 19:14:23 -0700437 if (entry.getValue().mHasLocationPermission) {
Anil Admal94ec76a2019-01-15 09:42:01 -0800438 locationPermissionEnabledProxyApps[i++] = proxyApp;
439 }
440 }
Anil Admal204e22e2019-01-22 15:14:17 -0800441 return locationPermissionEnabledProxyApps;
Anil Admal94ec76a2019-01-15 09:42:01 -0800442 }
443
444 private void handleNfwNotification(NfwNotification nfwNotification) {
Anil Admalc25a2712019-02-15 16:48:48 -0800445 if (DEBUG) Log.d(TAG, "Non-framework location access notification: " + nfwNotification);
Anil Admal94ec76a2019-01-15 09:42:01 -0800446
Anil Admalfd9bb8e2019-04-17 18:08:46 -0700447 if (nfwNotification.isEmergencyRequestNotification()) {
448 handleEmergencyNfwNotification(nfwNotification);
449 return;
Yu-Han Yang14d5fb42019-01-16 12:42:59 -0800450 }
Anil Admalfd9bb8e2019-04-17 18:08:46 -0700451
Anil Admal4b6a8302019-05-02 18:38:39 -0700452 final String proxyAppPkgName = nfwNotification.mProxyAppPackageName;
Anil Admal74de73e2019-05-14 19:14:23 -0700453 final ProxyAppState proxyAppState = mProxyAppsState.get(proxyAppPkgName);
Anil Admalfd9bb8e2019-04-17 18:08:46 -0700454 final boolean isLocationRequestAccepted = nfwNotification.isRequestAccepted();
Anil Admal27caa742019-05-30 14:44:52 -0700455 final boolean isPermissionMismatched = isPermissionMismatched(proxyAppState,
456 nfwNotification);
Yu-Han Yang14d5fb42019-01-16 12:42:59 -0800457 logEvent(nfwNotification, isPermissionMismatched);
458
Anil Admalfd9bb8e2019-04-17 18:08:46 -0700459 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"
Anil Admal74de73e2019-05-14 19:14:23 -0700471 + " configured proxy apps: " + mProxyAppsState.size());
Anil Admalfd9bb8e2019-04-17 18:08:46 -0700472 }
473 return;
474 }
475
Anil Admal8a246a22019-05-05 00:34:55 -0700476 Log.e(TAG, "ProxyAppPackageName field is not set. AppOps service not notified"
477 + " for notification: " + nfwNotification);
Anil Admal94ec76a2019-01-15 09:42:01 -0800478 return;
479 }
480
Anil Admal74de73e2019-05-14 19:14:23 -0700481 if (proxyAppState == null) {
Anil Admal8a246a22019-05-05 00:34:55 -0700482 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);
Anil Admal94ec76a2019-01-15 09:42:01 -0800485 return;
486 }
487
488 // Display location icon attributed to this proxy app.
Anil Admal4b6a8302019-05-02 18:38:39 -0700489 final ApplicationInfo proxyAppInfo = getProxyAppInfo(proxyAppPkgName);
490 if (proxyAppInfo == null) {
491 Log.e(TAG, "Proxy app " + proxyAppPkgName + " is not found. AppOps service not "
Anil Admal8a246a22019-05-05 00:34:55 -0700492 + "notified for notification: " + nfwNotification);
Anil Admalc25a2712019-02-15 16:48:48 -0800493 return;
494 }
Anil Admal4b6a8302019-05-02 18:38:39 -0700495
Anil Admal74de73e2019-05-14 19:14:23 -0700496 if (nfwNotification.isLocationProvided()) {
497 showLocationIcon(proxyAppState, nfwNotification, proxyAppInfo.uid, proxyAppPkgName);
498 mAppOps.noteOpNoThrow(AppOpsManager.OP_FINE_LOCATION, proxyAppInfo.uid,
499 proxyAppPkgName);
500 }
Anil Admal94ec76a2019-01-15 09:42:01 -0800501
502 // Log proxy app permission mismatch between framework and GNSS HAL.
Yu-Han Yang14d5fb42019-01-16 12:42:59 -0800503 if (isPermissionMismatched) {
Anil Admal27caa742019-05-30 14:44:52 -0700504 Log.w(TAG, "Permission mismatch. Proxy app " + proxyAppPkgName
Anil Admal74de73e2019-05-14 19:14:23 -0700505 + " location permission is set to " + proxyAppState.mHasLocationPermission
Anil Admal27caa742019-05-30 14:44:52 -0700506 + " and GNSS HAL enabled is set to " + mIsGpsEnabled
Anil Admal94ec76a2019-01-15 09:42:01 -0800507 + " but GNSS non-framework location access response type is "
Anil Admalc25a2712019-02-15 16:48:48 -0800508 + nfwNotification.getResponseTypeAsString() + " for notification: "
509 + nfwNotification);
Anil Admal94ec76a2019-01-15 09:42:01 -0800510 }
511 }
512
Anil Admal27caa742019-05-30 14:44:52 -0700513 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
Anil Admal74de73e2019-05-14 19:14:23 -0700522 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
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
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
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
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
Anil Admalfd9bb8e2019-04-17 18:08:46 -0700599 private void handleEmergencyNfwNotification(NfwNotification nfwNotification) {
Anil Admal8a246a22019-05-05 00:34:55 -0700600 boolean isPermissionMismatched = false;
601 if (!nfwNotification.isRequestAccepted()) {
Anil Admalfd9bb8e2019-04-17 18:08:46 -0700602 Log.e(TAG, "Emergency non-framework location request incorrectly rejected."
603 + " Notification: " + nfwNotification);
Anil Admal8a246a22019-05-05 00:34:55 -0700604 isPermissionMismatched = true;
Anil Admalfd9bb8e2019-04-17 18:08:46 -0700605 }
Anil Admal8a246a22019-05-05 00:34:55 -0700606
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
Anil Admalfd9bb8e2019-04-17 18:08:46 -0700613 logEvent(nfwNotification, isPermissionMismatched);
Anil Admal8a246a22019-05-05 00:34:55 -0700614
Anil Admal61da5f02019-05-17 09:04:13 -0700615 if (nfwNotification.isLocationProvided()) {
Soonil Nagarkarbfef65a2020-04-16 14:18:12 -0700616 displayNfwNotification(nfwNotification);
Anil Admal8a246a22019-05-05 00:34:55 -0700617 }
Anil Admalfd9bb8e2019-04-17 18:08:46 -0700618 }
619
Soonil Nagarkarbfef65a2020-04-16 14:18:12 -0700620 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);
Anil Admal61da5f02019-05-17 09:04:13 -0700630 }
631
Soonil Nagarkarbfef65a2020-04-16 14:18:12 -0700632 Notification.Builder builder = new Notification.Builder(mContext,
633 SystemNotificationChannels.NETWORK_ALERTS)
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))
Anil Admal61da5f02019-05-17 09:04:13 -0700641 .setAutoCancel(true)
Soonil Nagarkarbfef65a2020-04-16 14:18:12 -0700642 .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());
Anil Admal61da5f02019-05-17 09:04:13 -0700648 }
649
Yu-Han Yang14d5fb42019-01-16 12:42:59 -0800650 private void logEvent(NfwNotification notification, boolean isPermissionMismatched) {
Muhammad Qureshi6f207102020-01-28 10:37:41 -0800651 FrameworkStatsLog.write(FrameworkStatsLog.GNSS_NFW_NOTIFICATION_REPORTED,
Yu-Han Yang14d5fb42019-01-16 12:42:59 -0800652 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
Anil Admal204e22e2019-01-22 15:14:17 -0800663 private void runOnHandler(Runnable event) {
Anil Admal94ec76a2019-01-15 09:42:01 -0800664 // 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
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}