diff options
Diffstat (limited to 'src')
5 files changed, 254 insertions, 54 deletions
diff --git a/src/android/net/ip/IpClient.java b/src/android/net/ip/IpClient.java index f3dcdc8..018d6ab 100644 --- a/src/android/net/ip/IpClient.java +++ b/src/android/net/ip/IpClient.java @@ -64,7 +64,6 @@ import android.text.TextUtils; import android.util.LocalLog; import android.util.Log; import android.util.Pair; -import android.util.Patterns; import android.util.SparseArray; import androidx.annotation.NonNull; @@ -86,6 +85,8 @@ import com.android.server.NetworkStackService.NetworkStackServiceManager; import java.io.FileDescriptor; import java.io.PrintWriter; import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.URL; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.util.ArrayList; @@ -381,7 +382,7 @@ public class IpClient extends StateMachine { private static final int EVENT_READ_PACKET_FILTER_COMPLETE = 12; private static final int CMD_ADD_KEEPALIVE_PACKET_FILTER_TO_APF = 13; private static final int CMD_REMOVE_KEEPALIVE_PACKET_FILTER_FROM_APF = 14; - private static final int CMD_UPDATE_L2KEY_GROUPHINT = 15; + private static final int CMD_UPDATE_L2KEY_CLUSTER = 15; private static final int CMD_COMPLETE_PRECONNECTION = 16; private static final int CMD_UPDATE_L2INFORMATION = 17; @@ -477,7 +478,7 @@ public class IpClient extends StateMachine { private ProxyInfo mHttpProxy; private ApfFilter mApfFilter; private String mL2Key; // The L2 key for this network, for writing into the memory store - private String mGroupHint; // The group hint for this network, for writing into the memory store + private String mCluster; // The cluster for this network, for writing into the memory store private boolean mMulticastFiltering; private long mStartTimeMillis; private MacAddress mCurrentBssid; @@ -686,9 +687,9 @@ public class IpClient extends StateMachine { IpClient.this.stop(); } @Override - public void setL2KeyAndGroupHint(String l2Key, String groupHint) { + public void setL2KeyAndGroupHint(String l2Key, String cluster) { enforceNetworkStackCallingPermission(); - IpClient.this.setL2KeyAndGroupHint(l2Key, groupHint); + IpClient.this.setL2KeyAndCluster(l2Key, cluster); } @Override public void setTcpBufferSizes(String tcpBufferSizes) { @@ -805,7 +806,7 @@ public class IpClient extends StateMachine { if (req.mLayer2Info != null) { mL2Key = req.mLayer2Info.mL2Key; - mGroupHint = req.mLayer2Info.mGroupHint; + mCluster = req.mLayer2Info.mCluster; } sendMessage(CMD_START, new android.net.shared.ProvisioningConfiguration(req)); } @@ -853,14 +854,14 @@ public class IpClient extends StateMachine { } /** - * Set the L2 key and group hint for storing info into the memory store. + * Set the L2 key and cluster for storing info into the memory store. * * This method is only supported on Q devices. For R or above releases, * caller should call #updateLayer2Information() instead. */ - public void setL2KeyAndGroupHint(String l2Key, String groupHint) { + public void setL2KeyAndCluster(String l2Key, String cluster) { if (!ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q)) { - sendMessage(CMD_UPDATE_L2KEY_GROUPHINT, new Pair<>(l2Key, groupHint)); + sendMessage(CMD_UPDATE_L2KEY_CLUSTER, new Pair<>(l2Key, cluster)); } } @@ -917,7 +918,7 @@ public class IpClient extends StateMachine { } /** - * Update the network bssid, L2Key and GroupHint on L2 roaming happened. + * Update the network bssid, L2Key and cluster on L2 roaming happened. */ public void updateLayer2Information(@NonNull Layer2InformationParcelable info) { sendMessage(CMD_UPDATE_L2INFORMATION, info); @@ -1263,9 +1264,9 @@ public class IpClient extends StateMachine { } final String capportUrl = mDhcpResults.captivePortalApiUrl; - // Uri.parse does no syntax check; do a simple regex check to eliminate garbage. + // Uri.parse does no syntax check; do a simple check to eliminate garbage. // If the URL is still incorrect data fetching will fail later, which is fine. - if (capportUrl != null && Patterns.WEB_URL.matcher(capportUrl).matches()) { + if (isParseableUrl(capportUrl)) { NetworkInformationShimImpl.newInstance() .setCaptivePortalApiUrl(newLp, Uri.parse(capportUrl)); } @@ -1303,6 +1304,19 @@ public class IpClient extends StateMachine { return newLp; } + private static boolean isParseableUrl(String url) { + // Verify that a URL has a reasonable format that can be parsed as per the URL constructor. + // This does not use Patterns.WEB_URL as that pattern excludes URLs without TLDs, such as on + // localhost. + if (url == null) return false; + try { + new URL(url); + return true; + } catch (MalformedURLException e) { + return false; + } + } + private static void addAllReachableDnsServers( LinkProperties lp, Iterable<InetAddress> dnses) { // TODO: Investigate deleting this reachability check. We should be @@ -1545,7 +1559,7 @@ public class IpClient extends StateMachine { private void handleUpdateL2Information(@NonNull Layer2InformationParcelable info) { mL2Key = info.l2Key; - mGroupHint = info.groupHint; + mCluster = info.cluster; if (info.bssid == null || mCurrentBssid == null) { Log.wtf(mTag, "bssid in the parcelable or current tracked bssid should be non-null"); @@ -1619,10 +1633,10 @@ public class IpClient extends StateMachine { handleLinkPropertiesUpdate(NO_CALLBACKS); break; - case CMD_UPDATE_L2KEY_GROUPHINT: { + case CMD_UPDATE_L2KEY_CLUSTER: { final Pair<String, String> args = (Pair<String, String>) msg.obj; mL2Key = args.first; - mGroupHint = args.second; + mCluster = args.second; break; } @@ -1824,10 +1838,10 @@ public class IpClient extends StateMachine { transitionTo(mStoppingState); break; - case CMD_UPDATE_L2KEY_GROUPHINT: { + case CMD_UPDATE_L2KEY_CLUSTER: { final Pair<String, String> args = (Pair<String, String>) msg.obj; mL2Key = args.first; - mGroupHint = args.second; + mCluster = args.second; // TODO : attributes should be saved to the memory store with // these new values if they differ from the previous ones. // If the state machine is in pure StartedState, then the values to input diff --git a/src/com/android/server/connectivity/NetworkMonitor.java b/src/com/android/server/connectivity/NetworkMonitor.java index 8a1b0c7..bee3634 100755 --- a/src/com/android/server/connectivity/NetworkMonitor.java +++ b/src/com/android/server/connectivity/NetworkMonitor.java @@ -83,6 +83,7 @@ import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY; import static com.android.networkstack.apishim.ConstantsShim.DETECTION_METHOD_DNS_EVENTS; import static com.android.networkstack.apishim.ConstantsShim.DETECTION_METHOD_TCP_METRICS; +import static com.android.networkstack.apishim.ConstantsShim.TRANSPORT_TEST; import static com.android.networkstack.util.DnsUtils.PRIVATE_DNS_PROBE_HOST_SUFFIX; import static com.android.networkstack.util.DnsUtils.TYPE_ADDRCONFIG; @@ -387,7 +388,8 @@ public class NetworkMonitor extends StateMachine { private static final int CMD_BANDWIDTH_CHECK_TIMEOUT = 24; // Start mReevaluateDelayMs at this value and double. - private static final int INITIAL_REEVALUATE_DELAY_MS = 1000; + @VisibleForTesting + static final int INITIAL_REEVALUATE_DELAY_MS = 1000; private static final int MAX_REEVALUATE_DELAY_MS = 10 * 60 * 1000; // Default timeout of evaluating network bandwidth. private static final int DEFAULT_EVALUATING_BANDWIDTH_TIMEOUT_MS = 10_000; @@ -1434,8 +1436,9 @@ public class NetworkMonitor extends StateMachine { } final int token = ++mProbeToken; + final EvaluationThreadDeps deps = new EvaluationThreadDeps(mNetworkCapabilities); mThread = new Thread(() -> sendMessage(obtainMessage(CMD_PROBE_COMPLETE, token, 0, - isCaptivePortal()))); + isCaptivePortal(deps)))); mThread.start(); } @@ -2147,8 +2150,25 @@ public class NetworkMonitor extends StateMachine { return mCaptivePortalFallbackSpecs[idx]; } - @VisibleForTesting - protected CaptivePortalProbeResult isCaptivePortal() { + /** + * Parameters that can be accessed by the evaluation thread in a thread-safe way. + * + * Parameters such as LinkProperties and NetworkCapabilities cannot be accessed by the + * evaluation thread directly, as they are managed in the state machine thread and not + * synchronized. This class provides a copy of the required data that is not modified and can be + * used safely by the evaluation thread. + */ + private static class EvaluationThreadDeps { + // TODO: add parameters that are accessed in a non-thread-safe way from the evaluation + // thread (read from LinkProperties, NetworkCapabilities, useHttps, validationStage) + private final boolean mIsTestNetwork; + + EvaluationThreadDeps(NetworkCapabilities nc) { + this.mIsTestNetwork = nc.hasTransport(TRANSPORT_TEST); + } + } + + private CaptivePortalProbeResult isCaptivePortal(EvaluationThreadDeps deps) { if (!mIsCaptivePortalCheckEnabled) { validationLog("Validation disabled."); return CaptivePortalProbeResult.success(CaptivePortalProbeResult.PROBE_UNKNOWN); @@ -2196,11 +2216,11 @@ public class NetworkMonitor extends StateMachine { reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTP, result); } else if (mUseHttps && httpsUrls.length == 1 && httpUrls.length == 1) { // Probe results are reported inside sendHttpAndHttpsParallelWithFallbackProbes. - result = sendHttpAndHttpsParallelWithFallbackProbes( - proxyInfo, httpsUrls[0], httpUrls[0]); + result = sendHttpAndHttpsParallelWithFallbackProbes(deps, proxyInfo, + httpsUrls[0], httpUrls[0]); } else if (mUseHttps) { // Support result aggregation from multiple Urls. - result = sendMultiParallelHttpAndHttpsProbes(proxyInfo, httpsUrls, httpUrls); + result = sendMultiParallelHttpAndHttpsProbes(deps, proxyInfo, httpsUrls, httpUrls); } else { result = sendDnsAndHttpProbes(proxyInfo, httpUrls[0], ValidationProbeEvent.PROBE_HTTP); reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTP, result); @@ -2482,12 +2502,12 @@ public class NetworkMonitor extends StateMachine { private final CountDownLatch mLatch; private final Probe mProbe; - ProbeThread(CountDownLatch latch, ProxyInfo proxy, URL url, int probeType, - Uri captivePortalApiUrl) { + ProbeThread(CountDownLatch latch, EvaluationThreadDeps deps, ProxyInfo proxy, URL url, + int probeType, Uri captivePortalApiUrl) { mLatch = latch; mProbe = (probeType == ValidationProbeEvent.PROBE_HTTPS) - ? new HttpsProbe(proxy, url, captivePortalApiUrl) - : new HttpProbe(proxy, url, captivePortalApiUrl); + ? new HttpsProbe(deps, proxy, url, captivePortalApiUrl) + : new HttpProbe(deps, proxy, url, captivePortalApiUrl); mResult = CaptivePortalProbeResult.failed(probeType); } @@ -2512,11 +2532,14 @@ public class NetworkMonitor extends StateMachine { } private abstract static class Probe { + protected final EvaluationThreadDeps mDeps; protected final ProxyInfo mProxy; protected final URL mUrl; protected final Uri mCaptivePortalApiUrl; - protected Probe(ProxyInfo proxy, URL url, Uri captivePortalApiUrl) { + protected Probe(EvaluationThreadDeps deps, ProxyInfo proxy, URL url, + Uri captivePortalApiUrl) { + mDeps = deps; mProxy = proxy; mUrl = url; mCaptivePortalApiUrl = captivePortalApiUrl; @@ -2526,8 +2549,8 @@ public class NetworkMonitor extends StateMachine { } final class HttpsProbe extends Probe { - HttpsProbe(ProxyInfo proxy, URL url, Uri captivePortalApiUrl) { - super(proxy, url, captivePortalApiUrl); + HttpsProbe(EvaluationThreadDeps deps, ProxyInfo proxy, URL url, Uri captivePortalApiUrl) { + super(deps, proxy, url, captivePortalApiUrl); } @Override @@ -2537,8 +2560,8 @@ public class NetworkMonitor extends StateMachine { } final class HttpProbe extends Probe { - HttpProbe(ProxyInfo proxy, URL url, Uri captivePortalApiUrl) { - super(proxy, url, captivePortalApiUrl); + HttpProbe(EvaluationThreadDeps deps, ProxyInfo proxy, URL url, Uri captivePortalApiUrl) { + super(deps, proxy, url, captivePortalApiUrl); } private CaptivePortalDataShim tryCapportApiProbe() { @@ -2548,7 +2571,12 @@ public class NetworkMonitor extends StateMachine { final String apiContent; try { final URL url = new URL(mCaptivePortalApiUrl.toString()); - if (!"https".equals(url.getProtocol())) { + // Protocol must be HTTPS + // (as per https://www.ietf.org/id/draft-ietf-capport-api-07.txt, #4). + // Only allow HTTP on localhost, for testing. + final boolean isTestLocalhostHttp = mDeps.mIsTestNetwork + && "localhost".equals(url.getHost()) && "http".equals(url.getProtocol()); + if (!"https".equals(url.getProtocol()) && !isTestLocalhostHttp) { validationLog("Invalid captive portal API protocol: " + url.getProtocol()); return null; } @@ -2631,8 +2659,9 @@ public class NetworkMonitor extends StateMachine { && captivePortalApiUrl == null); } - private CaptivePortalProbeResult sendMultiParallelHttpAndHttpsProbes(@NonNull ProxyInfo proxy, - @NonNull URL[] httpsUrls, @NonNull URL[] httpUrls) { + private CaptivePortalProbeResult sendMultiParallelHttpAndHttpsProbes( + @NonNull EvaluationThreadDeps deps, @Nullable ProxyInfo proxy, @NonNull URL[] httpsUrls, + @NonNull URL[] httpUrls) { // If multiple URLs are required to ensure the correctness of validation, send parallel // probes to explore the result in separate probe threads and aggregate those results into // one as the final result for either HTTP or HTTPS. @@ -2657,13 +2686,13 @@ public class NetworkMonitor extends StateMachine { // TODO: Have the capport probe as a different probe for cleanliness. final URL urlMaybeWithCapport = httpUrls[0]; for (final URL url : httpUrls) { - futures.add(ecs.submit(() -> new HttpProbe(proxy, url, + futures.add(ecs.submit(() -> new HttpProbe(deps, proxy, url, url.equals(urlMaybeWithCapport) ? capportApiUrl : null).sendProbe())); } for (final URL url : httpsUrls) { - futures.add( - ecs.submit(() -> new HttpsProbe(proxy, url, capportApiUrl).sendProbe())); + futures.add(ecs.submit(() -> new HttpsProbe(deps, proxy, url, capportApiUrl) + .sendProbe())); } final ArrayList<CaptivePortalProbeResult> completedProbes = new ArrayList<>(); @@ -2759,15 +2788,15 @@ public class NetworkMonitor extends StateMachine { } private CaptivePortalProbeResult sendHttpAndHttpsParallelWithFallbackProbes( - ProxyInfo proxy, URL httpsUrl, URL httpUrl) { + EvaluationThreadDeps deps, ProxyInfo proxy, URL httpsUrl, URL httpUrl) { // Number of probes to wait for. If a probe completes with a conclusive answer // it shortcuts the latch immediately by forcing the count to 0. final CountDownLatch latch = new CountDownLatch(2); final Uri capportApiUrl = getCaptivePortalApiUrl(mLinkProperties); - final ProbeThread httpsProbe = new ProbeThread(latch, proxy, httpsUrl, + final ProbeThread httpsProbe = new ProbeThread(latch, deps, proxy, httpsUrl, ValidationProbeEvent.PROBE_HTTPS, capportApiUrl); - final ProbeThread httpProbe = new ProbeThread(latch, proxy, httpUrl, + final ProbeThread httpProbe = new ProbeThread(latch, deps, proxy, httpUrl, ValidationProbeEvent.PROBE_HTTP, capportApiUrl); try { diff --git a/src/com/android/server/connectivity/ipmemorystore/IpMemoryStoreDatabase.java b/src/com/android/server/connectivity/ipmemorystore/IpMemoryStoreDatabase.java index 834aa2d..0b05a5b 100644 --- a/src/com/android/server/connectivity/ipmemorystore/IpMemoryStoreDatabase.java +++ b/src/com/android/server/connectivity/ipmemorystore/IpMemoryStoreDatabase.java @@ -78,11 +78,17 @@ public class IpMemoryStoreDatabase { // is used to represent "infinite lease". public static final String COLTYPE_ASSIGNEDV4ADDRESSEXPIRY = "BIGINT"; - // Please note that the group hint is only a *hint*, hence its name. The client can offer - // this information to nudge the grouping in the decision it thinks is right, but it can't - // decide for the memory store what is the same L3 network. - public static final String COLNAME_GROUPHINT = "groupHint"; - public static final String COLTYPE_GROUPHINT = "TEXT"; + // An optional cluster representing a notion of group owned by the client. The memory + // store uses this as a hint for grouping, but not as an overriding factor. The client + // can then use this to find networks belonging to a cluster. An example of this could + // be the SSID for WiFi, where same SSID-networks may not be the same L3 networks but + // it's still useful for managing networks. + // Note that "groupHint" is the legacy name of the column. The column should be renamed + // in the database – ALTER TABLE ${NetworkAttributesContract.TABLENAME RENAME} COLUMN + // groupHint TO cluster – but this has been postponed to reduce risk as the Mainline + // release winter imposes a lot of changes be pushed at the same time in the next release. + public static final String COLNAME_CLUSTER = "groupHint"; + public static final String COLTYPE_CLUSTER = "TEXT"; public static final String COLNAME_DNSADDRESSES = "dnsAddresses"; // Stored in marshalled form as is @@ -97,7 +103,7 @@ public class IpMemoryStoreDatabase { + COLNAME_EXPIRYDATE + " " + COLTYPE_EXPIRYDATE + ", " + COLNAME_ASSIGNEDV4ADDRESS + " " + COLTYPE_ASSIGNEDV4ADDRESS + ", " + COLNAME_ASSIGNEDV4ADDRESSEXPIRY + " " + COLTYPE_ASSIGNEDV4ADDRESSEXPIRY + ", " - + COLNAME_GROUPHINT + " " + COLTYPE_GROUPHINT + ", " + + COLNAME_CLUSTER + " " + COLTYPE_CLUSTER + ", " + COLNAME_DNSADDRESSES + " " + COLTYPE_DNSADDRESSES + ", " + COLNAME_MTU + " " + COLTYPE_MTU + ")"; public static final String DROP_TABLE = "DROP TABLE IF EXISTS " + TABLENAME; @@ -165,7 +171,7 @@ public class IpMemoryStoreDatabase { try { if (oldVersion < 2) { // upgrade from version 1 to version 2 - // since we starts from version 2, do nothing here + // since we start from version 2, do nothing here } if (oldVersion < 3) { @@ -250,8 +256,8 @@ public class IpMemoryStoreDatabase { values.put(NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESSEXPIRY, attributes.assignedV4AddressExpiry); } - if (null != attributes.groupHint) { - values.put(NetworkAttributesContract.COLNAME_GROUPHINT, attributes.groupHint); + if (null != attributes.cluster) { + values.put(NetworkAttributesContract.COLNAME_CLUSTER, attributes.cluster); } if (null != attributes.dnsAddresses) { values.put(NetworkAttributesContract.COLNAME_DNSADDRESSES, @@ -299,7 +305,7 @@ public class IpMemoryStoreDatabase { NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESS, 0); final long assignedV4AddressExpiry = getLong(cursor, NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESSEXPIRY, 0); - final String groupHint = getString(cursor, NetworkAttributesContract.COLNAME_GROUPHINT); + final String cluster = getString(cursor, NetworkAttributesContract.COLNAME_CLUSTER); final byte[] dnsAddressesBlob = getBlob(cursor, NetworkAttributesContract.COLNAME_DNSADDRESSES); final int mtu = getInt(cursor, NetworkAttributesContract.COLNAME_MTU, -1); @@ -309,7 +315,7 @@ public class IpMemoryStoreDatabase { if (0 != assignedV4AddressExpiry) { builder.setAssignedV4AddressExpiry(assignedV4AddressExpiry); } - builder.setGroupHint(groupHint); + builder.setCluster(cluster); if (null != dnsAddressesBlob) { builder.setDnsAddresses(decodeAddressList(dnsAddressesBlob)); } @@ -596,6 +602,64 @@ public class IpMemoryStoreDatabase { return bestKey; } + /** + * Delete a single entry by key. + * + * If |needWipe| is true, the data will be wiped from disk immediately. Otherwise, it will + * only be marked deleted, and overwritten by subsequent writes or reclaimed during the next + * maintenance window. + * Note that wiping data is a very expensive operation. This is meant for clients that need + * this data gone from disk immediately for security reasons. Functionally it makes no + * difference at all. + */ + static StatusAndCount delete(@NonNull final SQLiteDatabase db, @NonNull final String l2key, + final boolean needWipe) { + return deleteEntriesWithColumn(db, + NetworkAttributesContract.COLNAME_L2KEY, l2key, needWipe); + } + + /** + * Delete all entries that have a particular cluster value. + * + * If |needWipe| is true, the data will be wiped from disk immediately. Otherwise, it will + * only be marked deleted, and overwritten by subsequent writes or reclaimed during the next + * maintenance window. + * Note that wiping data is a very expensive operation. This is meant for clients that need + * this data gone from disk immediately for security reasons. Functionally it makes no + * difference at all. + */ + static StatusAndCount deleteCluster(@NonNull final SQLiteDatabase db, + @NonNull final String cluster, final boolean needWipe) { + return deleteEntriesWithColumn(db, + NetworkAttributesContract.COLNAME_CLUSTER, cluster, needWipe); + } + + // Delete all entries where the given column has the given value. + private static StatusAndCount deleteEntriesWithColumn(@NonNull final SQLiteDatabase db, + @NonNull final String column, @NonNull final String value, final boolean needWipe) { + db.beginTransaction(); + int deleted = 0; + try { + deleted = db.delete(NetworkAttributesContract.TABLENAME, + column + "= ?", new String[] { value }); + db.setTransactionSuccessful(); + } catch (SQLiteException e) { + Log.e(TAG, "Could not delete from the memory store", e); + // Unclear what might have happened ; deleting records is not supposed to be able + // to fail barring a syntax error in the SQL query. + return new StatusAndCount(Status.ERROR_UNKNOWN, 0); + } finally { + db.endTransaction(); + } + + if (needWipe) { + final int vacuumStatus = vacuum(db); + // This is a problem for the client : return the failure + if (Status.SUCCESS != vacuumStatus) return new StatusAndCount(vacuumStatus, deleted); + } + return new StatusAndCount(Status.SUCCESS, deleted); + } + // Drops all records that are expired. Relevance has decayed to zero of these records. Returns // an int out of Status.{SUCCESS, ERROR_*} static int dropAllExpiredRecords(@NonNull final SQLiteDatabase db) { @@ -702,4 +766,13 @@ public class IpMemoryStoreDatabase { final int columnIndex = cursor.getColumnIndex(columnName); return (columnIndex >= 0) ? cursor.getLong(columnIndex) : defaultValue; } + private static int vacuum(@NonNull final SQLiteDatabase db) { + try { + db.execSQL("VACUUM"); + return Status.SUCCESS; + } catch (SQLiteException e) { + // Vacuuming may fail from lack of storage, because it makes a copy of the database. + return Status.ERROR_STORAGE; + } + } } diff --git a/src/com/android/server/connectivity/ipmemorystore/IpMemoryStoreService.java b/src/com/android/server/connectivity/ipmemorystore/IpMemoryStoreService.java index cd29e0d..ae9c875 100644 --- a/src/com/android/server/connectivity/ipmemorystore/IpMemoryStoreService.java +++ b/src/com/android/server/connectivity/ipmemorystore/IpMemoryStoreService.java @@ -33,6 +33,7 @@ import android.net.ipmemorystore.IOnBlobRetrievedListener; import android.net.ipmemorystore.IOnL2KeyResponseListener; import android.net.ipmemorystore.IOnNetworkAttributesRetrievedListener; import android.net.ipmemorystore.IOnSameL3NetworkResponseListener; +import android.net.ipmemorystore.IOnStatusAndCountListener; import android.net.ipmemorystore.IOnStatusListener; import android.net.ipmemorystore.NetworkAttributes; import android.net.ipmemorystore.NetworkAttributesParcelable; @@ -116,7 +117,11 @@ public class IpMemoryStoreService extends IIpMemoryStore.Stub { // TODO : investigate replacing this scheme with a scheme where each thread has its own // instance of the database, as it may be faster. It is likely however that IpMemoryStore // operations are mostly IO-bound anyway, and additional contention is unlikely to bring - // benefits. Alternatively, a read-write lock might increase throughput. + // benefits. Alternatively, a read-write lock might increase throughput. Also if doing + // this work, care must be taken around the privacy-preserving VACUUM operations as + // VACUUM will fail if there are other open transactions at the same time, and using + // multiple threads will open the possibility of this failure happening, threatening + // the privacy guarantees. mExecutor = Executors.newSingleThreadExecutor(); RegularMaintenanceJobService.schedule(mContext, this); } @@ -405,6 +410,56 @@ public class IpMemoryStoreService extends IIpMemoryStore.Stub { } /** + * Delete a single entry. + * + * @param l2Key The L2 key of the entry to delete. + * @param needWipe Whether the data must be wiped from disk immediately for security reasons. + * This is very expensive and makes no functional difference ; only pass + * true if security requires this data must be removed from disk immediately. + * @param listener A listener that will be invoked to inform of the completion of this call, + * or null if the client is not interested in learning about success/failure. + * returns (through the listener) A status to indicate success and the number of deleted records + */ + public void delete(@NonNull final String l2Key, final boolean needWipe, + @Nullable final IOnStatusAndCountListener listener) { + mExecutor.execute(() -> { + try { + final StatusAndCount res = IpMemoryStoreDatabase.delete(mDb, l2Key, needWipe); + if (null != listener) listener.onComplete(makeStatus(res.status), res.count); + } catch (final RemoteException e) { + // Client at the other end died + } + }); + } + + /** + * Delete all entries in a cluster. + * + * This method will delete all entries in the memory store that have the cluster attribute + * passed as an argument. + * + * @param cluster The cluster to delete. + * @param needWipe Whether the data must be wiped from disk immediately for security reasons. + * This is very expensive and makes no functional difference ; only pass + * true if security requires this data must be removed from disk immediately. + * @param listener A listener that will be invoked to inform of the completion of this call, + * or null if the client is not interested in learning about success/failure. + * returns (through the listener) A status to indicate success and the number of deleted records + */ + public void deleteCluster(@NonNull final String cluster, final boolean needWipe, + @Nullable final IOnStatusAndCountListener listener) { + mExecutor.execute(() -> { + try { + final StatusAndCount res = + IpMemoryStoreDatabase.deleteCluster(mDb, cluster, needWipe); + if (null != listener) listener.onComplete(makeStatus(res.status), res.count); + } catch (final RemoteException e) { + // Client at the other end died + } + }); + } + + /** * Wipe the data in IpMemoryStore database upon network factory reset. */ @Override diff --git a/src/com/android/server/connectivity/ipmemorystore/StatusAndCount.java b/src/com/android/server/connectivity/ipmemorystore/StatusAndCount.java new file mode 100644 index 0000000..2cbe843 --- /dev/null +++ b/src/com/android/server/connectivity/ipmemorystore/StatusAndCount.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2019 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.server.connectivity.ipmemorystore; + +/** + * Small data class to wrap a Status and an int. + */ +public class StatusAndCount { + public final int status; + public final int count; + public StatusAndCount(final int status, final int count) { + this.status = status; + this.count = count; + } +} |