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         private static final int SCHEMA_VERSION = 4;
151         private static final String DATABASE_FILENAME = "IpMemoryStore.db";
152         private static final String TRIGGER_NAME = "delete_cascade_to_private";
153 
DbHelper(@onNull final Context context)154         public DbHelper(@NonNull final Context context) {
155             super(context, DATABASE_FILENAME, null, SCHEMA_VERSION);
156             setIdleConnectionTimeout(IDLE_CONNECTION_TIMEOUT_MS);
157         }
158 
159         /** Called when the database is created */
160         @Override
onCreate(@onNull final SQLiteDatabase db)161         public void onCreate(@NonNull final SQLiteDatabase db) {
162             db.execSQL(NetworkAttributesContract.CREATE_TABLE);
163             db.execSQL(PrivateDataContract.CREATE_TABLE);
164             createTrigger(db);
165         }
166 
167         /** Called when the database is upgraded */
168         @Override
onUpgrade(@onNull final SQLiteDatabase db, final int oldVersion, final int newVersion)169         public void onUpgrade(@NonNull final SQLiteDatabase db, final int oldVersion,
170                 final int newVersion) {
171             try {
172                 if (oldVersion < 2) {
173                     // upgrade from version 1 to version 2
174                     // since we start from version 2, do nothing here
175                 }
176 
177                 if (oldVersion < 3) {
178                     // upgrade from version 2 to version 3
179                     final String sqlUpgradeAddressExpiry = "alter table"
180                             + " " + NetworkAttributesContract.TABLENAME + " ADD"
181                             + " " + NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESSEXPIRY
182                             + " " + NetworkAttributesContract.COLTYPE_ASSIGNEDV4ADDRESSEXPIRY;
183                     db.execSQL(sqlUpgradeAddressExpiry);
184                 }
185 
186                 if (oldVersion < 4) {
187                     createTrigger(db);
188                 }
189             } catch (SQLiteException e) {
190                 Log.e(TAG, "Could not upgrade to the new version", e);
191                 // create database with new version
192                 db.execSQL(NetworkAttributesContract.DROP_TABLE);
193                 db.execSQL(PrivateDataContract.DROP_TABLE);
194                 onCreate(db);
195             }
196         }
197 
198         /** Called when the database is downgraded */
199         @Override
onDowngrade(@onNull final SQLiteDatabase db, final int oldVersion, final int newVersion)200         public void onDowngrade(@NonNull final SQLiteDatabase db, final int oldVersion,
201                 final int newVersion) {
202             // Downgrades always nuke all data and recreate an empty table.
203             db.execSQL(NetworkAttributesContract.DROP_TABLE);
204             db.execSQL(PrivateDataContract.DROP_TABLE);
205             db.execSQL("DROP TRIGGER " + TRIGGER_NAME);
206             onCreate(db);
207         }
208 
createTrigger(@onNull final SQLiteDatabase db)209         private void createTrigger(@NonNull final SQLiteDatabase db) {
210             final String createTrigger = "CREATE TRIGGER " + TRIGGER_NAME
211                     + " DELETE ON " + NetworkAttributesContract.TABLENAME
212                     + " BEGIN"
213                     + " DELETE FROM " + PrivateDataContract.TABLENAME + " WHERE OLD."
214                     + NetworkAttributesContract.COLNAME_L2KEY
215                     + "=" + PrivateDataContract.COLNAME_L2KEY
216                     + "; END;";
217             db.execSQL(createTrigger);
218         }
219     }
220 
221     @NonNull
encodeAddressList(@onNull final List<InetAddress> addresses)222     private static byte[] encodeAddressList(@NonNull final List<InetAddress> addresses) {
223         final ByteArrayOutputStream os = new ByteArrayOutputStream();
224         for (final InetAddress address : addresses) {
225             final byte[] b = address.getAddress();
226             os.write(b.length);
227             os.write(b, 0, b.length);
228         }
229         return os.toByteArray();
230     }
231 
232     @NonNull
decodeAddressList(@onNull final byte[] encoded)233     private static ArrayList<InetAddress> decodeAddressList(@NonNull final byte[] encoded) {
234         final ByteArrayInputStream is = new ByteArrayInputStream(encoded);
235         final ArrayList<InetAddress> addresses = new ArrayList<>();
236         int d = -1;
237         while ((d = is.read()) != -1) {
238             final byte[] bytes = new byte[d];
239             is.read(bytes, 0, d);
240             try {
241                 addresses.add(InetAddress.getByAddress(bytes));
242             } catch (UnknownHostException e) { /* Hopefully impossible */ }
243         }
244         return addresses;
245     }
246 
247     @NonNull
toContentValues(@ullable final NetworkAttributes attributes)248     private static ContentValues toContentValues(@Nullable final NetworkAttributes attributes) {
249         final ContentValues values = new ContentValues();
250         if (null == attributes) return values;
251         if (null != attributes.assignedV4Address) {
252             values.put(NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESS,
253                     inet4AddressToIntHTH(attributes.assignedV4Address));
254         }
255         if (null != attributes.assignedV4AddressExpiry) {
256             values.put(NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESSEXPIRY,
257                     attributes.assignedV4AddressExpiry);
258         }
259         if (null != attributes.cluster) {
260             values.put(NetworkAttributesContract.COLNAME_CLUSTER, attributes.cluster);
261         }
262         if (null != attributes.dnsAddresses) {
263             values.put(NetworkAttributesContract.COLNAME_DNSADDRESSES,
264                     encodeAddressList(attributes.dnsAddresses));
265         }
266         if (null != attributes.mtu) {
267             values.put(NetworkAttributesContract.COLNAME_MTU, attributes.mtu);
268         }
269         return values;
270     }
271 
272     // Convert a NetworkAttributes object to content values to store them in a table compliant
273     // with the contract defined in NetworkAttributesContract.
274     @NonNull
toContentValues(@onNull final String key, @Nullable final NetworkAttributes attributes, final long expiry)275     private static ContentValues toContentValues(@NonNull final String key,
276             @Nullable final NetworkAttributes attributes, final long expiry) {
277         final ContentValues values = toContentValues(attributes);
278         values.put(NetworkAttributesContract.COLNAME_L2KEY, key);
279         values.put(NetworkAttributesContract.COLNAME_EXPIRYDATE, expiry);
280         return values;
281     }
282 
283     // Convert a byte array into content values to store it in a table compliant with the
284     // contract defined in PrivateDataContract.
285     @NonNull
toContentValues(@onNull final String key, @NonNull final String clientId, @NonNull final String name, @NonNull final byte[] data)286     private static ContentValues toContentValues(@NonNull final String key,
287             @NonNull final String clientId, @NonNull final String name,
288             @NonNull final byte[] data) {
289         final ContentValues values = new ContentValues();
290         values.put(PrivateDataContract.COLNAME_L2KEY, key);
291         values.put(PrivateDataContract.COLNAME_CLIENT, clientId);
292         values.put(PrivateDataContract.COLNAME_DATANAME, name);
293         values.put(PrivateDataContract.COLNAME_DATA, data);
294         return values;
295     }
296 
297     @Nullable
readNetworkAttributesLine(@onNull final Cursor cursor)298     private static NetworkAttributes readNetworkAttributesLine(@NonNull final Cursor cursor) {
299         // Make sure the data hasn't expired
300         final long expiry = getLong(cursor, NetworkAttributesContract.COLNAME_EXPIRYDATE, -1L);
301         if (expiry < System.currentTimeMillis()) return null;
302 
303         final NetworkAttributes.Builder builder = new NetworkAttributes.Builder();
304         final int assignedV4AddressInt = getInt(cursor,
305                 NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESS, 0);
306         final long assignedV4AddressExpiry = getLong(cursor,
307                 NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESSEXPIRY, 0);
308         final String cluster = getString(cursor, NetworkAttributesContract.COLNAME_CLUSTER);
309         final byte[] dnsAddressesBlob =
310                 getBlob(cursor, NetworkAttributesContract.COLNAME_DNSADDRESSES);
311         final int mtu = getInt(cursor, NetworkAttributesContract.COLNAME_MTU, -1);
312         if (0 != assignedV4AddressInt) {
313             builder.setAssignedV4Address(intToInet4AddressHTH(assignedV4AddressInt));
314         }
315         if (0 != assignedV4AddressExpiry) {
316             builder.setAssignedV4AddressExpiry(assignedV4AddressExpiry);
317         }
318         builder.setCluster(cluster);
319         if (null != dnsAddressesBlob) {
320             builder.setDnsAddresses(decodeAddressList(dnsAddressesBlob));
321         }
322         if (mtu >= 0) {
323             builder.setMtu(mtu);
324         }
325         return builder.build();
326     }
327 
328     private static final String[] EXPIRY_COLUMN = new String[] {
329         NetworkAttributesContract.COLNAME_EXPIRYDATE
330     };
331     static final int EXPIRY_ERROR = -1; // Legal values for expiry are positive
332 
333     static final String SELECT_L2KEY = NetworkAttributesContract.COLNAME_L2KEY + " = ?";
334 
335     // Returns the expiry date of the specified row, or one of the error codes above if the
336     // row is not found or some other error
getExpiry(@onNull final SQLiteDatabase db, @NonNull final String key)337     static long getExpiry(@NonNull final SQLiteDatabase db, @NonNull final String key) {
338         try (Cursor cursor = db.query(NetworkAttributesContract.TABLENAME,
339                 EXPIRY_COLUMN, // columns
340                 SELECT_L2KEY, // selection
341                 new String[] { key }, // selectionArgs
342                 null, // groupBy
343                 null, // having
344                 null)) { // orderBy
345             // L2KEY is the primary key ; it should not be possible to get more than one
346             // result here. 0 results means the key was not found.
347             if (cursor.getCount() != 1) return EXPIRY_ERROR;
348             cursor.moveToFirst();
349             return cursor.getLong(0); // index in the EXPIRY_COLUMN array
350         }
351     }
352 
353     static final int RELEVANCE_ERROR = -1; // Legal values for relevance are positive
354 
355     // Returns the relevance of the specified row, or one of the error codes above if the
356     // row is not found or some other error
getRelevance(@onNull final SQLiteDatabase db, @NonNull final String key)357     static int getRelevance(@NonNull final SQLiteDatabase db, @NonNull final String key) {
358         final long expiry = getExpiry(db, key);
359         return expiry < 0 ? (int) expiry : RelevanceUtils.computeRelevanceForNow(expiry);
360     }
361 
362     // If the attributes are null, this will only write the expiry.
363     // Returns an int out of Status.{SUCCESS, ERROR_*}
storeNetworkAttributes(@onNull final SQLiteDatabase db, @NonNull final String key, final long expiry, @Nullable final NetworkAttributes attributes)364     static int storeNetworkAttributes(@NonNull final SQLiteDatabase db, @NonNull final String key,
365             final long expiry, @Nullable final NetworkAttributes attributes) {
366         final ContentValues cv = toContentValues(key, attributes, expiry);
367         db.beginTransaction();
368         try {
369             // Unfortunately SQLite does not have any way to do INSERT OR UPDATE. Options are
370             // to either insert with on conflict ignore then update (like done here), or to
371             // construct a custom SQL INSERT statement with nested select.
372             final long resultId = db.insertWithOnConflict(NetworkAttributesContract.TABLENAME,
373                     null, cv, SQLiteDatabase.CONFLICT_IGNORE);
374             if (resultId < 0) {
375                 db.update(NetworkAttributesContract.TABLENAME, cv, SELECT_L2KEY, new String[]{key});
376             }
377             db.setTransactionSuccessful();
378             return Status.SUCCESS;
379         } catch (SQLiteException e) {
380             // No space left on disk or something
381             Log.e(TAG, "Could not write to the memory store", e);
382         } finally {
383             db.endTransaction();
384         }
385         return Status.ERROR_STORAGE;
386     }
387 
388     // 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)389     static int storeBlob(@NonNull final SQLiteDatabase db, @NonNull final String key,
390             @NonNull final String clientId, @NonNull final String name,
391             @NonNull final byte[] data) {
392         final long res = db.insertWithOnConflict(PrivateDataContract.TABLENAME, null,
393                 toContentValues(key, clientId, name, data), SQLiteDatabase.CONFLICT_REPLACE);
394         return (res == -1) ? Status.ERROR_STORAGE : Status.SUCCESS;
395     }
396 
397     @Nullable
retrieveNetworkAttributes(@onNull final SQLiteDatabase db, @NonNull final String key)398     static NetworkAttributes retrieveNetworkAttributes(@NonNull final SQLiteDatabase db,
399             @NonNull final String key) {
400         try (Cursor cursor = db.query(NetworkAttributesContract.TABLENAME,
401                 null, // columns, null means everything
402                 NetworkAttributesContract.COLNAME_L2KEY + " = ?", // selection
403                 new String[] { key }, // selectionArgs
404                 null, // groupBy
405                 null, // having
406                 null)) { // orderBy
407             // L2KEY is the primary key ; it should not be possible to get more than one
408             // result here. 0 results means the key was not found.
409             if (cursor.getCount() != 1) return null;
410             cursor.moveToFirst();
411             return readNetworkAttributesLine(cursor);
412         }
413     }
414 
415     private static final String[] DATA_COLUMN = new String[] {
416             PrivateDataContract.COLNAME_DATA
417     };
418 
419     @Nullable
retrieveBlob(@onNull final SQLiteDatabase db, @NonNull final String key, @NonNull final String clientId, @NonNull final String name)420     static byte[] retrieveBlob(@NonNull final SQLiteDatabase db, @NonNull final String key,
421             @NonNull final String clientId, @NonNull final String name) {
422         try (Cursor cursor = db.query(PrivateDataContract.TABLENAME,
423                 DATA_COLUMN, // columns
424                 PrivateDataContract.COLNAME_L2KEY + " = ? AND " // selection
425                 + PrivateDataContract.COLNAME_CLIENT + " = ? AND "
426                 + PrivateDataContract.COLNAME_DATANAME + " = ?",
427                 new String[] { key, clientId, name }, // selectionArgs
428                 null, // groupBy
429                 null, // having
430                 null)) { // orderBy
431             // The query above is querying by (composite) primary key, so it should not be possible
432             // to get more than one result here. 0 results means the key was not found.
433             if (cursor.getCount() != 1) return null;
434             cursor.moveToFirst();
435             return cursor.getBlob(0); // index in the DATA_COLUMN array
436         }
437     }
438 
439     /**
440      * Wipe all data in tables when network factory reset occurs.
441      */
wipeDataUponNetworkReset(@onNull final SQLiteDatabase db)442     static void wipeDataUponNetworkReset(@NonNull final SQLiteDatabase db) {
443         for (int remainingRetries = 3; remainingRetries > 0; --remainingRetries) {
444             db.beginTransaction();
445             try {
446                 db.delete(NetworkAttributesContract.TABLENAME, null, null);
447                 db.delete(PrivateDataContract.TABLENAME, null, null);
448                 try (Cursor cursorNetworkAttributes = db.query(
449                         // table name
450                         NetworkAttributesContract.TABLENAME,
451                         // column name
452                         new String[] { NetworkAttributesContract.COLNAME_L2KEY },
453                         null, // selection
454                         null, // selectionArgs
455                         null, // groupBy
456                         null, // having
457                         null, // orderBy
458                         "1")) { // limit
459                     if (0 != cursorNetworkAttributes.getCount()) continue;
460                 }
461                 try (Cursor cursorPrivateData = db.query(
462                         // table name
463                         PrivateDataContract.TABLENAME,
464                         // column name
465                         new String[] { PrivateDataContract.COLNAME_L2KEY },
466                         null, // selection
467                         null, // selectionArgs
468                         null, // groupBy
469                         null, // having
470                         null, // orderBy
471                         "1")) { // limit
472                     if (0 != cursorPrivateData.getCount()) continue;
473                 }
474                 db.setTransactionSuccessful();
475             } catch (SQLiteException e) {
476                 Log.e(TAG, "Could not wipe the data in database", e);
477             } finally {
478                 db.endTransaction();
479             }
480         }
481     }
482 
483     /**
484      * The following is a horrible hack that is necessary because the Android SQLite API does not
485      * have a way to query a binary blob. This, almost certainly, is an overlook.
486      *
487      * The Android SQLite API has two family of methods : one for query that returns data, and
488      * one for more general SQL statements that can execute any statement but may not return
489      * anything. All the query methods, however, take only String[] for the arguments.
490      *
491      * In principle it is simple to write a function that will encode the binary blob in the
492      * way SQLite expects it. However, because the API forces the argument to be coerced into a
493      * String, the SQLiteQuery object generated by the default query methods will bind all
494      * arguments as Strings and SQL will *sanitize* them. This works okay for numeric types,
495      * but the format for blobs is x'<hex string>'. Note the presence of quotes, which will
496      * be sanitized, changing the contents of the field, and the query will fail to match the
497      * blob.
498      *
499      * As far as I can tell, there are two possible ways around this problem. The first one
500      * is to put the data in the query string and eschew it being an argument. This would
501      * require doing the sanitizing by hand. The other is to call bindBlob directly on the
502      * generated SQLiteQuery object, which not only is a lot less dangerous than rolling out
503      * sanitizing, but also will do the right thing if the underlying format ever changes.
504      *
505      * But none of the methods that take an SQLiteQuery object can return data ; this *must*
506      * be called with SQLiteDatabase#query. This object is not accessible from outside.
507      * However, there is a #query version that accepts a CursorFactory and this is pretty
508      * straightforward to implement as all the arguments are coming in and the SQLiteCursor
509      * class is public API.
510      * With this, it's possible to intercept the SQLiteQuery object, and assuming the args
511      * are available, to bind them directly and work around the API's oblivious coercion into
512      * Strings.
513      *
514      * This is really sad, but I don't see another way of having this work than this or the
515      * hand-rolled sanitizing, and this is the lesser evil.
516      */
517     private static class CustomCursorFactory implements SQLiteDatabase.CursorFactory {
518         @NonNull
519         private final ArrayList<Object> mArgs;
CustomCursorFactory(@onNull final ArrayList<Object> args)520         CustomCursorFactory(@NonNull final ArrayList<Object> args) {
521             mArgs = args;
522         }
523         @Override
newCursor(final SQLiteDatabase db, final SQLiteCursorDriver masterQuery, final String editTable, final SQLiteQuery query)524         public Cursor newCursor(final SQLiteDatabase db, final SQLiteCursorDriver masterQuery,
525                 final String editTable,
526                 final SQLiteQuery query) {
527             int index = 1; // bind is 1-indexed
528             for (final Object arg : mArgs) {
529                 if (arg instanceof String) {
530                     query.bindString(index++, (String) arg);
531                 } else if (arg instanceof Long) {
532                     query.bindLong(index++, (Long) arg);
533                 } else if (arg instanceof Integer) {
534                     query.bindLong(index++, Long.valueOf((Integer) arg));
535                 } else if (arg instanceof byte[]) {
536                     query.bindBlob(index++, (byte[]) arg);
537                 } else {
538                     throw new IllegalStateException("Unsupported type CustomCursorFactory "
539                             + arg.getClass().toString());
540                 }
541             }
542             return new SQLiteCursor(masterQuery, editTable, query);
543         }
544     }
545 
546     // Returns the l2key of the closest match, if and only if it matches
547     // closely enough (as determined by group-closeness).
548     @Nullable
findClosestAttributes(@onNull final SQLiteDatabase db, @NonNull final NetworkAttributes attr)549     static String findClosestAttributes(@NonNull final SQLiteDatabase db,
550             @NonNull final NetworkAttributes attr) {
551         if (attr.isEmpty()) return null;
552         final ContentValues values = toContentValues(attr);
553 
554         // Build the selection and args. To cut down on the number of lines to search, limit
555         // the search to those with at least one argument equals to the requested attributes.
556         // This works only because null attributes match only will not result in group-closeness.
557         final StringJoiner sj = new StringJoiner(" OR ");
558         final ArrayList<Object> args = new ArrayList<>();
559         args.add(System.currentTimeMillis());
560         for (final String field : values.keySet()) {
561             sj.add(field + " = ?");
562             args.add(values.get(field));
563         }
564 
565         final String selection = NetworkAttributesContract.COLNAME_EXPIRYDATE + " > ? AND ("
566                 + sj.toString() + ")";
567         try (Cursor cursor = db.queryWithFactory(new CustomCursorFactory(args),
568                 false, // distinct
569                 NetworkAttributesContract.TABLENAME,
570                 null, // columns, null means everything
571                 selection, // selection
572                 null, // selectionArgs, horrendously passed to the cursor factory instead
573                 null, // groupBy
574                 null, // having
575                 null, // orderBy
576                 null)) { // limit
577             if (cursor.getCount() <= 0) return null;
578             cursor.moveToFirst();
579             String bestKey = null;
580             float bestMatchConfidence =
581                     GROUPCLOSE_CONFIDENCE; // Never return a match worse than this.
582             while (!cursor.isAfterLast()) {
583                 final NetworkAttributes read = readNetworkAttributesLine(cursor);
584                 final float confidence = read.getNetworkGroupSamenessConfidence(attr);
585                 if (confidence > bestMatchConfidence) {
586                     bestKey = getString(cursor, NetworkAttributesContract.COLNAME_L2KEY);
587                     bestMatchConfidence = confidence;
588                 }
589                 cursor.moveToNext();
590             }
591             return bestKey;
592         }
593     }
594 
595     /**
596      * Delete a single entry by key.
597      *
598      * If |needWipe| is true, the data will be wiped from disk immediately. Otherwise, it will
599      * only be marked deleted, and overwritten by subsequent writes or reclaimed during the next
600      * maintenance window.
601      * Note that wiping data is a very expensive operation. This is meant for clients that need
602      * this data gone from disk immediately for security reasons. Functionally it makes no
603      * difference at all.
604      */
delete(@onNull final SQLiteDatabase db, @NonNull final String l2key, final boolean needWipe)605     static StatusAndCount delete(@NonNull final SQLiteDatabase db, @NonNull final String l2key,
606             final boolean needWipe) {
607         return deleteEntriesWithColumn(db,
608                 NetworkAttributesContract.COLNAME_L2KEY, l2key, needWipe);
609     }
610 
611     /**
612      * Delete all entries that have a particular cluster value.
613      *
614      * If |needWipe| is true, the data will be wiped from disk immediately. Otherwise, it will
615      * only be marked deleted, and overwritten by subsequent writes or reclaimed during the next
616      * maintenance window.
617      * Note that wiping data is a very expensive operation. This is meant for clients that need
618      * this data gone from disk immediately for security reasons. Functionally it makes no
619      * difference at all.
620      */
deleteCluster(@onNull final SQLiteDatabase db, @NonNull final String cluster, final boolean needWipe)621     static StatusAndCount deleteCluster(@NonNull final SQLiteDatabase db,
622             @NonNull final String cluster, final boolean needWipe) {
623         return deleteEntriesWithColumn(db,
624                 NetworkAttributesContract.COLNAME_CLUSTER, cluster, needWipe);
625     }
626 
627     // 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)628     private static StatusAndCount deleteEntriesWithColumn(@NonNull final SQLiteDatabase db,
629             @NonNull final String column, @NonNull final String value, final boolean needWipe) {
630         db.beginTransaction();
631         int deleted = 0;
632         try {
633             deleted = db.delete(NetworkAttributesContract.TABLENAME,
634                     column + "= ?", new String[] { value });
635             db.setTransactionSuccessful();
636         } catch (SQLiteException e) {
637             Log.e(TAG, "Could not delete from the memory store", e);
638             // Unclear what might have happened ; deleting records is not supposed to be able
639             // to fail barring a syntax error in the SQL query.
640             return new StatusAndCount(Status.ERROR_UNKNOWN, 0);
641         } finally {
642             db.endTransaction();
643         }
644 
645         if (needWipe) {
646             final int vacuumStatus = vacuum(db);
647             // This is a problem for the client : return the failure
648             if (Status.SUCCESS != vacuumStatus) return new StatusAndCount(vacuumStatus, deleted);
649         }
650         return new StatusAndCount(Status.SUCCESS, deleted);
651     }
652 
653     // Drops all records that are expired. Relevance has decayed to zero of these records. Returns
654     // an int out of Status.{SUCCESS, ERROR_*}
dropAllExpiredRecords(@onNull final SQLiteDatabase db)655     static int dropAllExpiredRecords(@NonNull final SQLiteDatabase db) {
656         db.beginTransaction();
657         try {
658             // Deletes NetworkAttributes that have expired.
659             db.delete(NetworkAttributesContract.TABLENAME,
660                     NetworkAttributesContract.COLNAME_EXPIRYDATE + " < ?",
661                     new String[]{Long.toString(System.currentTimeMillis())});
662             db.setTransactionSuccessful();
663         } catch (SQLiteException e) {
664             Log.e(TAG, "Could not delete data from memory store", e);
665             return Status.ERROR_STORAGE;
666         } finally {
667             db.endTransaction();
668         }
669 
670         // Execute vacuuming here if above operation has no exception. If above operation got
671         // exception, vacuuming can be ignored for reducing unnecessary consumption.
672         try {
673             db.execSQL("VACUUM");
674         } catch (SQLiteException e) {
675             // Do nothing.
676         }
677         return Status.SUCCESS;
678     }
679 
680     // Drops number of records that start from the lowest expiryDate. Returns an int out of
681     // Status.{SUCCESS, ERROR_*}
dropNumberOfRecords(@onNull final SQLiteDatabase db, int number)682     static int dropNumberOfRecords(@NonNull final SQLiteDatabase db, int number) {
683         if (number <= 0) {
684             return Status.ERROR_ILLEGAL_ARGUMENT;
685         }
686 
687         // Queries number of NetworkAttributes that start from the lowest expiryDate.
688         final long expiryDate;
689         try (Cursor cursor = db.query(NetworkAttributesContract.TABLENAME,
690                 new String[] {NetworkAttributesContract.COLNAME_EXPIRYDATE}, // columns
691                 null, // selection
692                 null, // selectionArgs
693                 null, // groupBy
694                 null, // having
695                 NetworkAttributesContract.COLNAME_EXPIRYDATE, // orderBy
696                 Integer.toString(number))) { // limit
697             if (cursor == null || cursor.getCount() <= 0) return Status.ERROR_GENERIC;
698             cursor.moveToLast();
699 
700             // Get the expiryDate from last record.
701             expiryDate = getLong(cursor, NetworkAttributesContract.COLNAME_EXPIRYDATE, 0);
702         }
703 
704         db.beginTransaction();
705         try {
706             // Deletes NetworkAttributes that expiryDate are lower than given value.
707             db.delete(NetworkAttributesContract.TABLENAME,
708                     NetworkAttributesContract.COLNAME_EXPIRYDATE + " <= ?",
709                     new String[]{Long.toString(expiryDate)});
710             db.setTransactionSuccessful();
711         } catch (SQLiteException e) {
712             Log.e(TAG, "Could not delete data from memory store", e);
713             return Status.ERROR_STORAGE;
714         } finally {
715             db.endTransaction();
716         }
717 
718         // Execute vacuuming here if above operation has no exception. If above operation got
719         // exception, vacuuming can be ignored for reducing unnecessary consumption.
720         try {
721             db.execSQL("VACUUM");
722         } catch (SQLiteException e) {
723             // Do nothing.
724         }
725         return Status.SUCCESS;
726     }
727 
getTotalRecordNumber(@onNull final SQLiteDatabase db)728     static int getTotalRecordNumber(@NonNull final SQLiteDatabase db) {
729         // Query the total number of NetworkAttributes
730         try (Cursor cursor = db.query(NetworkAttributesContract.TABLENAME,
731                 new String[] {"COUNT(*)"}, // columns
732                 null, // selection
733                 null, // selectionArgs
734                 null, // groupBy
735                 null, // having
736                 null)) { // orderBy
737             cursor.moveToFirst();
738             return cursor == null ? 0 : cursor.getInt(0);
739         }
740     }
741 
742     // Helper methods
getString(final Cursor cursor, final String columnName)743     private static String getString(final Cursor cursor, final String columnName) {
744         final int columnIndex = cursor.getColumnIndex(columnName);
745         return (columnIndex >= 0) ? cursor.getString(columnIndex) : null;
746     }
getBlob(final Cursor cursor, final String columnName)747     private static byte[] getBlob(final Cursor cursor, final String columnName) {
748         final int columnIndex = cursor.getColumnIndex(columnName);
749         return (columnIndex >= 0) ? cursor.getBlob(columnIndex) : null;
750     }
getInt(final Cursor cursor, final String columnName, final int defaultValue)751     private static int getInt(final Cursor cursor, final String columnName,
752             final int defaultValue) {
753         final int columnIndex = cursor.getColumnIndex(columnName);
754         return (columnIndex >= 0) ? cursor.getInt(columnIndex) : defaultValue;
755     }
getLong(final Cursor cursor, final String columnName, final long defaultValue)756     private static long getLong(final Cursor cursor, final String columnName,
757             final long defaultValue) {
758         final int columnIndex = cursor.getColumnIndex(columnName);
759         return (columnIndex >= 0) ? cursor.getLong(columnIndex) : defaultValue;
760     }
vacuum(@onNull final SQLiteDatabase db)761     private static int vacuum(@NonNull final SQLiteDatabase db) {
762         try {
763             db.execSQL("VACUUM");
764             return Status.SUCCESS;
765         } catch (SQLiteException e) {
766             // Vacuuming may fail from lack of storage, because it makes a copy of the database.
767             return Status.ERROR_STORAGE;
768         }
769     }
770 }
771