Android:如果我在使用API​​ 24或25(牛轧糖)

时间:2017-06-19 14:35:26

标签: android android-intent android-camera android-permissions avd

我是Android开发人员,在跨版本支持方面遇到一些困难:我正在开发一个应用程序(名称为RECIPE),最低SDK版本要求为21(来自Lollipop及以上) )。

目前该应用程序只有很少的功能:它通过意图切换活动,通过发送意图打开相机,允许用户使用相机模块制作单张照片,然后存储照片并将预览返回到主应用程序。

当调用相机意图时会出现问题:在这种情况下,如果我在带有API 23的设备的模拟器上运行应用程序,那么应用程序运行顺畅(Marshmallow;对于21和22 Lollipop的API,现在应用程序不起作用,因为我必须做一些许可管理);但不幸的是,如果我在使用API​​ 24或25(牛轧糖)的设备上运行它,应用程序崩溃。

如果要重现此问题,请在安装应用程序后(在具有API 24或25的物理或模拟设备上)打开它,然后单击“转到单张照片模式”,然后单击“拍摄照片”启动相机意图。 通常,系统还会提示您允许存储照片文件的写入权限。

我认为该错误来自写入权限或来自相关意图的内容。

这里有代码

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>

提前致谢, 的Alessandro

编辑20/06/2017 :我认为这个问题与URI异常有关,因为我发现以下文章指出这些异常是从Android N版本开始给出的,因为在调试时我的app我获得此异常作为回报。 https://developer.android.com/reference/android/os/FileUriExposedException.html

1 个答案:

答案 0 :(得分:2)

已解决:发现问题与我试图管理从一个应用程序(主应用程序)到另一个应用程序(相机应用程序,这是一个服务器应用程序)的方式有关)。 实际上,对于targetSDKVersion为24及更高版本的应用程序(这正是我所针对的应用程序),旧文件Uri方案被禁止;从该目标SDK开始,开发人员应使用文件提供程序来管理从一个应用程序到另一个应用程序的文件。

我遵循了一些在线教程和文章,如下所示:

在这里,我将尝试解释为使用适用于这些API(牛轧糖)的应用程序所做的所有重要修改。

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>

使用“provider_paths.xml”

在res文件夹中,我创建了一个“xml”文件夹,我在其中创建了名为“provider_paths.xml”的以下.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>

修改SinglePhotoShooting.java

此外,java文件“SinglePhotoShooting.java”已被修改,但最重要的修改是关于“SPSATakePhoto”方法的修改,主要是在调用“FileProvider.getUriForFile”函数时。

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);
    }

我还修改了“MainMenu.java”文件以允许该应用程序在Lollipop上运行(正如您可以在原始帖子中看到的那样,用于因Lollipop不支持的权限管理而崩溃的应用程序),具有较旧的权限安装时的系统(而不是Marshmallow支持的“运行时”的新系统),但为此我会让你直接在存储库中查看它,因为这些修改不包括原始问题的目的

在这里,您可以找到包含应用程序的存储库以及相应的修改,以使其工作:

https://github.com/AlessandroIu/photosavingapp