Android開發之深入理解Android 7.0系統權限更改相關文檔


摘要:

Android 6.0之后的版本增加了運行時權限,應用程序在執行每個需要系統權限的功能時,需要添加權限請求代碼(默認權限禁止),否則應用程序無法響應;Android 7.0在Android 6.0的基礎上,對系統權限進一步更改,這次的權限更改包括三個方面:

  1. APP應用程序的私有文件不再向使用者放寬
  2. Intent組件傳遞file://URI的方式可能給接收器留下無法訪問的路徑,觸發FileUriExposedException異常,推薦使用FileProvider
  3. DownloadManager不再按文件名分享私人存儲的文件。舊版應用在訪問COLUMN_LOCAL_FILENAME時可能出現無法訪問的路徑。面向 Android 7.0 或更高版本的應用在嘗試訪問 COLUMN_LOCAL_FILENAME 時會觸發 SecurityException

簡單的三句話,無法讓TeachCourse真正理解Android 7.0系統權限更改的含義,如果不按照文檔的方式去做,API 24開發的應用程序是否就用不了?

Android 7.0系統權限變更

一、深入理解FileProvider

FileProvider屬於Android 7.0新增的一個類,該類位於v4包下,詳情可見android.support.v4.content.FileProvider,使用方法類似與ContentProvider,簡單概括為三個步驟,這里先以調用系統相機拍照並保存sdcard公共目錄為例,演示使用過程:

  • 在資源文件夾res/xml下新建file_provider.xml文件,文件聲明權限請求的路徑,代碼如下:
<?xml version="1.0" encoding="utf-8"?>
	<paths xmlns:android="http://schemas.android.com/apk/res/android">
	    <!--3、對應外部內存卡根目錄:Environment.getExternalStorageDirectory()-->
	    <external-path name="ext_root" path="/" />
	</paths>
  • AndroidManifest.xml添加組件provider相關信息,類似組件activity,指定resource屬性引用上一步創建的xml文件(后面會詳細介紹各個屬性的用法),代碼如下:
    <!-- 定義FileProvider -->
    <provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="@string/install_apk_path"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_provider" />
    </provider>
  • 最后一步,Java代碼申請權限,使用新增的方法getUriForFile()grantUriPermission(),代碼如下(后面會詳細介紹方法對應參數的使用):
    if (Build.VERSION.SDK_INT > 23) {
        /**Android 7.0以上的方式**/
        Uri contentUri = getUriForFile(this, getString(R.string.install_apk_path), file);
        grantUriPermission(getPackageName(), contentUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        intent.putExtra(MediaStore.EXTRA_OUTPUT, contentUri);
    } 
  • 修改build.gradle文件compileSdkVersion大於或等於24,targetSdkVersion等於24,使用Android 7.0模擬器運行Demo,效果圖:

Android 7.0系統權限更改

那么,我們已經了解Android 7.0系統權限申請的步驟,接下來說明每一個步驟需要注意的事項、相關方法參數的說明、屬性的含義以及可以的申請權限目錄(最后下載相關Demo)。

1.1 定義一個FileProvider

直接使用FileProvider本身或者它的子類,需要在AndroidManifest.xml文件中聲明組件的相關屬性,包括:

  • android:name,對應屬性值:android.support.v4.content.FileProvider或者子類完整路徑
  • android:authorities,對應屬性值是一個常量,通常定義的方式packagename.fileprovider,例如:cn.teachcourse.fileprovider
  • android:exported ,對應屬性值是一個boolean變量,設置為false
  • android:grantUriPermissions,對應屬性值也是一個boolean變量,設置為true,允許獲得文件臨時的訪問權限
<manifest>
    ...
    <application>
        ...
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.mydomain.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            ...
        </provider>
        ...
    </application>
</manifest>

想要關聯res/xml文件夾下創建的file_provider.xml文件,需要在<provider>標簽內,添加<meta-data>子標簽,設置<meta-data>標簽的屬性值,包括:

  • android:name,對應屬性值是一個固定的系統常量android.support.FILE_PROVIDER_PATHS
  • android:resource,對應屬性值指向我們的xml文件@xml/file_provider
<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="com.mydomain.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_provider" />
</provider>

1.2 指定授予臨時訪問權限的文件目錄

上一步說明了怎么定義一個FileProvider,這一步主要說明怎么定義一個@xml/file_provider文件。Android Studio或Eclipse開發工具創建Android項目的時候默認不會創建res/xml文件夾,需要開發者手動創建,點擊res文件夾新建目錄,命名xml,如下圖:

Android Studio新建xml目錄

然后,在xml文件夾下新建一個xml文件,文件命名file_provider.xml,指定根標簽為paths,如下圖:

xml新建file_provider.xml

在xml文件中指定文件存儲的區塊和區塊的相對路徑,在<paths>根標簽中添加<files-path>子標簽(稍后詳細列出所有子標簽),設置子標簽的屬性值,包括:

  • name,是一個虛設的文件名(可以自由命名),對外可見路徑的一部分,隱藏真實文件目錄
  • path,是一個相對目錄,相對於當前的子標簽<files-path>根目錄
  • <files-path>,表示內部內存卡根目錄,對應根目錄等價於Context.getFilesDir(),查看完整路徑:
    /data/user/0/cn.teachcourse.demos/files
  • 代碼如下:
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <files-path name="my_images" path="images/"/>
    ...
</paths>

<paths>根標簽下可以添加的子標簽也是有限的,參考官網的開發文檔,除了上述的提到的<files-path>這個子標簽外,還包括下面幾個:

  1. <cache-path>,表示應用默認緩存根目錄,對應根目錄等價於getCacheDir(),查看完整路徑:/data/user/0/cn.teachcourse.demos/cache

  2. <external-path>,表示外部內存卡根目錄,對應根目錄等價於
    Environment.getExternalStorageDirectory()
    查看完整路徑:/storage/emulated/0

  3. <external-files-path>,表示外部內存卡根目錄下的APP公共目錄,對應根目錄等價於
    Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)
    查看完整路徑:
    /storage/emulated/0/Android/data/cn.teachcourse.demos/files/Download

  4. <external-cache-path>,表示外部內存卡根目錄下的APP緩存目錄,對應根目錄等價於Context.getExternalCacheDir(),查看完整路徑:
    /storage/emulated/0/Android/data/cn.teachcourse.demos/cache

最終,在file_provider.xml文件中,添加上述5種類型的臨時訪問權限的文件目錄,代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <!--
    1、name對應的屬性值,開發者可以自由定義;
    2、path對應的屬性值,當前external-path標簽下的相對路徑
    比如:/storage/emulated/0/92Recycle-release.apk
    sdcard路徑:/storage/emulated/0(WriteToReadActivity.java:176)
                      at cn.teachcourse.nougat.WriteToReadActivity.onClick(WriteToReadActivity.java:97)
                      at android.view.View.performClick(View.java:5610)
                      at android.view.View$PerformClick.run(View.java:22265)
    相對路徑:/
    -->
    <!--1、對應內部內存卡根目錄:Context.getFileDir()-->
    <files-path
        name="int_root"
        path="/" />
    <!--2、對應應用默認緩存根目錄:Context.getCacheDir()-->
    <cache-path
        name="app_cache"
        path="/" />
    <!--3、對應外部內存卡根目錄:Environment.getExternalStorageDirectory()-->
    <external-path
        name="ext_root"
        path="pictures/" />
    <!--4、對應外部內存卡根目錄下的APP公共目錄:Context.getExternalFileDir(String)-->
    <external-files-path
        name="ext_pub"
        path="/" />
    <!--5、對應外部內存卡根目錄下的APP緩存目錄:Context.getExternalCacheDir()-->
    <external-cache-path
        name="ext_cache"
        path="/" />
</paths>

1.3 生成指定文件的Content URI

Content URI方便與另一個APP應用程序共享同一個文件,共享的方式通過ContentResolver.openFileDescriptor獲得一個ParcelFileDescriptor對象,讀取文件內容。那么,如何生成一條完整的Content URI呢?TeachCourse總結后,概括為三個步驟,第一步:明確上述5種類型中的哪一種,第二步:明確指定文件的完整路徑(包括目錄、文件名),第三步:調用getUriForFile()方法生成URI

File imagePath = new File(Environment.getExternalStorageDirectory(), "download");
File newFile = new File(imagePath, "default_image.jpg");
Uri contentUri = getUriForFile(getContext(), "cn.teachcourse.fileprovider", newFile);

1.4 授予Content URI臨時訪問權限

上一步獲得的Content URI,並沒有獲得指定文件的讀寫權限,想要獲得文件的讀寫權限需要調用Context.grantUriPermission(package, Uri, mode_flags)方法,該方法向指定包名的應用程序申請獲得讀取或者寫入文件的權限,參數說明如下:

  • package,指定應用程序的包名,Android Studio真正的包名指build.gradle聲明的applicationId屬性值;getPackageName()AndroidManifest.xml文件聲明的package屬性值,如果兩者不一致,就不能提供getPackageName()獲取包名,否則報錯!
  • Uri,指定請求授予臨時權限的URI,例如:contentUri
  • mode_flags,指定授予臨時權限的類型,選擇其中一個常量或兩個:Intent.FLAG_GRANT_READ_URI_PERMISSIONIntent.FLAG_GRANT_WRITE_URI_PERMISSION

授予文件的臨時讀取或寫入權限,如果不再需要了,TeachCourse該如何撤銷授予呢?撤銷權限有兩種方式:第一種:通過調用revokeUriPermission()撤銷,第二種:重啟系統后自動撤銷

1.5 對外提供可訪問的Content URI

有多種方式可以向客戶端APP提供可訪問文件的Content URI,其中一種常用的方式是通過發送Intent給需要啟動的Activity,在重寫的startActivityResult()方法中獲取授予臨時權限的Content URI或向用戶提供可訪問的接口來獲取文件,后面的這種方式獲取文件后轉換成Content URI,以文章開頭拍照的功能為例,TeachCourse想要在sdcard的公共目錄pictures/查看已保存的照片,實現過程:

  • 請求授予訪問公共目錄的權限,代碼如下:
    if (Build.VERSION.SDK_INT > 23) {
        /**Android 7.0以上的方式**/
        mStorageManager = this.getSystemService(StorageManager.class);
        StorageVolume storageVolume = mStorageManager.getPrimaryStorageVolume();
        Intent intent = storageVolume.createAccessIntent(Environment.DIRECTORY_PICTURES);
        startActivityForResult(intent, REQUEST_CODE_GRAINT_URI);
    }
  • 在重寫的startActivityResult()方法中獲取授予臨時權限的Content URI,代碼如下:
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        switch (requestCode) {
            case REQUEST_CODE_GRAINT_URI:
                updateDirectoryEntries(data.getData());
                Log.d(TAG, "onActivityResult:Uri= "+data.getData());
                break;

        }
    }
  • 查詢Environment.DIRECTORY_PICTURES目錄,返回的Content URI包含的文件和文件類型相關信息,代碼如下:
    private static final String[] DIRECTORY_SELECTION = new String[]{
            DocumentsContract.Document.COLUMN_DISPLAY_NAME,
            DocumentsContract.Document.COLUMN_MIME_TYPE,
            DocumentsContract.Document.COLUMN_DOCUMENT_ID,
    };
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private void updateDirectoryEntries(Uri uri) {
        ContentResolver contentResolver = this.getContentResolver();
        Uri docUri = DocumentsContract.buildDocumentUriUsingTree(uri,
                DocumentsContract.getTreeDocumentId(uri));
        Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri,
                DocumentsContract.getTreeDocumentId(uri));

        try (Cursor docCursor = contentResolver
                .query(docUri, DIRECTORY_SELECTION, null, null, null)) {
            while (docCursor != null && docCursor.moveToNext()) {
                mPath_tv.setText(docCursor.getString(docCursor.getColumnIndex(
                        DocumentsContract.Document.COLUMN_DISPLAY_NAME)));
            }
        }

        try (Cursor childCursor = contentResolver
                .query(childrenUri, DIRECTORY_SELECTION, null, null, null)) {
            while (childCursor != null && childCursor.moveToNext()) {
                String fileName = childCursor.getString(childCursor.getColumnIndex(
                        DocumentsContract.Document.COLUMN_DISPLAY_NAME));
                String mimeType = childCursor.getString(childCursor.getColumnIndex(
                        DocumentsContract.Document.COLUMN_MIME_TYPE));
                Log.e(TAG, "updateDirectoryEntries: "+fileName+"\n"+mimeType);
            }

        }
    }

運行Demo,控制台打印效果圖:

Android 7.0訪問sdcard

更多說明,可以參考Google提供的例子

二、深入理解DownloadManager

同樣,為了方便理解DownloadManager的用法,首先以一個簡單例子開始:從指定的url下載資源,然后顯示下載資源的相關信息,運行Demo的效果圖:

DownloadManager詳解

Android 7.0系統權限更改的第三點,簡單的說:通過訪問COLUMN_LOCAL_FILENAME,在Android 7.0系統上可能無法獲取Demo效果圖fileName對應的文件路徑,這時候可能觸發異常SecurityException,打印的log信息,如下:

      Caused by: java.lang.SecurityException: COLUMN_LOCAL_FILENAME is deprecated; use ContentResolver.openFileDescriptor() instead
      at android.app.DownloadManager$CursorTranslator.getString(DownloadManager.java:1499)
      at cn.teachcourse.download.DownloadManagerActivity.query(DownloadManagerActivity.java:244)
      at cn.teachcourse.download.DownloadManagerActivity.access$100(DownloadManagerActivity.java:34)
      at cn.teachcourse.download.DownloadManagerActivity$1.onReceive(DownloadManagerActivity.java:186)
      at android.app.LoadedApk$ReceiverDispatcher$Args.run(LoadedApk.java:1122)
      at android.os.Handler.handleCallback(Handler.java:751) 
      at android.os.Handler.dispatchMessage(Handler.java:95) 
      at android.os.Looper.loop(Looper.java:154) 
      at android.app.ActivityThread.main(ActivityThread.java:6077) 
      at java.lang.reflect.Method.invoke(Native Method) 
      at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:865) 
      at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:755) 

2.1 關於DownloadManager

DownloadManager是一個用於處理長時間HTTP請求的系統服務,客戶端請求的URI可能是將要下載的指定的文件,處於后台的下載管理器將控制着下載的任務,並監測下載的狀態,在下載失敗或連接改變以及系統重啟后嘗試重新下載。

  • 如何初始化DownloadManager實例?首先調用getSystemService(String)方法,傳入DOWNLOAD_SERVICE常量,來初始化DownloadManager實例,代碼如下:
mDownloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
  • 如何配置請求參數?首先需要使用到內部類DownloadManager.Request,查看源碼學習該類的各個方法的使用,TeachCourse簡單總結:該類主要用於配置一條新下載任務相關內容,這些內容包括下載任務的保存路徑,下載任務所處的網絡狀態(WiFi或流量狀態)和下載任務通知欄顯示樣式等等,代碼如下:
/**
 * 設置請求下載的數據
 */
private void initData() {
    //Request內部類配置新下載任務相關內容,比如:保存路徑,WiFi或流量狀態,下載通知欄樣式
    request = new DownloadManager.Request(Uri.parse(mUrl + mFileName));
    request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, mFileName);
    request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE);
    request.setTitle("正在下載應用程序");
    request.setDescription("92回收,就愛回收APP");
    request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE);
}
  • 如何開啟下載任務?下載任務參數配置完成后,就可以開啟后台服務下載,同一個DownloadManager實例,可以開啟多個下載任務,需要上一步中配置多條URI,每個下載任務分配唯一的id,代碼如下:
/**
 * 下載任務的唯一標識ID,用於查詢下載文件的相關信息
 */
private void start() {
    mDownloadUniqueId = mDownloadManager.enqueue(request);
    mDownloadManager_btn.setText("正在下載。。。");
    mDownloadManager_btn.setClickable(false);
}
  • DownloadManager通過兩種狀態的廣播,第一種:任務下載完成后發送,廣播攔截器過濾action是DownloadManager.ACTION_DOWNLOAD_COMPLETE(關於廣播的知識,不懂的可以參考TeachCourse博客另外的幾篇文章);第二種:點擊通知欄進度條后發送,廣播攔截器過濾action是DownloadManager.ACTION_NOTIFICATION_CLICKED,代碼如下:
/**
 * 注冊下載完成廣播接收器,還可以注冊其它監聽器,比如:DownloadManager.ACTION_NOTIFICATION_CLICKED
 */
private void registerReceiverCompleted() {
    IntentFilter intentFilter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
    registerReceiver(mBroadcastReceiver, intentFilter);
}
/**
 * 接收下載完成廣播
 */
private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        long reference = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
        if (mDownloadUniqueId == reference) {
            query(reference);
            mShowInformation_tv.setText(information);
            mDownloadManager_btn.setText("點擊下載");
            mDownloadManager_btn.setClickable(true);
        }
    }
};
  • 如何查詢下載任務的相關信息?首先需要使用到內部類DownloadManager.Query,查看源碼學習該類各個方法的使用,TeachCourse簡單總結:該類正如文章開頭樣式的例子,通過分配的id查詢下載任務相關的信息,這些信息包括文件類型、文件的Uri和文件的長度等,代碼如下:
/**
 * 查詢下載任務相關的信息,比如:文件名、文件大小、文件類型等
 *
 * @param reference
 */
private void query(long reference) {
    DownloadManager.Query query = new DownloadManager.Query();
    /**指定查詢條件**/
    query.setFilterById(reference);
    /**查詢正在等待、運行、暫停、成功、失敗狀態的下載任務**/
    query.setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL);

    Cursor cursor = mDownloadManager.query(query);
    if (cursor.moveToFirst()) {
        int fileId = cursor.getColumnIndex(DownloadManager.COLUMN_ID);
        int fileTitleId = cursor.getColumnIndex(DownloadManager.COLUMN_TITLE);
        int fileDescriptionId = cursor.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION);
        int fileTypeId = cursor.getColumnIndex(DownloadManager.COLUMN_MEDIA_TYPE);
        int fileLengthId = cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES);
        int fileUriId = cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI);
        /**過時的方式:DownloadManager.COLUMN_LOCAL_FILENAME**/
        int fileNameId = cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME);
        int statusCodeId = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS);
        int statusReasonId = cursor.getColumnIndex(DownloadManager.COLUMN_REASON);
        int downloadSizeId = cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR);
        int lastModifiedTimeId = cursor.getColumnIndex(DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP);
        int mediaUriId = cursor.getColumnIndex(DownloadManager.COLUMN_MEDIAPROVIDER_URI);

        String id = cursor.getString(fileId);
        String fileTitle = cursor.getString(fileTitleId);
        String description = cursor.getString(fileDescriptionId);
        String type = cursor.getString(fileTypeId);
        String length = cursor.getString(fileLengthId);
        String statusCode = cursor.getString(statusCodeId);
        String statusReason = cursor.getString(statusReasonId);
        String downloadSize = cursor.getString(downloadSizeId);
        String modifiedTime = cursor.getString(lastModifiedTimeId);
        String mediaUri = cursor.getString(mediaUriId);
        String fileUri = cursor.getString(fileUriId);
        String fileName = null;
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
            openFile(type, Uri.parse(fileUri));
            fileName = Uri.parse(fileUri).getPath();
        } else {
            /**Android 7.0以上的方式:請求獲取寫入權限,這一步報錯**/
            fileName = cursor.getString(fileNameId);
            openFile(type, Uri.parse(fileUri));
        }


        /**清空StringBuffer存儲的數據**/
        mStringBuffer.delete(0, mStringBuffer.length());
        mStringBuffer.append("id:" + id + "\n");
        mStringBuffer.append("fileTitle:" + fileTitle + "\n");
        mStringBuffer.append("description:" + description + "\n");
        mStringBuffer.append("type:" + type + "\n");
        mStringBuffer.append("length:" + length + "\n");
        mStringBuffer.append("fileName:" + fileName + "\n");
        mStringBuffer.append("fileUri:" + fileUri + "\n");
        mStringBuffer.append("statusCode:" + statusCode + "\n");
        mStringBuffer.append("statusReason:" + statusReason + "\n");
        mStringBuffer.append("downloadSize:" + downloadSize + "\n");
        mStringBuffer.append("modifiedTime:" + modifiedTime + "\n");
        mStringBuffer.append("mediaUri:" + mediaUri + "\n");
        information = mStringBuffer.toString();


    }
    cursor.close();
}
  • 代碼加入判斷語句,如果非Android 7.0系統繼續訪問COLUMN_LOCAL_FILENAME獲得文件存儲的絕對路徑(上面中間部分代碼),openFile()方法代碼如下:
/**
 * 根據文件的類型,指定可以打開的應用程序
 *
 * @param type
 * @param uri
 */
private void openFile(String type, Uri uri) {
    if (type.contains("image/")) {
        try {
            ParcelFileDescriptor descriptor = getContentResolver().openFileDescriptor(uri, "r");
            FileDescriptor fileDescriptor = descriptor.getFileDescriptor();
            Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
            mShowPic_iv.setVisibility(View.VISIBLE);
            mShowPic_iv.setImageBitmap(bitmap);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
}

現在,我們已經掌握了DownloadManager怎么實例化、怎么配置下載任務、怎么開啟后台服務以及如何查詢任務相關信息,想要實現一個應用程序版本更新就變得很簡單,實現多任務下載也不是難事,完整源碼參考文章后台通過的Demo。

三、關於ParcelFileDescriptor和FileDescriptor總結

官網的文檔推薦我們使用ContentResolver.openFileDescriptor()方法,獲得一個ParcelFileDescriptor對象,再通過getFileDescriptor()方法返回一個FileDescriptor,它們之間的關系參考上面的代碼。

FileDescriptor通常被稱為文件描述符,可以理解成本地的一個文件,通過流的方式讀取文件內容以及通過流的方式寫入數據到文件,這里是讀取或寫入數據到FileDescriptor中,假如我們的Uri表示的是一個txt文件,獲取FileDescriptor對象后,通過下面的代碼讀取txt文件的內容:

FileInputStream fis = new FileInputStream(fd);

同理,寫入數據到txt文件,代碼如下:

FileOutputStream out = new FileOutputStream(fd);
				 out.write('寫入數據到txt文件中');
				 out.close();

獲取到輸入流或輸出流后,剩下的就是關於流的操作了,划分為:文件字節流文件字符流緩沖流數組流

3.1 改寫上面的例子

openFile()方法使用封裝好的decodeFileDescriptor(),查看BitmapFactory.decodeFileDescriptor()相關源碼,學習如何讀取文件描述符中的內容,這里TeachCourse根據讀取流的方式,改寫如下:

   ...
Bitmap bitmap = BitmapFactory.decodeStream(getStreamByFileDescriptor(fileDescriptor));
   ...
/**
 * 通過流的方式讀取內容
 *
 * @param fileDescriptor
 * @return
 */
private InputStream getStreamByFileDescriptor(FileDescriptor fileDescriptor) {
    return new FileInputStream(fileDescriptor);
}

於是,可以對FileDescriptor進行簡單的封裝成writeData()readData(),代碼如下:

/**往FileDescriptor中寫入數據
 * @param fileDescriptor
 * @param content
 */
private void writeData(FileDescriptor fileDescriptor, String content) {
    FileOutputStream fos = new FileOutputStream(fileDescriptor);
    try {
        fos.write(content.getBytes());
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

/**從FileDescriptor中讀取數據
 * @param fileDescriptor
 * @return
 */
private String readData(FileDescriptor fileDescriptor) {
    FileInputStream fis = new FileInputStream(fileDescriptor);
    byte[] b = new byte[1024];
    int read;
    String content=null;
    try {
        while ((read = fis.read(b)) != -1) {
            content = new String(b, 0, read);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return content;
}

總結:

Android 7.0系統的權限更改,包括三個方面,文章從第二方面開始講解,着重介紹了FileProviderDownloadManager兩個類的使用,花了好長時間整理、測試和編輯,如果對你有幫忙,別忘了收藏和分享咯!

  1. FileProvider源碼路徑:nougat/WriteToReadActivity.java
  2. DownloadManager源碼路徑:download/DownloadActivity.java
  3. Demo源碼

參考資料:https://developer.android.google.cn/about/versions/nougat/android-7.0-changes.html

版權聲明:本文著作權歸TeachCourse所有,未經許可禁止轉載,謝謝支持!
轉載請注明出處:http://teachcourse.cn/2379.html 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM