Android7.0新特性——FileProvider的使用

android.jpg

前言

記得剛學java的時候,那個體育老師就告訴我們在java的世界里萬物皆對象。沒錯,我的java是體育老師教的!既然是對象,都會經(jīng)歷不斷的進化,就像人類由最初的猿人慢慢的進化到如今的高等人類。Android的世界亦如此,經(jīng)歷一代又一代的迭代,如今Android已經(jīng)推出了8.0的版本。不過目前大多數(shù)公司應該沒適配8.0吧,國內(nèi)的手機也沒幾個是8.0系統(tǒng)吧(我只是推測,因為我上個月買的華為榮耀V9還是7.0版本,已經(jīng)是很新的手機了)。每一個版本的迭代,必定會帶來一些技術上的變革,當然肯定是朝著更為人性化的方向變革。今天要說的就是Android7.0中的一個新特性——FileProvider。

問題描述

在Android7.0之前,第三方的應用可以訪問我們自身應用的私有路徑。比如做一些第三方分享,通過Intent的方式,分享一張圖片。只需要通過圖片的路徑就可以將圖片找到然后暴露給第三方平臺,以供分享。但是,在Android7.0以后,出于保護用戶隱私的考慮,第三方應用是不可以隨便訪問我們APP的私有路徑的。這里說不能隨便訪問,也就是說還是可以訪問的,只是會變得矜持些,不那么隨便了。
這里就要引入FileProvider這個類了,字面意思就是文件提供者,就是提供圖片或者視頻文件的。那么這個類到底怎么使用呢?凡事靠對比,才能看出二者的差異,下面我就從兩個使用場景來指出在7.0上這兩個場景與之前版本的差異。

FileProvider的使用

一、拍照

1.準備工作:

(1) 既然是適配7.0的系統(tǒng),那么首先需要在build.gradle中配置相應的SDK版本號,我的配置如下:

gradle.png

(2) 在清單文件中,需要加入相機權限和寫權限,如下:

permission.png

2.調(diào)用相機拍照:

(1) 7.0以前的調(diào)用方式:
 private void takePhotoOld() {
      Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);   // 調(diào)起相機的意圖
      if (intent.resolveActivity(this.getPackageManager()) != null) {
           File file = new File(getCameraSavePath());
           intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file));
           this.startActivityForResult(intent, 1000);   // 為了驗證圖片存儲成功我選擇在onActivityResult方法中
                                                           將它設置到一個ImageView上,代碼就不上了,很簡單
      }
  }

 // 照完的圖片存儲路徑(可以自行定義)
 private String getCameraSavePath() {
      String sdcardPath = null;
      if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
          sdcardPath = Environment.getExternalStorageDirectory().getPath();
      } else {
          sdcardPath = Environment.getDataDirectory().getPath() + "/data/" + "com.demo.android";
      }
      initDataDirectory(sdcardPath + "/Demo/");   // 注意:這里必須對路徑進行初始化,否則文件無法創(chuàng)建,導致照相之后無法返回Activity
      return sdcardPath + "/Demo/Camera.png";
   }

 // 初始化路徑
 private void initDataDirectory(String path) {
     File file = new File(path);
     if (!file.exists()) {
         file.mkdirs();
     }
  }

以上就是在7.0之前調(diào)用系統(tǒng)相機拍照并保存圖片的方法,下面再來看一下7.0以后是如何調(diào)用的。在上代碼之前說一下,如果在7.0的手機上也調(diào)用以上方法調(diào)用系統(tǒng)相機拍照,會導致程序崩潰并拋出以下異常。

error.png

從字面意思理解就是暴露文件路徑異常,驗證了我開始說的吧,7.0以上系統(tǒng)不再那么隨便的將自身的私有路徑暴露給第三方應用程序了。有人問了,相機也是第三方應用程序嗎,這個答案是這樣,當然是啦!好了,那下面就看一下如何成功的在7.0以上系統(tǒng)調(diào)用系統(tǒng)相機進行拍照。(這里需要說到一點,就是關于6.0動態(tài)權限的問題,在這先忽略,講解完7.0調(diào)用相機的方法之后我會寫上針對目前系統(tǒng)都適配的完整的調(diào)用系統(tǒng)相機的代碼)

(2) 7.0以后的調(diào)用方式:

1.首先,需要在清單文件中聲明一個provider,如下:

  <provider
      android:name="android.support.v4.content.FileProvider"                                                                  
      android:authorities="com.android.demo.fileProvider"   // 名字,這個可以自行設置,通常都設置為包名.fileProvider
      android:exported="false"
      android:grantUriPermissions="true">
      <meta-data
          android:name="android.support.FILE_PROVIDER_PATHS"
          android:resource="@xml/camera_path" />   // 在res下創(chuàng)建xml文件夾
  </provider>

之所以需要聲明,是因為FileProvider是身為Android四大組件之一的ContentProvider的子類,因此需要在清單文件中聲明。

2.剛才的聲明的最后一行,看到了一個xml文件。這個文件的作用是為FileProvider提供可以暴露的路徑,一旦一個路徑在文件中被聲明,那么就可以被FileProvider提供。下面看一下這個xml文件中的內(nèi)容:

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-path path="Demo/" name="Camera" />   // 外部存儲,本文案例用的就是這個,注意這里的path就是共享的圖片路徑(我前面設置的文件路徑就是外部存儲下的Demo文件夾),name代表使用這個字段去訪問真實的文件路徑
    // 以下是path可以設置的其他根節(jié)點路徑
    <root-path/>   // 設備根目錄,等同于直接new File("/")
    <files-path/>    // 內(nèi)部存儲空間應用私有目錄下的files/ 目錄,等同于Context.getFilesDir() 所獲取的目錄路徑
    <cache-path/>    // 內(nèi)部存儲空間應用私有目錄下的cache/ 目錄,等同于Context.getCacheDir() 所獲取的目錄路徑
    <external-files-path>   // 外部存儲空間應用私有目錄下的files/ 目錄,等同于Context.getExternalFilesDir(null) 所獲取的目錄路徑
    <external-cache-path>   // 外部存儲空間應用私有目錄下的cache/ 目錄,等同于Context.getExternalCacheDir()
</paths>

應該很多朋友好奇為什么要將路徑寫到這么一個xml文件里。因為我們現(xiàn)在使用FileProvider來提供這個文件,而FileProvider是ContentProvider的子類,它用content:// Uri 代替了 file:/// Uri。因此需要通過path以及name一起來供FileProvider來找到文件的位置。這樣也更加安全的向第三方程序提供文件內(nèi)容了。

3.接下來,就來看看FileProvider類是如何幫助我們來調(diào)用系統(tǒng)相機的:

 private void takePhoto7() {
     Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
     if (intent.resolveActivity(this.getPackageManager()) != null) {
         File file = new File(getCameraSavePath());
         Uri uri = FileProvider.getUriForFile(this, "com.android.demo.fileProvider", file);   // 主要就是這行代碼,通過FileProvider獲取文件的uri
         intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
         this.startActivityForResult(intent, 1000);
     } 
  }

getUriFromFile方法中的第二個參數(shù)就是我們在清單文件中聲明的provider的authorities。(一定要一樣)

4.在7.0的手機上運行后,就會發(fā)現(xiàn)成功的調(diào)用了系統(tǒng)的相機并實現(xiàn)了拍照。同時,可以打印出uri。我這個案例中的uri為:

content://com.android.demo.fileProvider/Camera/Camera.png

怎么樣,是不是驗證我上面說的。

(3) 針對全部機型適配后的方法:

1.前面說到了,沒有對6.0機型的動態(tài)權限進行適配。如果沒有這個權限處理的話,那么在6.0以上的機型上進行此操作等待你的將是崩潰,因為啟用照相機需要訪問照相機權限以及寫權限。因此,這里在調(diào)用相機操作之前,應該先處理權限問題。在6.0系統(tǒng)發(fā)布不久后,網(wǎng)上關于動態(tài)權限處理這一塊就出現(xiàn)了好多的開源庫供使用,基本方法都差不多,這里我舉出EasyPermissions這個開源項目做舉例。(關于這個庫的使用可以自行百度,很簡單)

2.這里,我直接上代碼,在代碼中會有注釋,保證每一步都能簡單易懂(我只寫了用到的方法):

 public class MainActivity implements EasyPermissions.PermissionCallbacks {

      // 權限請求的回調(diào)結果,有成功失敗兩種結果,分別對應granted和denied兩種狀態(tài)。
      @Override
      public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
            // 在這里將回調(diào)結果賦給該方法,然后此方法會根據(jù)權限請求的成功與否分別調(diào)用下面兩個方法
            EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
      }

      // 當權限獲取成功時,回調(diào)此方法
      @Override
      public void onPermissionsGranted(int requestCode, List<String> perms) {
            // 在這里,再來調(diào)用照相方法
             if (requestCode == 1001) {
                 takePhoto();
             }
      }
 
      // 當權限獲取失敗時,回調(diào)此方法
      @Override
      public void onPermissionsDenied(int requestCode, List<String> perms) {
            // 這里可以給用戶一些提示信息,比如告訴用戶無此權限無法正常使用相機功能
      }

      // 檢查權限并調(diào)用相機的方法
      private void checkPermissionAndTakePhoto() {
          // 判斷是否有相機和寫權限
          if (EasyPermissions.hasPermissions(this, Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
              // 有的話直接調(diào)用照相方法
              takePhoto();
          } else {
              // 沒有的話去請求權限
              EasyPermissions.requestPermissions(this, "請求相機和寫入權限",
              1001,    // 這個請求碼自定義
              Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE);
          }
      }

      // 調(diào)用相機拍照的方法
      private void takePhoto() {
          Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);   // 調(diào)用相機的意圖

          if (intent.resolveActivity(this.getPackageManager()) != null) {
              File photoFile = new File(getCameraSavePath());   // 圖片保存的文件
              // 版本判斷
              if (Build.VERSION.SDK_INT >= 24) {
                    Uri uri = FileProvider.getUriForFile(this, "com.android.demo.fileProvider", photoFile);
                    intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
               } else {
                    intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(photoFile));
               }

               startActivityForResult(intent, 1000);
           }
       }
  }

3.嗯,以上就是完整的針對目前各個系統(tǒng)都能用的調(diào)用系統(tǒng)相機的方法了。主要就是掌握FileProvider的使用,以及6.0權限檢查機制。

二、向第三方應用分享(圖片或者視頻)

(1) 描述:

不知道大家知不知道國外的一些比較出名的APP, 例如Instagram,Youtube,Twitter等。這些都是一些海外用戶手機必備的一些軟件。就像在國內(nèi)微信,微信視頻,微博一樣非常的火熱。那么通常海外的一些APP為了增加自己產(chǎn)品的知名度,通常會選擇向這些APP渠道上發(fā)布自己產(chǎn)品的一些信息,也就是為我們常說的第三方分享。但是國內(nèi)的一些APP分享一般都是提供相應的SDK以及調(diào)用文檔,我們調(diào)用相應的API即可實現(xiàn)在對應的平臺上分享文字圖片或視頻等。
然而,還有一種分享不需要調(diào)用相比之下比較繁瑣的API接口,只需要通過Intent即可調(diào)起相應的客戶端實現(xiàn)分享。很多產(chǎn)品的分享更多這個功能,就是利用Intent來調(diào)起可以實現(xiàn)相應分享類型的客戶端的。下面,我就來說明一下具體的實現(xiàn)步驟,以及在這個過程中對FileProvider的使用。

(2) 調(diào)用第三方客戶端分享

1.確定Intent的分享類型:

   // 1.這里的第一個參數(shù)圖片路徑,一定要和上面相機圖片的存儲路徑一樣 也要在xml文件中聲明。
      2.這里用的FileProvider也和上面是一個。

   public Intent makeShareIntent(String shareImagePath, String shareContent) {   
        Intent intent = new Intent(Intent.ACTION_SEND);   // 指定Intent用于分享
        File file = null;
        if (!TextUtils.isEmpty(shareImagePath)) {
            file = new File(shareImagePath);
        }

        if (file != null && file.exists()) {
            if (Build.VERSION.SDK_INT >= 24) {   // 7.0以上適配
                Uri uri = FileProvider.getUriForFile(context, "com.android.demo.fileProvider", file);
                intent.putExtra(Intent.EXTRA_STREAM, uri);
            } else {   // 7.0以下
                intent.putExtra(Intent.EXTRA_STREAM, Uri.parse(shareImagePath));
            }
            intent.setType("image/*");   // 設置分享的類型為分享圖片(視頻為video/*)
        } else {
            intent.setType("text/plain");   // 當路徑為空將分享類型設為分享文字
        }
        intent.putExtra(Intent.EXTRA_TEXT, shareContent);

        return intent;
    }

(這里額外說明一點,其實可以不進行SDK的版本適配,所有的系統(tǒng)都用FileProvider獲取Uri。但這是需要給這個Intent設置一個flag標記如下):

intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);  (如果不加在7.0以下的機型就會發(fā)生崩潰,具體原因可以自行研究,不多說了!)
2.尋找指定的第三方平臺:
   public void startShareActivity(Context context, String pkgName, 
                                  String shareImagePath, String shareContent) {
        ResolveInfo resolveInfo = null;   // APK文件信息類
        Intent intent = makeShareIntent(shareImagePath, shareContent);   // 指定分享類型的Intent,上面的方法
        PackageManager packageManager = context.getPackageManager();   // 應用程序管理類
        List<ResolveInfo> apks = packageManager.queryIntentActivities(intent, 0);   // 查詢所有能分享此類型Intent的應用,
                                                                                       并為其附一個標記0
        int apkNum = apks.size();
        for (int i = 0; i < apkNum; i++) {
            ResolveInfo info = apks.get(i);
            if (pkgName.equals(info.activityInfo.packageName)) {    // 查找與指定包名一致的應用
                resolveInfo = info;
                break;
            }
        }

        ActivityInfo ai = resolveInfo.activityInfo;   // 獲取應用的節(jié)點信息
        intent.setComponent(new ComponentName(ai.applicationInfo.packageName, ai.name));   // 獲取應用包名和應用程序名
        context.startActivity(intent);   // 啟動第三方應用進行分享
    }
3.彈出所有能分享此類型的應用列表
    public void showAllShareActivities(Context context, String shareImagePath, String shareContent) {
        Intent intent = makeAllShareIntent(shareImagePath, shareContent);   // 確定指定分享類型的Intent
        context.startActivity(Intent.createChooser(intent, "Share"));   // 調(diào)起所有能分享的應用菜單
    }
以上FileProvider在像第三方應用共享文件時的使用方式。

三、總結:寫了兩種使用場景,其實FileProvider的使用方式都是一樣的。無非以下幾步:

(1). 在xml聲明可訪問的路徑
(2). 在清單文件中注冊FileProvider
(3). 在代碼中調(diào)用注冊的FileProvider。(注意系統(tǒng)版本適配,F(xiàn)ileProvider的名字一定不要寫錯)
end.jpg
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容