Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 216 additions & 0 deletions core/java/android/app/AppsScope.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
/*
* Copyright (C) 2026 GrapheneOS
*
* 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 android.app;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.ArrayMap;
import android.util.Slog;

import org.json.JSONException;
import org.json.JSONObject;

import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;

/**
* Configuration for Apps Scope, which controls the visibility of other apps.
* @hide
*/
public final class AppsScope implements Parcelable {
private static final String TAG = "AppsScope";

public static final int FLAG_RESTRICT_SELF = 1;
public static final int FLAG_RESTRICT_SHARED_CERT = 1 << 1;
public static final int FLAG_RESTRICT_SYSTEM = 1 << 2;
public static final int FLAG_RESTRICT_QUERIES = 1 << 3;

public final int flags;
@NonNull
public final Map<String, Boolean> specificRules;

public AppsScope(int flags, @NonNull Map<String, Boolean> specificRules) {
this.flags = flags;
ArrayMap<String, Boolean> map = new ArrayMap<>(specificRules.size());
map.putAll(specificRules);
this.specificRules = Collections.unmodifiableMap(map);
}

public boolean isDefault() {
return flags == 0 && specificRules.isEmpty();
}

private AppsScope(Parcel in) {
flags = in.readInt();
int size = in.readInt();
ArrayMap<String, Boolean> map = new ArrayMap<>(size);
for (int i = 0; i < size; i++) {
map.put(in.readString(), in.readBoolean());
}
specificRules = Collections.unmodifiableMap(map);
}

@Override
public void writeToParcel(@NonNull Parcel dest, int parcelFlags) {
dest.writeInt(this.flags);
dest.writeInt(specificRules.size());
for (Map.Entry<String, Boolean> entry : specificRules.entrySet()) {
dest.writeString(entry.getKey());
dest.writeBoolean(entry.getValue());
}
}

@Override
public int describeContents() {
return 0;
}

public static final @NonNull Creator<AppsScope> CREATOR = new Creator<AppsScope>() {
@Override
public AppsScope createFromParcel(Parcel in) {
return new AppsScope(in);
}

@Override
public AppsScope[] newArray(int size) {
return new AppsScope[size];
}
};

private static final int VERSION = 1;

@NonNull
public static android.content.Intent createConfigActivityIntent(@NonNull String packageName) {
android.content.Intent intent = new android.content.Intent("android.settings.APPS_SCOPE_DETAILS");
intent.setData(android.net.Uri.parse("package:" + packageName));
return intent;
}

@Nullable
public static byte[] serialize(@Nullable AppsScope config) {
if (config == null) {
return null;
}

try {
JSONObject json = new JSONObject();
json.put("restrictSelf", (config.flags & FLAG_RESTRICT_SELF) != 0);
json.put("restrictSharedCert", (config.flags & FLAG_RESTRICT_SHARED_CERT) != 0);
json.put("restrictSystem", (config.flags & FLAG_RESTRICT_SYSTEM) != 0);
json.put("restrictQueries", (config.flags & FLAG_RESTRICT_QUERIES) != 0);

JSONObject specificRules = new JSONObject();
for (Map.Entry<String, Boolean> entry : config.specificRules.entrySet()) {
specificRules.put(entry.getKey(), entry.getValue());
}
json.put("specificRules", specificRules);

return json.toString(2).getBytes(StandardCharsets.UTF_8);
} catch (JSONException e) {
Slog.e(TAG, "serialization failed", e);
return null;
}
}

@Nullable
public static AppsScope deserialize(@Nullable byte[] data) {
if (data == null) {
return null;
}

try {
String str = new String(data, StandardCharsets.UTF_8);
JSONObject json = new JSONObject(str);

int flags = 0;
if (json.optBoolean("restrictSelf", false)) flags |= FLAG_RESTRICT_SELF;
if (json.optBoolean("restrictSharedCert", false)) flags |= FLAG_RESTRICT_SHARED_CERT;
if (json.optBoolean("restrictSystem", false)) flags |= FLAG_RESTRICT_SYSTEM;
if (json.optBoolean("restrictQueries", false)) flags |= FLAG_RESTRICT_QUERIES;

ArrayMap<String, Boolean> specificRules = new ArrayMap<>();
JSONObject rulesObj = json.optJSONObject("specificRules");
if (rulesObj != null) {
Iterator<String> keys = rulesObj.keys();
while (keys.hasNext()) {
String pkg = keys.next();
specificRules.put(pkg, rulesObj.optBoolean(pkg, true));
}
}

return new AppsScope(flags, specificRules);
} catch (JSONException e) {
Slog.e(TAG, "deserialization failed", e);
return null;
}
}

/** @hide */
public static class Builder {
private int flags;
private final ArrayMap<String, Boolean> specificRules;

public Builder() {
this.flags = 0;
this.specificRules = new ArrayMap<>();
}

public Builder(AppsScope config) {
this.flags = config.flags;
this.specificRules = new ArrayMap<>();
this.specificRules.putAll(config.specificRules);
}

@NonNull
public static Builder from(@Nullable AppsScope config) {
return config != null ? new Builder(config) : new Builder();
}

public Builder setFlags(int flags) {
this.flags = flags;
return this;
}

public Builder addFlag(int flag) {
this.flags |= flag;
return this;
}

public Builder clearFlag(int flag) {
this.flags &= ~flag;
return this;
}

public Builder addPackage(String pkg, boolean allowed) {
specificRules.put(pkg, allowed);
return this;
}

public Builder removePackage(String pkg) {
specificRules.remove(pkg);
return this;
}

public AppsScope build() {
return new AppsScope(flags, specificRules);
}
}
}
60 changes: 56 additions & 4 deletions core/java/android/content/pm/GosPackageState.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,23 @@ public final class GosPackageState implements Parcelable {
public final byte[] storageScopes;
@Nullable
public final byte[] contactScopes;
/** @hide */
@Nullable
public final byte[] appsScopes;

@Nullable
private volatile android.app.AppsScope mCachedAppsScope;

/** @hide */
@Nullable
public android.app.AppsScope getAppsScope() {
android.app.AppsScope cached = mCachedAppsScope;
if (cached != null) return cached;
if (appsScopes == null) return null;
cached = android.app.AppsScope.deserialize(appsScopes);
mCachedAppsScope = cached;
return cached;
}
/**
* These flags are lazily derived from persistent state. They are intentionally skipped from
* equals() and hashCode(). derivedFlags are stored here for performance reasons, to avoid
Expand Down Expand Up @@ -64,14 +81,21 @@ public final class GosPackageState implements Parcelable {
/** @hide */
public GosPackageState(long flagStorage1, long packageFlagStorage,
@Nullable byte[] storageScopes, @Nullable byte[] contactScopes) {
this(flagStorage1, packageFlagStorage, storageScopes, contactScopes, null);
}
/** @hide */
public GosPackageState(long flagStorage1, long packageFlagStorage,
@Nullable byte[] storageScopes, @Nullable byte[] contactScopes,
@Nullable byte[] appsScopes) {
this.flagStorage1 = flagStorage1;
this.packageFlagStorage = packageFlagStorage;
this.storageScopes = storageScopes;
this.contactScopes = contactScopes;
this.appsScopes = appsScopes;
}

private static GosPackageState createEmpty() {
return new GosPackageState(0L, 0L, null, null);
return new GosPackageState(0L, 0L, null, null, null);
}

private static final int TYPE_NONE = 0;
Expand All @@ -94,6 +118,7 @@ public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeLong(this.packageFlagStorage);
dest.writeByteArray(storageScopes);
dest.writeByteArray(contactScopes);
dest.writeByteArray(appsScopes);
dest.writeInt(derivedFlags);
}

Expand All @@ -106,7 +131,7 @@ public GosPackageState createFromParcel(Parcel in) {
case TYPE_NONE: return NONE;
};
var res = new GosPackageState(in.readLong(), in.readLong(),
in.createByteArray(), in.createByteArray());
in.createByteArray(), in.createByteArray(), in.createByteArray());
res.derivedFlags = in.readInt();
return res;
}
Expand All @@ -119,7 +144,8 @@ public GosPackageState[] newArray(int size) {

@Override
public int hashCode() {
return Long.hashCode(flagStorage1) + Arrays.hashCode(storageScopes) + Arrays.hashCode(contactScopes) + Long.hashCode(packageFlagStorage);
return Long.hashCode(flagStorage1) + Arrays.hashCode(storageScopes) + Arrays.hashCode(contactScopes) +
Arrays.hashCode(appsScopes) + Long.hashCode(packageFlagStorage);
}

@Override
Expand All @@ -139,6 +165,9 @@ public boolean equals(Object obj) {
if (!Arrays.equals(contactScopes, o.contactScopes)) {
return false;
}
if (!Arrays.equals(appsScopes, o.appsScopes)) {
return false;
}
if (packageFlagStorage != o.packageFlagStorage) {
return false;
}
Expand Down Expand Up @@ -236,6 +265,7 @@ public static class Editor {
private long packageFlagStorage;
private byte[] storageScopes;
private byte[] contactScopes;
private byte[] appsScopes;
private int editorFlags;

/** @hide */
Expand All @@ -246,6 +276,7 @@ public Editor(GosPackageState s, String packageName, int userId) {
this.packageFlagStorage = s.packageFlagStorage;
this.storageScopes = s.storageScopes;
this.contactScopes = s.contactScopes;
this.appsScopes = s.appsScopes;
}

@NonNull
Expand Down Expand Up @@ -304,6 +335,27 @@ public Editor setContactScopes(@Nullable byte[] contactScopes) {
return this;
}

@NonNull
public Editor setAppsScopeConfig(@Nullable byte[] appsScopes) {
this.appsScopes = appsScopes;
return this;
}

/** @hide */
public long getFlags() {
return flagStorage1;
}

/** @hide */
public long getPackageFlags() {
return packageFlagStorage;
}

@Nullable
public byte[] getAppsScopeConfig() {
return appsScopes;
}

@NonNull
public Editor killUidAfterApply() {
return setKillUidAfterApply(true);
Expand Down Expand Up @@ -334,7 +386,7 @@ public Editor setNotifyUidAfterApply(boolean v) {
public boolean apply() {
try {
return ActivityThread.getPackageManager().setGosPackageState(packageName, userId,
new GosPackageState(flagStorage1, packageFlagStorage, storageScopes, contactScopes),
new GosPackageState(flagStorage1, packageFlagStorage, storageScopes, contactScopes, appsScopes),
editorFlags);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
Expand Down
2 changes: 2 additions & 0 deletions core/java/android/content/pm/GosPackageStateFlag.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public interface GosPackageStateFlag {
/** @hide */ int PLAY_INTEGRITY_API_USED_AT_LEAST_ONCE = 26;
/** @hide */ int SUPPRESS_PLAY_INTEGRITY_API_NOTIF = 27;
/** @hide */ int BLOCK_PLAY_INTEGRITY_API = 28;
/** @hide */ int APPS_SCOPES_ENABLED = 29;

/** @hide */
@IntDef(value = {
Expand Down Expand Up @@ -64,6 +65,7 @@ public interface GosPackageStateFlag {
PLAY_INTEGRITY_API_USED_AT_LEAST_ONCE,
SUPPRESS_PLAY_INTEGRITY_API_NOTIF,
BLOCK_PLAY_INTEGRITY_API,
APPS_SCOPES_ENABLED,
})
@Retention(RetentionPolicy.SOURCE)
@interface Enum {}
Expand Down
Loading