Port Superuser management UI

Port the feature from our Android 9 implementation, originally from
LineageOS.

Issue: FP2A10-127
Change-Id: Ic93f926b9f00d22380e65c4967685562485505ce
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 842a902..de4296b 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -1226,6 +1226,21 @@
             </intent-filter>
         </activity>
 
+        <activity android:name="Settings$AppOpsSummaryActivity"
+                android:label="@string/privacy_guard_manager_title"
+                android:taskAffinity=""
+                android:excludeFromRecents="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.settings.APP_OPS_SETTINGS" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.VOICE_LAUNCH" />
+                <category android:name="com.android.settings.SHORTCUT" />
+            </intent-filter>
+            <meta-data android:name="com.android.settings.FRAGMENT_CLASS"
+                android:value="com.android.settings.applications.appops.AppOpsSummary" />
+        </activity>
+
         <activity
             android:name="Settings$LocationSettingsActivity"
             android:label="@string/location_settings_title"
diff --git a/res/layout/app_ops_summary.xml b/res/layout/app_ops_summary.xml
new file mode 100644
index 0000000..01510f5
--- /dev/null
+++ b/res/layout/app_ops_summary.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2013, 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:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <androidx.viewpager.widget.ViewPager
+            android:id="@+id/pager"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_weight="1">
+        <androidx.viewpager.widget.PagerTabStrip
+                android:id="@+id/tabs"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_gravity="top"
+                android:textAppearance="@style/TextAppearance.PagerTabs"
+                android:paddingLeft="@dimen/pager_tabs_padding"
+                android:paddingRight="@dimen/pager_tabs_padding">
+        </androidx.viewpager.widget.PagerTabStrip>
+    </androidx.viewpager.widget.ViewPager>
+
+</LinearLayout>
diff --git a/res/menu/appops_manager.xml b/res/menu/appops_manager.xml
new file mode 100644
index 0000000..393d827
--- /dev/null
+++ b/res/menu/appops_manager.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The CyanogenMod 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.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@+id/show_user_apps"
+          android:title="@string/app_ops_show_user_apps"
+          android:checkable="true" />
+    <item android:id="@+id/show_system_apps"
+          android:title="@string/app_ops_show_system_apps"
+          android:checkable="true" />
+</menu>
diff --git a/res/values/cm_strings.xml b/res/values/cm_strings.xml
index af6954f..8e71ea8 100644
--- a/res/values/cm_strings.xml
+++ b/res/values/cm_strings.xml
@@ -16,6 +16,28 @@
      limitations under the License.
 -->
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- App ops permissions -->
+    <string name="app_ops_permissions_allowed">Allowed</string>
+    <string name="app_ops_permissions_ignored">Ignored</string>
+    <string name="app_ops_permissions_always_ask">Always ask</string>
+
+    <!-- App ops menu options -->
+    <string name="app_ops_show_user_apps">Show user apps</string>
+    <string name="app_ops_show_system_apps">Show built-in apps</string>
+
+    <!-- Names of categories of app ops tabs - extension of AOSP -->
+    <string name="app_ops_categories_location">Location</string>
+    <string name="app_ops_categories_personal">Personal</string>
+    <string name="app_ops_categories_messaging">Messaging</string>
+    <string name="app_ops_categories_media">Media</string>
+    <string name="app_ops_categories_device">Device</string>
+    <string name="app_ops_categories_run_in_background">Run in background</string>
+    <string name="app_ops_categories_bootup">Bootup</string>
+    <string name="app_ops_categories_su">Root access</string>
+    <string name="app_ops_categories_other">Other</string>
+
+    <string name="app_ops_labels_su">Get root access</string>
+
     <!-- Manual provisioning support -->
     <string name="sim_enabler_summary"><xliff:g id="displayName">%1$s</xliff:g> is <xliff:g id="status" example="disabled">%2$s</xliff:g></string>
     <string name="sim_disabled">disabled</string>
@@ -39,4 +61,8 @@
     <string name="root_access_apps">Apps only</string>
     <string name="root_access_adb">ADB only</string>
     <string name="root_access_all">Apps and ADB</string>
+
+    <!-- Preference link for root appops -->
+    <string name="root_appops_title">Manage root accesses</string>
+    <string name="root_appops_summary">View and control the root rules</string>
 </resources>
diff --git a/res/values/lineage_arrays.xml b/res/values/lineage_arrays.xml
index 8e51bee..a0a2caf 100644
--- a/res/values/lineage_arrays.xml
+++ b/res/values/lineage_arrays.xml
@@ -40,4 +40,9 @@
         <item>0</item>
         <item>2</item>
     </string-array>
+
+    <!-- Names of categories of app ops tabs - extension of AOSP -->
+    <string-array name="app_ops_categories_lineage" translatable="false">
+        <item>@string/app_ops_categories_su</item>
+    </string-array>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index c95c69f..eeb8573 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -11447,4 +11447,7 @@
 
     <!-- Label for button to not allow grant the permission for remote devices. [CHAR_LIMIT=50] -->
     <string name="request_manage_bluetooth_permission_dont_allow">Don\u2019t allow</string>
+
+    <!-- Privacy Guard -->
+    <string name="privacy_guard_manager_title">Privacy Guard</string>
 </resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index d3d3199..c311805 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -197,6 +197,8 @@
         <item name="android:textAppearance">@android:style/TextAppearance.DeviceDefault.Small</item>
     </style>
 
+    <style name="TextAppearance.PagerTabs" parent="@android:style/TextAppearance.Material.Widget.TabWidget" />
+
     <style name="TextAppearance" parent="android:TextAppearance.DeviceDefault"/>
 
     <style name="TextAppearance.info_label">
diff --git a/res/xml/development_settings.xml b/res/xml/development_settings.xml
index b142383..c7b92c1 100644
--- a/res/xml/development_settings.xml
+++ b/res/xml/development_settings.xml
@@ -136,6 +136,10 @@
         android:title="@string/root_access"
         android:persistent="false" />
 
+    <Preference
+        android:key="root_appops"
+        android:title="@string/root_appops_title"
+        android:summary="@string/root_appops_summary" />
     </PreferenceCategory>
 
     <PreferenceCategory
diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java
index e63edd7..46e7b5f 100644
--- a/src/com/android/settings/Settings.java
+++ b/src/com/android/settings/Settings.java
@@ -18,6 +18,7 @@
 
 import android.os.Bundle;
 
+import com.android.settings.applications.appops.AppOpsSummary;
 import com.android.settings.enterprise.EnterprisePrivacySettings;
 
 /**
@@ -62,6 +63,15 @@
     public static class ManageApplicationsActivity extends SettingsActivity { /* empty */ }
     public static class ManageAssistActivity extends SettingsActivity { /* empty */ }
     public static class HighPowerApplicationsActivity extends SettingsActivity { /* empty */ }
+    public static class AppOpsSummaryActivity extends SettingsActivity {
+        @Override
+        public boolean isValidFragment(String className) {
+            if (AppOpsSummary.class.getName().equals(className)) {
+                return true;
+            }
+            return super.isValidFragment(className);
+        }
+    }
     public static class BackgroundCheckSummaryActivity extends SettingsActivity { /* empty */ }
     public static class StorageUseActivity extends SettingsActivity { /* empty */ }
     public static class DevelopmentSettingsDashboardActivity extends SettingsActivity { /* empty */ }
diff --git a/src/com/android/settings/applications/appops/AppOpsCategory.java b/src/com/android/settings/applications/appops/AppOpsCategory.java
index 57d7dc4..5482cae 100644
--- a/src/com/android/settings/applications/appops/AppOpsCategory.java
+++ b/src/com/android/settings/applications/appops/AppOpsCategory.java
@@ -16,9 +16,11 @@
 
 package com.android.settings.applications.appops;
 
+import android.app.AlertDialog;
 import android.app.AppOpsManager;
 import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.ActivityInfo;
@@ -33,6 +35,7 @@
 import android.widget.ListView;
 import android.widget.Switch;
 import android.widget.TextView;
+import android.widget.Toast;
 
 import androidx.fragment.app.ListFragment;
 import androidx.loader.app.LoaderManager;
@@ -288,14 +291,21 @@
             }
 
             AppOpEntry item = getItem(position);
+
+            AppOpsManager.OpEntry op = item.getOpEntry(0);
+            final int switchOp = AppOpsManager.opToSwitch(op.getOp());
+            final int mode = mState.getAppOpsManager().checkOpNoThrow(switchOp,
+                    item.getAppEntry().getApplicationInfo().uid,
+                    item.getAppEntry().getApplicationInfo().packageName);
+            ((TextView) view.findViewById(R.id.op_name)).setText(
+                    MODE_STRING_RES[modeToPosition(mode)]);
+
             ((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(
+            ((TextView) view.findViewById(R.id.op_time)).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);
+            view.findViewById(R.id.op_switch).setVisibility(View.GONE);
 
             return view;
         }
@@ -331,17 +341,45 @@
     @Override public void onListItemClick(ListView l, View v, int position, long id) {
         AppOpEntry entry = mAdapter.getItem(position);
         if (entry != null) {
-            // We treat this as tapping on the check box, toggling the app op state.
-            Switch sw = 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(),
+
+            final int switchOp = AppOpsManager.opToSwitch(op.getOp());
+            final int mode = mState.getAppOpsManager().checkOpNoThrow(switchOp,
                     entry.getAppEntry().getApplicationInfo().uid,
-                    entry.getAppEntry().getApplicationInfo().packageName,
-                    mode);
-            entry.overridePrimaryOpMode(mode);
+                    entry.getAppEntry().getApplicationInfo().packageName);
+
+            AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+            builder.setTitle(R.string.app_ops_labels_su);
+
+            String[] listItems = {
+                getActivity().getString(MODE_STRING_RES[MODE_ALLOWED]),
+                getActivity().getString(MODE_STRING_RES[MODE_IGNORED]),
+                getActivity().getString(MODE_STRING_RES[MODE_ASK]),
+            };
+
+            builder.setSingleChoiceItems(listItems, modeToPosition(mode), new DialogInterface.OnClickListener() {
+                @Override
+                public void onClick(DialogInterface dialog, int which) {
+                    int mode = positionToMode(which);
+                    mState.getAppOpsManager().setMode(op.getOp(),
+                            entry.getAppEntry().getApplicationInfo().uid,
+                            entry.getAppEntry().getApplicationInfo().packageName,
+                            mode);
+                    entry.overridePrimaryOpMode(mode);
+                    mAdapter.notifyDataSetChanged();
+                    dialog.dismiss();
+                }
+            });
+
+            builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+                @Override
+                public void onClick(DialogInterface dialog, int which) {
+                    dialog.dismiss();
+                }
+            });
+
+            AlertDialog dialog = builder.create();
+            dialog.show();
         }
     }
 
@@ -370,4 +408,40 @@
         // Clear the data in the adapter.
         mAdapter.setData(null);
     }
+
+    private static final int MODE_ALLOWED = 0;
+    private static final int MODE_IGNORED = 1;
+    private static final int MODE_ASK = 2;
+
+    private static final int[] MODE_STRING_RES = {
+        R.string.app_ops_permissions_allowed,
+        R.string.app_ops_permissions_ignored,
+        R.string.app_ops_permissions_always_ask,
+    };
+
+    private static int modeToPosition(int mode) {
+        switch (mode) {
+            case AppOpsManager.MODE_ALLOWED:
+                return MODE_ALLOWED;
+            case AppOpsManager.MODE_IGNORED:
+                return MODE_IGNORED;
+            case AppOpsManager.MODE_ASK:
+                return MODE_ASK;
+            default:
+                return MODE_IGNORED;
+        }
+    }
+
+    private static int positionToMode(int position) {
+        switch (position) {
+            case MODE_ALLOWED:
+                return AppOpsManager.MODE_ALLOWED;
+            case MODE_IGNORED:
+                return AppOpsManager.MODE_IGNORED;
+            case MODE_ASK:
+                return AppOpsManager.MODE_ASK;
+            default:
+                return AppOpsManager.MODE_IGNORED;
+        }
+    }
 }
diff --git a/src/com/android/settings/applications/appops/AppOpsState.java b/src/com/android/settings/applications/appops/AppOpsState.java
index 3c8d647..001074d 100644
--- a/src/com/android/settings/applications/appops/AppOpsState.java
+++ b/src/com/android/settings/applications/appops/AppOpsState.java
@@ -16,6 +16,7 @@
 
 package com.android.settings.applications.appops;
 
+import android.app.Activity;
 import android.app.AppOpsManager;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
@@ -23,6 +24,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.Resources;
+import android.content.SharedPreferences;
 import android.graphics.drawable.Drawable;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -50,12 +52,15 @@
     final CharSequence[] mOpSummaries;
     final CharSequence[] mOpLabels;
 
+    private SharedPreferences mPreferences;
+
     public AppOpsState(Context context) {
         mContext = context;
         mAppOps = (AppOpsManager)context.getSystemService(Context.APP_OPS_SERVICE);
         mPm = context.getPackageManager();
         mOpSummaries = context.getResources().getTextArray(R.array.app_ops_summaries);
         mOpLabels = context.getResources().getTextArray(R.array.app_ops_labels);
+        mPreferences = context.getSharedPreferences("appops_manager", Activity.MODE_PRIVATE);
     }
 
     public static class OpsTemplate implements Parcelable {
@@ -211,9 +216,15 @@
             new boolean[] { false }
             );
 
+    public static final OpsTemplate SU_TEMPLATE = new OpsTemplate(
+            new int[] { AppOpsManager.OP_SU },
+            new boolean[] { false }
+            );
+
     public static final OpsTemplate[] ALL_TEMPLATES = new OpsTemplate[] {
             LOCATION_TEMPLATE, PERSONAL_TEMPLATE, MESSAGING_TEMPLATE,
-            MEDIA_TEMPLATE, DEVICE_TEMPLATE, RUN_IN_BACKGROUND_TEMPLATE
+            MEDIA_TEMPLATE, DEVICE_TEMPLATE, RUN_IN_BACKGROUND_TEMPLATE,
+            SU_TEMPLATE
     };
 
     /**
@@ -501,19 +512,34 @@
     }
 
     private AppEntry getAppEntry(final Context context, final HashMap<String, AppEntry> appEntries,
-            final String packageName, ApplicationInfo appInfo) {
+            final String packageName, ApplicationInfo appInfo, boolean applyFilters) {
+
+        if (appInfo == null) {
+            try {
+                appInfo = mPm.getApplicationInfo(packageName,
+                        PackageManager.GET_DISABLED_COMPONENTS
+                        | PackageManager.GET_UNINSTALLED_PACKAGES);
+            } catch (PackageManager.NameNotFoundException e) {
+                Log.w(TAG, "Unable to find info for package " + packageName);
+                return null;
+            }
+        }
+
+        if (applyFilters) {
+            // Hide user apps if needed
+            if (!shouldShowUserApps() &&
+                    (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
+                return null;
+            }
+            // Hide system apps if needed
+            if (!shouldShowSystemApps() &&
+                     (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
+                return null;
+            }
+        }
+
         AppEntry appEntry = appEntries.get(packageName);
         if (appEntry == null) {
-            if (appInfo == null) {
-                try {
-                    appInfo = mPm.getApplicationInfo(packageName,
-                            PackageManager.MATCH_DISABLED_COMPONENTS
-                            | PackageManager.MATCH_ANY_USER);
-                } catch (PackageManager.NameNotFoundException e) {
-                    Log.w(TAG, "Unable to find info for package " + packageName);
-                    return null;
-                }
-            }
             appEntry = new AppEntry(this, appInfo);
             appEntry.loadLabel(context);
             appEntries.put(packageName, appEntry);
@@ -521,6 +547,14 @@
         return appEntry;
     }
 
+    private boolean shouldShowUserApps() {
+        return mPreferences.getBoolean("show_user_apps", true);
+    }
+
+    private boolean shouldShowSystemApps() {
+        return mPreferences.getBoolean("show_system_apps", true);
+    }
+
     public List<AppOpEntry> buildState(OpsTemplate tpl, int uid, String packageName) {
         return buildState(tpl, uid, packageName, RECENCY_COMPARATOR);
     }
@@ -546,6 +580,9 @@
             }
         }
 
+        // Whether to apply hide user / system app filters
+        final boolean applyFilters = (packageName == null);
+
         List<AppOpsManager.PackageOps> pkgs;
         if (packageName != null) {
             pkgs = mAppOps.getOpsForPackage(uid, packageName, tpl.ops);
@@ -556,7 +593,8 @@
         if (pkgs != null) {
             for (int i=0; i<pkgs.size(); i++) {
                 AppOpsManager.PackageOps pkgOps = pkgs.get(i);
-                AppEntry appEntry = getAppEntry(context, appEntries, pkgOps.getPackageName(), null);
+                AppEntry appEntry = getAppEntry(context, appEntries, pkgOps.getPackageName(), null,
+                        applyFilters);
                 if (appEntry == null) {
                     continue;
                 }
@@ -584,7 +622,7 @@
         for (int i=0; i<apps.size(); i++) {
             PackageInfo appInfo = apps.get(i);
             AppEntry appEntry = getAppEntry(context, appEntries, appInfo.packageName,
-                    appInfo.applicationInfo);
+                    appInfo.applicationInfo, applyFilters);
             if (appEntry == null) {
                 continue;
             }
diff --git a/src/com/android/settings/applications/appops/AppOpsSummary.java b/src/com/android/settings/applications/appops/AppOpsSummary.java
new file mode 100644
index 0000000..ed8a16d
--- /dev/null
+++ b/src/com/android/settings/applications/appops/AppOpsSummary.java
@@ -0,0 +1,244 @@
+/**
+ * Copyright (C) 2013 The Android Open Source Project
+ * Copyright (C) 2016 The CyanogenMod 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.appops;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.Resources;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.PreferenceFrameLayout;
+import android.util.Pair;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentPagerAdapter;
+import androidx.viewpager.widget.PagerTabStrip;
+import androidx.viewpager.widget.ViewPager;
+
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.settings.core.InstrumentedPreferenceFragment;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import com.android.settings.development.RootAccessPreferenceController;
+import com.android.settings.R;
+
+public class AppOpsSummary extends InstrumentedPreferenceFragment {
+    // layout inflater object used to inflate views
+    private LayoutInflater mInflater;
+
+    private ViewGroup mContentContainer;
+    private View mRootView;
+    private ViewPager mViewPager;
+
+    private MyPagerAdapter mAdapter;
+
+    private Activity mActivity;
+    private SharedPreferences mPreferences;
+
+    @Override
+    public int getMetricsCategory() {
+        return MetricsEvent.APP_OPS_SUMMARY;
+    }
+
+    static class MyPagerAdapter extends FragmentPagerAdapter
+            implements ViewPager.OnPageChangeListener {
+        private List<Pair<CharSequence, AppOpsState.OpsTemplate>> mPageData;
+        private int mCurPos;
+
+        public MyPagerAdapter(FragmentManager fm,
+                List<Pair<CharSequence, AppOpsState.OpsTemplate>> data) {
+            super(fm);
+            mPageData = data;
+        }
+
+        @Override
+        public Fragment getItem(int position) {
+            return new AppOpsCategory(mPageData.get(position).second);
+        }
+
+        @Override
+        public int getCount() {
+            return mPageData.size();
+        }
+
+        @Override
+        public CharSequence getPageTitle(int position) {
+            return mPageData.get(position).first;
+        }
+
+        @Override
+        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+        }
+
+        @Override
+        public void onPageSelected(int position) {
+            mCurPos = position;
+        }
+
+        public int getCurrentPage() {
+            return mCurPos;
+        }
+
+        @Override
+        public void onPageScrollStateChanged(int state) {
+            if (state == ViewPager.SCROLL_STATE_IDLE) {
+                //updateCurrentTab(mCurPos);
+            }
+        }
+    }
+
+    private void resetAdapter() {
+        // trigger adapter load, preserving the selected page
+        int curPos = mAdapter.getCurrentPage();
+        mViewPager.setAdapter(mAdapter);
+        mViewPager.setOnPageChangeListener(mAdapter);
+        mViewPager.setCurrentItem(curPos);
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+        // initialize the inflater
+        mInflater = inflater;
+
+        View rootView = mInflater.inflate(R.layout.app_ops_summary,
+                container, false);
+        mContentContainer = container;
+        mRootView = rootView;
+
+        CharSequence[] pageNames = getResources().getTextArray(R.array.app_ops_categories_lineage);
+        // FP2: modified to only show superuser
+        AppOpsState.OpsTemplate[] templates = new AppOpsState.OpsTemplate[] { AppOpsState.SU_TEMPLATE };
+        assert(pageNames.length == templates.length);
+
+        int specificTab = -1;
+        Bundle bundle = getArguments();
+        if (bundle != null) {
+            specificTab = Arrays.asList(pageNames).indexOf(bundle.getString("appops_tab", ""));
+        }
+
+        List<Pair<CharSequence, AppOpsState.OpsTemplate>> pageData = new ArrayList<>();
+        for (int i = 0; i < pageNames.length; i++) {
+            pageData.add(Pair.create(pageNames[i], templates[i]));
+        }
+        filterPageData(pageData, specificTab);
+
+        mViewPager = (ViewPager) rootView.findViewById(R.id.pager);
+        mAdapter = new MyPagerAdapter(getChildFragmentManager(), pageData);
+        mViewPager.setAdapter(mAdapter);
+        mViewPager.setOnPageChangeListener(mAdapter);
+        PagerTabStrip tabs = (PagerTabStrip) rootView.findViewById(R.id.tabs);
+
+        // HACK - https://code.google.com/p/android/issues/detail?id=213359
+        ((ViewPager.LayoutParams) tabs.getLayoutParams()).isDecor = true;
+
+        Resources.Theme theme = tabs.getContext().getTheme();
+        TypedValue typedValue = new TypedValue();
+        theme.resolveAttribute(android.R.attr.colorAccent, typedValue, true);
+        final int colorAccent = getContext().getColor(typedValue.resourceId);
+        tabs.setTabIndicatorColor(colorAccent);
+
+        // 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;
+        }
+
+        mActivity = getActivity();
+
+        return rootView;
+    }
+
+    private void filterPageData(List<Pair<CharSequence, AppOpsState.OpsTemplate>> data, int tab) {
+        if (tab >= 0 && tab < data.size()) {
+            Pair<CharSequence, AppOpsState.OpsTemplate> item = data.get(tab);
+            data.clear();
+            data.add(item);
+        } else if (!RootAccessPreferenceController.isRootForAppsEnabled()) {
+            for (Pair<CharSequence, AppOpsState.OpsTemplate> item : data) {
+                if (item.second == AppOpsState.SU_TEMPLATE) {
+                    data.remove(item);
+                    return;
+                }
+            }
+        }
+    }
+
+    private boolean shouldShowUserApps() {
+        return mPreferences.getBoolean("show_user_apps", true);
+    }
+
+    private boolean shouldShowSystemApps() {
+        return mPreferences.getBoolean("show_system_apps", true);
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+
+        // get shared preferences
+        mPreferences = mActivity.getSharedPreferences("appops_manager", Activity.MODE_PRIVATE);
+
+        setHasOptionsMenu(true);
+    }
+
+    @Override
+    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+        super.onCreateOptionsMenu(menu, inflater);
+        inflater.inflate(R.menu.appops_manager, menu);
+        menu.findItem(R.id.show_user_apps).setChecked(shouldShowUserApps());
+        menu.findItem(R.id.show_system_apps).setChecked(shouldShowSystemApps());
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case R.id.show_user_apps:
+                final String prefNameUserApps = "show_user_apps";
+                // set the menu checkbox and save it in shared preference
+                item.setChecked(!item.isChecked());
+                mPreferences.edit().putBoolean(prefNameUserApps, item.isChecked()).commit();
+                // reload content
+                resetAdapter();
+                return true;
+            case R.id.show_system_apps:
+                final String prefNameSysApps = "show_system_apps";
+                // set the menu checkbox and save it in shared preference
+                item.setChecked(!item.isChecked());
+                mPreferences.edit().putBoolean(prefNameSysApps, item.isChecked()).commit();
+                // reload view content
+                resetAdapter();
+                return true;
+            default:
+                return super.onContextItemSelected(item);
+        }
+    }
+}
diff --git a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
index 9915680..b5eeb87 100644
--- a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
+++ b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
@@ -499,6 +499,7 @@
         controllers.add(new BubbleGlobalPreferenceController(context));
         controllers.add(new EnableGnssRawMeasFullTrackingPreferenceController(context));
         controllers.add(new RootAccessPreferenceController(context, fragment));
+        controllers.add(new RootAppOpsPreferenceController(context));
         controllers.add(new DefaultLaunchPreferenceController(context, "running_apps"));
         controllers.add(new DefaultLaunchPreferenceController(context, "demo_mode"));
         controllers.add(new DefaultLaunchPreferenceController(context, "quick_settings_tiles"));
diff --git a/src/com/android/settings/development/RootAppOpsPreferenceController.java b/src/com/android/settings/development/RootAppOpsPreferenceController.java
new file mode 100644
index 0000000..89c6dc2
--- /dev/null
+++ b/src/com/android/settings/development/RootAppOpsPreferenceController.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2018 The LineageOS 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.development;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.ServiceManager;
+import android.os.UserManager;
+import android.provider.Settings;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.ListPreference;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.R;
+import com.android.settings.Utils;
+import com.android.settings.core.PreferenceControllerMixin;
+import com.android.settings.Settings.AppOpsSummaryActivity;
+import com.android.settingslib.development.DeveloperOptionsPreferenceController;
+
+import static com.android.settings.SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS;
+
+public class RootAppOpsPreferenceController extends DeveloperOptionsPreferenceController
+        implements Preference.OnPreferenceClickListener, PreferenceControllerMixin {
+
+    private static final String TAG = "RootAppOpsPreferenceController";
+    private static final String PREF_KEY = "root_appops";
+
+    public RootAppOpsPreferenceController(Context context) {
+        super(context);
+    }
+
+    @Override
+    public boolean isAvailable() {
+        // User builds don't get root, and eng always gets root
+        return Build.IS_DEBUGGABLE || "eng".equals(Build.TYPE);
+    }
+
+    @Override
+    public String getPreferenceKey() {
+        return PREF_KEY;
+    }
+
+    @Override
+    public void displayPreference(PreferenceScreen screen) {
+        super.displayPreference(screen);
+
+        mPreference.setOnPreferenceClickListener(this);
+
+        if (!RootAccessPreferenceController.isRootForAppsEnabled() || !isAdminUser()) {
+            mPreference.setEnabled(false);
+        }
+    }
+
+    @Override
+    protected void onDeveloperOptionsSwitchEnabled() {
+        if (isAdminUser()) {
+            mPreference.setEnabled(true);
+        }
+    }
+
+    @Override
+    public boolean onPreferenceClick(Preference preference) {
+        Intent intent = new Intent(Intent.ACTION_MAIN);
+        Bundle args = new Bundle();
+        args.putString("appops_tab",
+                mContext.getResources().getString(R.string.app_ops_categories_su));
+        intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, args);
+        intent.setClass(mContext, AppOpsSummaryActivity.class);
+        mContext.startActivity(intent);
+        return true;
+    }
+
+    @VisibleForTesting
+    boolean isAdminUser() {
+        return ((UserManager) mContext.getSystemService(Context.USER_SERVICE)).isAdminUser();
+    }
+}