/* * This source is part of the * _____ ___ ____ * __ / / _ \/ _ | / __/___ _______ _ * / // / , _/ __ |/ _/_/ _ \/ __/ _ `/ * \___/_/|_/_/ |_/_/ (_)___/_/ \_, / * /___/ * repository. * * Copyright (C) 2013-2015 Carmen Alvarez (c@rmen.ca) * Copyright (C) 2013 Benoit 'BoD' Lubek (BoD@JRAF.org) * * 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 ca.rmen.android.networkmonitor.provider; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Set; import java8.util.stream.Collectors; import android.content.ContentProvider; import android.content.ContentProviderOperation; import android.content.ContentProviderResult; import android.content.ContentValues; import android.content.Context; import android.content.OperationApplicationException; import android.content.UriMatcher; import android.content.pm.ProviderInfo; import android.database.Cursor; import android.database.sqlite.SQLiteCursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteQuery; import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.provider.BaseColumns; import android.support.annotation.NonNull; import ca.rmen.android.networkmonitor.Constants; import ca.rmen.android.networkmonitor.util.Log; import java8.util.stream.StreamSupport; public class NetMonProvider extends ContentProvider { // NO_UCD (use default) private static final String TAG = Constants.TAG + NetMonProvider.class.getSimpleName(); private static final String TYPE_CURSOR_ITEM = "vnd.android.cursor.item/"; private static final String TYPE_CURSOR_DIR = "vnd.android.cursor.dir/"; public static final String AUTHORITY = "ca.rmen.android.networkmonitor.provider"; static final String CONTENT_URI_BASE = "content://" + AUTHORITY; public static final String QUERY_PARAMETER_NOTIFY = "QUERY_PARAMETER_NOTIFY"; public static final String QUERY_PARAMETER_LIMIT = "QUERY_PARAMETER_LIMIT"; private static final String QUERY_PARAMETER_GROUP_BY = "QUERY_PARAMETER_GROUP_BY"; private static final int URI_TYPE_NETWORKMONITOR = 0; private static final int URI_TYPE_NETWORKMONITOR_ID = 1; private static final int URI_TYPE_SUMMARY = 2; private static final int URI_TYPE_UNIQUE_VALUES_ID = 3; private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); private Context mContext; static { URI_MATCHER.addURI(AUTHORITY, NetMonColumns.TABLE_NAME, URI_TYPE_NETWORKMONITOR); URI_MATCHER.addURI(AUTHORITY, NetMonColumns.TABLE_NAME + "/#", URI_TYPE_NETWORKMONITOR_ID); URI_MATCHER.addURI(AUTHORITY, ConnectionTestStatsColumns.VIEW_NAME, URI_TYPE_SUMMARY); URI_MATCHER.addURI(AUTHORITY, UniqueValuesColumns.NAME + "/*", URI_TYPE_UNIQUE_VALUES_ID); } private NetMonDatabase mNetworkMonitorDatabase; @Override public boolean onCreate() { mNetworkMonitorDatabase = new NetMonDatabase(mContext); return true; } @Override public void attachInfo(Context context, ProviderInfo info) { mContext = context; super.attachInfo(context, info); } @Override public String getType(@NonNull Uri uri) { final int match = URI_MATCHER.match(uri); switch (match) { case URI_TYPE_NETWORKMONITOR: case URI_TYPE_SUMMARY: return TYPE_CURSOR_DIR + NetMonColumns.TABLE_NAME; case URI_TYPE_NETWORKMONITOR_ID: return TYPE_CURSOR_ITEM + NetMonColumns.TABLE_NAME; case URI_TYPE_UNIQUE_VALUES_ID: return TYPE_CURSOR_DIR + UniqueValuesColumns.NAME; } return null; } @Override public Uri insert(@NonNull Uri uri, ContentValues values) { Log.d(TAG, "insert uri=" + uri + " values=" + values); final String table = uri.getLastPathSegment(); final long rowId = mNetworkMonitorDatabase.getWritableDatabase().insert(table, null, values); String notify; if (rowId != -1 && ((notify = uri.getQueryParameter(QUERY_PARAMETER_NOTIFY)) == null || "true".equals(notify))) { mContext.getContentResolver().notifyChange(uri, null); } return uri.buildUpon().appendEncodedPath(String.valueOf(rowId)).build(); } @Override public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) { Log.d(TAG, "bulkInsert uri=" + uri + " values.length=" + values.length); final String table = uri.getLastPathSegment(); final SQLiteDatabase db = mNetworkMonitorDatabase.getWritableDatabase(); int res = 0; db.beginTransaction(); try { for (final ContentValues v : values) { final long id = db.insert(table, null, v); if (id != -1) { res++; } } db.setTransactionSuccessful(); } finally { db.endTransaction(); } String notify; if (res != 0 && ((notify = uri.getQueryParameter(QUERY_PARAMETER_NOTIFY)) == null || "true".equals(notify))) { mContext.getContentResolver().notifyChange(uri, null); } return res; } @Override public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) { Log.d(TAG, "update uri=" + uri + " values=" + values + " selection=" + selection); final QueryParams queryParams = getQueryParams(uri, selection); final int res = mNetworkMonitorDatabase.getWritableDatabase().update(queryParams.table, values, queryParams.whereClause, selectionArgs); String notify; if (res != 0 && ((notify = uri.getQueryParameter(QUERY_PARAMETER_NOTIFY)) == null || "true".equals(notify))) { mContext.getContentResolver().notifyChange(uri, null); } return res; } @Override public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) { Log.d(TAG, "delete uri=" + uri + " selection=" + selection); final QueryParams queryParams = getQueryParams(uri, selection); final int res = mNetworkMonitorDatabase.getWritableDatabase().delete(queryParams.table, queryParams.whereClause, selectionArgs); String notify; if (res != 0 && ((notify = uri.getQueryParameter(QUERY_PARAMETER_NOTIFY)) == null || "true".equals(notify))) { mContext.getContentResolver().notifyChange(uri, null); } return res; } @Override public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { final String groupBy = uri.getQueryParameter(QUERY_PARAMETER_GROUP_BY); final String limit = uri.getQueryParameter(QUERY_PARAMETER_LIMIT); Log.d(TAG, "query uri=" + uri + ", projection = " + Arrays.toString(projection) + ", selection=" + selection + ", selectionArgs = " + Arrays.toString(selectionArgs) + ", sortOrder=" + sortOrder + ", groupBy=" + groupBy); final int matchedId = URI_MATCHER.match(uri); final Cursor res; switch (matchedId) { case URI_TYPE_NETWORKMONITOR: case URI_TYPE_NETWORKMONITOR_ID: final QueryParams queryParams = getQueryParams(uri, selection); res = mNetworkMonitorDatabase.getReadableDatabase().query(queryParams.table, projection, queryParams.whereClause, selectionArgs, groupBy, null, sortOrder == null ? queryParams.orderBy : sortOrder, limit); break; case URI_TYPE_SUMMARY: res = mNetworkMonitorDatabase.getReadableDatabase().query(ConnectionTestStatsColumns.VIEW_NAME, projection, selection, selectionArgs, groupBy, null, sortOrder, limit); break; case URI_TYPE_UNIQUE_VALUES_ID: String columnName = uri.getLastPathSegment(); Map<String, String> projectionMap = new HashMap<>(); projectionMap.put(UniqueValuesColumns.VALUE, columnName); projectionMap.put(UniqueValuesColumns.COUNT, "count(*)"); SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); qb.setDistinct(true); qb.setTables(NetMonColumns.TABLE_NAME); qb.setProjectionMap(projectionMap); res = qb.query(mNetworkMonitorDatabase.getReadableDatabase(), projection, selection, selectionArgs, columnName, null, sortOrder, limit); break; default: return null; } res.setNotificationUri(mContext.getContentResolver(), uri); logCursor(res, selectionArgs); return res; } /** * Perform all operations in a single transaction and notify all relevant URIs at the end. * * @see android.content.ContentProvider#applyBatch(java.util.ArrayList) */ @Override public @NonNull ContentProviderResult[] applyBatch(@NonNull ArrayList<ContentProviderOperation> operations) throws OperationApplicationException { Log.v(TAG, "applyBatch: " + operations.size()); Set<Uri> urisToNotify = StreamSupport.stream(operations) .map(ContentProviderOperation::getUri) .collect(Collectors.toSet()); Log.v(TAG, "applyBatch: will notify these uris after persisting: " + urisToNotify); SQLiteDatabase db = mNetworkMonitorDatabase.getWritableDatabase(); db.beginTransaction(); try { int batchSize = 100; int operationsProcessed = 0; ContentProviderResult[] result = new ContentProviderResult[operations.size()]; while (!operations.isEmpty()) { ArrayList<ContentProviderOperation> batch = new ArrayList<>(batchSize); for (int i = 0; i < batchSize && !operations.isEmpty(); i++) batch.add(operations.remove(0)); Log.v(TAG, "applyBatch of " + batch.size() + " operations"); ContentProviderResult[] batchResult = super.applyBatch(batch); System.arraycopy(batchResult, 0, result, operationsProcessed, batchResult.length); operationsProcessed += batch.size(); } db.setTransactionSuccessful(); for (Uri uri : urisToNotify) mContext.getContentResolver().notifyChange(uri, null); return result; } finally { db.endTransaction(); } } private static class QueryParams { public String table; public String whereClause; public String orderBy; } private QueryParams getQueryParams(Uri uri, String selection) { final QueryParams res = new QueryParams(); String id = null; final int matchedId = URI_MATCHER.match(uri); switch (matchedId) { case URI_TYPE_NETWORKMONITOR: case URI_TYPE_NETWORKMONITOR_ID: res.table = NetMonColumns.TABLE_NAME; res.orderBy = NetMonColumns.DEFAULT_ORDER; break; case URI_TYPE_SUMMARY: // Nothing to do here. We will construct our query params in query(). break; default: throw new IllegalArgumentException("The uri '" + uri + "' is not supported by this ContentProvider"); } switch (matchedId) { case URI_TYPE_NETWORKMONITOR_ID: id = uri.getLastPathSegment(); } if (id != null) { if (selection != null) { res.whereClause = BaseColumns._ID + "=" + id + " and (" + selection + ")"; } else { res.whereClause = BaseColumns._ID + "=" + id; } } else { res.whereClause = selection; } return res; } /** * Log the query of the given cursor. */ private void logCursor(Cursor cursor, String[] selectionArgs) { try { Field queryField = SQLiteCursor.class.getDeclaredField("mQuery"); queryField.setAccessible(true); SQLiteQuery sqliteQuery = (SQLiteQuery) queryField.get(cursor); Log.v(TAG, sqliteQuery.toString() + ": " + Arrays.toString(selectionArgs)); } catch (Exception e) { Log.v(TAG, e.getMessage(), e); } } }