【问题标题】:Android 6.0 Marshmallow. Cannot write to SD Card安卓 6.0 棉花糖。无法写入 SD 卡
【发布时间】:2016-01-13 09:25:53
【问题描述】:

我有一个使用外部存储来存储照片的应用程序。根据需要,在其清单中请求以下权限

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

它使用以下内容来检索所需的目录

File sdDir = Environment
            .getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);

SimpleDateFormat dateFormat = new SimpleDateFormat("MM-dd", Locale.US);
String date = dateFormat.format(new Date());
storageDir = new File(sdDir, getResources().getString(
            R.string.storagedir)
            + "-" + date);

// Create directory, error handling
if (!storageDir.exists() && !storageDir.mkdirs()) {
 ... fails here

该应用在 Android 5.1 到 2.3 上运行良好;它已经在 Google Play 上架了一年多。

在我的一部测试手机(Android One)升级到 6 之后,它现在在尝试创建必要的目录“/sdcard/Pictures/myapp-yy-mm”时返回错误。

SD卡配置为“便携式存储”。我已经格式化了sd卡。我已经更换了它。我已经重启了。一切都无济于事。

此外,“由于存储空间有限,或者应用程序或您的组织不允许”,内置的 android 屏幕截图功能(通过电源+降低音量)失败。

有什么想法吗?

【问题讨论】:

  • 你能发布你的 Logcat 吗?
  • 你的targetSdkVersion 23 吗?还是更早的版本?
  • logcat 中没有异常,可能是因为“错误”被应用程序捕获了。
  • 您是否要求运行时权限!
  • '..在尝试创建必需目录“/sdcard/Pictures/”时返回错误。不,这不是您的代码中发生的情况。您正在尝试创建一个失败的目录,例如 /sdcard/Pictures/myfolder。你甚至没有检查 /sdcard/Pictures 是否存在。

标签: android android-sdcard android-6.0-marshmallow


【解决方案1】:

首先我会给你Android M中的危险权限列表及更高版本

然后举例说明如何在Android M更高版本版本中请求权限。

我请求用户 WRITE_EXTERNAL_STORAGE 许可。

首先在你的 android 清单文件中添加权限

第 1 步声明请求代码

 private static String TAG = "PermissionDemo";
 private static final int REQUEST_WRITE_STORAGE = 112; 

第 2 步当您想请求用户许可时添加此代码

 //ask for the permission in android M
    int permission = ContextCompat.checkSelfPermission(this,
            Manifest.permission.WRITE_EXTERNAL_STORAGE);

    if (permission != PackageManager.PERMISSION_GRANTED) {
        Log.i(TAG, "Permission to record denied");

        if (ActivityCompat.shouldShowRequestPermissionRationale(this,
                Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
            AlertDialog.Builder builder = new AlertDialog.Builder(this);
            builder.setMessage("Permission to access the SD-CARD is required for this app to Download PDF.")
                    .setTitle("Permission required");

            builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {

                public void onClick(DialogInterface dialog, int id) {
                    Log.i(TAG, "Clicked");
                    makeRequest();
                }
            });

            AlertDialog dialog = builder.create();
            dialog.show();

        } else {
            makeRequest();
        }
    }

    protected void makeRequest() {
        ActivityCompat.requestPermissions(this,
                new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
                REQUEST_WRITE_STORAGE);
    }

第 3 步为请求添加覆盖方法

 @Override
public void onRequestPermissionsResult(int requestCode,
                                       String permissions[], int[] grantResults) {
    switch (requestCode) {
        case REQUEST_WRITE_STORAGE: {

            if (grantResults.length == 0
                    || grantResults[0] !=
                    PackageManager.PERMISSION_GRANTED) {

                Log.i(TAG, "Permission has been denied by user");

            } else {

                Log.i(TAG, "Permission has been granted by user");

            }
            return;
        }
    }
}

注意:不要忘记在清单文件中添加权限

下面的最佳示例,包含多个权限以及涵盖所有场景

我添加了 cmets,以便您可以轻松理解。

import android.Manifest;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.provider.Settings;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import com.production.hometech.busycoder.R;

import java.util.ArrayList;

public class PermissionInActivity extends AppCompatActivity implements View.OnClickListener {

    private static final int REQUEST_PERMISSION_SETTING = 99;
    private Button bt_camera;
    private static final String[] PARAMS_TAKE_PHOTO = {
            Manifest.permission.CAMERA,
            Manifest.permission.WRITE_EXTERNAL_STORAGE
    };
    private static final int RESULT_PARAMS_TAKE_PHOTO = 11;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_permission_in);

        bt_camera = (Button) findViewById(R.id.bt_camera);

        bt_camera.setOnClickListener(this);

    }

    @Override
    public void onClick(View view) {

        switch (view.getId()) {

            case R.id.bt_camera:

                takePhoto();

                break;

        }
    }


    /**
     * shouldShowRequestPermissionRationale() = This will return true if the user had previously declined to grant you permission
     * NOTE :  that ActivityCompat also has a backwards-compatible implementation of
     * shouldShowRequestPermissionRationale(), so you can avoid your own API level
     * checks.
     * <p>
     * shouldShowRequestPermissionRationale() =  returns false if the user declined the permission and checked the checkbox to ask you to stop pestering the
     * user.
     * <p>
     * requestPermissions() = request for the permisssiion
     */
    private void takePhoto() {

        if (canTakePhoto()) {

            Toast.makeText(this, "You can take PHOTO", Toast.LENGTH_SHORT).show();

        } else if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA) || ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {

            Toast.makeText(this, "You should give permission", Toast.LENGTH_SHORT).show();
            ActivityCompat.requestPermissions(this, netPermisssion(PARAMS_TAKE_PHOTO), RESULT_PARAMS_TAKE_PHOTO);

        } else {
            ActivityCompat.requestPermissions(this, netPermisssion(PARAMS_TAKE_PHOTO), RESULT_PARAMS_TAKE_PHOTO);
        }

    }

    //  This method return  permission denied String[] so we can request again
    private String[] netPermisssion(String[] wantedPermissions) {
        ArrayList<String> result = new ArrayList<>();

        for (String permission : wantedPermissions) {
            if (!hasPermission(permission)) {
                result.add(permission);
            }
        }

        return (result.toArray(new String[result.size()]));

    }

    private boolean canTakePhoto() {
        return (hasPermission(Manifest.permission.CAMERA) && hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE));
    }

    /**
     * checkSelfPermission() = you can check if you have been granted a runtime permission or not
     * ex = ContextCompat.checkSelfPermission(this,permissionString)== PackageManager.PERMISSION_GRANTED
     * <p>
     * ContextCompat offers a backwards-compatible implementation of checkSelfPermission(), ActivityCompat offers a backwards-compatible
     * implementation of requestPermissions() that you can use.
     *
     * @param permissionString
     * @return
     */
    private boolean hasPermission(String permissionString) {
        return (ContextCompat.checkSelfPermission(this, permissionString) == PackageManager.PERMISSION_GRANTED);
    }

    /**
     * requestPermissions() action goes to onRequestPermissionsResult() whether user can GARNT or DENIED those permisssions
     *
     * @param requestCode
     * @param permissions
     * @param grantResults
     */
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

        if (requestCode == RESULT_PARAMS_TAKE_PHOTO) {

            if (canTakePhoto()) {

                Toast.makeText(this, "You can take picture", Toast.LENGTH_SHORT).show();

            } else if (!(ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA) || ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE))) {


                final AlertDialog.Builder settingDialog = new AlertDialog.Builder(PermissionInActivity.this);
                settingDialog.setTitle("Permissioin");
                settingDialog.setMessage("Now you need to enable permisssion from the setting because without permission this app won't run properly \n\n  goto -> setting -> appInfo");
                settingDialog.setCancelable(false);

                settingDialog.setPositiveButton("Setting", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {

                        dialogInterface.cancel();

                        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
                        Uri uri = Uri.fromParts("package", getPackageName(), null);
                        intent.setData(uri);
                        startActivityForResult(intent, REQUEST_PERMISSION_SETTING);
                        Toast.makeText(getBaseContext(), "Go to Permissions to Grant all permission ENABLE", Toast.LENGTH_LONG).show();

                    }
                });
                settingDialog.show();

                Toast.makeText(this, "You need to grant permission from setting", Toast.LENGTH_SHORT).show();

            }

        }

    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        if (requestCode == REQUEST_PERMISSION_SETTING) {

            if (canTakePhoto()) {

                Toast.makeText(this, "You can take PHOTO", Toast.LENGTH_SHORT).show();

            }

        }

    }


}

配置更改的特殊情况

当我们的权限对话框在前台时,用户可能会旋转设备或以其他方式触发配置更改。由于我们的 Activity 在该对话框后面仍然可见,因此我们会被销毁并重新创建……但我们不想再次重新引发权限对话框。

这就是为什么我们有一个名为 isInPermission 的布尔值来跟踪是否 我们正在请求权限。我们坚持这个价值 onSaveInstanceState():

@Override
protected void onSaveInstanceState(Bundle outState) {
  super.onSaveInstanceState(outState);
  outState.putBoolean(STATE_IN_PERMISSION, isInPermission);
}

我们在onCreate() 中恢复它。如果我们不拥有所有所需的权限,但 isInPermission 为真,我们跳过请求权限,因为我们在 已经在这样做了。

【讨论】:

  • 非活动状态下如何操作?
  • 这让@Arpit 很头疼。我已将课程更改为活动并删除了 setcontentview,因为我需要在同一个课程中执行异步任务。感谢搜索
  • @Prabs 惠康
  • @Arpit Patel 当用户旋转设备时,如何防止应用在对话框中崩溃?我设法通过在我的 Activity 的 onPause() 中将其关闭来防止我的 AlertDialog 崩溃,但是如果您现在旋转设备,如果显示 Google 的权限对话框,则应用程序仍然崩溃。
【解决方案2】:

也许您不能在项目中使用生成代码中的清单类。因此,您可以使用来自 android sdk“android.Manifest.permission.WRITE_EXTERNAL_STORAGE”的清单类。但在 Marsmallow 版本中,必须授予 2 个权限,即存储类别中的 WRITE 和 READ EXTERNAL STORAGE。查看我的程序,我的程序将请求权限,直到用户选择是,并在授予权限后执行某些操作。

            if (Build.VERSION.SDK_INT >= 23) {
                if (ContextCompat.checkSelfPermission(LoginActivity.this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
                        != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(LoginActivity.this, android.Manifest.permission.READ_EXTERNAL_STORAGE)
                        != PackageManager.PERMISSION_GRANTED) {
                    ActivityCompat.requestPermissions(LoginActivity.this,
                            new String[]{android.Manifest.permission.WRITE_EXTERNAL_STORAGE, android.Manifest.permission.READ_EXTERNAL_STORAGE},
                            1);
                } else {
                    //do something
                }
            } else {
                    //do something
            }

【讨论】:

    【解决方案3】:

    Android Documentation on Manifest.permission.Manifest.permission.WRITE_EXTERNAL_STORAGE states:

    从 API 级别 19 开始,读取/写入 getExternalFilesDir(String) 和 getExternalCacheDir() 返回的应用程序特定目录中的文件不需要此权限。


    认为这意味着您不必为 WRITE_EXTERNAL_STORAGE 权限的运行时实现编写代码,除非应用正在写入非特定目录到您的应用。

    您可以根据权限在清单中定义最大 sdk 版本,例如:

     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="19" />
    

    还要确保在 build.graddle 而不是清单中更改目标 SDK,gradle 设置将始终覆盖清单设置。

    android {
    compileSdkVersion 23
    buildToolsVersion '23.0.1'
    defaultConfig {
        minSdkVersion 17
        targetSdkVersion 22
    }
    

    【讨论】:

    • +1 帮了我很多,但它不应该读作:` `(而不是19),也就是说,如果您使用 getExternalFilesDir(String) 和 getExternalCacheDir(),则在 API 级别 19 之前,您将获得“旧方式”权限。
    【解决方案4】:

    我遇到了同样的问题。 Android中有两种权限:

    • 危险(访问联系人、写入外部存储...)
    • 正常

    普通权限由 Android 自动批准,而危险权限需要 Android 用户批准。

    这是在 Android 6.0 中获取危险权限的策略

    1. 检查您是否获得了权限
    2. 如果您的应用已获得权限,请继续并正常执行。
    3. 如果您的应用还没有权限,请请求用户批准
    4. 在 onRequestPermissionsResult 中听取用户批准

    这是我的情况:我需要写入外部存储。

    首先,我检查我是否有权限:

    ...
    private static final int REQUEST_WRITE_STORAGE = 112;
    ...
    boolean hasPermission = (ContextCompat.checkSelfPermission(activity,
                Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED);
    if (!hasPermission) {
        ActivityCompat.requestPermissions(parentActivity,
                    new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
                    REQUEST_WRITE_STORAGE);
    }
    

    然后检查用户是否认可:

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        switch (requestCode)
        {
            case REQUEST_WRITE_STORAGE: {
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED)
                {
                    //reload my activity with permission granted or use the features what required the permission
                } else
                {
                    Toast.makeText(parentActivity, "The app was not allowed to write to your storage. Hence, it cannot function properly. Please consider granting it this permission", Toast.LENGTH_LONG).show();
                }
            }
        }
    
    }
    

    您可以在此处阅读有关新权限模型的更多信息:https://developer.android.com/training/permissions/requesting.html

    【讨论】:

    • 对。我了解,如果我的应用程序针对 6.0.0 版本,则存在不同的安全协议。但是,让我们退后一步,再看看这个问题。我所做的只是让我的 Android One 手机升级到 6.0(就像我之前从 4.4 升级到 5.0,然后是 5.1 一样)。而且,繁荣,写入 SD 的应用程序停止工作? [请注意,这些应用程序不针对 API 23]
    • 您是否尝试过将 compileSdkVersion 和 targetSdkVersion 设置为低于 23?我已经做到了,我的应用程序在 android 6.0 上运行良好
    • 我正在将 Eclipse 的 sdk 升级到 API23 (Marshmallow),之后我计划将新的权限协议合并到我的应用程序中。但是,这并不能解决我手机的问题:升级到 6.0 后,一些已安装的应用程序停止运行。手机现在似乎受到迟到的开发人员(比如我)的摆布。坦率地说,这没有任何意义...... 6.0 中只需要进行一些“向后兼容性调整”。 [就目前而言,我的用户中有近 1% 已经升级到 6.0 ......我必须快速行动 :) ]
    • 又如何对 extSdCard(二级存储)做到这一点?
    • 请记住我们必须有 noHistory=false 才能接收回调。如果您没有收到回拨,也请参考this。我浪费了几个小时来弄清楚。
    【解决方案5】:

    Android 更改了权限在 Android 6.0 上的工作方式,这就是您出现错误的原因。您必须实际请求并检查用户是否授予使用权限。因此清单文件中的权限仅适用于 21 以下的 api。 检查此链接以获取有关如何在 api23 http://android-developers.blogspot.nl/2015/09/google-play-services-81-and-android-60.html?m=1 中请求权限的 sn-p

    代码:-

    If (ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE) !=
                    PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, STORAGE_PERMISSION_RC);
                return;
            }`
    
    
    ` @Override
        public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
            if (requestCode == STORAGE_PERMISSION_RC) {
                if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    //permission granted  start reading
                } else {
                    Toast.makeText(this, "No permission to read external storage.", Toast.LENGTH_SHORT).show();
                }
            }
        }
    }
    

    【讨论】:

    • 问题是,该应用程序不是针对 api 23。它只是在 6.0 上运行。不应该向后兼容吗?此外,使用 SD 卡的内置应用程序,如相机 (2.7.008),也因“没有可用的 SD 卡”而失败。如果我通过“设置->应用程序”查看“应用程序权限”,它会显示单个权限(例如相机、存储等)的新粒度,所有权限均正确授予。
    • 另一件事:当配置为“便携式存储”时,sd 卡图标在通知区域中仍然可见。为什么?
    • 安装了一个流行的相机应用程序,打开相机(100 万次下载)......它也无法保存照片。我开始怀疑 Marshmallow 是否需要 SD 卡中的特定规范(如果是,为什么它接受我尝试过的那些?)。
    • 好吧,我还没有测试过上面提到的应用程序。可能开发者没有实现请求权限的重组方式。我只是说明为什么以及如何完成。
    • 这对我不起作用....我的应用程序显示对话框,并接受用户回答(允许)。但是,除了/PathToExtSdCard/Android/data/myapp中的文件夹外,外置SD卡仍然无法写入应用程序。似乎允许或拒绝没有区别。有什么建议吗?
    【解决方案6】:

    没错。所以我终于找到了问题的根源:这是一次拙劣的就地 OTA 升级。

    在我的 Garmin Fenix 2 无法通过蓝牙连接并在谷歌上搜索“棉花糖升级问题”之后,我的怀疑更加强烈。无论如何,“恢复出厂设置”解决了这个问题。

    令人惊讶的是,重置并没有将手机恢复到原来的 Kitkat;相反,擦除过程选择了 OTA 下载的 6.0 升级包并运行它,导致(我猜)“更干净”的升级。

    当然,这意味着手机丢失了我安装的所有应用程序。但是,新安装的应用程序,包括我的应用程序,无需任何更改即可工作(即具有向后兼容性)。呼!

    【讨论】:

    • 因此,在将我的应用程序更新为目标 API23 并尝试为 6 实现权限处理的过程中,我更加困惑。由于我已经从 6 个用户那里获得了几百次下载(我目前的目标是 api21),我想知道我是否应该打扰,如果他们真的在使用我的应用程序就好了?这是一项真正的任务,试图解决 23 中的所有弃用问题,包括库代码,我担心我会从 SO 的各种示例中实现一些黑客攻击并导致用户问题,可能会导致差评。但是,我觉得我现在回到
    • 我的项目的构建目标是 20,并且事情似乎进展顺利,因为 (a) 该应用程序在我的 Android One 手机上运行良好,目前升级到 6.0 和 (b) 几百6 位用户实际上没有投诉。因此,我看不出有任何令人信服的理由来匆忙修改代码周。一切顺利。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-09-23
    • 1970-01-01
    • 2023-03-17
    • 2011-07-30
    相关资源
    最近更新 更多