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