web-dev-qa-db-ja.com

Androidの外部SDカードに書き込む汎用的な方法

私のアプリケーションでは、デバイスストレージに大量の画像を保存する必要があります。そのようなファイルはデバイスのストレージを満たす傾向があるため、ユーザーが宛先フォルダーとして外部SDカードを選択できるようにします。

Androidでは外部SDカードへの書き込みが許可されていないことをどこでも読みました。SDカードとは、外部およびマウント可能なSDカードを意味し、外部ストレージではなくアプリケーションは、すべてのAndroidバージョンで外部SDに書き込むことができます。

異なるAPIレベル(Pre-KitKat、KitKat、Lollipop +)で外部SDカードへの読み取り/書き込みアクセスを許可するより良い方法は何ですか?

更新1

Doomknightの回答から方法1を試しましたが、役に立ちませんでした。ご覧のとおり、SDに書き込む前に、実行時にパーミッションをチェックしています。

HashSet<String> extDirs = getStorageDirectories();
for(String dir: extDirs) {
    Log.e("SD",dir);
    File f = new File(new File(dir),"TEST.TXT");
    try {
        if(ActivityCompat.checkSelfPermission(this,Manifest.permission.WRITE_EXTERNAL_STORAGE)==PackageManager.PERMISSION_GRANTED) {
            f.createNewFile();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

しかし、アクセスエラーが発生し、HTC10とShield K1の2つの異なるデバイスで試しました。

10-22 14:52:57.329 30280-30280/? E/SD: /mnt/media_rw/F38E-14F8
10-22 14:52:57.329 30280-30280/? W/System.err: Java.io.IOException: open failed: EACCES (Permission denied)
10-22 14:52:57.329 30280-30280/? W/System.err:     at Java.io.File.createNewFile(File.Java:939)
10-22 14:52:57.329 30280-30280/? W/System.err:     at com.myapp.activities.TestActivity.onResume(TestActivity.Java:167)
10-22 14:52:57.329 30280-30280/? W/System.err:     at Android.app.Instrumentation.callActivityOnResume(Instrumentation.Java:1326)
10-22 14:52:57.330 30280-30280/? W/System.err:     at Android.app.Activity.performResume(Activity.Java:6338)
10-22 14:52:57.330 30280-30280/? W/System.err:     at Android.app.ActivityThread.performResumeActivity(ActivityThread.Java:3336)
10-22 14:52:57.330 30280-30280/? W/System.err:     at Android.app.ActivityThread.handleResumeActivity(ActivityThread.Java:3384)
10-22 14:52:57.330 30280-30280/? W/System.err:     at Android.app.ActivityThread.handleLaunchActivity(ActivityThread.Java:2574)
10-22 14:52:57.330 30280-30280/? W/System.err:     at Android.app.ActivityThread.access$900(ActivityThread.Java:150)
10-22 14:52:57.330 30280-30280/? W/System.err:     at Android.app.ActivityThread$H.handleMessage(ActivityThread.Java:1399)
10-22 14:52:57.330 30280-30280/? W/System.err:     at Android.os.Handler.dispatchMessage(Handler.Java:102)
10-22 14:52:57.330 30280-30280/? W/System.err:     at Android.os.Looper.loop(Looper.Java:168)
10-22 14:52:57.330 30280-30280/? W/System.err:     at Android.app.ActivityThread.main(ActivityThread.Java:5885)
10-22 14:52:57.330 30280-30280/? W/System.err:     at Java.lang.reflect.Method.invoke(Native Method)
10-22 14:52:57.330 30280-30280/? W/System.err:     at com.Android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.Java:819)
10-22 14:52:57.330 30280-30280/? W/System.err:     at com.Android.internal.os.ZygoteInit.main(ZygoteInit.Java:709)
10-22 14:52:57.330 30280-30280/? W/System.err: Caused by: Android.system.ErrnoException: open failed: EACCES (Permission denied)
10-22 14:52:57.330 30280-30280/? W/System.err:     at libcore.io.Posix.open(Native Method)
10-22 14:52:57.330 30280-30280/? W/System.err:     at libcore.io.BlockGuardOs.open(BlockGuardOs.Java:186)
10-22 14:52:57.330 30280-30280/? W/System.err:     at Java.io.File.createNewFile(File.Java:932)
10-22 14:52:57.330 30280-30280/? W/System.err:  ... 14 more
64
Vektor88

これを実現する方法は2つあると思います。

METHOD 1:(doesNOT6.0以降では動作しますが、許可の変更による)

私はこの方法を何年もの間、多くのデバイスバージョンで問題なく使用しています。 クレジットは、私が書いたのではないため、元のソースによるものです。

文字列ディレクトリの場所のリストで、すべてのマウントされたメディア(リアルSDカードを含む)を返します。リストを使用して、ユーザーに保存先などを尋ねることができます。

次の方法で呼び出すことができます。

 HashSet<String> extDirs = getStorageDirectories();

方法:

/**
 * Returns all the possible SDCard directories
 */
public static HashSet<String> getStorageDirectories() {
    final HashSet<String> out = new HashSet<String>();
    String reg = "(?i).*vold.*(vfat|ntfs|exfat|fat32|ext3|ext4).*rw.*";
    String s = "";
    try {
        final Process process = new ProcessBuilder().command("mount")
                .redirectErrorStream(true).start();
        process.waitFor();
        final InputStream is = process.getInputStream();
        final byte[] buffer = new byte[1024];
        while (is.read(buffer) != -1) {
            s = s + new String(buffer);
        }
        is.close();
    } catch (final Exception e) {
        e.printStackTrace();
    }

    // parse output
    final String[] lines = s.split("\n");
    for (String line : lines) {
        if (!line.toLowerCase().contains("asec")) {
            if (line.matches(reg)) {
                String[] parts = line.split(" ");
                for (String part : parts) {
                    if (part.startsWith("/"))
                        if (!part.toLowerCase().contains("vold"))
                            out.add(part);
                }
            }
        }
    }
    return out;
}

メソッド2:

V4サポートライブラリを使用する

import Android.support.v4.content.ContextCompat;

以下を呼び出して、ストレージのFileロケーションのリストを取得してください。

 File[] list = ContextCompat.getExternalFilesDirs(myContext, null);

ただし、場所は使用方法が異なります。

アプリケーションが所有する永続ファイルを配置できるすべての外部ストレージデバイス上のアプリケーション固有のディレクトリへの絶対パスを返します。これらのファイルはアプリケーションの内部にあり、通常はメディアとしてユーザーに表示されません。

ここで返される外部ストレージデバイスは、エミュレートされた外部ストレージと物理的なメディアスロット(バッテリーコンパートメントのSDカードなど)の両方を含む、デバイスの永続的な部分と見なされます。返されるパスには、USBフラッシュドライブなどの一時的なデバイスは含まれません。

アプリケーションは、返されたデバイスの一部またはすべてにデータを保存できます。たとえば、アプリは、利用可能なスペースが最も大きいデバイスに大きなファイルを保存することを選択できます

ContextCompatの詳細

それらはアプリ固有のファイルのようなものです。他のアプリから隠されています。

9
Doomsknight

他のすべてのニースの回答に加えて、この質問にもう少し追加して、読者に幅広い報道を提供することができます。ここでの答えでは、2つのカウント可能なリソースを使用して外部ストレージを提示します。

最初のリソースは、Android Programming、The Big Nerd Ranch Guide 2nd edition、第16章、294ページからです。

この本は、基本および外部のファイルとディレクトリの方法を説明しています。私はあなたの質問に関連する可能性のあるものの履歴書を作成しようとします。

本の次の部分:

外部ストレージ

写真は画面上の場所以上のものを必要とします。フルサイズの画像は大きすぎてSQLiteデータベース内に収まりきれず、Intentにはなりません。彼らはあなたのデバイスのファイルシステムに住む場所を必要とします。通常、プライベートストレージに保存します。プライベートストレージを使用してSQLiteデータベースを保存したことを思い出してください。 Context.getFileStreamPath(String)Context.getFilesDir()などのメソッドを使用すると、通常のファイルでも同じことができます(SQLiteデータベースが格納されているデータベースサブフォルダーに隣接するサブフォルダーに格納されます)

Contextの基本的なファイルおよびディレクトリメソッド

| Method                                                                                |
|---------------------------------------------------------------------------------------|
|File getFilesDir()                                                                      |
| - Returns a handle to the directory for private application files.                    |
|                                                                                       |
|FileInputStream openFileInput(String name)                                             |
| - Opens an existing file for input (relative to the files directory).                 |
|                                                                                       |
|FileOutputStream openFileOutput(String name, int mode)                                 |
| - Opens a file for output, possibly creating it (relative to the files directory).    |
|                                                                                       |
|File getDir(String name, int mode)                                                     |
| - Gets (and possibly creates) a subdirectory within the files directory.              |
|                                                                                       |
|String[] fileList()                                                                    |
| - Gets a list of file names in the main files directory, such as for use with         |
|   openFileInput(String).                                                              |
|                                                                                       |
|File getCacheDir()                                                                     |
| - Returns a handle to a directory you can use specifically for storing cache files.   |
|   You should take care to keep this directory tidy and use as little space as possible|

現在のアプリケーションのみが使用する必要があるファイルを保存している場合、これらのメソッドはまさにあなたが必要とするものです。

一方、これらのファイルに書き込むために別のアプリケーションが必要な場合は運が悪い:Context.MODE_WORLD_READABLEフラグがopenFileOutput(String, int)に渡すことができますが、これは非推奨であり、新しいデバイスへの影響は完全には信頼できません。ファイルを保存して他のアプリと共有したり、他のアプリからファイルを受信する場合(保存された画像などのファイル)、代わりに外部ストレージに保存する必要があります。

外部ストレージには、プライマリとその他すべての2種類があります。すべてのAndroidデバイスには、外部ストレージ用の少なくとも1つの場所があります。それは、Environment.getExternalStorageDirectory()によって返されるフォルダー内にあるプライマリの場所です。これはSDカードの場合もありますが、最近ではデバイス自体に組み込まれることが多くなっています。一部のデバイスには追加の外部ストレージがあります。それは「その他すべて」に該当します。

コンテキストは、外部ストレージを取得するためのかなりの数のメソッドも提供します。これらのメソッドは、プライマリ外部ストレージを取得する簡単な方法と、他のすべてを取得するためのちょっとした簡単な方法を提供します。これらのメソッドはすべて、公開されている場所にもファイルを保存するため、注意が必要です。

Contextの外部ファイルおよびディレクトリメソッド

| Method                                                                                |
| --------------------------------------------------------------------------------------|
|File getExternalCacheDir()                                                             |
| - Returns a handle to a cache folder in primary external storage. Treat it like you do|
|   getCacheDir(), except a little more carefully. Android is even less likely to clean |
|   up this folder than the private storage one.                                        |
|                                                                                       |
|File[] getExternalCacheDirs()                                                          |
| - Returns cache folders for multiple external storage locations.                      |
|                                                                                       |
|File getExternalFilesDir(String)                                                       |
| - Returns a handle to a folder on primary external storage in which to store regular  |
|   files. If you pass in a type String, you can access a specific subfolder dedicated  |
|   to a particular type of content. Type constants are defined in Environment, where   |
|   they are prefixed with DIRECTORY_.                                                  |
|   For example, pictures go in Environment.DIRECTORY_PICTURES.                         |
|                                                                                       |
|File[] getExternalFilesDirs(String)                                                    |
| - Same as getExternalFilesDir(String), but returns all possible file folders for the  |
|   given type.                                                                         |
|                                                                                       |
|File[] getExternalMediaDirs()                                                          |
| - Returns handles to all the external folders Android makes available for storing     |
|   media – pictures, movies, and music. What makes this different from calling         |
|   getExternalFilesDir(Environment.DIRECTORY_PICTURES) is that the media scanner       |
|   automatically scans this folder. The media scanner makes files available to         |
|   applications that play music, or browse movies and photos, so anything that you     |
|   put in a folder returned by getExternalMediaDirs() will automatically appear in     |
|   those apps.                                                                         |

技術的には、一部のデバイスは外部ストレージにリムーバブルSDカードを使用しているため、上記の外部フォルダーは使用できない場合があります。実際、これはほとんど問題になりません。ほとんどすべての最新のデバイスには、「外部」ストレージ用の取り外し不可能な内部ストレージがあるためです。したがって、それを説明するために極端な長さに行く価値はありません。しかし、すぐに行う可能性を防ぐために、簡単なコードを含めることをお勧めします。

外部ストレージ許可

一般に、外部ストレージへの書き込みまたは読み取りの許可が必要です。権限は、<uses-permission>タグを使用してマニフェストに配置する既知の文字列値です。彼らはAndroidにAndroidに許可を求めてほしいことをしたいことを伝えます。

ここで、Androidは、何らかの説明責任を実施したいので、許可を求めることを期待しています。外部ストレージにアクセスする必要があることをAndroidに伝えると、Androidは、ユーザーがアプリケーションをインストールしようとするときにこれを行うことの1つであることをユーザーに伝えます。そうすれば、あなたが彼らのSDカードに物事を保存し始めるとき誰も驚かない。

Android 4.4 KitKatでは、この制限が緩和されました。 Context.getExternalFilesDir(String)はアプリ固有のフォルダーを返すため、そこに存在するファイルを読み書きできるようにしたいのは理にかなっています。したがって、Android 4.4(API 19)以降では、このフォルダーにこのアクセス許可は必要ありません。 (ただし、他の種類の外部ストレージにはまだ必要です。)

マニフェストに外部ストレージの読み取り許可を要求する行を追加します。ただし、APIリスト16.5のみ外部ストレージ許可の要求(AndroidManifest.xml

<manifest xmlns:Android="http://schemas.Android.com/apk/res/Android"
         package="com.bignerdranch.Android.criminalintent" >
<uses-permission Android:name="Android.permission.READ_EXTERNAL_STORAGE"
         Android:maxSdkVersion="18" />

MaxSdkVersion属性により、API 19より古いAndroidのバージョン、Android KitKatでのみアプリがこの許可を要求するようになります。外部ストレージの読み取りのみを要求していることに注意してください。 WRITE_EXTERNAL_STORAGEパーミッションもありますが、必要ありません。外部ストレージに何かを書き込むことはありません:カメラアプリがあなたのためにそれを行います

2番目のリソースはこれです link すべてを読み取りますが、External Storageセクションを使用してジャンプすることもできます。

参照:

その他の読み物:

免責事項:この情報は、Android Programming:The Big Nerd Ranch Guide from authors from著者からの許可を得たものです。この本の詳細またはコピーの購入については、bignerdranch.comをご覧ください。

ちょうど別の答え。ここに投稿されたDoomknightの回答がAndroid 4.4以下で行う最善の方法であると信じているため、この回答には5.0+のみが表示されます。

これは元々ここに投稿されています( AndroidでSDカードのサイズを取得する方法はありますか? )Android 5.0+で外部SDカードのサイズを取得します

外部SDカードをFileとして取得するには:

public File getExternalSdCard() {
    File externalStorage = null;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Lollipop) {
        File storage = new File("/storage");

        if(storage.exists()) {
            File[] files = storage.listFiles();

            for (File file : files) {
                if (file.exists()) {
                    try {
                        if (Environment.isExternalStorageRemovable(file)) {
                            externalStorage = file;
                            break;
                        }
                    } catch (Exception e) {
                        Log.e("TAG", e.toString());
                    }
                }
            }
        }
    } else {
        // do one of many old methods
        // I believe Doomsknight's method is the best option here
    }

    return externalStorage;
}

注:「最初の」外部SDカードのみを取得しますが、変更してFileの代わりにArrayList<File>を返し、最初のSDカードが見つかった後にbreakを呼び出す代わりにループを続行できます。

このトピックは少し古いですが、解決策を探していましたが、いくつかの調査の後、以下のコードを使用して、利用可能な「外部」マウントポイントのリストを取得しました。

基本的に、使用可能なマウントポイントを読み取り、無効なマウントポイントを除外し、残りがアクセス可能かどうかをテストし、すべての条件が満たされた場合に追加します。

もちろん、コードを呼び出す前に、必要な権限を付与する必要があります。

// Notice: FileSystemDevice is just my own wrapper class. Feel free to replace it with your own. 

private List<FileSystemDevice> getDevices() {

    List<FileSystemDevice> devices = new ArrayList<>();

    // Add default external storage if available.
    File sdCardFromSystem = null;
    switch(Environment.getExternalStorageState()) {
        case Environment.MEDIA_MOUNTED:
        case Environment.MEDIA_MOUNTED_READ_ONLY:
        case Environment.MEDIA_SHARED:
            sdCardFromSystem = Environment.getExternalStorageDirectory();
            break;
    }

    if (sdCardFromSystem != null) {
        devices.add(new FileSystemDevice(sdCardFromSystem));
    }

    // Read /proc/mounts and add all mount points that are available
    // and are not "special". Also, check if the default external storage
    // is not contained inside the mount point. 
    try {
        FileInputStream fs = new FileInputStream("/proc/mounts");
        String mounts = IOUtils.toString(fs, "UTF-8");
        for(String line : mounts.split("\n")) {
            String[] parts = line.split(" ");

            // parts[0] - mount type
            // parts[1] - mount point
            if (parts.length > 1) {
                try {

                    // Skip "special" mount points and mount points that can be accessed
                    // directly by Android's functions. 
                    if (parts[0].equals("proc")) { continue; }
                    if (parts[0].equals("rootfs")) { continue; }
                    if (parts[0].equals("devpts")) { continue; }
                    if (parts[0].equals("none")) { continue; }
                    if (parts[0].equals("sysfs")) { continue; }
                    if (parts[0].equals("selinuxfs")) { continue; }
                    if (parts[0].equals("debugfs")) { continue; }
                    if (parts[0].equals("tmpfs")) { continue; }
                    if (parts[1].equals(Environment.getRootDirectory().getAbsolutePath())) { continue; }
                    if (parts[1].equals(Environment.getDataDirectory().getAbsolutePath())) { continue; }
                    if (parts[1].equals(Environment.getExternalStorageDirectory().getAbsolutePath())) { continue; }

                    // Verify that the mount point is accessible by listing its content. 
                    File file = new File(parts[1]);
                    if (file.listFiles() != null) {
                        try {

                            // Get canonical path for case it's just symlink to another mount point.
                            String devPath = file.getCanonicalPath();

                            for(FileSystemDevice device : devices) {

                                if (!devices.contains(devPath)) {                        
                                    devices.add(new FileSystemDevice(new File(devPath)));
                                }

                            }
                        } catch (Exception e) {
                            // Silently skip the exception as it can only occur if the mount point is not valid. 
                            e.printStackTrace();
                        }
                    }
                } catch (Exception e) {
                    // Silently skip the exception as it can only occur if the mount point is not valid. 
                    e.printStackTrace();
                }
            }
        }

        fs.close();
    } catch (FileNotFoundException e) {
        // Silently skip the exception as it can only occur if the /proc/mounts file is unavailable. 
        // Possibly, another detection method can be called here.
        e.printStackTrace();
    } catch (IOException e) {
        // Silently skip the exception as it can only occur if the /proc/mounts file is unavailable.
        // Possibly, another detection method can be called here.
        e.printStackTrace();            
    }

    return devices;
}
0
Václav Hodek