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