Share the Cache

A lot of Android apps make viewing images a core part of their user experience.  Many of these apps use image caching libraries, like Glide, to make image caching easy, robust, and configurable.  Sharing these cached images can be a bit tricky.  A few questions arose when I tried:

  • How do I access files in the cache?
  • Do other apps have access to these files?
  • What are the needed permissions if I need to manually copy the file somewhere else?

Those are just a few of the questions that came up, each with a pretty simple answer.  A first pass at sharing images in my cache involved, re-caching the image to a public directory (ie. root of the external storage device), generating a URI, and passing that URI to an Intent to share with other apps.  This sounds simple, but it is complicated by the fact that I needed to ask the user for permission to read and write to the external storage on their device, if they were running Android 6.0 Marshmallow or above.  This flow worked, but I was looking for something much simpler for the user and myself, the developer.  Enter the FileProvider.

The FileProvider API

The FileProvider API, added to the Android Support libraries in version 22.0.0, is a ContentProvider-like API that allows URI specific sharing of files relevant to your application.  It can, temporarily, enable access (read and/or write) to the file at the URI.  You also do not need to copy the file to a more accessible location on the user’s device.  Setting up the FileProvider is very straightforward.

Setting up a File Provider

Setting up the FileProvider is a three step process.

First, like we would do with a ContentProvider, we added a <provider> entry to our AndroidManifest.xml file.


<provider 
    android:name="android.support.v4.content.FileProvider" 
    android:authorities="com.yourdomain.android.fileprovider" 
    android:grantUriPermissions="true" 
    android:exported="false">

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

This provider entry has a <meta-data> element that points to an XML file, that defines what paths, within our application file structure, we want to expose with our FileProvider.  This is important.  You can only share files in the paths contained in this XML file.  I created an XML file in the “res/xml” folder.


<paths>
    <external-files-path name="image_files" path="." />
</paths>

In my particular instance, I am caching images to the External Files directory (which can be located on the SD card or a portion of internal memory, simulating an SD Card).  Other tags you can use here:

The name attribute is a string that will be used as a URI-path component in the URI that’s generated by FileProvider.  The path attribute, is the relative path to folder containing the files you would like share.  In my case, I’m fine with sharing the root because it only contains cached images, but you should probably be as granular as possible in the case where you have multiple directories or file types in this directory.

Now that my FileProvider is set up and configured, how do I share files out of my cache.

Sharing Cached Files

The specifics on sharing cached image files is highly dependant on the Image caching library used and how it’s configured.  At a high level, the process is:

  1. Cache an image to a location (done by an image caching library) that falls in the location specified in XML configuration file given to the FileProvider.
  2. Get a reference to that cached image using the java.io.File API.
  3. Pass the File reference to FileProvider.getUriForFile() to get a URI you can pass to other apps.
  4. Pack that URI into an Android Intent to start sharing with other apps.

Very straightforward.  Now this is how I did it with Glide.

My first step was to specify a location for Glide to store cached images.  I needed to subclass DiskLruCacheFactory to make this happen (Note: you don’t need to subclass DiskLruCacheFactory if you want to use the Cache directory).


private static class DiskCacheFactory extends DiskLruCacheFactory {

    DiskCacheFactory(final Context context, final String diskCacheName, long diskCacheSize) {
        super(new CacheDirectoryGetter() {
            File cacheDirectory = new File(context.getExternalFilesDir(null), diskCacheName);
            return cacheDirectory;
        }, (int) diskCacheSize);
    }
}

As you can see from the code snippet, I pass in a reference to the External Files directory.  By default, Glide uses the Cache directory.

I implement a GlideModule so that I can customize caching behavior in my application.  I specify my subclassed DiskLruCacheFactory class in my GlideModule.


public class MyGlideModule implements GlideModule {
    private static final int IMAGE_CACHE_SIZE = 200_000_000;

    @Override
    public void applyOptions(Context context, GlideBuilder builder) {
        builder.setDiskCache(new DiskCacheFactory(context, “.”, IMAGE_CACHE_SIZE));
    }

    @Override
    public void registerComponents(Context context, Glide glide) {
        // ...
    }
}

Now, I instruct Glide where it can find my GlideModule by adding a <meta-data> tag to the AndroidManifest.xml file, in <application> element.


<meta-data android:name="com.yourdomain.android.MyGlideModule" android:value="GlideModule"/>

Next, I invoke a manual file caching with Glide.


File file = Glide.with(context)
    .load(uri) // uri to the location on the web where the image originates
    .downloadOnly(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
    .get();

This is specific to Glide and worth mentioning.  When Glide caches a file to disk, the file name is comprised of generated key with an integer appended to it.  There is no extension.  This is important because sharing an extension-less image will make it difficult for the apps you are sharing with to determine how this file should be handled (despite the MIME type being sent along with the image in the Intent).

Contents of a Glide image cache folder

 

I worked around this issue by simply copying that file, appending an extension to the duplicate in the process.  Since I know I am always handling JPEGs, I give these files the “.jpg” extension.  This may not work in cases where you may be dealing with different types of images like, GIFs, PNGs, WebP, etc.

Finally, I can get a URI from the FileProvider for my cached image by calling FileProvider.getUriForFile().

// be sure to use the authority given to your FileProvider in the AndroidManifest.xml file
String authority = “com.yourdomain.android.fileprovider”;
Uri uri = FileProvider.getUriForFile(context, authority, file);
Intent intent = new Intent(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_STREAM, uri);
intent.setType("image/jpeg");
context.startActivity(Intent.createChooser(intent, “Share via”);

The Uri generated by the FileProvider looks like:

content://com.yourdomain.android.fileprovider/external_files/bb65e6a364264255b4833d34e____some_key.0.jpg

Once the Intent makes its way to the target app, things should look just like you are sharing a picture you’ve just taken.

Sharing an image from Traffcams to Gmail

#Dassit

Testing Your ContentProvider with Robolectric

You like testing?  I love testing, especially unit testing.  ContentProviders are the underpinnings of many data layer implementations in Android apps and obviously, an important thing to test.  I added some new code to a ContentProvider in the RadioPublic app and wanted to verify that the ContentProvider and model code worked.  I spent an hour looking through the documentation and online. I also wanted to use the Robolectric test framework already setup in the app.  After concluding my research, I found what I was looking for and its very straightforward.

First of all, if you haven’t already done so, in your module’s build.gradle file, depedencies section:

testCompile 'junit:junit:4.12'
testCompile 'org.robolectric:robolectric:3.1.1'

In your unit test class, class annotations

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 18)

Register your ContentProvider with the appropriate Authority:

private static final String AUTHORITY = "com.example.debug";

@Before
public void setup() {
    YourProvider provider = new YourProvider();
    provider.onCreate();
    ShadowContentResolver.registerProvider(
            AUTHORITY, provider
    );
}

Get a reference to the ContentResolver, since this is, most likely, how you’ll be interacting with your provider:

ContentResolver contentResolver = RuntimeEnvironment.application.getContentResolver();

Finally, test your ContentProvider

@Test
public void getSomeData() {
   ...
   cursor = contentResolver.query(Test.MyUri, null, null, null, null);
   ...
}

That’s it!  Hopefully this will save you time and encourage you to write tests for your ContentProviders.

Use ints instead of enums on Android

Romain Guy, an Android framework engineer, gave a great talk at Devoxx on building Android apps in a memory conscious fashion.  See the link at the end of the post for the entire slide deck.

One thing always stood out to me, using int variables instead of enums.  As great as Java enums are (self documenting, type safety, etc), the following slide convinced me not to use them unless I really needed to.

Screenshot from 2015-03-04 19:11:44

 

It may seem like a trivial amount, but 1) that’s an order of magnitude smaller when you use int variables instead of enums and 2) a couple thousand bytes here and there in aggregate can lead to substantial savings.  Those memory savings matter, especially on lower end devices.

-> Android Memories

Identifying The Right Customers

Shift Jelly developer, Russell Ivanovic, shared some insights on the breakdown of Pocket Casts users on Android.

First of all, Android version adoption numbers.

screen-shot-2015-01-31-at-10-25-14-am

Developers should spend an overwhelming amount of their time developing and shipping products for Android 4.1 “Jellybean” – Android 4.4 “KitKat” because that’s where the users are right?  Well, it depends.

Pocket Casts specific Android adoption numbers (ie. what versions of Android are my users running).

pc_numbers

My lone Android application, NC Traffic Cams, Android adoption numbers.

Screenshot from 2015-01-31 11:33:01

So while Android 5.0 has less than 1% adoption in the overall Android eco-system, 23% of our customers already run it. This makes sense when you put a bit of thought into these numbers. People that have the money to buy apps, and are passionate about Android, have up to date phones. While some users who run Android 2.3 on their 5 year old phone might be perfectly happy, they probably weren’t ever going to buy Pocket Casts. It’s also worth noting that Pocket Casts sells in much larger volumes (and makes more revenue) than any numbers I’ve seen for an equivalent iOS app. We’ve slowly moved our minimum version from 2.3, to 4.0, to 4.1 and it hasn’t hurt sales at all.

I, 100%, agree with Rusty.

Android adoption numbers, provided by Google, are only relevant if your target customers include ALL Android users.  This is rarely the case.  I’d imagine only a very few developers and companies can afford to target all Android users as customers for their apps and services (depending on the complexity of their apps, most in this group are large public corporations or venture backed startups ie. the Facebooks, Twitters, Instagrams, and Ubers of the world who aim for ubiquity).  Android is such a huge market (1 billion devices sold in 2014) that there are bound to be smaller, profitable, niche markets available.  Enthusiast and indie developers need to spend sometime identifying these smaller, niche markets before starting on new products.

While building NC Traffic Cams, a relatively simple product, I essentially targeted Android users who were interested in North Carolina traffic imagery.  I wasn’t using the latest and greatest APIs because my priority was providing visual traffic information to as many Android users as possible.  Early on, I used libraries (ActionSherlock, Google Play Services, etc.) that made it easy for me to provide a consistent user experience across various versions of Android (at that time, starting at Android 2.1).  I eventually dropped support for Android 2.1 and 2.2 because the Google Play Services library eventually dropped support for these versions.

I, of course, did not completely follow my latest insights with regards to my current project, PremoFM.  In the beginning, I identified, at a very-high-level, business-diagram-type-of-way, the customers I am targeting, but I failed to identify the specific profile of Android users I am targeting until midway through the project.  For example, I targeted Android customers running Android 4.0 through to Android 5.0 (I at least had enough foresight to cut out Gingerbread support).  Then I began writing code and realized how many specific APIs weren’t available on older versions of Android.  I was definitely on the road to a bad user experience and high support costs, given my real life constraints (one person, doing this in my free time).  I took a step back and asked a few questions, some of them being:

  • How much effort or time am I willing to sacrifice to provide support to users with older devices (fixing bugs specific to these version, etc.)?
  • What versions of Android will allow me to provide the best user experience possible?
  • Are there enough paying customers using older versions of Android to make supporting these versions worthwhile?

In the end, it’s all about establishing your priorities (and more importantly, de-prioritizing other things!).  The time you spend working on XYZ will mean you won’t have that time to work on ABC.

As an side, it would be really interesting to see general data on which types of users spend the most money and what versions of Android they use.  My guess is pretty consistent with Android adoption numbers provided by Russell.

-> How New Versions of Android Work @ RustysRants