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