From 6c52efc50c2906b3fcd18934d9942399e0879cb1 Mon Sep 17 00:00:00 2001 From: Avinash David Date: Thu, 6 Apr 2017 15:41:45 -0500 Subject: [PATCH 1/3] feat: share image directly via share menu item Solves issue #30. Previously, there were permission problems from trying to share the image to image sharing apps, according to the issue tracker. The following changes were made: FileProvider class is defined in AndroidManifest, 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. Temporary file of the image is written to keep track of exact file location, as well as have a temporary cache of previously shared images. This is done in an AsyncTask that is defined at the bottom of SubjectFragment and executed in the updateShareIntent method. In devices with Android M or above, explicit permission to write and read images is obtained if not already provided. The FileProvider class is used to create the URI that will eventually be sent in the intent. An intent flag is added for purposes of magic (I'm not really sure what it does, but it works). 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. --- app/app.iml | 24 ++----- app/src/main/AndroidManifest.xml | 10 +++ .../galaxyzoo/app/ClassifyFragment.java | 1 + .../galaxyzoo/app/SubjectFragment.java | 67 +++++++++++++++++++ app/src/main/res/values/strings.xml | 3 + 5 files changed, 88 insertions(+), 17 deletions(-) diff --git a/app/app.iml b/app/app.iml index 19fd22bb..5a074cf1 100644 --- a/app/app.iml +++ b/app/app.iml @@ -63,13 +63,6 @@ - - - - - - - @@ -77,27 +70,24 @@ + + + + + + + - - - - - - - - - - diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 031dcdf6..46754a9d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -139,6 +139,16 @@ + + + + diff --git a/app/src/main/java/com/murrayc/galaxyzoo/app/ClassifyFragment.java b/app/src/main/java/com/murrayc/galaxyzoo/app/ClassifyFragment.java index 0416f366..a75c77ad 100644 --- a/app/src/main/java/com/murrayc/galaxyzoo/app/ClassifyFragment.java +++ b/app/src/main/java/com/murrayc/galaxyzoo/app/ClassifyFragment.java @@ -24,6 +24,7 @@ import android.database.Cursor; import android.net.Uri; import android.os.Bundle; +import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; diff --git a/app/src/main/java/com/murrayc/galaxyzoo/app/SubjectFragment.java b/app/src/main/java/com/murrayc/galaxyzoo/app/SubjectFragment.java index 2733d689..e0823b21 100644 --- a/app/src/main/java/com/murrayc/galaxyzoo/app/SubjectFragment.java +++ b/app/src/main/java/com/murrayc/galaxyzoo/app/SubjectFragment.java @@ -23,11 +23,17 @@ 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; @@ -45,6 +51,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} @@ -427,7 +438,63 @@ private void updateShareActionIntent() { //shareIntent.putExtra(Intent.EXTRA_STREAM, mUriImageStandard); //shareIntent.setType("image/*"); + if (mUriStandardRemote!=null) { + GetImageBitmapAsyncTask getImageBitmapAsyncTask = new GetImageBitmapAsyncTask(){ + @Override + protected void onPostExecute(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); + } + mShareActionProvider.setShareIntent(shareIntent); } + + private static final int REQUEST_EXTERNAL_STORAGE = 1; + private static String[] PERMISSIONS_STORAGE = { + "android.permission.WRITE_EXTERNAL_STORAGE", + "android.permission.READ_EXTERNAL_STORAGE" + }; + + public static void verifyStoragePermissions(Activity activity) { + int permission = ActivityCompat.checkSelfPermission(activity, "android.permission.WRITE_EXTERNAL_STORAGE"); + + if (permission != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions( + activity, + PERMISSIONS_STORAGE, + REQUEST_EXTERNAL_STORAGE + ); + } + } + + private class GetImageBitmapAsyncTask extends AsyncTask { + @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); + + //TODO: PROVIDE A MORE DESCRIPTIVE FILENAME HERE IF PLAUSIBLE. Filename will be visible when sharing via certain apps like Gmail etc. + 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; + } + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3eeb687b..a98a69ae 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -186,6 +186,9 @@ //when specifying a transition when starting an activity that contains this SubjectFragment. transition_subject_image + + com.murrayc.galaxyzoo.app.fileprovider + 5 subjects From 8d31c996ad51bf85daeb6bf73eacef0c50d96014 Mon Sep 17 00:00:00 2001 From: Avinash David Date: Fri, 7 Apr 2017 10:17:09 -0500 Subject: [PATCH 2/3] feat: share image directly via share menu item Solves issue #30. Now added some documentation, removed some generated files and hardcoded strings. Previously, there were permission problems from trying to share the image to image sharing apps, according to the issue tracker. The following changes were made: FileProvider class is defined in AndroidManifest, 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. Temporary file of the image is written to keep track of exact file location, as well as have a temporary cache of previously shared images. This is done in an AsyncTask that is defined at the bottom of SubjectFragment and executed in the updateShareIntent method. In devices with Android M or above, explicit permission to write and read images is obtained if not already provided. The FileProvider class is used to create the URI that will eventually be sent in the intent. An intent flag is added for purposes of magic (I'm not really sure what it does, but it works). 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. --- app/app.iml | 7 ----- app/src/main/AndroidManifest.xml | 7 +++++ .../galaxyzoo/app/ClassifyFragment.java | 1 - .../galaxyzoo/app/SubjectFragment.java | 28 ++++++++++++++----- app/src/main/res/xml/provider_paths.xml | 6 ++++ 5 files changed, 34 insertions(+), 15 deletions(-) create mode 100644 app/src/main/res/xml/provider_paths.xml diff --git a/app/app.iml b/app/app.iml index 5a074cf1..f9f7358b 100644 --- a/app/app.iml +++ b/app/app.iml @@ -70,13 +70,6 @@ - - - - - - - diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 46754a9d..3e9a2678 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -140,11 +140,18 @@ android:resource="@xml/syncadapter" /> + + diff --git a/app/src/main/java/com/murrayc/galaxyzoo/app/ClassifyFragment.java b/app/src/main/java/com/murrayc/galaxyzoo/app/ClassifyFragment.java index a75c77ad..0416f366 100644 --- a/app/src/main/java/com/murrayc/galaxyzoo/app/ClassifyFragment.java +++ b/app/src/main/java/com/murrayc/galaxyzoo/app/ClassifyFragment.java @@ -24,7 +24,6 @@ import android.database.Cursor; import android.net.Uri; import android.os.Bundle; -import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; diff --git a/app/src/main/java/com/murrayc/galaxyzoo/app/SubjectFragment.java b/app/src/main/java/com/murrayc/galaxyzoo/app/SubjectFragment.java index e0823b21..23c7b09b 100644 --- a/app/src/main/java/com/murrayc/galaxyzoo/app/SubjectFragment.java +++ b/app/src/main/java/com/murrayc/galaxyzoo/app/SubjectFragment.java @@ -19,6 +19,8 @@ package com.murrayc.galaxyzoo.app; +import android.*; +import android.Manifest; import android.app.Activity; import android.app.DownloadManager; import android.content.Context; @@ -438,6 +440,11 @@ 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 @@ -451,18 +458,20 @@ protected void onPostExecute(Uri uri) { getImageBitmapAsyncTask.execute(mUriStandardRemote); } - - mShareActionProvider.setShareIntent(shareIntent); } + /** + *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 = { - "android.permission.WRITE_EXTERNAL_STORAGE", - "android.permission.READ_EXTERNAL_STORAGE" + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE }; public static void verifyStoragePermissions(Activity activity) { - int permission = ActivityCompat.checkSelfPermission(activity, "android.permission.WRITE_EXTERNAL_STORAGE"); + int permission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE); if (permission != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions( @@ -473,7 +482,13 @@ public static void verifyStoragePermissions(Activity activity) { } } - private class GetImageBitmapAsyncTask extends AsyncTask { + /** + * 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 { @Override protected Uri doInBackground(String... params) { try { @@ -481,7 +496,6 @@ protected Uri doInBackground(String... params) { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); image.compress(Bitmap.CompressFormat.JPEG, 100, bytes); - //TODO: PROVIDE A MORE DESCRIPTIVE FILENAME HERE IF PLAUSIBLE. Filename will be visible when sharing via certain apps like Gmail etc. String filename = "galaxy_zoo_image.jpg"; String pathname = Environment.getExternalStorageDirectory() + File.separator + filename; diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml new file mode 100644 index 00000000..5aafec20 --- /dev/null +++ b/app/src/main/res/xml/provider_paths.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file From 88c6d4d8abb107eec4fa8bfc7612e44b4302811a Mon Sep 17 00:00:00 2001 From: Avinash David Date: Sun, 9 Apr 2017 23:44:16 -0500 Subject: [PATCH 3/3] feat: share image directly via share menu item Solves issue #30. Now added some documentation, removed some generated files and hardcoded strings. Previously, there were permission problems from trying to share the image to image sharing apps, according to the issue tracker. The following changes were made: FileProvider class is defined in AndroidManifest, 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. Temporary file of the image is written to keep track of exact file location, as well as have a temporary cache of previously shared images. This is done in an AsyncTask that is defined at the bottom of SubjectFragment and executed in the updateShareIntent method. In devices with Android M or above, explicit permission to write and read images is obtained if not already provided. The FileProvider class is used to create the URI that will eventually be sent in the intent. An intent flag is added for purposes of magic (I'm not really sure what it does, but it works). 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. --- .../main/java/com/murrayc/galaxyzoo/app/ClassifyFragment.java | 1 + .../main/java/com/murrayc/galaxyzoo/app/SubjectFragment.java | 3 +++ 2 files changed, 4 insertions(+) diff --git a/app/src/main/java/com/murrayc/galaxyzoo/app/ClassifyFragment.java b/app/src/main/java/com/murrayc/galaxyzoo/app/ClassifyFragment.java index 0416f366..a75c77ad 100644 --- a/app/src/main/java/com/murrayc/galaxyzoo/app/ClassifyFragment.java +++ b/app/src/main/java/com/murrayc/galaxyzoo/app/ClassifyFragment.java @@ -24,6 +24,7 @@ import android.database.Cursor; import android.net.Uri; import android.os.Bundle; +import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; diff --git a/app/src/main/java/com/murrayc/galaxyzoo/app/SubjectFragment.java b/app/src/main/java/com/murrayc/galaxyzoo/app/SubjectFragment.java index 23c7b09b..b36810e9 100644 --- a/app/src/main/java/com/murrayc/galaxyzoo/app/SubjectFragment.java +++ b/app/src/main/java/com/murrayc/galaxyzoo/app/SubjectFragment.java @@ -419,6 +419,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;