blob: c3f25bfa2e5eee94770aeaad04d23b264b5904b1 [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
19import android.annotation.SuppressLint;
20import android.app.AppOpsManager;
21import android.content.ActivityNotFoundException;
Anil Admal204e22e2019-01-22 15:14:17 -080022import android.content.BroadcastReceiver;
Anil Admal94ec76a2019-01-15 09:42:01 -080023import android.content.Context;
24import android.content.Intent;
Anil Admal204e22e2019-01-22 15:14:17 -080025import android.content.IntentFilter;
Anil Admal94ec76a2019-01-15 09:42:01 -080026import android.content.pm.PackageManager;
27import android.os.Handler;
28import android.os.Looper;
29import android.os.PowerManager;
30import android.os.UserHandle;
31import android.text.TextUtils;
32import android.util.Log;
Yu-Han Yang14d5fb42019-01-16 12:42:59 -080033import android.util.StatsLog;
Anil Admal94ec76a2019-01-15 09:42:01 -080034
35import java.util.Arrays;
36import java.util.HashMap;
37import java.util.List;
38import java.util.Map;
39
40/**
41 * Handles GNSS non-framework location access user visibility and control.
42 *
43 * The state of the GnssVisibilityControl object must be accessed/modified through the Handler
44 * thread only.
45 */
46class GnssVisibilityControl {
47 private static final String TAG = "GnssVisibilityControl";
48 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
49
50 // Constants related to non-framework (NFW) location access permission proxy apps.
51 private static final String NFW_PROXY_APP_PKG_ACTIVITY_NAME_SUFFIX =
52 ".NonFrameworkLocationAccessActivity";
53 private static final String NFW_INTENT_ACTION_NFW_LOCATION_ACCESS_SUFFIX =
54 ".intent.action.NON_FRAMEWORK_LOCATION_ACCESS";
55 private static final String NFW_INTENT_TYPE = "text/plain";
56
57 private static final String LOCATION_PERMISSION_NAME =
58 "android.permission.ACCESS_FINE_LOCATION";
59
Anil Admal204e22e2019-01-22 15:14:17 -080060 private static final String[] NO_LOCATION_ENABLED_PROXY_APPS = new String[0];
61
Anil Admal94ec76a2019-01-15 09:42:01 -080062 // Wakelocks
63 private static final String WAKELOCK_KEY = TAG;
64 private static final long WAKELOCK_TIMEOUT_MILLIS = 60 * 1000;
65 private final PowerManager.WakeLock mWakeLock;
66
67 private final AppOpsManager mAppOps;
68 private final PackageManager mPackageManager;
69
70 private final Handler mHandler;
71 private final Context mContext;
72
Anil Admal204e22e2019-01-22 15:14:17 -080073 private boolean mIsMasterLocationSettingsEnabled = true;
Anil Admal204e22e2019-01-22 15:14:17 -080074
Anil Admal94ec76a2019-01-15 09:42:01 -080075 // Number of non-framework location access proxy apps is expected to be small (< 5).
76 private static final int HASH_MAP_INITIAL_CAPACITY_PROXY_APP_TO_LOCATION_PERMISSIONS = 7;
77 private HashMap<String, Boolean> mProxyAppToLocationPermissions = new HashMap<>(
78 HASH_MAP_INITIAL_CAPACITY_PROXY_APP_TO_LOCATION_PERMISSIONS);
79
80 private PackageManager.OnPermissionsChangedListener mOnPermissionsChangedListener =
Anil Admal204e22e2019-01-22 15:14:17 -080081 uid -> runOnHandler(() -> handlePermissionsChanged(uid));
Anil Admal94ec76a2019-01-15 09:42:01 -080082
83 GnssVisibilityControl(Context context, Looper looper) {
84 mContext = context;
85 PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
86 mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_KEY);
87 mHandler = new Handler(looper);
88 mAppOps = mContext.getSystemService(AppOpsManager.class);
89 mPackageManager = mContext.getPackageManager();
Anil Admal204e22e2019-01-22 15:14:17 -080090
91 // Set to empty proxy app list initially until the configuration properties are loaded.
92 updateNfwLocationAccessProxyAppsInGnssHal();
93
94 // Listen for proxy app package installation, removal events.
95 listenForProxyAppsPackageUpdates();
Anil Admal204e22e2019-01-22 15:14:17 -080096
Anil Admal94ec76a2019-01-15 09:42:01 -080097 // TODO(b/122855984): Handle global location settings on/off.
Anil Admal94ec76a2019-01-15 09:42:01 -080098 }
99
100 void updateProxyApps(List<String> nfwLocationAccessProxyApps) {
101 // NOTE: This class doesn't explicitly register and listen for SIM_STATE_CHANGED event
102 // but rather piggy backs on the GnssLocationProvider SIM_STATE_CHANGED handling
103 // so that the order of processing is preserved. GnssLocationProvider should
104 // first load the new config parameters for the new SIM and then call this method.
Anil Admal204e22e2019-01-22 15:14:17 -0800105 runOnHandler(() -> handleUpdateProxyApps(nfwLocationAccessProxyApps));
106 }
107
108 void masterLocationSettingsUpdated(boolean enabled) {
109 runOnHandler(() -> handleMasterLocationSettingsUpdated(enabled));
Anil Admal94ec76a2019-01-15 09:42:01 -0800110 }
111
112 void reportNfwNotification(String proxyAppPackageName, byte protocolStack,
113 String otherProtocolStackName, byte requestor, String requestorId, byte responseType,
114 boolean inEmergencyMode, boolean isCachedLocation) {
Anil Admal204e22e2019-01-22 15:14:17 -0800115 runOnHandler(() -> handleNfwNotification(
Anil Admal94ec76a2019-01-15 09:42:01 -0800116 new NfwNotification(proxyAppPackageName, protocolStack, otherProtocolStackName,
117 requestor, requestorId, responseType, inEmergencyMode, isCachedLocation)));
118 }
119
Anil Admal204e22e2019-01-22 15:14:17 -0800120 private void listenForProxyAppsPackageUpdates() {
121 IntentFilter intentFilter = new IntentFilter();
122 intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
123 intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
124 intentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED);
125 intentFilter.addDataScheme("package");
126 mContext.registerReceiverAsUser(new BroadcastReceiver() {
127 @Override
128 public void onReceive(Context context, Intent intent) {
129 String action = intent.getAction();
130 if (action == null) {
131 return;
132 }
133
134 switch (action) {
135 case Intent.ACTION_PACKAGE_ADDED:
136 case Intent.ACTION_PACKAGE_REMOVED:
137 case Intent.ACTION_PACKAGE_REPLACED:
138 String pkgName = intent.getData().getEncodedSchemeSpecificPart();
139 handleProxyAppPackageUpdate(pkgName, action);
140 break;
141 }
142 }
143 }, UserHandle.ALL, intentFilter, null, mHandler);
144 }
145
146 private void handleProxyAppPackageUpdate(String pkgName, String action) {
147 final Boolean locationPermission = mProxyAppToLocationPermissions.get(pkgName);
148 // pkgName is not one of the proxy apps in our list.
149 if (locationPermission == null) {
150 return;
151 }
152
153 Log.i(TAG, "Proxy app " + pkgName + " package changed: " + action);
154 final boolean updatedLocationPermission = hasLocationPermission(pkgName);
155 if (locationPermission != updatedLocationPermission) {
156 // Permission changed. So, update the GNSS HAL with the updated list.
157 mProxyAppToLocationPermissions.put(pkgName, updatedLocationPermission);
158 updateNfwLocationAccessProxyAppsInGnssHal();
159 }
160 }
161
162 private void handleUpdateProxyApps(List<String> nfwLocationAccessProxyApps) {
163 if (!isProxyAppListUpdated(nfwLocationAccessProxyApps)) {
164 return;
165 }
166
Anil Admal94ec76a2019-01-15 09:42:01 -0800167 if (nfwLocationAccessProxyApps.isEmpty()) {
168 // Stop listening for app permission changes. Clear the app list in GNSS HAL.
169 if (!mProxyAppToLocationPermissions.isEmpty()) {
170 mPackageManager.removeOnPermissionsChangeListener(mOnPermissionsChangedListener);
171 mProxyAppToLocationPermissions.clear();
172 updateNfwLocationAccessProxyAppsInGnssHal();
173 }
174 return;
175 }
176
177 if (mProxyAppToLocationPermissions.isEmpty()) {
178 mPackageManager.addOnPermissionsChangeListener(mOnPermissionsChangedListener);
179 } else {
180 mProxyAppToLocationPermissions.clear();
181 }
182
183 for (String proxApp : nfwLocationAccessProxyApps) {
184 mProxyAppToLocationPermissions.put(proxApp, hasLocationPermission(proxApp));
185 }
186
187 updateNfwLocationAccessProxyAppsInGnssHal();
188 }
189
Anil Admal204e22e2019-01-22 15:14:17 -0800190 private boolean isProxyAppListUpdated(List<String> nfwLocationAccessProxyApps) {
191 if (nfwLocationAccessProxyApps.size() != mProxyAppToLocationPermissions.size()) {
192 return true;
193 }
194
195 for (String nfwLocationAccessProxyApp : nfwLocationAccessProxyApps) {
196 if (!mProxyAppToLocationPermissions.containsKey(nfwLocationAccessProxyApp)) {
197 return true;
198 }
199 }
200
201 return false;
202 }
203
204 private void handleMasterLocationSettingsUpdated(boolean enabled) {
205 mIsMasterLocationSettingsEnabled = enabled;
206 Log.i(TAG, "Master location settings switch changed to "
207 + (enabled ? "enabled" : "disabled"));
208 updateNfwLocationAccessProxyAppsInGnssHal();
209 }
210
Anil Admal94ec76a2019-01-15 09:42:01 -0800211 // Represents NfwNotification structure in IGnssVisibilityControlCallback.hal
212 private static class NfwNotification {
213 private static final String KEY_PROTOCOL_STACK = "ProtocolStack";
214 private static final String KEY_OTHER_PROTOCOL_STACK_NAME = "OtherProtocolStackName";
215 private static final String KEY_REQUESTOR = "Requestor";
216 private static final String KEY_REQUESTOR_ID = "RequestorId";
217 private static final String KEY_RESPONSE_TYPE = "ResponseType";
218 private static final String KEY_IN_EMERGENCY_MODE = "InEmergencyMode";
219 private static final String KEY_IS_CACHED_LOCATION = "IsCachedLocation";
220
221 // This must match with NfwResponseType enum in IGnssVisibilityControlCallback.hal.
222 private static final byte NFW_RESPONSE_TYPE_REJECTED = 0;
223 private static final byte NFW_RESPONSE_TYPE_ACCEPTED_NO_LOCATION_PROVIDED = 1;
224 private static final byte NFW_RESPONSE_TYPE_ACCEPTED_LOCATION_PROVIDED = 2;
225
226 private final String mProxyAppPackageName;
227 private final byte mProtocolStack;
228 private final String mOtherProtocolStackName;
229 private final byte mRequestor;
230 private final String mRequestorId;
231 private final byte mResponseType;
232 private final boolean mInEmergencyMode;
233 private final boolean mIsCachedLocation;
234
Anil Admal204e22e2019-01-22 15:14:17 -0800235 private NfwNotification(String proxyAppPackageName, byte protocolStack,
Anil Admal94ec76a2019-01-15 09:42:01 -0800236 String otherProtocolStackName, byte requestor, String requestorId,
237 byte responseType, boolean inEmergencyMode, boolean isCachedLocation) {
238 mProxyAppPackageName = proxyAppPackageName;
239 mProtocolStack = protocolStack;
240 mOtherProtocolStackName = otherProtocolStackName;
241 mRequestor = requestor;
242 mRequestorId = requestorId;
243 mResponseType = responseType;
244 mInEmergencyMode = inEmergencyMode;
245 mIsCachedLocation = isCachedLocation;
246 }
247
Anil Admal204e22e2019-01-22 15:14:17 -0800248 private void copyFieldsToIntent(Intent intent) {
Anil Admal94ec76a2019-01-15 09:42:01 -0800249 intent.putExtra(KEY_PROTOCOL_STACK, mProtocolStack);
250 if (!TextUtils.isEmpty(mOtherProtocolStackName)) {
251 intent.putExtra(KEY_OTHER_PROTOCOL_STACK_NAME, mOtherProtocolStackName);
252 }
253 intent.putExtra(KEY_REQUESTOR, mRequestor);
254 if (!TextUtils.isEmpty(mRequestorId)) {
255 intent.putExtra(KEY_REQUESTOR_ID, mRequestorId);
256 }
257 intent.putExtra(KEY_RESPONSE_TYPE, mResponseType);
258 intent.putExtra(KEY_IN_EMERGENCY_MODE, mInEmergencyMode);
259 if (mResponseType == NFW_RESPONSE_TYPE_ACCEPTED_LOCATION_PROVIDED) {
260 intent.putExtra(KEY_IS_CACHED_LOCATION, mIsCachedLocation);
261 }
262 }
263
264 @SuppressLint("DefaultLocale")
265 public String toString() {
266 return String.format(
267 "[Notification] proxyAppPackageName: %s, protocolStack: %d"
268 + ", otherProtocolStackName: %s, requestor: %d, requestorId: %s"
269 + ", responseType: %d, inEmergencyMode: %b, isCachedLocation: %b",
270 mProxyAppPackageName, mProtocolStack, mOtherProtocolStackName,
271 mRequestor, mRequestorId, mResponseType, mInEmergencyMode, mIsCachedLocation);
272 }
273
Anil Admal204e22e2019-01-22 15:14:17 -0800274 private String getResponseTypeAsString() {
Anil Admal94ec76a2019-01-15 09:42:01 -0800275 switch (mResponseType) {
276 case NFW_RESPONSE_TYPE_REJECTED:
277 return "REJECTED";
278 case NFW_RESPONSE_TYPE_ACCEPTED_NO_LOCATION_PROVIDED:
279 return "ACCEPTED_NO_LOCATION_PROVIDED";
280 case NFW_RESPONSE_TYPE_ACCEPTED_LOCATION_PROVIDED:
281 return "ACCEPTED_LOCATION_PROVIDED";
282 default:
283 return "<Unknown>";
284 }
285 }
286 }
287
288 private void handlePermissionsChanged(int uid) {
289 if (mProxyAppToLocationPermissions.isEmpty()) {
290 return;
291 }
292
293 for (Map.Entry<String, Boolean> entry : mProxyAppToLocationPermissions.entrySet()) {
294 // Cannot cache uid since the application could be uninstalled and reinstalled.
295 final String proxyApp = entry.getKey();
296 final Integer nfwProxyAppUid = getApplicationUid(proxyApp);
297 if (nfwProxyAppUid == null || nfwProxyAppUid != uid) {
298 continue;
299 }
300
301 final boolean isLocationPermissionEnabled = hasLocationPermission(proxyApp);
302 if (isLocationPermissionEnabled != entry.getValue()) {
303 Log.i(TAG, "Location permission setting is changed to "
304 + (isLocationPermissionEnabled ? "enabled" : "disabled")
305 + " for non-framework location access proxy app "
306 + proxyApp);
307 entry.setValue(isLocationPermissionEnabled);
308 updateNfwLocationAccessProxyAppsInGnssHal();
309 return;
310 }
311 }
312 }
313
314 private Integer getApplicationUid(String pkgName) {
315 try {
316 return mPackageManager.getApplicationInfo(pkgName, 0).uid;
317 } catch (PackageManager.NameNotFoundException e) {
318 if (DEBUG) {
319 Log.d(TAG, "Non-framework location access proxy app "
320 + pkgName + " is not found.");
321 }
322 return null;
323 }
324 }
325
326 private boolean hasLocationPermission(String pkgName) {
327 return mPackageManager.checkPermission(LOCATION_PERMISSION_NAME, pkgName)
328 == PackageManager.PERMISSION_GRANTED;
329 }
330
331 private void updateNfwLocationAccessProxyAppsInGnssHal() {
Anil Admal204e22e2019-01-22 15:14:17 -0800332 final String[] locationPermissionEnabledProxyApps = shouldDisableNfwLocationAccess()
333 ? NO_LOCATION_ENABLED_PROXY_APPS : getLocationPermissionEnabledProxyApps();
334 final String proxyAppsStr = Arrays.toString(locationPermissionEnabledProxyApps);
335 Log.i(TAG, "Updating non-framework location access proxy apps in the GNSS HAL to: "
336 + proxyAppsStr);
337 boolean result = native_enable_nfw_location_access(locationPermissionEnabledProxyApps);
338 if (!result) {
339 Log.e(TAG, "Failed to update non-framework location access proxy apps in the"
340 + " GNSS HAL to: " + proxyAppsStr);
341 }
342 }
343
344 private boolean shouldDisableNfwLocationAccess() {
Anil Admal4374b212019-01-28 08:45:10 -0800345 return !mIsMasterLocationSettingsEnabled;
Anil Admal204e22e2019-01-22 15:14:17 -0800346 }
347
348 private String[] getLocationPermissionEnabledProxyApps() {
Anil Admal94ec76a2019-01-15 09:42:01 -0800349 // Get a count of proxy apps with location permission enabled to array creation size.
350 int countLocationPermissionEnabledProxyApps = 0;
351 for (Boolean hasLocationPermissionEnabled : mProxyAppToLocationPermissions.values()) {
352 if (hasLocationPermissionEnabled) {
353 ++countLocationPermissionEnabledProxyApps;
354 }
355 }
356
357 int i = 0;
358 String[] locationPermissionEnabledProxyApps =
359 new String[countLocationPermissionEnabledProxyApps];
360 for (Map.Entry<String, Boolean> entry : mProxyAppToLocationPermissions.entrySet()) {
361 final String proxyApp = entry.getKey();
362 final boolean hasLocationPermissionEnabled = entry.getValue();
363 if (hasLocationPermissionEnabled) {
364 locationPermissionEnabledProxyApps[i++] = proxyApp;
365 }
366 }
Anil Admal204e22e2019-01-22 15:14:17 -0800367 return locationPermissionEnabledProxyApps;
Anil Admal94ec76a2019-01-15 09:42:01 -0800368 }
369
370 private void handleNfwNotification(NfwNotification nfwNotification) {
371 if (DEBUG) Log.d(TAG, nfwNotification.toString());
372
373 final String proxyAppPackageName = nfwNotification.mProxyAppPackageName;
Yu-Han Yang14d5fb42019-01-16 12:42:59 -0800374 Boolean isLocationPermissionEnabled = mProxyAppToLocationPermissions.get(
375 proxyAppPackageName);
376 boolean isLocationRequestAccepted =
377 nfwNotification.mResponseType != NfwNotification.NFW_RESPONSE_TYPE_REJECTED;
378 boolean isPermissionMismatched;
379 if (isLocationPermissionEnabled == null) {
380 isPermissionMismatched = isLocationRequestAccepted;
381 } else {
382 isPermissionMismatched = (isLocationPermissionEnabled != isLocationRequestAccepted);
383 }
384 logEvent(nfwNotification, isPermissionMismatched);
385
Anil Admal94ec76a2019-01-15 09:42:01 -0800386 if (TextUtils.isEmpty(proxyAppPackageName)) {
387 Log.e(TAG, "ProxyAppPackageName field is not set. Not sending intent to proxy app for "
388 + nfwNotification);
389 return;
390 }
391
Anil Admal94ec76a2019-01-15 09:42:01 -0800392 if (isLocationPermissionEnabled == null) {
393 // App is not in the configured list.
394 Log.e(TAG, "Could not find proxy app with name: " + proxyAppPackageName + " in the "
395 + "value specified for config parameter: "
396 + GnssConfiguration.CONFIG_NFW_PROXY_APPS + ". Not sending intent to proxy app"
397 + " for " + nfwNotification);
398 return;
399 }
400
401 // Send intent to non-framework location proxy app with notification information.
402 final Intent intent = new Intent(
403 proxyAppPackageName + NFW_INTENT_ACTION_NFW_LOCATION_ACCESS_SUFFIX);
404 final String proxAppActivityName =
405 proxyAppPackageName + NFW_PROXY_APP_PKG_ACTIVITY_NAME_SUFFIX;
406 intent.setClassName(proxyAppPackageName, proxAppActivityName);
407 intent.setType(NFW_INTENT_TYPE);
408 nfwNotification.copyFieldsToIntent(intent);
409
410 // Check if the proxy app is still installed.
411 final Integer clsAppUid = getApplicationUid(proxyAppPackageName);
412 if (clsAppUid == null || intent.resolveActivity(mPackageManager) == null) {
413 Log.i(TAG, "Proxy application " + proxyAppPackageName + " and/or activity "
414 + proxAppActivityName + " is not found. Not sending"
415 + " intent to proxy app for " + nfwNotification);
416 return;
417 }
418
419 // Display location icon attributed to this proxy app.
420 mAppOps.noteOpNoThrow(AppOpsManager.OP_FINE_LOCATION, clsAppUid, proxyAppPackageName);
421
422 // Log proxy app permission mismatch between framework and GNSS HAL.
Yu-Han Yang14d5fb42019-01-16 12:42:59 -0800423 if (isPermissionMismatched) {
Anil Admal94ec76a2019-01-15 09:42:01 -0800424 Log.w(TAG, "Permission mismatch. Framework proxy app " + proxyAppPackageName
425 + " location permission is set to " + isLocationPermissionEnabled
426 + " but GNSS non-framework location access response type is "
427 + nfwNotification.getResponseTypeAsString() + " for " + nfwNotification);
428 }
429
430 // Notify proxy app.
431 try {
432 if (DEBUG) {
433 Log.d(TAG, "Sending non-framework location access notification intent: " + intent);
434 }
435 mContext.startActivityAsUser(intent, UserHandle.getUserHandleForUid(clsAppUid));
436 } catch (ActivityNotFoundException e) {
437 Log.w(TAG, "Activity not found. Failed to send non-framework location access"
438 + " notification intent to proxy app activity: " + proxAppActivityName);
439 }
440 }
441
Yu-Han Yang14d5fb42019-01-16 12:42:59 -0800442 private void logEvent(NfwNotification notification, boolean isPermissionMismatched) {
443 StatsLog.write(StatsLog.GNSS_NFW_NOTIFICATION_REPORTED,
444 notification.mProxyAppPackageName,
445 notification.mProtocolStack,
446 notification.mOtherProtocolStackName,
447 notification.mRequestor,
448 notification.mRequestorId,
449 notification.mResponseType,
450 notification.mInEmergencyMode,
451 notification.mIsCachedLocation,
452 isPermissionMismatched);
453 }
454
Anil Admal204e22e2019-01-22 15:14:17 -0800455 private void runOnHandler(Runnable event) {
Anil Admal94ec76a2019-01-15 09:42:01 -0800456 // Hold a wake lock until this message is delivered.
457 // Note that this assumes the message will not be removed from the queue before
458 // it is handled (otherwise the wake lock would be leaked).
459 mWakeLock.acquire(WAKELOCK_TIMEOUT_MILLIS);
460 if (!mHandler.post(runEventAndReleaseWakeLock(event))) {
461 mWakeLock.release();
462 }
463 }
464
465 private Runnable runEventAndReleaseWakeLock(Runnable event) {
466 return () -> {
467 try {
468 event.run();
469 } finally {
470 mWakeLock.release();
471 }
472 };
473 }
474
475 private native boolean native_enable_nfw_location_access(String[] proxyApps);
476}