diff --git a/AndroidManifest.xml b/AndroidManifest.xml index ecdcfc8197e..78101d53cc4 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2522,5 +2522,23 @@ + + + + + + + + + + + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 6d87ca71a72..9483a0475ac 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -6855,4 +6855,63 @@ %1$d of %2$d characters used + + + + Apps that can draw overlay + + Draw over other apps + + Draw over other apps + + Apps + + Can draw overlays + + Permit drawing overlays + + App draw on top permission + + This permission allows an app to display on top of other apps you\u2019re using and may interfere with your use of the interface in other applications, or change what you think you are seeing in other applications. + + + system alert window dialog draw on top other apps + + Overlay settings + + + %d of %d apps allowed to draw on top of other apps + + + Apps with permission + + Yes + + No + + + + + Write system settings + + write modify system settings + + %d of %d apps allowed to read or write system settings + + + Can write system settings + + Can write system settings + + Write system settings + + App write system settings permission + + Allow write system settings + + This permission allows an app to read or write system settings. + + Yes + + No diff --git a/res/xml/advanced_apps.xml b/res/xml/advanced_apps.xml index ccdcbf096c6..70b68568c26 100644 --- a/res/xml/advanced_apps.xml +++ b/res/xml/advanced_apps.xml @@ -42,6 +42,26 @@ android:title="@string/default_apps_title" settings:keywords="@string/keywords_default_apps" /> + + + + + + + + + xmlns:settings="http://schemas.android.com/apk/res/com.android.settings"> + android:key="app_ops_settings_switch" /> + android:key="app_ops_settings_preference" /> diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java index ea4f77af9fd..f606193a7b3 100644 --- a/src/com/android/settings/Settings.java +++ b/src/com/android/settings/Settings.java @@ -114,5 +114,6 @@ public class Settings extends SettingsActivity { public static class ApnSettingsActivity extends SettingsActivity { /* empty */ } public static class WifiCallingSettingsActivity extends SettingsActivity { /* empty */ } public static class MemorySettingsActivity extends SettingsActivity { /* empty */ } + public static class OverlaySettingsActivity extends SettingsActivity { /* empty */ } + public static class WriteSettingsActivity extends SettingsActivity { /* empty */ } } - diff --git a/src/com/android/settings/applications/AdvancedAppSettings.java b/src/com/android/settings/applications/AdvancedAppSettings.java index 7df269e4452..54d38302f81 100644 --- a/src/com/android/settings/applications/AdvancedAppSettings.java +++ b/src/com/android/settings/applications/AdvancedAppSettings.java @@ -15,11 +15,14 @@ */ package com.android.settings.applications; +import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.os.Bundle; +import android.os.AsyncTask; import android.preference.Preference; +import android.provider.Settings; import com.android.internal.logging.MetricsLogger; import com.android.settings.R; @@ -40,11 +43,15 @@ public class AdvancedAppSettings extends SettingsPreferenceFragment implements private static final String KEY_APP_PERM = "manage_perms"; private static final String KEY_APP_DOMAIN_URLS = "domain_urls"; private static final String KEY_HIGH_POWER_APPS = "high_power_apps"; + private static final String KEY_SYSTEM_ALERT_WINDOW = "system_alert_window"; + private static final String KEY_WRITE_SETTINGS_APPS = "write_settings_apps"; private Session mSession; private Preference mAppPermsPreference; private Preference mAppDomainURLsPreference; private Preference mHighPowerPreference; + private Preference mSystemAlertWindowPreference; + private Preference mWriteSettingsPreference; private BroadcastReceiver mPermissionReceiver; @@ -63,6 +70,8 @@ public class AdvancedAppSettings extends SettingsPreferenceFragment implements mAppPermsPreference = findPreference(KEY_APP_PERM); mAppDomainURLsPreference = findPreference(KEY_APP_DOMAIN_URLS); mHighPowerPreference = findPreference(KEY_HIGH_POWER_APPS); + mSystemAlertWindowPreference = findPreference(KEY_SYSTEM_ALERT_WINDOW); + mWriteSettingsPreference = findPreference(KEY_WRITE_SETTINGS_APPS); updateUI(); } @@ -97,6 +106,16 @@ public class AdvancedAppSettings extends SettingsPreferenceFragment implements } mPermissionReceiver = PermissionsSummaryHelper.getAppWithPermissionsCounts(getContext(), mPermissionCallback); + + Activity activity = getActivity(); + ApplicationsState appState = ApplicationsState.getInstance(activity + .getApplication()); + AppStateOverlayBridge overlayBridge = new AppStateOverlayBridge(activity, + appState, null); + AppStateWriteSettingsBridge writeSettingsBridge = new AppStateWriteSettingsBridge( + activity, appState, null); + new CountAppsWithOverlayPermission().execute(overlayBridge); + new CountAppsWithWriteSettingsPermission().execute(writeSettingsBridge); } @Override @@ -159,4 +178,50 @@ public class AdvancedAppSettings extends SettingsPreferenceFragment implements } } }; + + private class CountAppsWithOverlayPermission extends + AsyncTask { + int numOfPackagesRequestedPermission = 0; + + @Override + protected Integer doInBackground(AppStateOverlayBridge... params) { + AppStateOverlayBridge overlayBridge = params[0]; + numOfPackagesRequestedPermission = overlayBridge + .getNumberOfPackagesWithPermission(); + return overlayBridge.getNumberOfPackagesCanDrawOverlay(); + } + + @Override + protected void onPostExecute(Integer result) { + // checks if fragment is still there before updating the preference object + if (isAdded()) { + mSystemAlertWindowPreference.setSummary(getContext().getString( + R.string.system_alert_window_summary, result, + numOfPackagesRequestedPermission)); + } + } + } + + private class CountAppsWithWriteSettingsPermission extends + AsyncTask { + int numOfPackagesRequestedPermission = 0; + + @Override + protected Integer doInBackground(AppStateWriteSettingsBridge... params) { + AppStateWriteSettingsBridge writeSettingsBridge = params[0]; + numOfPackagesRequestedPermission = writeSettingsBridge + .getNumberOfPackagesWithPermission(); + return writeSettingsBridge.getNumberOfPackagesCanWriteSettings(); + } + + @Override + protected void onPostExecute(Integer result) { + // checks if fragment is still there before updating the preference object + if (isAdded()) { + mWriteSettingsPreference.setSummary(getContext().getString( + R.string.write_settings_summary, result, + numOfPackagesRequestedPermission)); + } + } + } } diff --git a/src/com/android/settings/applications/AppStateAppOpsBridge.java b/src/com/android/settings/applications/AppStateAppOpsBridge.java new file mode 100644 index 00000000000..20a00bdb0a6 --- /dev/null +++ b/src/com/android/settings/applications/AppStateAppOpsBridge.java @@ -0,0 +1,301 @@ +/* + * 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.Manifest; +import android.app.AppGlobals; +import android.app.AppOpsManager; +import android.app.AppOpsManager.PackageOps; +import android.content.Context; +import android.content.pm.IPackageManager; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.RemoteException; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.ArrayMap; +import android.util.Log; +import android.util.SparseArray; + +import com.android.settingslib.applications.ApplicationsState; +import com.android.settingslib.applications.ApplicationsState.AppEntry; +import com.android.settingslib.applications.ApplicationsState.AppFilter; + +import java.util.Collection; +import java.util.List; + +/* + * Connects app ops info to the ApplicationsState. Makes use of AppOpsManager to + * determine further permission level. + */ +public abstract class AppStateAppOpsBridge extends AppStateBaseBridge { + + private static final String TAG = "AppStateAppOpsBridge"; + + private final IPackageManager mIPackageManager; + private final UserManager mUserManager; + private final List mProfiles; + private final AppOpsManager mAppOpsManager; + private final Context mContext; + private final int[] mAppOpsOpCodes; + private final String[] mPermissions; + + public AppStateAppOpsBridge(Context context, ApplicationsState appState, Callback callback, + int appOpsOpCode, String permissionName) { + super(appState, callback); + mContext = context; + mIPackageManager = AppGlobals.getPackageManager(); + mUserManager = UserManager.get(context); + mProfiles = mUserManager.getUserProfiles(); + mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); + mAppOpsOpCodes = new int[] {appOpsOpCode}; + mPermissions = new String[] {permissionName}; + } + + private boolean isThisUserAProfileOfCurrentUser(final int userId) { + final int profilesMax = mProfiles.size(); + for (int i = 0; i < profilesMax; i++) { + if (mProfiles.get(i).getIdentifier() == userId) { + return true; + } + } + return false; + } + + protected abstract void updateExtraInfo(AppEntry app, String pkg, int uid); + + public PermissionState getPermissionInfo(String pkg, int uid) { + PermissionState permissionState = new PermissionState(pkg, new UserHandle(UserHandle + .getUserId(uid))); + try { + permissionState.packageInfo = mIPackageManager.getPackageInfo(pkg, + PackageManager.GET_PERMISSIONS, permissionState.userHandle.getIdentifier()); + // Check static permission state (whatever that is declared in package manifest) + String[] requestedPermissions = permissionState.packageInfo.requestedPermissions; + int[] permissionFlags = permissionState.packageInfo.requestedPermissionsFlags; + if (requestedPermissions != null) { + for (int i = 0; i < requestedPermissions.length; i++) { + if (mPermissions[0].equals(requestedPermissions[i]) && + (permissionFlags[i] & PackageInfo.REQUESTED_PERMISSION_GRANTED) != 0) { + permissionState.permissionDeclared = true; + break; + } + } + } + // Check app op state. + List ops = mAppOpsManager.getOpsForPackage(uid, pkg, mAppOpsOpCodes); + if (ops != null && ops.size() > 0 && ops.get(0).getOps().size() > 0) { + permissionState.appOpMode = ops.get(0).getOps().get(0).getMode(); + } + } catch (RemoteException e) { + Log.w(TAG, "PackageManager is dead. Can't get package info " + pkg, e); + } + return permissionState; + } + + @Override + protected void loadAllExtraInfo() { + SparseArray> entries = getEntries(); + + // Load state info. + loadPermissionsStates(entries); + loadAppOpsStates(entries); + + // Map states to application info. + List apps = mAppSession.getAllApps(); + final int N = apps.size(); + for (int i = 0; i < N; i++) { + AppEntry app = apps.get(i); + int userId = UserHandle.getUserId(app.info.uid); + ArrayMap userMap = entries.get(userId); + app.extraInfo = userMap != null ? userMap.get(app.info.packageName) : null; + } + } + + /* + * Gets a sparse array that describes every user on the device and all the associated packages + * of each user, together with the packages available for that user. + */ + private SparseArray> getEntries() { + try { + final String[] packages = mIPackageManager.getAppOpPermissionPackages(mPermissions[0]); + + if (packages == null) { + // No packages are requesting permission as specified by mPermissions. + return null; + } + + // Create a sparse array that maps profileIds to an ArrayMap that maps package names to + // an associated PermissionState object + SparseArray> entries = new SparseArray<>(); + for (final UserHandle profile : mProfiles) { + final ArrayMap entriesForProfile = new ArrayMap<>(); + final int profileId = profile.getIdentifier(); + entries.put(profileId, entriesForProfile); + for (final String packageName : packages) { + final boolean isAvailable = mIPackageManager.isPackageAvailable(packageName, + profileId); + if (!shouldIgnorePackage(packageName) && isAvailable) { + final PermissionState newEntry = new PermissionState(packageName, profile); + entriesForProfile.put(packageName, newEntry); + } + } + } + + return entries; + } catch (RemoteException e) { + Log.w(TAG, "PackageManager is dead. Can't get list of packages requesting " + + mPermissions[0], e); + return null; + } + } + + /* + * This method will set the packageInfo and permissionDeclared field of the associated + * PermissionState, which describes a particular package. + */ + private void loadPermissionsStates(SparseArray> entries) { + // Load the packages that have been granted the permission specified in mPermission. + try { + for (final UserHandle profile : mProfiles) { + final int profileId = profile.getIdentifier(); + final ArrayMap entriesForProfile = entries.get(profileId); + if (entriesForProfile == null) { + continue; + } + @SuppressWarnings("unchecked") + final List packageInfos = mIPackageManager + .getPackagesHoldingPermissions(mPermissions, 0, profileId).getList(); + final int packageInfoCount = packageInfos != null ? packageInfos.size() : 0; + for (int i = 0; i < packageInfoCount; i++) { + final PackageInfo packageInfo = packageInfos.get(i); + final PermissionState pe = entriesForProfile.get(packageInfo.packageName); + if (pe != null) { + pe.packageInfo = packageInfo; + pe.permissionDeclared = true; + } + } + } + } catch (RemoteException e) { + Log.w(TAG, "PackageManager is dead. Can't get list of packages granted " + + mPermissions[0], e); + return; + } + } + + /* + * This method will set the appOpMode field of the associated PermissionState, which describes + * a particular package. + */ + private void loadAppOpsStates(SparseArray> entries) { + // Find out which packages have been granted permission from AppOps. + final List packageOps = mAppOpsManager.getPackagesForOps( + mAppOpsOpCodes); + final int packageOpsCount = packageOps != null ? packageOps.size() : 0; + for (int i = 0; i < packageOpsCount; i++) { + final AppOpsManager.PackageOps packageOp = packageOps.get(i); + final int userId = UserHandle.getUserId(packageOp.getUid()); + if (!isThisUserAProfileOfCurrentUser(userId)) { + // This AppOp does not belong to any of this user's profiles. + continue; + } + + final ArrayMap entriesForProfile = entries.get(userId); + if (entriesForProfile == null) { + continue; + } + final PermissionState pe = entriesForProfile.get(packageOp.getPackageName()); + if (pe == null) { + Log.w(TAG, "AppOp permission exists for package " + packageOp.getPackageName() + + " of user " + userId + " but package doesn't exist or did not request " + + mPermissions[0] + " access"); + continue; + } + + if (packageOp.getOps().size() < 1) { + Log.w(TAG, "No AppOps permission exists for package " + packageOp.getPackageName()); + continue; + } + pe.appOpMode = packageOp.getOps().get(0).getMode(); + } + } + + /* + * Check for packages that should be ignored for further processing + */ + private boolean shouldIgnorePackage(String packageName) { + return packageName.equals("android") || packageName.equals(mContext.getPackageName()); + } + + public int getNumPackagesDeclaredPermission() { + SparseArray> entries = getEntries(); + if (entries == null) { + return 0; + } + final ArrayMap entriesForProfile = entries.get(mUserManager + .getUserHandle()); + if (entriesForProfile == null) { + return 0; + } + return entriesForProfile.size(); + } + + public int getNumPackagesAllowedByAppOps() { + SparseArray> entries = getEntries(); + if (entries == null) { + return 0; + } + loadPermissionsStates(entries); + loadAppOpsStates(entries); + final ArrayMap entriesForProfile = entries.get(mUserManager + .getUserHandle()); + if (entriesForProfile == null) { + return 0; + } + Collection permStates = entriesForProfile.values(); + int result = 0; + for (PermissionState permState : permStates) { + if (permState.isPermissible()) { + result++; + } + } + return result; + } + + public static class PermissionState { + public final String packageName; + public final UserHandle userHandle; + public PackageInfo packageInfo; + public boolean permissionDeclared; + public int appOpMode; + + public PermissionState(String packageName, UserHandle userHandle) { + this.packageName = packageName; + this.appOpMode = AppOpsManager.MODE_DEFAULT; + this.userHandle = userHandle; + } + + public boolean isPermissible() { + // defining the default behavior as permissible as long as the package requested this + // permission (this means pre-M gets approval during install time; M apps gets approval + // during runtime. + if (appOpMode == AppOpsManager.MODE_DEFAULT) { + return permissionDeclared; + } + return appOpMode == AppOpsManager.MODE_ALLOWED; + } + } +} diff --git a/src/com/android/settings/applications/AppStateOverlayBridge.java b/src/com/android/settings/applications/AppStateOverlayBridge.java new file mode 100644 index 00000000000..21586bcee45 --- /dev/null +++ b/src/com/android/settings/applications/AppStateOverlayBridge.java @@ -0,0 +1,84 @@ +/* + * 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.Manifest; +import android.app.AppOpsManager; +import android.content.Context; +import android.util.Log; + +import com.android.settingslib.applications.ApplicationsState; +import com.android.settingslib.applications.ApplicationsState.AppEntry; +import com.android.settingslib.applications.ApplicationsState.AppFilter; + +/* + * Connects info of apps that draw overlay to the ApplicationsState. Wraps around the generic + * AppStateAppOpsBridge class to tailor to the semantics of SYSTEM_ALERT_WINDOW. Also provides app + * filters that can use the info. + */ +public class AppStateOverlayBridge extends AppStateAppOpsBridge { + + private static final String TAG = "AppStateOverlayBridge"; + private static final int APP_OPS_OP_CODE = AppOpsManager.OP_SYSTEM_ALERT_WINDOW; + private static final String PM_SYSTEM_ALERT_WINDOW = Manifest.permission.SYSTEM_ALERT_WINDOW; + + public AppStateOverlayBridge(Context context, ApplicationsState appState, Callback callback) { + super(context, appState, callback, APP_OPS_OP_CODE, PM_SYSTEM_ALERT_WINDOW); + } + + @Override + protected void updateExtraInfo(AppEntry app, String pkg, int uid) { + app.extraInfo = getOverlayInfo(pkg, uid); + } + + public OverlayState getOverlayInfo(String pkg, int uid) { + PermissionState permissionState = super.getPermissionInfo(pkg, uid); + return new OverlayState(permissionState); + } + + // TODO: figure out how to filter out system apps for this method + public int getNumberOfPackagesWithPermission() { + return super.getNumPackagesDeclaredPermission(); + } + + // TODO: figure out how to filter out system apps for this method + public int getNumberOfPackagesCanDrawOverlay() { + return super.getNumPackagesAllowedByAppOps(); + } + + public static class OverlayState { + PermissionState mPermissionState; + + public OverlayState(PermissionState permissionState) { + mPermissionState = permissionState; + } + + public boolean isAllowed() { + return mPermissionState.isPermissible(); + } + } + + public static final AppFilter FILTER_SYSTEM_ALERT_WINDOW = new AppFilter() { + @Override + public void init() { + } + + @Override + public boolean filterApp(AppEntry info) { + return info.extraInfo != null; + } + }; +} diff --git a/src/com/android/settings/applications/AppStateUsageBridge.java b/src/com/android/settings/applications/AppStateUsageBridge.java index c06492c0d09..a1529017ea9 100644 --- a/src/com/android/settings/applications/AppStateUsageBridge.java +++ b/src/com/android/settings/applications/AppStateUsageBridge.java @@ -16,65 +16,28 @@ package com.android.settings.applications; import android.Manifest; -import android.app.AppGlobals; import android.app.AppOpsManager; -import android.app.AppOpsManager.PackageOps; import android.content.Context; -import android.content.pm.IPackageManager; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.os.RemoteException; -import android.os.UserHandle; -import android.os.UserManager; -import android.util.ArrayMap; import android.util.Log; -import android.util.SparseArray; import com.android.settingslib.applications.ApplicationsState; import com.android.settingslib.applications.ApplicationsState.AppEntry; import com.android.settingslib.applications.ApplicationsState.AppFilter; -import java.util.List; - /* - * Connects app usage info to the ApplicationsState. - * Also provides app filters that can use the info. + * Connects app usage info to the ApplicationsState. Wraps around the generic AppStateAppOpsBridge + * class to tailor to the semantics of PACKAGE_USAGE_STATS. Also provides app filters that can use + * the info. */ -public class AppStateUsageBridge extends AppStateBaseBridge { +public class AppStateUsageBridge extends AppStateAppOpsBridge { private static final String TAG = "AppStateUsageBridge"; - private static final String[] PM_USAGE_STATS_PERMISSION = { - Manifest.permission.PACKAGE_USAGE_STATS - }; - - private static final int[] APP_OPS_OP_CODES = { - AppOpsManager.OP_GET_USAGE_STATS - }; - - private final IPackageManager mIPackageManager; - private final UserManager mUserManager; - private final List mProfiles; - private final AppOpsManager mAppOpsManager; - private final Context mContext; + private static final String PM_USAGE_STATS = Manifest.permission.PACKAGE_USAGE_STATS; + private static final int APP_OPS_OP_CODE = AppOpsManager.OP_GET_USAGE_STATS; public AppStateUsageBridge(Context context, ApplicationsState appState, Callback callback) { - super(appState, callback); - mContext = context; - mIPackageManager = AppGlobals.getPackageManager(); - mUserManager = UserManager.get(context); - mProfiles = mUserManager.getUserProfiles(); - mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); - } - - private boolean isThisUserAProfileOfCurrentUser(final int userId) { - final int profilesMax = mProfiles.size(); - for (int i = 0; i < profilesMax; i++) { - if (mProfiles.get(i).getIdentifier() == userId) { - return true; - } - } - return false; + super(context, appState, callback, APP_OPS_OP_CODE, PM_USAGE_STATS); } @Override @@ -83,173 +46,17 @@ public class AppStateUsageBridge extends AppStateBaseBridge { } public UsageState getUsageInfo(String pkg, int uid) { - UsageState usageState = new UsageState(pkg, new UserHandle(UserHandle.getUserId(uid))); - try { - usageState.packageInfo = mIPackageManager.getPackageInfo(pkg, - PackageManager.GET_PERMISSIONS, usageState.userHandle.getIdentifier()); - // Check permission state. - String[] requestedPermissions = usageState.packageInfo.requestedPermissions; - int[] permissionFlags = usageState.packageInfo.requestedPermissionsFlags; - if (requestedPermissions != null) { - for (int i = 0; i < requestedPermissions.length; i++) { - if (Manifest.permission.PACKAGE_USAGE_STATS.equals(requestedPermissions[i]) - && (permissionFlags[i] & PackageInfo.REQUESTED_PERMISSION_GRANTED) - != 0) { - usageState.permissionGranted = true; - break; - } - } - } - // Check app op state. - List ops = mAppOpsManager.getOpsForPackage(uid, pkg, APP_OPS_OP_CODES); - if (ops != null && ops.size() > 0 && ops.get(0).getOps().size() > 0) { - usageState.appOpMode = ops.get(0).getOps().get(0).getMode(); - } - } catch (RemoteException e) { - Log.w(TAG, "PackageManager is dead. Can't get package info " + pkg, e); - } - return usageState; + PermissionState permissionState = super.getPermissionInfo(pkg, uid); + return new UsageState(permissionState); } - @Override - protected void loadAllExtraInfo() { - SparseArray> entries = getEntries(); + public static class UsageState extends AppStateAppOpsBridge.PermissionState { - // Load state info. - loadPermissionsStates(entries); - loadAppOpsStates(entries); - - // Map states to application info. - List apps = mAppSession.getAllApps(); - final int N = apps.size(); - for (int i = 0; i < N; i++) { - AppEntry app = apps.get(i); - int userId = UserHandle.getUserId(app.info.uid); - ArrayMap userMap = entries.get(userId); - app.extraInfo = userMap != null ? userMap.get(app.info.packageName) : null; - } - } - - private SparseArray> getEntries() { - try { - final String[] packages = mIPackageManager.getAppOpPermissionPackages( - Manifest.permission.PACKAGE_USAGE_STATS); - - if (packages == null) { - // No packages are requesting permission to use the UsageStats API. - return null; - } - - SparseArray> entries = new SparseArray<>(); - for (final UserHandle profile : mProfiles) { - final ArrayMap entriesForProfile = new ArrayMap<>(); - final int profileId = profile.getIdentifier(); - entries.put(profileId, entriesForProfile); - for (final String packageName : packages) { - final boolean isAvailable = mIPackageManager.isPackageAvailable(packageName, - profileId); - if (!shouldIgnorePackage(packageName) && isAvailable) { - final UsageState newEntry = new UsageState(packageName, profile); - entriesForProfile.put(packageName, newEntry); - } - } - } - - return entries; - } catch (RemoteException e) { - Log.w(TAG, "PackageManager is dead. Can't get list of packages requesting " - + Manifest.permission.PACKAGE_USAGE_STATS, e); - return null; - } - } - - private void loadPermissionsStates(SparseArray> entries) { - // Load the packages that have been granted the PACKAGE_USAGE_STATS permission. - try { - for (final UserHandle profile : mProfiles) { - final int profileId = profile.getIdentifier(); - final ArrayMap entriesForProfile = entries.get(profileId); - if (entriesForProfile == null) { - continue; - } - @SuppressWarnings("unchecked") - final List packageInfos = mIPackageManager - .getPackagesHoldingPermissions(PM_USAGE_STATS_PERMISSION, 0, profileId) - .getList(); - final int packageInfoCount = packageInfos != null ? packageInfos.size() : 0; - for (int i = 0; i < packageInfoCount; i++) { - final PackageInfo packageInfo = packageInfos.get(i); - final UsageState pe = entriesForProfile.get(packageInfo.packageName); - if (pe != null) { - pe.packageInfo = packageInfo; - pe.permissionGranted = true; - } - } - } - } catch (RemoteException e) { - Log.w(TAG, "PackageManager is dead. Can't get list of packages granted " - + Manifest.permission.PACKAGE_USAGE_STATS, e); - return; - } - } - - private void loadAppOpsStates(SparseArray> entries) { - // Find out which packages have been granted permission from AppOps. - final List packageOps = mAppOpsManager.getPackagesForOps( - APP_OPS_OP_CODES); - final int packageOpsCount = packageOps != null ? packageOps.size() : 0; - for (int i = 0; i < packageOpsCount; i++) { - final AppOpsManager.PackageOps packageOp = packageOps.get(i); - final int userId = UserHandle.getUserId(packageOp.getUid()); - if (!isThisUserAProfileOfCurrentUser(userId)) { - // This AppOp does not belong to any of this user's profiles. - continue; - } - - final ArrayMap entriesForProfile = entries.get(userId); - if (entriesForProfile == null) { - continue; - } - final UsageState pe = entriesForProfile.get(packageOp.getPackageName()); - if (pe == null) { - Log.w(TAG, "AppOp permission exists for package " + packageOp.getPackageName() - + " of user " + userId + - " but package doesn't exist or did not request UsageStats access"); - continue; - } - - if (packageOp.getOps().size() < 1) { - Log.w(TAG, "No AppOps permission exists for package " - + packageOp.getPackageName()); - continue; - } - - pe.appOpMode = packageOp.getOps().get(0).getMode(); - } - } - - private boolean shouldIgnorePackage(String packageName) { - return packageName.equals("android") || packageName.equals(mContext.getPackageName()); - } - - public static class UsageState { - public final String packageName; - public final UserHandle userHandle; - public PackageInfo packageInfo; - public boolean permissionGranted; - public int appOpMode; - - public UsageState(String packageName, UserHandle userHandle) { - this.packageName = packageName; - this.appOpMode = AppOpsManager.MODE_DEFAULT; - this.userHandle = userHandle; - } - - public boolean hasAccess() { - if (appOpMode == AppOpsManager.MODE_DEFAULT) { - return permissionGranted; - } - return appOpMode == AppOpsManager.MODE_ALLOWED; + public UsageState(PermissionState permissionState) { + super(permissionState.packageName, permissionState.userHandle); + this.packageInfo = permissionState.packageInfo; + this.appOpMode = permissionState.appOpMode; + this.permissionDeclared = permissionState.permissionDeclared; } } diff --git a/src/com/android/settings/applications/AppStateWriteSettingsBridge.java b/src/com/android/settings/applications/AppStateWriteSettingsBridge.java new file mode 100644 index 00000000000..4ab737f7bc4 --- /dev/null +++ b/src/com/android/settings/applications/AppStateWriteSettingsBridge.java @@ -0,0 +1,85 @@ +/* + * 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.Manifest; +import android.app.AppOpsManager; +import android.content.Context; +import android.util.Log; + +import com.android.settingslib.applications.ApplicationsState; +import com.android.settingslib.applications.ApplicationsState.AppEntry; +import com.android.settingslib.applications.ApplicationsState.AppFilter; + +/* + * Connects info of apps that draw overlay to the ApplicationsState. Wraps around the generic + * AppStateAppOpsBridge class to tailor to the semantics of SYSTEM_ALERT_WINDOW. Also provides app + * filters that can use the info. + */ +public class AppStateWriteSettingsBridge extends AppStateAppOpsBridge { + + private static final String TAG = "AppStateWriteSettingsBridge"; + private static final int APP_OPS_OP_CODE = AppOpsManager.OP_WRITE_SETTINGS; + private static final String PM_WRITE_SETTINGS = Manifest.permission.WRITE_SETTINGS; + + public AppStateWriteSettingsBridge(Context context, ApplicationsState appState, Callback + callback) { + super(context, appState, callback, APP_OPS_OP_CODE, PM_WRITE_SETTINGS); + } + + @Override + protected void updateExtraInfo(AppEntry app, String pkg, int uid) { + app.extraInfo = getWriteSettingsInfo(pkg, uid); + } + + public WriteSettingsState getWriteSettingsInfo(String pkg, int uid) { + PermissionState permissionState = super.getPermissionInfo(pkg, uid); + return new WriteSettingsState(permissionState); + } + + // TODO: figure out how to filter out system apps for this method + public int getNumberOfPackagesWithPermission() { + return super.getNumPackagesDeclaredPermission(); + } + + // TODO: figure out how to filter out system apps for this method + public int getNumberOfPackagesCanWriteSettings() { + return super.getNumPackagesAllowedByAppOps(); + } + + public static class WriteSettingsState { + PermissionState mPermissionState; + + public WriteSettingsState(PermissionState permissionState) { + mPermissionState = permissionState; + } + + public boolean canWrite() { + return mPermissionState.isPermissible(); + } + } + + public static final AppFilter FILTER_WRITE_SETTINGS = new AppFilter() { + @Override + public void init() { + } + + @Override + public boolean filterApp(AppEntry info) { + return info.extraInfo != null; + } + }; +} diff --git a/src/com/android/settings/applications/DrawOverlayDetails.java b/src/com/android/settings/applications/DrawOverlayDetails.java new file mode 100644 index 00000000000..078c2c5f0a6 --- /dev/null +++ b/src/com/android/settings/applications/DrawOverlayDetails.java @@ -0,0 +1,212 @@ +/* + * 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.AlertDialog; +import android.app.AppOpsManager; +import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Bundle; +import android.os.UserHandle; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceChangeListener; +import android.preference.Preference.OnPreferenceClickListener; +import android.preference.SwitchPreference; +import android.provider.Settings; +import android.util.Log; + +import com.android.internal.logging.MetricsLogger; +import com.android.settings.InstrumentedFragment; +import com.android.settings.R; +import com.android.settings.applications.AppStateOverlayBridge.OverlayState; +import com.android.settingslib.applications.ApplicationsState; +import com.android.settingslib.applications.ApplicationsState.AppEntry; + +import java.util.List; + +public class DrawOverlayDetails extends AppInfoWithHeader implements OnPreferenceChangeListener, + OnPreferenceClickListener { + + private static final String KEY_APP_OPS_SETTINGS_SWITCH = "app_ops_settings_switch"; + private static final String KEY_APP_OPS_SETTINGS_PREFS = "app_ops_settings_preference"; + private static final String KEY_APP_OPS_SETTINGS_DESC = "app_ops_settings_description"; + private static final String LOG_TAG = "DrawOverlayDetails"; + + private static final int [] APP_OPS_OP_CODE = { + AppOpsManager.OP_SYSTEM_ALERT_WINDOW + }; + + // Use a bridge to get the overlay details but don't initialize it to connect with all state. + // TODO: Break out this functionality into its own class. + private AppStateOverlayBridge mOverlayBridge; + private AppOpsManager mAppOpsManager; + private SwitchPreference mSwitchPref; + private Preference mOverlayPrefs; + private Preference mOverlayDesc; + private Intent mSettingsIntent; + private OverlayState mOverlayState; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Context context = getActivity(); + mOverlayBridge = new AppStateOverlayBridge(context, mState, null); + mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); + + // find preferences + addPreferencesFromResource(R.xml.app_ops_permissions_details); + mSwitchPref = (SwitchPreference) findPreference(KEY_APP_OPS_SETTINGS_SWITCH); + mOverlayPrefs = findPreference(KEY_APP_OPS_SETTINGS_PREFS); + mOverlayDesc = findPreference(KEY_APP_OPS_SETTINGS_DESC); + + // set title/summary for all of them + getPreferenceScreen().setTitle(R.string.draw_overlay); + mSwitchPref.setTitle(R.string.permit_draw_overlay); + mOverlayPrefs.setTitle(R.string.app_overlay_permission_preference); + mOverlayDesc.setSummary(R.string.allow_overlay_description); + + // install event listeners + mSwitchPref.setOnPreferenceChangeListener(this); + mOverlayPrefs.setOnPreferenceClickListener(this); + + mSettingsIntent = new Intent(Intent.ACTION_MAIN) + .setAction(Settings.ACTION_MANAGE_OVERLAY_PERMISSION); + } + + @Override + public boolean onPreferenceClick(Preference preference) { + if (preference == mOverlayPrefs) { + if (mSettingsIntent != null) { + try { + getActivity().startActivityAsUser(mSettingsIntent, new UserHandle(mUserId)); + } catch (ActivityNotFoundException e) { + Log.w(TAG, "Unable to launch app draw overlay settings " + mSettingsIntent, e); + } + } + return true; + } + return false; + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (preference == mSwitchPref) { + if (mOverlayState != null && (Boolean) newValue != mOverlayState.isAllowed()) { + setCanDrawOverlay(!mOverlayState.isAllowed()); + refreshUi(); + } + return true; + } + return false; + } + + private void setCanDrawOverlay(boolean newState) { + mAppOpsManager.setMode(AppOpsManager.OP_SYSTEM_ALERT_WINDOW, + mPackageInfo.applicationInfo.uid, mPackageName, newState + ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_ERRORED); + canDrawOverlay(mPackageName); + } + + private boolean canDrawOverlay(String pkgName) { + int result = mAppOpsManager.noteOpNoThrow(AppOpsManager.OP_SYSTEM_ALERT_WINDOW, + mPackageInfo.applicationInfo.uid, pkgName); + if (result == AppOpsManager.MODE_ALLOWED) { + return true; + } + + return false; + } + + @Override + protected boolean refreshUi() { + mOverlayState = mOverlayBridge.getOverlayInfo(mPackageName, + mPackageInfo.applicationInfo.uid); + + boolean isAllowed = mOverlayState.isAllowed(); + mSwitchPref.setChecked(isAllowed); + mOverlayPrefs.setEnabled(isAllowed); + + ResolveInfo resolveInfo = mPm.resolveActivityAsUser(mSettingsIntent, + PackageManager.GET_META_DATA, mUserId); + if (resolveInfo == null) { + if (findPreference(KEY_APP_OPS_SETTINGS_PREFS) != null) { + getPreferenceScreen().removePreference(mOverlayPrefs); + } + } + + return true; + } + + @Override + protected AlertDialog createDialog(int id, int errorCode) { + return null; + } + + @Override + protected int getMetricsCategory() { + return MetricsLogger.SYSTEM_ALERT_WINDOW_APPS; + } + + public static CharSequence getSummary(Context context, AppEntry entry) { + return getSummary(context, entry.info.packageName); + } + + public static CharSequence getSummary(Context context, String pkg) { + // first check if pkg is a system pkg + boolean isSystem = false; + PackageManager packageManager = context.getPackageManager(); + try { + ApplicationInfo appInfo = packageManager.getApplicationInfo(pkg, 0); + if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { + isSystem = true; + } + } catch (PackageManager.NameNotFoundException e) { + // pkg doesn't even exist? + Log.w(TAG, "Package " + pkg + " not found", e); + return context.getString(R.string.system_alert_window_off); + } + + AppOpsManager appOpsManager = (AppOpsManager) context.getSystemService(Context + .APP_OPS_SERVICE); + List packageOps = appOpsManager.getPackagesForOps( + APP_OPS_OP_CODE); + if (packageOps == null) { + return context.getString(R.string.system_alert_window_off); + } + + int uid = isSystem ? 0 : -1; + for (AppOpsManager.PackageOps packageOp : packageOps) { + if (pkg.equals(packageOp.getPackageName())) { + uid = packageOp.getUid(); + break; + } + } + + if (uid == -1) { + return context.getString(R.string.system_alert_window_off); + } + + int mode = appOpsManager.noteOpNoThrow(AppOpsManager.OP_SYSTEM_ALERT_WINDOW, uid, pkg); + return context.getString((mode == AppOpsManager.MODE_ALLOWED) ? + R.string.system_alert_window_on : R.string.system_alert_window_off); + } +} diff --git a/src/com/android/settings/applications/ManageApplications.java b/src/com/android/settings/applications/ManageApplications.java index 0a09133ffb0..3c73ef454fd 100644 --- a/src/com/android/settings/applications/ManageApplications.java +++ b/src/com/android/settings/applications/ManageApplications.java @@ -59,8 +59,11 @@ import com.android.settings.Settings.HighPowerApplicationsActivity; import com.android.settings.Settings.NotificationAppListActivity; import com.android.settings.Settings.StorageUseActivity; import com.android.settings.Settings.UsageAccessSettingsActivity; +import com.android.settings.Settings.OverlaySettingsActivity; +import com.android.settings.Settings.WriteSettingsActivity; import com.android.settings.SettingsActivity; import com.android.settings.Utils; +import com.android.settings.applications.AppStateAppOpsBridge.PermissionState; import com.android.settings.applications.AppStateUsageBridge.UsageState; import com.android.settings.fuelgauge.HighPowerDetail; import com.android.settings.notification.AppNotificationSettings; @@ -122,6 +125,8 @@ public class ManageApplications extends InstrumentedFragment public static final int FILTER_APPS_WORK = 10; public static final int FILTER_APPS_WITH_DOMAIN_URLS = 11; public static final int FILTER_APPS_USAGE_ACCESS = 12; + public static final int FILTER_APPS_WITH_OVERLAY = 13; + public static final int FILTER_APPS_WRITE_SETTINGS = 14; // This is the string labels for the filter modes above, the order must be kept in sync. public static final int[] FILTER_LABELS = new int[] { @@ -138,6 +143,8 @@ public class ManageApplications extends InstrumentedFragment R.string.filter_work_apps, // Work R.string.filter_with_domain_urls_apps, // Domain URLs R.string.filter_all_apps, // Usage access screen, never displayed + R.string.filter_overlay_apps, // Apps with overlay permission + R.string.filter_write_settings_apps, // Apps that can write system settings }; // This is the actual mapping to filters from FILTER_ constants above, the order must // be kept in sync. @@ -155,6 +162,8 @@ public class ManageApplications extends InstrumentedFragment ApplicationsState.FILTER_WORK, // Work ApplicationsState.FILTER_WITH_DOMAIN_URLS, // Apps with Domain URLs AppStateUsageBridge.FILTER_APP_USAGE, // Apps with Domain URLs + AppStateOverlayBridge.FILTER_SYSTEM_ALERT_WINDOW, // Apps that can draw overlays + AppStateWriteSettingsBridge.FILTER_WRITE_SETTINGS, // Apps that can write system settings }; // sort order @@ -195,6 +204,8 @@ public class ManageApplications extends InstrumentedFragment public static final int LIST_TYPE_STORAGE = 3; public static final int LIST_TYPE_USAGE_ACCESS = 4; public static final int LIST_TYPE_HIGH_POWER = 5; + public static final int LIST_TYPE_OVERLAY = 6; + public static final int LIST_TYPE_WRITE_SETTINGS = 7; private View mRootView; @@ -252,6 +263,12 @@ public class ManageApplications extends InstrumentedFragment startApplicationDetailsActivity(); } } + } else if (className.equals(OverlaySettingsActivity.class.getName())) { + mListType = LIST_TYPE_OVERLAY; + getActivity().getActionBar().setTitle(R.string.system_alert_window_access_title); + } else if (className.equals(WriteSettingsActivity.class.getName())) { + mListType = LIST_TYPE_WRITE_SETTINGS; + getActivity().getActionBar().setTitle(R.string.write_settings_title); } else { mListType = LIST_TYPE_MAIN; } @@ -358,6 +375,10 @@ public class ManageApplications extends InstrumentedFragment return FILTER_APPS_USAGE_ACCESS; case LIST_TYPE_HIGH_POWER: return FILTER_APPS_POWER_WHITELIST; + case LIST_TYPE_OVERLAY: + return FILTER_APPS_WITH_OVERLAY; + case LIST_TYPE_WRITE_SETTINGS: + return FILTER_APPS_WRITE_SETTINGS; default: return FILTER_APPS_ALL; } @@ -378,6 +399,10 @@ public class ManageApplications extends InstrumentedFragment return MetricsLogger.USAGE_ACCESS; case LIST_TYPE_HIGH_POWER: return MetricsLogger.APPLICATIONS_HIGH_POWER_APPS; + case LIST_TYPE_OVERLAY: + return MetricsLogger.SYSTEM_ALERT_WINDOW_APPS; + case LIST_TYPE_WRITE_SETTINGS: + return MetricsLogger.SYSTEM_ALERT_WINDOW_APPS; default: return MetricsLogger.VIEW_UNKNOWN; } @@ -430,7 +455,8 @@ public class ManageApplications extends InstrumentedFragment if (requestCode == INSTALLED_APP_DETAILS && mCurrentPkgName != null) { if (mListType == LIST_TYPE_NOTIFICATION) { mApplications.mExtraInfoBridge.forceUpdate(mCurrentPkgName, mCurrentUid); - } else if (mListType == LIST_TYPE_HIGH_POWER) { + } else if (mListType == LIST_TYPE_HIGH_POWER || mListType == LIST_TYPE_OVERLAY + || mListType == LIST_TYPE_WRITE_SETTINGS) { if (mFinishAfterDialog) { getActivity().onBackPressed(); } else { @@ -462,6 +488,12 @@ public class ManageApplications extends InstrumentedFragment HighPowerDetail.show(this, mCurrentPkgName, INSTALLED_APP_DETAILS, mFinishAfterDialog); break; + case LIST_TYPE_OVERLAY: + startAppInfoFragment(DrawOverlayDetails.class, R.string.overlay_settings); + break; + case LIST_TYPE_WRITE_SETTINGS: + startAppInfoFragment(WriteSettingsDetails.class, R.string.write_system_settings); + break; // TODO: Figure out if there is a way where we can spin up the profile's settings // process ahead of time, to avoid a long load of data when user clicks on a managed app. // Maybe when they load the list of apps that contains managed profile apps. @@ -719,6 +751,10 @@ public class ManageApplications extends InstrumentedFragment mExtraInfoBridge = new AppStateUsageBridge(mContext, mState, this); } else if (mManageApplications.mListType == LIST_TYPE_HIGH_POWER) { mExtraInfoBridge = new AppStatePowerBridge(mState, this); + } else if (mManageApplications.mListType == LIST_TYPE_OVERLAY) { + mExtraInfoBridge = new AppStateOverlayBridge(mContext, mState, this); + } else if (mManageApplications.mListType == LIST_TYPE_WRITE_SETTINGS) { + mExtraInfoBridge = new AppStateWriteSettingsBridge(mContext, mState, this); } else { mExtraInfoBridge = null; } @@ -1017,8 +1053,9 @@ public class ManageApplications extends InstrumentedFragment case LIST_TYPE_USAGE_ACCESS: if (holder.entry.extraInfo != null) { - holder.summary.setText(((UsageState) holder.entry.extraInfo).hasAccess() ? - R.string.switch_on_text : R.string.switch_off_text); + holder.summary.setText((new UsageState((PermissionState)holder.entry + .extraInfo)).isPermissible() ? R.string.switch_on_text : + R.string.switch_off_text); } else { holder.summary.setText(null); } @@ -1028,6 +1065,16 @@ public class ManageApplications extends InstrumentedFragment holder.summary.setText(HighPowerDetail.getSummary(mContext, holder.entry)); break; + case LIST_TYPE_OVERLAY: + holder.summary.setText(DrawOverlayDetails.getSummary(mContext, + holder.entry)); + break; + + case LIST_TYPE_WRITE_SETTINGS: + holder.summary.setText(WriteSettingsDetails.getSummary(mContext, + holder.entry)); + break; + default: holder.updateSizeText(mManageApplications.mInvalidSizeStr, mWhichSize); break; diff --git a/src/com/android/settings/applications/UsageAccessDetails.java b/src/com/android/settings/applications/UsageAccessDetails.java index 6d5995b78a3..5317282cd94 100644 --- a/src/com/android/settings/applications/UsageAccessDetails.java +++ b/src/com/android/settings/applications/UsageAccessDetails.java @@ -40,8 +40,10 @@ import com.android.settings.applications.AppStateUsageBridge.UsageState; public class UsageAccessDetails extends AppInfoWithHeader implements OnPreferenceChangeListener, OnPreferenceClickListener { - private static final String KEY_USAGE_SWITCH = "usage_switch"; - private static final String KEY_USAGE_PREFS = "app_usage_preference"; + private static final String KEY_APP_OPS_PREFERENCE_SCREEN = "app_ops_preference_screen"; + private static final String KEY_APP_OPS_SETTINGS_SWITCH = "app_ops_settings_switch"; + private static final String KEY_APP_OPS_SETTINGS_PREFS = "app_ops_settings_preference"; + private static final String KEY_APP_OPS_SETTINGS_DESC = "app_ops_settings_description"; // Use a bridge to get the usage stats but don't initialize it to connect with all state. // TODO: Break out this functionality into its own class. @@ -49,6 +51,7 @@ public class UsageAccessDetails extends AppInfoWithHeader implements OnPreferenc private AppOpsManager mAppOpsManager; private SwitchPreference mSwitchPref; private Preference mUsagePrefs; + private Preference mUsageDesc; private Intent mSettingsIntent; private UsageState mUsageState; private DevicePolicyManager mDpm; @@ -62,9 +65,15 @@ public class UsageAccessDetails extends AppInfoWithHeader implements OnPreferenc mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); mDpm = context.getSystemService(DevicePolicyManager.class); - addPreferencesFromResource(R.xml.usage_access_details); - mSwitchPref = (SwitchPreference) findPreference(KEY_USAGE_SWITCH); - mUsagePrefs = findPreference(KEY_USAGE_PREFS); + addPreferencesFromResource(R.xml.app_ops_permissions_details); + mSwitchPref = (SwitchPreference) findPreference(KEY_APP_OPS_SETTINGS_SWITCH); + mUsagePrefs = findPreference(KEY_APP_OPS_SETTINGS_PREFS); + mUsageDesc = findPreference(KEY_APP_OPS_SETTINGS_DESC); + + getPreferenceScreen().setTitle(R.string.usage_access); + mSwitchPref.setTitle(R.string.permit_usage_access); + mUsagePrefs.setTitle(R.string.app_usage_preference); + mUsageDesc.setSummary(R.string.usage_access_description); mSwitchPref.setOnPreferenceChangeListener(this); mUsagePrefs.setOnPreferenceClickListener(this); @@ -92,8 +101,8 @@ public class UsageAccessDetails extends AppInfoWithHeader implements OnPreferenc @Override public boolean onPreferenceChange(Preference preference, Object newValue) { if (preference == mSwitchPref) { - if (mUsageState != null && (Boolean) newValue != mUsageState.hasAccess()) { - if (mUsageState.hasAccess() && mDpm.isProfileOwnerApp(mPackageName)) { + if (mUsageState != null && (Boolean) newValue != mUsageState.isPermissible()) { + if (mUsageState.isPermissible() && mDpm.isProfileOwnerApp(mPackageName)) { new AlertDialog.Builder(getContext()) .setIcon(com.android.internal.R.drawable.ic_dialog_alert_material) .setTitle(android.R.string.dialog_alert_title) @@ -101,7 +110,7 @@ public class UsageAccessDetails extends AppInfoWithHeader implements OnPreferenc .setPositiveButton(R.string.okay, null) .show(); } - setHasAccess(!mUsageState.hasAccess()); + setHasAccess(!mUsageState.isPermissible()); refreshUi(); } return true; @@ -119,14 +128,14 @@ public class UsageAccessDetails extends AppInfoWithHeader implements OnPreferenc mUsageState = mUsageBridge.getUsageInfo(mPackageName, mPackageInfo.applicationInfo.uid); - boolean hasAccess = mUsageState.hasAccess(); + boolean hasAccess = mUsageState.isPermissible(); mSwitchPref.setChecked(hasAccess); mUsagePrefs.setEnabled(hasAccess); ResolveInfo resolveInfo = mPm.resolveActivityAsUser(mSettingsIntent, PackageManager.GET_META_DATA, mUserId); if (resolveInfo != null) { - if (findPreference(KEY_USAGE_PREFS) == null) { + if (findPreference(KEY_APP_OPS_SETTINGS_PREFS) == null) { getPreferenceScreen().addPreference(mUsagePrefs); } Bundle metaData = resolveInfo.activityInfo.metaData; @@ -138,7 +147,7 @@ public class UsageAccessDetails extends AppInfoWithHeader implements OnPreferenc metaData.getString(Settings.METADATA_USAGE_ACCESS_REASON)); } } else { - if (findPreference(KEY_USAGE_PREFS) != null) { + if (findPreference(KEY_APP_OPS_SETTINGS_PREFS) != null) { getPreferenceScreen().removePreference(mUsagePrefs); } } diff --git a/src/com/android/settings/applications/WriteSettingsDetails.java b/src/com/android/settings/applications/WriteSettingsDetails.java new file mode 100644 index 00000000000..eeee90c14d3 --- /dev/null +++ b/src/com/android/settings/applications/WriteSettingsDetails.java @@ -0,0 +1,211 @@ +/* + * 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.AlertDialog; +import android.app.AppOpsManager; +import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Bundle; +import android.os.UserHandle; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceChangeListener; +import android.preference.Preference.OnPreferenceClickListener; +import android.preference.SwitchPreference; +import android.provider.Settings; +import android.util.Log; + +import com.android.internal.logging.MetricsLogger; +import com.android.settings.InstrumentedFragment; +import com.android.settings.R; +import com.android.settings.applications.AppStateWriteSettingsBridge.WriteSettingsState; +import com.android.settingslib.applications.ApplicationsState; +import com.android.settingslib.applications.ApplicationsState.AppEntry; + +import java.util.List; + +public class WriteSettingsDetails extends AppInfoWithHeader implements OnPreferenceChangeListener, + OnPreferenceClickListener { + + private static final String KEY_APP_OPS_PREFERENCE_SCREEN = "app_ops_preference_screen"; + private static final String KEY_APP_OPS_SETTINGS_SWITCH = "app_ops_settings_switch"; + private static final String KEY_APP_OPS_SETTINGS_PREFS = "app_ops_settings_preference"; + private static final String KEY_APP_OPS_SETTINGS_DESC = "app_ops_settings_description"; + private static final String LOG_TAG = "WriteSettingsDetails"; + + private static final int [] APP_OPS_OP_CODE = { + AppOpsManager.OP_WRITE_SETTINGS + }; + + // Use a bridge to get the overlay details but don't initialize it to connect with all state. + // TODO: Break out this functionality into its own class. + private AppStateWriteSettingsBridge mAppBridge; + private AppOpsManager mAppOpsManager; + private SwitchPreference mSwitchPref; + private Preference mWriteSettingsPrefs; + private Preference mWriteSettingsDesc; + private Intent mSettingsIntent; + private WriteSettingsState mWriteSettingsState; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Context context = getActivity(); + mAppBridge = new AppStateWriteSettingsBridge(context, mState, null); + mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); + + addPreferencesFromResource(R.xml.app_ops_permissions_details); + mSwitchPref = (SwitchPreference) findPreference(KEY_APP_OPS_SETTINGS_SWITCH); + mWriteSettingsPrefs = findPreference(KEY_APP_OPS_SETTINGS_PREFS); + mWriteSettingsDesc = findPreference(KEY_APP_OPS_SETTINGS_DESC); + + getPreferenceScreen().setTitle(R.string.write_settings); + mSwitchPref.setTitle(R.string.permit_write_settings); + mWriteSettingsPrefs.setTitle(R.string.write_settings_preference); + mWriteSettingsDesc.setSummary(R.string.write_settings_description); + + mSwitchPref.setOnPreferenceChangeListener(this); + mWriteSettingsPrefs.setOnPreferenceClickListener(this); + + mSettingsIntent = new Intent(Intent.ACTION_MAIN) + .addCategory(Settings.INTENT_CATEGORY_USAGE_ACCESS_CONFIG) + .setPackage(mPackageName); + } + + @Override + public boolean onPreferenceClick(Preference preference) { + if (preference == mWriteSettingsPrefs) { + if (mSettingsIntent != null) { + try { + getActivity().startActivityAsUser(mSettingsIntent, new UserHandle(mUserId)); + } catch (ActivityNotFoundException e) { + Log.w(TAG, "Unable to launch write system settings " + mSettingsIntent, e); + } + } + return true; + } + return false; + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (preference == mSwitchPref) { + if (mWriteSettingsState != null && (Boolean) newValue != mWriteSettingsState.canWrite()) { + setCanWriteSettings(!mWriteSettingsState.canWrite()); + refreshUi(); + } + return true; + } + return false; + } + + private void setCanWriteSettings(boolean newState) { + mAppOpsManager.setMode(AppOpsManager.OP_WRITE_SETTINGS, + mPackageInfo.applicationInfo.uid, mPackageName, newState + ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_ERRORED); + canWriteSettings(mPackageName); + } + + private boolean canWriteSettings(String pkgName) { + int result = mAppOpsManager.noteOpNoThrow(AppOpsManager.OP_WRITE_SETTINGS, + mPackageInfo.applicationInfo.uid, pkgName); + if (result == AppOpsManager.MODE_ALLOWED) { + return true; + } + + return false; + } + + @Override + protected boolean refreshUi() { + mWriteSettingsState = mAppBridge.getWriteSettingsInfo(mPackageName, + mPackageInfo.applicationInfo.uid); + + boolean canWrite = mWriteSettingsState.canWrite(); + mSwitchPref.setChecked(canWrite); + mWriteSettingsPrefs.setEnabled(canWrite); + + ResolveInfo resolveInfo = mPm.resolveActivityAsUser(mSettingsIntent, + PackageManager.GET_META_DATA, mUserId); + if (resolveInfo == null) { + if (findPreference(KEY_APP_OPS_SETTINGS_PREFS) != null) { + getPreferenceScreen().removePreference(mWriteSettingsPrefs); + } + } + + return true; + } + + @Override + protected AlertDialog createDialog(int id, int errorCode) { + return null; + } + + @Override + protected int getMetricsCategory() { + return MetricsLogger.SYSTEM_ALERT_WINDOW_APPS; + } + + public static CharSequence getSummary(Context context, AppEntry entry) { + return getSummary(context, entry.info.packageName); + } + + public static CharSequence getSummary(Context context, String pkg) { + // first check if pkg is a system pkg + boolean isSystem = false; + PackageManager packageManager = context.getPackageManager(); + try { + ApplicationInfo appInfo = packageManager.getApplicationInfo(pkg, 0); + if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { + isSystem = true; + } + } catch (PackageManager.NameNotFoundException e) { + // pkg doesn't even exist? + Log.w(TAG, "Package " + pkg + " not found", e); + return context.getString(R.string.system_alert_window_off); + } + + AppOpsManager appOpsManager = (AppOpsManager) context.getSystemService(Context + .APP_OPS_SERVICE); + List packageOps = appOpsManager.getPackagesForOps( + APP_OPS_OP_CODE); + if (packageOps == null) { + return context.getString(R.string.system_alert_window_off); + } + + int uid = isSystem ? 0 : -1; + for (AppOpsManager.PackageOps packageOp : packageOps) { + if (pkg.equals(packageOp.getPackageName())) { + uid = packageOp.getUid(); + break; + } + } + + if (uid == -1) { + return context.getString(R.string.system_alert_window_off); + } + + int mode = appOpsManager.noteOpNoThrow(AppOpsManager.OP_WRITE_SETTINGS, uid, pkg); + return context.getString((mode == AppOpsManager.MODE_ALLOWED) ? + R.string.write_settings_on : R.string.write_settings_off); + } +}