Android:如果我 运行 在具有 API 24 或 25(牛轧糖)的设备上应用程序崩溃

Android: the app crashes if I run it on a device with API 24 or 25 (Nougat)

我是新手Android开发者在跨版本支持方面遇到了一些困难:我正在开发一个应用程序(名称是RECIPE),使用最少的SDK 21 的版本要求(从 Lollipop 开始)。

目前该应用只有几个功能:通过意图切换活动,通过发送意图打开相机,允许用户使用相机模块拍摄单张照片,然后存储照片和return 返回主应用程序的预览。

调用相机意图时会出现问题:在这种情况下,如果我 运行 在具有 API 23(棉花糖; 对于 21 和 22 Lollipop 的 API 现在该应用程序无法运行,因为我必须进行一些权限管理);但不幸的是,如果我 运行 在具有 API 24 或 25(牛轧糖).

的设备上,应用程序会崩溃

如果您想重现该问题,请在安装应用程序(在具有 API 24 或 25 的物理或模拟设备上)后打开它,然后单击 "GO TO SINGLE PHOTO SHOOTING MODE",然后单击 "TAKE PHOTO" 启动相机意图。 通常还会提示您允许写入权限以存储照片文件。

我认为该错误来自写入权限或与相机意图有关的问题。

下面有代码

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>

提前致谢, 亚历山德罗

编辑 20/06/2017:我认为问题与 URI 异常有关,因为我发现以下文章指出这些异常是从 [=81 开始给出的=] N 个版本,因为在调试我的应用程序时,我在 return 中获得了这个异常。 https://developer.android.com/reference/android/os/FileUriExposedException.html

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

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

在这里,我将尝试解释为拥有一个也适用于这些 API (Nougat) 的应用程序所做的所有重要修改。

字符串添加到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 上 运行(正如您在原文 post 中看到的那样,该应用程序曾因权限管理而崩溃Lollipop 不支持),在安装时使用较旧的权限系统(而不是 Marshmallow 等支持的较新系统 "on run-time")但是为此我会让你直接在存储库中检查它,因为这些修改不会'涵盖原问题的目的。

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

https://github.com/AlessandroIu/photosavingapp