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 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 121 public void onCreate(Bundle savedInstanceState) { 122 super.onCreate(savedInstanceState); 123 124 addPreferencesFromResource(R.xml.directory_access_details); 125 126 } 127 128 @Override 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 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 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 303 protected AlertDialog createDialog(int id, int errorCode) { 304 return null; 305 } 306 307 @Override 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 317 ExternalVolume(String uuid) { 318 this.uuid = uuid; 319 } 320 321 @Override 322 public String toString() { 323 return "ExternalVolume: [uuid=" + uuid + ", granted=" + granted + 324 ", children=" + children + "]"; 325 } 326 } 327 } 328