運行時權限動態申請html
在Android7.0系統上,Android 框架強制執行了 StrictMode API 政策禁止向你的應用外公開 file:// URI。 若是一項包含文件 file:// URI類型 的 Intent 離開你的應用,應用失敗,並出現 FileUriExposedException 異常,如調用系統相機拍照錄制視頻,或裁切照片。java
若要在應用間共享文件,能夠發送 content:// URI類型的Uri,並授予 URI 臨時訪問權限。 進行此受權的最簡單方式是使用 FileProvider類。android
使用FileProvider的大體步驟以下:git
1.在manifest清單文件中註冊providergithub
<provider android:name="android.support.v4.content.FileProvider" android:authorities="com.jph.takephoto.fileprovider" android:grantUriPermissions="true" android:exported="false">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
複製代碼
exported:要求必須爲false,爲true則會報安全異常。grantUriPermissions:true,表示授予 URI 臨時訪問權限。安全
2.指定共享的目錄markdown
爲了指定共享的目錄咱們須要在資源(res)目錄下建立一個xml目錄,而後建立一個名爲「file_paths」(名字能夠隨便起,只要和在manifest註冊的provider所引用的resource保持一致便可)的資源文件,內容以下:網絡
<!-- 內部存儲空間應用私有目錄下的files/目錄,等同於Context.getFilesDir() 所獲取的目錄路徑 /data/data/包名/files目錄-->
<files-path name="DocDir" path="/" />
<!-- 內部存儲空間應用私有目錄下的cache/目錄,等同於Context.getCacheDir() 所獲取的目錄路徑 /data/data/包名/cache目錄-->
<cache-path name="CacheDocDir" path="/" />
<!--外部存儲空間應用私有目錄下的files/目錄,等同於Context.getExternalFilesDir(null) 所獲取的目錄路徑 /storage/sdcard/Android/data/包名/files-->
<external-files-path name="ExtDocDir" path="/" />
<!--外部存儲空間應用私有目錄下的cache/目錄,等同於Context.getExternalCacheDir() /storage/sdcard/Android/data/包名/cache-->
<external-cache-path name="ExtCacheDir" path="/" />
複製代碼
3.使用FileProviderapp
File file=new File(Environment.getExternalStorageDirectory(), "/temp/"+System.currentTimeMillis() + ".jpg");
if (!file.getParentFile().exists()){
file.getParentFile().mkdirs();
}
//經過FileProvider建立一個content類型的Uri
Uri imageUri = FileProvider.getUriForFile(context, "com.jph.takephoto.fileprovider", file);
Intent intent = new Intent();
//添加這一句表示對目標應用臨時受權該Uri所表明的文件
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
//設置Action爲拍照
intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
//將拍取的照片保存到指定URI
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(intent,1006);
複製代碼
1、通知適配框架
Android 8.0 引入了通知渠道,其容許您爲要顯示的每種通知類型建立用戶可自定義的渠道。用戶界面將通知渠道稱之爲通知類別。詳見
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager notificationManager = (NotificationManager)
getSystemService(Context.NOTIFICATION_SERVICE);
//分組(可選)
//groupId要惟一
String groupId = "group_001";
NotificationChannelGroup group = new NotificationChannelGroup(groupId, "廣告");
//建立group
notificationManager.createNotificationChannelGroup(group);
//channelId要惟一
String channelId = "channel_001";
NotificationChannel adChannel = new NotificationChannel(channelId,
"推廣信息", NotificationManager.IMPORTANCE_DEFAULT);
//補充channel的含義(可選)
adChannel.setDescription("推廣信息");
//將渠道添加進組(先建立組才能添加)
adChannel.setGroup(groupId);
//建立channel
notificationManager.createNotificationChannel(adChannel);
//建立通知時,標記你的渠道id
Notification notification = new Notification.Builder(MainActivity.this, channelId)
.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
.setContentTitle("一條新通知")
.setContentText("這是一條測試消息")
.setAutoCancel(true)
.build();
notificationManager.notify(1, notification);
}
}
複製代碼
刪除渠道代碼以下:
private void deleteNotificationChannel(String channelId){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager mNotificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
mNotificationManager.deleteNotificationChannel(channelId);
}
}
複製代碼
2、後臺限制執行
應用在兩個方面受到限制:
**後臺服務限制:**處於空閒狀態時,應用可使用的後臺服務存在限制。 這些限制不適用於前臺服務,由於前臺服務更容易引發用戶注意。
廣播限制:除了有限的例外狀況,應用沒法使用清單註冊隱式廣播。 它們仍然能夠在運行時註冊這些廣播,而且可使用清單註冊專門針對它們的顯式廣播。
在大多數狀況下,應用均可以使用 JobScheduler 克服這些限制。 這種方式讓應用安排爲在未活躍運行時執行工做,不過仍可以使系統能夠在不影響用戶體驗的狀況下安排這些做業。
private void sendNotification() {
if (Build.VERSION.SDK_INT>=26) {
Intent intent = new Intent(this, StopActivity.class);
//建立NotificationChannel並與NotificationManager、Notification關聯,不然系統會
//提示Failed to post notification on channel 「null」
String channelId = "channel1";
Notification nf = new Notification.Builder(this, channelId)
.setContentTitle("通知")
.setContentText("一個服務正在運行")
.setSmallIcon(R.mipmap.ic_launcher)
.setContentIntent(PendingIntent.getActivities(this, 100, new Intent[]{intent}, PendingIntent.FLAG_CANCEL_CURRENT))
.build();
startForeground(10, nf);
}
}
if (Build.VERSION.SDK_INT>=26) {
startForegroundService(new Intent(this, MyService.class));
}else{
startService(new Intent(this, MyService.class));
}
複製代碼
關於的用法能夠參考官方例子:android-JobScheduler
固然還有後臺位置的限制須要去注意。
3、APK文件下載成功沒有正常跳到應用安裝界面
Android O (Android 8.0) 中,Google 移除掉了容易被濫用的「容許未知來源」應用的開關,在安裝 Play Store 以外的第三方來源的 Android 應用的時候,居然沒有了「容許未知來源」的檢查框,若是你仍是想要安裝某個被本身所信任的開發者的 app,則須要在每一次都手動授予「安裝未知應用」的許可。
首先在AndroidManifest.xml 清單文件中添加安裝未知來源應用的權限
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
複製代碼
而後在用戶點擊更新時判斷是否開啓了該應用的「容許安裝未知來源」的權限,沒有的話,就引導用戶去開啓該應用的「容許安裝未知來源」的權限
private void downloadAPK(){
boolean hasInstallPerssion = getPackageManager().canRequestPackageInstalls();
if (hasInstallPerssion ) {
//安裝應用的邏輯
} else {
//跳轉至「安裝未知應用」權限界面,引導用戶開啓權限,能夠在onActivityResult中接收權限的開啓結果
Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);
startActivityForResult(intent, REQUEST_CODE_UNKNOWN_APP);
}
}
//接收「安裝未知應用」權限的開啓結果
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_UNKNOWN_APP) {
downloadAPK();
}
}
複製代碼
這樣點擊更新時引導用戶開啓「容許安裝未知來源」的權限後,APK文件下載成功後也 成功的跳轉到應用安裝界面。第三個問題也獲得瞭解決。
1、Http請求失敗
在9.0中默認狀況下啓用網絡傳輸層安全協議 (TLS),默認狀況下已停用明文支持。也就是不容許使用http請求,要求使用https。
解決方法是須要咱們添加網絡安全配置。首先在res 目錄下新建xml文件夾,添加network_security_config.xml文件:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
複製代碼
AndroidManifest.xml中的 application添加
以上這是一種簡單粗暴的配置方法,要麼支持http,要麼不支持http。爲了安全靈活,咱們能夠指定支持的http域名:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- Android 9.0 上部分域名時使用 http -->
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">secure.example.com</domain>
<domain includeSubdomains="true">cdn.example1.com</domain>
</domain-config>
</network-security-config>
複製代碼
2、前臺服務
Android P(9.0)要求建立一個前臺服務須要請求 FOREGROUND_SERVICE 權限,不然系統會引起 SecurityException
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
複製代碼
3、其餘
在 Android 9 中,調用Build.SERIAL 會始終返回 UNKNOWN 以保護用戶的隱私。若是你的應用須要訪問設備的硬件序列號,那麼須要先請求 READ_PHONE_STATE 權限,而後調用 Build.getSerial
1、分區存儲
應用只能看到本應用專有的目錄(經過 Context.getExternalFilesDir() 訪問)以及特定類型的媒體。除非您的應用須要訪問存放在應用的專有目錄以及 MediaStore 以外的文件,不然最好使用分區存儲。
要點: 1.Android Q文件存儲機制修改爲了沙盒模式 2.APP只能訪問本身目錄下的文件和公共媒體文件 3.Android Q版本如下機型,仍是使用老的文件存儲方式 4.Android Q及以上版本機型,全部應用均須要分區存儲, 因此應用須要提早確保支持分區存儲
須要注意:在適配AndroidQ的時候還要兼容Q系統版本如下的,使用SDK_VERSION區分 外部存儲:被分爲應用私有目錄以及共享目錄兩個部分
應用私有目錄:存儲應用私有數據,外部存儲應用私有目錄對應
1.外部存儲應用私有目錄對應/Android/data/包名/,內部存儲應用私有目錄對應/data/data/包名/ 2.應用私有目錄文件訪問方式與以前Android版本一致,能夠經過File path獲取資源。
共享目錄:
1.存儲其餘應用可訪問文件, 包含媒體文件、文檔文件以及其餘文件,對應設備DCIM、Pictures、Alarms, Music, Notifications,Podcasts, Ringtones、Movies、Download等目錄。
2.共享目錄文件須要經過MediaStore API或者Storage Access Framework方式訪問。
3.MediaStore API在共享目錄指定目錄下建立文件或者訪問應用本身建立文件,不須要申請存儲權限
4.MediaStore API訪問其餘應用在共享目錄建立的媒體文件(圖片、音頻、視頻), 須要申請存儲權限,未申請存儲權限,經過ContentResolver查詢不到文件Uri,即便經過其餘方式獲取到文件Uri,讀取或建立文件會拋出異常
5.MediaStore API不可以訪問其餘應用建立的非媒體文件(pdf、office、doc、txt等), 只可以經過Storage Access Framework方式訪問
受影響的變動
圖片位置信息 一些圖片會包含位置信息,由於位置對於用戶屬於敏感信息, Android 10應用在分區存儲模式下圖片位置信息默認獲取不到,應用經過如下兩項設置能夠獲取圖片位置信息: 1.1在manifest中申請ACCESS_MEDIA_LOCATION 1.2調用MediaStore setRequireOriginal(Uri uri)接口更新圖片Uri
兼容模式
應用未完成外部存儲適配工做,能夠臨時以兼容模式運行, 兼容模式下應用申請存儲權限,便可擁有外部存儲完整目錄訪問權限,經過Android10以前文件訪問方式運行,如下設置應用以兼容模式運行。
tagretSDK 大於等於Android 10(API level 29), 在manifest中設置requestLegacyExternalStorage屬性爲true
<manifest ...>
...
<application android:requestLegacyExternalStorage="true" ... >
...
</manifest>
複製代碼
**判斷兼容模式接口**
```
//返回值
//true : 應用以兼容模式運行
//false:應用以分區存儲特性運行
Environment.isExternalStorageLegacy();
```
**File Path路徑訪問受影響接口**
開啓分區存儲新特性, Andrioid 10不可以經過File Path路徑直接訪問共享目錄下資源,如下接口經過File 路徑操做文件資源,功能會受到影響,應用須要使用MediaStore或者SAF方式訪問。
**存儲特性Android版本差別概覽**
複製代碼
適配指導
AndroidQ中使用ContentResolver進行文件的增刪改查。
1)獲取(建立)私有目錄下的文件夾
File apkFile = context.getExternalFilesDir("apk");
複製代碼
2)建立私有目錄文件
生成須要下載的路徑,經過輸入輸出流讀取寫入
String apkFilePath = context.getExternalFilesDir("apk").getAbsolutePath();
File newFile = new File(apkFilePath + File.separator + "demo.apk");
OutputStream os = null;
try {
os = new FileOutputStream(newFile);
if (os != null) {
os.write("file is created".getBytes(StandardCharsets.UTF_8));
os.flush();
}
} catch (IOException e) {
} finally {
try {
if (os != null) {
os.close();
}catch (IOException e1) {
}
}
複製代碼
3)建立共享目錄文件或文件夾
主要是在公共目錄下建立文件或文件夾拿到本地路徑uri,不一樣的Uri,能夠保存到不一樣的公共目錄中。接下來使用輸入輸出流就能夠寫入文件。
重點:AndroidQ中不支持file://類型訪問文件,只能經過uri方式訪問。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ContentResolver resolver = context.getContentResolver();
ContentValues values = new ContentValues();
values.put(MediaStore.Downloads.DISPLAY_NAME, fileName);
values.put(MediaStore.Downloads.DESCRIPTION, fileName);
//設置文件類型
values.put(MediaStore.Downloads.MIME_TYPE, "application/vnd.android.package-archive");
//注意MediaStore.Downloads.RELATIVE_PATH須要targetVersion=29,
//故該方法只可在Android10的手機上執行
values.put(MediaStore.Downloads.RELATIVE_PATH, "Download" + File.separator + "apk");
Uri external = MediaStore.Downloads.EXTERNAL_CONTENT_URI;
String status = Environment.getExternalStorageState();
// 判斷是否有SD卡,優先使用SD卡存儲,當沒有SD卡時使用手機存儲
if (status.equals(Environment.MEDIA_MOUNTED)) {
return resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,values);
} else {
return resolver.insert(MediaStore.Images.Media.INTERNAL_CONTENT_URI, values);
}
}
複製代碼
2、定位權限
用戶能夠更好地控制應用什麼時候能夠訪問設備位置。當在Android Q上運行的應用程序請求位置訪問時,會經過對話框的形式給用戶進行受權提示。此對話框容許用戶授予對兩個不一樣範圍的位置訪問權限:在使用中(僅限前臺)或始終(前臺和後臺)
新增權限 ACCESS_BACKGROUND_LOCATION
若是你的應用針對 Android Q 而且須要在後臺運行時訪問用戶的位置,則必須在應用的清單文件中聲明新權限
<manifest>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
</manifest>
複製代碼
3、新增 ACCESS_MEDIA_LOCATION 權限
一些照片在其數據中會包含位置信息,容許用戶查看拍攝照片的位置。因爲此位置信息是敏感的,所以咱們想獲取位置信息須要如下幾步:
photoUri = MediaStore.setRequireOriginal(photoUri);
//從流中讀取位置信息
InputStream stream = getContentResolver().openInputStream(photoUri);
複製代碼
1、Scoped Storage(分區存儲)
不過須要注意的是,應用targetSdkVersion >= 30,強制執行分區存儲機制。以前在AndroidManifest.xml中添加 android:requestLegacyExternalStorage="true"的適配方式已不起做用。
還有一個變化:Android 11 容許使用除 MediaStore API 以外的 API 經過文件路徑直接訪問共享存儲空間中的媒體文件。其中包括:
若是你以前沒有適配Android 10,這一點對你來講是個好消息。Android 10在AndroidManifest.xml中添加 android:requestLegacyExternalStorage="true"來適配,Android 11上直接使用File API訪問媒體文件。
不過,使用原始文件路徑直接訪問共享存儲空間中的媒體文件會重定向到 MediaStoreAPI,此次重定向會形成性能影響(隨機讀寫慢一倍左右)。並且直接使用原始文件路徑,並不會比使用 MediaStore API 有更多優點,所以官方強烈建議直接使用 MediaStore API。
固然還有一種簡單粗暴的適配方法,獲取外部存儲管理權限。若是你的應用是手機管家、文件管理器這類須要訪問大量文件的app,能夠申請MANAGE_EXTERNAL_STORAGE權限,將用戶引導至系統設置頁面開啓。代碼以下:
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
複製代碼
2、單次權限受權
從 Android 11 開始,每當應用請求與位置信息、麥克風或攝像頭相關的權限時,面向用戶的權限對話框會包含僅限這一次選項。若是用戶在對話框中選擇此選項,系統會嚮應用授予臨時的單次受權。
單次權限受權的應用能夠在一段時間內訪問相關數據,具體時間取決於應用的行爲和用戶的操做:
當用戶下次打開應用而且應用中的某項功能請求訪問位置信息、麥克風或攝像頭時,系統會再次提示用戶授予權限。
3、請求位置權限
這部分在Android 10的適配有過調整,當時規則以下:
請求ACCESS_FINE_LOCATION或 ACCESS_COARSE_LOCATION權限表示在前臺時擁有訪問設備位置信息的權限。在請求彈框中,選擇「始終容許」表示先後臺均可以獲取位置信息,選擇「僅在應用使用過程當中容許」只表示擁有前臺的權限。
在Android 11中,請求彈框中取消了「始終容許」這一選項。也就是說默認不會授予你後臺訪問設備位置信息的權限。若是嘗試請求ACCESS_BACKGROUND_LOCATION權限的同時請求任何其餘權限,系統會拋出異常,不會嚮應用授予其中的任一權限。
官方給出的適配建議及緣由以下:
建議應用對位置權限執行遞增請求,先請求前臺位置信息訪問權限,再請求後臺位置信息訪問權限。執行遞增請求能夠爲用戶提供更大的控制權和透明度,由於他們能夠更好地瞭解應用中的哪些功能須要後臺位置信息訪問權限。
總結一下得出兩點:
先請求前臺位置信息訪問權限,再請求後臺位置信息訪問權限。
單獨請求後臺位置信息訪問權限,不要與其餘權限一同請求。
4、軟件包可見性
軟件包可見性是Android 11上提高系統隱私安全性的一個新特性。它的做用是限制app隨意獲取其餘app的信息和安裝狀態。避免病毒軟件、間諜軟件利用,引起網絡釣魚、用戶安裝信息泄露等安全事件。
軟件包可見性是Android 11上提高系統隱私安全性的一個新特性。它的做用是限制app隨意獲取其餘app的信息和安裝狀態。避免病毒軟件、間諜軟件利用,引起網絡釣魚、用戶安裝信息泄露等安全事件。
解決方法很簡單,在AndroidManifest.xml 中添加queries元素,裏面添加須要可見的應用包名。
<manifest package="com.example.app">
<queries>
<!-- 微博 -->
<package android:name="com.sina.weibo" />
<!-- QQ -->
<package android:name="com.tencent.mobileqq" />
<!-- 支付寶 -->
<package android:name="com.eg.android.AlipayGphone" />
<!-- AlipayHK -->
<package android:name="hk.alipay.wallet" />
</queries>
</manifest>
複製代碼
除了直接添加包名的方式外,咱們能夠按intent和provider來添加:
<manifest package="com.example.app">
<queries>
<intent>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="image/jpeg" />
</intent>
<provider android:authorities="com.example.settings.files" />
</queries>
複製代碼
具體的規則參見:管理軟件包可見性
固然,還有一種簡單粗暴的方式,能夠直接申請權限QUERY_ALL_PACKAGES。若是你的應用須要上架Google Play,那麼可能要注意相關政策。爲了尊重用戶隱私,建議咱們的應用按正常工做所需的最小軟件包可見性來適配。
最後須要注意的是,使用queries元素須要Android Gradle 插件版本是 4.1及以上,由於舊版本的插件並不兼容此元素,出現合併 manifest 的錯誤。
5、前臺服務類型
Android 10中,在前臺服務訪問位置信息,須要在對應的service中添加 location 服務類型。
一樣的,Android 11中,在前臺服務訪問攝像頭或麥克風,須要在對應的service中添加camera或microphone 服務類型。
<manifest>
<service android:name="MyService" android:foregroundServiceType="location|microphone|camera" />
</manifest>
複製代碼
這一限制的變動,使得程序沒法在後臺啓動服務訪問攝像頭和麥克風。如需使用,只能是前臺開啓前臺服務。除非有以下狀況:
6、權限自動重置
若是應用以 Android 11 或更高版本爲目標平臺而且數月未使用,系統會經過自動重置用戶已授予應用的運行時敏感權限來保護用戶數據。以下圖所示:
注意上圖中有一個啓動自動重置的開關。若是咱們的應用有特殊須要,能夠引導用戶關閉它。示例代碼以下:
public void checkAutoRevokePermission(Context context) {
// 判斷是否開啓
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
!context.getPackageManager().isAutoRevokeWhitelisted()) {
// 跳轉設置頁
Intent intent = new Intent(Intent.ACTION_AUTO_REVOKE_PERMISSIONS);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setData(Uri.fromParts("package", context.getPackageName(), null));
context.startActivity(intent);
}
}
複製代碼
7、讀取手機號 若是你是經過TelecomManager的getLine1Number方法,或TelephonyManager的getMsisdn方法獲取電話號碼。那麼在Android 11中須要增長READ_PHONE_NUMBERS權限。使用其餘方法不受限。
<manifest>
<!-- 若是應用僅在 Android 10及更低版本中使用該權限,能夠添加 maxSdkVersion="29" -->
<uses-permission android:name="android.permission.READ_PHONE_STATE" android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
</manifest>
複製代碼
參考文章 Android R(11.0) 適配