• 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.networkstack.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 com.android.internal.annotations.VisibleForTesting;
39 
40 import java.io.ByteArrayInputStream;
41 import java.io.ByteArrayOutputStream;
42 import java.io.File;
43 import java.net.InetAddress;
44 import java.net.UnknownHostException;
45 import java.util.ArrayList;
46 import java.util.List;
47 import java.util.StringJoiner;
48 
49 /**
50  * Encapsulating class for using the SQLite database backing the memory store.
51  *
52  * This class groups together the contracts and the SQLite helper used to
53  * use the database.
54  *
55  * @hide
56  */
57 public class IpMemoryStoreDatabase {
58     private static final String TAG = IpMemoryStoreDatabase.class.getSimpleName();
59     // A pair of NetworkAttributes objects is group-close if the confidence that they are
60     // the same is above this cutoff. See NetworkAttributes and SameL3NetworkResponse.
61     private static final float GROUPCLOSE_CONFIDENCE = 0.5f;
62 
63     /**
64      * Contract class for the Network Attributes table.
65      */
66     public static final class NetworkAttributesContract {
NetworkAttributesContract()67         private NetworkAttributesContract() {}
68 
69         public static final String TABLENAME = "NetworkAttributes";
70 
71         public static final String COLNAME_L2KEY = "l2Key";
72         public static final String COLTYPE_L2KEY = "TEXT NOT NULL";
73 
74         public static final String COLNAME_EXPIRYDATE = "expiryDate";
75         // Milliseconds since the Epoch, in true Java style
76         public static final String COLTYPE_EXPIRYDATE = "BIGINT";
77 
78         public static final String COLNAME_ASSIGNEDV4ADDRESS = "assignedV4Address";
79         public static final String COLTYPE_ASSIGNEDV4ADDRESS = "INTEGER";
80 
81         public static final String COLNAME_ASSIGNEDV4ADDRESSEXPIRY = "assignedV4AddressExpiry";
82         // The lease expiry timestamp in uint of milliseconds since the Epoch. Long.MAX_VALUE
83         // is used to represent "infinite lease".
84         public static final String COLTYPE_ASSIGNEDV4ADDRESSEXPIRY = "BIGINT";
85 
86         // An optional cluster representing a notion of group owned by the client. The memory
87         // store uses this as a hint for grouping, but not as an overriding factor. The client
88         // can then use this to find networks belonging to a cluster. An example of this could
89         // be the SSID for WiFi, where same SSID-networks may not be the same L3 networks but
90         // it's still useful for managing networks.
91         // Note that "groupHint" is the legacy name of the column. The column should be renamed
92         // in the database – ALTER TABLE ${NetworkAttributesContract.TABLENAME RENAME} COLUMN
93         // groupHint TO cluster – but this has been postponed to reduce risk as the Mainline
94         // release winter imposes a lot of changes be pushed at the same time in the next release.
95         public static final String COLNAME_CLUSTER = "groupHint";
96         public static final String COLTYPE_CLUSTER = "TEXT";
97 
98         public static final String COLNAME_DNSADDRESSES = "dnsAddresses";
99         // Stored in marshalled form as is
100         public static final String COLTYPE_DNSADDRESSES = "BLOB";
101 
102         public static final String COLNAME_MTU = "mtu";
103         public static final String COLTYPE_MTU = "INTEGER DEFAULT -1";
104 
105         public static final String CREATE_TABLE = "CREATE TABLE IF NOT EXISTS "
106                 + TABLENAME                       + " ("
107                 + COLNAME_L2KEY                   + " " + COLTYPE_L2KEY + " PRIMARY KEY NOT NULL, "
108                 + COLNAME_EXPIRYDATE              + " " + COLTYPE_EXPIRYDATE              + ", "
109                 + COLNAME_ASSIGNEDV4ADDRESS       + " " + COLTYPE_ASSIGNEDV4ADDRESS       + ", "
110                 + COLNAME_ASSIGNEDV4ADDRESSEXPIRY + " " + COLTYPE_ASSIGNEDV4ADDRESSEXPIRY + ", "
111                 + COLNAME_CLUSTER                 + " " + COLTYPE_CLUSTER                 + ", "
112                 + COLNAME_DNSADDRESSES            + " " + COLTYPE_DNSADDRESSES            + ", "
113                 + COLNAME_MTU                     + " " + COLTYPE_MTU                     + ")";
114         public static final String DROP_TABLE = "DROP TABLE IF EXISTS " + TABLENAME;
115     }
116 
117     /**
118      * Contract class for the Private Data table.
119      */
120     public static final class PrivateDataContract {
PrivateDataContract()121         private PrivateDataContract() {}
122 
123         public static final String TABLENAME = "PrivateData";
124 
125         public static final String COLNAME_L2KEY = "l2Key";
126         public static final String COLTYPE_L2KEY = "TEXT NOT NULL";
127 
128         public static final String COLNAME_CLIENT = "client";
129         public static final String COLTYPE_CLIENT = "TEXT NOT NULL";
130 
131         public static final String COLNAME_DATANAME = "dataName";
132         public static final String COLTYPE_DATANAME = "TEXT NOT NULL";
133 
134         public static final String COLNAME_DATA = "data";
135         public static final String COLTYPE_DATA = "BLOB NOT NULL";
136 
137         public static final String CREATE_TABLE = "CREATE TABLE IF NOT EXISTS "
138                 + TABLENAME        + " ("
139                 + COLNAME_L2KEY    + " " + COLTYPE_L2KEY    + ", "
140                 + COLNAME_CLIENT   + " " + COLTYPE_CLIENT   + ", "
141                 + COLNAME_DATANAME + " " + COLTYPE_DATANAME + ", "
142                 + COLNAME_DATA     + " " + COLTYPE_DATA     + ", "
143                 + "PRIMARY KEY ("
144                 + COLNAME_L2KEY    + ", "
145                 + COLNAME_CLIENT   + ", "
146                 + COLNAME_DATANAME + "))";
147         public static final String DROP_TABLE = "DROP TABLE IF EXISTS " + TABLENAME;
148     }
149 
150     /**
151      * Contract class for the network events table.
152      */
153     public static final class NetworkEventsContract {
NetworkEventsContract()154         private NetworkEventsContract() {}
155 
156         public static final String TABLENAME = "NetworkEvents";
157 
158         public static final String COLNAME_CLUSTER = "cluster";
159         public static final String COLTYPE_CLUSTER = "TEXT NOT NULL";
160 
161         public static final String COLNAME_TIMESTAMP = "timestamp";
162         public static final String COLTYPE_TIMESTAMP = "BIGINT";
163 
164         public static final String COLNAME_EVENTTYPE = "eventType";
165         public static final String COLTYPE_EVENTTYPE = "INTEGER";
166 
167         public static final String COLNAME_EXPIRY = "expiry";
168         // Milliseconds since the Epoch, in true Java style
169         public static final String COLTYPE_EXPIRY = "BIGINT";
170 
171         public static final String CREATE_TABLE = "CREATE TABLE IF NOT EXISTS "
172                 + TABLENAME           + " ("
173                 + COLNAME_CLUSTER     + " " + COLTYPE_CLUSTER    + ", "
174                 + COLNAME_TIMESTAMP   + " " + COLTYPE_TIMESTAMP  + ", "
175                 + COLNAME_EVENTTYPE   + " " + COLTYPE_EVENTTYPE  + ", "
176                 + COLNAME_EXPIRY      + " " + COLTYPE_EXPIRY     + ")";
177         public static final String INDEX_NAME = "idx_" + COLNAME_CLUSTER + "_" + COLNAME_TIMESTAMP
178                 + "_" + COLNAME_EVENTTYPE;
179         public static final String CREATE_INDEX = "CREATE INDEX IF NOT EXISTS " + INDEX_NAME
180                 + " ON " + TABLENAME
181                 + " (" + COLNAME_CLUSTER + ", " + COLNAME_TIMESTAMP + ", " + COLNAME_EVENTTYPE
182                 + ")";
183         public static final String DROP_TABLE = "DROP TABLE IF EXISTS " + TABLENAME;
184     }
185 
186     // To save memory when the DB is not used, close it after 30s of inactivity. This is
187     // determined manually based on what feels right.
188     private static final long IDLE_CONNECTION_TIMEOUT_MS = 30_000;
189 
190     /** The SQLite DB helper */
191     public static class DbHelper extends SQLiteOpenHelper {
192         // Update this whenever changing the schema.
193         @VisibleForTesting
194         static final int SCHEMA_VERSION = 5;
195         private static final String DATABASE_FILENAME = "IpMemoryStoreV2.db";
196         private static final String TRIGGER_NAME = "delete_cascade_to_private";
197         private static final String LEGACY_DATABASE_FILENAME = "IpMemoryStore.db";
198 
DbHelper(@onNull final Context context)199         public DbHelper(@NonNull final Context context) {
200             super(context, DATABASE_FILENAME, null, SCHEMA_VERSION);
201             setIdleConnectionTimeout(IDLE_CONNECTION_TIMEOUT_MS);
202         }
203 
204         @VisibleForTesting
DbHelper(@onNull final Context context, int schemaVersion)205         DbHelper(@NonNull final Context context, int schemaVersion) {
206             super(context, DATABASE_FILENAME, null, schemaVersion);
207             setIdleConnectionTimeout(IDLE_CONNECTION_TIMEOUT_MS);
208         }
209 
210         /** Called when the database is created */
211         @Override
onCreate(@onNull final SQLiteDatabase db)212         public void onCreate(@NonNull final SQLiteDatabase db) {
213             db.execSQL(NetworkAttributesContract.CREATE_TABLE);
214             db.execSQL(PrivateDataContract.CREATE_TABLE);
215             db.execSQL(NetworkEventsContract.CREATE_TABLE);
216             db.execSQL(NetworkEventsContract.CREATE_INDEX);
217             createTrigger(db);
218         }
219 
220         /** Called when the database is upgraded */
221         @Override
onUpgrade(@onNull final SQLiteDatabase db, final int oldVersion, final int newVersion)222         public void onUpgrade(@NonNull final SQLiteDatabase db, final int oldVersion,
223                 final int newVersion) {
224             try {
225                 if (oldVersion < 2) {
226                     // upgrade from version 1 to version 2
227                     // since we start from version 2, do nothing here
228                 }
229 
230                 if (oldVersion < 3) {
231                     // upgrade from version 2 to version 3
232                     final String sqlUpgradeAddressExpiry = "alter table"
233                             + " " + NetworkAttributesContract.TABLENAME + " ADD"
234                             + " " + NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESSEXPIRY
235                             + " " + NetworkAttributesContract.COLTYPE_ASSIGNEDV4ADDRESSEXPIRY;
236                     db.execSQL(sqlUpgradeAddressExpiry);
237                 }
238 
239                 if (oldVersion < 4) {
240                     createTrigger(db);
241                 }
242 
243                 if (oldVersion < 5) {
244                     // upgrade from version 4 to version 5, the NetworkEventsTable doesn't exist
245                     // on previous version and onCreate won't be called during upgrade, therefore,
246                     // create the table manually.
247                     db.execSQL(NetworkEventsContract.CREATE_TABLE);
248                     db.execSQL(NetworkEventsContract.CREATE_INDEX);
249                 }
250             } catch (SQLiteException e) {
251                 Log.e(TAG, "Could not upgrade to the new version", e);
252                 // create database with new version
253                 db.execSQL(NetworkAttributesContract.DROP_TABLE);
254                 db.execSQL(PrivateDataContract.DROP_TABLE);
255                 db.execSQL(NetworkEventsContract.DROP_TABLE);
256                 onCreate(db);
257             }
258         }
259 
260         /** Called when the database is downgraded */
261         @Override
onDowngrade(@onNull final SQLiteDatabase db, final int oldVersion, final int newVersion)262         public void onDowngrade(@NonNull final SQLiteDatabase db, final int oldVersion,
263                 final int newVersion) {
264             // Downgrades always nuke all data and recreate an empty table.
265             db.execSQL(NetworkAttributesContract.DROP_TABLE);
266             db.execSQL(PrivateDataContract.DROP_TABLE);
267             db.execSQL(NetworkEventsContract.DROP_TABLE);
268             onCreate(db);
269         }
270 
createTrigger(@onNull final SQLiteDatabase db)271         private void createTrigger(@NonNull final SQLiteDatabase db) {
272             final String createTrigger = "CREATE TRIGGER " + TRIGGER_NAME
273                     + " DELETE ON " + NetworkAttributesContract.TABLENAME
274                     + " BEGIN"
275                     + " DELETE FROM " + PrivateDataContract.TABLENAME + " WHERE OLD."
276                     + NetworkAttributesContract.COLNAME_L2KEY
277                     + "=" + PrivateDataContract.COLNAME_L2KEY
278                     + "; END;";
279             db.execSQL(createTrigger);
280         }
281 
282         /**
283          * Renames the database file to prevent crashes during downgrades.
284          * <p>
285          * Previous versions (before 5) has a bug(b/171340630) that would cause a crash when
286          * onDowngrade is triggered. We cannot just bump the schema version without
287          * renaming the database filename, because only bumping the schema version still causes
288          * crash when downgrading to an older version.
289          * <p>
290          * After rename the db file, if the module is rolled back, the legacy file is not present.
291          * The code will create a new legacy database, and will trigger onCreate path. The new
292          * database will continue to exist, but the legacy code does not know about it.
293          * <p>
294          * In later stage, if the module is rolled forward again, the legacy database will overwrite
295          * the new database, the user's data will be preserved.
296          */
maybeRenameDatabaseFile(Context context)297         public static void maybeRenameDatabaseFile(Context context) {
298             final File legacyDb = context.getDatabasePath(LEGACY_DATABASE_FILENAME);
299             if (legacyDb.exists()) {
300                 final File newDb = context.getDatabasePath(DATABASE_FILENAME);
301                 final boolean result = legacyDb.renameTo(newDb);
302                 if (!result) {
303                     Log.w(TAG, "failed to rename the IP Memory store database to "
304                             + DATABASE_FILENAME);
305                 }
306             }
307         }
308     }
309 
310     @NonNull
encodeAddressList(@onNull final List<InetAddress> addresses)311     private static byte[] encodeAddressList(@NonNull final List<InetAddress> addresses) {
312         final ByteArrayOutputStream os = new ByteArrayOutputStream();
313         for (final InetAddress address : addresses) {
314             final byte[] b = address.getAddress();
315             os.write(b.length);
316             os.write(b, 0, b.length);
317         }
318         return os.toByteArray();
319     }
320 
321     @NonNull
decodeAddressList(@onNull final byte[] encoded)322     private static ArrayList<InetAddress> decodeAddressList(@NonNull final byte[] encoded) {
323         final ByteArrayInputStream is = new ByteArrayInputStream(encoded);
324         final ArrayList<InetAddress> addresses = new ArrayList<>();
325         int d = -1;
326         while ((d = is.read()) != -1) {
327             final byte[] bytes = new byte[d];
328             is.read(bytes, 0, d);
329             try {
330                 addresses.add(InetAddress.getByAddress(bytes));
331             } catch (UnknownHostException e) { /* Hopefully impossible */ }
332         }
333         return addresses;
334     }
335 
336     @NonNull
toContentValues(@ullable final NetworkAttributes attributes)337     private static ContentValues toContentValues(@Nullable final NetworkAttributes attributes) {
338         final ContentValues values = new ContentValues();
339         if (null == attributes) return values;
340         if (null != attributes.assignedV4Address) {
341             values.put(NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESS,
342                     inet4AddressToIntHTH(attributes.assignedV4Address));
343         }
344         if (null != attributes.assignedV4AddressExpiry) {
345             values.put(NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESSEXPIRY,
346                     attributes.assignedV4AddressExpiry);
347         }
348         if (null != attributes.cluster) {
349             values.put(NetworkAttributesContract.COLNAME_CLUSTER, attributes.cluster);
350         }
351         if (null != attributes.dnsAddresses) {
352             values.put(NetworkAttributesContract.COLNAME_DNSADDRESSES,
353                     encodeAddressList(attributes.dnsAddresses));
354         }
355         if (null != attributes.mtu) {
356             values.put(NetworkAttributesContract.COLNAME_MTU, attributes.mtu);
357         }
358         return values;
359     }
360 
361     // Convert a NetworkAttributes object to content values to store them in a table compliant
362     // with the contract defined in NetworkAttributesContract.
363     @NonNull
toContentValues(@onNull final String key, @Nullable final NetworkAttributes attributes, final long expiry)364     private static ContentValues toContentValues(@NonNull final String key,
365             @Nullable final NetworkAttributes attributes, final long expiry) {
366         final ContentValues values = toContentValues(attributes);
367         values.put(NetworkAttributesContract.COLNAME_L2KEY, key);
368         values.put(NetworkAttributesContract.COLNAME_EXPIRYDATE, expiry);
369         return values;
370     }
371 
372     // Convert a byte array into content values to store it in a table compliant with the
373     // contract defined in PrivateDataContract.
374     @NonNull
toContentValues(@onNull final String key, @NonNull final String clientId, @NonNull final String name, @NonNull final byte[] data)375     private static ContentValues toContentValues(@NonNull final String key,
376             @NonNull final String clientId, @NonNull final String name,
377             @NonNull final byte[] data) {
378         final ContentValues values = new ContentValues();
379         values.put(PrivateDataContract.COLNAME_L2KEY, key);
380         values.put(PrivateDataContract.COLNAME_CLIENT, clientId);
381         values.put(PrivateDataContract.COLNAME_DATANAME, name);
382         values.put(PrivateDataContract.COLNAME_DATA, data);
383         return values;
384     }
385 
386     /**
387      * Convert a network event (including cluster, timestamp of when it happened, expiry and
388      * event type) into content values to store them in a table compliant with the ontract defined
389      * in NetworkEventsContract.
390      */
391     @NonNull
toContentValues(@onNull final String cluster, final long timestamp, final long expiry, final int eventType)392     private static ContentValues toContentValues(@NonNull final String cluster,
393             final long timestamp, final long expiry, final int eventType) {
394         final ContentValues values = new ContentValues();
395         values.put(NetworkEventsContract.COLNAME_CLUSTER, cluster);
396         values.put(NetworkEventsContract.COLNAME_TIMESTAMP, timestamp);
397         values.put(NetworkEventsContract.COLNAME_EVENTTYPE, eventType);
398         values.put(NetworkEventsContract.COLNAME_EXPIRY, expiry);
399         return values;
400     }
401 
402     @Nullable
readNetworkAttributesLine(@onNull final Cursor cursor)403     private static NetworkAttributes readNetworkAttributesLine(@NonNull final Cursor cursor) {
404         // Make sure the data hasn't expired
405         final long expiry = getLong(cursor, NetworkAttributesContract.COLNAME_EXPIRYDATE, -1L);
406         if (expiry < System.currentTimeMillis()) return null;
407 
408         final NetworkAttributes.Builder builder = new NetworkAttributes.Builder();
409         final int assignedV4AddressInt = getInt(cursor,
410                 NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESS, 0);
411         final long assignedV4AddressExpiry = getLong(cursor,
412                 NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESSEXPIRY, 0);
413         final String cluster = getString(cursor, NetworkAttributesContract.COLNAME_CLUSTER);
414         final byte[] dnsAddressesBlob =
415                 getBlob(cursor, NetworkAttributesContract.COLNAME_DNSADDRESSES);
416         final int mtu = getInt(cursor, NetworkAttributesContract.COLNAME_MTU, -1);
417         if (0 != assignedV4AddressInt) {
418             builder.setAssignedV4Address(intToInet4AddressHTH(assignedV4AddressInt));
419         }
420         if (0 != assignedV4AddressExpiry) {
421             builder.setAssignedV4AddressExpiry(assignedV4AddressExpiry);
422         }
423         builder.setCluster(cluster);
424         if (null != dnsAddressesBlob) {
425             builder.setDnsAddresses(decodeAddressList(dnsAddressesBlob));
426         }
427         if (mtu >= 0) {
428             builder.setMtu(mtu);
429         }
430         return builder.build();
431     }
432 
433     private static final String[] EXPIRY_COLUMN = new String[] {
434         NetworkAttributesContract.COLNAME_EXPIRYDATE
435     };
436     static final int EXPIRY_ERROR = -1; // Legal values for expiry are positive
437 
438     static final String SELECT_L2KEY = NetworkAttributesContract.COLNAME_L2KEY + " = ?";
439 
440     // Returns the expiry date of the specified row, or one of the error codes above if the
441     // row is not found or some other error
getExpiry(@onNull final SQLiteDatabase db, @NonNull final String key)442     static long getExpiry(@NonNull final SQLiteDatabase db, @NonNull final String key) {
443         try (Cursor cursor = db.query(NetworkAttributesContract.TABLENAME,
444                 EXPIRY_COLUMN, // columns
445                 SELECT_L2KEY, // selection
446                 new String[] { key }, // selectionArgs
447                 null, // groupBy
448                 null, // having
449                 null)) { // orderBy
450             // L2KEY is the primary key ; it should not be possible to get more than one
451             // result here. 0 results means the key was not found.
452             if (cursor.getCount() != 1) return EXPIRY_ERROR;
453             cursor.moveToFirst();
454             return cursor.getLong(0); // index in the EXPIRY_COLUMN array
455         }
456     }
457 
458     static final int RELEVANCE_ERROR = -1; // Legal values for relevance are positive
459 
460     // Returns the relevance of the specified row, or one of the error codes above if the
461     // row is not found or some other error
getRelevance(@onNull final SQLiteDatabase db, @NonNull final String key)462     static int getRelevance(@NonNull final SQLiteDatabase db, @NonNull final String key) {
463         final long expiry = getExpiry(db, key);
464         return expiry < 0 ? (int) expiry : RelevanceUtils.computeRelevanceForNow(expiry);
465     }
466 
467     // If the attributes are null, this will only write the expiry.
468     // Returns an int out of Status.{SUCCESS, ERROR_*}
storeNetworkAttributes(@onNull final SQLiteDatabase db, @NonNull final String key, final long expiry, @Nullable final NetworkAttributes attributes)469     static int storeNetworkAttributes(@NonNull final SQLiteDatabase db, @NonNull final String key,
470             final long expiry, @Nullable final NetworkAttributes attributes) {
471         final ContentValues cv = toContentValues(key, attributes, expiry);
472         db.beginTransaction();
473         try {
474             try {
475                 // Unfortunately SQLite does not have any way to do INSERT OR UPDATE. Options are
476                 // to either insert with on conflict ignore then update (like done here), or to
477                 // construct a custom SQL INSERT statement with nested select.
478                 final long resultId = db.insertWithOnConflict(NetworkAttributesContract.TABLENAME,
479                         null, cv, SQLiteDatabase.CONFLICT_IGNORE);
480                 if (resultId < 0) {
481                     db.update(NetworkAttributesContract.TABLENAME,
482                             cv, SELECT_L2KEY, new String[]{key});
483                 }
484                 db.setTransactionSuccessful();
485                 return Status.SUCCESS;
486             } finally {
487                 db.endTransaction();
488             }
489         } catch (SQLiteException e) {
490             // No space left on disk or something
491             Log.e(TAG, "Could not write to the memory store", e);
492         }
493         return Status.ERROR_STORAGE;
494     }
495 
496     // 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)497     static int storeBlob(@NonNull final SQLiteDatabase db, @NonNull final String key,
498             @NonNull final String clientId, @NonNull final String name,
499             @NonNull final byte[] data) {
500         final long res = db.insertWithOnConflict(PrivateDataContract.TABLENAME, null,
501                 toContentValues(key, clientId, name, data), SQLiteDatabase.CONFLICT_REPLACE);
502         return (res == -1) ? Status.ERROR_STORAGE : Status.SUCCESS;
503     }
504 
505     @Nullable
retrieveNetworkAttributes(@onNull final SQLiteDatabase db, @NonNull final String key)506     static NetworkAttributes retrieveNetworkAttributes(@NonNull final SQLiteDatabase db,
507             @NonNull final String key) {
508         try (Cursor cursor = db.query(NetworkAttributesContract.TABLENAME,
509                 null, // columns, null means everything
510                 NetworkAttributesContract.COLNAME_L2KEY + " = ?", // selection
511                 new String[] { key }, // selectionArgs
512                 null, // groupBy
513                 null, // having
514                 null)) { // orderBy
515             // L2KEY is the primary key ; it should not be possible to get more than one
516             // result here. 0 results means the key was not found.
517             if (cursor.getCount() != 1) return null;
518             cursor.moveToFirst();
519             return readNetworkAttributesLine(cursor);
520         }
521     }
522 
523     private static final String[] DATA_COLUMN = new String[] {
524             PrivateDataContract.COLNAME_DATA
525     };
526 
527     @Nullable
retrieveBlob(@onNull final SQLiteDatabase db, @NonNull final String key, @NonNull final String clientId, @NonNull final String name)528     static byte[] retrieveBlob(@NonNull final SQLiteDatabase db, @NonNull final String key,
529             @NonNull final String clientId, @NonNull final String name) {
530         try (Cursor cursor = db.query(PrivateDataContract.TABLENAME,
531                 DATA_COLUMN, // columns
532                 PrivateDataContract.COLNAME_L2KEY + " = ? AND " // selection
533                 + PrivateDataContract.COLNAME_CLIENT + " = ? AND "
534                 + PrivateDataContract.COLNAME_DATANAME + " = ?",
535                 new String[] { key, clientId, name }, // selectionArgs
536                 null, // groupBy
537                 null, // having
538                 null)) { // orderBy
539             // The query above is querying by (composite) primary key, so it should not be possible
540             // to get more than one result here. 0 results means the key was not found.
541             if (cursor.getCount() != 1) return null;
542             cursor.moveToFirst();
543             return cursor.getBlob(0); // index in the DATA_COLUMN array
544         }
545     }
546 
547     /**
548      * Wipe all data in tables when network factory reset occurs.
549      */
wipeDataUponNetworkReset(@onNull final SQLiteDatabase db)550     static void wipeDataUponNetworkReset(@NonNull final SQLiteDatabase db) {
551         for (int remainingRetries = 3; remainingRetries > 0; --remainingRetries) {
552             db.beginTransaction();
553             try {
554                 try {
555                     db.delete(NetworkAttributesContract.TABLENAME, null, null);
556                     db.delete(PrivateDataContract.TABLENAME, null, null);
557                     db.delete(NetworkEventsContract.TABLENAME, null, null);
558                     try (Cursor cursorNetworkAttributes = db.query(
559                             // table name
560                             NetworkAttributesContract.TABLENAME,
561                             // column name
562                             new String[] { NetworkAttributesContract.COLNAME_L2KEY },
563                             null, // selection
564                             null, // selectionArgs
565                             null, // groupBy
566                             null, // having
567                             null, // orderBy
568                             "1")) { // limit
569                         if (0 != cursorNetworkAttributes.getCount()) continue;
570                     }
571                     try (Cursor cursorPrivateData = db.query(
572                             // table name
573                             PrivateDataContract.TABLENAME,
574                             // column name
575                             new String[] { PrivateDataContract.COLNAME_L2KEY },
576                             null, // selection
577                             null, // selectionArgs
578                             null, // groupBy
579                             null, // having
580                             null, // orderBy
581                             "1")) { // limit
582                         if (0 != cursorPrivateData.getCount()) continue;
583                     }
584                     try (Cursor cursorNetworkEvents = db.query(
585                             // table name
586                             NetworkEventsContract.TABLENAME,
587                             // column name
588                             new String[] { NetworkEventsContract.COLNAME_CLUSTER },
589                             null, // selection
590                             null, // selectionArgs
591                             null, // groupBy
592                             null, // having
593                             null, // orderBy
594                             "1")) { // limit
595                         if (0 != cursorNetworkEvents.getCount()) continue;
596                     }
597                     db.setTransactionSuccessful();
598                 } finally {
599                     db.endTransaction();
600                 }
601             } catch (SQLiteException e) {
602                 Log.e(TAG, "Could not wipe the data in database", e);
603             }
604         }
605     }
606 
607     /**
608      * The following is a horrible hack that is necessary because the Android SQLite API does not
609      * have a way to query a binary blob. This, almost certainly, is an overlook.
610      *
611      * The Android SQLite API has two family of methods : one for query that returns data, and
612      * one for more general SQL statements that can execute any statement but may not return
613      * anything. All the query methods, however, take only String[] for the arguments.
614      *
615      * In principle it is simple to write a function that will encode the binary blob in the
616      * way SQLite expects it. However, because the API forces the argument to be coerced into a
617      * String, the SQLiteQuery object generated by the default query methods will bind all
618      * arguments as Strings and SQL will *sanitize* them. This works okay for numeric types,
619      * but the format for blobs is x'<hex string>'. Note the presence of quotes, which will
620      * be sanitized, changing the contents of the field, and the query will fail to match the
621      * blob.
622      *
623      * As far as I can tell, there are two possible ways around this problem. The first one
624      * is to put the data in the query string and eschew it being an argument. This would
625      * require doing the sanitizing by hand. The other is to call bindBlob directly on the
626      * generated SQLiteQuery object, which not only is a lot less dangerous than rolling out
627      * sanitizing, but also will do the right thing if the underlying format ever changes.
628      *
629      * But none of the methods that take an SQLiteQuery object can return data ; this *must*
630      * be called with SQLiteDatabase#query. This object is not accessible from outside.
631      * However, there is a #query version that accepts a CursorFactory and this is pretty
632      * straightforward to implement as all the arguments are coming in and the SQLiteCursor
633      * class is public API.
634      * With this, it's possible to intercept the SQLiteQuery object, and assuming the args
635      * are available, to bind them directly and work around the API's oblivious coercion into
636      * Strings.
637      *
638      * This is really sad, but I don't see another way of having this work than this or the
639      * hand-rolled sanitizing, and this is the lesser evil.
640      */
641     private static class CustomCursorFactory implements SQLiteDatabase.CursorFactory {
642         @NonNull
643         private final ArrayList<Object> mArgs;
CustomCursorFactory(@onNull final ArrayList<Object> args)644         CustomCursorFactory(@NonNull final ArrayList<Object> args) {
645             mArgs = args;
646         }
647         @Override
newCursor(final SQLiteDatabase db, final SQLiteCursorDriver masterQuery, final String editTable, final SQLiteQuery query)648         public Cursor newCursor(final SQLiteDatabase db, final SQLiteCursorDriver masterQuery,
649                 final String editTable,
650                 final SQLiteQuery query) {
651             int index = 1; // bind is 1-indexed
652             for (final Object arg : mArgs) {
653                 if (arg instanceof String) {
654                     query.bindString(index++, (String) arg);
655                 } else if (arg instanceof Long) {
656                     query.bindLong(index++, (Long) arg);
657                 } else if (arg instanceof Integer) {
658                     query.bindLong(index++, Long.valueOf((Integer) arg));
659                 } else if (arg instanceof byte[]) {
660                     query.bindBlob(index++, (byte[]) arg);
661                 } else {
662                     throw new IllegalStateException("Unsupported type CustomCursorFactory "
663                             + arg.getClass().toString());
664                 }
665             }
666             return new SQLiteCursor(masterQuery, editTable, query);
667         }
668     }
669 
670     // Returns the l2key of the closest match, if and only if it matches
671     // closely enough (as determined by group-closeness).
672     @Nullable
findClosestAttributes(@onNull final SQLiteDatabase db, @NonNull final NetworkAttributes attr)673     static String findClosestAttributes(@NonNull final SQLiteDatabase db,
674             @NonNull final NetworkAttributes attr) {
675         if (attr.isEmpty()) return null;
676         final ContentValues values = toContentValues(attr);
677 
678         // Build the selection and args. To cut down on the number of lines to search, limit
679         // the search to those with at least one argument equals to the requested attributes.
680         // This works only because null attributes match only will not result in group-closeness.
681         final StringJoiner sj = new StringJoiner(" OR ");
682         final ArrayList<Object> args = new ArrayList<>();
683         args.add(System.currentTimeMillis());
684         for (final String field : values.keySet()) {
685             sj.add(field + " = ?");
686             args.add(values.get(field));
687         }
688 
689         final String selection = NetworkAttributesContract.COLNAME_EXPIRYDATE + " > ? AND ("
690                 + sj.toString() + ")";
691         try (Cursor cursor = db.queryWithFactory(new CustomCursorFactory(args),
692                 false, // distinct
693                 NetworkAttributesContract.TABLENAME,
694                 null, // columns, null means everything
695                 selection, // selection
696                 null, // selectionArgs, horrendously passed to the cursor factory instead
697                 null, // groupBy
698                 null, // having
699                 null, // orderBy
700                 null)) { // limit
701             if (cursor.getCount() <= 0) return null;
702             cursor.moveToFirst();
703             String bestKey = null;
704             float bestMatchConfidence =
705                     GROUPCLOSE_CONFIDENCE; // Never return a match worse than this.
706             while (!cursor.isAfterLast()) {
707                 final NetworkAttributes read = readNetworkAttributesLine(cursor);
708                 final float confidence = read.getNetworkGroupSamenessConfidence(attr);
709                 if (confidence > bestMatchConfidence) {
710                     bestKey = getString(cursor, NetworkAttributesContract.COLNAME_L2KEY);
711                     bestMatchConfidence = confidence;
712                 }
713                 cursor.moveToNext();
714             }
715             return bestKey;
716         }
717     }
718 
719     /**
720      * Delete a single entry by key.
721      *
722      * The NetworkAttributes table is indexed by a L2 key although it also has a cluster column,
723      * so this API only targets the NetworkAttributes table. For deleting the entries by cluster,
724      * see {@link deleteCluster}.
725      *
726      * If |needWipe| is true, the data will be wiped from disk immediately. Otherwise, it will
727      * only be marked deleted, and overwritten by subsequent writes or reclaimed during the next
728      * maintenance window.
729      * Note that wiping data is a very expensive operation. This is meant for clients that need
730      * this data gone from disk immediately for security reasons. Functionally it makes no
731      * difference at all.
732      */
delete(@onNull final SQLiteDatabase db, @NonNull final String l2key, final boolean needWipe)733     static StatusAndCount delete(@NonNull final SQLiteDatabase db, @NonNull final String l2key,
734             final boolean needWipe) {
735         return deleteEntriesWithColumn(db,
736                 NetworkAttributesContract.TABLENAME,     // table
737                 NetworkAttributesContract.COLNAME_L2KEY, // column
738                 l2key,                                   // value
739                 needWipe);
740     }
741 
742     /**
743      * Delete all entries that have a particular cluster value in NetworkAttributes and
744      * NetworkEvents tables.
745      *
746      * So far the cluster column exists in both the NetworkAttributes and NetworkEvents
747      * tables, and this API is only called when WiFi attempts to remove a network, see
748      * {@link WifiHealthMonitor.OnNetworkUpdateListener#onNetworkRemoved} and
749      * {@link WifiNetworkSuggestionManager#remove}. It makes more sense to delete the
750      * cluster column from both tables when this API is called.
751      *
752      * If |needWipe| is true, the data will be wiped from disk immediately. Otherwise, it will
753      * only be marked deleted, and overwritten by subsequent writes or reclaimed during the next
754      * maintenance window.
755      * Note that wiping data is a very expensive operation. This is meant for clients that need
756      * this data gone from disk immediately for security reasons. Functionally it makes no
757      * difference at all.
758      */
deleteCluster(@onNull final SQLiteDatabase db, @NonNull final String cluster, final boolean needWipe)759     static StatusAndCount deleteCluster(@NonNull final SQLiteDatabase db,
760             @NonNull final String cluster, final boolean needWipe) {
761         // Delete all entries that have cluster value from NetworkAttributes table.
762         final StatusAndCount naDeleteResult = deleteEntriesWithColumn(db,
763                 NetworkAttributesContract.TABLENAME,       // table
764                 NetworkAttributesContract.COLNAME_CLUSTER, // column
765                 cluster,                                   // value
766                 needWipe);
767         // And then delete all entries that have cluster value from NetworkEvents table.
768         final StatusAndCount neDeleteResult = deleteEntriesWithColumn(db,
769                 NetworkEventsContract.TABLENAME,           // table
770                 NetworkEventsContract.COLNAME_CLUSTER,     // column
771                 cluster,                                   // value
772                 needWipe);
773         int status = Status.ERROR_GENERIC;
774         if (naDeleteResult.status == Status.SUCCESS && neDeleteResult.status == Status.SUCCESS) {
775             status = Status.SUCCESS;
776         } else if (naDeleteResult.status != Status.SUCCESS) {
777             // If deleteCluster fails on both tables, return the status code on deleting the entries
778             // from the NetworkAttributes table, keep consistent with previous behavior.
779             status = naDeleteResult.status;
780         } else {
781             status = neDeleteResult.status;
782         }
783         return new StatusAndCount(status, naDeleteResult.count + neDeleteResult.count);
784     }
785 
786     // Delete all entries where the given column has the given value.
deleteEntriesWithColumn(@onNull final SQLiteDatabase db, @NonNull final String table, @NonNull final String column, @NonNull final String value, final boolean needWipe)787     private static StatusAndCount deleteEntriesWithColumn(@NonNull final SQLiteDatabase db,
788             @NonNull final String table, @NonNull final String column, @NonNull final String value,
789             final boolean needWipe) {
790         db.beginTransaction();
791         int deleted = 0;
792         try {
793             try {
794                 deleted = db.delete(table,
795                         column + "= ?", new String[] { value });
796                 db.setTransactionSuccessful();
797             } finally {
798                 db.endTransaction();
799             }
800         } catch (SQLiteException e) {
801             Log.e(TAG, "Could not delete from the memory store", e);
802             // Unclear what might have happened ; deleting records is not supposed to be able
803             // to fail barring a syntax error in the SQL query.
804             return new StatusAndCount(Status.ERROR_UNKNOWN, 0);
805         }
806 
807         if (needWipe) {
808             final int vacuumStatus = vacuum(db);
809             // This is a problem for the client : return the failure
810             if (Status.SUCCESS != vacuumStatus) return new StatusAndCount(vacuumStatus, deleted);
811         }
812         return new StatusAndCount(Status.SUCCESS, deleted);
813     }
814 
815     // Drops all records that are expired. Relevance has decayed to zero of these records. Returns
816     // an int out of Status.{SUCCESS, ERROR_*}
dropAllExpiredRecords(@onNull final SQLiteDatabase db)817     static int dropAllExpiredRecords(@NonNull final SQLiteDatabase db) {
818         db.beginTransaction();
819         try {
820             try {
821                 final long currentTimestamp = System.currentTimeMillis();
822                 // Deletes NetworkAttributes that have expired.
823                 db.delete(NetworkAttributesContract.TABLENAME,
824                         NetworkAttributesContract.COLNAME_EXPIRYDATE + " < ?",
825                         new String[]{Long.toString(currentTimestamp)});
826                 // Deletes NetworkEvents that have expired.
827                 db.delete(NetworkEventsContract.TABLENAME,
828                         NetworkEventsContract.COLNAME_EXPIRY + " < ?",
829                         new String[]{Long.toString(currentTimestamp)});
830                 db.setTransactionSuccessful();
831             } finally {
832                 db.endTransaction();
833             }
834         } catch (SQLiteException e) {
835             Log.e(TAG, "Could not delete data from memory store", e);
836             return Status.ERROR_STORAGE;
837         }
838 
839         // Execute vacuuming here if above operation has no exception. If above operation got
840         // exception, vacuuming can be ignored for reducing unnecessary consumption.
841         try {
842             db.execSQL("VACUUM");
843         } catch (SQLiteException e) {
844             // Do nothing.
845         }
846         return Status.SUCCESS;
847     }
848 
849     // Drops number of records that start from the lowest expiryDate. Returns an int out of
850     // Status.{SUCCESS, ERROR_*}
dropNumberOfRecords(@onNull final SQLiteDatabase db, int number)851     static int dropNumberOfRecords(@NonNull final SQLiteDatabase db, int number) {
852         if (number <= 0) {
853             return Status.ERROR_ILLEGAL_ARGUMENT;
854         }
855 
856         // Queries number of NetworkAttributes that start from the lowest expiryDate.
857         final long expiryDate;
858         try (Cursor cursor = db.query(NetworkAttributesContract.TABLENAME,
859                 new String[] {NetworkAttributesContract.COLNAME_EXPIRYDATE}, // columns
860                 null, // selection
861                 null, // selectionArgs
862                 null, // groupBy
863                 null, // having
864                 NetworkAttributesContract.COLNAME_EXPIRYDATE, // orderBy
865                 Integer.toString(number))) { // limit
866             if (cursor == null || cursor.getCount() <= 0) return Status.ERROR_GENERIC;
867             cursor.moveToLast();
868 
869             // Get the expiryDate from last record.
870             expiryDate = getLong(cursor, NetworkAttributesContract.COLNAME_EXPIRYDATE, 0);
871         }
872 
873         db.beginTransaction();
874         try {
875             try {
876                 // Deletes NetworkAttributes that expiryDate are lower than given value.
877                 db.delete(NetworkAttributesContract.TABLENAME,
878                         NetworkAttributesContract.COLNAME_EXPIRYDATE + " <= ?",
879                         new String[]{Long.toString(expiryDate)});
880                 db.setTransactionSuccessful();
881             } finally {
882                 db.endTransaction();
883             }
884         } catch (SQLiteException e) {
885             Log.e(TAG, "Could not delete data from memory store", e);
886             return Status.ERROR_STORAGE;
887         }
888 
889         // Execute vacuuming here if above operation has no exception. If above operation got
890         // exception, vacuuming can be ignored for reducing unnecessary consumption.
891         try {
892             db.execSQL("VACUUM");
893         } catch (SQLiteException e) {
894             // Do nothing.
895         }
896         return Status.SUCCESS;
897     }
898 
getTotalRecordNumber(@onNull final SQLiteDatabase db)899     static int getTotalRecordNumber(@NonNull final SQLiteDatabase db) {
900         // Query the total number of NetworkAttributes
901         try (Cursor cursor = db.query(NetworkAttributesContract.TABLENAME,
902                 new String[] {"COUNT(*)"}, // columns
903                 null, // selection
904                 null, // selectionArgs
905                 null, // groupBy
906                 null, // having
907                 null)) { // orderBy
908             cursor.moveToFirst();
909             return cursor == null ? 0 : cursor.getInt(0);
910         }
911     }
912 
storeNetworkEvent(@onNull final SQLiteDatabase db, @NonNull final String cluster, final long timestamp, final long expiry, final int eventType)913     static int storeNetworkEvent(@NonNull final SQLiteDatabase db, @NonNull final String cluster,
914             final long timestamp, final long expiry, final int eventType) {
915         final ContentValues cv = toContentValues(cluster, timestamp, expiry, eventType);
916         db.beginTransaction();
917         try {
918             try {
919                 final long resultId = db.insertOrThrow(NetworkEventsContract.TABLENAME,
920                         null /* nullColumnHack */, cv);
921                 if (resultId < 0) {
922                     // Should not fail to insert a row to NetworkEvents table which doesn't have
923                     // uniqueness constraint.
924                     return Status.ERROR_STORAGE;
925                 }
926                 db.setTransactionSuccessful();
927                 return Status.SUCCESS;
928             } finally {
929                 db.endTransaction();
930             }
931         } catch (SQLiteException e) {
932             // No space left on disk or something
933             Log.e(TAG, "Could not write to the memory store", e);
934         }
935         return Status.ERROR_STORAGE;
936     }
937 
retrieveNetworkEventCount(@onNull final SQLiteDatabase db, @NonNull final String cluster, @NonNull final long[] sinceTimes, @NonNull final int[] eventTypes)938     static int[] retrieveNetworkEventCount(@NonNull final SQLiteDatabase db,
939             @NonNull final String cluster, @NonNull final long[] sinceTimes,
940             @NonNull final int[] eventTypes) {
941         final int[] counts = new int[sinceTimes.length];
942         for (int i = 0; i < counts.length; i++) {
943             final String[] selectionArgs = new String[eventTypes.length + 2];
944             selectionArgs[0] = cluster;
945             selectionArgs[1] = String.valueOf(sinceTimes[i]);
946             for (int j = 0; j < eventTypes.length; j++) {
947                 selectionArgs[j + 2] = String.valueOf(eventTypes[j]);
948             }
949             final StringBuilder selectionBuilder =
950                     new StringBuilder(NetworkEventsContract.COLNAME_CLUSTER + " = ? " + "AND "
951                             + NetworkEventsContract.COLNAME_TIMESTAMP + " >= ? " + "AND "
952                             + NetworkEventsContract.COLNAME_EVENTTYPE + " IN (");
953             for (int k = 0; k < eventTypes.length; k++) {
954                 selectionBuilder.append("?");
955                 if (k < eventTypes.length - 1) {
956                     selectionBuilder.append(",");
957                 }
958             }
959             selectionBuilder.append(")");
960             try (Cursor cursor = db.query(
961                     NetworkEventsContract.TABLENAME,
962                     new String[] {"COUNT(*)"}, // columns
963                     selectionBuilder.toString(),
964                     selectionArgs,
965                     null, // groupBy
966                     null, // having
967                     null)) { // orderBy
968                 cursor.moveToFirst();
969                 counts[i] = cursor.getInt(0);
970             }
971         }
972         return counts;
973     }
974 
975     // Helper methods
getString(final Cursor cursor, final String columnName)976     private static String getString(final Cursor cursor, final String columnName) {
977         final int columnIndex = cursor.getColumnIndex(columnName);
978         return (columnIndex >= 0) ? cursor.getString(columnIndex) : null;
979     }
getBlob(final Cursor cursor, final String columnName)980     private static byte[] getBlob(final Cursor cursor, final String columnName) {
981         final int columnIndex = cursor.getColumnIndex(columnName);
982         return (columnIndex >= 0) ? cursor.getBlob(columnIndex) : null;
983     }
getInt(final Cursor cursor, final String columnName, final int defaultValue)984     private static int getInt(final Cursor cursor, final String columnName,
985             final int defaultValue) {
986         final int columnIndex = cursor.getColumnIndex(columnName);
987         return (columnIndex >= 0) ? cursor.getInt(columnIndex) : defaultValue;
988     }
getLong(final Cursor cursor, final String columnName, final long defaultValue)989     private static long getLong(final Cursor cursor, final String columnName,
990             final long defaultValue) {
991         final int columnIndex = cursor.getColumnIndex(columnName);
992         return (columnIndex >= 0) ? cursor.getLong(columnIndex) : defaultValue;
993     }
vacuum(@onNull final SQLiteDatabase db)994     private static int vacuum(@NonNull final SQLiteDatabase db) {
995         try {
996             db.execSQL("VACUUM");
997             return Status.SUCCESS;
998         } catch (SQLiteException e) {
999             // Vacuuming may fail from lack of storage, because it makes a copy of the database.
1000             return Status.ERROR_STORAGE;
1001         }
1002     }
1003 }
1004