Issue
I'm a novice Android developer experiencing some difficulties in cross-version support: I'm developing an app (the name is RECIPE) with minimum SDK version requirement of 21 (from Lollipop and on).
For now the app has just few features: it switches activities via intents, open the camera by sending an intent, allow the user to make a single photo with the camera module and then stores the photo and returns a preview back to the main app.
The problem occurs when the camera intent is called: in this case the app runs smooth if I run it on an emulator with a device with API 23 (Marshmallow; for 21 and 22 Lollipop's API now the app doesn't work because I have to do some permission managing); but unfortunately, the app crashes if I run it on a device with API 24 or 25 (Nougat).
If you want to reproduce the problem, after having installed the app (on a physical or emulated device with API 24 or 25) open it, then click on "GO TO SINGLE PHOTO SHOOTING MODE" and then on "TAKE PHOTO" to start the camera intent. Normally you will also be prompted of allowing writing permission to store the photo file.
I think that the bug comes from the writing permissions or from something regarding the camera intent.
Down here there is the code
MainMenu.java
package it.iudiconenext.alessandro.recipecrowdsourcingapp;
/**
* TODO=check if AppCompatActivity is necessary
*/
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.pm.ActivityInfoCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
/** The main class with the one that extends and the implementation of the callback for the result
* of requesting permission */
public class MainMenu extends AppCompatActivity
implements ActivityCompat.OnRequestPermissionsResultCallback {
// Id to identify the Write External Storage permission request (it can be a random number)
private static final int REQUEST_WES = 0;
// The following string is used in log messages
public static final String TAG = "MainMenu";
/**Called when the activity is first created.*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_menu);
}
/** The button for going from the Main Menu Activity to the Single Photo Shooting Activity*/
public void buttonMMAtoSPSA(View view) {
Log.i(TAG, "Accessing to SPSA. Checking permission.");
//Check if the Write External Storage permission is already available.
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
// Write External Storage permission is already available, show the SPSA.
MMAtoSPSA();
} else {
// Write External Storage permission has not been granted.
// Provide an additional rationale to the user if the permission was not granted and the
// user would benefit from additional context for the use of the permission.
if (shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
Toast.makeText(this, "External Storage Writing access is required.",
Toast.LENGTH_SHORT).show();
}
// Request Write External Storage permission
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_WES);
}
}
private void MMAtoSPSA () {
//The part of the code for switching to Single Photo Shooting Activity
Intent myIntent = new Intent(this, SinglePhotoShooting.class);
startActivityForResult(myIntent, 0);
}
/** The button for going from the Main Menu Activity to the Multi Photo Shooting Activity*/
public void buttonMMAtoMPSA(View view) {
//The part of the code for switching to Multi Photo Shooting Activity
Intent myIntent = new Intent(view.getContext(), MultiPhotoShooting.class);
startActivityForResult(myIntent, 0);
}
//@Override
public void onRequestPermissionResult (int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults){
if (requestCode == REQUEST_WES){
if (grantResults [0] == PackageManager.PERMISSION_GRANTED){
Log.i(TAG, "WES permission has now been granted; continuing.");
Toast.makeText(this, "WES permission has now been granted; continuing.",
Toast.LENGTH_SHORT).show();
}else {
Log.i(TAG, "WES permission was denied; stopping.");
}
} else
{
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
}
MultiPhotoShooting.java
package it.iudiconenext.alessandro.recipecrowdsourcingapp;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
/**
* Created by alessandro on 09/06/17.
* This is the Java Class corresponding to the "Multi Photo Shooting mode"
*/
public class MultiPhotoShooting extends AppCompatActivity{
/** Called when the activity is first created. */
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.multi_photo_shooting);}
/** The button for going from the Multi Photo Shooting Activity to the Main Menu Activity */
public void MPSAtoMMA(View view){
Intent myIntent = new Intent(view.getContext(), MainMenu.class);
startActivityForResult(myIntent, 0);
}
}
SinglePhotoShooting.java
package it.iudiconenext.alessandro.recipecrowdsourcingapp;
import android.Manifest;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.ImageView;
import android.widget.Toast;
import java.io.File;
import java.io.IOException;
import java.util.Date;
import java.text.SimpleDateFormat;
/**
* Created by alessandro on 08/06/17.
* This is the Java Class corresponding to the "Single Photo Shooting mode"
*/
public class SinglePhotoShooting extends AppCompatActivity {
// This variable is needed as request code in the takePhoto method
private static final int ACTIVITY_START_CAMERA_APP = 0;
// This ImageView variable is useful for finding the view that we want inside the layout
private ImageView SPSAPhotoTakenImageView;
// A variable in the activity that saves the location of the file where we've written to
private String mImageFileLocation = "";
/** Called when the activity is first created. */
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.single_photo_shooting);
SPSAPhotoTakenImageView = (ImageView) findViewById(R.id. PrewievSPSA);
}
/** The button for going from the Single Photo Shooting Activity to the Main Menu Activity */
public void SPSAtoMMA(View view){
Intent myIntent = new Intent(view.getContext(), MainMenu.class);
startActivityForResult(myIntent, 0);
}
/** The method to call an Intent to open the camera app */
public void SPSATakePhoto(View view) {
// int permissionCheck = ContextCompat.checkSelfPermission(SinglePhotoShooting,
// Manifest.permission.WRITE_EXTERNAL_STORAGE);
Intent callCameraApplicationIntent = new Intent();
callCameraApplicationIntent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
// We give some instruction to the intent to save the image
File photoFile = null;
try {
// If the createImageFile will be succesful, photofile will have the address of the file
photoFile = createImageFile();
// Here we call the function that will try to catch the exception made by the throw function
} catch (IOException e){
e.printStackTrace();
}
// Here we add an extra filed to the intent to put the address on to
callCameraApplicationIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(photoFile));
startActivityForResult(callCameraApplicationIntent, ACTIVITY_START_CAMERA_APP);
}
/** The method to give a Bitmap back to the application for a preview */
protected void onActivityResult (int requestCode, int resultCode, Intent data){
if(requestCode == ACTIVITY_START_CAMERA_APP && resultCode == RESULT_OK ){
/** The code that handles the preview for the photo */
// Here we create a bitmap and use BitmapFactory to decode the file
Bitmap SPSAPhotoTakenBitmap = BitmapFactory.decodeFile(mImageFileLocation);
// Assign the bitmap to the ImageView
SPSAPhotoTakenImageView.setImageBitmap(SPSAPhotoTakenBitmap);
}
}
/** The function that specifies the location and the name of the file that we want to create */
// As certain function calls quite important rights, we wanna catch and be notified when something goes wrong and for this we throw an exception
File createImageFile() throws IOException {
// Here we create a "non-collision file name", alternatively said, "an unique filename" using the "timeStamp" functionality
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmSS").format(new Date());
String imageFileName = "IMAGE_" + timeStamp + "_";
// Here we specify the location and environment where we want to save the so-created file
File storageDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
// Here we create the file using a prefix, a suffix and a directory
File image = File.createTempFile(imageFileName, ".jpg", storageDirectory);
// Here the location is saved into the string mImageFileLocation
mImageFileLocation = image.getAbsolutePath();
// The file is returned to the previous intent across the camera application
return image;
}
}
activity_main_menu.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="it.iudiconenext.alessandro.recipecrowdsourcingapp.MainMenu">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="8dp">
<TextView
android:id="@+id/Activity_Main_Menu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:text="Main Menu"
android:textSize="24sp"
android:textStyle="bold|italic"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/Activity_Main_Menu"
android:orientation="vertical">
<LinearLayout
android:id="@+id/TakePhotoButtons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="160dp"
android:layout_weight="1"
android:layout_margin="8dp">
<Button
android:id="@+id/ButtonMMtoSPSA"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="1dp"
android:paddingTop="30dp"
android:onClick="buttonMMAtoSPSA"
android:text="Go to Single Photo Shooting Mode"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:layout_margin="8dp">
<Button
android:id="@+id/ButtonMMtoMPSA"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="1dp"
android:paddingTop="30dp"
android:onClick="buttonMMAtoMPSA"
android:text="Go to Multi Photo Shooting Mode"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</LinearLayout>
multi_photo_shooting.xml
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="center"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="Multi Photo Shooting mode"
android:textSize="24sp"
android:textStyle="bold|italic"/>
<Button
android:id="@+id/ButtonMPSAtoMM"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Press me for going back to the Main Menu"
android:onClick="MPSAtoMMA"/>
</LinearLayout>
</ScrollView>
single_photo_shooting.xml
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="center"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="Multi Photo Shooting mode"
android:textSize="24sp"
android:textStyle="bold|italic"/>
<Button
android:id="@+id/ButtonMPSAtoMM"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Press me for going back to the Main Menu"
android:onClick="MPSAtoMMA"/>
</LinearLayout>
</ScrollView>
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="it.iudiconenext.alessandro.recipecrowdsourcingapp">
<!-- Permissions managing. -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- camera permission is unnecessary due to the use of an external resource
<uses-permission android:name="android.permission.CAMERA"/>
-->
<!-- Read permission is unnecessary due to the already present write permission
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
-->
<!-- There is no need to access the location for the moment
TODO: verify if giving this permission can give access to the GPS by the camera and so to photo
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
-->
<!-- The following permission is needed only if your app targets Android 5.0 (API level 21) or
higher and uses GPS localization service.
<uses-feature android:name="android.hardware.location.gps" />
-->
<uses-feature
android:name="android.hardware.camera2"
android:required="true"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainMenu"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".SinglePhotoShooting"
android:theme="@style/PhotoTakingTheme"
android:label="Single photo mode"/>
<activity
android:name=".MultiPhotoShooting"
android:theme="@style/PhotoTakingTheme"
android:label="Multi photo mode"/>
</application>
</manifest>
Thanks in advance, Alessandro
EDIT 20/06/2017: I think that the problem regards something about URI exceptions because I find the following article which states that these exceptions are given starting from android N versions and because in debugging my app I obtain this exception in return. https://developer.android.com/reference/android/os/FileUriExposedException.html
Solution
SOLVED: Find out that the problem was about the way in which I was trying to manage files from one app (the main app) to the other (the camera app, which is a server app). In fact, the old file Uri scheme is banned for the apps with targetSDKVersion of 24 and higher (that was exactly the one I targeted); starting from that target SDK, the developer should use a File Provider in order to manage files from one app to another.
I followed some online tutorials and articles, like the following:
Here I will try to explain all the important modifications made for having an app that works also for these API (Nougat).
Strings added in the AndroidManifest.xml
<!-- The following component is a file provider needed from target Version Android API 24 (Nougat) and on
-->
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
Use of "provider_paths.xml"
In the res Folder, I've created a "xml" folder where I created the following .xml file named "provider_paths.xml"
<?xml version="1.0" encoding="utf-8"?>
<!--In this file we specify the list of storage area and path in XML, using child elements of the <paths> element.
These paths are used by the provider set in the manifest
The <paths> element must contain one or more of the following child elements: "<files-path name="name" path="path" /> Represents files in the files/ subdirectory of your app's internal storage area. This subdirectory is the same as the value returned by Context.getFilesDir()."
For example, the following paths element tells FileProvider that you intend to request content URIs for the images/ subdirectory of your private file area.-->
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path
name="my_images"
path="."/>
</paths>
Modification of SinglePhotoShooting.java
Also the java file "SinglePhotoShooting.java" has been modified, but the most important modifications are the ones regarding the "SPSATakePhoto" method, mostly when the "FileProvider.getUriForFile" function is called.
public void SPSATakePhoto(View view) {
// int permissionCheck = ContextCompat.checkSelfPermission(SinglePhotoShooting,
// Manifest.permission.WRITE_EXTERNAL_STORAGE);
Logger.getAnonymousLogger().info("Beginning of Take Photo");
Intent callCameraApplicationIntent = new Intent();
callCameraApplicationIntent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
// We give some instruction to the intent to save the image
File photoFile = null;
try {
// If the createImageFile will be successful, the photo file will have the address of the file
photoFile = createImageFile();
// Here we call the function that will try to catch the exception made by the throw function
} catch (IOException e) {
Logger.getAnonymousLogger().info("Exception error in generating the file");
e.printStackTrace();
}
// Here we add an extra file to the intent to put the address on to. For this purpose we use the FileProvider, declared in the AndroidManifest.
Uri outputUri = FileProvider.getUriForFile(
this,
BuildConfig.APPLICATION_ID + ".provider",
photoFile);
callCameraApplicationIntent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
// The following is a new line with a trying attempt
callCameraApplicationIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
Logger.getAnonymousLogger().info("Calling the camera App by intent");
// The following strings calls the camera app and wait for his file in return.
startActivityForResult(callCameraApplicationIntent, ACTIVITY_START_CAMERA_APP);
}
I also modified the "MainMenu.java" file for allowing the app to run on Lollipop (as you can read in the original post, the app used to crash due to permission managing not supported in Lollipop), with the older permission system at install time (rather than the newer system "on run-time" supported from Marshmallow and on) but for this I'll let you check it out directly in the repository because these modifications doesn't cover the purpose of the original question.
Here you can find the repository with the app and the corresponding modifications to make it work:
https://github.com/AlessandroIu/photosavingapp
Answered By - Alessandro Iudicone
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.