【问题标题】:Android: the app crashes if I run it on a device with API 24 or 25 (Nougat)Android:如果我在 API 24 或 25 (Nougat) 的设备上运行应用程序,应用程序会崩溃
【发布时间】:2017-11-21 20:05:27
【问题描述】:

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

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

调用相机意图时会出现问题:在这种情况下,如果我在具有 API 23(棉花糖;对于 21 和 22 Lollipop 的 API)的设备的模拟器上运行应用程序,应用程序运行顺利,因为我必须做一些权限管理);但不幸的是,如果我在使用 API 24 或 25 (Nougat) 的设备上运行该应用程序会崩溃

如果您想重现该问题,请在安装应用程序后(在具有 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>

提前致谢, 亚历山德罗

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

【问题讨论】:

  • “如果我在使用 API 24 或 25 (Nougat) 的设备上运行应用程序会崩溃”——请编辑您的问题并提供完整的 Java 堆栈跟踪以及该堆栈中引用的代码跟踪(因此,当您的 GitHub 存储库中的代码发生更改时,该问题在将来很有用)。
  • 这是个好主意,似乎是一个更好的解决方案。我已经更改了我的问题的名称,并且关于 Java 代码,我应该在发布之前对其进行编辑(比如删除一些“待办事项”和无用的 cmets)......我会尽快这样做。谢谢
  • 我现在也添加了代码(并删除了存储库的链接),同时我发现一篇似乎描述了我的问题的文章......我已经添加了它到我原来的帖子。现在我正在努力寻找解决方案。
  • 我没有看到完整的 Java 堆栈跟踪,这是我要求的最重要的事情。如果您收到FileUriExposedException,则您运行的是Android 7.0+ 和targetSdkVersion 为24 或更高的应用程序,以及the file Uri scheme is banned。您可以通过使用Uri.fromFile() 获得此信息。使用FileProvider 或其他东西给你一个content Uri 来代替使用。
  • Here is a sample app 演示了FileProviderACTION_IMAGE_CAPTURE 的使用。

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


【解决方案1】:

已解决:发现问题出在我尝试从一个应用程序(主应用程序)到另一个应用程序(相机应用程序,它是一个服务器应用程序)管理文件的方式上)。 实际上,对于 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 上运行(正如您在原始帖子中看到的那样,由于 Lollipop 不支持权限管理,该应用程序曾经崩溃),使用较旧的权限安装时的系统(而不是 Marshmallow 和 on 支持的“运行时”新系统),但为此我会让您直接在存储库中检查它,因为这些修改不涵盖原始问题的目的.

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

https://github.com/AlessandroIu/photosavingapp

【讨论】:

    猜你喜欢
    • 2017-10-24
    • 1970-01-01
    • 1970-01-01
    • 2011-04-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-01-07
    • 1970-01-01
    相关资源
    最近更新 更多