Home | History | Annotate | Download | only in applications
      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