/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.statusbar.policy; import static android.app.AppOpsManager.OP_COARSE_LOCATION; import static android.app.AppOpsManager.OP_FINE_LOCATION; import static android.app.AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION; import static com.android.settingslib.Utils.updateLocationEnabled; import android.app.AppOpsManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.PermissionChecker; import android.content.pm.PackageManager; import android.content.pm.UserInfo; import android.database.ContentObserver; import android.location.LocationManager; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.UserHandle; import android.os.UserManager; import android.provider.DeviceConfig; import android.provider.Settings; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.logging.UiEvent; import com.android.internal.logging.UiEventLogger; import com.android.systemui.BootCompleteCache; import com.android.systemui.appops.AppOpItem; import com.android.systemui.appops.AppOpsController; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.settings.UserTracker; import com.android.systemui.util.DeviceConfigProxy; import com.android.systemui.util.settings.SecureSettings; import java.util.ArrayList; import java.util.List; import javax.inject.Inject; /** * A controller to manage changes of location related states and update the views accordingly. */ @SysUISingleton public class LocationControllerImpl extends BroadcastReceiver implements LocationController, AppOpsController.Callback { private final Context mContext; private final AppOpsController mAppOpsController; private final DeviceConfigProxy mDeviceConfigProxy; private final BootCompleteCache mBootCompleteCache; private final UserTracker mUserTracker; private final UiEventLogger mUiEventLogger; private final H mHandler; private final Handler mBackgroundHandler; private final PackageManager mPackageManager; private final ContentObserver mContentObserver; private final SecureSettings mSecureSettings; private boolean mAreActiveLocationRequests; private boolean mShouldDisplayAllAccesses; private boolean mShowSystemAccessesFlag; private boolean mShowSystemAccessesSetting; @Inject public LocationControllerImpl(Context context, AppOpsController appOpsController, DeviceConfigProxy deviceConfigProxy, @Main Looper mainLooper, @Background Handler backgroundHandler, BroadcastDispatcher broadcastDispatcher, BootCompleteCache bootCompleteCache, UserTracker userTracker, PackageManager packageManager, UiEventLogger uiEventLogger, SecureSettings secureSettings) { mContext = context; mAppOpsController = appOpsController; mDeviceConfigProxy = deviceConfigProxy; mBootCompleteCache = bootCompleteCache; mHandler = new H(mainLooper); mUserTracker = userTracker; mUiEventLogger = uiEventLogger; mSecureSettings = secureSettings; mBackgroundHandler = backgroundHandler; mPackageManager = packageManager; mShouldDisplayAllAccesses = getAllAccessesSetting(); mShowSystemAccessesFlag = getShowSystemFlag(); mShowSystemAccessesSetting = getShowSystemSetting(); mContentObserver = new ContentObserver(mBackgroundHandler) { @Override public void onChange(boolean selfChange) { mShowSystemAccessesSetting = getShowSystemSetting(); } }; // Register to listen for changes in Settings.Secure settings. mSecureSettings.registerContentObserverForUser( Settings.Secure.LOCATION_SHOW_SYSTEM_OPS, mContentObserver, UserHandle.USER_ALL); // Register to listen for changes in DeviceConfig settings. mDeviceConfigProxy.addOnPropertiesChangedListener( DeviceConfig.NAMESPACE_PRIVACY, backgroundHandler::post, properties -> { mShouldDisplayAllAccesses = getAllAccessesSetting(); mShowSystemAccessesFlag = getShowSystemSetting(); updateActiveLocationRequests(); }); // Register to listen for changes in location settings. IntentFilter filter = new IntentFilter(); filter.addAction(LocationManager.MODE_CHANGED_ACTION); broadcastDispatcher.registerReceiverWithHandler(this, filter, mHandler, UserHandle.ALL); // Listen to all accesses and filter the ones interested in based on flags. mAppOpsController.addCallback( new int[]{OP_COARSE_LOCATION, OP_FINE_LOCATION, OP_MONITOR_HIGH_POWER_LOCATION}, this); // Examine the current location state and initialize the status view. backgroundHandler.post(this::updateActiveLocationRequests); } /** * Add a callback to listen for changes in location settings. */ @Override public void addCallback(@NonNull LocationChangeCallback cb) { mHandler.obtainMessage(H.MSG_ADD_CALLBACK, cb).sendToTarget(); mHandler.sendEmptyMessage(H.MSG_LOCATION_SETTINGS_CHANGED); } @Override public void removeCallback(@NonNull LocationChangeCallback cb) { mHandler.obtainMessage(H.MSG_REMOVE_CALLBACK, cb).sendToTarget(); } /** * Enable or disable location in settings. * *

This will attempt to enable/disable every type of location setting * (e.g. high and balanced power). * *

If enabling, a user consent dialog will pop up prompting the user to accept. * If the user doesn't accept, network location won't be enabled. * * @return true if attempt to change setting was successful. */ public boolean setLocationEnabled(boolean enabled) { // QuickSettings always runs as the owner, so specifically set the settings // for the current foreground user. int currentUserId = mUserTracker.getUserId(); if (isUserLocationRestricted(currentUserId)) { return false; } // When enabling location, a user consent dialog will pop up, and the // setting won't be fully enabled until the user accepts the agreement. updateLocationEnabled(mContext, enabled, currentUserId, Settings.Secure.LOCATION_CHANGER_QUICK_SETTINGS); return true; } /** * Returns true if location is enabled in settings. Will return false if * {@link LocationManager} service has not been completely initialized */ public boolean isLocationEnabled() { // QuickSettings always runs as the owner, so specifically retrieve the settings // for the current foreground user. LocationManager locationManager = (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE); return mBootCompleteCache.isBootComplete() && locationManager.isLocationEnabledForUser( mUserTracker.getUserHandle()); } @Override public boolean isLocationActive() { return mAreActiveLocationRequests; } /** * Returns true if the current user is restricted from using location. */ private boolean isUserLocationRestricted(int userId) { final UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE); return um.hasUserRestriction(UserManager.DISALLOW_SHARE_LOCATION, UserHandle.of(userId)); } private boolean getAllAccessesSetting() { return mDeviceConfigProxy.getBoolean(DeviceConfig.NAMESPACE_PRIVACY, SystemUiDeviceConfigFlags.PROPERTY_LOCATION_INDICATORS_SMALL_ENABLED, false); } private boolean getShowSystemFlag() { return mDeviceConfigProxy.getBoolean(DeviceConfig.NAMESPACE_PRIVACY, SystemUiDeviceConfigFlags.PROPERTY_LOCATION_INDICATORS_SHOW_SYSTEM, false); } private boolean getShowSystemSetting() { return mSecureSettings.getIntForUser(Settings.Secure.LOCATION_SHOW_SYSTEM_OPS, 0, UserHandle.USER_CURRENT) == 1; } /** * Returns true if there currently exist active high power location requests. */ @VisibleForTesting protected boolean areActiveHighPowerLocationRequests() { List appOpsItems = mAppOpsController.getActiveAppOps(); final int numItems = appOpsItems.size(); for (int i = 0; i < numItems; i++) { if (appOpsItems.get(i).getCode() == OP_MONITOR_HIGH_POWER_LOCATION) { return true; } } return false; } /** * Returns true if there currently exist active location requests. */ @VisibleForTesting protected void areActiveLocationRequests() { if (!mShouldDisplayAllAccesses) { return; } boolean hadActiveLocationRequests = mAreActiveLocationRequests; boolean shouldDisplay = false; boolean showSystem = mShowSystemAccessesFlag || mShowSystemAccessesSetting; boolean systemAppOp = false; boolean nonSystemAppOp = false; boolean isSystemApp; List appOpsItems = mAppOpsController.getActiveAppOps(); final List profiles = mUserTracker.getUserProfiles(); final int numItems = appOpsItems.size(); for (int i = 0; i < numItems; i++) { if (appOpsItems.get(i).getCode() == OP_FINE_LOCATION || appOpsItems.get(i).getCode() == OP_COARSE_LOCATION) { isSystemApp = isSystemApp(profiles, appOpsItems.get(i)); if (isSystemApp) { systemAppOp = true; } else { nonSystemAppOp = true; } shouldDisplay = showSystem || shouldDisplay || !isSystemApp; } } boolean highPowerOp = areActiveHighPowerLocationRequests(); mAreActiveLocationRequests = shouldDisplay; if (mAreActiveLocationRequests != hadActiveLocationRequests) { mHandler.sendEmptyMessage(H.MSG_LOCATION_ACTIVE_CHANGED); } // Log each of the types of location access that would cause the location indicator to be // shown, regardless of device's setting state. This is used to understand how often a // user would see the location indicator based on any settings state the device could be in. if (!hadActiveLocationRequests && (highPowerOp || systemAppOp || nonSystemAppOp)) { if (highPowerOp) { mUiEventLogger.log( LocationIndicatorEvent.LOCATION_INDICATOR_MONITOR_HIGH_POWER); } if (systemAppOp) { mUiEventLogger.log(LocationIndicatorEvent.LOCATION_INDICATOR_SYSTEM_APP); } if (nonSystemAppOp) { mUiEventLogger.log(LocationIndicatorEvent.LOCATION_INDICATOR_NON_SYSTEM_APP); } } } private boolean isSystemApp(List profiles, AppOpItem item) { final String permission = AppOpsManager.opToPermission(item.getCode()); UserHandle user = UserHandle.getUserHandleForUid(item.getUid()); // Don't show apps belonging to background users except managed users. boolean foundUser = false; final int numProfiles = profiles.size(); for (int i = 0; i < numProfiles; i++) { if (profiles.get(i).getUserHandle().equals(user)) { foundUser = true; } } if (!foundUser) { return true; } final int permissionFlags = mPackageManager.getPermissionFlags( permission, item.getPackageName(), user); if (PermissionChecker.checkPermissionForPreflight(mContext, permission, PermissionChecker.PID_UNKNOWN, item.getUid(), item.getPackageName()) == PermissionChecker.PERMISSION_GRANTED) { return (permissionFlags & PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED) == 0; } else { return (permissionFlags & PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_DENIED) == 0; } } // Reads the active location requests from either OP_MONITOR_HIGH_POWER_LOCATION, // OP_FINE_LOCATION, or OP_COARSE_LOCATION and updates the status view if necessary. private void updateActiveLocationRequests() { if (mShouldDisplayAllAccesses) { mBackgroundHandler.post(this::areActiveLocationRequests); } else { boolean hadActiveLocationRequests = mAreActiveLocationRequests; mAreActiveLocationRequests = areActiveHighPowerLocationRequests(); if (mAreActiveLocationRequests != hadActiveLocationRequests) { mHandler.sendEmptyMessage(H.MSG_LOCATION_ACTIVE_CHANGED); if (mAreActiveLocationRequests) { // Log that the indicator was shown for a high power op. mUiEventLogger.log( LocationIndicatorEvent.LOCATION_INDICATOR_MONITOR_HIGH_POWER); } } } } @Override public void onReceive(Context context, Intent intent) { if (LocationManager.MODE_CHANGED_ACTION.equals(intent.getAction())) { mHandler.locationSettingsChanged(); } } @Override public void onActiveStateChanged(int code, int uid, String packageName, boolean active) { updateActiveLocationRequests(); } private final class H extends Handler { private static final int MSG_LOCATION_SETTINGS_CHANGED = 1; private static final int MSG_LOCATION_ACTIVE_CHANGED = 2; private static final int MSG_ADD_CALLBACK = 3; private static final int MSG_REMOVE_CALLBACK = 4; private final ArrayList mSettingsChangeCallbacks = new ArrayList<>(); H(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_LOCATION_SETTINGS_CHANGED: locationSettingsChanged(); break; case MSG_LOCATION_ACTIVE_CHANGED: locationActiveChanged(); break; case MSG_ADD_CALLBACK: mSettingsChangeCallbacks.add((LocationChangeCallback) msg.obj); break; case MSG_REMOVE_CALLBACK: mSettingsChangeCallbacks.remove((LocationChangeCallback) msg.obj); break; } } private void locationActiveChanged() { synchronized (mSettingsChangeCallbacks) { final int n = mSettingsChangeCallbacks.size(); for (int i = 0; i < n; i++) { mSettingsChangeCallbacks.get(i) .onLocationActiveChanged(mAreActiveLocationRequests); } } } private void locationSettingsChanged() { boolean isEnabled = isLocationEnabled(); synchronized (mSettingsChangeCallbacks) { final int n = mSettingsChangeCallbacks.size(); for (int i = 0; i < n; i++) { mSettingsChangeCallbacks.get(i).onLocationSettingsChanged(isEnabled); } } } } /** * Enum for events which prompt the location indicator to appear. */ enum LocationIndicatorEvent implements UiEventLogger.UiEventEnum { @UiEvent(doc = "Location indicator shown for high power access") LOCATION_INDICATOR_MONITOR_HIGH_POWER(935), @UiEvent(doc = "Location indicator shown for system app access") LOCATION_INDICATOR_SYSTEM_APP(936), @UiEvent(doc = "Location indicator shown for non system app access") LOCATION_INDICATOR_NON_SYSTEM_APP(937); private final int mId; LocationIndicatorEvent(int id) { mId = id; } @Override public int getId() { return mId; } } }