FileProvider N 7.0 升級 安裝APK 選擇文件 拍照 臨時權限 MD


Markdown版本筆記 我的GitHub首頁 我的博客 我的微信 我的郵箱
MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina.com

FileProvider N 7.0 升級 安裝APK 選擇文件 拍照 臨時權限 MD


目錄

問題

我們在開發 app 時避免不了需要添加應用內升級功能。當 app 啟動時,如果檢測到最新版本,將 apk 安裝包從服務器下載下來,執行安裝。
安裝apk的代碼一般寫法如下,網上隨處可以搜到

public static void installApk(Context context, File file) {
    Intent intent = new Intent(Intent.ACTION_VIEW);
    Uri data = Uri.fromFile(file); //核心代碼
    intent.setDataAndType(data, "application/vnd.android.package-archive");
    context.startActivity(intent);
}

然而,當我們在Android7.0手機中執行時,會發現會報如下錯誤日志:

Caused by: android.os.FileUriExposedException: file:///storage/emulated/0/Android/data/net.csdn.blog.ruancoder/cache/test.apk exposed beyond app through Intent.getData()
  at android.os.StrictMode.onFileUriExposed(StrictMode.java:1799)
  at android.net.Uri.checkFileUriExposed(Uri.java:2346)
  at android.content.Intent.prepareToLeaveProcess(Intent.java:8933)
  at android.content.Intent.prepareToLeaveProcess(Intent.java:8894)
  at android.app.Instrumentation.execStartActivity(Instrumentation.java:1517)
  at android.app.Activity.startActivityForResult(Activity.java:4224)
  at android.support.v4.app.BaseFragmentActivityJB.startActivityForResult(BaseFragmentActivityJB.java:50)
  at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:79)
  at android.app.Activity.startActivityForResult(Activity.java:4183)
  at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:859)
  at android.app.Activity.startActivity(Activity.java:4507)
  at android.app.Activity.startActivity(Activity.java:4475)

官方文檔的相關描述

FileUriExposedException官方文檔 的一些描述:

  • 當應用程序將文件以【file://】形式的Uri公開到另一個應用程序時拋出的異常
  • 不鼓勵這種曝光方式,因為接收的app可能無法訪問你所共享的路徑。例如,接收app可能未請求運行時權限Manifest.permission.READ_EXTERNAL_STORAGE ,或者平台可能跨用戶配置文件邊界[user profile boundaries]共享Uri。
  • 相反,應用程序應使用【content://】形式的Uris,以便平台可以擴展接收應用程序的臨時權限以訪問資源。
  • 僅針對 Build.VERSION_CODES.N 或更高版本的應用程序拋出此操作。 早期SDK版本的app可以以【file://】形式的Uri共享文件,但強烈建議不要這樣做。

FileProvider 官方文檔 的一些描述:

  • FileProvider是ContentProvider的一個特殊子類,它通過創建 content:// Uri 而不是 file:/// Uri 來促進與應用程序關聯的文件的安全共享。
  • content URI 允許您使用臨時訪問權限來獲取讀寫訪問權限。當您創建包含 content URI 的Intent時,為了將 content URI 發送到客戶端app,您還可以調用 Intent.setFlags() 來添加權限。只要接收 Activity 的堆棧處於活動狀態,客戶端應用程序就可以使用這些權限。對於轉到Service的Intent,只要Service正在運行,權限就可用。
  • 相比之下,要控制對 file:/// Uri 的訪問,您必須修改基礎文件的文件系統權限。您提供的權限可供任何app使用,並在您更改之前保持有效。這種訪問level從根本上說是不安全的。
  • 通過 content URI 提高文件訪問安全性使FileProvider成為Android安全基礎架構的關鍵部分。

配置

不要再去看垃圾官方文檔了,也不要去看網上各種垃圾文章了,也不要通過看源碼什么的去研究怎么個性化設置了,MLGB,我在幾個月的時間內幾次嘗試理清怎么配置,結果還是發現了各種各樣的bug,真的不要再折騰了,這些個性化的東西不重要,只要按照我下面的配置就行了,保證是最簡單穩定的。

聲明 FileProvider

在清單文件中聲明FileProvider:

<manifest>
    <application>
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>
    </application>
</manifest>

其中:

  • android:name 固定寫法

    如果要覆蓋FileProvider方法的任何默認行為,可擴展FileProvider類並在這里使用完全限定的類名。

  • android:authorities 需自定義,是用來標識該 provider 的唯一標識,建議結合包名來保證 authority 的唯一性

    Set the android:authorities attribute to a URI authority based on a domain you control; for example, if you control the domain mydomain.com you should use the authority com.mydomain.fileprovider

  • android:exported 必須設置成 false,否則運行時會報錯 java.lang.SecurityException: Provider must not be exported

    the FileProvider does not need to be public

  • android:grantUriPermissions 用來控制是否允許臨時授予文件的訪問權限,必須設置成 true
  • meta-data 節點
    • android:name 固定寫法。
    • android:resource 指定共享文件的路徑,此文件放在res/xml/

配置 resource

文件內容完全照抄就行了,鱉折騰。

res/xml/下添加file_paths.xml配置文件

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <files-path
        name="files"
        path="/"/>
    <cache-path
        name="cache"
        path="/"/>
    <external-path
        name="external"
        path="/"/>
    <external-files-path
        name="external_file_path"
        path="/"/>
    <external-cache-path
        name="external_cache_path"
        path="/"/>
    <!--<external-media-path
        name="external-media-path"
        path=""/>-->
</paths>

可配置的元素:

  • files-path 對應內部存儲目錄 Context.getFilesDir()
  • cache-path 對應內部存儲目錄 Context.getCacheDir()
  • external-path 對應 Environment.getExternalStorageDirectory()
  • external-files-path 對應 Context.getExternalFilesDir()
  • external-cache-path 對應 Context.getExternalCacheDir()
  • external-media-path 對應 Context.getExternalMediaDirs()

系統提供的各種文件路徑

很少會用到的路徑:

String downloadCache = Environment.getDownloadCacheDirectory().getAbsolutePath(); //【/cache】
String data = Environment.getDataDirectory().getAbsolutePath(); //【/data】
String root = Environment.getRootDirectory().getAbsolutePath(); //【/system】

SD卡上的路徑

String ext = Environment.getExternalStorageDirectory().getAbsolutePath(); //【/storage/emulated/0】
String extDowmload = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath(); //【/storage/emulated/0/Download】
String extDcim = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath(); //【/storage/emulated/0/DCIM】

data/包名下的路徑:

String filesPath = getFilesDir().getAbsolutePath(); //【/data/user/0/包名/files】
String cachePath = getCacheDir().getAbsolutePath(); //【/data/user/0/包名/cache】
String extCachePath = getExternalCacheDir().getAbsolutePath(); //【/storage/emulated/0/Android/data/包名/cache】

String extFileDowmloadPath = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();//【/storage/emulated/0/Android/data/包名/files/Download】
String extFilesDCIMPath = getExternalFilesDir(Environment.DIRECTORY_DCIM).getAbsolutePath();//【/storage/emulated/0/Android/data/包名/files/DCIM】

使用案例

安裝指定路徑的apk

記得要申請權限:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>

示例代碼:

public class MainActivity extends AppCompatActivity {
    private File apkFile;
    public static final String FROM_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/temp.apk";

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        apkFile = new File(FROM_PATH);
        findViewById(R.id.tv).setOnClickListener(v -> {
            //在6.0的華為手機上不能安裝【getFilesDir()】目錄下的安裝包,但可以安裝【getExternalStorageDirectory()】下的安裝包
            //而在8.0的小米手機上既可以安裝【getFilesDir()】目錄下的安裝包,也可以安裝【getExternalStorageDirectory()】下的安裝包
            File fileDir = new File(getFilesDir(), "bqt");
            if (!fileDir.exists()) fileDir.mkdirs();
            apkFile = new File(fileDir, "temp2.apk");
            copyFile(new File(FROM_PATH), apkFile);
        });
        findViewById(R.id.tv2).setOnClickListener(v -> installApk(this, apkFile));
    }

    public static void copyFile(File from, File to) {
        try {
            FileInputStream fis = new FileInputStream(from);
            FileOutputStream fos = new FileOutputStream(to);
            byte[] buf = new byte[1024];
            int len;
            while ((len = fis.read(buf)) != -1) {
                fos.write(buf, 0, len);
            }
            fis.close();
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void installApk(Context context, File file) {
        Intent intent = new Intent(Intent.ACTION_VIEW);
        Uri uri;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            uri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", file);
            //【content://{$authority}/external/temp.apk】或【content://{$authority}/files/bqt/temp2.apk】
        } else {
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);//【file:///storage/emulated/0/temp.apk】
            uri = Uri.fromFile(file);
        }
        Log.i("bqt", "【Uri】" + uri);
        intent.setDataAndType(uri, "application/vnd.android.package-archive");
        context.startActivity(intent);
    }
}

拍照並指定保存位置

記得要申請權限:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.CAMERA"/>

示例代碼:

public class MainActivity extends Activity {

    private int REQUEST_CODE_CAMERA = 10086;
    private File tempFile;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.tv).setOnClickListener(v -> {
            //TMLGB,在6.0的華為手機上,使用 getFilesDir() 鐵定失敗
            //在8.0的小米6上,使用Environment.getExternalStorageDirectory()也同樣失敗
            File fileDir = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ? getFilesDir() : Environment.getExternalStorageDirectory();
            fileDir = new File(fileDir, "bqt");
            if (!fileDir.exists()) fileDir.mkdirs();
            tempFile = new File(fileDir, "temp");
            showCamera(this, tempFile, REQUEST_CODE_CAMERA);
        });
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == REQUEST_CODE_CAMERA && resultCode == Activity.RESULT_OK) {
            Log.i("【bqt", tempFile.getAbsolutePath());//【data/user/0/包名/files/bqt/temp】或【/storage/emulated/0/bqt/temp】
        }
    }

    public static void showCamera(Activity activity, File file, int requestCode) {
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        Uri uri;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            String authority = activity.getPackageName() + ".fileprovider"; //【清單文件中provider的authorities屬性的值】
            uri = FileProvider.getUriForFile(activity, authority, file);
        } else {
            uri = Uri.fromFile(file);
        }
        Log.i("bqt", "【uri】" + uri);//【content://{$authority}/files/bqt/temp】或【file:///storage/emulated/0/bqt/temp】
        intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
        activity.startActivityForResult(intent, requestCode);
    }
}

2018-8-28


免責聲明!

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



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