blob: 4896ef2acac64d2bd16f023b5b293a82574f0eaf [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.connectivity.ipmemorystore;
import static android.net.shared.Inet4AddressUtils.inet4AddressToIntHTH;
import static android.net.shared.Inet4AddressUtils.intToInet4AddressHTH;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteCursor;
import android.database.sqlite.SQLiteCursorDriver;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQuery;
import android.net.ipmemorystore.NetworkAttributes;
import android.net.ipmemorystore.Status;
import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.StringJoiner;
/**
* Encapsulating class for using the SQLite database backing the memory store.
*
* This class groups together the contracts and the SQLite helper used to
* use the database.
*
* @hide
*/
public class IpMemoryStoreDatabase {
private static final String TAG = IpMemoryStoreDatabase.class.getSimpleName();
// A pair of NetworkAttributes objects is group-close if the confidence that they are
// the same is above this cutoff. See NetworkAttributes and SameL3NetworkResponse.
private static final float GROUPCLOSE_CONFIDENCE = 0.5f;
/**
* Contract class for the Network Attributes table.
*/
public static class NetworkAttributesContract {
public static final String TABLENAME = "NetworkAttributes";
public static final String COLNAME_L2KEY = "l2Key";
public static final String COLTYPE_L2KEY = "TEXT NOT NULL";
public static final String COLNAME_EXPIRYDATE = "expiryDate";
// Milliseconds since the Epoch, in true Java style
public static final String COLTYPE_EXPIRYDATE = "BIGINT";
public static final String COLNAME_ASSIGNEDV4ADDRESS = "assignedV4Address";
public static final String COLTYPE_ASSIGNEDV4ADDRESS = "INTEGER";
public static final String COLNAME_ASSIGNEDV4ADDRESSEXPIRY = "assignedV4AddressExpiry";
// The lease expiry timestamp in uint of milliseconds since the Epoch. Long.MAX_VALUE
// is used to represent "infinite lease".
public static final String COLTYPE_ASSIGNEDV4ADDRESSEXPIRY = "BIGINT";
// Please note that the group hint is only a *hint*, hence its name. The client can offer
// this information to nudge the grouping in the decision it thinks is right, but it can't
// decide for the memory store what is the same L3 network.
public static final String COLNAME_GROUPHINT = "groupHint";
public static final String COLTYPE_GROUPHINT = "TEXT";
public static final String COLNAME_DNSADDRESSES = "dnsAddresses";
// Stored in marshalled form as is
public static final String COLTYPE_DNSADDRESSES = "BLOB";
public static final String COLNAME_MTU = "mtu";
public static final String COLTYPE_MTU = "INTEGER DEFAULT -1";
public static final String CREATE_TABLE = "CREATE TABLE IF NOT EXISTS "
+ TABLENAME + " ("
+ COLNAME_L2KEY + " " + COLTYPE_L2KEY + " PRIMARY KEY NOT NULL, "
+ COLNAME_EXPIRYDATE + " " + COLTYPE_EXPIRYDATE + ", "
+ COLNAME_ASSIGNEDV4ADDRESS + " " + COLTYPE_ASSIGNEDV4ADDRESS + ", "
+ COLNAME_ASSIGNEDV4ADDRESSEXPIRY + " " + COLTYPE_ASSIGNEDV4ADDRESSEXPIRY + ", "
+ COLNAME_GROUPHINT + " " + COLTYPE_GROUPHINT + ", "
+ COLNAME_DNSADDRESSES + " " + COLTYPE_DNSADDRESSES + ", "
+ COLNAME_MTU + " " + COLTYPE_MTU + ")";
public static final String DROP_TABLE = "DROP TABLE IF EXISTS " + TABLENAME;
}
/**
* Contract class for the Private Data table.
*/
public static class PrivateDataContract {
public static final String TABLENAME = "PrivateData";
public static final String COLNAME_L2KEY = "l2Key";
public static final String COLTYPE_L2KEY = "TEXT NOT NULL";
public static final String COLNAME_CLIENT = "client";
public static final String COLTYPE_CLIENT = "TEXT NOT NULL";
public static final String COLNAME_DATANAME = "dataName";
public static final String COLTYPE_DATANAME = "TEXT NOT NULL";
public static final String COLNAME_DATA = "data";
public static final String COLTYPE_DATA = "BLOB NOT NULL";
public static final String CREATE_TABLE = "CREATE TABLE IF NOT EXISTS "
+ TABLENAME + " ("
+ COLNAME_L2KEY + " " + COLTYPE_L2KEY + ", "
+ COLNAME_CLIENT + " " + COLTYPE_CLIENT + ", "
+ COLNAME_DATANAME + " " + COLTYPE_DATANAME + ", "
+ COLNAME_DATA + " " + COLTYPE_DATA + ", "
+ "PRIMARY KEY ("
+ COLNAME_L2KEY + ", "
+ COLNAME_CLIENT + ", "
+ COLNAME_DATANAME + "))";
public static final String DROP_TABLE = "DROP TABLE IF EXISTS " + TABLENAME;
}
// To save memory when the DB is not used, close it after 30s of inactivity. This is
// determined manually based on what feels right.
private static final long IDLE_CONNECTION_TIMEOUT_MS = 30_000;
/** The SQLite DB helper */
public static class DbHelper extends SQLiteOpenHelper {
// Update this whenever changing the schema.
private static final int SCHEMA_VERSION = 4;
private static final String DATABASE_FILENAME = "IpMemoryStore.db";
private static final String TRIGGER_NAME = "delete_cascade_to_private";
public DbHelper(@NonNull final Context context) {
super(context, DATABASE_FILENAME, null, SCHEMA_VERSION);
setIdleConnectionTimeout(IDLE_CONNECTION_TIMEOUT_MS);
}
/** Called when the database is created */
@Override
public void onCreate(@NonNull final SQLiteDatabase db) {
db.execSQL(NetworkAttributesContract.CREATE_TABLE);
db.execSQL(PrivateDataContract.CREATE_TABLE);
createTrigger(db);
}
/** Called when the database is upgraded */
@Override
public void onUpgrade(@NonNull final SQLiteDatabase db, final int oldVersion,
final int newVersion) {
try {
if (oldVersion < 2) {
// upgrade from version 1 to version 2
// since we starts from version 2, do nothing here
}
if (oldVersion < 3) {
// upgrade from version 2 to version 3
final String sqlUpgradeAddressExpiry = "alter table"
+ " " + NetworkAttributesContract.TABLENAME + " ADD"
+ " " + NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESSEXPIRY
+ " " + NetworkAttributesContract.COLTYPE_ASSIGNEDV4ADDRESSEXPIRY;
db.execSQL(sqlUpgradeAddressExpiry);
}
if (oldVersion < 4) {
createTrigger(db);
}
} catch (SQLiteException e) {
Log.e(TAG, "Could not upgrade to the new version", e);
// create database with new version
db.execSQL(NetworkAttributesContract.DROP_TABLE);
db.execSQL(PrivateDataContract.DROP_TABLE);
onCreate(db);
}
}
/** Called when the database is downgraded */
@Override
public void onDowngrade(@NonNull final SQLiteDatabase db, final int oldVersion,
final int newVersion) {
// Downgrades always nuke all data and recreate an empty table.
db.execSQL(NetworkAttributesContract.DROP_TABLE);
db.execSQL(PrivateDataContract.DROP_TABLE);
db.execSQL("DROP TRIGGER " + TRIGGER_NAME);
onCreate(db);
}
private void createTrigger(@NonNull final SQLiteDatabase db) {
final String createTrigger = "CREATE TRIGGER " + TRIGGER_NAME
+ " DELETE ON " + NetworkAttributesContract.TABLENAME
+ " BEGIN"
+ " DELETE FROM " + PrivateDataContract.TABLENAME + " WHERE OLD."
+ NetworkAttributesContract.COLNAME_L2KEY
+ "=" + PrivateDataContract.COLNAME_L2KEY
+ "; END;";
db.execSQL(createTrigger);
}
}
@NonNull
private static byte[] encodeAddressList(@NonNull final List<InetAddress> addresses) {
final ByteArrayOutputStream os = new ByteArrayOutputStream();
for (final InetAddress address : addresses) {
final byte[] b = address.getAddress();
os.write(b.length);
os.write(b, 0, b.length);
}
return os.toByteArray();
}
@NonNull
private static ArrayList<InetAddress> decodeAddressList(@NonNull final byte[] encoded) {
final ByteArrayInputStream is = new ByteArrayInputStream(encoded);
final ArrayList<InetAddress> addresses = new ArrayList<>();
int d = -1;
while ((d = is.read()) != -1) {
final byte[] bytes = new byte[d];
is.read(bytes, 0, d);
try {
addresses.add(InetAddress.getByAddress(bytes));
} catch (UnknownHostException e) { /* Hopefully impossible */ }
}
return addresses;
}
@NonNull
private static ContentValues toContentValues(@Nullable final NetworkAttributes attributes) {
final ContentValues values = new ContentValues();
if (null == attributes) return values;
if (null != attributes.assignedV4Address) {
values.put(NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESS,
inet4AddressToIntHTH(attributes.assignedV4Address));
}
if (null != attributes.assignedV4AddressExpiry) {
values.put(NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESSEXPIRY,
attributes.assignedV4AddressExpiry);
}
if (null != attributes.groupHint) {
values.put(NetworkAttributesContract.COLNAME_GROUPHINT, attributes.groupHint);
}
if (null != attributes.dnsAddresses) {
values.put(NetworkAttributesContract.COLNAME_DNSADDRESSES,
encodeAddressList(attributes.dnsAddresses));
}
if (null != attributes.mtu) {
values.put(NetworkAttributesContract.COLNAME_MTU, attributes.mtu);
}
return values;
}
// Convert a NetworkAttributes object to content values to store them in a table compliant
// with the contract defined in NetworkAttributesContract.
@NonNull
private static ContentValues toContentValues(@NonNull final String key,
@Nullable final NetworkAttributes attributes, final long expiry) {
final ContentValues values = toContentValues(attributes);
values.put(NetworkAttributesContract.COLNAME_L2KEY, key);
values.put(NetworkAttributesContract.COLNAME_EXPIRYDATE, expiry);
return values;
}
// Convert a byte array into content values to store it in a table compliant with the
// contract defined in PrivateDataContract.
@NonNull
private static ContentValues toContentValues(@NonNull final String key,
@NonNull final String clientId, @NonNull final String name,
@NonNull final byte[] data) {
final ContentValues values = new ContentValues();
values.put(PrivateDataContract.COLNAME_L2KEY, key);
values.put(PrivateDataContract.COLNAME_CLIENT, clientId);
values.put(PrivateDataContract.COLNAME_DATANAME, name);
values.put(PrivateDataContract.COLNAME_DATA, data);
return values;
}
@Nullable
private static NetworkAttributes readNetworkAttributesLine(@NonNull final Cursor cursor) {
// Make sure the data hasn't expired
final long expiry = getLong(cursor, NetworkAttributesContract.COLNAME_EXPIRYDATE, -1L);
if (expiry < System.currentTimeMillis()) return null;
final NetworkAttributes.Builder builder = new NetworkAttributes.Builder();
final int assignedV4AddressInt = getInt(cursor,
NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESS, 0);
final long assignedV4AddressExpiry = getLong(cursor,
NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESSEXPIRY, 0);
final String groupHint = getString(cursor, NetworkAttributesContract.COLNAME_GROUPHINT);
final byte[] dnsAddressesBlob =
getBlob(cursor, NetworkAttributesContract.COLNAME_DNSADDRESSES);
final int mtu = getInt(cursor, NetworkAttributesContract.COLNAME_MTU, -1);
if (0 != assignedV4AddressInt) {
builder.setAssignedV4Address(intToInet4AddressHTH(assignedV4AddressInt));
}
if (0 != assignedV4AddressExpiry) {
builder.setAssignedV4AddressExpiry(assignedV4AddressExpiry);
}
builder.setGroupHint(groupHint);
if (null != dnsAddressesBlob) {
builder.setDnsAddresses(decodeAddressList(dnsAddressesBlob));
}
if (mtu >= 0) {
builder.setMtu(mtu);
}
return builder.build();
}
private static final String[] EXPIRY_COLUMN = new String[] {
NetworkAttributesContract.COLNAME_EXPIRYDATE
};
static final int EXPIRY_ERROR = -1; // Legal values for expiry are positive
static final String SELECT_L2KEY = NetworkAttributesContract.COLNAME_L2KEY + " = ?";
// Returns the expiry date of the specified row, or one of the error codes above if the
// row is not found or some other error
static long getExpiry(@NonNull final SQLiteDatabase db, @NonNull final String key) {
final Cursor cursor = db.query(NetworkAttributesContract.TABLENAME,
EXPIRY_COLUMN, // columns
SELECT_L2KEY, // selection
new String[] { key }, // selectionArgs
null, // groupBy
null, // having
null // orderBy
);
// L2KEY is the primary key ; it should not be possible to get more than one
// result here. 0 results means the key was not found.
if (cursor.getCount() != 1) return EXPIRY_ERROR;
cursor.moveToFirst();
final long result = cursor.getLong(0); // index in the EXPIRY_COLUMN array
cursor.close();
return result;
}
static final int RELEVANCE_ERROR = -1; // Legal values for relevance are positive
// Returns the relevance of the specified row, or one of the error codes above if the
// row is not found or some other error
static int getRelevance(@NonNull final SQLiteDatabase db, @NonNull final String key) {
final long expiry = getExpiry(db, key);
return expiry < 0 ? (int) expiry : RelevanceUtils.computeRelevanceForNow(expiry);
}
// If the attributes are null, this will only write the expiry.
// Returns an int out of Status.{SUCCESS, ERROR_*}
static int storeNetworkAttributes(@NonNull final SQLiteDatabase db, @NonNull final String key,
final long expiry, @Nullable final NetworkAttributes attributes) {
final ContentValues cv = toContentValues(key, attributes, expiry);
db.beginTransaction();
try {
// Unfortunately SQLite does not have any way to do INSERT OR UPDATE. Options are
// to either insert with on conflict ignore then update (like done here), or to
// construct a custom SQL INSERT statement with nested select.
final long resultId = db.insertWithOnConflict(NetworkAttributesContract.TABLENAME,
null, cv, SQLiteDatabase.CONFLICT_IGNORE);
if (resultId < 0) {
db.update(NetworkAttributesContract.TABLENAME, cv, SELECT_L2KEY, new String[]{key});
}
db.setTransactionSuccessful();
return Status.SUCCESS;
} catch (SQLiteException e) {
// No space left on disk or something
Log.e(TAG, "Could not write to the memory store", e);
} finally {
db.endTransaction();
}
return Status.ERROR_STORAGE;
}
// Returns an int out of Status.{SUCCESS, ERROR_*}
static int storeBlob(@NonNull final SQLiteDatabase db, @NonNull final String key,
@NonNull final String clientId, @NonNull final String name,
@NonNull final byte[] data) {
final long res = db.insertWithOnConflict(PrivateDataContract.TABLENAME, null,
toContentValues(key, clientId, name, data), SQLiteDatabase.CONFLICT_REPLACE);
return (res == -1) ? Status.ERROR_STORAGE : Status.SUCCESS;
}
@Nullable
static NetworkAttributes retrieveNetworkAttributes(@NonNull final SQLiteDatabase db,
@NonNull final String key) {
final Cursor cursor = db.query(NetworkAttributesContract.TABLENAME,
null, // columns, null means everything
NetworkAttributesContract.COLNAME_L2KEY + " = ?", // selection
new String[] { key }, // selectionArgs
null, // groupBy
null, // having
null); // orderBy
// L2KEY is the primary key ; it should not be possible to get more than one
// result here. 0 results means the key was not found.
if (cursor.getCount() != 1) return null;
cursor.moveToFirst();
final NetworkAttributes attributes = readNetworkAttributesLine(cursor);
cursor.close();
return attributes;
}
private static final String[] DATA_COLUMN = new String[] {
PrivateDataContract.COLNAME_DATA
};
@Nullable
static byte[] retrieveBlob(@NonNull final SQLiteDatabase db, @NonNull final String key,
@NonNull final String clientId, @NonNull final String name) {
final Cursor cursor = db.query(PrivateDataContract.TABLENAME,
DATA_COLUMN, // columns
PrivateDataContract.COLNAME_L2KEY + " = ? AND " // selection
+ PrivateDataContract.COLNAME_CLIENT + " = ? AND "
+ PrivateDataContract.COLNAME_DATANAME + " = ?",
new String[] { key, clientId, name }, // selectionArgs
null, // groupBy
null, // having
null); // orderBy
// The query above is querying by (composite) primary key, so it should not be possible to
// get more than one result here. 0 results means the key was not found.
if (cursor.getCount() != 1) return null;
cursor.moveToFirst();
final byte[] result = cursor.getBlob(0); // index in the DATA_COLUMN array
cursor.close();
return result;
}
/**
* Wipe all data in tables when network factory reset occurs.
*/
static void wipeDataUponNetworkReset(@NonNull final SQLiteDatabase db) {
for (int remainingRetries = 3; remainingRetries > 0; --remainingRetries) {
db.beginTransaction();
try {
db.delete(NetworkAttributesContract.TABLENAME, null, null);
db.delete(PrivateDataContract.TABLENAME, null, null);
final Cursor cursorNetworkAttributes = db.query(
// table name
NetworkAttributesContract.TABLENAME,
// column name
new String[] { NetworkAttributesContract.COLNAME_L2KEY },
null, // selection
null, // selectionArgs
null, // groupBy
null, // having
null, // orderBy
"1"); // limit
if (0 != cursorNetworkAttributes.getCount()) {
cursorNetworkAttributes.close();
continue;
}
cursorNetworkAttributes.close();
final Cursor cursorPrivateData = db.query(
// table name
PrivateDataContract.TABLENAME,
// column name
new String[] { PrivateDataContract.COLNAME_L2KEY },
null, // selection
null, // selectionArgs
null, // groupBy
null, // having
null, // orderBy
"1"); // limit
if (0 != cursorPrivateData.getCount()) {
cursorPrivateData.close();
continue;
}
cursorPrivateData.close();
db.setTransactionSuccessful();
return;
} catch (SQLiteException e) {
Log.e(TAG, "Could not wipe the data in database", e);
} finally {
db.endTransaction();
}
}
}
/**
* The following is a horrible hack that is necessary because the Android SQLite API does not
* have a way to query a binary blob. This, almost certainly, is an overlook.
*
* The Android SQLite API has two family of methods : one for query that returns data, and
* one for more general SQL statements that can execute any statement but may not return
* anything. All the query methods, however, take only String[] for the arguments.
*
* In principle it is simple to write a function that will encode the binary blob in the
* way SQLite expects it. However, because the API forces the argument to be coerced into a
* String, the SQLiteQuery object generated by the default query methods will bind all
* arguments as Strings and SQL will *sanitize* them. This works okay for numeric types,
* but the format for blobs is x'<hex string>'. Note the presence of quotes, which will
* be sanitized, changing the contents of the field, and the query will fail to match the
* blob.
*
* As far as I can tell, there are two possible ways around this problem. The first one
* is to put the data in the query string and eschew it being an argument. This would
* require doing the sanitizing by hand. The other is to call bindBlob directly on the
* generated SQLiteQuery object, which not only is a lot less dangerous than rolling out
* sanitizing, but also will do the right thing if the underlying format ever changes.
*
* But none of the methods that take an SQLiteQuery object can return data ; this *must*
* be called with SQLiteDatabase#query. This object is not accessible from outside.
* However, there is a #query version that accepts a CursorFactory and this is pretty
* straightforward to implement as all the arguments are coming in and the SQLiteCursor
* class is public API.
* With this, it's possible to intercept the SQLiteQuery object, and assuming the args
* are available, to bind them directly and work around the API's oblivious coercion into
* Strings.
*
* This is really sad, but I don't see another way of having this work than this or the
* hand-rolled sanitizing, and this is the lesser evil.
*/
private static class CustomCursorFactory implements SQLiteDatabase.CursorFactory {
@NonNull
private final ArrayList<Object> mArgs;
CustomCursorFactory(@NonNull final ArrayList<Object> args) {
mArgs = args;
}
@Override
public Cursor newCursor(final SQLiteDatabase db, final SQLiteCursorDriver masterQuery,
final String editTable,
final SQLiteQuery query) {
int index = 1; // bind is 1-indexed
for (final Object arg : mArgs) {
if (arg instanceof String) {
query.bindString(index++, (String) arg);
} else if (arg instanceof Long) {
query.bindLong(index++, (Long) arg);
} else if (arg instanceof Integer) {
query.bindLong(index++, Long.valueOf((Integer) arg));
} else if (arg instanceof byte[]) {
query.bindBlob(index++, (byte[]) arg);
} else {
throw new IllegalStateException("Unsupported type CustomCursorFactory "
+ arg.getClass().toString());
}
}
return new SQLiteCursor(masterQuery, editTable, query);
}
}
// Returns the l2key of the closest match, if and only if it matches
// closely enough (as determined by group-closeness).
@Nullable
static String findClosestAttributes(@NonNull final SQLiteDatabase db,
@NonNull final NetworkAttributes attr) {
if (attr.isEmpty()) return null;
final ContentValues values = toContentValues(attr);
// Build the selection and args. To cut down on the number of lines to search, limit
// the search to those with at least one argument equals to the requested attributes.
// This works only because null attributes match only will not result in group-closeness.
final StringJoiner sj = new StringJoiner(" OR ");
final ArrayList<Object> args = new ArrayList<>();
args.add(System.currentTimeMillis());
for (final String field : values.keySet()) {
sj.add(field + " = ?");
args.add(values.get(field));
}
final String selection = NetworkAttributesContract.COLNAME_EXPIRYDATE + " > ? AND ("
+ sj.toString() + ")";
final Cursor cursor = db.queryWithFactory(new CustomCursorFactory(args),
false, // distinct
NetworkAttributesContract.TABLENAME,
null, // columns, null means everything
selection, // selection
null, // selectionArgs, horrendously passed to the cursor factory instead
null, // groupBy
null, // having
null, // orderBy
null); // limit
if (cursor.getCount() <= 0) return null;
cursor.moveToFirst();
String bestKey = null;
float bestMatchConfidence = GROUPCLOSE_CONFIDENCE; // Never return a match worse than this.
while (!cursor.isAfterLast()) {
final NetworkAttributes read = readNetworkAttributesLine(cursor);
final float confidence = read.getNetworkGroupSamenessConfidence(attr);
if (confidence > bestMatchConfidence) {
bestKey = getString(cursor, NetworkAttributesContract.COLNAME_L2KEY);
bestMatchConfidence = confidence;
}
cursor.moveToNext();
}
cursor.close();
return bestKey;
}
// Drops all records that are expired. Relevance has decayed to zero of these records. Returns
// an int out of Status.{SUCCESS, ERROR_*}
static int dropAllExpiredRecords(@NonNull final SQLiteDatabase db) {
db.beginTransaction();
try {
// Deletes NetworkAttributes that have expired.
db.delete(NetworkAttributesContract.TABLENAME,
NetworkAttributesContract.COLNAME_EXPIRYDATE + " < ?",
new String[]{Long.toString(System.currentTimeMillis())});
db.setTransactionSuccessful();
} catch (SQLiteException e) {
Log.e(TAG, "Could not delete data from memory store", e);
return Status.ERROR_STORAGE;
} finally {
db.endTransaction();
}
// Execute vacuuming here if above operation has no exception. If above operation got
// exception, vacuuming can be ignored for reducing unnecessary consumption.
try {
db.execSQL("VACUUM");
} catch (SQLiteException e) {
// Do nothing.
}
return Status.SUCCESS;
}
// Drops number of records that start from the lowest expiryDate. Returns an int out of
// Status.{SUCCESS, ERROR_*}
static int dropNumberOfRecords(@NonNull final SQLiteDatabase db, int number) {
if (number <= 0) {
return Status.ERROR_ILLEGAL_ARGUMENT;
}
// Queries number of NetworkAttributes that start from the lowest expiryDate.
final Cursor cursor = db.query(NetworkAttributesContract.TABLENAME,
new String[] {NetworkAttributesContract.COLNAME_EXPIRYDATE}, // columns
null, // selection
null, // selectionArgs
null, // groupBy
null, // having
NetworkAttributesContract.COLNAME_EXPIRYDATE, // orderBy
Integer.toString(number)); // limit
if (cursor == null || cursor.getCount() <= 0) return Status.ERROR_GENERIC;
cursor.moveToLast();
//Get the expiryDate from last record.
final long expiryDate = getLong(cursor, NetworkAttributesContract.COLNAME_EXPIRYDATE, 0);
cursor.close();
db.beginTransaction();
try {
// Deletes NetworkAttributes that expiryDate are lower than given value.
db.delete(NetworkAttributesContract.TABLENAME,
NetworkAttributesContract.COLNAME_EXPIRYDATE + " <= ?",
new String[]{Long.toString(expiryDate)});
db.setTransactionSuccessful();
} catch (SQLiteException e) {
Log.e(TAG, "Could not delete data from memory store", e);
return Status.ERROR_STORAGE;
} finally {
db.endTransaction();
}
// Execute vacuuming here if above operation has no exception. If above operation got
// exception, vacuuming can be ignored for reducing unnecessary consumption.
try {
db.execSQL("VACUUM");
} catch (SQLiteException e) {
// Do nothing.
}
return Status.SUCCESS;
}
static int getTotalRecordNumber(@NonNull final SQLiteDatabase db) {
// Query the total number of NetworkAttributes
final Cursor cursor = db.query(NetworkAttributesContract.TABLENAME,
new String[] {"COUNT(*)"}, // columns
null, // selection
null, // selectionArgs
null, // groupBy
null, // having
null); // orderBy
cursor.moveToFirst();
return cursor == null ? 0 : cursor.getInt(0);
}
// Helper methods
private static String getString(final Cursor cursor, final String columnName) {
final int columnIndex = cursor.getColumnIndex(columnName);
return (columnIndex >= 0) ? cursor.getString(columnIndex) : null;
}
private static byte[] getBlob(final Cursor cursor, final String columnName) {
final int columnIndex = cursor.getColumnIndex(columnName);
return (columnIndex >= 0) ? cursor.getBlob(columnIndex) : null;
}
private static int getInt(final Cursor cursor, final String columnName,
final int defaultValue) {
final int columnIndex = cursor.getColumnIndex(columnName);
return (columnIndex >= 0) ? cursor.getInt(columnIndex) : defaultValue;
}
private static long getLong(final Cursor cursor, final String columnName,
final long defaultValue) {
final int columnIndex = cursor.getColumnIndex(columnName);
return (columnIndex >= 0) ? cursor.getLong(columnIndex) : defaultValue;
}
}