Adding Manage External Sources Settings

Added a settings fragment to manage external sources. It lists all
applications that have either requested REQUEST_INSTALL_PACKAGES or have
their app op changed from default.

Test: Will include in follow-up CL. Tracked in b/33792674

Bug: 31002700
Change-Id: Ibd2a1922be214b62aec4eefa45f7b9691256b205
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index ed511ef..41db2eb 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -2905,6 +2905,17 @@
                 android:value="com.android.settings.applications.ManageApplications" />
         </activity>
 
+        <activity android:name="Settings$ManageExternalSourcesActivity"
+                android:label="@string/install_other_apps"
+                android:taskAffinity="">
+            <intent-filter android:priority="1">
+                <action android:name="android.settings.action.MANAGE_EXTERNAL_SOURCES" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <meta-data android:name="com.android.settings.FRAGMENT_CLASS"
+                android:value="com.android.settings.applications.ManageApplications" />
+        </activity>
+
         <activity android:name="Settings$AppWriteSettingsActivity"
                 android:label="@string/write_settings_title"
                 android:taskAffinity="">
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 54ed1df..baa9547 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -7224,6 +7224,12 @@
     <!-- Summary of app not allowed to draw overlay [CHAR LIMIT=60] -->
     <string name="system_alert_window_off">No</string>
 
+    <!-- Title for settings screen for controlling apps that can install other apps on device [CHAR LIMIT=30] -->
+    <string name="install_other_apps">Install other apps</string>
+    <!-- Keywords for setting screen for controlling apps that can install other apps on device -->
+    <string name="keywords_install_other_apps">install apps external unknown sources</string>
+    <!-- Label for setting which controls whether app is trusted to install apps on the device [CHAR LIMIT=45] -->
+    <string name="permit_install_other_apps">Allows to install other apps</string>
 
     <!-- Write Settings settings -->
     <!-- Settings title in main settings screen for WRITE_SETTINGS [CHAR LIMIT=30] -->
@@ -7233,6 +7239,8 @@
     <!-- Summary of number of apps currently can draw overlays [CHAR LIMIT=60] -->
     <string name="write_settings_summary"><xliff:g id="count" example="10">%1$d</xliff:g> of <xliff:g id="count" example="10">%2$d</xliff:g> apps allowed to modify system settings</string>
 
+    <!-- Label for showing apps that can install other apps [CHAR LIMIT=45] -->
+    <string name="filter_install_sources_apps">Can install other apps</string>
     <!-- Label for showing apps that can write system settings [CHAR LIMIT=45] -->
     <string name="filter_write_settings_apps">Can modify system settings</string>
     <!-- Title for the apps that are allowed to write system settings [CHAR LIMIT=60] -->
@@ -7249,6 +7257,12 @@
     <string name="write_settings_on">Yes</string>
     <!-- Summary of app not allowed to write system settings [CHAR LIMIT=45] -->
     <string name="write_settings_off">No</string>
+    <!-- Summary of app trusted to install apps [CHAR LIMIT=45] -->
+    <string name="external_source_trusted">Yes</string>
+    <!-- Summary of app not trusted to install apps [CHAR LIMIT=45] -->
+    <string name="external_source_untrusted">No</string>
+    <!-- Title of switch preference that controls whether an external app source is trusted or not [CHAR LIMIT=100] -->
+    <string name="external_source_switch_title">Trust apps from this source</string>
 
     <!-- Title of setting that controls gesture to open camera [CHAR LIMIT=40] -->
     <string name="camera_gesture_title">Double twist for camera</string>
diff --git a/res/xml/external_sources_details.xml b/res/xml/external_sources_details.xml
new file mode 100644
index 0000000..fb443f4
--- /dev/null
+++ b/res/xml/external_sources_details.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <SwitchPreference
+        android:key="external_sources_settings_switch" />
+
+    <Preference
+        android:key="external_sources_settings_description"
+        android:selectable="false" />
+
+</PreferenceScreen>
diff --git a/res/xml/special_access.xml b/res/xml/special_access.xml
index 4de167a..f8a5bf4 100644
--- a/res/xml/special_access.xml
+++ b/res/xml/special_access.xml
@@ -92,4 +92,15 @@
             android:name="classname"
             android:value="com.android.settings.Settings$UsageAccessSettingsActivity" />
     </Preference>
+
+    <Preference
+        android:key="manage_external_sources"
+        android:title="@string/install_other_apps"
+        android:fragment="com.android.settings.applications.ManageApplications"
+        settings:keywords="@string/keywords_install_other_apps">
+        <extra
+            android:name="classname"
+            android:value="com.android.settings.Settings$ManageExternalSourcesActivity" />
+    </Preference>
+
 </PreferenceScreen>
diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java
index 97e53e5..a393436 100644
--- a/src/com/android/settings/Settings.java
+++ b/src/com/android/settings/Settings.java
@@ -138,6 +138,9 @@
     public static class AppWriteSettingsActivity extends SettingsActivity { /* empty */ }
     public static class AdvancedAppsActivity extends SettingsActivity { /* empty */ }
 
+    public static class ManageExternalSourcesActivity extends SettingsActivity {
+        /* empty */ }
+
     public static class WifiCallingSuggestionActivity extends SettingsActivity { /* empty */ }
     public static class ZenModeAutomationSuggestionActivity extends SettingsActivity { /* empty */ }
     public static class FingerprintSuggestionActivity extends FingerprintSettings { /* empty */ }
diff --git a/src/com/android/settings/applications/AppStateInstallAppsBridge.java b/src/com/android/settings/applications/AppStateInstallAppsBridge.java
new file mode 100644
index 0000000..6d22e25
--- /dev/null
+++ b/src/com/android/settings/applications/AppStateInstallAppsBridge.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2017 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.content.Context;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.settings.R;
+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 op info to the ApplicationsState. Wraps around the generic AppStateBaseBridge
+ * class to tailor to the semantics of {@link AppOpsManager#OP_REQUEST_INSTALL_PACKAGES}
+ * Also provides app filters that can use the info.
+ */
+public class AppStateInstallAppsBridge extends AppStateBaseBridge {
+
+    private static final String TAG = AppStateInstallAppsBridge.class.getSimpleName();
+
+    private final IPackageManager mIpm;
+    private final AppOpsManager mAppOpsManager;
+    private final Context mContext;
+
+    public AppStateInstallAppsBridge(Context context, ApplicationsState appState,
+            Callback callback) {
+        super(appState, callback);
+        mContext = context;
+        mIpm = AppGlobals.getPackageManager();
+        mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
+    }
+
+    @Override
+    protected void updateExtraInfo(AppEntry app, String packageName, int uid) {
+        app.extraInfo = createInstallAppsStateFor(packageName, uid);
+    }
+
+    @Override
+    protected void loadAllExtraInfo() {
+        // TODO: consider making this a batch operation with a single binder call
+        final List<AppEntry> allApps = mAppSession.getAllApps();
+        for (int i = 0; i < allApps.size(); i++) {
+            AppEntry currentEntry = allApps.get(i);
+            updateExtraInfo(currentEntry, currentEntry.info.packageName, currentEntry.info.uid);
+        }
+    }
+
+    private boolean hasRequestedAppOpPermission(String permission, String packageName) {
+        try {
+            String[] packages = mIpm.getAppOpPermissionPackages(permission);
+            return ArrayUtils.contains(packages, packageName);
+        } catch (RemoteException exc) {
+            Log.e(TAG, "PackageManager dead. Cannot get permission info");
+            return false;
+        }
+    }
+
+    private boolean hasPermission(String permission, int uid) {
+        try {
+            int result = mIpm.checkUidPermission(permission, uid);
+            return result == PackageManager.PERMISSION_GRANTED;
+        } catch (RemoteException e) {
+            Log.e(TAG, "PackageManager dead. Cannot get permission info");
+            return false;
+        }
+    }
+
+    private int getAppOpMode(int appOpCode, int uid, String packageName) {
+        return mAppOpsManager.checkOpNoThrow(appOpCode, uid, packageName);
+    }
+
+    InstallAppsState createInstallAppsStateFor(String packageName, int uid) {
+        final InstallAppsState appState = new InstallAppsState();
+        appState.permissionRequested = hasRequestedAppOpPermission(
+                Manifest.permission.REQUEST_INSTALL_PACKAGES, packageName);
+        appState.permissionGranted = hasPermission(Manifest.permission.REQUEST_INSTALL_PACKAGES,
+                uid);
+        appState.appOpMode = getAppOpMode(AppOpsManager.OP_REQUEST_INSTALL_PACKAGES, uid,
+                packageName);
+        return appState;
+    }
+
+    /**
+     * Collection of information to be used as {@link AppEntry#extraInfo} objects
+     */
+    public static class InstallAppsState {
+        boolean permissionRequested;
+        boolean permissionGranted;
+        int appOpMode;
+
+        public InstallAppsState() {
+            this.appOpMode = AppOpsManager.MODE_DEFAULT;
+        }
+
+        public boolean canInstallApps() {
+            if (appOpMode == AppOpsManager.MODE_DEFAULT) {
+                return permissionGranted;
+            } else {
+                return appOpMode == AppOpsManager.MODE_ALLOWED;
+            }
+        }
+
+        public int getSummary() {
+            return canInstallApps() ? R.string.external_source_trusted
+                    : R.string.external_source_untrusted;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder("[permissionGranted: " + permissionGranted);
+            sb.append(", permissionRequested: " + permissionRequested);
+            sb.append(", appOpMode: " + appOpMode);
+            sb.append("]");
+            return sb.toString();
+        }
+    }
+
+    static final AppFilter FILTER_APP_SOURCES = new AppFilter() {
+
+        @Override
+        public void init() {
+        }
+
+        @Override
+        public boolean filterApp(AppEntry info) {
+            if (info.extraInfo == null || !(info.extraInfo instanceof InstallAppsState)) {
+                return false;
+            }
+            InstallAppsState state = (InstallAppsState) info.extraInfo;
+            return (state.appOpMode != AppOpsManager.MODE_DEFAULT) || state.permissionRequested;
+        }
+    };
+}
diff --git a/src/com/android/settings/applications/ExternalSourcesDetails.java b/src/com/android/settings/applications/ExternalSourcesDetails.java
new file mode 100644
index 0000000..fd8221c
--- /dev/null
+++ b/src/com/android/settings/applications/ExternalSourcesDetails.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2017 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 com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+
+import android.app.AlertDialog;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v14.preference.SwitchPreference;
+import android.support.v7.preference.Preference;
+import android.support.v7.preference.Preference.OnPreferenceChangeListener;
+
+import com.android.settings.R;
+import com.android.settings.applications.AppStateInstallAppsBridge.InstallAppsState;
+
+public class ExternalSourcesDetails extends AppInfoWithHeader
+        implements OnPreferenceChangeListener {
+
+    private static final String KEY_EXTERNAL_SOURCES_SETTINGS_SWITCH =
+            "external_sources_settings_switch";
+    private static final String KEY_EXTERNAL_SOURCES_SETTINGS_DESC =
+            "external_sources_settings_description";
+
+    private AppStateInstallAppsBridge mAppBridge;
+    private AppOpsManager mAppOpsManager;
+    private SwitchPreference mSwitchPref;
+    private Preference mExternalSourcesSettingsDesc;
+    private InstallAppsState mInstallAppsState;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        final Context context = getActivity();
+        mAppBridge = new AppStateInstallAppsBridge(context, mState, null);
+        mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
+
+        addPreferencesFromResource(R.xml.external_sources_details);
+        mSwitchPref = (SwitchPreference) findPreference(KEY_EXTERNAL_SOURCES_SETTINGS_SWITCH);
+        mExternalSourcesSettingsDesc = findPreference(KEY_EXTERNAL_SOURCES_SETTINGS_DESC);
+
+        getPreferenceScreen().setTitle(R.string.install_other_apps);
+        mSwitchPref.setTitle(R.string.external_source_switch_title);
+        mExternalSourcesSettingsDesc.setSummary(R.string.install_all_warning);
+
+        mSwitchPref.setOnPreferenceChangeListener(this);
+    }
+
+    @Override
+    public boolean onPreferenceChange(Preference preference, Object newValue) {
+        final boolean checked = (Boolean) newValue;
+        if (preference == mSwitchPref) {
+            if (mInstallAppsState != null && checked != mInstallAppsState.canInstallApps()) {
+                setCanInstallApps(checked);
+                refreshUi();
+            }
+            return true;
+        }
+        return false;
+    }
+
+    private void setCanInstallApps(boolean newState) {
+        mAppOpsManager.setMode(AppOpsManager.OP_REQUEST_INSTALL_PACKAGES,
+                mPackageInfo.applicationInfo.uid, mPackageName,
+                newState ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_ERRORED);
+    }
+
+    @Override
+    protected boolean refreshUi() {
+        mInstallAppsState = mAppBridge.createInstallAppsStateFor(mPackageName,
+                mPackageInfo.applicationInfo.uid);
+
+        final boolean canWrite = mInstallAppsState.canInstallApps();
+        mSwitchPref.setChecked(canWrite);
+        return true;
+    }
+
+    @Override
+    protected AlertDialog createDialog(int id, int errorCode) {
+        return null;
+    }
+
+    @Override
+    public int getMetricsCategory() {
+        return MetricsEvent.MANAGE_EXTERNAL_SOURCES;
+    }
+}
diff --git a/src/com/android/settings/applications/ManageApplications.java b/src/com/android/settings/applications/ManageApplications.java
index 43bb02a..a87ba53 100644
--- a/src/com/android/settings/applications/ManageApplications.java
+++ b/src/com/android/settings/applications/ManageApplications.java
@@ -58,6 +58,7 @@
 import com.android.settings.R;
 import com.android.settings.Settings.AllApplicationsActivity;
 import com.android.settings.Settings.HighPowerApplicationsActivity;
+import com.android.settings.Settings.ManageExternalSourcesActivity;
 import com.android.settings.Settings.NotificationAppListActivity;
 import com.android.settings.Settings.OverlaySettingsActivity;
 import com.android.settings.Settings.StorageUseActivity;
@@ -66,6 +67,7 @@
 import com.android.settings.SettingsActivity;
 import com.android.settings.Utils;
 import com.android.settings.applications.AppStateAppOpsBridge.PermissionState;
+import com.android.settings.applications.AppStateInstallAppsBridge.InstallAppsState;
 import com.android.settings.applications.AppStateUsageBridge.UsageState;
 import com.android.settings.core.InstrumentedPreferenceFragment;
 import com.android.settings.dashboard.SummaryLoader;
@@ -136,6 +138,7 @@
     public static final int FILTER_APPS_USAGE_ACCESS = 8;
     public static final int FILTER_APPS_WITH_OVERLAY = 9;
     public static final int FILTER_APPS_WRITE_SETTINGS = 10;
+    public static final int FILTER_APPS_INSTALL_SOURCES = 12;
 
     // 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[]{
@@ -151,6 +154,7 @@
             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
+            R.string.filter_install_sources_apps, // Apps that are trusted sources of apks
     };
     // This is the actual mapping to filters from FILTER_ constants above, the order must
     // be kept in sync.
@@ -169,6 +173,7 @@
             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
+            AppStateInstallAppsBridge.FILTER_APP_SOURCES,
     };
 
     // sort order
@@ -210,6 +215,7 @@
     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;
+    public static final int LIST_TYPE_MANAGE_SOURCES = 8;
 
     private View mRootView;
 
@@ -259,6 +265,8 @@
             mListType = LIST_TYPE_OVERLAY;
         } else if (className.equals(WriteSettingsActivity.class.getName())) {
             mListType = LIST_TYPE_WRITE_SETTINGS;
+        } else if (className.equals(ManageExternalSourcesActivity.class.getName())) {
+            mListType = LIST_TYPE_MANAGE_SOURCES;
         } else {
             mListType = LIST_TYPE_MAIN;
         }
@@ -379,6 +387,8 @@
                 return FILTER_APPS_WITH_OVERLAY;
             case LIST_TYPE_WRITE_SETTINGS:
                 return FILTER_APPS_WRITE_SETTINGS;
+            case LIST_TYPE_MANAGE_SOURCES:
+                return FILTER_APPS_INSTALL_SOURCES;
             default:
                 return FILTER_APPS_ALL;
         }
@@ -412,6 +422,8 @@
                 return MetricsEvent.SYSTEM_ALERT_WINDOW_APPS;
             case LIST_TYPE_WRITE_SETTINGS:
                 return MetricsEvent.SYSTEM_ALERT_WINDOW_APPS;
+            case LIST_TYPE_MANAGE_SOURCES:
+                return MetricsEvent.MANAGE_EXTERNAL_SOURCES;
             default:
                 return MetricsEvent.VIEW_UNKNOWN;
         }
@@ -503,6 +515,9 @@
             case LIST_TYPE_WRITE_SETTINGS:
                 startAppInfoFragment(WriteSettingsDetails.class, R.string.write_system_settings);
                 break;
+            case LIST_TYPE_MANAGE_SOURCES:
+                startAppInfoFragment(ExternalSourcesDetails.class, R.string.install_other_apps);
+                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.
@@ -796,6 +811,8 @@
                 mExtraInfoBridge = new AppStateOverlayBridge(mContext, mState, this);
             } else if (mManageApplications.mListType == LIST_TYPE_WRITE_SETTINGS) {
                 mExtraInfoBridge = new AppStateWriteSettingsBridge(mContext, mState, this);
+            } else if (mManageApplications.mListType == LIST_TYPE_MANAGE_SOURCES) {
+                mExtraInfoBridge = new AppStateInstallAppsBridge(mContext, mState, this);
             } else {
                 mExtraInfoBridge = null;
             }
@@ -1206,6 +1223,11 @@
                             holder.entry));
                     break;
 
+                case LIST_TYPE_MANAGE_SOURCES:
+                    holder.summary
+                            .setText(((InstallAppsState) holder.entry.extraInfo).getSummary());
+                    break;
+
                 default:
                     holder.updateSizeText(mManageApplications.mInvalidSizeStr, mWhichSize);
                     break;