Android 對於權限的使用授權,在 6.0 Marshmallow (棉花糖) 之後做了一些更動,除了必須在 AndroidManifest.xml 中做設定,某些權限還必須在執行時取得使用者的授權。這篇文章以儲存圖片做為範例,說明新的授權機制如何使用。
關於權限及授權
在 Android 6.0 之前,要使用權限,只要在 AndroidManifest.xml 中加入權限的請求,例如:<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
這樣就可以讓 App 擁有可以寫入檔案的權限。可是這樣有個問題,就是使用者無法對單一項目允許或禁止授權,使用者在安裝 App 時就必須全部接受這些權限的請求,否則便無法安裝。新的授權機制就是為了解決這個問題。首先你仍必須在 AndroidManifest.xml 中指定要使用的權限,當使用者安裝了該 App 後,如果某項權限是屬於危除層級的,那該權限在此時並沒有真的被授權,你可以開啟裝置的設定->應用程式->進入該App來查看,如下圖: 點選「權限」項目後會看到: 灰色表示該權限還未被使用者允許。
使用者可以在這裡將權限打開,或是在 App 中,當它執行到需要該權限時,App 才發出請求授權的要求,如下圖:
一般權限與危險權限
App 執行時才請求授權的機制並不是全部的權限都需要,因此權限分為兩級,一般權限只要在 AndroidManifest.xml 中指定即可,危險權限才須要額外在需要時請求授權。危險權限大都和存取使用者資料有關,像是讀寫檔案、讀取電話狀態等等,可以在 Dangerous Permissions 查看,一般權限在 Normal Permissions。
在請求危險權限時要做確認的動作,如果已經取得授權,下次執行時就可以直接執行該功能,不必做重覆請求授權的動作。
範例:儲存圖片
指定使用權限
這個範例會儲存圖片到裝置中,所以要有寫入檔案的權限,在 AndroidManifest.xml 加入存取檔案的使用權限:<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
Layout
activity_main.xml<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:orientation="vertical"
tools:context="com.tonycube.demo.permissionsdemo.MainActivity">
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/imgPicture"
android:src="@drawable/coding"
android:scaleType="fitCenter"
android:adjustViewBounds="true"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Save Picture"
android:id="@+id/btnSavePicture"
/>
</LinearLayout>
只有 ImageView 和 Button 兩個元件,當按下按鈕時,將 ImageView 中的圖片儲存到裝罝中。看起來像這樣:
MainActivity.java
public class MainActivity extends AppCompatActivity {
private ImageView imgPicture;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
private void initView() {
imgPicture = (ImageView)findViewById(R.id.imgPicture);
//
Button btnSavePicture = (Button)findViewById(R.id.btnSavePicture);
btnSavePicture.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
doSavePicture();
}
});
}
private void doSavePicture() {
if (saveToPictureFolder()) {
Toast.makeText(MainActivity.this, "儲存成功", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(MainActivity.this, "儲存失敗", Toast.LENGTH_SHORT).show();
}
}
/**
* 儲存到圖片庫
*/
private boolean saveToPictureFolder() {
//取得 Pictures 目錄
File picDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
Log.d(">>>", "Pictures Folder path: " + picDir.getAbsolutePath());
//假如有該目錄
if (picDir.exists()) {
//儲存圖片
File pic = new File(picDir, "pic.jpg");
imgPicture.setDrawingCacheEnabled(true);
imgPicture.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_AUTO);
Bitmap bmp = imgPicture.getDrawingCache();
return saveBitmap(bmp, pic);
}
return false;
}
private boolean saveBitmap(Bitmap bmp, File pic) {
if (bmp == null || pic == null) return false;
//
FileOutputStream out = null;
try {
out = new FileOutputStream(pic);
bmp.compress(Bitmap.CompressFormat.JPEG, 80, out);
out.flush();
scanGallery(this, pic);
Log.d(">>>", "bmp path: " + pic.getAbsolutePath());
return true;
} catch (Exception e) {
Log.e(">>>", "save bitmap failed!");
e.printStackTrace();
} finally {
try {
if (out != null) {
out.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
return false;
}
private void scanGallery(Context ctx, File file) {
Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
Uri contentUri = Uri.fromFile(file);
mediaScanIntent.setData(contentUri);
ctx.sendBroadcast(mediaScanIntent);
}
}
當按下按鈕後,會先將 ImageView 中的圖片轉存成 Bitmap,接著取得預設的圖片庫目錄,然後就是把 Bitmap 輸出到圖片庫中成為圖檔。scanGallery() 是要求圖片庫做重新掃描的動作,不然即使儲存成功了,在圖片庫中也看不到。以上的功能在 Android 6.0 (API 23) 之前的裝置上是可以正常運作的,但是在 Android 6.0 之後的裝置上就會失敗。接下來就是要處理請求授權的動作。
範例:請求授權
在 MainActivity.java 中加入處理授權的部份後就是整個完整的程式碼:public class MainActivity extends AppCompatActivity {
private final String PERMISSION_WRITE_STORAGE = "android.permission.WRITE_EXTERNAL_STORAGE";
private ImageView imgPicture;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
private void initView() {
imgPicture = (ImageView)findViewById(R.id.imgPicture);
//
Button btnSavePicture = (Button)findViewById(R.id.btnSavePicture);
btnSavePicture.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (!hasPermission()) {
if (needCheckPermission()) {
//如果須要檢查權限,由於這個步驟要等待使用者確認,
//所以不能立即執行儲存的動作,
//必須在 onRequestPermissionsResult 回應中才執行
return;
}
}
doSavePicture();
}
});
}
private void doSavePicture() {
if (saveToPictureFolder()) {
Toast.makeText(MainActivity.this, "儲存成功", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(MainActivity.this, "儲存失敗", Toast.LENGTH_SHORT).show();
}
}
/**
* 儲存到圖片庫
*/
private boolean saveToPictureFolder() {
//取得 Pictures 目錄
File picDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
Log.d(">>>", "Pictures Folder path: " + picDir.getAbsolutePath());
//假如有該目錄
if (picDir.exists()) {
//儲存圖片
File pic = new File(picDir, "pic.jpg");
imgPicture.setDrawingCacheEnabled(true);
imgPicture.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_AUTO);
Bitmap bmp = imgPicture.getDrawingCache();
return saveBitmap(bmp, pic);
}
return false;
}
private boolean saveBitmap(Bitmap bmp, File pic) {
if (bmp == null || pic == null) return false;
//
FileOutputStream out = null;
try {
out = new FileOutputStream(pic);
bmp.compress(Bitmap.CompressFormat.JPEG, 80, out);
out.flush();
scanGallery(this, pic);
Log.d(">>>", "bmp path: " + pic.getAbsolutePath());
return true;
} catch (Exception e) {
Log.e(">>>", "save bitmap failed!");
e.printStackTrace();
} finally {
try {
if (out != null) {
out.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
return false;
}
private void scanGallery(Context ctx, File file) {
Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
Uri contentUri = Uri.fromFile(file);
mediaScanIntent.setData(contentUri);
ctx.sendBroadcast(mediaScanIntent);
}
////////////////////////////////////////////////
/**
* 確認是否要請求權限(API > 23)
* API < 23 一律不用詢問權限
*/
private boolean needCheckPermission() {
//MarshMallow(API-23)之後要在 Runtime 詢問權限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
String[] perms = {PERMISSION_WRITE_STORAGE};
int permsRequestCode = 200;
requestPermissions(perms, permsRequestCode);
return true;
}
return false;
}
/**
* 是否已經請求過該權限
* API < 23 一律回傳 true
*/
private boolean hasPermission(){
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
return(ActivityCompat.checkSelfPermission(this, PERMISSION_WRITE_STORAGE) == PackageManager.PERMISSION_GRANTED);
}
return true;
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == 200){
if (grantResults.length > 0) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.d(">>>", "取得授權,可以執行動作了");
doSavePicture();
}
}
}
}
}
needCheckPermission()
用來檢查是否需要請求授權,因為只需對 API >= 23 的裝置做動作,所以小於這個版本的裝罝全部忽略。因為這個請求授權的動作是非同步的,要等使用者「允許」後才能繼續,所以如果我們發出 requestPermissions() 的請求時,就要把之後的 doSavePicture() 忽略,這裡我直接 return。
hasPermission()
這是用來檢查該項權限是否已經被授權允許 (GRANT),如果使用者已經允許,那什麼都不用做,直接執行 doSavePicture() 。和前一個方法一樣,如果裝罝小於 API 23,就沒有允不允許的問題,一律視同允許。
onRequestPermissionsResult()
這個方法是覆寫我們繼承 AppCompatActivity 時所擁有的方法,用來接收使用者對授權請求的回應。如果結果是允許使用該權限,就可以直接執行 doSavePicture() 了。參考資料
- <uses-permission>
- Requesting Permissions at Run Time
- System Permissions
- Runtime Permissions: Best Practices and How to Gracefully Handle Permission Removal
本文網址:http://blog.tonycube.com/2016/07/android-60-android-save-image-and.html
由 Tony Blog 撰寫,請勿全文複製,轉載時請註明出處及連結,謝謝 😀
由 Tony Blog 撰寫,請勿全文複製,轉載時請註明出處及連結,謝謝 😀
大讚!!!
回覆刪除API target版本 25 檔案都讀不到
看到這篇文 總算有了改善
辛苦了 謝謝整理C":
APP開啟第一次儲存圖片都是會空白,之後才儲存正常,甚麼原因?
回覆刪除看看儲存圖片時,資料是否有真的拿到,檢查 bitmap 物件是不是 null。
刪除