1 /*
2  * Copyright 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.car.settings.applications.specialaccess;
18 
19 import android.app.AppOpsManager;
20 import android.car.drivingstate.CarUxRestrictions;
21 import android.content.Context;
22 import android.content.pm.ApplicationInfo;
23 import android.content.pm.PackageManager;
24 
25 import androidx.annotation.CallSuper;
26 import androidx.annotation.VisibleForTesting;
27 import androidx.preference.Preference;
28 import androidx.preference.PreferenceGroup;
29 import androidx.preference.SwitchPreference;
30 
31 import com.android.car.settings.R;
32 import com.android.car.settings.applications.specialaccess.AppStateAppOpsBridge.PermissionState;
33 import com.android.car.settings.common.FragmentController;
34 import com.android.car.settings.common.PreferenceController;
35 import com.android.settingslib.applications.ApplicationsState;
36 import com.android.settingslib.applications.ApplicationsState.AppEntry;
37 import com.android.settingslib.applications.ApplicationsState.AppFilter;
38 import com.android.settingslib.applications.ApplicationsState.CompoundFilter;
39 
40 import java.util.List;
41 
42 /**
43  * Displays a list of toggles for applications requesting permission to perform the operation with
44  * which this controller was initialized. {@link #init(int, String, int)} should be called when
45  * this controller is instantiated to specify the {@link AppOpsManager} operation code to control
46  * access for.
47  */
48 public class AppOpsPreferenceController extends PreferenceController<PreferenceGroup> {
49 
50     private static final AppFilter FILTER_HAS_INFO = new AppFilter() {
51         @Override
52         public void init() {
53             // No op.
54         }
55 
56         @Override
57         public boolean filterApp(AppEntry info) {
58             return info.extraInfo != null;
59         }
60     };
61 
62     private final AppOpsManager mAppOpsManager;
63 
64     private final Preference.OnPreferenceChangeListener mOnPreferenceChangeListener =
65             new Preference.OnPreferenceChangeListener() {
66                 @Override
67                 public boolean onPreferenceChange(Preference preference, Object newValue) {
68                     AppOpPreference appOpPreference = (AppOpPreference) preference;
69                     AppEntry entry = appOpPreference.mEntry;
70                     PermissionState extraInfo = (PermissionState) entry.extraInfo;
71                     boolean allowOp = (Boolean) newValue;
72                     if (allowOp != extraInfo.isPermissible()) {
73                         mAppOpsManager.setMode(mAppOpsOpCode, entry.info.uid,
74                                 entry.info.packageName,
75                                 allowOp ? AppOpsManager.MODE_ALLOWED : mNegativeOpMode);
76                         // Update the extra info of this entry so that it reflects the new mode.
77                         mAppEntryListManager.forceUpdate(entry);
78                         return true;
79                     }
80                     return false;
81                 }
82             };
83 
84     private final AppEntryListManager.Callback mCallback = new AppEntryListManager.Callback() {
85         @Override
86         public void onAppEntryListChanged(List<AppEntry> entries) {
87             mEntries = entries;
88             refreshUi();
89         }
90     };
91 
92     private int mAppOpsOpCode = AppOpsManager.OP_NONE;
93     private String mPermission;
94     private int mNegativeOpMode = -1;
95 
96     @VisibleForTesting
97     AppEntryListManager mAppEntryListManager;
98     private List<AppEntry> mEntries;
99 
100     private boolean mShowSystem;
101 
AppOpsPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)102     public AppOpsPreferenceController(Context context, String preferenceKey,
103             FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
104         super(context, preferenceKey, fragmentController, uxRestrictions);
105         mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
106         mAppEntryListManager = new AppEntryListManager(context);
107     }
108 
109     @Override
getPreferenceType()110     protected Class<PreferenceGroup> getPreferenceType() {
111         return PreferenceGroup.class;
112     }
113 
114     /**
115      * Initializes this controller with the {@code appOpsOpCode} (selected from the operations in
116      * {@link AppOpsManager}) to control access for.
117      *
118      * @param permission     the {@link android.Manifest.permission} apps must hold to perform the
119      *                       operation.
120      * @param negativeOpMode the operation mode that will be passed to {@link
121      *                       AppOpsManager#setMode(int, int, String, int)} when access for a app is
122      *                       revoked.
123      */
init(int appOpsOpCode, String permission, int negativeOpMode)124     public void init(int appOpsOpCode, String permission, int negativeOpMode) {
125         mAppOpsOpCode = appOpsOpCode;
126         mPermission = permission;
127         mNegativeOpMode = negativeOpMode;
128     }
129 
130     /**
131      * Rebuilds the preference list to show system applications if {@code showSystem} is true.
132      * System applications will be hidden otherwise.
133      */
setShowSystem(boolean showSystem)134     public void setShowSystem(boolean showSystem) {
135         if (mShowSystem != showSystem) {
136             mShowSystem = showSystem;
137             mAppEntryListManager.forceUpdate();
138         }
139     }
140 
141     @Override
checkInitialized()142     protected void checkInitialized() {
143         if (mAppOpsOpCode == AppOpsManager.OP_NONE) {
144             throw new IllegalStateException("App operation code must be initialized");
145         }
146         if (mPermission == null) {
147             throw new IllegalStateException("Manifest permission must be initialized");
148         }
149         if (mNegativeOpMode == -1) {
150             throw new IllegalStateException("Negative case app operation mode must be initialized");
151         }
152     }
153 
154     @Override
onCreateInternal()155     protected void onCreateInternal() {
156         AppStateAppOpsBridge extraInfoBridge = new AppStateAppOpsBridge(getContext(), mAppOpsOpCode,
157                 mPermission);
158         mAppEntryListManager.init(extraInfoBridge, this::getAppFilter, mCallback);
159     }
160 
161     @Override
onStartInternal()162     protected void onStartInternal() {
163         mAppEntryListManager.start();
164     }
165 
166     @Override
onStopInternal()167     protected void onStopInternal() {
168         mAppEntryListManager.stop();
169     }
170 
171     @Override
onDestroyInternal()172     protected void onDestroyInternal() {
173         mAppEntryListManager.destroy();
174     }
175 
176     @Override
updateState(PreferenceGroup preference)177     protected void updateState(PreferenceGroup preference) {
178         if (mEntries == null) {
179             // Still loading.
180             return;
181         }
182         preference.removeAll();
183         for (AppEntry entry : mEntries) {
184             Preference appOpPreference = new AppOpPreference(getContext(), entry);
185             appOpPreference.setOnPreferenceChangeListener(mOnPreferenceChangeListener);
186             preference.addPreference(appOpPreference);
187         }
188     }
189 
190     @CallSuper
getAppFilter()191     protected AppFilter getAppFilter() {
192         AppFilter filterObj = new CompoundFilter(FILTER_HAS_INFO,
193                 ApplicationsState.FILTER_NOT_HIDE);
194         if (!mShowSystem) {
195             filterObj = new CompoundFilter(filterObj,
196                     ApplicationsState.FILTER_DOWNLOADED_AND_LAUNCHER);
197         }
198         return filterObj;
199     }
200 
201     private static class AppOpPreference extends SwitchPreference {
202 
203         private final AppEntry mEntry;
204 
AppOpPreference(Context context, AppEntry entry)205         AppOpPreference(Context context, AppEntry entry) {
206             super(context);
207             String key = entry.info.packageName + "|" + entry.info.uid;
208             setKey(key);
209             setTitle(entry.label);
210             setIcon(entry.icon);
211             setSummary(getAppStateText(entry.info));
212             setPersistent(false);
213             PermissionState extraInfo = (PermissionState) entry.extraInfo;
214             setChecked(extraInfo.isPermissible());
215             mEntry = entry;
216         }
217 
getAppStateText(ApplicationInfo info)218         private String getAppStateText(ApplicationInfo info) {
219             if ((info.flags & ApplicationInfo.FLAG_INSTALLED) == 0) {
220                 return getContext().getString(R.string.not_installed);
221             } else if (!info.enabled || info.enabledSetting
222                     == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED) {
223                 return getContext().getString(R.string.disabled);
224             }
225             return null;
226         }
227     }
228 }
229