Java Question How to create a Content Privider with B4A or from a JAVA Library?

DonManfred

Expert
Licensed User
Longtime User
Whattsapp does support new Stickerpacks to be imported from our Apps.
One needs to create a Content-Provider to do so (it is one of the Steps mandatory).

This one is Provided with the Exampleapp

B4X:
/*
 * Copyright (c) WhatsApp Inc. and its affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.example.samplestickerapp;

import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

public class StickerContentProvider extends ContentProvider {

    /**
     * Do not change the strings listed below, as these are used by WhatsApp. And changing these will break the interface between sticker app and WhatsApp.
     */
    public static final String STICKER_PACK_IDENTIFIER_IN_QUERY = "sticker_pack_identifier";
    public static final String STICKER_PACK_NAME_IN_QUERY = "sticker_pack_name";
    public static final String STICKER_PACK_PUBLISHER_IN_QUERY = "sticker_pack_publisher";
    public static final String STICKER_PACK_ICON_IN_QUERY = "sticker_pack_icon";
    public static final String ANDROID_APP_DOWNLOAD_LINK_IN_QUERY = "android_play_store_link";
    public static final String IOS_APP_DOWNLOAD_LINK_IN_QUERY = "ios_app_download_link";
    public static final String PUBLISHER_EMAIL = "sticker_pack_publisher_email";
    public static final String PUBLISHER_WEBSITE = "sticker_pack_publisher_website";
    public static final String PRIVACY_POLICY_WEBSITE = "sticker_pack_privacy_policy_website";
    public static final String LICENSE_AGREENMENT_WEBSITE = "sticker_pack_license_agreement_website";

    public static final String STICKER_FILE_NAME_IN_QUERY = "sticker_file_name";
    public static final String STICKER_FILE_EMOJI_IN_QUERY = "sticker_emoji";
    public static final String CONTENT_FILE_NAME = "contents.json";

    public static Uri AUTHORITY_URI = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(BuildConfig.CONTENT_PROVIDER_AUTHORITY).appendPath(StickerContentProvider.METADATA).build();

    /**
     * Do not change the values in the UriMatcher because otherwise, WhatsApp will not be able to fetch the stickers from the ContentProvider.
     */
    private static final UriMatcher MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
    static final String METADATA = "metadata";
    private static final int METADATA_CODE = 1;

    private static final int METADATA_CODE_FOR_SINGLE_PACK = 2;

    static final String STICKERS = "stickers";
    private static final int STICKERS_CODE = 3;

    static final String STICKERS_ASSET = "stickers_asset";
    private static final int STICKERS_ASSET_CODE = 4;

    private static final int STICKER_PACK_TRAY_ICON_CODE = 5;

    private List<StickerPack> stickerPackList;

    @Override
    public boolean onCreate() {
        final String authority = BuildConfig.CONTENT_PROVIDER_AUTHORITY;
        if (!authority.startsWith(Objects.requireNonNull(getContext()).getPackageName())) {
            throw new IllegalStateException("your authority (" + authority + ") for the content provider should start with your package name: " + getContext().getPackageName());
        }

        //the call to get the metadata for the sticker packs.
        MATCHER.addURI(authority, METADATA, METADATA_CODE);

        //the call to get the metadata for single sticker pack. * represent the identifier
        MATCHER.addURI(authority, METADATA + "/*", METADATA_CODE_FOR_SINGLE_PACK);

        //gets the list of stickers for a sticker pack, * respresent the identifier.
        MATCHER.addURI(authority, STICKERS + "/*", STICKERS_CODE);

        for (StickerPack stickerPack : getStickerPackList()) {
            MATCHER.addURI(authority, STICKERS_ASSET + "/" + stickerPack.identifier + "/" + stickerPack.trayImageFile, STICKER_PACK_TRAY_ICON_CODE);
            for (Sticker sticker : stickerPack.getStickers()) {
                MATCHER.addURI(authority, STICKERS_ASSET + "/" + stickerPack.identifier + "/" + sticker.imageFileName, STICKERS_ASSET_CODE);
            }
        }

        return true;
    }

    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        final int code = MATCHER.match(uri);
        if (code == METADATA_CODE) {
            return getPackForAllStickerPacks(uri);
        } else if (code == METADATA_CODE_FOR_SINGLE_PACK) {
            return getCursorForSingleStickerPack(uri);
        } else if (code == STICKERS_CODE) {
            return getStickersForAStickerPack(uri);
        } else {
            throw new IllegalArgumentException("Unknown URI: " + uri);
        }
    }

    @Nullable
    @Override
    public AssetFileDescriptor openAssetFile(@NonNull Uri uri, @NonNull String mode) {
        final int matchCode = MATCHER.match(uri);
        if (matchCode == STICKERS_ASSET_CODE || matchCode == STICKER_PACK_TRAY_ICON_CODE) {
            return getImageAsset(uri);
        }
        return null;
    }


    @Override
    public String getType(@NonNull Uri uri) {
        final int matchCode = MATCHER.match(uri);
        switch (matchCode) {
            case METADATA_CODE:
                return "vnd.android.cursor.dir/vnd." + BuildConfig.CONTENT_PROVIDER_AUTHORITY + "." + METADATA;
            case METADATA_CODE_FOR_SINGLE_PACK:
                return "vnd.android.cursor.item/vnd." + BuildConfig.CONTENT_PROVIDER_AUTHORITY + "." + METADATA;
            case STICKERS_CODE:
                return "vnd.android.cursor.dir/vnd." + BuildConfig.CONTENT_PROVIDER_AUTHORITY + "." + STICKERS;
            case STICKERS_ASSET_CODE:
                return "image/webp";
            case STICKER_PACK_TRAY_ICON_CODE:
                return "image/png";
            default:
                throw new IllegalArgumentException("Unknown URI: " + uri);
        }
    }

    private synchronized void readContentFile(@NonNull Context context) {
        try (InputStream contentsInputStream = context.getAssets().open(CONTENT_FILE_NAME)) {
            stickerPackList = ContentFileParser.parseStickerPacks(contentsInputStream);
        } catch (IOException | IllegalStateException e) {
            throw new RuntimeException(CONTENT_FILE_NAME + " file has some issues: " + e.getMessage(), e);
        }
    }

    public List<StickerPack> getStickerPackList() {
        if (stickerPackList == null) {
            readContentFile(Objects.requireNonNull(getContext()));
        }
        return stickerPackList;
    }

    private Cursor getPackForAllStickerPacks(@NonNull Uri uri) {
        return getStickerPackInfo(uri, getStickerPackList());
    }

    private Cursor getCursorForSingleStickerPack(@NonNull Uri uri) {
        final String identifier = uri.getLastPathSegment();
        for (StickerPack stickerPack : getStickerPackList()) {
            if (identifier.equals(stickerPack.identifier)) {
                return getStickerPackInfo(uri, Collections.singletonList(stickerPack));
            }
        }

        return getStickerPackInfo(uri, new ArrayList<>());
    }

    @NonNull
    private Cursor getStickerPackInfo(@NonNull Uri uri, @NonNull List<StickerPack> stickerPackList) {
        MatrixCursor cursor = new MatrixCursor(
                new String[]{
                        STICKER_PACK_IDENTIFIER_IN_QUERY,
                        STICKER_PACK_NAME_IN_QUERY,
                        STICKER_PACK_PUBLISHER_IN_QUERY,
                        STICKER_PACK_ICON_IN_QUERY,
                        ANDROID_APP_DOWNLOAD_LINK_IN_QUERY,
                        IOS_APP_DOWNLOAD_LINK_IN_QUERY,
                        PUBLISHER_EMAIL,
                        PUBLISHER_WEBSITE,
                        PRIVACY_POLICY_WEBSITE,
                        LICENSE_AGREENMENT_WEBSITE
                });
        for (StickerPack stickerPack : stickerPackList) {
            MatrixCursor.RowBuilder builder = cursor.newRow();
            builder.add(stickerPack.identifier);
            builder.add(stickerPack.name);
            builder.add(stickerPack.publisher);
            builder.add(stickerPack.trayImageFile);
            builder.add(stickerPack.androidPlayStoreLink);
            builder.add(stickerPack.iosAppStoreLink);
            builder.add(stickerPack.publisherEmail);
            builder.add(stickerPack.publisherWebsite);
            builder.add(stickerPack.privacyPolicyWebsite);
            builder.add(stickerPack.licenseAgreementWebsite);
        }
        cursor.setNotificationUri(Objects.requireNonNull(getContext()).getContentResolver(), uri);
        return cursor;
    }

    @NonNull
    private Cursor getStickersForAStickerPack(@NonNull Uri uri) {
        final String identifier = uri.getLastPathSegment();
        MatrixCursor cursor = new MatrixCursor(new String[]{STICKER_FILE_NAME_IN_QUERY, STICKER_FILE_EMOJI_IN_QUERY});
        for (StickerPack stickerPack : getStickerPackList()) {
            if (identifier.equals(stickerPack.identifier)) {
                for (Sticker sticker : stickerPack.getStickers()) {
                    cursor.addRow(new Object[]{sticker.imageFileName, TextUtils.join(",", sticker.emojis)});
                }
            }
        }
        cursor.setNotificationUri(Objects.requireNonNull(getContext()).getContentResolver(), uri);
        return cursor;
    }

    private AssetFileDescriptor getImageAsset(Uri uri) throws IllegalArgumentException {
        AssetManager am = Objects.requireNonNull(getContext()).getAssets();
        final List<String> pathSegments = uri.getPathSegments();
        if (pathSegments.size() != 3) {
            throw new IllegalArgumentException("path segments should be 3, uri is: " + uri);
        }
        String fileName = pathSegments.get(pathSegments.size() - 1);
        final String identifier = pathSegments.get(pathSegments.size() - 2);
        if (TextUtils.isEmpty(identifier)) {
            throw new IllegalArgumentException("identifier is empty, uri: " + uri);
        }
        if (TextUtils.isEmpty(fileName)) {
            throw new IllegalArgumentException("file name is empty, uri: " + uri);
        }
        //making sure the file that is trying to be fetched is in the list of stickers.
        for (StickerPack stickerPack : getStickerPackList()) {
            if (identifier.equals(stickerPack.identifier)) {
                if (fileName.equals(stickerPack.trayImageFile)) {
                    return fetchFile(uri, am, fileName, identifier);
                } else {
                    for (Sticker sticker : stickerPack.getStickers()) {
                        if (fileName.equals(sticker.imageFileName)) {
                            return fetchFile(uri, am, fileName, identifier);
                        }
                    }
                }
            }
        }
        return null;
    }

    private AssetFileDescriptor fetchFile(@NonNull Uri uri, @NonNull AssetManager am, @NonNull String fileName, @NonNull String identifier) {
        try {
            return am.openFd(identifier + "/" + fileName);
        } catch (IOException e) {
            Log.e(Objects.requireNonNull(getContext()).getPackageName(), "IOException when getting asset file, uri:" + uri, e);
            return null;
        }
    }


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

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

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

As you can see it is using a Value from Android Studios BuildConfig
BuildConfig.CONTENT_PROVIDER_AUTHORITY

I found out that the content of this value is the Value of the Packagename +".StickerContentProvider"
If i understand correctly as there is no BuildConfig in b4a i would need to find a way to set this Values dynamically.

How could i use such a ContentProvider in my java-wrapper-library?

In my case it is included in the jar created by SLC. com.whatsappex.StickerContentProvider

But i´m not able to setup the Manifest correctly to reference this StickerContentProvider

I tried
B4X:
AddApplicationText(
<provider
    android:name=".StickerContentProvider"
    android:authorities="${applicationId}.StickerContentProvider"
    android:enabled="true"
    android:exported="true"
    android:readPermission="com.whatsapp.sticker.READ" />
)

java.lang.RuntimeException: Unable to get provider de.donmanfred.whatsappsticker.StickerContentProvider: java.lang.ClassNotFoundException: Didn't find class "de.donmanfred.whatsappsticker.StickerContentProvider" on path: DexPathList[[zip file "/data/app/de.donmanfred.whatsappsticker-lTmAWBTR4UyqLJ1NilNo6g==/base.apk"],nativeLibraryDirectories=[/data/app/de.donmanfred.whatsappsticker-lTmAWBTR4UyqLJ1NilNo6g==/lib/arm64, /system/lib64, /system/vendor/lib64]]
at android.app.ActivityThread.installProvider(ActivityThread.java:6577)
at android.app.ActivityThread.installContentProviders(ActivityThread.java:6129)
at android.app.ActivityThread.handleBindApplication(ActivityThread.java:6039)
at android.app.ActivityThread.-wrap1(Unknown Source:0)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1764)
at android.os.Handler.dispatchMessage(Handler.java:105)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6940)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:327)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1374)
Caused by: java.lang.ClassNotFoundException: Didn't find class "de.donmanfred.whatsappsticker.StickerContentProvider" on path: DexPathList[[zip file "/data/app/de.donmanfred.whatsappsticker-lTmAWBTR4UyqLJ1NilNo6g==/base.apk"],nativeLibraryDirectories=[/data/app/de.donmanfred.whatsappsticker-lTmAWBTR4UyqLJ1NilNo6g==/lib/arm64, /system/lib64, /system/vendor/lib64]]
at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:93)
at java.lang.ClassLoader.loadClass(ClassLoader.java:379)
at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
at android.app.ActivityThread.installProvider(ActivityThread.java:6562)
... 10 more

For reference: This is what the told in the Description:
----------------------------------------------------------------
Advanced development
For advanced developers looking to make richer sticker apps, follow the instructions below.

Overview
Sticker apps communicate with WhatsApp as follows:

  • Your app should provide a ContentProvider (the sample app provides an example) to share the sticker pack information to WhatsApp. The ContentProvider shares information about the sticker pack's name, publisher, identifier and everything else that is listed in contents.json file. It also allows WhatsApp to fetch actual sticker files from the ContentProvider. The ContentProvider is identified by its authority. And a sticker pack is identified by the combination of the authority and identifier.
  • Your app should send an intent to launch WhatsApp's activity. The intent contains three pieces of information: the ContentProvider authority, the pack identifier of the pack that user wants to add, and the sticker pack name. Once the user confirms that they want to add that sticker pack to WhatsApp, WhatsApp will remember the pair of authority and identifier and will load the pack's stickers in the WhatsApp sticker picker/tray.
ContentProvider
The ContentProvider in the sample app is StickerContentProvider. The ContentProvider provides 4 APIs:

  1. <authority>/metadata, this returns information about all the sticker packs in your app. Replace <authority> with the actual authority string. In the sample app, it is com.example.samplestickerapp.stickercontentprovider
  2. <authority>/metadata/<pack_identifier>, this returns information about a single pack. Replace <pack_identifier> with the actual identifier of the pack. In the sample app, it is 1.
  3. <authority>/stickers/<pack_identifier>, this returns information about the stickers in a pack. The returned information includes the sticker file name and emoji associated with the sticker.
  4. <authority>/stickers_asset/<pack_identifier>/<sticker_file_name>, this returns the binary information of the sticker: AssetFileDescriptor, which points to the asset file for the sticker. Replace <sticker_file_name> with the actual sticker file name that should be fetched.
The ContentProvider needs to have a read permission of com.whatsapp.sticker.READ in AndroidManifest.xml. It also needs to be exported and enabled. See below for an example:

B4X:
  <provider
       android:name=".StickerContentProvider"
       android:authorities="${contentProviderAuthority}"
       android:enabled="true"
       android:exported="true"
       android:readPermission="com.whatsapp.sticker.READ" />


- Is it possible at all to add a ContentProvider with a b4a library?
- Does anyone have an example on how it would work?
- How does the Manifest looks like? I always have problems seting up the Manifest correctly ;-(
 

Erel

B4X founder
Staff member
Licensed User
Longtime User
The component name should be:
B4X:
AddApplicationText(
<provider
    android:name="com.example.samplestickerapp.StickerContentProvider"
    android:authorities="${applicationId}.StickerContentProvider"
    android:enabled="true"
    android:exported="true"
    android:readPermission="com.whatsapp.sticker.READ" />
)
 

DonManfred

Expert
Licensed User
Longtime User
The component name should be
this would work for the example app project as it is using the apps packaganame.

I had a partially success. Based on your info i made a 2nd jar with the Classes in there for the provider. I named the classes in them com.sticker4w.*
I then used #additionaljar to add the jar to my project. And edited the manifest to reference com.sticker2w.StickerContentProvider.

It does work setting up the Manifest like this. Android can now find the Provider successfully.

But Whatsapp expect the Provider to start with the packagename of my App. It should not start with a different Packaname. The ContentProvider Authority must start with the packagename of my App.

The Provider does include hardcoded the AUTHORITY which is set to com.example.samplestickerapp.StickerContentProvider where
com.example.samplestickerapp is the Example Apps Packagename. But it does not work with my App.

But i must change the Provider to use a Different AUTHORITY. In my example the packagename is de.donmanfred.stickertest. So i need to use a ContentProvider Authority of
de.donmanfred.stickertest.StickerContentProvider to let Whatsapp recognize it.
 

Erel

B4X founder
Staff member
Licensed User
Longtime User
But Whatsapp expect the Provider to start with the packagename of my App.
Are you sure? It is not a standard requirement.

In that case you will need to implement it as a public static class with inline Java. Note that the component name will be something like:
B4X:
${applicationId}.starter$MyContentProvider
 

DonManfred

Expert
Licensed User
Longtime User
Are you sure?
Yes. With the partially successfull provider in my manifest i found a entry in the unfiltered log that the Provider Authority must begin with my Packagename. I get this entry in the unfiltered log after i try to send the intent to whatsapp to add the Stickerpack. The output in the log is from Whatsapp app.

I do not have the exact stacktrace anymore as i did more tests then. Sorry. I´ll try to reproduce and post it later.
 

DonManfred

Expert
Licensed User
Longtime User
Are you sure? It is not a standard requirement.
I´ll try to reproduce and post it later.

B4X:
Process: de.donmanfred.sticker, PID: 20411
java.lang.RuntimeException: Unable to get provider com.sticker4w.StickerContentProvider: java.lang.IllegalStateException: your authority (com.sticker4w.stickercontentprovider) for the content provider should start with your package name: de.donmanfred.sticker
    at android.app.ActivityThread.installProvider(ActivityThread.java:6577)
    at android.app.ActivityThread.installContentProviders(ActivityThread.java:6129)
    at android.app.ActivityThread.handleBindApplication(ActivityThread.java:6039)
    at android.app.ActivityThread.-wrap1(Unknown Source:0)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1764)
    at android.os.Handler.dispatchMessage(Handler.java:105)
    at android.os.Looper.loop(Looper.java:164)
    at android.app.ActivityThread.main(ActivityThread.java:6940)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:327)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1374)
Caused by: java.lang.IllegalStateException: your authority (com.sticker4w.stickercontentprovider) for the content provider should start with your package name: de.donmanfred.sticker
    at com.sticker4w.StickerContentProvider.onCreate(StickerContentProvider.java:86)
    at android.content.ContentProvider.attachInfo(ContentProvider.java:1925)
    at android.content.ContentProvider.attachInfo(ContentProvider.java:1900)
    at android.app.ActivityThread.installProvider(ActivityThread.java:6574)
    ... 10 more

In that case you will need to implement it as a public static class with inline Java.
Can you give a bit help on how it must be done?
The Provider Class they provided is in #1.
 
Top