একটি কাস্টম নথি প্রদানকারী তৈরি করুন

আপনি যদি এমন একটি অ্যাপ তৈরি করেন যা ফাইলগুলির জন্য স্টোরেজ পরিষেবা প্রদান করে (যেমন একটি ক্লাউড সংরক্ষণ পরিষেবা), আপনি একটি কাস্টম নথি প্রদানকারী লিখে স্টোরেজ অ্যাক্সেস ফ্রেমওয়ার্ক (SAF) এর মাধ্যমে আপনার ফাইলগুলি উপলব্ধ করতে পারেন৷ এই পৃষ্ঠাটি বর্ণনা করে কিভাবে একটি কাস্টম নথি প্রদানকারী তৈরি করতে হয়।

স্টোরেজ অ্যাক্সেস ফ্রেমওয়ার্ক কীভাবে কাজ করে সে সম্পর্কে আরও তথ্যের জন্য, স্টোরেজ অ্যাক্সেস ফ্রেমওয়ার্ক ওভারভিউ দেখুন।

উদ্ভাসিত

একটি কাস্টম নথি প্রদানকারী বাস্তবায়ন করতে, আপনার অ্যাপ্লিকেশনের ম্যানিফেস্টে নিম্নলিখিতগুলি যোগ করুন:

  • API স্তর 19 বা উচ্চতর একটি লক্ষ্য।
  • একটি <provider> উপাদান যা আপনার কাস্টম স্টোরেজ প্রদানকারী ঘোষণা করে।
  • অ্যাট্রিবিউট android:name আপনার DocumentsProvider সাবক্লাসের নামে সেট করা হয়েছে, যা প্যাকেজের নাম সহ এটির ক্লাসের নাম:

    com.example.android.storageprovider.MyCloudProvider

  • অ্যাট্রিবিউট android:authority অ্যাট্রিবিউট, যা আপনার প্যাকেজের নাম (এই উদাহরণে, com.example.android.storageprovider ) এবং বিষয়বস্তু প্রদানকারীর ধরন ( documents )।
  • অ্যাট্রিবিউট android:exported হয়েছে "true" । আপনাকে অবশ্যই আপনার সরবরাহকারীকে রপ্তানি করতে হবে যাতে অন্য অ্যাপগুলি এটি দেখতে পারে৷
  • অ্যাট্রিবিউট android:grantUriPermissions "true" এ সেট করা হয়েছে। এই সেটিং সিস্টেমটিকে আপনার প্রদানকারীর সামগ্রীতে অন্যান্য অ্যাপকে অ্যাক্সেস দেওয়ার অনুমতি দেয়৷ এই অন্যান্য অ্যাপগুলি কীভাবে আপনার প্রদানকারীর সামগ্রীতে তাদের অ্যাক্সেস বজায় রাখতে পারে সে সম্পর্কে আলোচনার জন্য, Persist permissions দেখুন।
  • MANAGE_DOCUMENTS অনুমতি। ডিফল্টরূপে একজন প্রদানকারী প্রত্যেকের জন্য উপলব্ধ। এই অনুমতি যোগ করা আপনার প্রদানকারীকে সিস্টেমে সীমাবদ্ধ করে। নিরাপত্তার জন্য এই নিষেধাজ্ঞা গুরুত্বপূর্ণ।
  • একটি ইন্টেন্ট ফিল্টার যাতে android.content.action.DOCUMENTS_PROVIDER অ্যাকশন অন্তর্ভুক্ত থাকে, যাতে সিস্টেম যখন প্রোভাইডারদের খোঁজ করে তখন আপনার প্রদানকারী পিকারে উপস্থিত হয়।

এখানে একটি নমুনা ম্যানিফেস্ট থেকে উদ্ধৃতাংশ রয়েছে যা একটি প্রদানকারীকে অন্তর্ভুক্ত করে:

<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">
            <intent-filter>
                <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
            </intent-filter>
        </provider>
    </application>

</manifest>

অ্যান্ড্রয়েড 4.3 এবং তার নিচের সংস্করণে চলমান সমর্থনকারী ডিভাইস

ACTION_OPEN_DOCUMENT অভিপ্রায় শুধুমাত্র Android 4.4 এবং উচ্চতর সংস্করণে চালিত ডিভাইসগুলিতে উপলব্ধ৷ আপনি যদি চান যে আপনার অ্যাপ্লিকেশানটি ACTION_GET_CONTENT সমর্থন করে এমন ডিভাইসগুলি যেগুলি Android 4.3 এবং তার নীচের সংস্করণগুলি চালাচ্ছে, সেগুলির জন্য আপনার ম্যানিফেস্টে ACTION_GET_CONTENT অভিপ্রায় ফিল্টারটি Android 4.4 বা উচ্চতর সংস্করণে চলমান ডিভাইসগুলির জন্য অক্ষম করা উচিত৷ একটি নথি প্রদানকারী এবং ACTION_GET_CONTENT পারস্পরিক একচেটিয়া বিবেচনা করা উচিত৷ আপনি যদি একই সাথে উভয়কেই সমর্থন করেন, আপনার সঞ্চিত ডেটা অ্যাক্সেস করার দুটি ভিন্ন উপায় অফার করে আপনার অ্যাপ সিস্টেম পিকার UI-তে দুইবার প্রদর্শিত হবে। এটি ব্যবহারকারীদের জন্য বিভ্রান্তিকর।

অ্যান্ড্রয়েড সংস্করণ 4.4 বা উচ্চতর চলমান ডিভাইসগুলির জন্য ACTION_GET_CONTENT অভিপ্রায় ফিল্টার নিষ্ক্রিয় করার প্রস্তাবিত উপায় এখানে রয়েছে:

  1. আপনার bool.xml রিসোর্স ফাইলে res/values/ অধীনে, এই লাইনটি যোগ করুন:
    <bool name="atMostJellyBeanMR2">true</bool>
  2. আপনার bool.xml রিসোর্স ফাইলে res/values-v19/ অধীনে, এই লাইনটি যোগ করুন:
    <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 val DEFAULT_ROOT_PROJECTION: Array<String> = arrayOf(
        DocumentsContract.Root.COLUMN_ROOT_ID,
        DocumentsContract.Root.COLUMN_MIME_TYPES,
        DocumentsContract.Root.COLUMN_FLAGS,
        DocumentsContract.Root.COLUMN_ICON,
        DocumentsContract.Root.COLUMN_TITLE,
        DocumentsContract.Root.COLUMN_SUMMARY,
        DocumentsContract.Root.COLUMN_DOCUMENT_ID,
        DocumentsContract.Root.COLUMN_AVAILABLE_BYTES
)
private val DEFAULT_DOCUMENT_PROJECTION: Array<String> = arrayOf(
        DocumentsContract.Document.COLUMN_DOCUMENT_ID,
        DocumentsContract.Document.COLUMN_MIME_TYPE,
        DocumentsContract.Document.COLUMN_DISPLAY_NAME,
        DocumentsContract.Document.COLUMN_LAST_MODIFIED,
        DocumentsContract.Document.COLUMN_FLAGS,
        DocumentsContract.Document.COLUMN_SIZE
)

জাভা

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() বাস্তবায়নের জন্য DocumentsContract.Root এ সংজ্ঞায়িত কলাম ব্যবহার করে আপনার নথি প্রদানকারীর সমস্ত রুট ডিরেক্টরির দিকে নির্দেশ করে একটি Cursor ফেরত দিতে হবে।

নিম্নলিখিত স্নিপেটে, projection প্যারামিটার নির্দিষ্ট ক্ষেত্রগুলিকে প্রতিনিধিত্ব করে যেগুলি কলকারী ফিরে পেতে চায়৷ স্নিপেট একটি নতুন কার্সার তৈরি করে এবং এতে একটি সারি যোগ করে—একটি রুট, একটি শীর্ষ স্তরের ডিরেক্টরি, যেমন ডাউনলোড বা চিত্র। বেশিরভাগ প্রদানকারীর শুধুমাত্র একটি রুট আছে। আপনার একাধিক ব্যবহারকারী থাকতে পারে, উদাহরণস্বরূপ, একাধিক ব্যবহারকারীর অ্যাকাউন্টের ক্ষেত্রে। সেই ক্ষেত্রে, কার্সারে শুধু একটি দ্বিতীয় সারি যোগ করুন।

কোটলিন

override fun queryRoots(projection: Array<out String>?): Cursor {
    // Use a MatrixCursor to build a cursor
    // with either the requested fields, or the default
    // projection if "projection" is null.
    val result = 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.
    result.newRow().apply {
        add(DocumentsContract.Root.COLUMN_ROOT_ID, ROOT)

        // You can provide an optional summary, which helps distinguish roots
        // with the same title. You can also use this field for displaying an
        // user account name.
        add(DocumentsContract.Root.COLUMN_SUMMARY, context.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.
        add(
            DocumentsContract.Root.COLUMN_FLAGS,
            DocumentsContract.Root.FLAG_SUPPORTS_CREATE or
                DocumentsContract.Root.FLAG_SUPPORTS_RECENTS or
                DocumentsContract.Root.FLAG_SUPPORTS_SEARCH
        )

        // COLUMN_TITLE is the root title (e.g. Gallery, Drive).
        add(DocumentsContract.Root.COLUMN_TITLE, context.getString(R.string.title))

        // This document id cannot change after it's shared.
        add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, getDocIdForFile(baseDir))

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

    return result
}

জাভা

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

    // Use a MatrixCursor to build 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.
    final MatrixCursor.RowBuilder row = result.newRow();
    row.add(Root.COLUMN_ROOT_ID, ROOT);

    // You can provide an optional summary, which helps distinguish roots
    // with the same title. You can also use this field for displaying an
    // user account name.
    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 after it's shared.
    row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(baseDir));

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

    return result;
}

যদি আপনার ডকুমেন্ট প্রদানকারী একটি ডাইনামিক সেট রুটের সাথে সংযোগ করে—উদাহরণস্বরূপ, একটি USB ডিভাইস যা সংযোগ বিচ্ছিন্ন হতে পারে বা এমন একটি অ্যাকাউন্ট যা থেকে ব্যবহারকারী সাইন আউট করতে পারে—আপনি ContentResolver.notifyChange() ব্যবহার করে সেই পরিবর্তনগুলির সাথে সিঙ্কে থাকার জন্য নথি UI আপডেট করতে পারেন ContentResolver.notifyChange() পদ্ধতি, যেমনটি নিম্নলিখিত কোড স্নিপেটে দেখানো হয়েছে।

কোটলিন

val rootsUri: Uri = DocumentsContract.buildRootsUri(BuildConfig.DOCUMENTS_AUTHORITY)
context.contentResolver.notifyChange(rootsUri, null)

জাভা

Uri rootsUri = DocumentsContract.buildRootsUri(BuildConfig.DOCUMENTS_AUTHORITY);
context.getContentResolver().notifyChange(rootsUri, null);

সরবরাহকারীতে নথির তালিকা করুন

আপনার queryChildDocuments() বাস্তবায়নের জন্য অবশ্যই একটি Cursor ফেরত দিতে হবে যা DocumentsContract.Document এ সংজ্ঞায়িত কলাম ব্যবহার করে নির্দিষ্ট ডিরেক্টরির সমস্ত ফাইলকে নির্দেশ করে।

ব্যবহারকারী যখন পিকার UI এ আপনার রুট বেছে নেয় তখন এই পদ্ধতিটি বলা হয়। পদ্ধতিটি COLUMN_DOCUMENT_ID দ্বারা নির্দিষ্ট করা ডকুমেন্ট আইডির বাচ্চাদের পুনরুদ্ধার করে। ব্যবহারকারী আপনার নথি প্রদানকারীর মধ্যে একটি সাবডিরেক্টরি নির্বাচন করলে সিস্টেমটি এই পদ্ধতিটিকে কল করে।

এই স্নিপেটটি অনুরোধ করা কলামগুলির সাথে একটি নতুন কার্সার তৈরি করে, তারপরে কার্সারে মূল ডিরেক্টরির প্রতিটি অবিলম্বে সন্তানের তথ্য যোগ করে। একটি শিশু একটি ইমেজ, অন্য ডিরেক্টরি হতে পারে-যেকোন ফাইল:

কোটলিন

override fun queryChildDocuments(
        parentDocumentId: String?,
        projection: Array<out String>?,
        sortOrder: String?
): Cursor {
    return MatrixCursor(resolveDocumentProjection(projection)).apply {
        val parent: File = getFileForDocId(parentDocumentId)
        parent.listFiles()
                .forEach { file ->
                    includeFile(this, null, file)
                }
    }
}

জাভা

@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() বাস্তবায়নের জন্য অবশ্যই একটি Cursor ফেরত দিতে হবে যা DocumentsContract.Document এ সংজ্ঞায়িত কলাম ব্যবহার করে নির্দিষ্ট ফাইলের দিকে নির্দেশ করে।

queryDocument() পদ্ধতিটি একই তথ্য প্রদান করে যা queryChildDocuments() এ পাস করা হয়েছিল, কিন্তু একটি নির্দিষ্ট ফাইলের জন্য:

কোটলিন

override fun queryDocument(documentId: String?, projection: Array<out String>?): Cursor {
    // Create a cursor with the requested projection, or the default projection.
    return MatrixCursor(resolveDocumentProjection(projection)).apply {
        includeFile(this, documentId, null)
    }
}

জাভা

@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;
}

আপনার ডকুমেন্ট প্রদানকারী DocumentsProvider.openDocumentThumbnail() পদ্ধতি ওভাররাইড করে এবং সমর্থিত ফাইলগুলিতে FLAG_SUPPORTS_THUMBNAIL পতাকা যোগ করে একটি নথির থাম্বনেইল প্রদান করতে পারে। নিম্নলিখিত কোড স্নিপেট কিভাবে DocumentsProvider.openDocumentThumbnail() বাস্তবায়ন করতে হয় তার একটি উদাহরণ প্রদান করে।

কোটলিন

override fun openDocumentThumbnail(
        documentId: String?,
        sizeHint: Point?,
        signal: CancellationSignal?
): AssetFileDescriptor {
    val file = getThumbnailFileForDocId(documentId)
    val pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
    return AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH)
}

জাভা

@Override
public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHint,
                                                     CancellationSignal signal)
        throws FileNotFoundException {

    final File file = getThumbnailFileForDocId(documentId);
    final ParcelFileDescriptor pfd =
        ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
    return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH);
}

সতর্কতা : একটি ডকুমেন্ট প্রদানকারীকে sizeHint প্যারামিটার দ্বারা নির্ধারিত আকারের দ্বিগুণের বেশি থাম্বনেইল চিত্রগুলি ফেরত দেওয়া উচিত নয়৷

একটি নথি খুলুন

নির্দিষ্ট ফাইলের প্রতিনিধিত্বকারী একটি ParcelFileDescriptor ফেরত দিতে আপনাকে openDocument() প্রয়োগ করতে হবে। অন্যান্য অ্যাপ্লিকেশানগুলি ডেটা স্ট্রিম করতে ফেরত দেওয়া ParcelFileDescriptor ব্যবহার করতে পারে৷ ব্যবহারকারী একটি ফাইল নির্বাচন করার পরে সিস্টেমটি এই পদ্ধতিটিকে কল করে এবং ক্লায়েন্ট অ্যাপ openFileDescriptor() কল করে এটিতে অ্যাক্সেসের অনুরোধ করে। উদাহরণ স্বরূপ:

কোটলিন

override fun openDocument(
        documentId: String,
        mode: String,
        signal: CancellationSignal
): ParcelFileDescriptor {
    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).

    val file: File = getFileForDocId(documentId)
    val accessMode: Int = ParcelFileDescriptor.parseMode(mode)

    val isWrite: Boolean = mode.contains("w")
    return if (isWrite) {
        val handler = Handler(context.mainLooper)
        // Attach a close listener if the document is opened in write mode.
        try {
            ParcelFileDescriptor.open(file, accessMode, handler) {
                // 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 (e: IOException) {
            throw FileNotFoundException(
                    "Failed to open document with id $documentId and mode $mode"
            )
        }
    } else {
        ParcelFileDescriptor.open(file, accessMode)
    }
}

জাভা

@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 int accessMode = ParcelFileDescriptor.parseMode(mode);

    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);
    }
}

যদি আপনার নথি প্রদানকারী ফাইলগুলি স্ট্রিম করে বা জটিল ডেটা স্ট্রাকচার পরিচালনা করে, createReliablePipe() বা createReliableSocketPair() পদ্ধতি প্রয়োগ করার কথা বিবেচনা করুন। এই পদ্ধতিগুলি আপনাকে ParcelFileDescriptor বস্তুর একটি জোড়া তৈরি করতে দেয়, যেখানে আপনি একটিকে ফেরত দিতে পারেন এবং একটি ParcelFileDescriptor.AutoCloseOutputStream বা ParcelFileDescriptor.AutoCloseInputStream এর মাধ্যমে পাঠাতে পারেন।

সাম্প্রতিক নথি এবং অনুসন্ধান সমর্থন

আপনি queryRecentDocuments() পদ্ধতিকে ওভাররাইড করে এবং FLAG_SUPPORTS_RECENTS রিটার্ন করে আপনার নথি প্রদানকারীর মূলের অধীনে সম্প্রতি পরিবর্তিত নথিগুলির একটি তালিকা প্রদান করতে পারেন, নিম্নলিখিত কোড স্নিপেটটি queryRecentDocuments() পদ্ধতিগুলিকে কীভাবে বাস্তবায়ন করতে হয় তার একটি উদাহরণ দেখায়৷

কোটলিন

override fun queryRecentDocuments(rootId: String?, projection: Array<out String>?): Cursor {
    // This example implementation walks a
    // local file structure to find the most recently
    // modified files.  Other implementations might
    // include making a network call to query a
    // server.

    // Create a cursor with the requested projection, or the default projection.
    val result = MatrixCursor(resolveDocumentProjection(projection))

    val parent: File = getFileForDocId(rootId)

    // Create a queue to store the most recent documents,
    // which orders by last modified.
    val lastModifiedFiles = PriorityQueue(
            5,
            Comparator<File> { i, j ->
                Long.compare(i.lastModified(), j.lastModified())
            }
    )

    // Iterate through all files and directories
    // in the file structure under the root.  If
    // the file is more recent than the least
    // recently modified, add it to the queue,
    // limiting the number of results.
    val pending : MutableList<File> = mutableListOf()

    // Start by adding the parent to the list of files to be processed
    pending.add(parent)

    // Do while we still have unexamined files
    while (pending.isNotEmpty()) {
        // Take a file from the list of unprocessed files
        val file: File = pending.removeAt(0)
        if (file.isDirectory) {
            // If it's a directory, add all its children to the unprocessed list
            pending += file.listFiles()
        } else {
            // If it's a file, add it to the ordered queue.
            lastModifiedFiles.add(file)
        }
    }

    // Add the most recent files to the cursor,
    // not exceeding the max number of results.
    for (i in 0 until Math.min(MAX_LAST_MODIFIED + 1, lastModifiedFiles.size)) {
        val file: File = lastModifiedFiles.remove()
        includeFile(result, null, file)
    }
    return result
}

জাভা

@Override
public Cursor queryRecentDocuments(String rootId, String[] projection)
        throws FileNotFoundException {

    // This example implementation walks a
    // local file structure to find the most recently
    // modified files.  Other implementations might
    // include making a network call to query a
    // server.

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

    final File parent = getFileForDocId(rootId);

    // Create a queue to store the most recent documents,
    // which orders by last modified.
    PriorityQueue lastModifiedFiles =
        new PriorityQueue(5, new Comparator() {

        public int compare(File i, File j) {
            return Long.compare(i.lastModified(), j.lastModified());
        }
    });

    // Iterate through all files and directories
    // in the file structure under the root.  If
    // the file is more recent than the least
    // recently modified, add it to the queue,
    // limiting the number of results.
    final LinkedList pending = new LinkedList();

    // Start by adding the parent to the list of files to be processed
    pending.add(parent);

    // Do while we still have unexamined files
    while (!pending.isEmpty()) {
        // Take a file from the list of unprocessed files
        final File file = pending.removeFirst();
        if (file.isDirectory()) {
            // If it's a directory, add all its children to the unprocessed list
            Collections.addAll(pending, file.listFiles());
        } else {
            // If it's a file, add it to the ordered queue.
            lastModifiedFiles.add(file);
        }
    }

    // Add the most recent files to the cursor,
    // not exceeding the max number of results.
    for (int i = 0; i < Math.min(MAX_LAST_MODIFIED + 1, lastModifiedFiles.size()); i++) {
        final File file = lastModifiedFiles.remove();
        includeFile(result, null, file);
    }
    return result;
}

আপনি StorageProvider কোড নমুনা ডাউনলোড করে উপরের স্নিপেটের জন্য সম্পূর্ণ কোড পেতে পারেন।

নথি তৈরি সমর্থন

আপনি ক্লায়েন্ট অ্যাপগুলিকে আপনার নথি প্রদানকারীর মধ্যে ফাইল তৈরি করার অনুমতি দিতে পারেন। যদি একটি ক্লায়েন্ট অ্যাপ একটি ACTION_CREATE_DOCUMENT উদ্দেশ্য পাঠায়, আপনার নথি প্রদানকারী সেই ক্লায়েন্ট অ্যাপটিকে নথি প্রদানকারীর মধ্যে নতুন নথি তৈরি করার অনুমতি দিতে পারে।

নথি তৈরি করতে সহায়তা করার জন্য, আপনার রুটে FLAG_SUPPORTS_CREATE পতাকা থাকা দরকার৷ যে ডিরেক্টরিগুলি তাদের মধ্যে নতুন ফাইল তৈরি করার অনুমতি দেয় তাদের FLAG_DIR_SUPPORTS_CREATE পতাকা থাকা দরকার৷

আপনার নথি প্রদানকারীকে createDocument() পদ্ধতি প্রয়োগ করতে হবে। যখন একজন ব্যবহারকারী একটি নতুন ফাইল সংরক্ষণ করার জন্য আপনার নথি প্রদানকারীর মধ্যে একটি ডিরেক্টরি নির্বাচন করেন, তখন নথি প্রদানকারী createDocument() এর জন্য একটি কল পায়। createDocument() পদ্ধতির বাস্তবায়নের ভিতরে, আপনি ফাইলের জন্য একটি নতুন COLUMN_DOCUMENT_ID ফেরত দেন। ক্লায়েন্ট অ্যাপটি ফাইলটির জন্য একটি হ্যান্ডেল পেতে সেই আইডিটি ব্যবহার করতে পারে এবং শেষ পর্যন্ত, নতুন ফাইলে লিখতে openDocument() কল করতে পারে।

নিম্নলিখিত কোড স্নিপেট প্রদর্শন করে কিভাবে একটি নথি প্রদানকারীর মধ্যে একটি নতুন ফাইল তৈরি করতে হয়।

কোটলিন

override fun createDocument(documentId: String?, mimeType: String?, displayName: String?): String {
    val parent: File = getFileForDocId(documentId)
    val file: File = try {
        File(parent.path, displayName).apply {
            createNewFile()
            setWritable(true)
            setReadable(true)
        }
    } catch (e: IOException) {
        throw FileNotFoundException(
                "Failed to create document with name $displayName and documentId $documentId"
        )
    }

    return getDocIdForFile(file)
}

জাভা

@Override
public String createDocument(String documentId, String mimeType, String displayName)
        throws FileNotFoundException {

    File parent = getFileForDocId(documentId);
    File file = new File(parent.getPath(), displayName);
    try {
        file.createNewFile();
        file.setWritable(true);
        file.setReadable(true);
    } catch (IOException e) {
        throw new FileNotFoundException("Failed to create document with name " +
                displayName +" and documentId " + documentId);
    }
    return getDocIdForFile(file);
}

আপনি StorageProvider কোড নমুনা ডাউনলোড করে উপরের স্নিপেটের জন্য সম্পূর্ণ কোড পেতে পারেন।

নথি ব্যবস্থাপনা বৈশিষ্ট্য সমর্থন

ফাইল খোলা, তৈরি এবং দেখার পাশাপাশি, আপনার নথি প্রদানকারী ক্লায়েন্ট অ্যাপগুলিকে ফাইলগুলি পুনঃনামকরণ, অনুলিপি, সরানো এবং মুছে ফেলার ক্ষমতাও দিতে পারে৷ আপনার নথি প্রদানকারীতে নথি ব্যবস্থাপনা কার্যকারিতা যোগ করতে, সমর্থিত কার্যকারিতা নির্দেশ করতে নথির COLUMN_FLAGS কলামে একটি পতাকা যোগ করুন। আপনাকে DocumentsProvider ক্লাসের সংশ্লিষ্ট পদ্ধতি বাস্তবায়ন করতে হবে।

নিম্নলিখিত সারণীটি COLUMN_FLAGS পতাকা এবং DocumentsProvider পদ্ধতি প্রদান করে যা একটি নথি প্রদানকারীকে নির্দিষ্ট বৈশিষ্ট্যগুলি প্রকাশ করার জন্য প্রয়োগ করতে হবে।

বৈশিষ্ট্য পতাকা পদ্ধতি
একটি ফাইল মুছুন FLAG_SUPPORTS_DELETE deleteDocument()
একটি ফাইলের নাম পরিবর্তন করুন FLAG_SUPPORTS_RENAME renameDocument()
নথি প্রদানকারীর মধ্যে একটি নতুন প্যারেন্ট ডিরেক্টরিতে একটি ফাইল অনুলিপি করুন FLAG_SUPPORTS_COPY copyDocument()
নথি প্রদানকারীর মধ্যে একটি ডিরেক্টরি থেকে অন্য ডিরেক্টরিতে একটি ফাইল সরান FLAG_SUPPORTS_MOVE moveDocument()
এর মূল ডিরেক্টরি থেকে একটি ফাইল সরান FLAG_SUPPORTS_REMOVE removeDocument()

ভার্চুয়াল ফাইল এবং বিকল্প ফাইল ফরম্যাট সমর্থন

ভার্চুয়াল ফাইল , Android 7.0 (API লেভেল 24) তে প্রবর্তিত একটি বৈশিষ্ট্য, নথি প্রদানকারীদের এমন ফাইলগুলিতে দেখার অ্যাক্সেস প্রদান করতে দেয় যেগুলির সরাসরি বাইটকোড উপস্থাপনা নেই৷ ভার্চুয়াল ফাইলগুলি দেখতে অন্যান্য অ্যাপগুলিকে সক্ষম করতে, আপনার নথি প্রদানকারীকে ভার্চুয়াল ফাইলগুলির জন্য একটি বিকল্প খোলাযোগ্য ফাইল উপস্থাপনা তৈরি করতে হবে৷

উদাহরণস্বরূপ, কল্পনা করুন যে একটি নথি প্রদানকারী একটি ফাইল বিন্যাস ধারণ করে যা অন্যান্য অ্যাপ সরাসরি খুলতে পারে না, মূলত একটি ভার্চুয়াল ফাইল। যখন একটি ক্লায়েন্ট অ্যাপ CATEGORY_OPENABLE বিভাগ ছাড়াই একটি ACTION_VIEW উদ্দেশ্য পাঠায়, তখন ব্যবহারকারীরা দেখার জন্য নথি প্রদানকারীর মধ্যে এই ভার্চুয়াল ফাইলগুলি নির্বাচন করতে পারেন৷ ডকুমেন্ট প্রদানকারী তারপর ভার্চুয়াল ফাইলটিকে একটি ভিন্ন, কিন্তু খোলাযোগ্য, একটি চিত্রের মতো ফাইল বিন্যাসে ফেরত দেয়। ক্লায়েন্ট অ্যাপটি ব্যবহারকারীর দেখার জন্য ভার্চুয়াল ফাইলটি খুলতে পারে।

প্রদানকারীর একটি নথি ভার্চুয়াল বলে ঘোষণা করার জন্য, আপনাকে queryDocument() পদ্ধতি দ্বারা ফেরত ফাইলটিতে FLAG_VIRTUAL_DOCUMENT পতাকা যোগ করতে হবে। এই পতাকাটি ক্লায়েন্ট অ্যাপগুলিকে সতর্ক করে যে ফাইলটিতে সরাসরি বাইটকোড উপস্থাপনা নেই এবং সরাসরি খোলা যাবে না।

আপনি যদি ঘোষণা করেন যে আপনার নথি প্রদানকারীর একটি ফাইল ভার্চুয়াল, তাহলে এটি দৃঢ়ভাবে সুপারিশ করা হয় যে আপনি এটিকে অন্য একটি MIME ধরনের যেমন একটি ছবি বা PDF এ উপলব্ধ করুন৷ নথি প্রদানকারী বিকল্প MIME প্রকারগুলি ঘোষণা করে যা এটি getDocumentStreamTypes() পদ্ধতিকে ওভাররাইড করে একটি ভার্চুয়াল ফাইল দেখার জন্য সমর্থন করে। যখন ক্লায়েন্ট অ্যাপগুলি getStreamTypes(android.net.Uri, java.lang.String) পদ্ধতিতে কল করে, তখন সিস্টেমটি নথি প্রদানকারীর getDocumentStreamTypes() পদ্ধতিতে কল করে। getDocumentStreamTypes() পদ্ধতি তারপর বিকল্প MIME প্রকারের একটি অ্যারে প্রদান করে যা নথি প্রদানকারী ফাইলটির জন্য সমর্থন করে।

ক্লায়েন্ট নির্ধারণ করার পরে যে নথি প্রদানকারী একটি দর্শনযোগ্য ফাইল বিন্যাসে নথিটি তৈরি করতে পারে, ক্লায়েন্ট অ্যাপটি openTypedAssetFileDescriptor() পদ্ধতিকে কল করে, যা অভ্যন্তরীণভাবে নথি প্রদানকারীর openTypedDocument() পদ্ধতিকে কল করে। নথি প্রদানকারী অনুরোধকৃত ফাইল বিন্যাসে ক্লায়েন্ট অ্যাপে ফাইলটি ফেরত দেয়।

নিম্নলিখিত কোড স্নিপেট getDocumentStreamTypes() এবং openTypedDocument() পদ্ধতির একটি সহজ বাস্তবায়ন প্রদর্শন করে।

কোটলিন

var SUPPORTED_MIME_TYPES : Array<String> = arrayOf("image/png", "image/jpg")
override fun openTypedDocument(
        documentId: String?,
        mimeTypeFilter: String,
        opts: Bundle?,
        signal: CancellationSignal?
): AssetFileDescriptor? {
    return try {
        // Determine which supported MIME type the client app requested.
        when(mimeTypeFilter) {
            "image/jpg" -> openJpgDocument(documentId)
            "image/png", "image/*", "*/*" -> openPngDocument(documentId)
            else -> throw IllegalArgumentException("Invalid mimeTypeFilter $mimeTypeFilter")
        }
    } catch (ex: Exception) {
        Log.e(TAG, ex.message)
        null
    }
}

override fun getDocumentStreamTypes(documentId: String, mimeTypeFilter: String): Array<String> {
    return when (mimeTypeFilter) {
        "*/*", "image/*" -> {
            // Return all supported MIME types if the client app
            // passes in '*/*' or 'image/*'.
            SUPPORTED_MIME_TYPES
        }
        else -> {
            // Filter the list of supported mime types to find a match.
            SUPPORTED_MIME_TYPES.filter { it == mimeTypeFilter }.toTypedArray()
        }
    }
}

জাভা


public static String[] SUPPORTED_MIME_TYPES = {"image/png", "image/jpg"};

@Override
public AssetFileDescriptor openTypedDocument(String documentId,
    String mimeTypeFilter,
    Bundle opts,
    CancellationSignal signal) {

    try {

        // Determine which supported MIME type the client app requested.
        if ("image/png".equals(mimeTypeFilter) ||
            "image/*".equals(mimeTypeFilter) ||
            "*/*".equals(mimeTypeFilter)) {

            // Return the file in the specified format.
            return openPngDocument(documentId);

        } else if ("image/jpg".equals(mimeTypeFilter)) {
            return openJpgDocument(documentId);
        } else {
            throw new IllegalArgumentException("Invalid mimeTypeFilter " + mimeTypeFilter);
        }

    } catch (Exception ex) {
        Log.e(TAG, ex.getMessage());
    } finally {
        return null;
    }
}

@Override
public String[] getDocumentStreamTypes(String documentId, String mimeTypeFilter) {

    // Return all supported MIME tyupes if the client app
    // passes in '*/*' or 'image/*'.
    if ("*/*".equals(mimeTypeFilter) ||
        "image/*".equals(mimeTypeFilter)) {
        return SUPPORTED_MIME_TYPES;
    }

    ArrayList requestedMimeTypes = new ArrayList&lt;&gt;();

    // Iterate over the list of supported mime types to find a match.
    for (int i=0; i &lt; SUPPORTED_MIME_TYPES.length; i++) {
        if (SUPPORTED_MIME_TYPES[i].equals(mimeTypeFilter)) {
            requestedMimeTypes.add(SUPPORTED_MIME_TYPES[i]);
        }
    }
    return (String[])requestedMimeTypes.toArray();
}

নিরাপত্তা

ধরুন আপনার নথি প্রদানকারী একটি পাসওয়ার্ড-সুরক্ষিত ক্লাউড স্টোরেজ পরিষেবা এবং আপনি নিশ্চিত করতে চান যে ব্যবহারকারীরা তাদের ফাইলগুলি ভাগ করা শুরু করার আগে লগ ইন করেছেন৷ ব্যবহারকারী লগ ইন না করলে আপনার অ্যাপের কি করা উচিত? সমাধান হল আপনার queryRoots() বাস্তবায়নে শূন্য রুট ফিরিয়ে দেওয়া। অর্থাৎ, একটি খালি রুট কার্সার:

কোটলিন

override fun queryRoots(projection: Array<out String>): Cursor {
...
    // If user is not logged in, return an empty root cursor.  This removes our
    // provider from the list entirely.
    if (!isUserLoggedIn()) {
        return result
    }

জাভা

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 fun onLoginButtonClick() {
    loginOrLogout()
    getContentResolver().notifyChange(
        DocumentsContract.buildRootsUri(AUTHORITY),
        null
    )
}

জাভা

private void onLoginButtonClick() {
    loginOrLogout();
    getContentResolver().notifyChange(DocumentsContract
            .buildRootsUri(AUTHORITY), null);
}

এই পৃষ্ঠার সাথে সম্পর্কিত নমুনা কোডের জন্য, পড়ুন:

এই পৃষ্ঠার সাথে সম্পর্কিত ভিডিওগুলির জন্য, পড়ুন:

অতিরিক্ত সম্পর্কিত তথ্যের জন্য, পড়ুন: