• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.connectivity.ipmemorystore;
18 
19 import static com.android.net.module.util.Inet4AddressUtils.inet4AddressToIntHTH;
20 import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
21 
22 import android.content.ContentValues;
23 import android.content.Context;
24 import android.database.Cursor;
25 import android.database.sqlite.SQLiteCursor;
26 import android.database.sqlite.SQLiteCursorDriver;
27 import android.database.sqlite.SQLiteDatabase;
28 import android.database.sqlite.SQLiteException;
29 import android.database.sqlite.SQLiteOpenHelper;
30 import android.database.sqlite.SQLiteQuery;
31 import android.net.ipmemorystore.NetworkAttributes;
32 import android.net.ipmemorystore.Status;
33 import android.util.Log;
34 
35 import androidx.annotation.NonNull;
36 import androidx.annotation.Nullable;
37 
38 import java.io.ByteArrayInputStream;
39 import java.io.ByteArrayOutputStream;
40 import java.net.InetAddress;
41 import java.net.UnknownHostException;
42 import java.util.ArrayList;
43 import java.util.List;
44 import java.util.StringJoiner;
45 
46 /**
47  * Encapsulating class for using the SQLite database backing the memory store.
48  *
49  * This class groups together the contracts and the SQLite helper used to
50  * use the database.
51  *
52  * @hide
53  */
54 public class IpMemoryStoreDatabase {
55     private static final String TAG = IpMemoryStoreDatabase.class.getSimpleName();
56     // A pair of NetworkAttributes objects is group-close if the confidence that they are
57     // the same is above this cutoff. See NetworkAttributes and SameL3NetworkResponse.
58     private static final float GROUPCLOSE_CONFIDENCE = 0.5f;
59 
60     /**
61      * Contract class for the Network Attributes table.
62      */
63     public static class NetworkAttributesContract {
64         public static final String TABLENAME = "NetworkAttributes";
65 
66         public static final String COLNAME_L2KEY = "l2Key";
67         public static final String COLTYPE_L2KEY = "TEXT NOT NULL";
68 
69         public static final String COLNAME_EXPIRYDATE = "expiryDate";
70         // Milliseconds since the Epoch, in true Java style
71         public static final String COLTYPE_EXPIRYDATE = "BIGINT";
72 
73         public static final String COLNAME_ASSIGNEDV4ADDRESS = "assignedV4Address";
74         public static final String COLTYPE_ASSIGNEDV4ADDRESS = "INTEGER";
75 
76         public static final String COLNAME_ASSIGNEDV4ADDRESSEXPIRY = "assignedV4AddressExpiry";
77         // The lease expiry timestamp in uint of milliseconds since the Epoch. Long.MAX_VALUE
78         // is used to represent "infinite lease".
79         public static final String COLTYPE_ASSIGNEDV4ADDRESSEXPIRY = "BIGINT";
80 
81         // An optional cluster representing a notion of group owned by the client. The memory
82         // store uses this as a hint for grouping, but not as an overriding factor. The client
83         // can then use this to find networks belonging to a cluster. An example of this could
84         // be the SSID for WiFi, where same SSID-networks may not be the same L3 networks but
85         // it's still useful for managing networks.
86         // Note that "groupHint" is the legacy name of the column. The column should be renamed
87         // in the database – ALTER TABLE ${NetworkAttributesContract.TABLENAME RENAME} COLUMN
88         // groupHint TO cluster – but this has been postponed to reduce risk as the Mainline
89         // release winter imposes a lot of changes be pushed at the same time in the next release.
90         public static final String COLNAME_CLUSTER = "groupHint";
91         public static final String COLTYPE_CLUSTER = "TEXT";
92 
93         public static final String COLNAME_DNSADDRESSES = "dnsAddresses";
94         // Stored in marshalled form as is
95         public static final String COLTYPE_DNSADDRESSES = "BLOB";
96 
97         public static final String COLNAME_MTU = "mtu";
98         public static final String COLTYPE_MTU = "INTEGER DEFAULT -1";
99 
100         public static final String CREATE_TABLE = "CREATE TABLE IF NOT EXISTS "
101                 + TABLENAME                       + " ("
102                 + COLNAME_L2KEY                   + " " + COLTYPE_L2KEY + " PRIMARY KEY NOT NULL, "
103                 + COLNAME_EXPIRYDATE              + " " + COLTYPE_EXPIRYDATE              + ", "
104                 + COLNAME_ASSIGNEDV4ADDRESS       + " " + COLTYPE_ASSIGNEDV4ADDRESS       + ", "
105                 + COLNAME_ASSIGNEDV4ADDRESSEXPIRY + " " + COLTYPE_ASSIGNEDV4ADDRESSEXPIRY + ", "
106                 + COLNAME_CLUSTER                 + " " + COLTYPE_CLUSTER                 + ", "
107                 + COLNAME_DNSADDRESSES            + " " + COLTYPE_DNSADDRESSES            + ", "
108                 + COLNAME_MTU                     + " " + COLTYPE_MTU                     + ")";
109         public static final String DROP_TABLE = "DROP TABLE IF EXISTS " + TABLENAME;
110     }
111 
112     /**
113      * Contract class for the Private Data table.
114      */
115     public static class PrivateDataContract {
116         public static final String TABLENAME = "PrivateData";
117 
118         public static final String COLNAME_L2KEY = "l2Key";
119         public static final String COLTYPE_L2KEY = "TEXT NOT NULL";
120 
121         public static final String COLNAME_CLIENT = "client";
122         public static final String COLTYPE_CLIENT = "TEXT NOT NULL";
123 
124         public static final String COLNAME_DATANAME = "dataName";
125         public static final String COLTYPE_DATANAME = "TEXT NOT NULL";
126 
127         public static final String COLNAME_DATA = "data";
128         public static final String COLTYPE_DATA = "BLOB NOT NULL";
129 
130         public static final String CREATE_TABLE = "CREATE TABLE IF NOT EXISTS "
131                 + TABLENAME        + " ("
132                 + COLNAME_L2KEY    + " " + COLTYPE_L2KEY    + ", "
133                 + COLNAME_CLIENT   + " " + COLTYPE_CLIENT   + ", "
134                 + COLNAME_DATANAME + " " + COLTYPE_DATANAME + ", "
135                 + COLNAME_DATA     + " " + COLTYPE_DATA     + ", "
136                 + "PRIMARY KEY ("
137                 + COLNAME_L2KEY    + ", "
138                 + COLNAME_CLIENT   + ", "
139                 + COLNAME_DATANAME + "))";
140         public static final String DROP_TABLE = "DROP TABLE IF EXISTS " + TABLENAME;
141     }
142 
143     // To save memory when the DB is not used, close it after 30s of inactivity. This is
144     // determined manually based on what feels right.
145     private static final long IDLE_CONNECTION_TIMEOUT_MS = 30_000;
146 
147     /** The SQLite DB helper */
148     public static class DbHelper extends SQLiteOpenHelper {
149         // Update this whenever changing the schema.
150         // DO NOT CHANGE without solid testing for downgrades, and checking onDowngrade
151         // below: b/171340630
152         private static final int SCHEMA_VERSION = 4;
153         private static final String DATABASE_FILENAME = "IpMemoryStore.db";
154         private static final String TRIGGER_NAME = "delete_cascade_to_private";
155 
DbHelper(@onNull final Context context)156         public DbHelper(@NonNull final Context context) {
157             super(context, DATABASE_FILENAME, null, SCHEMA_VERSION);
158             setIdleConnectionTimeout(IDLE_CONNECTION_TIMEOUT_MS);
159         }
160 
161         /** Called when the database is created */
162         @Override
onCreate(@onNull final SQLiteDatabase db)163         public void onCreate(@NonNull final SQLiteDatabase db) {
164             db.execSQL(NetworkAttributesContract.CREATE_TABLE);
165             db.execSQL(PrivateDataContract.CREATE_TABLE);
166             createTrigger(db);
167         }
168 
169         /** Called when the database is upgraded */
170         @Override
onUpgrade(@onNull final SQLiteDatabase db, final int oldVersion, final int newVersion)171         public void onUpgrade(@NonNull final SQLiteDatabase db, final int oldVersion,
172                 final int newVersion) {
173             try {
174                 if (oldVersion < 2) {
175                     // upgrade from version 1 to version 2
176                     // since we start from version 2, do nothing here
177                 }
178 
179                 if (oldVersion < 3) {
180                     // upgrade from version 2 to version 3
181                     final String sqlUpgradeAddressExpiry = "alter table"
182                             + " " + NetworkAttributesContract.TABLENAME + " ADD"
183                             + " " + NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESSEXPIRY
184                             + " " + NetworkAttributesContract.COLTYPE_ASSIGNEDV4ADDRESSEXPIRY;
185                     db.execSQL(sqlUpgradeAddressExpiry);
186                 }
187 
188                 if (oldVersion < 4) {
189                     createTrigger(db);
190                 }
191             } catch (SQLiteException e) {
192                 Log.e(TAG, "Could not upgrade to the new version", e);
193                 // create database with new version
194                 db.execSQL(NetworkAttributesContract.DROP_TABLE);
195                 db.execSQL(PrivateDataContract.DROP_TABLE);
196                 onCreate(db);
197             }
198         }
199 
200         /** Called when the database is downgraded */
201         @Override
onDowngrade(@onNull final SQLiteDatabase db, final int oldVersion, final int newVersion)202         public void onDowngrade(@NonNull final SQLiteDatabase db, final int oldVersion,
203                 final int newVersion) {
204             // Downgrades always nuke all data and recreate an empty table.
205             db.execSQL(NetworkAttributesContract.DROP_TABLE);
206             db.execSQL(PrivateDataContract.DROP_TABLE);
207             // TODO: add test for downgrades. Triggers should already be dropped
208             // when the table is dropped, so this may be a bug.
209             // Note that fixing this code does not affect how older versions
210             // will handle downgrades.
211             db.execSQL("DROP TRIGGER " + TRIGGER_NAME);
212             onCreate(db);
213         }
214 
createTrigger(@onNull final SQLiteDatabase db)215         private void createTrigger(@NonNull final SQLiteDatabase db) {
216             final String createTrigger = "CREATE TRIGGER " + TRIGGER_NAME
217                     + " DELETE ON " + NetworkAttributesContract.TABLENAME
218                     + " BEGIN"
219                     + " DELETE FROM " + PrivateDataContract.TABLENAME + " WHERE OLD."
220                     + NetworkAttributesContract.COLNAME_L2KEY
221                     + "=" + PrivateDataContract.COLNAME_L2KEY
222                     + "; END;";
223             db.execSQL(createTrigger);
224         }
225     }
226 
227     @NonNull
encodeAddressList(@onNull final List<InetAddress> addresses)228     private static byte[] encodeAddressList(@NonNull final List<InetAddress> addresses) {
229         final ByteArrayOutputStream os = new ByteArrayOutputStream();
230         for (final InetAddress address : addresses) {
231             final byte[] b = address.getAddress();
232             os.write(b.length);
233             os.write(b, 0, b.length);
234         }
235         return os.toByteArray();
236     }
237 
238     @NonNull
decodeAddressList(@onNull final byte[] encoded)239     private static ArrayList<InetAddress> decodeAddressList(@NonNull final byte[] encoded) {
240         final ByteArrayInputStream is = new ByteArrayInputStream(encoded);
241         final ArrayList<InetAddress> addresses = new ArrayList<>();
242         int d = -1;
243         while ((d = is.read()) != -1) {
244             final byte[] bytes = new byte[d];
245             is.read(bytes, 0, d);
246             try {
247                 addresses.add(InetAddress.getByAddress(bytes));
248             } catch (UnknownHostException e) { /* Hopefully impossible */ }
249         }
250         return addresses;
251     }
252 
253     @NonNull
toContentValues(@ullable final NetworkAttributes attributes)254     private static ContentValues toContentValues(@Nullable final NetworkAttributes attributes) {
255         final ContentValues values = new ContentValues();
256         if (null == attributes) return values;
257         if (null != attributes.assignedV4Address) {
258             values.put(NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESS,
259                     inet4AddressToIntHTH(attributes.assignedV4Address));
260         }
261         if (null != attributes.assignedV4AddressExpiry) {
262             values.put(NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESSEXPIRY,
263                     attributes.assignedV4AddressExpiry);
264         }
265         if (null != attributes.cluster) {
266             values.put(NetworkAttributesContract.COLNAME_CLUSTER, attributes.cluster);
267         }
268         if (null != attributes.dnsAddresses) {
269             values.put(NetworkAttributesContract.COLNAME_DNSADDRESSES,
270                     encodeAddressList(attributes.dnsAddresses));
271         }
272         if (null != attributes.mtu) {
273             values.put(NetworkAttributesContract.COLNAME_MTU, attributes.mtu);
274         }
275         return values;
276     }
277 
278     // Convert a NetworkAttributes object to content values to store them in a table compliant
279     // with the contract defined in NetworkAttributesContract.
280     @NonNull
toContentValues(@onNull final String key, @Nullable final NetworkAttributes attributes, final long expiry)281     private static ContentValues toContentValues(@NonNull final String key,
282             @Nullable final NetworkAttributes attributes, final long expiry) {
283         final ContentValues values = toContentValues(attributes);
284         values.put(NetworkAttributesContract.COLNAME_L2KEY, key);
285         values.put(NetworkAttributesContract.COLNAME_EXPIRYDATE, expiry);
286         return values;
287     }
288 
289     // Convert a byte array into content values to store it in a table compliant with the
290     // contract defined in PrivateDataContract.
291     @NonNull
toContentValues(@onNull final String key, @NonNull final String clientId, @NonNull final String name, @NonNull final byte[] data)292     private static ContentValues toContentValues(@NonNull final String key,
293             @NonNull final String clientId, @NonNull final String name,
294             @NonNull final byte[] data) {
295         final ContentValues values = new ContentValues();
296         values.put(PrivateDataContract.COLNAME_L2KEY, key);
297         values.put(PrivateDataContract.COLNAME_CLIENT, clientId);
298         values.put(PrivateDataContract.COLNAME_DATANAME, name);
299         values.put(PrivateDataContract.COLNAME_DATA, data);
300         return values;
301     }
302 
303     @Nullable
readNetworkAttributesLine(@onNull final Cursor cursor)304     private static NetworkAttributes readNetworkAttributesLine(@NonNull final Cursor cursor) {
305         // Make sure the data hasn't expired
306         final long expiry = getLong(cursor, NetworkAttributesContract.COLNAME_EXPIRYDATE, -1L);
307         if (expiry < System.currentTimeMillis()) return null;
308 
309         final NetworkAttributes.Builder builder = new NetworkAttributes.Builder();
310         final int assignedV4AddressInt = getInt(cursor,
311                 NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESS, 0);
312         final long assignedV4AddressExpiry = getLong(cursor,
313                 NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESSEXPIRY, 0);
314         final String cluster = getString(cursor, NetworkAttributesContract.COLNAME_CLUSTER);
315         final byte[] dnsAddressesBlob =
316                 getBlob(cursor, NetworkAttributesContract.COLNAME_DNSADDRESSES);
317         final int mtu = getInt(cursor, NetworkAttributesContract.COLNAME_MTU, -1);
318         if (0 != assignedV4AddressInt) {
319             builder.setAssignedV4Address(intToInet4AddressHTH(assignedV4AddressInt));
320         }
321         if (0 != assignedV4AddressExpiry) {
322             builder.setAssignedV4AddressExpiry(assignedV4AddressExpiry);
323         }
324         builder.setCluster(cluster);
325         if (null != dnsAddressesBlob) {
326             builder.setDnsAddresses(decodeAddressList(dnsAddressesBlob));
327         }
328         if (mtu >= 0) {
329             builder.setMtu(mtu);
330         }
331         return builder.build();
332     }
333 
334     private static final String[] EXPIRY_COLUMN = new String[] {
335         NetworkAttributesContract.COLNAME_EXPIRYDATE
336     };
337     static final int EXPIRY_ERROR = -1; // Legal values for expiry are positive
338 
339     static final String SELECT_L2KEY = NetworkAttributesContract.COLNAME_L2KEY + " = ?";
340 
341     // Returns the expiry date of the specified row, or one of the error codes above if the
342     // row is not found or some other error
getExpiry(@onNull final SQLiteDatabase db, @NonNull final String key)343     static long getExpiry(@NonNull final SQLiteDatabase db, @NonNull final String key) {
344         try (Cursor cursor = db.query(NetworkAttributesContract.TABLENAME,
345                 EXPIRY_COLUMN, // columns
346                 SELECT_L2KEY, // selection
347                 new String[] { key }, // selectionArgs
348                 null, // groupBy
349                 null, // having
350                 null)) { // orderBy
351             // L2KEY is the primary key ; it should not be possible to get more than one
352             // result here. 0 results means the key was not found.
353             if (cursor.getCount() != 1) return EXPIRY_ERROR;
354             cursor.moveToFirst();
355             return cursor.getLong(0); // index in the EXPIRY_COLUMN array
356         }
357     }
358 
359     static final int RELEVANCE_ERROR = -1; // Legal values for relevance are positive
360 
361     // Returns the relevance of the specified row, or one of the error codes above if the
362     // row is not found or some other error
getRelevance(@onNull final SQLiteDatabase db, @NonNull final String key)363     static int getRelevance(@NonNull final SQLiteDatabase db, @NonNull final String key) {
364         final long expiry = getExpiry(db, key);
365         return expiry < 0 ? (int) expiry : RelevanceUtils.computeRelevanceForNow(expiry);
366     }
367 
368     // If the attributes are null, this will only write the expiry.
369     // Returns an int out of Status.{SUCCESS, ERROR_*}
storeNetworkAttributes(@onNull final SQLiteDatabase db, @NonNull final String key, final long expiry, @Nullable final NetworkAttributes attributes)370     static int storeNetworkAttributes(@NonNull final SQLiteDatabase db, @NonNull final String key,
371             final long expiry, @Nullable final NetworkAttributes attributes) {
372         final ContentValues cv = toContentValues(key, attributes, expiry);
373         db.beginTransaction();
374         try {
375             // Unfortunately SQLite does not have any way to do INSERT OR UPDATE. Options are
376             // to either insert with on conflict ignore then update (like done here), or to
377             // construct a custom SQL INSERT statement with nested select.
378             final long resultId = db.insertWithOnConflict(NetworkAttributesContract.TABLENAME,
379                     null, cv, SQLiteDatabase.CONFLICT_IGNORE);
380             if (resultId < 0) {
381                 db.update(NetworkAttributesContract.TABLENAME, cv, SELECT_L2KEY, new String[]{key});
382             }
383             db.setTransactionSuccessful();
384             return Status.SUCCESS;
385         } catch (SQLiteException e) {
386             // No space left on disk or something
387             Log.e(TAG, "Could not write to the memory store", e);
388         } finally {
389             db.endTransaction();
390         }
391         return Status.ERROR_STORAGE;
392     }
393 
394     // Returns an int out of Status.{SUCCESS, ERROR_*}
storeBlob(@onNull final SQLiteDatabase db, @NonNull final String key, @NonNull final String clientId, @NonNull final String name, @NonNull final byte[] data)395     static int storeBlob(@NonNull final SQLiteDatabase db, @NonNull final String key,
396             @NonNull final String clientId, @NonNull final String name,
397             @NonNull final byte[] data) {
398         final long res = db.insertWithOnConflict(PrivateDataContract.TABLENAME, null,
399                 toContentValues(key, clientId, name, data), SQLiteDatabase.CONFLICT_REPLACE);
400         return (res == -1) ? Status.ERROR_STORAGE : Status.SUCCESS;
401     }
402 
403     @Nullable
retrieveNetworkAttributes(@onNull final SQLiteDatabase db, @NonNull final String key)404     static NetworkAttributes retrieveNetworkAttributes(@NonNull final SQLiteDatabase db,
405             @NonNull final String key) {
406         try (Cursor cursor = db.query(NetworkAttributesContract.TABLENAME,
407                 null, // columns, null means everything
408                 NetworkAttributesContract.COLNAME_L2KEY + " = ?", // selection
409                 new String[] { key }, // selectionArgs
410                 null, // groupBy
411                 null, // having
412                 null)) { // orderBy
413             // L2KEY is the primary key ; it should not be possible to get more than one
414             // result here. 0 results means the key was not found.
415             if (cursor.getCount() != 1) return null;
416             cursor.moveToFirst();
417             return readNetworkAttributesLine(cursor);
418         }
419     }
420 
421     private static final String[] DATA_COLUMN = new String[] {
422             PrivateDataContract.COLNAME_DATA
423     };
424 
425     @Nullable
retrieveBlob(@onNull final SQLiteDatabase db, @NonNull final String key, @NonNull final String clientId, @NonNull final String name)426     static byte[] retrieveBlob(@NonNull final SQLiteDatabase db, @NonNull final String key,
427             @NonNull final String clientId, @NonNull final String name) {
428         try (Cursor cursor = db.query(PrivateDataContract.TABLENAME,
429                 DATA_COLUMN, // columns
430                 PrivateDataContract.COLNAME_L2KEY + " = ? AND " // selection
431                 + PrivateDataContract.COLNAME_CLIENT + " = ? AND "
432                 + PrivateDataContract.COLNAME_DATANAME + " = ?",
433                 new String[] { key, clientId, name }, // selectionArgs
434                 null, // groupBy
435                 null, // having
436                 null)) { // orderBy
437             // The query above is querying by (composite) primary key, so it should not be possible
438             // to get more than one result here. 0 results means the key was not found.
439             if (cursor.getCount() != 1) return null;
440             cursor.moveToFirst();
441             return cursor.getBlob(0); // index in the DATA_COLUMN array
442         }
443     }
444 
445     /**
446      * Wipe all data in tables when network factory reset occurs.
447      */
wipeDataUponNetworkReset(@onNull final SQLiteDatabase db)448     static void wipeDataUponNetworkReset(@NonNull final SQLiteDatabase db) {
449         for (int remainingRetries = 3; remainingRetries > 0; --remainingRetries) {
450             db.beginTransaction();
451             try {
452                 db.delete(NetworkAttributesContract.TABLENAME, null, null);
453                 db.delete(PrivateDataContract.TABLENAME, null, null);
454                 try (Cursor cursorNetworkAttributes = db.query(
455                         // table name
456                         NetworkAttributesContract.TABLENAME,
457                         // column name
458                         new String[] { NetworkAttributesContract.COLNAME_L2KEY },
459                         null, // selection
460                         null, // selectionArgs
461                         null, // groupBy
462                         null, // having
463                         null, // orderBy
464                         "1")) { // limit
465                     if (0 != cursorNetworkAttributes.getCount()) continue;
466                 }
467                 try (Cursor cursorPrivateData = db.query(
468                         // table name
469                         PrivateDataContract.TABLENAME,
470                         // column name
471                         new String[] { PrivateDataContract.COLNAME_L2KEY },
472                         null, // selection
473                         null, // selectionArgs
474                         null, // groupBy
475                         null, // having
476                         null, // orderBy
477                         "1")) { // limit
478                     if (0 != cursorPrivateData.getCount()) continue;
479                 }
480                 db.setTransactionSuccessful();
481             } catch (SQLiteException e) {
482                 Log.e(TAG, "Could not wipe the data in database", e);
483             } finally {
484                 db.endTransaction();
485             }
486         }
487     }
488 
489     /**
490      * The following is a horrible hack that is necessary because the Android SQLite API does not
491      * have a way to query a binary blob. This, almost certainly, is an overlook.
492      *
493      * The Android SQLite API has two family of methods : one for query that returns data, and
494      * one for more general SQL statements that can execute any statement but may not return
495      * anything. All the query methods, however, take only String[] for the arguments.
496      *
497      * In principle it is simple to write a function that will encode the binary blob in the
498      * way SQLite expects it. However, because the API forces the argument to be coerced into a
499      * String, the SQLiteQuery object generated by the default query methods will bind all
500      * arguments as Strings and SQL will *sanitize* them. This works okay for numeric types,
501      * but the format for blobs is x'<hex string>'. Note the presence of quotes, which will
502      * be sanitized, changing the contents of the field, and the query will fail to match the
503      * blob.
504      *
505      * As far as I can tell, there are two possible ways around this problem. The first one
506      * is to put the data in the query string and eschew it being an argument. This would
507      * require doing the sanitizing by hand. The other is to call bindBlob directly on the
508      * generated SQLiteQuery object, which not only is a lot less dangerous than rolling out
509      * sanitizing, but also will do the right thing if the underlying format ever changes.
510      *
511      * But none of the methods that take an SQLiteQuery object can return data ; this *must*
512      * be called with SQLiteDatabase#query. This object is not accessible from outside.
513      * However, there is a #query version that accepts a CursorFactory and this is pretty
514      * straightforward to implement as all the arguments are coming in and the SQLiteCursor
515      * class is public API.
516      * With this, it's possible to intercept the SQLiteQuery object, and assuming the args
517      * are available, to bind them directly and work around the API's oblivious coercion into
518      * Strings.
519      *
520      * This is really sad, but I don't see another way of having this work than this or the
521      * hand-rolled sanitizing, and this is the lesser evil.
522      */
523     private static class CustomCursorFactory implements SQLiteDatabase.CursorFactory {
524         @NonNull
525         private final ArrayList<Object> mArgs;
CustomCursorFactory(@onNull final ArrayList<Object> args)526         CustomCursorFactory(@NonNull final ArrayList<Object> args) {
527             mArgs = args;
528         }
529         @Override
newCursor(final SQLiteDatabase db, final SQLiteCursorDriver masterQuery, final String editTable, final SQLiteQuery query)530         public Cursor newCursor(final SQLiteDatabase db, final SQLiteCursorDriver masterQuery,
531                 final String editTable,
532                 final SQLiteQuery query) {
533             int index = 1; // bind is 1-indexed
534             for (final Object arg : mArgs) {
535                 if (arg instanceof String) {
536                     query.bindString(index++, (String) arg);
537                 } else if (arg instanceof Long) {
538                     query.bindLong(index++, (Long) arg);
539                 } else if (arg instanceof Integer) {
540                     query.bindLong(index++, Long.valueOf((Integer) arg));
541                 } else if (arg instanceof byte[]) {
542                     query.bindBlob(index++, (byte[]) arg);
543                 } else {
544                     throw new IllegalStateException("Unsupported type CustomCursorFactory "
545                             + arg.getClass().toString());
546                 }
547             }
548             return new SQLiteCursor(masterQuery, editTable, query);
549         }
550     }
551 
552     // Returns the l2key of the closest match, if and only if it matches
553     // closely enough (as determined by group-closeness).
554     @Nullable
findClosestAttributes(@onNull final SQLiteDatabase db, @NonNull final NetworkAttributes attr)555     static String findClosestAttributes(@NonNull final SQLiteDatabase db,
556             @NonNull final NetworkAttributes attr) {
557         if (attr.isEmpty()) return null;
558         final ContentValues values = toContentValues(attr);
559 
560         // Build the selection and args. To cut down on the number of lines to search, limit
561         // the search to those with at least one argument equals to the requested attributes.
562         // This works only because null attributes match only will not result in group-closeness.
563         final StringJoiner sj = new StringJoiner(" OR ");
564         final ArrayList<Object> args = new ArrayList<>();
565         args.add(System.currentTimeMillis());
566         for (final String field : values.keySet()) {
567             sj.add(field + " = ?");
568             args.add(values.get(field));
569         }
570 
571         final String selection = NetworkAttributesContract.COLNAME_EXPIRYDATE + " > ? AND ("
572                 + sj.toString() + ")";
573         try (Cursor cursor = db.queryWithFactory(new CustomCursorFactory(args),
574                 false, // distinct
575                 NetworkAttributesContract.TABLENAME,
576                 null, // columns, null means everything
577                 selection, // selection
578                 null, // selectionArgs, horrendously passed to the cursor factory instead
579                 null, // groupBy
580                 null, // having
581                 null, // orderBy
582                 null)) { // limit
583             if (cursor.getCount() <= 0) return null;
584             cursor.moveToFirst();
585             String bestKey = null;
586             float bestMatchConfidence =
587                     GROUPCLOSE_CONFIDENCE; // Never return a match worse than this.
588             while (!cursor.isAfterLast()) {
589                 final NetworkAttributes read = readNetworkAttributesLine(cursor);
590                 final float confidence = read.getNetworkGroupSamenessConfidence(attr);
591                 if (confidence > bestMatchConfidence) {
592                     bestKey = getString(cursor, NetworkAttributesContract.COLNAME_L2KEY);
593                     bestMatchConfidence = confidence;
594                 }
595                 cursor.moveToNext();
596             }
597             return bestKey;
598         }
599     }
600 
601     /**
602      * Delete a single entry by key.
603      *
604      * If |needWipe| is true, the data will be wiped from disk immediately. Otherwise, it will
605      * only be marked deleted, and overwritten by subsequent writes or reclaimed during the next
606      * maintenance window.
607      * Note that wiping data is a very expensive operation. This is meant for clients that need
608      * this data gone from disk immediately for security reasons. Functionally it makes no
609      * difference at all.
610      */
delete(@onNull final SQLiteDatabase db, @NonNull final String l2key, final boolean needWipe)611     static StatusAndCount delete(@NonNull final SQLiteDatabase db, @NonNull final String l2key,
612             final boolean needWipe) {
613         return deleteEntriesWithColumn(db,
614                 NetworkAttributesContract.COLNAME_L2KEY, l2key, needWipe);
615     }
616 
617     /**
618      * Delete all entries that have a particular cluster value.
619      *
620      * If |needWipe| is true, the data will be wiped from disk immediately. Otherwise, it will
621      * only be marked deleted, and overwritten by subsequent writes or reclaimed during the next
622      * maintenance window.
623      * Note that wiping data is a very expensive operation. This is meant for clients that need
624      * this data gone from disk immediately for security reasons. Functionally it makes no
625      * difference at all.
626      */
deleteCluster(@onNull final SQLiteDatabase db, @NonNull final String cluster, final boolean needWipe)627     static StatusAndCount deleteCluster(@NonNull final SQLiteDatabase db,
628             @NonNull final String cluster, final boolean needWipe) {
629         return deleteEntriesWithColumn(db,
630                 NetworkAttributesContract.COLNAME_CLUSTER, cluster, needWipe);
631     }
632 
633     // Delete all entries where the given column has the given value.
deleteEntriesWithColumn(@onNull final SQLiteDatabase db, @NonNull final String column, @NonNull final String value, final boolean needWipe)634     private static StatusAndCount deleteEntriesWithColumn(@NonNull final SQLiteDatabase db,
635             @NonNull final String column, @NonNull final String value, final boolean needWipe) {
636         db.beginTransaction();
637         int deleted = 0;
638         try {
639             deleted = db.delete(NetworkAttributesContract.TABLENAME,
640                     column + "= ?", new String[] { value });
641             db.setTransactionSuccessful();
642         } catch (SQLiteException e) {
643             Log.e(TAG, "Could not delete from the memory store", e);
644             // Unclear what might have happened ; deleting records is not supposed to be able
645             // to fail barring a syntax error in the SQL query.
646             return new StatusAndCount(Status.ERROR_UNKNOWN, 0);
647         } finally {
648             db.endTransaction();
649         }
650 
651         if (needWipe) {
652             final int vacuumStatus = vacuum(db);
653             // This is a problem for the client : return the failure
654             if (Status.SUCCESS != vacuumStatus) return new StatusAndCount(vacuumStatus, deleted);
655         }
656         return new StatusAndCount(Status.SUCCESS, deleted);
657     }
658 
659     // Drops all records that are expired. Relevance has decayed to zero of these records. Returns
660     // an int out of Status.{SUCCESS, ERROR_*}
dropAllExpiredRecords(@onNull final SQLiteDatabase db)661     static int dropAllExpiredRecords(@NonNull final SQLiteDatabase db) {
662         db.beginTransaction();
663         try {
664             // Deletes NetworkAttributes that have expired.
665             db.delete(NetworkAttributesContract.TABLENAME,
666                     NetworkAttributesContract.COLNAME_EXPIRYDATE + " < ?",
667                     new String[]{Long.toString(System.currentTimeMillis())});
668             db.setTransactionSuccessful();
669         } catch (SQLiteException e) {
670             Log.e(TAG, "Could not delete data from memory store", e);
671             return Status.ERROR_STORAGE;
672         } finally {
673             db.endTransaction();
674         }
675 
676         // Execute vacuuming here if above operation has no exception. If above operation got
677         // exception, vacuuming can be ignored for reducing unnecessary consumption.
678         try {
679             db.execSQL("VACUUM");
680         } catch (SQLiteException e) {
681             // Do nothing.
682         }
683         return Status.SUCCESS;
684     }
685 
686     // Drops number of records that start from the lowest expiryDate. Returns an int out of
687     // Status.{SUCCESS, ERROR_*}
dropNumberOfRecords(@onNull final SQLiteDatabase db, int number)688     static int dropNumberOfRecords(@NonNull final SQLiteDatabase db, int number) {
689         if (number <= 0) {
690             return Status.ERROR_ILLEGAL_ARGUMENT;
691         }
692 
693         // Queries number of NetworkAttributes that start from the lowest expiryDate.
694         final long expiryDate;
695         try (Cursor cursor = db.query(NetworkAttributesContract.TABLENAME,
696                 new String[] {NetworkAttributesContract.COLNAME_EXPIRYDATE}, // columns
697                 null, // selection
698                 null, // selectionArgs
699                 null, // groupBy
700                 null, // having
701                 NetworkAttributesContract.COLNAME_EXPIRYDATE, // orderBy
702                 Integer.toString(number))) { // limit
703             if (cursor == null || cursor.getCount() <= 0) return Status.ERROR_GENERIC;
704             cursor.moveToLast();
705 
706             // Get the expiryDate from last record.
707             expiryDate = getLong(cursor, NetworkAttributesContract.COLNAME_EXPIRYDATE, 0);
708         }
709 
710         db.beginTransaction();
711         try {
712             // Deletes NetworkAttributes that expiryDate are lower than given value.
713             db.delete(NetworkAttributesContract.TABLENAME,
714                     NetworkAttributesContract.COLNAME_EXPIRYDATE + " <= ?",
715                     new String[]{Long.toString(expiryDate)});
716             db.setTransactionSuccessful();
717         } catch (SQLiteException e) {
718             Log.e(TAG, "Could not delete data from memory store", e);
719             return Status.ERROR_STORAGE;
720         } finally {
721             db.endTransaction();
722         }
723 
724         // Execute vacuuming here if above operation has no exception. If above operation got
725         // exception, vacuuming can be ignored for reducing unnecessary consumption.
726         try {
727             db.execSQL("VACUUM");
728         } catch (SQLiteException e) {
729             // Do nothing.
730         }
731         return Status.SUCCESS;
732     }
733 
getTotalRecordNumber(@onNull final SQLiteDatabase db)734     static int getTotalRecordNumber(@NonNull final SQLiteDatabase db) {
735         // Query the total number of NetworkAttributes
736         try (Cursor cursor = db.query(NetworkAttributesContract.TABLENAME,
737                 new String[] {"COUNT(*)"}, // columns
738                 null, // selection
739                 null, // selectionArgs
740                 null, // groupBy
741                 null, // having
742                 null)) { // orderBy
743             cursor.moveToFirst();
744             return cursor == null ? 0 : cursor.getInt(0);
745         }
746     }
747 
748     // Helper methods
getString(final Cursor cursor, final String columnName)749     private static String getString(final Cursor cursor, final String columnName) {
750         final int columnIndex = cursor.getColumnIndex(columnName);
751         return (columnIndex >= 0) ? cursor.getString(columnIndex) : null;
752     }
getBlob(final Cursor cursor, final String columnName)753     private static byte[] getBlob(final Cursor cursor, final String columnName) {
754         final int columnIndex = cursor.getColumnIndex(columnName);
755         return (columnIndex >= 0) ? cursor.getBlob(columnIndex) : null;
756     }
getInt(final Cursor cursor, final String columnName, final int defaultValue)757     private static int getInt(final Cursor cursor, final String columnName,
758             final int defaultValue) {
759         final int columnIndex = cursor.getColumnIndex(columnName);
760         return (columnIndex >= 0) ? cursor.getInt(columnIndex) : defaultValue;
761     }
getLong(final Cursor cursor, final String columnName, final long defaultValue)762     private static long getLong(final Cursor cursor, final String columnName,
763             final long defaultValue) {
764         final int columnIndex = cursor.getColumnIndex(columnName);
765         return (columnIndex >= 0) ? cursor.getLong(columnIndex) : defaultValue;
766     }
vacuum(@onNull final SQLiteDatabase db)767     private static int vacuum(@NonNull final SQLiteDatabase db) {
768         try {
769             db.execSQL("VACUUM");
770             return Status.SUCCESS;
771         } catch (SQLiteException e) {
772             // Vacuuming may fail from lack of storage, because it makes a copy of the database.
773             return Status.ERROR_STORAGE;
774         }
775     }
776 }
777