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