Easy Picture Sharing On Android

  • Basile Perrenoud

Data sharing is easy on Android. You can choose a text or an image and the user will be prompted with a choice of apps that support it.
But sometimes, you might want to share more than an image...

PicShare

When letting your user share a picture, you might want to add a watermark or reduce the size of the image. At Liip, the case that we encountered was that we wanted to share an image and some text with it. Turned out many applications won't support getting text and image at the same time. We chose to add the text inside our image directly, and no solution existed that worked out of the box.

We ended up making a library with more options for image sharing, get it on github or read a bit more about it here

How basic sharing works

When you want to share some data, you simply start an activity with the built-in intent type ACTION_SEND:

val sendIntent: Intent = Intent().apply {
    action = Intent.ACTION_SEND
    putExtra(Intent.EXTRA_TEXT, "This is my text to send.")
    type = "text/plain"
}
startActivity(sendIntent)

Alternatively, for an image, you would use:

    putExtra(Intent.EXTRA_STREAM, uriToImage)
    type = "image/*"

Tip: The support library now includes a helper for sharing (see ShareCompat)

The common problem with these solutions is that you have to define the type of data you send (using text/plain or image/* for example). This type will be used by Android to define which app can be proposed to the user when sharing your data. Or course, you can set the type to */* but this leaves the responsibility to the called app to identify the type of data sent. Few apps support it and you have no control over how they will interpret it.

What does the PicShare lib do?

PicShare ultimately only allows you to share images. But it will let you crop it, inlay as many text and visuals onto it and then present a preview to your user. All the flow between these different screens is handled by the lib so you have nothing else to do.
Simply call the startSharing function with the desired options:

val cropOptions = CropOptions()
                    .setFixedSize(1024, 1024)

val inlayOptions = InlayOptions(InlayViewProvider(R.layout.inlay))

val previewOptions = PreviewOptions()
                    .setTitle(resources.getString(R.string.picshare_app_title))

startSharing(this, 1024, 1024, "my share panel", "my shared image", cropOptions, inlayOptions, previewOptions)

Under the hood 1: Workflow

When called, Picshare will start an activity with no layout, for the sole purpose of handling the workflow. This activity will call an image selection app, or any other view necessary for sharing your image, and listen for the results in onActivityResult. It will then choose what is the next view to present, and so on until the sharing is complete or canceled.

Under the hood 2: Content Provider

PicShare doesn't require any permission. The image selected by the user for sharing is copied in a folder accessible by a content provider. PicShare reads the selected image from the MediaStore to get the bitmap but doesn't save it back there for sharing. This means that it is not written on public storage and won't be accessible by other apps (like the gallery) while it is stored there before sharing. Using a content provider is a good practice on android and is a nice way to let other apps access some of your media without writing it on public storage. Here is how you can store files and share them using a file provider:

You can get a Uri by doing the following. This uri is readable and writable by your app only. If you print it you will see that it starts with file://.

var file = File("${context.cacheDir}/myFolder", "myFileName")
var localUri = Uri.fromFile(file)

Tip: Files in context.cacheDir are accessible by your app only.

To get a uri that other apps can read if you share it, you need to use a content provider in the following way. This uri can be shared. If you print it you will see that is starts with content://. But in order for this to work, "${context.packageName}.provider" must be an existing file provider referenced in your manifest:

var file = File("${context.cacheDir}/myFolder", "myFileName")
val shareUri = FileProvider.getUriForFile(context, "${context.packageName}.provider", file)

The file provider itself is defined in its own xml file. The manifest will list all your file providers.

fileprovider.xml

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <cache-path name="myfileprovider" path="myfileprovider"/>
</paths>

manifest.xml

<provider
    android:name="com.company.appname.sharing.SharingFileProvider"
    android:authorities="com.company.appname.provider"
    android:exported="false"
    android:grantUriPermissions="true"
    android:writePermission="false">

    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/fileprovider" />
</provider>

Under the hood 3: Pass a custom view to an activity

In PicShare, you can create an xml layout to inlay the user image with. This seems simple but there is a trick. We cannot pass an already inflated view to an activity. And if we just pass the layout resource id, PicShare won't know how to dynamically customize it once inflated. The trick here is to use a Serializable class. Instances of Serializable classes can be passed in bundles to activities. In PicShare, we prepared an open class that is serializable and contains everything we need to inflate it later.

open class InlayViewProvider(val viewResourceId: Int) : Serializable {
    open fun populate(view: View, context: Context) {
    }
}

The user of the lib simply adds their own custom subclass instance to the inlay parameters:

var inlayOptions = InlayOptions(InlayCustomProvider(R.layout.inlay))

Behind the scene, PicShare will bundle it and retrieve the view from the launched activity. Once inflated, it can populate it with the populate function defined by the custom implementation:

var inlayViewProvider: InlayViewProvider
        get() = optionBundle.getSerializable(inlayViewProviderKey) as InlayViewProvider
        private set(provider) = optionBundle.putSerializable(inlayViewProviderKey, provider)
val view = layoutInflater.inflate(inlayViewProvider.viewResourceId, null)
inlayViewProvider.populate(view, this)

Conclusion

PicShare will help you share custom images from you app with just a few options. It is available on github and can easily be added in your app. It is distributed under the MIT licence.


Sag uns was du denkst