androidkotlinipcandroid-contentprovider

Android ContentProvider returns null cursor when accessed from second app but works within provider app


Communication Between Two Android Apps Using ContentProvider Not Working Problem Statement I'm trying to share data between two separate Android applications using a ContentProvider. App A provides data via a custom ContentProvider, and I can access this data within App A. However, when I try to query the same ContentProvider from App B, the cursor is returning null. What I'm Trying to Achieve I want to access data from App A's ContentProvider in App B. Code Structure App A (Provider App) AndroidManifest.xml: xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.ContentProvider"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.ContentProvider">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <provider
            android:name=".MyContentProvider"
            android:authorities="com.example.app1.provider"
            android:exported="true"
            android:grantUriPermissions="true" />
    </application>
    
    <permission 
        android:name="com.example.app1.permission.READ_DATA" 
        android:protectionLevel="dangerous" />
</manifest>

MyContentProvider.java:

javapackage com.example.app1;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.util.Log;
import android.content.UriMatcher;

public class MyContentProvider extends ContentProvider {

    private static final int URI_CODE_SHARED_DATA = 1;
    private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);

    static {
        // Register the URI pattern with the UriMatcher
        URI_MATCHER.addURI("com.example.app1.provider", "shared_data", URI_CODE_SHARED_DATA);
    }

    @Override
    public boolean onCreate() {
        Log.d("MyContentProvider", "Provider initialized");
        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        Log.d("MyContentProvider", "Query received: " + uri);
        
        switch (URI_MATCHER.match(uri)) {
            case URI_CODE_SHARED_DATA:
                MatrixCursor matrixCursor = new MatrixCursor(new String[]{"data"});
                matrixCursor.addRow(new Object[]{"my content provider data"});
                return matrixCursor;
            default:
                return null; // No match found for the URI
        }
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        throw new UnsupportedOperationException("Insert not supported");
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        throw new UnsupportedOperationException("Update not supported");
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        throw new UnsupportedOperationException("Delete not supported");
    }

    @Override
    public String getType(Uri uri) {
        return "vnd.android.cursor.dir/vnd.com.example.app1.provider.shared_data";
    }
}

MainActivity of App A:

javapackage com.example.app1;

import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import androidx.activity.ComponentActivity;

public class MainActivity extends ComponentActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // Test accessing our own content provider
        getContentResolver().query(
            Uri.parse("content://com.example.app1.provider/shared_data"),
            null,
            null,
            null,
            null
        )?.use { cursor ->
            if (cursor.moveToFirst()) {
                int index = cursor.getColumnIndex("data");
                if (index != -1) {
                    String value = cursor.getString(index);
                    Log.d("MyContentProvider", "Provider data: " + value);
                } else {
                    Log.d("MyContentProvider", "No 'data' column found");
                }
            } else {
                Log.d("MyContentProvider", "Cursor is empty");
            }
        }
        
        // Rest of onCreate...
    }
}

App B (Consumer App) AndroidManifest.xml: xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    
    <!-- Added permission to read from App A -->
    <uses-permission android:name="com.example.app1.permission.READ_DATA"/>
    
    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.App2"
        tools:targetApi="31">
        
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.App2">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

MainActivity of App B:

javapackage com.example.app2;

import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import androidx.activity.ComponentActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

public class MainActivity extends ComponentActivity {
    private static final int REQUEST_PERMISSION_CODE = 1001;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // Check permission
        if (ContextCompat.checkSelfPermission(
                this,
                "com.example.app1.permission.READ_DATA"
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            // Request permission
            ActivityCompat.requestPermissions(
                this,
                new String[]{"com.example.app1.permission.READ_DATA"},
                REQUEST_PERMISSION_CODE
            );
        } else {
            // Permission already granted
            tryReadSharedData();
        }
        
        // Rest of onCreate...
    }
    
    @Override
    public void onRequestPermissionsResult(
        int requestCode, String[] permissions, int[] grantResults
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

        if (requestCode == REQUEST_PERMISSION_CODE) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // Permission granted
                tryReadSharedData();
            } else {
                Toast.makeText(this, "Permission denied. Cannot fetch data.", Toast.LENGTH_SHORT).show();
            }
        }
    }
    
    private void tryReadSharedData() {
        Log.i("SharedData", "Attempting to read from content provider");
        
        try {
            Uri uri = Uri.parse("content://com.example.app1.provider/shared_data");
            Log.i("SharedData", "Querying with URI: " + uri);
            
            // This is returning null
            Cursor cursor = getContentResolver().query(uri, null, null, null, null);
            
            if (cursor == null) {
                Log.e("SharedData", "Cursor is null - failed to query content provider");
                return;
            }
            
            if (cursor.moveToFirst()) {
                int dataIndex = cursor.getColumnIndex("data");
                if (dataIndex != -1) {
                    String data = cursor.getString(dataIndex);
                    Log.d("SharedData", "Successfully read data: " + data);
                }
            } else {
                Log.d("SharedData", "Cursor is empty");
            }
            cursor.close();
            
        } catch (Exception e) {
            Log.e("SharedData", "Error when reading from Content Provider", e);
        }
    }
}

The Issue When I try to query the content provider from App B, the cursor is null. However, I can successfully query the same content provider from within App A. What I've Tried

Added the necessary permission in App B's manifest Set the content provider to be exported in App A Requested runtime permissions in App B Verified the URI pattern is identical in both apps

Debug Information

Logcat in App B shows: "Cursor is null - failed to query content provider" No other exceptions are thrown Both apps are installed on the same device

Questions

Why am I getting a null cursor when querying from App B? Is there something missing in my permission setup or content provider configuration? Are there any additional steps needed to enable cross-app communication via ContentProvider?


Solution

  • To connect to a non-system ContentProvider, such as one provided by another app of yours, it appears that you need to add a <queries> element to the client app's manifest, identifying the app with the provider.

    The specific package approach should work:

    <manifest package="com.example.game">
        <queries>
            <package android:name="com.whatever.the.application.id.is.of.the.app.with.the.provider" />
        </queries>
        ...
    </manifest>