Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6d88d79a authored by Felipe Leme's avatar Felipe Leme
Browse files

Finished initial implementation of StorageAccessDetails.

It still needs some polishing and unit tests, which will be added later.

Test: manual verification

Bug: 63720392

Change-Id: I8da1094aaaf4ebbe5d388021e859265091fd429c
parent 578d0e8e
Loading
Loading
Loading
Loading
+20 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:settings="http://schemas.android.com/apk/res-auto"
        android:key="directory_access_details"
        android:title="@string/directory_access"/>
+1 −3
Original line number Diff line number Diff line
@@ -111,7 +111,6 @@
            android:value="com.android.settings.Settings$VrListenersSettingsActivity" />
    </Preference>

<!-- TODO(b/63720392): add when ready
    <Preference
        android:key="special_app_directory_access"
        android:title="@string/directory_access"
@@ -119,8 +118,7 @@
        settings:keywords="@string/keywords_directory_access">
        <extra
            android:name="classname"
            android:value="com.android.settings.Settings$DirectoryeAccessSettingsActivity" />
            android:value="com.android.settings.Settings$DirectoryAccessSettingsActivity" />
    </Preference>
 -->

</PreferenceScreen>
+19 −8
Original line number Diff line number Diff line
@@ -33,11 +33,15 @@ import com.android.settingslib.applications.ApplicationsState.AppFilter;

import java.util.Set;

// TODO(b/63720392): add unit tests
// TODO(b/72055774): add unit tests
public class AppStateDirectoryAccessBridge extends AppStateBaseBridge {

    private static final String TAG = "DirectoryAccessBridge";

    // TODO(b/72055774): set to false once feature is ready (or use Log.isLoggable)
    static final boolean DEBUG = true;
    static final boolean VERBOSE = true;

    public AppStateDirectoryAccessBridge(ApplicationsState appState, Callback callback) {
        super(appState, callback);
    }
@@ -59,26 +63,33 @@ public class AppStateDirectoryAccessBridge extends AppStateBaseBridge {

        @Override
        public void init(Context context) {
            try (Cursor cursor = context.getContentResolver().query(
                    new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
            mPackages = null;
            final Uri providerUri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
                    .authority(AUTHORITY).appendPath(TABLE_PACKAGES).appendPath("*")
                            .build(), TABLE_PACKAGES_COLUMNS, null, null)) {
                    .build();
            try (Cursor cursor = context.getContentResolver().query(providerUri,
                    TABLE_PACKAGES_COLUMNS, null, null)) {
                if (cursor == null) {
                    Log.w(TAG, "didn't get cursor");
                    Log.w(TAG, "Didn't get cursor for " + providerUri);
                    return;
                }
                final int count = cursor.getCount();
                if (count == 0) {
                    Log.d(TAG, "no packages");
                    if (DEBUG) {
                        Log.d(TAG, "No packages anymore (was " + mPackages + ")");
                    }
                    return;
                }
                mPackages = new ArraySet<>(count);
                while (cursor.moveToNext()) {
                    mPackages.add(cursor.getString(TABLE_PACKAGES_COL_PACKAGE));
                }
                Log.v(TAG, "init(): " + mPackages);
                if (DEBUG) {
                    Log.d(TAG, "init(): " + mPackages);
                }
            }
        }


        @Override
        public boolean filterApp(AppEntry info) {
+190 −38
Original line number Diff line number Diff line
@@ -17,84 +17,236 @@
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.Preference.OnPreferenceChangeListener;
import android.support.v7.preference.Preference.OnPreferenceClickListener;
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.List;
import java.util.Map;

/**
 * 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/63720392): explain its layout
// TODO(b/63720392): add unit tests
public class DirectoryAccessDetails extends AppInfoWithHeader implements OnPreferenceChangeListener,
        OnPreferenceClickListener {
    private static final String MY_TAG = "DirectoryAccessDetails";
// 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);

        if (true) {
            // TODO(b/63720392): temporarily hack so the screen doesn't crash..
            addPreferencesFromResource(R.xml.app_ops_permissions_details);
            // ... we need to dynamically create the preferences by calling the provider instead:
            try (Cursor cursor = getContentResolver().query(
                    new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
        final Context context = getPrefContext();
        addPreferencesFromResource(R.xml.directory_access_details);
        final PreferenceScreen prefsGroup = getPreferenceScreen();

        // Set external directory UUIDs.
        ArraySet<String> externalDirectoryUuids = null;

        final Uri providerUri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
                .authority(AUTHORITY).appendPath(TABLE_PERMISSIONS).appendPath("*")
                            .build(),
                    TABLE_PERMISSIONS_COLUMNS, null, null)) {
                .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");
                Log.w(TAG, "Didn't get cursor for " + mPackageName);
                return;
            }
            final int count = cursor.getCount();
            if (count == 0) {
                    Log.d(TAG, "no permissions");
                if (DEBUG) {
                    Log.d(TAG, "No permissions for " + mPackageName);
                }
                // TODO(b/72055774): display empty message
                return;
            }

            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;
                    Log.v(MY_TAG, "pkg:" + pkg + " uuid: " + uuid + " dir: " + dir + "> "
                            + granted);
                if (VERBOSE) {
                    Log.v(TAG, "Pkg:"  + pkg + " uuid: " + uuid + " dir: " + dir);
                }

                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) {
                    // Primary storage entry: add right away
                    prefsGroup.addPreference(
                            newPreference(context, dir, providerUri, /* uuid= */ null, dir));
                } else {
                    // External volume entry: save it for later.
                    if (externalDirectoryUuids == null) {
                        externalDirectoryUuids = new ArraySet<>(1);
                    }
                    externalDirectoryUuids.add(uuid);
                }
            }
        }

    @Override
    public boolean onPreferenceClick(Preference preference) {
        // TODO(b/63720392): implement or remove listener
        return false;
        // Add entries from external volumes
        if (externalDirectoryUuids != null) {
            if (VERBOSE) {
                Log.v(TAG, "adding external directories: " + externalDirectoryUuids);
            }

    @Override
    public boolean onPreferenceChange(Preference preference, Object newValue) {
        // TODO(b/63720392): implement or remove listener
        return false;
            // 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;
            }
            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);
            }

            externalDirectoryUuids.forEach((uuid) ->{
                final String name = volumeNames.get(uuid);
                // TODO(b/72055774): add separator
                prefsGroup.addPreference(
                        newPreference(context, name, providerUri, uuid, /* dir= */ null));
            });
        }
        return;
    }


    private SwitchPreference newPreference(Context context, String title, Uri providerUri,
            String uuid, String dir) {
        final SwitchPreference pref = new SwitchPreference(context);
        pref.setKey(String.format("%s:%s", uuid, dir));
        pref.setTitle(title);
        pref.setChecked(false);
        pref.setOnPreferenceChangeListener((unused, value) -> {
            resetDoNotAskAgain(context, value, providerUri, uuid, dir);
            return true;
        });
        return pref;
    }

    private void resetDoNotAskAgain(Context context, Object value, Uri providerUri,
            @Nullable String uuid, @Nullable String directory) {
        if (!Boolean.class.isInstance(value)) {
            // Sanity check
            Log.wtf(TAG, "Invalid value from switch: " + value);
            return;
        }
        final boolean newValue = ((Boolean) value).booleanValue();
        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 boolean refreshUi() {
        // TODO(b/63720392): implement
        return true;
    }