CompatPermissionManager.java

/*
 * Copyright 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 androidx.slice.compat;

import static androidx.core.content.PermissionChecker.PERMISSION_DENIED;
import static androidx.core.content.PermissionChecker.PERMISSION_GRANTED;

import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;

import androidx.annotation.RestrictTo;
import androidx.collection.ArraySet;

import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;

/**
 * @hide
 */
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class CompatPermissionManager {

    private static final String TAG = "CompatPermissionManager";
    public static final String ALL_SUFFIX = "_all";

    private final Context mContext;
    private final String mPrefsName;
    private final int mMyUid;
    private final String[] mAutoGrantPermissions;

    public CompatPermissionManager(Context context, String prefsName, int myUid,
            String[] autoGrantPermissions) {
        mContext = context;
        mPrefsName = prefsName;
        mMyUid = myUid;
        mAutoGrantPermissions = autoGrantPermissions;
    }

    private SharedPreferences getPrefs() {
        return mContext.getSharedPreferences(mPrefsName, Context.MODE_PRIVATE);
    }

    public int checkSlicePermission(Uri uri, int pid, int uid) {
        if (uid == mMyUid) {
            return PERMISSION_GRANTED;
        }
        String[] pkgs = mContext.getPackageManager().getPackagesForUid(uid);
        for (String pkg : pkgs) {
            if (checkSlicePermission(uri, pkg) == PERMISSION_GRANTED) {
                return PERMISSION_GRANTED;
            }
        }
        for (String autoGrantPermission : mAutoGrantPermissions) {
            if (mContext.checkPermission(autoGrantPermission, pid, uid) == PERMISSION_GRANTED) {
                for (String pkg : pkgs) {
                    grantSlicePermission(uri, pkg);
                }
                return PERMISSION_GRANTED;
            }
        }
        // Fall back to allowing uri permissions through.
        return mContext.checkUriPermission(uri, pid, uid, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
    }

    private int checkSlicePermission(Uri uri, String pkg) {
        PermissionState state = getPermissionState(pkg, uri.getAuthority());
        return state.hasAccess(uri.getPathSegments()) ? PERMISSION_GRANTED : PERMISSION_DENIED;
    }

    public void grantSlicePermission(Uri uri, String toPkg) {
        PermissionState state = getPermissionState(toPkg, uri.getAuthority());
        if (state.addPath(uri.getPathSegments())) {
            persist(state);
        }
    }

    public void revokeSlicePermission(Uri uri, String toPkg) {
        PermissionState state = getPermissionState(toPkg, uri.getAuthority());
        if (state.removePath(uri.getPathSegments())) {
            persist(state);
        }
    }

    private synchronized void persist(PermissionState state) {
        if (!getPrefs().edit()
                .putStringSet(state.getKey(), state.toPersistable())
                .putBoolean(state.getKey() + ALL_SUFFIX, state.hasAllPermissions())
                .commit()) {
            Log.e(TAG, "Unable to persist permissions");
        }
    }

    private PermissionState getPermissionState(String pkg, String authority) {
        String key = pkg + "_" + authority;
        Set<String> grant = getPrefs().getStringSet(key, Collections.<String>emptySet());
        boolean hasAllPermissions = getPrefs().getBoolean(key + ALL_SUFFIX, false);
        return new PermissionState(grant, key, hasAllPermissions);
    }

    public static class PermissionState {

        private final ArraySet<String[]> mPaths = new ArraySet<>();
        private final String mKey;

        PermissionState(Set<String> grant, String key, boolean hasAllPermissions) {
            if (hasAllPermissions) {
                mPaths.add(new String[0]);
            } else {
                for (String g : grant) {
                    mPaths.add(decodeSegments(g));
                }
            }
            mKey = key;
        }

        public boolean hasAllPermissions() {
            return hasAccess(Collections.<String>emptyList());
        }

        public String getKey() {
            return mKey;
        }

        public Set<String> toPersistable() {
            ArraySet<String> ret = new ArraySet<>();
            for (String[] path : mPaths) {
                ret.add(encodeSegments(path));
            }
            return ret;
        }

        public boolean hasAccess(List<String> path) {
            String[] inPath = path.toArray(new String[path.size()]);
            for (String[] p : mPaths) {
                if (isPathPrefixMatch(p, inPath)) {
                    return true;
                }
            }
            return false;
        }

        boolean addPath(List<String> path) {
            String[] pathSegs = path.toArray(new String[path.size()]);
            for (int i = mPaths.size() - 1; i >= 0; i--) {
                String[] existing = mPaths.valueAt(i);
                if (isPathPrefixMatch(existing, pathSegs)) {
                    // Nothing to add here.
                    return false;
                }
                if (isPathPrefixMatch(pathSegs, existing)) {
                    mPaths.removeAt(i);
                }
            }
            mPaths.add(pathSegs);
            return true;
        }

        boolean removePath(List<String> path) {
            boolean changed = false;
            String[] pathSegs = path.toArray(new String[path.size()]);
            for (int i = mPaths.size() - 1; i >= 0; i--) {
                String[] existing = mPaths.valueAt(i);
                if (isPathPrefixMatch(pathSegs, existing)) {
                    changed = true;
                    mPaths.removeAt(i);
                }
            }
            return changed;
        }

        private boolean isPathPrefixMatch(String[] prefix, String[] path) {
            final int prefixSize = prefix.length;
            if (path.length < prefixSize) return false;

            for (int i = 0; i < prefixSize; i++) {
                if (!Objects.equals(path[i], prefix[i])) {
                    return false;
                }
            }

            return true;
        }

        private String encodeSegments(String[] s) {
            String[] out = new String[s.length];
            for (int i = 0; i < s.length; i++) {
                out[i] = Uri.encode(s[i]);
            }
            return TextUtils.join("/", out);
        }

        private String[] decodeSegments(String s) {
            String[] sets = s.split("/", -1);
            for (int i = 0; i < sets.length; i++) {
                sets[i] = Uri.decode(sets[i]);
            }
            return sets;
        }
    }
}