Merge "Add a toggle switch for accessibility large pointer icons."
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 37edca7..43fe796 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -1161,6 +1161,18 @@
</activity>
-->
+ <activity android:name="Settings$BackgroundCheckSummaryActivity"
+ android:label="@string/background_check_title"
+ android:taskAffinity=""
+ android:enabled="false">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ <meta-data android:name="com.android.settings.FRAGMENT_CLASS"
+ android:value="com.android.settings.applications.BackgroundCheckSummary" />
+ </activity>
+
<activity android:name="Settings$LocationSettingsActivity"
android:label="@string/location_settings_title"
android:icon="@drawable/ic_settings_location"
diff --git a/res/layout/app_ops_item.xml b/res/layout/app_ops_item.xml
index 8d21456..adba9da 100644
--- a/res/layout/app_ops_item.xml
+++ b/res/layout/app_ops_item.xml
@@ -25,7 +25,7 @@
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:paddingTop="8dip"
android:paddingBottom="8dip"
- android:columnCount="3">
+ android:columnCount="4">
<ImageView
android:id="@+id/app_icon"
@@ -47,6 +47,14 @@
android:textAppearance="?android:attr/textAppearanceMedium"
android:textAlignment="viewStart" />
+ <Switch
+ android:id="@+id/op_switch"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_rowSpan="2"
+ android:focusable="false"
+ android:clickable="false" />
+
<TextView
android:id="@+id/op_name"
android:layout_width="0dip"
diff --git a/res/layout/background_check_summary.xml b/res/layout/background_check_summary.xml
new file mode 100644
index 0000000..ee47194
--- /dev/null
+++ b/res/layout/background_check_summary.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2015, 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.
+*/
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/appops_content"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+</LinearLayout>
diff --git a/res/values/arrays.xml b/res/values/arrays.xml
index f04892e..12a0172 100644
--- a/res/values/arrays.xml
+++ b/res/values/arrays.xml
@@ -620,11 +620,25 @@
<item>monitor high power location</item>
<item>get usage stats</item>
<item>mute/unmute microphone</item>
+ <item>show toast</item>
<item>project media</item>
<item>activate VPN</item>
<item>write wallpaper</item>
<item>assist structure</item>
<item>assist screenshot</item>
+ <item>read phone state</item>
+ <item>add voicemail</item>
+ <item>use sip</item>
+ <item>process outgoing call</item>
+ <item>fingerprint</item>
+ <item>body sensors</item>
+ <item>read cell broadcasts</item>
+ <item>mock location</item>
+ <item>read storage</item>
+ <item>write storage</item>
+ <item>turn on screen</item>
+ <item>get accounts</item>
+ <item>run in background</item>
</string-array>
<!-- User display names for app ops codes -->
@@ -674,11 +688,25 @@
<item>Location</item>
<item>Get usage stats</item>
<item>Mute/unmute microphone</item>
+ <item>Show toast</item>
<item>Project media</item>
<item>Activate VPN</item>
<item>Write wallpaper</item>
<item>Assist structure</item>
<item>Assist screenshot</item>
+ <item>Read phone state</item>
+ <item>Add voicemail</item>
+ <item>Use sip</item>
+ <item>Process outgoing call</item>
+ <item>Fingerprint</item>
+ <item>Body sensors</item>
+ <item>Read cell broadcasts</item>
+ <item>Mock location</item>
+ <item>Read storage</item>
+ <item>Write storage</item>
+ <item>Turn on screen</item>
+ <item>Get accounts</item>
+ <item>Run in background</item>
</string-array>
<!-- Titles for the list of long press timeout options. -->
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 1dabd75..f0955a6 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -2783,8 +2783,6 @@
<!-- Security & location settings screen, section header for settings relating to location -->
<string name="location_title">My Location</string>
- <!-- Title for managed profile preference category [CHAR_LIMIT=25] -->
- <string name="managed_profile_location_category">Work profile</string>
<!-- [CHAR LIMIT=30] Title for managed profile location switch -->
<string name="managed_profile_location_switch_title">Location for work profile</string>
<!-- [CHAR LIMIT=30] Text to show on managed profile location switch if MDM has locked down location access for managed profile-->
@@ -6429,6 +6427,12 @@
usb_use_file_transfer, use_use_photo_transfer, and usb_use_MIDI -->
<string name="usb_use">Use USB for</string>
+ <!-- Settings item title for background check prefs [CHAR LIMIT=35] -->
+ <string name="background_check_pref">Background check</string>
+
+ <!-- Settings screen title for background check prefs [CHAR LIMIT=35] -->
+ <string name="background_check_title">Full background access</string>
+
<!-- Title for the "context" preference to determine whether assist can access the data currently displayed on-screen [CHAR LIMIT=40] -->
<string name="assist_access_context_title">Use text from screen</string>
diff --git a/res/xml/development_prefs.xml b/res/xml/development_prefs.xml
index 84c3cbf..d3c8852 100644
--- a/res/xml/development_prefs.xml
+++ b/res/xml/development_prefs.xml
@@ -340,6 +340,10 @@
android:entries="@array/app_process_limit_entries"
android:entryValues="@array/app_process_limit_values" />
+ <Preference
+ android:key="background_check"
+ android:title="@string/background_check_pref" />
+
<SwitchPreference
android:key="show_all_anrs"
android:title="@string/show_all_anrs"
diff --git a/res/xml/location_settings.xml b/res/xml/location_settings.xml
index 2cd86f5..4ccc14a 100644
--- a/res/xml/location_settings.xml
+++ b/res/xml/location_settings.xml
@@ -27,19 +27,13 @@
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>
+ <SwitchPreference
+ 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="true" />
<PreferenceCategory
android:key="recent_location_requests"
diff --git a/src/com/android/settings/DevelopmentSettings.java b/src/com/android/settings/DevelopmentSettings.java
index 9bccbcf..950af3c 100644
--- a/src/com/android/settings/DevelopmentSettings.java
+++ b/src/com/android/settings/DevelopmentSettings.java
@@ -76,6 +76,7 @@
import android.widget.TextView;
import com.android.internal.logging.MetricsLogger;
+import com.android.settings.applications.BackgroundCheckSummary;
import com.android.settings.fuelgauge.InactiveApps;
import com.android.settings.search.BaseSearchIndexProvider;
import com.android.settings.search.Indexable;
@@ -170,6 +171,8 @@
= "immediately_destroy_activities";
private static final String APP_PROCESS_LIMIT_KEY = "app_process_limit";
+ private static final String BACKGROUND_CHECK_KEY = "background_check";
+
private static final String SHOW_ALL_ANRS_KEY = "show_all_anrs";
private static final String PACKAGE_MIME_TYPE = "application/vnd.android.package-archive";
@@ -1718,6 +1721,8 @@
writeForceResizableOptions();
} else if (INACTIVE_APPS_KEY.equals(preference.getKey())) {
startInactiveAppsFragment();
+ } else if (BACKGROUND_CHECK_KEY.equals(preference.getKey())) {
+ startBackgroundCheckFragment();
} else {
return super.onPreferenceTreeClick(preference);
}
@@ -1731,6 +1736,12 @@
null, R.string.inactive_apps_title, null, null, 0);
}
+ private void startBackgroundCheckFragment() {
+ ((SettingsActivity) getActivity()).startPreferencePanel(
+ BackgroundCheckSummary.class.getName(),
+ null, R.string.background_check_title, null, null, 0);
+ }
+
private boolean showKeyguardConfirmation(Resources resources, int requestCode) {
return new ChooseLockSettingsHelper(getActivity(), this).launchConfirmationActivity(
requestCode, resources.getString(R.string.oem_unlock_enable));
diff --git a/src/com/android/settings/ResetNetworkConfirm.java b/src/com/android/settings/ResetNetworkConfirm.java
index 9050448..4615a99 100644
--- a/src/com/android/settings/ResetNetworkConfirm.java
+++ b/src/com/android/settings/ResetNetworkConfirm.java
@@ -31,6 +31,7 @@
import android.widget.Button;
import android.widget.Toast;
+import com.android.ims.ImsManager;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.telephony.PhoneConstants;
@@ -94,6 +95,8 @@
btManager.getAdapter().factoryReset();
}
+ ImsManager.factoryReset(context);
+
Toast.makeText(context, R.string.reset_network_complete_toast, Toast.LENGTH_SHORT)
.show();
}
diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java
index 4380d1e..149ef45 100644
--- a/src/com/android/settings/Settings.java
+++ b/src/com/android/settings/Settings.java
@@ -61,6 +61,7 @@
return super.isValidFragment(className);
}
}
+ public static class BackgroundCheckSummaryActivity extends SettingsActivity { /* empty */ }
public static class StorageUseActivity extends SettingsActivity { /* empty */ }
public static class DevelopmentSettingsActivity extends SettingsActivity { /* empty */ }
public static class AccessibilitySettingsActivity extends SettingsActivity { /* empty */ }
diff --git a/src/com/android/settings/applications/AppOpsCategory.java b/src/com/android/settings/applications/AppOpsCategory.java
index 3ccd6bb..4b54891 100644
--- a/src/com/android/settings/applications/AppOpsCategory.java
+++ b/src/com/android/settings/applications/AppOpsCategory.java
@@ -16,6 +16,7 @@
package com.android.settings.applications;
+import android.app.AppOpsManager;
import android.app.ListFragment;
import android.app.LoaderManager;
import android.content.AsyncTaskLoader;
@@ -34,6 +35,7 @@
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ListView;
+import android.widget.Switch;
import android.widget.TextView;
import com.android.settings.R;
@@ -48,6 +50,7 @@
private static final int RESULT_APP_DETAILS = 1;
AppOpsState mState;
+ boolean mUserControlled;
// This is the Adapter being used to display the list's data.
AppListAdapter mAdapter;
@@ -58,8 +61,13 @@
}
public AppOpsCategory(AppOpsState.OpsTemplate template) {
+ this(template, false);
+ }
+
+ public AppOpsCategory(AppOpsState.OpsTemplate template, boolean userControlled) {
Bundle args = new Bundle();
args.putParcelable("template", template);
+ args.putBoolean("userControlled", userControlled);
setArguments(args);
}
@@ -117,18 +125,22 @@
final InterestingConfigChanges mLastConfig = new InterestingConfigChanges();
final AppOpsState mState;
final AppOpsState.OpsTemplate mTemplate;
+ final boolean mUserControlled;
List<AppOpEntry> mApps;
PackageIntentReceiver mPackageObserver;
- public AppListLoader(Context context, AppOpsState state, AppOpsState.OpsTemplate template) {
+ public AppListLoader(Context context, AppOpsState state, AppOpsState.OpsTemplate template,
+ boolean userControlled) {
super(context);
mState = state;
mTemplate = template;
+ mUserControlled = userControlled;
}
@Override public List<AppOpEntry> loadInBackground() {
- return mState.buildState(mTemplate);
+ return mState.buildState(mTemplate, 0, null,
+ mUserControlled ? AppOpsState.LABEL_COMPARATOR : AppOpsState.RECENCY_COMPARATOR);
}
/**
@@ -247,13 +259,15 @@
private final Resources mResources;
private final LayoutInflater mInflater;
private final AppOpsState mState;
+ private final boolean mUserControlled;
List<AppOpEntry> mList;
- public AppListAdapter(Context context, AppOpsState state) {
+ public AppListAdapter(Context context, AppOpsState state, boolean userControlled) {
mResources = context.getResources();
mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mState = state;
+ mUserControlled = userControlled;
}
public void setData(List<AppOpEntry> data) {
@@ -292,9 +306,18 @@
((ImageView)view.findViewById(R.id.app_icon)).setImageDrawable(
item.getAppEntry().getIcon());
((TextView)view.findViewById(R.id.app_name)).setText(item.getAppEntry().getLabel());
- ((TextView)view.findViewById(R.id.op_name)).setText(item.getSummaryText(mState));
- ((TextView)view.findViewById(R.id.op_time)).setText(
- item.getTimeText(mResources, false));
+ if (mUserControlled) {
+ ((TextView) view.findViewById(R.id.op_name)).setText(
+ item.getTimeText(mResources, false));
+ view.findViewById(R.id.op_time).setVisibility(View.GONE);
+ ((Switch) view.findViewById(R.id.op_switch)).setChecked(
+ item.getPrimaryOpMode() == AppOpsManager.MODE_ALLOWED);
+ } else {
+ ((TextView) view.findViewById(R.id.op_name)).setText(item.getSummaryText(mState));
+ ((TextView) view.findViewById(R.id.op_time)).setText(
+ item.getTimeText(mResources, false));
+ view.findViewById(R.id.op_switch).setVisibility(View.GONE);
+ }
return view;
}
@@ -304,6 +327,7 @@
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mState = new AppOpsState(getActivity());
+ mUserControlled = getArguments().getBoolean("userControlled");
}
@Override public void onActivityCreated(Bundle savedInstanceState) {
@@ -317,7 +341,7 @@
setHasOptionsMenu(true);
// Create an empty adapter we will use to display the loaded data.
- mAdapter = new AppListAdapter(getActivity(), mState);
+ mAdapter = new AppListAdapter(getActivity(), mState, mUserControlled);
setListAdapter(mAdapter);
// Start out with a progress indicator.
@@ -341,8 +365,22 @@
@Override public void onListItemClick(ListView l, View v, int position, long id) {
AppOpEntry entry = mAdapter.getItem(position);
if (entry != null) {
- mCurrentPkgName = entry.getAppEntry().getApplicationInfo().packageName;
- startApplicationDetailsActivity();
+ if (mUserControlled) {
+ // We treat this as tapping on the check box, toggling the app op state.
+ Switch sw = ((Switch) v.findViewById(R.id.op_switch));
+ boolean checked = !sw.isChecked();
+ sw.setChecked(checked);
+ AppOpsManager.OpEntry op = entry.getOpEntry(0);
+ int mode = checked ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED;
+ mState.getAppOpsManager().setMode(op.getOp(),
+ entry.getAppEntry().getApplicationInfo().uid,
+ entry.getAppEntry().getApplicationInfo().packageName,
+ mode);
+ entry.overridePrimaryOpMode(mode);
+ } else {
+ mCurrentPkgName = entry.getAppEntry().getApplicationInfo().packageName;
+ startApplicationDetailsActivity();
+ }
}
}
@@ -352,7 +390,7 @@
if (fargs != null) {
template = (AppOpsState.OpsTemplate)fargs.getParcelable("template");
}
- return new AppListLoader(getActivity(), mState, template);
+ return new AppListLoader(getActivity(), mState, template, mUserControlled);
}
@Override public void onLoadFinished(Loader<List<AppOpEntry>> loader, List<AppOpEntry> data) {
diff --git a/src/com/android/settings/applications/AppOpsState.java b/src/com/android/settings/applications/AppOpsState.java
index c3189d6..237eac6 100644
--- a/src/com/android/settings/applications/AppOpsState.java
+++ b/src/com/android/settings/applications/AppOpsState.java
@@ -180,6 +180,7 @@
false,
false,
false,
+ false,
false }
);
@@ -206,9 +207,14 @@
false }
);
+ public static final OpsTemplate RUN_IN_BACKGROUND_TEMPLATE = new OpsTemplate(
+ new int[] { AppOpsManager.OP_RUN_IN_BACKGROUND },
+ new boolean[] { false }
+ );
+
public static final OpsTemplate[] ALL_TEMPLATES = new OpsTemplate[] {
LOCATION_TEMPLATE, PERSONAL_TEMPLATE, MESSAGING_TEMPLATE,
- MEDIA_TEMPLATE, DEVICE_TEMPLATE
+ MEDIA_TEMPLATE, DEVICE_TEMPLATE, RUN_IN_BACKGROUND_TEMPLATE
};
/**
@@ -306,6 +312,7 @@
= new ArrayList<AppOpsManager.OpEntry>();
private final AppEntry mApp;
private final int mSwitchOrder;
+ private int mOverriddenPrimaryMode = -1;
public AppOpEntry(AppOpsManager.PackageOps pkg, AppOpsManager.OpEntry op, AppEntry app,
int switchOrder) {
@@ -363,6 +370,14 @@
return mOps.get(pos);
}
+ public int getPrimaryOpMode() {
+ return mOverriddenPrimaryMode >= 0 ? mOverriddenPrimaryMode : mOps.get(0).getMode();
+ }
+
+ public void overridePrimaryOpMode(int mode) {
+ mOverriddenPrimaryMode = mode;
+ }
+
private CharSequence getCombinedText(ArrayList<AppOpsManager.OpEntry> ops,
CharSequence[] items) {
if (ops.size() == 1) {
@@ -418,9 +433,9 @@
}
/**
- * Perform alphabetical comparison of application entry objects.
+ * Perform app op state comparison of application entry objects.
*/
- public static final Comparator<AppOpEntry> APP_OP_COMPARATOR = new Comparator<AppOpEntry>() {
+ public static final Comparator<AppOpEntry> RECENCY_COMPARATOR = new Comparator<AppOpEntry>() {
private final Collator sCollator = Collator.getInstance();
@Override
public int compare(AppOpEntry object1, AppOpEntry object2) {
@@ -440,6 +455,18 @@
}
};
+ /**
+ * Perform alphabetical comparison of application entry objects.
+ */
+ public static final Comparator<AppOpEntry> LABEL_COMPARATOR = new Comparator<AppOpEntry>() {
+ private final Collator sCollator = Collator.getInstance();
+ @Override
+ public int compare(AppOpEntry object1, AppOpEntry object2) {
+ return sCollator.compare(object1.getAppEntry().getLabel(),
+ object2.getAppEntry().getLabel());
+ }
+ };
+
private void addOp(List<AppOpEntry> entries, AppOpsManager.PackageOps pkgOps,
AppEntry appEntry, AppOpsManager.OpEntry opEntry, boolean allowMerge, int switchOrder) {
if (allowMerge && entries.size() > 0) {
@@ -466,8 +493,12 @@
entries.add(entry);
}
+ public AppOpsManager getAppOpsManager() {
+ return mAppOps;
+ }
+
public List<AppOpEntry> buildState(OpsTemplate tpl) {
- return buildState(tpl, 0, null);
+ return buildState(tpl, 0, null, RECENCY_COMPARATOR);
}
private AppEntry getAppEntry(final Context context, final HashMap<String, AppEntry> appEntries,
@@ -492,6 +523,11 @@
}
public List<AppOpEntry> buildState(OpsTemplate tpl, int uid, String packageName) {
+ return buildState(tpl, uid, packageName, RECENCY_COMPARATOR);
+ }
+
+ public List<AppOpEntry> buildState(OpsTemplate tpl, int uid, String packageName,
+ Comparator<AppOpEntry> comparator) {
final Context context = mContext;
final HashMap<String, AppEntry> appEntries = new HashMap<String, AppEntry>();
@@ -593,7 +629,7 @@
}
// Sort the list.
- Collections.sort(entries, APP_OP_COMPARATOR);
+ Collections.sort(entries, comparator);
// Done!
return entries;
diff --git a/src/com/android/settings/applications/BackgroundCheckSummary.java b/src/com/android/settings/applications/BackgroundCheckSummary.java
new file mode 100644
index 0000000..dfd4c49
--- /dev/null
+++ b/src/com/android/settings/applications/BackgroundCheckSummary.java
@@ -0,0 +1,66 @@
+/**
+ * Copyright (C) 2015 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.settings.applications;
+
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.res.TypedArray;
+import android.os.Bundle;
+import android.preference.PreferenceFrameLayout;
+import android.support.v13.app.FragmentPagerAdapter;
+import android.support.v4.view.PagerTabStrip;
+import android.support.v4.view.ViewPager;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.settings.InstrumentedFragment;
+import com.android.settings.R;
+
+public class BackgroundCheckSummary extends InstrumentedFragment {
+ // layout inflater object used to inflate views
+ private LayoutInflater mInflater;
+
+ @Override
+ protected int getMetricsCategory() {
+ return MetricsLogger.BACKGROUND_CHECK_SUMMARY;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ // initialize the inflater
+ mInflater = inflater;
+
+ View rootView = mInflater.inflate(R.layout.background_check_summary,
+ container, false);
+
+ // We have to do this now because PreferenceFrameLayout looks at it
+ // only when the view is added.
+ if (container instanceof PreferenceFrameLayout) {
+ ((PreferenceFrameLayout.LayoutParams) rootView.getLayoutParams()).removeBorders = true;
+ }
+
+ FragmentTransaction ft = getChildFragmentManager().beginTransaction();
+ ft.add(R.id.appops_content, new AppOpsCategory(AppOpsState.RUN_IN_BACKGROUND_TEMPLATE,
+ true), "appops");
+ ft.commitAllowingStateLoss();
+
+ return rootView;
+ }
+}
diff --git a/src/com/android/settings/location/LocationSettings.java b/src/com/android/settings/location/LocationSettings.java
index 2ed48e9..9974e89 100644
--- a/src/com/android/settings/location/LocationSettings.java
+++ b/src/com/android/settings/location/LocationSettings.java
@@ -17,7 +17,9 @@
package com.android.settings.location;
import android.app.Activity;
+import android.app.admin.DevicePolicyManager;
import android.content.BroadcastReceiver;
+import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
@@ -30,6 +32,7 @@
import android.support.v7.preference.PreferenceCategory;
import android.support.v7.preference.PreferenceGroup;
import android.support.v7.preference.PreferenceScreen;
+import android.support.v14.preference.SwitchPreference;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
@@ -81,16 +84,10 @@
private static final String TAG = "LocationSettings";
/**
- * Key for managed profile location preference category. Category is shown only
- * if there is a managed profile
+ * Key for managed profile location switch preference. 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";
+ private static final String KEY_MANAGED_PROFILE_SWITCH = "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" */
@@ -104,7 +101,7 @@
private Switch mSwitch;
private boolean mValidListener = false;
private UserHandle mManagedProfile;
- private Preference mManagedProfilePreference;
+ private SwitchPreference mManagedProfileSwitch;
private Preference mLocationMode;
private PreferenceCategory mCategoryRecentLocationRequests;
/** Receives UPDATE_INTENT */
@@ -250,20 +247,39 @@
mManagedProfile = Utils.getManagedProfile(mUm);
if (mManagedProfile == null) {
// There is no managed profile
- root.removePreference(root.findPreference(KEY_MANAGED_PROFILE_CATEGORY));
- mManagedProfilePreference = null;
+ root.removePreference(root.findPreference(KEY_MANAGED_PROFILE_SWITCH));
+ mManagedProfileSwitch = null;
} else {
- mManagedProfilePreference = root.findPreference(KEY_MANAGED_PROFILE_PREFERENCE);
- mManagedProfilePreference.setOnPreferenceClickListener(null);
+ mManagedProfileSwitch = (SwitchPreference)root
+ .findPreference(KEY_MANAGED_PROFILE_SWITCH);
+ mManagedProfileSwitch.setOnPreferenceClickListener(null);
}
}
- private void changeManagedProfileLocationAccessStatus(boolean enabled, int summaryResId) {
- if (mManagedProfilePreference == null) {
+ private void changeManagedProfileLocationAccessStatus(boolean mainSwitchOn) {
+ if (mManagedProfileSwitch == null) {
return;
}
- mManagedProfilePreference.setEnabled(enabled);
- mManagedProfilePreference.setSummary(summaryResId);
+ boolean enabled = mainSwitchOn;
+ int summaryResId = R.string.switch_off_text;
+ if (mUm.hasUserRestriction(UserManager.DISALLOW_SHARE_LOCATION, mManagedProfile)
+ && getAdminRestrictingManagedProfile() != null) {
+ summaryResId = R.string.managed_profile_location_switch_lockdown;
+ enabled = false;
+ }
+
+ mManagedProfileSwitch.setEnabled(enabled);
+ mManagedProfileSwitch.setOnPreferenceClickListener(null);
+ if (!enabled) {
+ mManagedProfileSwitch.setChecked(false);
+ } else {
+ final boolean isRestricted = isManagedProfileRestrictedByBase();
+ mManagedProfileSwitch.setChecked(!isRestricted);
+ summaryResId = (isRestricted ?
+ R.string.switch_off_text : R.string.switch_on_text);
+ mManagedProfileSwitch.setOnPreferenceClickListener(mManagedProfileSwitchClickListener);
+ }
+ mManagedProfileSwitch.setSummary(summaryResId);
}
/**
@@ -374,18 +390,7 @@
}
}
- 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);
- }
- }
- }
+ changeManagedProfileLocationAccessStatus(enabled);
// 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.
@@ -404,6 +409,47 @@
}
}
+ private ComponentName getAdminRestrictingManagedProfile() {
+ if (mManagedProfile == null) {
+ return null;
+ }
+ DevicePolicyManager dpm = (DevicePolicyManager)getActivity().getSystemService(
+ Context.DEVICE_POLICY_SERVICE);
+ if (dpm == null) {
+ return null;
+ }
+ List<ComponentName> admins = dpm.getActiveAdminsAsUser(mManagedProfile.getIdentifier());
+ for (int i = 0; i < admins.size(); ++i) {
+ final ComponentName admin = admins.get(i);
+ Bundle restrictions = dpm.getUserRestrictions(admin, mManagedProfile.getIdentifier());
+ if (restrictions != null && restrictions.getBoolean(UserManager.DISALLOW_SHARE_LOCATION,
+ false)) {
+ return admin;
+ }
+ }
+ return null;
+ }
+
+ private boolean isManagedProfileRestrictedByBase() {
+ if (mManagedProfile == null) {
+ return false;
+ }
+ return mUm.hasBaseUserRestriction(UserManager.DISALLOW_SHARE_LOCATION, mManagedProfile);
+ }
+
+ private Preference.OnPreferenceClickListener mManagedProfileSwitchClickListener =
+ new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ final boolean switchState = mManagedProfileSwitch.isChecked();
+ mUm.setUserRestriction(UserManager.DISALLOW_SHARE_LOCATION,
+ !switchState, mManagedProfile);
+ mManagedProfileSwitch.setSummary(switchState ?
+ R.string.switch_on_text : R.string.switch_off_text);
+ return true;
+ }
+ };
+
private class PackageEntryClickedListener
implements Preference.OnPreferenceClickListener {
private String mPackage;
diff --git a/src/com/android/settings/vpn2/AppPreference.java b/src/com/android/settings/vpn2/AppPreference.java
index 84897be..af4192e 100644
--- a/src/com/android/settings/vpn2/AppPreference.java
+++ b/src/com/android/settings/vpn2/AppPreference.java
@@ -39,18 +39,13 @@
private int mState = STATE_DISCONNECTED;
private String mPackageName;
private String mName;
- private int mUid;
+ private int mUserId = UserHandle.USER_NULL;
- public AppPreference(Context context, OnClickListener onManage, final String packageName,
- int uid) {
+ public AppPreference(Context context, OnClickListener onManage) {
super(context, null /* attrs */, onManage);
- mPackageName = packageName;
- mUid = uid;
- update();
}
public PackageInfo getPackageInfo() {
- UserHandle user = new UserHandle(UserHandle.getUserId(mUid));
try {
PackageManager pm = getUserContext().getPackageManager();
return pm.getPackageInfo(mPackageName, 0 /* flags */);
@@ -67,8 +62,18 @@
return mPackageName;
}
- public int getUid() {
- return mUid;
+ public void setPackageName(String name) {
+ mPackageName = name;
+ update();
+ }
+
+ public int getUserId() {
+ return mUserId;
+ }
+
+ public void setUserId(int userId) {
+ mUserId = userId;
+ update();
}
public int getState() {
@@ -81,6 +86,10 @@
}
private void update() {
+ if (mPackageName == null || mUserId == UserHandle.USER_NULL) {
+ return;
+ }
+
final String[] states = getContext().getResources().getStringArray(R.array.vpn_states);
setSummary(mState != STATE_DISCONNECTED ? states[mState] : "");
@@ -116,7 +125,7 @@
}
private Context getUserContext() throws PackageManager.NameNotFoundException {
- UserHandle user = new UserHandle(UserHandle.getUserId(mUid));
+ UserHandle user = UserHandle.of(mUserId);
return getContext().createPackageContextAsUser(
getContext().getPackageName(), 0 /* flags */, user);
}
@@ -128,7 +137,7 @@
if ((result = another.mState - mState) == 0 &&
(result = mName.compareToIgnoreCase(another.mName)) == 0 &&
(result = mPackageName.compareTo(another.mPackageName)) == 0) {
- result = mUid - another.mUid;
+ result = mUserId - another.mUserId;
}
return result;
} else if (preference instanceof ConfigPreference) {
diff --git a/src/com/android/settings/vpn2/ConfigPreference.java b/src/com/android/settings/vpn2/ConfigPreference.java
index a2736a0..c7ecddb 100644
--- a/src/com/android/settings/vpn2/ConfigPreference.java
+++ b/src/com/android/settings/vpn2/ConfigPreference.java
@@ -31,12 +31,15 @@
* state.
*/
public class ConfigPreference extends ManageablePreference {
- private VpnProfile mProfile;
- private int mState = -1;
+ public static int STATE_NONE = -1;
- ConfigPreference(Context context, OnClickListener onManage, VpnProfile profile) {
+ private VpnProfile mProfile;
+
+ /** One of the STATE_* fields from LegacyVpnInfo, or STATE_NONE */
+ private int mState = STATE_NONE;
+
+ ConfigPreference(Context context, OnClickListener onManage) {
super(context, null /* attrs */, onManage);
- setProfile(profile);
}
public VpnProfile getProfile() {
@@ -54,15 +57,16 @@
}
private void update() {
- if (mState < 0) {
+ if (mState == STATE_NONE) {
setSummary("");
} else {
- String[] states = getContext().getResources()
- .getStringArray(R.array.vpn_states);
+ final String[] states = getContext().getResources().getStringArray(R.array.vpn_states);
setSummary(states[mState]);
}
- setIcon(R.mipmap.ic_launcher_settings);
- setTitle(mProfile.name);
+ if (mProfile != null) {
+ setIcon(R.mipmap.ic_launcher_settings);
+ setTitle(mProfile.name);
+ }
notifyHierarchyChanged();
}
@@ -72,7 +76,7 @@
ConfigPreference another = (ConfigPreference) preference;
int result;
if ((result = another.mState - mState) == 0 &&
- (result = mProfile.name.compareTo(another.mProfile.name)) == 0 &&
+ (result = mProfile.name.compareToIgnoreCase(another.mProfile.name)) == 0 &&
(result = mProfile.type - another.mProfile.type) == 0) {
result = mProfile.key.compareTo(another.mProfile.key);
}
diff --git a/src/com/android/settings/vpn2/VpnSettings.java b/src/com/android/settings/vpn2/VpnSettings.java
index b903463..00f6a12 100644
--- a/src/com/android/settings/vpn2/VpnSettings.java
+++ b/src/com/android/settings/vpn2/VpnSettings.java
@@ -16,13 +16,16 @@
package com.android.settings.vpn2;
+import android.annotation.NonNull;
+import android.annotation.UiThread;
+import android.annotation.WorkerThread;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
-import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
+import android.net.ConnectivityManager;
import android.net.IConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
@@ -40,7 +43,9 @@
import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceGroup;
import android.support.v7.preference.PreferenceScreen;
-import android.util.SparseArray;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -57,8 +62,10 @@
import com.google.android.collect.Lists;
import java.util.ArrayList;
-import java.util.HashMap;
+import java.util.Collections;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
import static android.app.AppOpsManager.OP_ACTIVATE_VPN;
@@ -87,8 +94,8 @@
private final KeyStore mKeyStore = KeyStore.getInstance();
- private HashMap<String, ConfigPreference> mConfigPreferences = new HashMap<>();
- private HashMap<String, AppPreference> mAppPreferences = new HashMap<>();
+ private Map<String, ConfigPreference> mConfigPreferences = new ArrayMap<>();
+ private Map<AppVpnInfo, AppPreference> mAppPreferences = new ArrayMap<>();
private Handler mUpdater;
private LegacyVpnInfo mConnectedLegacyVpn;
@@ -211,58 +218,64 @@
public boolean handleMessage(Message message) {
mUpdater.removeMessages(RESCAN_MESSAGE);
- // Pref group within which to list VPNs
- PreferenceGroup vpnGroup = getPreferenceScreen();
- vpnGroup.removeAll();
- mConfigPreferences.clear();
- mAppPreferences.clear();
+ final List<VpnProfile> vpnProfiles = loadVpnProfiles(mKeyStore);
+ final List<AppVpnInfo> vpnApps = getVpnApps();
- // Fetch configured VPN profiles from KeyStore
- for (VpnProfile profile : loadVpnProfiles(mKeyStore)) {
- final ConfigPreference pref = new ConfigPreference(getPrefContext(), mManageListener,
- profile);
- pref.setOnPreferenceClickListener(this);
- mConfigPreferences.put(profile.key, pref);
- vpnGroup.addPreference(pref);
- }
+ final List<LegacyVpnInfo> connectedLegacyVpns = getConnectedLegacyVpns();
+ final List<AppVpnInfo> connectedAppVpns = getConnectedAppVpns();
- // 3rd-party VPN apps can change elsewhere. Reload them every time.
- for (AppOpsManager.PackageOps pkg : getVpnApps()) {
- String key = getVpnIdentifier(UserHandle.getUserId(pkg.getUid()), pkg.getPackageName());
- final AppPreference pref = new AppPreference(getPrefContext(), mManageListener,
- pkg.getPackageName(), pkg.getUid());
- pref.setOnPreferenceClickListener(this);
- mAppPreferences.put(key, pref);
- vpnGroup.addPreference(pref);
- }
+ // Refresh the PreferenceGroup which lists VPNs
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ // Find new VPNs by subtracting existing ones from the full set
+ final Set<Preference> updates = new ArraySet<>();
- // Mark out connections with a subtitle
- try {
- // Legacy VPNs
- mConnectedLegacyVpn = null;
- LegacyVpnInfo info = mConnectivityService.getLegacyVpnInfo(UserHandle.myUserId());
- if (info != null) {
- ConfigPreference preference = mConfigPreferences.get(info.key);
- if (preference != null) {
- preference.setState(info.state);
- mConnectedLegacyVpn = info;
+ for (VpnProfile profile : vpnProfiles) {
+ ConfigPreference p = findOrCreatePreference(profile);
+ p.setState(ConfigPreference.STATE_NONE);
+ updates.add(p);
}
- }
+ for (AppVpnInfo app : vpnApps) {
+ AppPreference p = findOrCreatePreference(app);
+ p.setState(AppPreference.STATE_DISCONNECTED);
+ updates.add(p);
+ }
- // Third-party VPNs
- for (UserHandle profile : mUserManager.getUserProfiles()) {
- VpnConfig cfg = mConnectivityService.getVpnConfig(profile.getIdentifier());
- if (cfg != null) {
- final String key = getVpnIdentifier(profile.getIdentifier(), cfg.user);
- final AppPreference preference = mAppPreferences.get(key);
+ // Trim preferences for deleted VPNs
+ mConfigPreferences.values().retainAll(updates);
+ mAppPreferences.values().retainAll(updates);
+
+ final PreferenceGroup vpnGroup = getPreferenceScreen();
+ for (int i = vpnGroup.getPreferenceCount() - 1; i >= 0; i--) {
+ Preference p = vpnGroup.getPreference(i);
+ if (updates.contains(p)) {
+ updates.remove(p);
+ } else {
+ vpnGroup.removePreference(p);
+ }
+ }
+
+ // Show any new preferences on the screen
+ for (Preference pref : updates) {
+ vpnGroup.addPreference(pref);
+ }
+
+ // Mark connected VPNs
+ for (LegacyVpnInfo info : connectedLegacyVpns) {
+ final ConfigPreference preference = mConfigPreferences.get(info.key);
+ if (preference != null) {
+ preference.setState(info.state);
+ }
+ }
+ for (AppVpnInfo app : connectedAppVpns) {
+ final AppPreference preference = mAppPreferences.get(app);
if (preference != null) {
preference.setState(AppPreference.STATE_CONNECTED);
}
}
}
- } catch (RemoteException e) {
- // ignore
- }
+ });
mUpdater.sendEmptyMessageDelayed(RESCAN_MESSAGE, RESCAN_INTERVAL_MS);
return true;
@@ -278,7 +291,7 @@
mConnectedLegacyVpn.intent.send();
return true;
} catch (Exception e) {
- // ignore
+ Log.w(LOG_TAG, "Starting config intent failed", e);
}
}
ConfigDialogFragment.show(this, profile, false /* editing */, true /* exists */);
@@ -289,7 +302,7 @@
if (!connected) {
try {
- UserHandle user = new UserHandle(UserHandle.getUserId(pref.getUid()));
+ UserHandle user = UserHandle.of(pref.getUserId());
Context userContext = getActivity().createPackageContextAsUser(
getActivity().getPackageName(), 0 /* flags */, user);
PackageManager pm = userContext.getPackageManager();
@@ -299,11 +312,11 @@
return true;
}
} catch (PackageManager.NameNotFoundException nnfe) {
- // Fall through
+ Log.w(LOG_TAG, "VPN provider does not exist: " + pref.getPackageName(), nnfe);
}
}
- // Already onnected or no launch intent available - show an info dialog
+ // Already connected or no launch intent available - show an info dialog
PackageInfo pkgInfo = pref.getPackageInfo();
AppDialogFragment.show(this, pkgInfo, pref.getLabel(), false /* editing */, connected);
return true;
@@ -311,6 +324,11 @@
return false;
}
+ @Override
+ protected int getHelpResource() {
+ return R.string.help_url_vpn;
+ }
+
private View.OnClickListener mManageListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
@@ -329,10 +347,6 @@
}
};
- private static String getVpnIdentifier(int userId, String packageName) {
- return Integer.toString(userId)+ "_" + packageName;
- }
-
private NetworkCallback mNetworkCallback = new NetworkCallback() {
@Override
public void onAvailable(Network network) {
@@ -349,18 +363,68 @@
}
};
- @Override
- protected int getHelpResource() {
- return R.string.help_url_vpn;
+ @UiThread
+ private ConfigPreference findOrCreatePreference(VpnProfile profile) {
+ ConfigPreference pref = mConfigPreferences.get(profile.key);
+ if (pref == null) {
+ pref = new ConfigPreference(getPrefContext(), mManageListener);
+ pref.setOnPreferenceClickListener(this);
+ mConfigPreferences.put(profile.key, pref);
+ }
+ pref.setProfile(profile);
+ return pref;
}
- private List<AppOpsManager.PackageOps> getVpnApps() {
- List<AppOpsManager.PackageOps> result = Lists.newArrayList();
+ @UiThread
+ private AppPreference findOrCreatePreference(AppVpnInfo app) {
+ AppPreference pref = mAppPreferences.get(app);
+ if (pref == null) {
+ pref = new AppPreference(getPrefContext(), mManageListener);
+ pref.setOnPreferenceClickListener(this);
+ mAppPreferences.put(app, pref);
+ }
+ pref.setUserId(app.userId);
+ pref.setPackageName(app.packageName);
+ return pref;
+ }
+
+ @WorkerThread
+ private List<LegacyVpnInfo> getConnectedLegacyVpns() {
+ try {
+ mConnectedLegacyVpn = mConnectivityService.getLegacyVpnInfo(UserHandle.myUserId());
+ if (mConnectedLegacyVpn != null) {
+ return Collections.singletonList(mConnectedLegacyVpn);
+ }
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Failure updating VPN list with connected legacy VPNs", e);
+ }
+ return Collections.emptyList();
+ }
+
+ @WorkerThread
+ private List<AppVpnInfo> getConnectedAppVpns() {
+ // Mark connected third-party services
+ List<AppVpnInfo> connections = new ArrayList<>();
+ try {
+ for (UserHandle profile : mUserManager.getUserProfiles()) {
+ VpnConfig config = mConnectivityService.getVpnConfig(profile.getIdentifier());
+ if (config != null && !config.legacy) {
+ connections.add(new AppVpnInfo(profile.getIdentifier(), config.user));
+ }
+ }
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Failure updating VPN list with connected app VPNs", e);
+ }
+ return connections;
+ }
+
+ private List<AppVpnInfo> getVpnApps() {
+ List<AppVpnInfo> result = Lists.newArrayList();
// Build a filter of currently active user profiles.
- SparseArray<Boolean> currentProfileIds = new SparseArray<>();
+ Set<Integer> currentProfileIds = new ArraySet<>();
for (UserHandle profile : mUserManager.getUserProfiles()) {
- currentProfileIds.put(profile.getIdentifier(), Boolean.TRUE);
+ currentProfileIds.add(profile.getIdentifier());
}
// Fetch VPN-enabled apps from AppOps.
@@ -369,7 +433,7 @@
if (apps != null) {
for (AppOpsManager.PackageOps pkg : apps) {
int userId = UserHandle.getUserId(pkg.getUid());
- if (currentProfileIds.get(userId) == null) {
+ if (!currentProfileIds.contains(userId)) {
// Skip packages for users outside of our profile group.
continue;
}
@@ -382,7 +446,7 @@
}
}
if (allowed) {
- result.add(pkg);
+ result.add(new AppVpnInfo(userId, pkg.getPackageName()));
}
}
}
@@ -407,4 +471,29 @@
}
return result;
}
+
+ /** Utility holder for packageName:userId pairs */
+ private static class AppVpnInfo {
+ public int userId;
+ public String packageName;
+
+ public AppVpnInfo(int userId, @NonNull String packageName) {
+ this.userId = userId;
+ this.packageName = packageName;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof AppVpnInfo) {
+ AppVpnInfo that = (AppVpnInfo) other;
+ return userId == that.userId && packageName.equals(that.packageName);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return (packageName != null ? packageName.hashCode() : 0) * 31 + userId;
+ }
+ }
}