Skip to content

Most visited

Recently visited

navigation

ストレージ アクセス フレームワーク

Android 4.4 (API レベル 19)は、ストレージ アクセス フレームワーク(SAF)を採用しています。SAF を利用することで、ユーザーは設定したドキュメント ストレージ プロバイダ全体から簡単にドキュメント、画像、その他のファイルを参照して開くことができます。 標準の使いやすい UI により、アプリやプロバイダを通じて一貫性のある方法でファイルを参照したり、最近使用したファイルにアクセスしたりできます。

サービスをカプセル化する DocumentsProvider を実装することで、クラウドやローカル ストレージ サービスをエコシステムに参加させることができます。 プロバイダのドキュメントへのアクセスが必要なクライアントも、数行のコードだけで SAF と統合できます。

SAF には次の項目が含まれます。

SAF は次のような機能も提供します。

概要

SAF は、DocumentsProvider クラスのサブクラスであるコンテンツ プロバイダを中心に展開します。 ドキュメント プロバイダでは、データは下図のように従来のファイル階層で構造化されます。

データモデル

図 1. ドキュメント プロバイダのデータモデル。ルートポイントが 1 つのドキュメントを指し、そこからツリー全体が広がります。

注:

コントロール フロー

前述のように、ドキュメント プロバイダのデータモデルは従来のファイル階層を基本とします。 ただし、DocumentsProvider API でアクセスできる場合は、任意の方法でデータを物理的に格納できます。 たとえば、データにタグベースのクラウド ストレージを使用できます。

図 2 の例は、写真アプリが格納されたデータに SAF を使ってアクセスする様子を表しています。

アプリ

図 2. ストレージ アクセス フレームワークのフロー

注:

図 3 は、Google Drive アカウントを選択したユーザーが画像を検索するピッカーを表しています。

ピッカー

図 3. ピッカー

ユーザーが Google Drive を選択すると、図 4 のように画像が表示されます。この時点から、ユーザーはプロバイダとクライアント アプリがサポートするすべての方法で、画像を操作できるようになります。

ピッカー

図 4. 画像

クライアント アプリを作成する

Android 4.3 以前では、別のアプリからファイルを取得する場合、ACTION_PICKACTION_GET_CONTENT といったインテントを呼び出す必要があります。 その後ユーザーは、ファイルを選択するためのアプリを 1 つ選びます。選択されたアプリはユーザーが使用可能なファイルを参照して選択するためのユーザー インターフェースを提供する必要があります。

Android 4.4 以降であれば、ACTION_OPEN_DOCUMENT インテントを使用するという選択肢もあります。このインテントではシステム制御のピッカー UI が表示され、ユーザーはそこから他のアプリで利用可能なすべてのファイル参照できます。 ユーザーは、この 1 つの UI から、サポートされるすべてのアプリのファイルを選択できます。

ACTION_OPEN_DOCUMENT は、ACTION_GET_CONTENT との置き換えを意図したものではありません。どちらを使用するかは、アプリのニーズによって異なります。

ここでは、ACTION_OPEN_DOCUMENTACTION_CREATE_DOCUMENT インテントに基づいたクライアント アプリの作成方法について説明します。

次のスニペットでは ACTION_OPEN_DOCUMENT を使って、画像ファイルがあるドキュメント プロバイダを検索します。

private static final int READ_REQUEST_CODE = 42;
...
/**
 * Fires an intent to spin up the "file chooser" UI and select an image.
 */
public void performFileSearch() {

    // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's file
    // browser.
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);

    // Filter to only show results that can be "opened", such as a
    // file (as opposed to a list of contacts or timezones)
    intent.addCategory(Intent.CATEGORY_OPENABLE);

    // Filter to show only images, using the image MIME data type.
    // If one wanted to search for ogg vorbis files, the type would be "audio/ogg".
    // To search for all documents available via installed storage providers,
    // it would be "*/*".
    intent.setType("image/*");

    startActivityForResult(intent, READ_REQUEST_CODE);
}

注:

プロセスの結果

ユーザーがピッカーでドキュメントを選択すると、onActivityResult() が呼び出されます。選択したドキュメントを指す URI は resultData パラメータに含まれます。 getData() を使って URI を抽出します。URI を抽出したら、その URI を使ってユーザーが希望するドキュメントを取得できます。 次に例を示します。

@Override
public void onActivityResult(int requestCode, int resultCode,
        Intent resultData) {

    // The ACTION_OPEN_DOCUMENT intent was sent with the request code
    // READ_REQUEST_CODE. If the request code seen here doesn't match, it's the
    // response to some other intent, and the code below shouldn't run at all.

    if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
        // The document selected by the user won't be returned in the intent.
        // Instead, a URI to that document will be contained in the return intent
        // provided to this method as a parameter.
        // Pull that URI using resultData.getData().
        Uri uri = null;
        if (resultData != null) {
            uri = resultData.getData();
            Log.i(TAG, "Uri: " + uri.toString());
            showImage(uri);
        }
    }
}

ドキュメント メタデータを確認する

ドキュメントのURI を取得したら、そのメタデータにアクセスできます。次のスニペットは、URI で指定したドキュメントのメタデータを取得して、ログに記録します。

public void dumpImageMetaData(Uri uri) {

    // The query, since it only applies to a single document, will only return
    // one row. There's no need to filter, sort, or select fields, since we want
    // all fields for one document.
    Cursor cursor = getActivity().getContentResolver()
            .query(uri, null, null, null, null, null);

    try {
    // moveToFirst() returns false if the cursor has 0 rows.  Very handy for
    // "if there's anything to look at, look at it" conditionals.
        if (cursor != null && cursor.moveToFirst()) {

            // Note it's called "Display Name".  This is
            // provider-specific, and might not necessarily be the file name.
            String displayName = cursor.getString(
                    cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
            Log.i(TAG, "Display Name: " + displayName);

            int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
            // If the size is unknown, the value stored is null.  But since an
            // int can't be null in Java, the behavior is implementation-specific,
            // which is just a fancy term for "unpredictable".  So as
            // a rule, check if it's null before assigning to an int.  This will
            // happen often:  The storage API allows for remote files, whose
            // size might not be locally known.
            String size = null;
            if (!cursor.isNull(sizeIndex)) {
                // Technically the column stores an int, but cursor.getString()
                // will do the conversion automatically.
                size = cursor.getString(sizeIndex);
            } else {
                size = "Unknown";
            }
            Log.i(TAG, "Size: " + size);
        }
    } finally {
        cursor.close();
    }
}

ドキュメントを開く

ドキュメントのURI を取得したら、そのドキュメントを開いてさまざまな操作を行うことができます。

ビットマップ

Bitmap を開く方法の例を次に示します。

private Bitmap getBitmapFromUri(Uri uri) throws IOException {
    ParcelFileDescriptor parcelFileDescriptor =
            getContentResolver().openFileDescriptor(uri, "r");
    FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
    Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor);
    parcelFileDescriptor.close();
    return image;
}

この操作を UI スレッドで実行しないでください。AsyncTask を使ってバックグラウンドで実行します。 ビットマップを開いたら、ImageView で表示できます。

InputStream を取得する

URI から InputStream を取得する方法の例を次に示します。このスニペットでは、ファイルの行が文字列に読み込まれます。

private String readTextFromUri(Uri uri) throws IOException {
    InputStream inputStream = getContentResolver().openInputStream(uri);
    BufferedReader reader = new BufferedReader(new InputStreamReader(
            inputStream));
    StringBuilder stringBuilder = new StringBuilder();
    String line;
    while ((line = reader.readLine()) != null) {
        stringBuilder.append(line);
    }
    fileInputStream.close();
    parcelFileDescriptor.close();
    return stringBuilder.toString();
}

新しいドキュメントを作成する

ACTION_CREATE_DOCUMENT インテントを使用すると、アプリでドキュメント プロバイダに新しいドキュメントを作成できます。 ファイルを作成するには、インテントに MIME タイプとファイル名を渡し、一意の要求コードを使ってインテントを起動します。 後は自分で設定します。

// Here are some examples of how you might call this method.
// The first parameter is the MIME type, and the second parameter is the name
// of the file you are creating:
//
// createFile("text/plain", "foobar.txt");
// createFile("image/png", "mypicture.png");

// Unique request code.
private static final int WRITE_REQUEST_CODE = 43;
...
private void createFile(String mimeType, String fileName) {
    Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);

    // Filter to only show results that can be "opened", such as
    // a file (as opposed to a list of contacts or timezones).
    intent.addCategory(Intent.CATEGORY_OPENABLE);

    // Create a file with the requested MIME type.
    intent.setType(mimeType);
    intent.putExtra(Intent.EXTRA_TITLE, fileName);
    startActivityForResult(intent, WRITE_REQUEST_CODE);
}

新しいドキュメントを作成したら、onActivityResult() で URI を取得して、ドキュメントに書き込み操作を実行できます。

ドキュメントを削除する

ドキュメントの URI と、SUPPORTS_DELETE を含むドキュメントの Document.COLUMN_FLAGS を取得すると、ドキュメントを削除できます。 次に例を示します。

DocumentsContract.deleteDocument(getContentResolver(), uri);

ドキュメントを編集する

SAF を使用するとテキスト ドキュメントを直接編集できます。このスニペットは ACTION_OPEN_DOCUMENT インテントを起動し、カテゴリ CATEGORY_OPENABLE を使って、開くことができるドキュメントのみを表示します。 次のように、テキスト ファイルのみを表示するようにフィルタリングします。

private static final int EDIT_REQUEST_CODE = 44;
/**
 * Open a file for writing and append some text to it.
 */
 private void editDocument() {
    // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's
    // file browser.
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);

    // Filter to only show results that can be "opened", such as a
    // file (as opposed to a list of contacts or timezones).
    intent.addCategory(Intent.CATEGORY_OPENABLE);

    // Filter to show only text files.
    intent.setType("text/plain");

    startActivityForResult(intent, EDIT_REQUEST_CODE);
}

次に、onActivityResult()プロセスの結果をご覧ください)からコードを呼び出して、編集を行います。次のスニペットは ContentResolver から FileOutputStream を取得します。 デフォルトでは、「書き込み」モードを使用します。必要最小限のアクセスのみを要求することをお勧めします。書き込みのみが必要な場合に読み込みと書き込みを要求しないでください。

private void alterDocument(Uri uri) {
    try {
        ParcelFileDescriptor pfd = getActivity().getContentResolver().
                openFileDescriptor(uri, "w");
        FileOutputStream fileOutputStream =
                new FileOutputStream(pfd.getFileDescriptor());
        fileOutputStream.write(("Overwritten by MyCloud at " +
                System.currentTimeMillis() + "\n").getBytes());
        // Let the document provider know you're done by closing the stream.
        fileOutputStream.close();
        pfd.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

パーミッションを固定する

アプリで読み込みや書き込み用にファイルを開くと、システムからアプリに対して、該当するファイルの URI パーミッションが付与されます。 このパーミッションはユーザーが端末を再起動するまで有効です。しかし、画像アプリでユーザーが最近編集した 5 つの画像にアプリから直接アクセスできるようにするにはどうすればいいでしょう。 ユーザーの端末が再起動されたら、ファイルを検索するためにユーザーをシステム ピッカーに戻す必要がありますが、これは適切な方法とは言えません。

このような状況を避けるために、システムがアプリに付与するパーミッションを固定します。 実際は、システムが提供する固定可能な URI パーミッションをアプリで「取得」します。 これにより、端末を再起動した場合でも、ユーザーはアプリからファイルに継続的にアクセスできます。

final int takeFlags = intent.getFlags()
            & (Intent.FLAG_GRANT_READ_URI_PERMISSION
            | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
// Check for the freshest data.
getContentResolver().takePersistableUriPermission(uri, takeFlags);

最後にもう 1 つ手順があります。アプリが最近アクセスした URI は、保存されていても既に無効になっている場合があります。別のアプリによってドキュメントが削除されたり、修正されたりすることが考えられます。 そこで、最新のデータを確認するには、常に getContentResolver().takePersistableUriPermission() を呼び出す必要があります。

カスタム ドキュメント プロバイダを作成する

ファイル用のストレージ サービスを提供するアプリ(クラウド保存サービスなど)を開発する場合、カスタム ドキュメント プロバイダを作成すれば、SAF を介してファイルを利用可能にできます。 ここではその方法について説明します。

マニフェスト

カスタム ドキュメント プロバイダを実装するには、アプリケーションのマニフェストに次のものを追加します。

プロバイダを含むサンプル マニフェストの抜粋を次に示します。

<manifest... >
    ...
    <uses-sdk
        android:minSdkVersion="19"
        android:targetSdkVersion="19" />
        ....
        <provider
            android:name="com.example.android.storageprovider.MyCloudProvider"
            android:authorities="com.example.android.storageprovider.documents"
            android:grantUriPermissions="true"
            android:exported="true"
            android:permission="android.permission.MANAGE_DOCUMENTS"
            android:enabled="@bool/atLeastKitKat">
            <intent-filter>
                <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
            </intent-filter>
        </provider>
    </application>

</manifest>

Android 4.3 以前を実行する端末をサポートする

ACTION_OPEN_DOCUMENT インテントは、Android 4.4 以降を実行する端末でのみ利用可能です。Android 4.3 以前を実行する端末に対応するためにアプリケーションで ACTION_GET_CONTENT をサポートするには、Android 4.4 以降を実行する端末のマニフェストで ACTION_GET_CONTENT インテント フィルタを無効にする必要があります。 ドキュメント プロバイダと ACTION_GET_CONTENT は互いに排他的な関係にあると考える必要があります。 両方を同時にサポートする場合、アプリはシステム ピッカー UI に 2 度表示され、格納されたデータへの 2 つの異なる方法を提供します。 これではユーザーも操作に迷ってしまいます。

ここでは、Android バージョン 4.4 以降を実行する端末で ACTION_GET_CONTENT インテント フィルタを無効にする際に推奨される方法を紹介します。

  1. res/values/ にある bool.xml リソース ファイルに、次の行を追加します。
    <bool name="atMostJellyBeanMR2">true</bool>
  2. res/values-v19/ にある bool.xml リソース ファイルに、次の行を追加します。
    <bool name="atMostJellyBeanMR2">false</bool>
  3. 4.4 (API レベル 19)以降のバージョンに対して ACTION_GET_CONTENT インテント フィルタを無効にするには、アクティビティ エイリアスを追加します。 次に例を示します。
    <!-- This activity alias is added so that GET_CONTENT intent-filter
         can be disabled for builds on API level 19 and higher. -->
    <activity-alias android:name="com.android.example.app.MyPicker"
            android:targetActivity="com.android.example.app.MyActivity"
            ...
            android:enabled="@bool/atMostJellyBeanMR2">
        <intent-filter>
            <action android:name="android.intent.action.GET_CONTENT" />
            <category android:name="android.intent.category.OPENABLE" />
            <category android:name="android.intent.category.DEFAULT" />
            <data android:mimeType="image/*" />
            <data android:mimeType="video/*" />
        </intent-filter>
    </activity-alias>
    

コントラクト

通常、カスタム コンテンツ プロバイダを作成するときには、コンテンツ プロバイダのデベロッパー ガイドに記載があるように、コントラクト クラスを実装するタスクが必要になります。 コントラクト クラスは public final クラスであり、URI、列名、MIME タイプ、プロバイダに関連するその他のメタデータを含みます。 コントラクト クラスは SAF によって提供されるため、独自に作成する必要はありません。

たとえば、ドキュメント プロバイダでドキュメントやルートを照会したときには、カーソルに次のような列が返されることがあります。

private static final String[] DEFAULT_ROOT_PROJECTION =
        new String[]{Root.COLUMN_ROOT_ID, Root.COLUMN_MIME_TYPES,
        Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE,
        Root.COLUMN_SUMMARY, Root.COLUMN_DOCUMENT_ID,
        Root.COLUMN_AVAILABLE_BYTES,};
private static final String[] DEFAULT_DOCUMENT_PROJECTION = new
        String[]{Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE,
        Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED,
        Document.COLUMN_FLAGS, Document.COLUMN_SIZE,};

サブクラス DocumentsProvider

カスタム ドキュメント プロバイダの作成の次の手順では、抽象クラス DocumentsProvider をサブクラス化します。 少なくとも、次のメソッドを実装する必要があります。

これらは実装する必要があるメソッドですが、これ以外にも多くのメソッドを実装できます。 詳細については、DocumentsProvider をご覧ください。

queryRoots を実装する

queryRoots() を実装すると、DocumentsContract.Root で定義される列を使って、ドキュメント プロバイダのすべてのルート ディレクトリを指す Cursor が返されます。

次のスニペットの projection パラメータは、呼び出し側が取得する特定のフィールドを表します。 スニペットは新しいカーソルを作成し、それに行を 1 つ追加します。[ダウンロード] や [画像] と同じように、1 つのルートに対して最上位のディレクトリが 1 つになります。 ほとんどのプロバイダでルートは 1 つになります。複数のユーザー アカウントを使用するような場合は、1 つ以上のルートを設定できます。 その場合、カーソルに 2 つ目の行を追加します。

@Override
public Cursor queryRoots(String[] projection) throws FileNotFoundException {

    // Create a cursor with either the requested fields, or the default
    // projection if "projection" is null.
    final MatrixCursor result =
            new MatrixCursor(resolveRootProjection(projection));

    // If user is not logged in, return an empty root cursor.  This removes our
    // provider from the list entirely.
    if (!isUserLoggedIn()) {
        return result;
    }

    // It's possible to have multiple roots (e.g. for multiple accounts in the
    // same app) -- just add multiple cursor rows.
    // Construct one row for a root called "MyCloud".
    final MatrixCursor.RowBuilder row = result.newRow();
    row.add(Root.COLUMN_ROOT_ID, ROOT);
    row.add(Root.COLUMN_SUMMARY, getContext().getString(R.string.root_summary));

    // FLAG_SUPPORTS_CREATE means at least one directory under the root supports
    // creating documents. FLAG_SUPPORTS_RECENTS means your application's most
    // recently used documents will show up in the "Recents" category.
    // FLAG_SUPPORTS_SEARCH allows users to search all documents the application
    // shares.
    row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE |
            Root.FLAG_SUPPORTS_RECENTS |
            Root.FLAG_SUPPORTS_SEARCH);

    // COLUMN_TITLE is the root title (e.g. Gallery, Drive).
    row.add(Root.COLUMN_TITLE, getContext().getString(R.string.title));

    // This document id cannot change once it's shared.
    row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(mBaseDir));

    // The child MIME types are used to filter the roots and only present to the
    //  user roots that contain the desired type somewhere in their file hierarchy.
    row.add(Root.COLUMN_MIME_TYPES, getChildMimeTypes(mBaseDir));
    row.add(Root.COLUMN_AVAILABLE_BYTES, mBaseDir.getFreeSpace());
    row.add(Root.COLUMN_ICON, R.drawable.ic_launcher);

    return result;
}

queryChildDocuments を実装する

queryChildDocuments() を実装すると、DocumentsContract.Document で定義される列を使って、指定したディレクトリ内のすべてのファイルを指す Cursor が返されます。

ピッカー UI でアプリケーション ルートを選択すると、このメソッドが呼び出されます。ルートの下のディレクトリの子ドキュメントを取得します。 ルートだけでなく、ファイル階層内の任意のレベルで呼び出せます。 このスニペットでは、要求した列を持つ新しいカーソルが作成され、 親ディレクトリのすぐ下のそれぞれの子の情報がカーソルに追加されます。 子は、画像、別のディレクトリなど任意のファイルとなります。

@Override
public Cursor queryChildDocuments(String parentDocumentId, String[] projection,
                              String sortOrder) throws FileNotFoundException {

    final MatrixCursor result = new
            MatrixCursor(resolveDocumentProjection(projection));
    final File parent = getFileForDocId(parentDocumentId);
    for (File file : parent.listFiles()) {
        // Adds the file's display name, MIME type, size, and so on.
        includeFile(result, null, file);
    }
    return result;
}

queryDocument を実装する

queryDocument() を実装すると、DocumentsContract.Document で定義される列を使って、指定したファイルを指す Cursor が返されます。

queryDocument() メソッドでは、特定のファイルの情報ではなく、queryChildDocuments() に渡したのと同じ情報が返されます。

@Override
public Cursor queryDocument(String documentId, String[] projection) throws
        FileNotFoundException {

    // Create a cursor with the requested projection, or the default projection.
    final MatrixCursor result = new
            MatrixCursor(resolveDocumentProjection(projection));
    includeFile(result, documentId, null);
    return result;
}

openDocument を実装する

指定したファイルを表す ParcelFileDescriptor を返すには、openDocument() を実装する必要があります。 他のアプリは、返された ParcelFileDescriptor を使ってデータをストリーミングします。 ユーザーがファイルを選択するとシステムがこのメソッドを呼び出し、クライアント アプリは openFileDescriptor() を呼び出してファイルへのアクセスを要求します。次に例を示します。

@Override
public ParcelFileDescriptor openDocument(final String documentId,
                                         final String mode,
                                         CancellationSignal signal) throws
        FileNotFoundException {
    Log.v(TAG, "openDocument, mode: " + mode);
    // It's OK to do network operations in this method to download the document,
    // as long as you periodically check the CancellationSignal. If you have an
    // extremely large file to transfer from the network, a better solution may
    // be pipes or sockets (see ParcelFileDescriptor for helper methods).

    final File file = getFileForDocId(documentId);

    final boolean isWrite = (mode.indexOf('w') != -1);
    if(isWrite) {
        // Attach a close listener if the document is opened in write mode.
        try {
            Handler handler = new Handler(getContext().getMainLooper());
            return ParcelFileDescriptor.open(file, accessMode, handler,
                        new ParcelFileDescriptor.OnCloseListener() {
                @Override
                public void onClose(IOException e) {

                    // Update the file with the cloud server. The client is done
                    // writing.
                    Log.i(TAG, "A file with id " +
                    documentId + " has been closed!
                    Time to " +
                    "update the server.");
                }

            });
        } catch (IOException e) {
            throw new FileNotFoundException("Failed to open document with id "
            + documentId + " and mode " + mode);
        }
    } else {
        return ParcelFileDescriptor.open(file, accessMode);
    }
}

セキュリティ

たとえば、ドキュメント プロバイダがパスワードで保護されたクラウド ストレージ サービスであり、ファイル共有を開始する前にユーザーがログインしているかどうかを確認するとします。ユーザーがログインしていない場合、アプリはどのような動作を行う必要があるでしょうか。 このような場合、queryRoots() の実装のゼロルートを返すようにします。 つまり、空のルートカーソルになります。

public Cursor queryRoots(String[] projection) throws FileNotFoundException {
...
    // If user is not logged in, return an empty root cursor.  This removes our
    // provider from the list entirely.
    if (!isUserLoggedIn()) {
        return result;
}

その他に getContentResolver().notifyChange() を呼び出す手順もあります。DocumentsContract については既に説明したとおりです。 これを使ってこの URI を作成します。 次のスニペットは、ユーザーのログイン ステータスが変わった場合に、ドキュメント プロバイダのルートを照会するようシステムに指示します。 ユーザーがログインしてない場合は、上述のように、queryRoots() を呼び出すと空のカーソルが返されます。 こうすることで、ユーザーがプロバイダにログインしている場合にのみ、プロバイダのドキュメントを利用できます。

private void onLoginButtonClick() {
    loginOrLogout();
    getContentResolver().notifyChange(DocumentsContract
            .buildRootsUri(AUTHORITY), null);
}
This site uses cookies to store your preferences for site-specific language and display options.

Get the latest Android developer news and tips that will help you find success on Google Play.

* Required Fields

Hooray!

Follow Google Developers on WeChat

Browse this site in ?

You requested a page in , but your language preference for this site is .

Would you like to change your language preference and browse this site in ? If you want to change your language preference later, use the language menu at the bottom of each page.

This class requires API level or higher

This doc is hidden because your selected API level for the documentation is . You can change the documentation API level with the selector above the left navigation.

For more information about specifying the API level your app requires, read Supporting Different Platform Versions.

Take a short survey?
Help us improve the Android developer experience.
(Sep 2017 survey)