Merge "Make Location Settings multiprofile aware" into lmp-mr1-dev

This commit is contained in:
Zoltan Szatmary-Ban
2014-12-08 12:50:10 +00:00
committed by Android (Google) Code Review
6 changed files with 207 additions and 68 deletions

View File

@@ -26,6 +26,21 @@
settings:keywords="@string/keywords_location_mode"
android:summary="@string/location_mode_location_off_title" />
<!-- This preference category gets removed if there is no managed profile -->
<PreferenceCategory
android:key="managed_profile_location_category"
android:title="@string/managed_profile_location_category">
<Preference
android:key="managed_profile_location_switch"
android:title="@string/managed_profile_location_switch_title"
android:summary="@string/managed_profile_location_switch_lockdown"
android:persistent="false"
android:enabled="false"
android:selectable="false" />
</PreferenceCategory>
<PreferenceCategory
android:key="recent_location_requests"
android:title="@string/location_category_recent_location_requests" />

View File

@@ -16,10 +16,14 @@
package com.android.settings.location;
import android.annotation.Nullable;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.preference.Preference;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
/**
* A preference item that can dim the icon when it's disabled, either directly or because its parent
@@ -29,16 +33,23 @@ public class DimmableIconPreference extends Preference {
private static final int ICON_ALPHA_ENABLED = 255;
private static final int ICON_ALPHA_DISABLED = 102;
public DimmableIconPreference(Context context, AttributeSet attrs, int defStyle) {
private final CharSequence mContentDescription;
public DimmableIconPreference(Context context, AttributeSet attrs, int defStyle,
@Nullable CharSequence contentDescription) {
super(context, attrs, defStyle);
mContentDescription = contentDescription;
}
public DimmableIconPreference(Context context, AttributeSet attrs) {
public DimmableIconPreference(Context context, AttributeSet attrs,
@Nullable CharSequence contentDescription) {
super(context, attrs);
mContentDescription = contentDescription;
}
public DimmableIconPreference(Context context) {
public DimmableIconPreference(Context context, @Nullable CharSequence contentDescription) {
super(context);
mContentDescription = contentDescription;
}
private void dimIcon(boolean dimmed) {
@@ -60,4 +71,13 @@ public class DimmableIconPreference extends Preference {
dimIcon(!enabled);
super.setEnabled(enabled);
}
@Override
protected void onBindView(View view) {
super.onBindView(view);
if (!TextUtils.isEmpty(mContentDescription)) {
final TextView titleView = (TextView) view.findViewById(android.R.id.title);
titleView.setContentDescription(mContentDescription);
}
}
}

View File

@@ -19,6 +19,7 @@ package com.android.settings.location;
import android.content.Intent;
import android.text.TextUtils;
import android.util.Log;
import android.os.UserHandle;
import com.android.internal.annotations.Immutable;
import com.android.internal.util.Preconditions;
@@ -52,6 +53,11 @@ class InjectedSetting {
*/
public final int iconId;
/**
* The user/profile associated with this setting (e.g. managed profile)
*/
public final UserHandle mUserHandle;
/**
* The activity to launch to allow the user to modify the settings value. Assumed to be in the
* {@link #packageName} package.
@@ -59,11 +65,12 @@ class InjectedSetting {
public final String settingsActivity;
private InjectedSetting(String packageName, String className,
String title, int iconId, String settingsActivity) {
String title, int iconId, UserHandle userHandle, String settingsActivity) {
this.packageName = Preconditions.checkNotNull(packageName, "packageName");
this.className = Preconditions.checkNotNull(className, "className");
this.title = Preconditions.checkNotNull(title, "title");
this.iconId = iconId;
this.mUserHandle = userHandle;
this.settingsActivity = Preconditions.checkNotNull(settingsActivity);
}
@@ -71,7 +78,7 @@ class InjectedSetting {
* Returns a new instance, or null.
*/
public static InjectedSetting newInstance(String packageName, String className,
String title, int iconId, String settingsActivity) {
String title, int iconId, UserHandle userHandle, String settingsActivity) {
if (packageName == null || className == null ||
TextUtils.isEmpty(title) || TextUtils.isEmpty(settingsActivity)) {
if (Log.isLoggable(SettingsInjector.TAG, Log.WARN)) {
@@ -81,7 +88,8 @@ class InjectedSetting {
}
return null;
}
return new InjectedSetting(packageName, className, title, iconId, settingsActivity);
return new InjectedSetting(packageName, className, title, iconId, userHandle,
settingsActivity);
}
@Override
@@ -91,6 +99,7 @@ class InjectedSetting {
", mClassName='" + className + '\'' +
", label=" + title +
", iconId=" + iconId +
", userId=" + mUserHandle.getIdentifier() +
", settingsActivity='" + settingsActivity + '\'' +
'}';
}
@@ -113,6 +122,7 @@ class InjectedSetting {
return packageName.equals(that.packageName) && className.equals(that.className)
&& title.equals(that.title) && iconId == that.iconId
&& mUserHandle.equals(that.mUserHandle)
&& settingsActivity.equals(that.settingsActivity);
}
@@ -122,6 +132,7 @@ class InjectedSetting {
result = 31 * result + className.hashCode();
result = 31 * result + title.hashCode();
result = 31 * result + iconId;
result = 31 * result + mUserHandle.hashCode();
result = 31 * result + settingsActivity.hashCode();
return result;
}

View File

@@ -21,16 +21,22 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.location.SettingInjectorService;
import android.os.Binder;
import android.os.Bundle;
import android.os.UserHandle;
import android.os.UserManager;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.preference.PreferenceGroup;
import android.preference.PreferenceScreen;
import android.preference.SwitchPreference;
import android.provider.Settings;
import android.util.Log;
import android.widget.Switch;
import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.Utils;
import com.android.settings.widget.SwitchBar;
import java.util.Collections;
@@ -45,6 +51,17 @@ public class LocationSettings extends LocationSettingsBase
private static final String TAG = "LocationSettings";
/**
* Key for managed profile location preference category. Category is shown only
* if there is a managed profile
*/
private static final String KEY_MANAGED_PROFILE_CATEGORY = "managed_profile_location_category";
/**
* Key for managed profile location preference. Note it used to be a switch pref and we had to
* keep the key as strings had been submitted for string freeze before the decision to
* demote this to a simple preference was made. TODO: Candidate for refactoring.
*/
private static final String KEY_MANAGED_PROFILE_PREFERENCE = "managed_profile_location_switch";
/** Key for preference screen "Mode" */
private static final String KEY_LOCATION_MODE = "location_mode";
/** Key for preference category "Recent location requests" */
@@ -55,17 +72,21 @@ public class LocationSettings extends LocationSettingsBase
private SwitchBar mSwitchBar;
private Switch mSwitch;
private boolean mValidListener = false;
private UserHandle mManagedProfile;
private Preference mManagedProfilePreference;
private Preference mLocationMode;
private PreferenceCategory mCategoryRecentLocationRequests;
/** Receives UPDATE_INTENT */
private BroadcastReceiver mReceiver;
private SettingsInjector injector;
private UserManager mUm;
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
final SettingsActivity activity = (SettingsActivity) getActivity();
mUm = (UserManager) activity.getSystemService(Context.USER_SERVICE);
mSwitchBar = activity.getSwitchBar();
mSwitch = mSwitchBar.getSwitch();
@@ -127,6 +148,7 @@ public class LocationSettings extends LocationSettingsBase
addPreferencesFromResource(R.xml.location_settings);
root = getPreferenceScreen();
setupManagedProfileCategory(root);
mLocationMode = root.findPreference(KEY_LOCATION_MODE);
mLocationMode.setOnPreferenceClickListener(
new Preference.OnPreferenceClickListener() {
@@ -155,12 +177,42 @@ public class LocationSettings extends LocationSettingsBase
mCategoryRecentLocationRequests.addPreference(banner);
}
addLocationServices(activity, root);
boolean lockdownOnLocationAccess = false;
// Checking if device policy has put a location access lock-down on the managed
// profile. If managed profile has lock-down on location access then its
// injected location services must not be shown.
if (mManagedProfile != null
&& mUm.hasUserRestriction(UserManager.DISALLOW_SHARE_LOCATION, mManagedProfile)) {
lockdownOnLocationAccess = true;
}
addLocationServices(activity, root, lockdownOnLocationAccess);
refreshLocationMode();
return root;
}
private void setupManagedProfileCategory(PreferenceScreen root) {
// Looking for a managed profile. If there are no managed profiles then we are removing the
// managed profile category.
mManagedProfile = Utils.getManagedProfile(mUm);
if (mManagedProfile == null) {
// There is no managed profile
root.removePreference(root.findPreference(KEY_MANAGED_PROFILE_CATEGORY));
mManagedProfilePreference = null;
} else {
mManagedProfilePreference = root.findPreference(KEY_MANAGED_PROFILE_PREFERENCE);
mManagedProfilePreference.setOnPreferenceClickListener(null);
}
}
private void changeManagedProfileLocationAccessStatus(boolean enabled, int summaryResId) {
if (mManagedProfilePreference == null) {
return;
}
mManagedProfilePreference.setEnabled(enabled);
mManagedProfilePreference.setSummary(summaryResId);
}
/**
* Add the settings injected by external apps into the "App Settings" category. Hides the
* category if there are no injected settings.
@@ -168,11 +220,15 @@ public class LocationSettings extends LocationSettingsBase
* Reloads the settings whenever receives
* {@link SettingInjectorService#ACTION_INJECTED_SETTING_CHANGED}.
*/
private void addLocationServices(Context context, PreferenceScreen root) {
private void addLocationServices(Context context, PreferenceScreen root,
boolean lockdownOnLocationAccess) {
PreferenceCategory categoryLocationServices =
(PreferenceCategory) root.findPreference(KEY_LOCATION_SERVICES);
injector = new SettingsInjector(context);
List<Preference> locationServices = injector.getInjectedSettings();
// If location access is locked down by device policy then we only show injected settings
// for the primary profile.
List<Preference> locationServices = injector.getInjectedSettings(lockdownOnLocationAccess ?
UserHandle.myUserId() : UserHandle.USER_CURRENT);
mReceiver = new BroadcastReceiver() {
@Override
@@ -223,7 +279,7 @@ public class LocationSettings extends LocationSettingsBase
// Restricted user can't change the location mode, so disable the master switch. But in some
// corner cases, the location might still be enabled. In such case the master switch should
// be disabled but checked.
boolean enabled = (mode != android.provider.Settings.Secure.LOCATION_MODE_OFF);
final boolean enabled = (mode != android.provider.Settings.Secure.LOCATION_MODE_OFF);
// Disable the whole switch bar instead of the switch itself. If we disabled the switch
// only, it would be re-enabled again if the switch bar is not disabled.
mSwitchBar.setEnabled(!restricted);
@@ -240,6 +296,20 @@ public class LocationSettings extends LocationSettingsBase
mSwitchBar.addOnSwitchChangeListener(this);
}
}
if (mManagedProfilePreference != null) {
if (mUm.hasUserRestriction(UserManager.DISALLOW_SHARE_LOCATION, mManagedProfile)) {
changeManagedProfileLocationAccessStatus(false,
R.string.managed_profile_location_switch_lockdown);
} else {
if (enabled) {
changeManagedProfileLocationAccessStatus(true, R.string.switch_on_text);
} else {
changeManagedProfileLocationAccessStatus(false, R.string.switch_off_text);
}
}
}
// As a safety measure, also reloads on location mode change to ensure the settings are
// up-to-date even if an affected app doesn't send the setting changed broadcast.
injector.reloadStatusMessages();

View File

@@ -77,35 +77,13 @@ public class RecentLocationApps {
}
}
/**
* Subclass of {@link Preference} to intercept views and allow content description to be set on
* them for accessibility purposes.
*/
private static class AccessiblePreference extends DimmableIconPreference {
public CharSequence mContentDescription;
public AccessiblePreference(Context context, CharSequence contentDescription) {
super(context);
mContentDescription = contentDescription;
}
@Override
protected void onBindView(View view) {
super.onBindView(view);
if (mContentDescription != null) {
final TextView titleView = (TextView) view.findViewById(android.R.id.title);
titleView.setContentDescription(mContentDescription);
}
}
}
private AccessiblePreference createRecentLocationEntry(
private DimmableIconPreference createRecentLocationEntry(
Drawable icon,
CharSequence label,
boolean isHighBattery,
CharSequence contentDescription,
Preference.OnPreferenceClickListener listener) {
AccessiblePreference pref = new AccessiblePreference(mActivity, contentDescription);
DimmableIconPreference pref = new DimmableIconPreference(mActivity, contentDescription);
pref.setIcon(icon);
pref.setTitle(label);
if (isHighBattery) {
@@ -198,7 +176,7 @@ public class RecentLocationApps {
int uid = ops.getUid();
int userId = UserHandle.getUserId(uid);
AccessiblePreference preference = null;
DimmableIconPreference preference = null;
try {
IPackageManager ipm = AppGlobals.getPackageManager();
ApplicationInfo appInfo =
@@ -215,6 +193,11 @@ public class RecentLocationApps {
Drawable icon = mPackageManager.getUserBadgedIcon(appIcon, userHandle);
CharSequence appLabel = mPackageManager.getApplicationLabel(appInfo);
CharSequence badgedAppLabel = mPackageManager.getUserBadgedLabel(appLabel, userHandle);
if (appLabel.toString().contentEquals(badgedAppLabel)) {
// If badged label is not different from original then no need for it as
// a separate content description.
badgedAppLabel = null;
}
preference = createRecentLocationEntry(icon,
appLabel, highBattery, badgedAppLabel,
new PackageEntryClickedListener(packageName));

View File

@@ -32,11 +32,15 @@ import android.os.Handler;
import android.os.Message;
import android.os.Messenger;
import android.os.SystemClock;
import android.os.UserHandle;
import android.os.UserManager;
import android.preference.Preference;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Xml;
import com.android.settings.R;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -99,27 +103,29 @@ class SettingsInjector {
}
/**
* Returns a list with one {@link InjectedSetting} object for each {@link android.app.Service}
* that responds to {@link SettingInjectorService#ACTION_SERVICE_INTENT} and provides the
* expected setting metadata.
* Returns a list for a profile with one {@link InjectedSetting} object for each
* {@link android.app.Service} that responds to
* {@link SettingInjectorService#ACTION_SERVICE_INTENT} and provides the expected setting
* metadata.
*
* Duplicates some code from {@link android.content.pm.RegisteredServicesCache}.
*
* TODO: unit test
*/
private List<InjectedSetting> getSettings() {
private List<InjectedSetting> getSettings(final UserHandle userHandle) {
PackageManager pm = mContext.getPackageManager();
Intent intent = new Intent(SettingInjectorService.ACTION_SERVICE_INTENT);
final int profileId = userHandle.getIdentifier();
List<ResolveInfo> resolveInfos =
pm.queryIntentServices(intent, PackageManager.GET_META_DATA);
pm.queryIntentServicesAsUser(intent, PackageManager.GET_META_DATA, profileId);
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Found services: " + resolveInfos);
Log.d(TAG, "Found services for profile id " + profileId + ": " + resolveInfos);
}
List<InjectedSetting> settings = new ArrayList<InjectedSetting>(resolveInfos.size());
for (ResolveInfo resolveInfo : resolveInfos) {
try {
InjectedSetting setting = parseServiceInfo(resolveInfo, pm);
InjectedSetting setting = parseServiceInfo(resolveInfo, userHandle, pm);
if (setting == null) {
Log.w(TAG, "Unable to load service info " + resolveInfo);
} else {
@@ -132,7 +138,7 @@ class SettingsInjector {
}
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Loaded settings: " + settings);
Log.d(TAG, "Loaded settings for profile id " + profileId + ": " + settings);
}
return settings;
@@ -144,8 +150,8 @@ class SettingsInjector {
*
* Duplicates some code from {@link android.content.pm.RegisteredServicesCache}.
*/
private static InjectedSetting parseServiceInfo(ResolveInfo service, PackageManager pm)
throws XmlPullParserException, IOException {
private static InjectedSetting parseServiceInfo(ResolveInfo service, UserHandle userHandle,
PackageManager pm) throws XmlPullParserException, IOException {
ServiceInfo si = service.serviceInfo;
ApplicationInfo ai = si.applicationInfo;
@@ -179,8 +185,9 @@ class SettingsInjector {
+ SettingInjectorService.ATTRIBUTES_NAME + " tag");
}
Resources res = pm.getResourcesForApplication(ai);
return parseAttributes(si.packageName, si.name, res, attrs);
Resources res = pm.getResourcesForApplicationAsUser(si.packageName,
userHandle.getIdentifier());
return parseAttributes(si.packageName, si.name, userHandle, res, attrs);
} catch (PackageManager.NameNotFoundException e) {
throw new XmlPullParserException(
"Unable to load resources for package " + si.packageName);
@@ -194,8 +201,8 @@ class SettingsInjector {
/**
* Returns an immutable representation of the static attributes for the setting, or null.
*/
private static InjectedSetting parseAttributes(
String packageName, String className, Resources res, AttributeSet attrs) {
private static InjectedSetting parseAttributes(String packageName, String className,
UserHandle userHandle, Resources res, AttributeSet attrs) {
TypedArray sa = res.obtainAttributes(attrs, android.R.styleable.SettingInjectorService);
try {
@@ -211,7 +218,7 @@ class SettingsInjector {
+ ", settingsActivity: " + settingsActivity);
}
return InjectedSetting.newInstance(packageName, className,
title, iconId, settingsActivity);
title, iconId, userHandle, settingsActivity);
} finally {
sa.recycle();
}
@@ -219,13 +226,24 @@ class SettingsInjector {
/**
* Gets a list of preferences that other apps have injected.
*
* @param profileId Identifier of the user/profile to obtain the injected settings for or
* UserHandle.USER_CURRENT for all profiles associated with current user.
*/
public List<Preference> getInjectedSettings() {
Iterable<InjectedSetting> settings = getSettings();
public List<Preference> getInjectedSettings(final int profileId) {
final UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
final List<UserHandle> profiles = um.getUserProfiles();
ArrayList<Preference> prefs = new ArrayList<Preference>();
for (InjectedSetting setting : settings) {
Preference pref = addServiceSetting(prefs, setting);
mSettings.add(new Setting(setting, pref));
final int profileCount = profiles.size();
for (int i = 0; i < profileCount; ++i) {
final UserHandle userHandle = profiles.get(i);
if (profileId == UserHandle.USER_CURRENT || profileId == userHandle.getIdentifier()) {
Iterable<InjectedSetting> settings = getSettings(userHandle);
for (InjectedSetting setting : settings) {
Preference pref = addServiceSetting(prefs, setting);
mSettings.add(new Setting(setting, pref));
}
}
}
reloadStatusMessages();
@@ -247,24 +265,46 @@ class SettingsInjector {
* Adds an injected setting to the root.
*/
private Preference addServiceSetting(List<Preference> prefs, InjectedSetting info) {
Preference pref = new DimmableIconPreference(mContext);
PackageManager pm = mContext.getPackageManager();
Drawable appIcon = pm.getDrawable(info.packageName, info.iconId, null);
Drawable icon = pm.getUserBadgedIcon(appIcon, info.mUserHandle);
CharSequence badgedAppLabel = pm.getUserBadgedLabel(info.title, info.mUserHandle);
if (info.title.contentEquals(badgedAppLabel)) {
// If badged label is not different from original then no need for it as
// a separate content description.
badgedAppLabel = null;
}
Preference pref = new DimmableIconPreference(mContext, badgedAppLabel);
pref.setTitle(info.title);
pref.setSummary(null);
PackageManager pm = mContext.getPackageManager();
Drawable icon = pm.getDrawable(info.packageName, info.iconId, null);
pref.setIcon(icon);
// Activity to start if they click on the preference. Must start in new task to ensure
// that "android.settings.LOCATION_SOURCE_SETTINGS" brings user back to Settings > Location.
Intent settingIntent = new Intent();
settingIntent.setClassName(info.packageName, info.settingsActivity);
settingIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
pref.setIntent(settingIntent);
pref.setOnPreferenceClickListener(new ServiceSettingClickedListener(info));
prefs.add(pref);
return pref;
}
private class ServiceSettingClickedListener
implements Preference.OnPreferenceClickListener {
private InjectedSetting mInfo;
public ServiceSettingClickedListener(InjectedSetting info) {
mInfo = info;
}
@Override
public boolean onPreferenceClick(Preference preference) {
// Activity to start if they click on the preference. Must start in new task to ensure
// that "android.settings.LOCATION_SOURCE_SETTINGS" brings user back to
// Settings > Location.
Intent settingIntent = new Intent();
settingIntent.setClassName(mInfo.packageName, mInfo.settingsActivity);
settingIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivityAsUser(settingIntent, mInfo.mUserHandle);
return true;
}
}
/**
* Loads the setting status values one at a time. Each load starts a subclass of {@link
* SettingInjectorService}, so to reduce memory pressure we don't want to load too many at
@@ -451,9 +491,9 @@ class SettingsInjector {
startMillis = 0;
}
// Start the service, making sure that this is attributed to the current user rather
// than the system user.
mContext.startServiceAsUser(intent, android.os.Process.myUserHandle());
// Start the service, making sure that this is attributed to the user associated with
// the setting rather than the system user.
mContext.startServiceAsUser(intent, setting.mUserHandle);
}
public long getElapsedTime() {