blob: 3caf2931e039a2ccf76834c1340fc6e10222b626 [file] [log] [blame]
/*
* Copyright (C) 2023 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 android.provider;
import static android.provider.Settings.CALL_METHOD_USER_KEY;
import static android.provider.Settings.ContentProviderHolder;
import static android.provider.Settings.LOCAL_LOGV;
import static android.provider.Settings.NameValueCache.NAME_EQ_PLACEHOLDER;
import static android.provider.Settings.NameValueCache.SELECT_VALUE_PROJECTION;
import static android.provider.Settings.TAG;
import android.annotation.NonNull;
import android.content.ContentResolver;
import android.content.IContentProvider;
import android.database.Cursor;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.IpcDataCache;
import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Log;
import androidx.annotation.Nullable;
import java.util.HashMap;
import java.util.Objects;
/** @hide */
final class SettingsIpcDataCache {
private static final boolean DEBUG = true;
private static final int NUM_MAX_ENTRIES = 2048;
static class GetQuery {
@NonNull final ContentResolver mContentResolver;
@NonNull final String mName;
GetQuery(@NonNull ContentResolver contentResolver, @NonNull String name) {
mContentResolver = contentResolver;
mName = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof GetQuery)) return false;
GetQuery getQuery = (GetQuery) o;
return mContentResolver.equals(
getQuery.mContentResolver) && mName.equals(getQuery.mName);
}
@Override
public int hashCode() {
return Objects.hash(mContentResolver, mName);
}
}
private static class GetQueryHandler extends IpcDataCache.QueryHandler<GetQuery, String> {
@NonNull final ContentProviderHolder mContentProviderHolder;
@NonNull final String mCallGetCommand;
@NonNull final Uri mUri;
final int mUserId;
private GetQueryHandler(
ContentProviderHolder contentProviderHolder, String callGetCommand, Uri uri) {
mContentProviderHolder = contentProviderHolder;
mCallGetCommand = callGetCommand;
mUri = uri;
mUserId = UserHandle.myUserId();
}
@Nullable
@Override
public String apply(GetQuery query) {
try {
return getValueFromContentProviderCall(mContentProviderHolder, mCallGetCommand,
mUri, mUserId, query);
} catch (RemoteException e) {
// Throw to prevent caching
e.rethrowAsRuntimeException();
}
return null;
}
}
@NonNull
static IpcDataCache<GetQuery, String> createValueCache(
@NonNull ContentProviderHolder contentProviderHolder,
@NonNull String callGetCommand, @NonNull Uri uri, @NonNull String type) {
if (DEBUG) {
Log.i(TAG, "Creating value cache for type:" + type);
}
IpcDataCache.Config config = new IpcDataCache.Config(
NUM_MAX_ENTRIES, IpcDataCache.MODULE_SYSTEM, type /* apiName */);
return new IpcDataCache<>(config.child("get"),
new GetQueryHandler(contentProviderHolder, callGetCommand, uri));
}
@Nullable
private static String getValueFromContentProviderCall(
@NonNull ContentProviderHolder providerHolder, @NonNull String callGetCommand,
@NonNull Uri uri, int userId, @NonNull GetQuery query)
throws RemoteException {
final ContentResolver cr = query.mContentResolver;
final String name = query.mName;
return getValueFromContentProviderCall(providerHolder, callGetCommand, uri, userId, cr,
name);
}
@Nullable
static String getValueFromContentProviderCall(
@NonNull ContentProviderHolder providerHolder, @NonNull String callGetCommand,
@NonNull Uri uri, int userId, ContentResolver cr, String name) throws RemoteException {
final IContentProvider cp = providerHolder.getProvider(cr);
// Try the fast path first, not using query(). If this
// fails (alternate Settings provider that doesn't support
// this interface?) then we fall back to the query/table
// interface.
if (callGetCommand != null) {
try {
Bundle args = new Bundle();
if (userId != UserHandle.myUserId()) {
args.putInt(CALL_METHOD_USER_KEY, userId);
}
Bundle b;
// If we're in system server and in a binder transaction we need to clear the
// calling uid. This works around code in system server that did not call
// clearCallingIdentity, previously this wasn't needed because reading settings
// did not do permission checking but that's no longer the case.
// Long term this should be removed and callers should properly call
// clearCallingIdentity or use a ContentResolver from the caller as needed.
if (Settings.isInSystemServer() && Binder.getCallingUid() != Process.myUid()) {
final long token = Binder.clearCallingIdentity();
try {
b = cp.call(cr.getAttributionSource(),
providerHolder.mUri.getAuthority(), callGetCommand, name,
args);
} finally {
Binder.restoreCallingIdentity(token);
}
} else {
b = cp.call(cr.getAttributionSource(),
providerHolder.mUri.getAuthority(), callGetCommand, name, args);
}
if (b != null) {
return b.getString(Settings.NameValueTable.VALUE);
}
// If the response Bundle is null, we fall through
// to the query interface below.
} catch (RemoteException e) {
// Not supported by the remote side? Fall through
// to query().
}
}
Cursor c = null;
try {
Bundle queryArgs = ContentResolver.createSqlQueryBundle(
NAME_EQ_PLACEHOLDER, new String[]{name}, null);
// Same workaround as above.
if (Settings.isInSystemServer() && Binder.getCallingUid() != Process.myUid()) {
final long token = Binder.clearCallingIdentity();
try {
c = cp.query(cr.getAttributionSource(), uri,
SELECT_VALUE_PROJECTION, queryArgs, null);
} finally {
Binder.restoreCallingIdentity(token);
}
} else {
c = cp.query(cr.getAttributionSource(), uri,
SELECT_VALUE_PROJECTION, queryArgs, null);
}
if (c == null) {
Log.w(TAG, "Can't get key " + name + " from " + uri);
return null;
}
String value = c.moveToNext() ? c.getString(0) : null;
if (LOCAL_LOGV) {
Log.v(TAG, "cache miss [" + uri.getLastPathSegment() + "]: "
+ name + " = " + (value == null ? "(null)" : value));
}
return value;
} finally {
if (c != null) c.close();
}
}
static class ListQuery {
@NonNull ContentResolver mContentResolver;
@NonNull final String mPrefix;
ListQuery(@NonNull ContentResolver contentResolver, String prefix) {
mContentResolver = contentResolver;
mPrefix = prefix;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ListQuery)) return false;
ListQuery listQuery = (ListQuery) o;
return mContentResolver.equals(listQuery.mContentResolver) && mPrefix.equals(
listQuery.mPrefix);
}
@Override
public int hashCode() {
return Objects.hash(mContentResolver, mPrefix);
}
}
private static class ListQueryHandler extends
IpcDataCache.QueryHandler<ListQuery, HashMap<String, String>> {
@NonNull final ContentProviderHolder mContentProviderHolder;
@NonNull final String mCallListCommand;
ListQueryHandler(@NonNull ContentProviderHolder contentProviderHolder,
@NonNull String callListCommand) {
mContentProviderHolder = contentProviderHolder;
mCallListCommand = callListCommand;
}
@Nullable
@Override
public HashMap<String, String> apply(@NonNull ListQuery query) {
try {
return getListFromContentProviderCall(query);
} catch (RemoteException e) {
// Throw to prevent caching
e.rethrowAsRuntimeException();
}
return null;
}
@Nullable
private HashMap<String, String> getListFromContentProviderCall(ListQuery query)
throws RemoteException {
final ContentResolver cr = query.mContentResolver;
final IContentProvider cp = mContentProviderHolder.getProvider(cr);
final String prefix = query.mPrefix;
final String namespace = prefix.substring(0, prefix.length() - 1);
HashMap<String, String> keyValues = new HashMap<>();
Bundle args = new Bundle();
args.putString(Settings.CALL_METHOD_PREFIX_KEY, prefix);
Bundle b;
// b/252663068: if we're in system server and the caller did not call
// clearCallingIdentity, the read would fail due to mismatched AttributionSources.
// TODO(b/256013480): remove this bypass after fixing the callers in system server.
if (namespace.equals(DeviceConfig.NAMESPACE_DEVICE_POLICY_MANAGER)
&& Settings.isInSystemServer()
&& Binder.getCallingUid() != Process.myUid()) {
final long token = Binder.clearCallingIdentity();
try {
// Fetch all flags for the namespace at once for caching purposes
b = cp.call(cr.getAttributionSource(),
mContentProviderHolder.mUri.getAuthority(), mCallListCommand, null,
args);
} finally {
Binder.restoreCallingIdentity(token);
}
} else {
// Fetch all flags for the namespace at once for caching purposes
b = cp.call(cr.getAttributionSource(),
mContentProviderHolder.mUri.getAuthority(), mCallListCommand, null, args);
}
if (b == null) {
// Invalid response, return an empty map
return keyValues;
}
// Cache all flags for the namespace
HashMap<String, String> flagsToValues =
(HashMap) b.getSerializable(Settings.NameValueTable.VALUE,
java.util.HashMap.class);
return flagsToValues;
}
}
@NonNull
static IpcDataCache<ListQuery, HashMap<String, String>> createListCache(
@NonNull ContentProviderHolder providerHolder,
@NonNull String callListCommand, String type) {
if (DEBUG) {
Log.i(TAG, "Creating cache for settings type:" + type);
}
IpcDataCache.Config config = new IpcDataCache.Config(
NUM_MAX_ENTRIES, IpcDataCache.MODULE_SYSTEM, type /* apiName */);
return new IpcDataCache<>(config.child("get"),
new ListQueryHandler(providerHolder, callListCommand));
}
static void invalidateCache(String type) {
if (DEBUG) {
Log.i(TAG, "Cache invalidated for type:" + type);
}
IpcDataCache.invalidateCache(IpcDataCache.MODULE_SYSTEM, type);
}
}