Skip to content

Commit

Permalink
Correct sharing of image via share menu item.
Browse files Browse the repository at this point in the history
Solves issue #30:
#30

Previously, there were permission problems from trying to share the
image to image sharing apps.

* AndroidManifest: Define FileProvider class, along with the required
  meta-data and associated files (a string was added to strings.xml for
  the provider authority). This is apparently required for sharing streams
  in Android M and above.

* SubjectFragment: Add a GetImageBitmapAsyncTask inner class to download
  the image in the background asynchronously. A temporary file of the
  image is written to keep track of exact file location, as well as to have
  a temporary cache of previously shared images.

  The FileProvider class is used to create the URI that will eventually be
  sent in the intent.  A FLAG_GRANT_READ_URI_PERMISSION intent flag is
  added for purposes of magic (I'm not really sure what it does, but it works).

* SubjectFragment.updateShareActionIntent(): Create the GetImageBitmapAsyncTask.
  In devices with Android M or above, explicit permission to write and read
  images is obtained if not already provided.

In the limited use case of trying to share images, this code works.
Additional changes might be required to distinguish text from image
intents to share, because the code right now will set the share intent
to an image type if mUriImageRemote is not null.
  • Loading branch information
avitaker authored and murraycu committed May 19, 2017
1 parent 944213e commit 863c54e
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 2 deletions.
20 changes: 20 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,26 @@
<meta-data android:name="android.content.SyncAdapter"
android:resource="@xml/syncadapter" />
</service>

<!-- Generates a FileProvider class for the app with the appropriate authority.
This allows the system to securely share files by creating content URI for files rather than
file URI, thus granting temporary file access permissions to sharing apps.
This is required for devices with Android M or above, while it wasn't necessary before.
Without a file provider class, the OS will deny other apps the permission to access
GalaxyZoo's files, and sharing locally stored images will be impossible. -->
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="@string/authority_fileprovider"
android:exported="false"
android:grantUriPermissions="true">

<!-- This meta data and the associated XML file specify that the files will be written to the root folder
of the app's external storage area. -->
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
</provider>
</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ public void onLoaderReset(final Loader<Cursor> cursorLoader) {
}
}

private GetNextIdLoader getNextIdLoader = new GetNextIdLoader();
private final GetNextIdLoader getNextIdLoader = new GetNextIdLoader();

This comment has been minimized.

Copy link
@murraycu

murraycu May 19, 2017

Owner

This (my change) shouldn't have been in this commit, but that was my fault when cleaning up the commit before pushing.


/**
* Mandatory empty constructor for the fragment manager to instantiate the
Expand Down
86 changes: 85 additions & 1 deletion app/src/main/java/com/murrayc/galaxyzoo/app/SubjectFragment.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,22 @@

package com.murrayc.galaxyzoo.app;

import android.Manifest;
import android.app.Activity;
import android.app.DownloadManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.FileProvider;
import android.support.v4.content.Loader;
import android.support.v4.view.MenuItemCompat;
import android.support.v7.widget.ShareActionProvider;
Expand All @@ -45,6 +52,11 @@
import com.squareup.picasso.Callback;
import com.squareup.picasso.Picasso;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

/**
* A fragment representing a single subject.
* This fragment is either contained in a {@link ListActivity}
Expand Down Expand Up @@ -406,6 +418,9 @@ public void onPause() {
}

private void updateShareActionIntent() {
/**
* Initialization and setup of the share intent is done here so that less work is left after the AsyncTask's execution
*/
if (mShareActionProvider == null) {
Log.error("updateShareActionIntent(): mShareActionProvider is null.");
return;
Expand All @@ -427,7 +442,76 @@ private void updateShareActionIntent() {
//shareIntent.putExtra(Intent.EXTRA_STREAM, mUriImageStandard);
//shareIntent.setType("image/*");

/**
* if the image URI is not null, a GetImageBitmapAsyncTask is executed for it, and the
* returned URI is set as a stream extra on the intent that will be shared, along with an
* explicit permission for recieving contexts to read the content URI, enabling them to
* access the generated image.
*/
if (mUriStandardRemote != null) {
GetImageBitmapAsyncTask getImageBitmapAsyncTask = new GetImageBitmapAsyncTask(){
@Override
protected void onPostExecute(final Uri uri) {
shareIntent.setType("image/*");
shareIntent.putExtra(Intent.EXTRA_STREAM, uri);
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
mShareActionProvider.setShareIntent(shareIntent);
}
};
getImageBitmapAsyncTask.execute(mUriStandardRemote);
}

}

/**
*These constants and the verifyStoragePermissions method will gain explicit permission from users to read and write
* files on their devices. This will allow us to save an image, that can then be shared to other apps.
*/
private static final int REQUEST_EXTERNAL_STORAGE = 1;
private static String[] PERMISSIONS_STORAGE = {
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
};

public static void verifyStoragePermissions(Activity activity) {
int permission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE);

if (permission != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(
activity,
PERMISSIONS_STORAGE,
REQUEST_EXTERNAL_STORAGE
);
}
}

mShareActionProvider.setShareIntent(shareIntent);
/**
* This AsyncTask, when executed, takes a String that represents a URI, creates the appropriate Bitmap, saves it to
* the external storage area of the app as a jpg, and returns the content Uri of that file as generated by the FileProvider class.
* Keeping a single file name ensures that future tasks will write over images previously generated by this task instead
* of creating new ones.
*/
private class GetImageBitmapAsyncTask extends AsyncTask<String, Integer, Uri> {
@Override
protected Uri doInBackground(String... params) {
try {
Bitmap image = Picasso.with(getContext()).load(params[0]).get();
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
image.compress(Bitmap.CompressFormat.JPEG, 100, bytes);

String filename = "galaxy_zoo_image.jpg";

String pathname = Environment.getExternalStorageDirectory() + File.separator + filename;
File f = new File(pathname);
f.createNewFile();
FileOutputStream fo = new FileOutputStream(f);
fo.write(bytes.toByteArray());
return FileProvider.getUriForFile(getActivity(), getString(R.string.authority_fileprovider), f);
} catch (IOException e) {
verifyStoragePermissions(getActivity());
e.printStackTrace();
}
return null;
}
}
}
3 changes: 3 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,9 @@
//when specifying a transition when starting an activity that contains this SubjectFragment.
<string name="transition_subject_image" translatable="false">transition_subject_image</string>

<!--share image strings-->
<string name="authority_fileprovider" translatable="false">com.murrayc.galaxyzoo.app.fileprovider</string>

<!-- The possible numbers of items that may be downloaded in advance before they are needed. -->
<string-array name="pref_cache_size_entries">
<item>5 subjects</item>
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/res/xml/provider_paths.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!--The FileProvider class will draw from the root directory of the app's external storage area (specified by path="."),
and the generated content URI will indicate that this directory is called "external_files" for security reasons.-->
<external-path name="external_files" path="."/>
</paths>

0 comments on commit 863c54e

Please sign in to comment.