• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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.settings.applications;
18 
19 import static android.os.storage.StorageVolume.ScopedAccessProviderContract.AUTHORITY;
20 import static android.os.storage.StorageVolume.ScopedAccessProviderContract.COL_DIRECTORY;
21 import static android.os.storage.StorageVolume.ScopedAccessProviderContract.COL_GRANTED;
22 import static android.os.storage.StorageVolume.ScopedAccessProviderContract.COL_PACKAGE;
23 import static android.os.storage.StorageVolume.ScopedAccessProviderContract.COL_VOLUME_UUID;
24 import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS;
25 import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COLUMNS;
26 import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_DIRECTORY;
27 import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_GRANTED;
28 import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_PACKAGE;
29 import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_VOLUME_UUID;
30 
31 import static com.android.settings.applications.AppStateDirectoryAccessBridge.DEBUG;
32 import static com.android.settings.applications.AppStateDirectoryAccessBridge.VERBOSE;
33 
34 import android.annotation.Nullable;
35 import android.app.Activity;
36 import android.app.AlertDialog;
37 import android.content.ContentResolver;
38 import android.content.ContentValues;
39 import android.content.Context;
40 import android.database.Cursor;
41 import android.net.Uri;
42 import android.os.Bundle;
43 import android.os.storage.StorageManager;
44 import android.os.storage.VolumeInfo;
45 import android.support.v14.preference.SwitchPreference;
46 import android.support.v7.preference.Preference;
47 import android.support.v7.preference.PreferenceGroupAdapter;
48 import android.support.v7.preference.Preference.OnPreferenceChangeListener;
49 import android.support.v7.preference.Preference.OnPreferenceClickListener;
50 import android.support.v7.preference.PreferenceCategory;
51 import android.text.TextUtils;
52 import android.support.v7.preference.PreferenceManager;
53 import android.support.v7.preference.PreferenceScreen;
54 import android.util.ArrayMap;
55 import android.util.ArraySet;
56 import android.util.IconDrawableFactory;
57 import android.util.Log;
58 import android.util.Pair;
59 
60 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
61 import com.android.settings.R;
62 import com.android.settings.widget.EntityHeaderController;
63 import com.android.settings.widget.EntityHeaderController.ActionType;
64 import com.android.settingslib.applications.AppUtils;
65 
66 import java.util.ArrayList;
67 import java.util.HashMap;
68 import java.util.HashSet;
69 import java.util.List;
70 import java.util.Map;
71 import java.util.Set;
72 
73 /**
74  * Detailed settings for an app's directory access permissions (A.K.A Scoped Directory Access).
75  *
76  * <p>Currently, it shows the entry for which the user denied access with the "Do not ask again"
77  * flag checked on: the user than can use the settings toggle to reset that deniel.
78  *
79  * <p>This fragments dynamically lists all such permissions, starting with one preference per
80  * directory in the primary storage, then adding additional entries for the external volumes (one
81  * entry for the whole volume).
82  */
83 // TODO(b/72055774): add unit tests
84 public class DirectoryAccessDetails extends AppInfoBase {
85 
86     @SuppressWarnings("hiding")
87     private static final String TAG = "DirectoryAccessDetails";
88 
89     private boolean mCreated;
90 
91     @Override
onActivityCreated(Bundle savedInstanceState)92     public void onActivityCreated(Bundle savedInstanceState) {
93         super.onActivityCreated(savedInstanceState);
94 
95         if (mCreated) {
96             Log.w(TAG, "onActivityCreated(): ignoring duplicate call");
97             return;
98         }
99         mCreated = true;
100         if (mPackageInfo == null) {
101             Log.w(TAG, "onActivityCreated(): no package info");
102             return;
103         }
104         final Activity activity = getActivity();
105         final Preference pref = EntityHeaderController
106                 .newInstance(activity, this, /* header= */ null )
107                 .setRecyclerView(getListView(), getLifecycle())
108                 .setIcon(IconDrawableFactory.newInstance(getPrefContext())
109                         .getBadgedIcon(mPackageInfo.applicationInfo))
110                 .setLabel(mPackageInfo.applicationInfo.loadLabel(mPm))
111                 .setIsInstantApp(AppUtils.isInstant(mPackageInfo.applicationInfo))
112                 .setPackageName(mPackageName)
113                 .setUid(mPackageInfo.applicationInfo.uid)
114                 .setHasAppInfoLink(false)
115                 .setButtonActions(ActionType.ACTION_NONE, ActionType.ACTION_NONE)
116                 .done(activity, getPrefContext());
117         getPreferenceScreen().addPreference(pref);
118     }
119 
120     @Override
onCreate(Bundle savedInstanceState)121     public void onCreate(Bundle savedInstanceState) {
122         super.onCreate(savedInstanceState);
123 
124         addPreferencesFromResource(R.xml.directory_access_details);
125 
126     }
127 
128     @Override
refreshUi()129     protected boolean refreshUi() {
130         final Context context = getPrefContext();
131         final PreferenceScreen prefsGroup = getPreferenceScreen();
132         prefsGroup.removeAll();
133 
134         final Map<String, ExternalVolume> externalVolumes = new HashMap<>();
135 
136         final Uri providerUri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
137                 .authority(AUTHORITY).appendPath(TABLE_PERMISSIONS).appendPath("*")
138                 .build();
139         // Query provider for entries.
140         try (Cursor cursor = context.getContentResolver().query(providerUri,
141                 TABLE_PERMISSIONS_COLUMNS, null, new String[] { mPackageName }, null)) {
142             if (cursor == null) {
143                 Log.w(TAG, "Didn't get cursor for " + mPackageName);
144                 return true;
145             }
146             final int count = cursor.getCount();
147             if (count == 0) {
148                 // This setting screen should not be reached if there was no permission, so just
149                 // ignore it
150                 Log.w(TAG, "No permissions for " + mPackageName);
151                 return true;
152             }
153 
154             while (cursor.moveToNext()) {
155                 final String pkg = cursor.getString(TABLE_PERMISSIONS_COL_PACKAGE);
156                 final String uuid = cursor.getString(TABLE_PERMISSIONS_COL_VOLUME_UUID);
157                 final String dir = cursor.getString(TABLE_PERMISSIONS_COL_DIRECTORY);
158                 final boolean granted = cursor.getInt(TABLE_PERMISSIONS_COL_GRANTED) == 1;
159                 if (VERBOSE) {
160                     Log.v(TAG, "Pkg:"  + pkg + " uuid: " + uuid + " dir: " + dir
161                             + " granted:" + granted);
162                 }
163 
164                 if (!mPackageName.equals(pkg)) {
165                     // Sanity check, shouldn't happen
166                     Log.w(TAG, "Ignoring " + uuid + "/" + dir + " due to package mismatch: "
167                             + "expected " + mPackageName + ", got " + pkg);
168                     continue;
169                 }
170 
171                 if (uuid == null) {
172                     if (dir == null) {
173                         // Sanity check, shouldn't happen
174                         Log.wtf(TAG, "Ignoring permission on primary storage root");
175                     } else {
176                         // Primary storage entry: add right away
177                         prefsGroup.addPreference(newPreference(context, dir, providerUri,
178                                 /* uuid= */ null, dir, granted, /* children= */ null));
179                     }
180                 } else {
181                     // External volume entry: save it for later.
182                     ExternalVolume externalVolume = externalVolumes.get(uuid);
183                     if (externalVolume == null) {
184                         externalVolume = new ExternalVolume(uuid);
185                         externalVolumes.put(uuid, externalVolume);
186                     }
187                     if (dir == null) {
188                         // Whole volume
189                         externalVolume.granted = granted;
190                     } else {
191                         // Directory only
192                         externalVolume.children.add(new Pair<>(dir, granted));
193                     }
194                 }
195             }
196         }
197 
198         if (VERBOSE) {
199             Log.v(TAG, "external volumes: " + externalVolumes);
200         }
201 
202         if (externalVolumes.isEmpty()) {
203             // We're done!
204             return true;
205         }
206 
207         // Add entries from external volumes
208 
209         // Query StorageManager to get the user-friendly volume names.
210         final StorageManager sm = context.getSystemService(StorageManager.class);
211         final List<VolumeInfo> volumes = sm.getVolumes();
212         if (volumes.isEmpty()) {
213             Log.w(TAG, "StorageManager returned no secondary volumes");
214             return true;
215         }
216         final Map<String, String> volumeNames = new HashMap<>(volumes.size());
217         for (VolumeInfo volume : volumes) {
218             final String uuid = volume.getFsUuid();
219             if (uuid == null) continue; // Primary storage; not used.
220 
221             String name = sm.getBestVolumeDescription(volume);
222             if (name == null) {
223                 Log.w(TAG, "No description for " + volume + "; using uuid instead: " + uuid);
224                 name = uuid;
225             }
226             volumeNames.put(uuid, name);
227         }
228         if (VERBOSE) {
229             Log.v(TAG, "UUID -> name mapping: " + volumeNames);
230         }
231 
232         for (ExternalVolume volume : externalVolumes.values()) {
233             final String volumeName = volumeNames.get(volume.uuid);
234             if (volumeName == null) {
235                 Log.w(TAG, "Ignoring entry for invalid UUID: " + volume.uuid);
236                 continue;
237             }
238             // First add the pref for the whole volume...
239             final PreferenceCategory category = new PreferenceCategory(context);
240             prefsGroup.addPreference(category);
241             final Set<SwitchPreference> children = new HashSet<>(volume.children.size());
242             category.addPreference(newPreference(context, volumeName, providerUri, volume.uuid,
243                     /* dir= */ null, volume.granted, children));
244 
245             // ... then the children prefs
246             volume.children.forEach((pair) -> {
247                 final String dir = pair.first;
248                 final String name = context.getResources()
249                         .getString(R.string.directory_on_volume, volumeName, dir);
250                 final SwitchPreference childPref =
251                         newPreference(context, name, providerUri, volume.uuid, dir, pair.second,
252                                 /* children= */ null);
253                 category.addPreference(childPref);
254                 children.add(childPref);
255             });
256         }
257         return true;
258     }
259 
newPreference(Context context, String title, Uri providerUri, String uuid, String dir, boolean granted, @Nullable Set<SwitchPreference> children)260     private SwitchPreference newPreference(Context context, String title, Uri providerUri,
261             String uuid, String dir, boolean granted, @Nullable Set<SwitchPreference> children) {
262         final SwitchPreference pref = new SwitchPreference(context);
263         pref.setKey(String.format("%s:%s", uuid, dir));
264         pref.setTitle(title);
265         pref.setChecked(granted);
266         pref.setOnPreferenceChangeListener((unused, value) -> {
267             if (!Boolean.class.isInstance(value)) {
268                 // Sanity check
269                 Log.wtf(TAG, "Invalid value from switch: " + value);
270                 return true;
271             }
272             final boolean newValue = ((Boolean) value).booleanValue();
273 
274             resetDoNotAskAgain(context, newValue, providerUri, uuid, dir);
275             if (children != null) {
276                 // When parent is granted, children should be hidden; and vice versa
277                 final boolean newChildValue = !newValue;
278                 for (SwitchPreference child : children) {
279                     child.setVisible(newChildValue);
280                 }
281             }
282             return true;
283         });
284         return pref;
285     }
286 
resetDoNotAskAgain(Context context, boolean newValue, Uri providerUri, @Nullable String uuid, @Nullable String directory)287     private void resetDoNotAskAgain(Context context, boolean newValue, Uri providerUri,
288             @Nullable String uuid, @Nullable String directory) {
289         if (DEBUG) {
290             Log.d(TAG, "Asking " + providerUri  + " to update " + uuid + "/" + directory + " to "
291                     + newValue);
292         }
293         final ContentValues values = new ContentValues(1);
294         values.put(COL_GRANTED, newValue);
295         final int updated = context.getContentResolver().update(providerUri, values,
296                 null, new String[] { mPackageName, uuid, directory });
297         if (DEBUG) {
298             Log.d(TAG, "Updated " + updated + " entries for " + uuid + "/" + directory);
299         }
300     }
301 
302     @Override
createDialog(int id, int errorCode)303     protected AlertDialog createDialog(int id, int errorCode) {
304         return null;
305     }
306 
307     @Override
getMetricsCategory()308     public int getMetricsCategory() {
309         return MetricsEvent.APPLICATIONS_DIRECTORY_ACCESS_DETAIL;
310     }
311 
312     private static class ExternalVolume {
313         final String uuid;
314         final List<Pair<String, Boolean>> children = new ArrayList<>();
315         boolean granted;
316 
ExternalVolume(String uuid)317         ExternalVolume(String uuid) {
318             this.uuid = uuid;
319         }
320 
321         @Override
toString()322         public String toString() {
323             return "ExternalVolume: [uuid=" + uuid + ", granted=" + granted +
324                     ", children=" + children + "]";
325         }
326     }
327 }
328