chromium/chrome/test/android/javatests/src/org/chromium/chrome/test/TestContentProvider.java

// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/** Content provider for testing content URLs. */
package org.chromium.chrome.test;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.database.AbstractCursor;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.util.Log;

import java.io.File;
import java.io.IOException;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.Map;

/**
 * Content provider for testing content:// urls.
 * Note: if you move this class, make sure you have also updated AndroidManifest.xml
 *
 * Important note about including chromium classes in this content provider:
 * TestContentProvider is part of ChromePublicTest APK. However the instrumentation tests
 * run in the process of the package under test, which is Chrome apk. Normally this is not
 * a problem, however when debug is set to true, Chromium build files enable multidex. In
 * multidex mode, the Chromium files main dex file is in Chrome apk, which are not accessible
 * from the process that runs this ContentProvider.
 *
 * One of the possible workarounds is running this ContentProvider in the same process with
 * Chrome apk (i.e the process that runs the instrumentation tests). However, this
 * requires declaring a sharedUserId between the Chrome apk and ChromePublicTestApk and
 * then setting the target process for ContentProvider for the instrumentation target package.
 *     android:process="{{manifest_package}}"
 *
 * Note that modifying the application manifest file could be problematic as Chrome has
 * side by side channels.
 *
 * The second one is moving the TestContentProvider to the ChromeTestSuport apk. This
 * seems a lot better path than above.
 */
public class TestContentProvider extends ContentProvider {
    private static final String ANDROID_DATA_FILE_PATH = "android/";
    private static final String AUTHORITY = "org.chromium.chrome.test.TestContentProvider";
    private static final String CONTENT_SCHEME = "content://";
    private static final String GET_RESOURCE_REQUEST_COUNT = "get_resource_request_count";
    private static final String RESET_RESOURCE_REQUEST_COUNTS = "reset_resource_request_counts";
    private static final String SET_DATA_PATH = "set_data_path";
    private static final String TAG = "TestContentProvider";
    private static final int EXPECTED_COLUMN_INDEX = 0;
    private Map<String, Integer> mResourceRequestCount;
    private String mDataFilePath;

    // Content providers can be accessed from multiple threads.
    private final Object mLock = new Object();

    public static String createContentUrl(String target) {
        return CONTENT_SCHEME + AUTHORITY + "/" + target;
    }

    private static Uri createRequestUri(final String target, String resource) {
        if (resource == null) {
            return Uri.parse(createContentUrl(target));
        } else {
            return Uri.parse(createContentUrl(target) + "?" + resource);
        }
    }

    public static void setDataFilePath(Context context, String resource) {
        Uri uri = createRequestUri(SET_DATA_PATH, resource);
        context.getContentResolver().query(uri, null, null, null, null);
    }

    public static int getResourceRequestCount(Context context, String resource) {
        Uri uri = createRequestUri(GET_RESOURCE_REQUEST_COUNT, resource);
        final Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
        try {
            cursor.moveToFirst();
            return cursor.getInt(EXPECTED_COLUMN_INDEX);
        } finally {
            cursor.close();
        }
    }

    public static void resetResourceRequestCounts(Context context) {
        Uri uri = createRequestUri(RESET_RESOURCE_REQUEST_COUNTS, null);
        // A null cursor is returned for this request.
        context.getContentResolver().query(uri, null, null, null, null);
    }

    @Override
    public boolean onCreate() {
        return true;
    }

    @Override
    public ParcelFileDescriptor openFile(final Uri uri, String mode) {
        String resource = uri.getLastPathSegment();
        synchronized (mLock) {
            if (mResourceRequestCount.containsKey(resource)) {
                mResourceRequestCount.put(resource, mResourceRequestCount.get(resource) + 1);
            } else {
                mResourceRequestCount.put(resource, 1);
            }
        }
        try {
            return ParcelFileDescriptor.open(
                    new File(mDataFilePath + "/" + ANDROID_DATA_FILE_PATH + resource),
                    ParcelFileDescriptor.MODE_READ_ONLY);
        } catch (IOException e) {
            Log.e(TAG, e.getMessage(), e);
        }
        return null;
    }

    @Override
    public String getType(Uri uri) {
        return URLConnection.guessContentTypeFromName(uri.getLastPathSegment());
    }

    @Override
    public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
        return 0;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        return 0;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        return null;
    }

    /** Cursor object for retrieving resource request counters. */
    private static class ProviderStateCursor extends AbstractCursor {
        private final int mResourceRequestCount;

        public ProviderStateCursor(int resourceRequestCount) {
            mResourceRequestCount = resourceRequestCount;
        }

        @Override
        public boolean isNull(int columnIndex) {
            return columnIndex != EXPECTED_COLUMN_INDEX;
        }

        @Override
        public int getCount() {
            return 1;
        }

        @Override
        public int getType(int columnIndex) {
            return columnIndex == EXPECTED_COLUMN_INDEX
                    ? Cursor.FIELD_TYPE_INTEGER
                    : Cursor.FIELD_TYPE_NULL;
        }

        private void unsupported() {
            throw new UnsupportedOperationException();
        }

        @Override
        public double getDouble(int columnIndex) {
            unsupported();
            return 0.0;
        }

        @Override
        public float getFloat(int columnIndex) {
            unsupported();
            return 0.0f;
        }

        @Override
        public int getInt(int columnIndex) {
            return columnIndex == EXPECTED_COLUMN_INDEX ? mResourceRequestCount : -1;
        }

        @Override
        public short getShort(int columnIndex) {
            unsupported();
            return 0;
        }

        @Override
        public long getLong(int columnIndex) {
            return getInt(columnIndex);
        }

        @Override
        public String getString(int columnIndex) {
            unsupported();
            return null;
        }

        @Override
        public String[] getColumnNames() {
            return new String[] {GET_RESOURCE_REQUEST_COUNT};
        }
    }

    @Override
    public Cursor query(
            Uri uri,
            String[] projection,
            String selection,
            String[] selectionArgs,
            String sortOrder) {
        synchronized (mLock) {
            String action = uri.getLastPathSegment();
            String resource = uri.getQuery();
            if (GET_RESOURCE_REQUEST_COUNT.equals(action)) {
                return new ProviderStateCursor(
                        mResourceRequestCount.containsKey(resource)
                                ? mResourceRequestCount.get(resource)
                                : 0);
            } else if (RESET_RESOURCE_REQUEST_COUNTS.equals(action)) {
                mResourceRequestCount = new HashMap<String, Integer>();
            } else if (SET_DATA_PATH.equals(action)) {
                mDataFilePath = resource;
            }
            return null;
        }
    }
}