summaryrefslogtreecommitdiffstats
path: root/src/com/android/settings/applications/DirectoryAccessDetails.java
blob: 4876e3daa01bcb8e1ebce1055553a818baed46b9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
/*
 * Copyright (C) 2018 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.
 */

package com.android.settings.applications;

import static android.os.storage.StorageVolume.ScopedAccessProviderContract.AUTHORITY;
import static android.os.storage.StorageVolume.ScopedAccessProviderContract.COL_DIRECTORY;
import static android.os.storage.StorageVolume.ScopedAccessProviderContract.COL_GRANTED;
import static android.os.storage.StorageVolume.ScopedAccessProviderContract.COL_PACKAGE;
import static android.os.storage.StorageVolume.ScopedAccessProviderContract.COL_VOLUME_UUID;
import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS;
import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COLUMNS;
import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_DIRECTORY;
import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_GRANTED;
import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_PACKAGE;
import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_VOLUME_UUID;

import static com.android.settings.applications.AppStateDirectoryAccessBridge.DEBUG;
import static com.android.settings.applications.AppStateDirectoryAccessBridge.VERBOSE;

import android.annotation.Nullable;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.storage.StorageManager;
import android.os.storage.VolumeInfo;
import android.support.v14.preference.SwitchPreference;
import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceGroupAdapter;
import android.support.v7.preference.Preference.OnPreferenceChangeListener;
import android.support.v7.preference.Preference.OnPreferenceClickListener;
import android.support.v7.preference.PreferenceCategory;
import android.text.TextUtils;
import android.support.v7.preference.PreferenceManager;
import android.support.v7.preference.PreferenceScreen;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.IconDrawableFactory;
import android.util.Log;
import android.util.Pair;

import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.settings.R;
import com.android.settings.widget.EntityHeaderController;
import com.android.settings.widget.EntityHeaderController.ActionType;
import com.android.settingslib.applications.AppUtils;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Detailed settings for an app's directory access permissions (A.K.A Scoped Directory Access).
 *
 * <p>Currently, it shows the entry for which the user denied access with the "Do not ask again"
 * flag checked on: the user than can use the settings toggle to reset that deniel.
 *
 * <p>This fragments dynamically lists all such permissions, starting with one preference per
 * directory in the primary storage, then adding additional entries for the external volumes (one
 * entry for the whole volume).
 */
// TODO(b/72055774): add unit tests
public class DirectoryAccessDetails extends AppInfoBase {

    @SuppressWarnings("hiding")
    private static final String TAG = "DirectoryAccessDetails";

    private boolean mCreated;

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        if (mCreated) {
            Log.w(TAG, "onActivityCreated(): ignoring duplicate call");
            return;
        }
        mCreated = true;
        if (mPackageInfo == null) {
            Log.w(TAG, "onActivityCreated(): no package info");
            return;
        }
        final Activity activity = getActivity();
        final Preference pref = EntityHeaderController
                .newInstance(activity, this, /* header= */ null )
                .setRecyclerView(getListView(), getLifecycle())
                .setIcon(IconDrawableFactory.newInstance(getPrefContext())
                        .getBadgedIcon(mPackageInfo.applicationInfo))
                .setLabel(mPackageInfo.applicationInfo.loadLabel(mPm))
                .setIsInstantApp(AppUtils.isInstant(mPackageInfo.applicationInfo))
                .setPackageName(mPackageName)
                .setUid(mPackageInfo.applicationInfo.uid)
                .setHasAppInfoLink(false)
                .setButtonActions(ActionType.ACTION_NONE, ActionType.ACTION_NONE)
                .done(activity, getPrefContext());
        getPreferenceScreen().addPreference(pref);
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        addPreferencesFromResource(R.xml.directory_access_details);

    }

    @Override
    protected boolean refreshUi() {
        final Context context = getPrefContext();
        final PreferenceScreen prefsGroup = getPreferenceScreen();
        prefsGroup.removeAll();

        final Map<String, ExternalVolume> externalVolumes = new HashMap<>();

        final Uri providerUri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
                .authority(AUTHORITY).appendPath(TABLE_PERMISSIONS).appendPath("*")
                .build();
        // Query provider for entries.
        try (Cursor cursor = context.getContentResolver().query(providerUri,
                TABLE_PERMISSIONS_COLUMNS, null, new String[] { mPackageName }, null)) {
            if (cursor == null) {
                Log.w(TAG, "Didn't get cursor for " + mPackageName);
                return true;
            }
            final int count = cursor.getCount();
            if (count == 0) {
                // This setting screen should not be reached if there was no permission, so just
                // ignore it
                Log.w(TAG, "No permissions for " + mPackageName);
                return true;
            }

            while (cursor.moveToNext()) {
                final String pkg = cursor.getString(TABLE_PERMISSIONS_COL_PACKAGE);
                final String uuid = cursor.getString(TABLE_PERMISSIONS_COL_VOLUME_UUID);
                final String dir = cursor.getString(TABLE_PERMISSIONS_COL_DIRECTORY);
                final boolean granted = cursor.getInt(TABLE_PERMISSIONS_COL_GRANTED) == 1;
                if (VERBOSE) {
                    Log.v(TAG, "Pkg:"  + pkg + " uuid: " + uuid + " dir: " + dir
                            + " granted:" + granted);
                }

                if (!mPackageName.equals(pkg)) {
                    // Sanity check, shouldn't happen
                    Log.w(TAG, "Ignoring " + uuid + "/" + dir + " due to package mismatch: "
                            + "expected " + mPackageName + ", got " + pkg);
                    continue;
                }

                if (uuid == null) {
                    if (dir == null) {
                        // Sanity check, shouldn't happen
                        Log.wtf(TAG, "Ignoring permission on primary storage root");
                    } else {
                        // Primary storage entry: add right away
                        prefsGroup.addPreference(newPreference(context, dir, providerUri,
                                /* uuid= */ null, dir, granted, /* children= */ null));
                    }
                } else {
                    // External volume entry: save it for later.
                    ExternalVolume externalVolume = externalVolumes.get(uuid);
                    if (externalVolume == null) {
                        externalVolume = new ExternalVolume(uuid);
                        externalVolumes.put(uuid, externalVolume);
                    }
                    if (dir == null) {
                        // Whole volume
                        externalVolume.granted = granted;
                    } else {
                        // Directory only
                        externalVolume.children.add(new Pair<>(dir, granted));
                    }
                }
            }
        }

        if (VERBOSE) {
            Log.v(TAG, "external volumes: " + externalVolumes);
        }

        if (externalVolumes.isEmpty()) {
            // We're done!
            return true;
        }

        // Add entries from external volumes

        // Query StorageManager to get the user-friendly volume names.
        final StorageManager sm = context.getSystemService(StorageManager.class);
        final List<VolumeInfo> volumes = sm.getVolumes();
        if (volumes.isEmpty()) {
            Log.w(TAG, "StorageManager returned no secondary volumes");
            return true;
        }
        final Map<String, String> volumeNames = new HashMap<>(volumes.size());
        for (VolumeInfo volume : volumes) {
            final String uuid = volume.getFsUuid();
            if (uuid == null) continue; // Primary storage; not used.

            String name = sm.getBestVolumeDescription(volume);
            if (name == null) {
                Log.w(TAG, "No description for " + volume + "; using uuid instead: " + uuid);
                name = uuid;
            }
            volumeNames.put(uuid, name);
        }
        if (VERBOSE) {
            Log.v(TAG, "UUID -> name mapping: " + volumeNames);
        }

        for (ExternalVolume volume : externalVolumes.values()) {
            final String volumeName = volumeNames.get(volume.uuid);
            if (volumeName == null) {
                Log.w(TAG, "Ignoring entry for invalid UUID: " + volume.uuid);
                continue;
            }
            // First add the pref for the whole volume...
            final PreferenceCategory category = new PreferenceCategory(context);
            prefsGroup.addPreference(category);
            final Set<SwitchPreference> children = new HashSet<>(volume.children.size());
            category.addPreference(newPreference(context, volumeName, providerUri, volume.uuid,
                    /* dir= */ null, volume.granted, children));

            // ... then the children prefs
            volume.children.forEach((pair) -> {
                final String dir = pair.first;
                final String name = context.getResources()
                        .getString(R.string.directory_on_volume, volumeName, dir);
                final SwitchPreference childPref =
                        newPreference(context, name, providerUri, volume.uuid, dir, pair.second,
                                /* children= */ null);
                category.addPreference(childPref);
                children.add(childPref);
            });
        }
        return true;
    }

    private SwitchPreference newPreference(Context context, String title, Uri providerUri,
            String uuid, String dir, boolean granted, @Nullable Set<SwitchPreference> children) {
        final SwitchPreference pref = new SwitchPreference(context);
        pref.setKey(String.format("%s:%s", uuid, dir));
        pref.setTitle(title);
        pref.setChecked(granted);
        pref.setOnPreferenceChangeListener((unused, value) -> {
            if (!Boolean.class.isInstance(value)) {
                // Sanity check
                Log.wtf(TAG, "Invalid value from switch: " + value);
                return true;
            }
            final boolean newValue = ((Boolean) value).booleanValue();

            resetDoNotAskAgain(context, newValue, providerUri, uuid, dir);
            if (children != null) {
                // When parent is granted, children should be hidden; and vice versa
                final boolean newChildValue = !newValue;
                for (SwitchPreference child : children) {
                    child.setVisible(newChildValue);
                }
            }
            return true;
        });
        return pref;
    }

    private void resetDoNotAskAgain(Context context, boolean newValue, Uri providerUri,
            @Nullable String uuid, @Nullable String directory) {
        if (DEBUG) {
            Log.d(TAG, "Asking " + providerUri  + " to update " + uuid + "/" + directory + " to "
                    + newValue);
        }
        final ContentValues values = new ContentValues(1);
        values.put(COL_GRANTED, newValue);
        final int updated = context.getContentResolver().update(providerUri, values,
                null, new String[] { mPackageName, uuid, directory });
        if (DEBUG) {
            Log.d(TAG, "Updated " + updated + " entries for " + uuid + "/" + directory);
        }
    }

    @Override
    protected AlertDialog createDialog(int id, int errorCode) {
        return null;
    }

    @Override
    public int getMetricsCategory() {
        return MetricsEvent.APPLICATIONS_DIRECTORY_ACCESS_DETAIL;
    }

    private static class ExternalVolume {
        final String uuid;
        final List<Pair<String, Boolean>> children = new ArrayList<>();
        boolean granted;

        ExternalVolume(String uuid) {
            this.uuid = uuid;
        }

        @Override
        public String toString() {
            return "ExternalVolume: [uuid=" + uuid + ", granted=" + granted +
                    ", children=" + children + "]";
        }
    }
}