Android 10(Android Q) 適配心得

Android 10(Android Q) 適配心得

參考網站

官方網站-沙盒存儲java

Android Q 中的隱私權-重大隱私權變動android

官方網站-展現時間敏感的通知bash

1. 設備硬件信息讀取限制

在Android10中, 系統不容許普通App請求android.permission.READ_PHONE_STATE權限, 故新版App須要取消該動態權限的申請。dom

當前獲取設備惟一ID的方式爲使用SSAID, 若獲取爲空的話則使用UUID.randomUUID().toString()得到一個隨機ID並存儲起來, 該ID保證惟一, 但App卸載重裝以後就會改變ide

SSAID的獲取方式爲:工具

String id = android.provider.Settings.Secure.getString(context.getContentResolver(), android.provider.Settings.Secure.ANDROID_ID);
複製代碼

2. 後臺App啓動限制

Android10中, 當App無前臺顯示的Activity時,其啓動Activity會被系統攔截, 致使啓動無效。測試

我的以爲官方這樣作的目的是在用戶使用的過程當中, 不但願被其餘App強制打斷, 但這樣就對鬧鐘類, 帶呼叫功能的App不太友好了。網站

對此官方給予的折中方案是使用全屏Intent(full-screen intent), 既建立通知欄通知時, 加入full-screen intent 設置, 示例代碼以下(基於官方文檔修改):ui

Intent fullScreenIntent = new Intent(this, CallActivity.class);
PendingIntent fullScreenPendingIntent = PendingIntent.getActivity(this, 0,
        fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT);

NotificationCompat.Builder notificationBuilder =
        new NotificationCompat.Builder(this, CHANNEL_ID)
    .setSmallIcon(R.drawable.notification_icon)
    .setContentTitle("Incoming call")
    .setContentText("(919) 555-1234")
    //如下爲關鍵的3行
    .setPriority(NotificationCompat.PRIORITY_HIGH)
    .setCategory(NotificationCompat.CATEGORY_CALL)
    .setFullScreenIntent(fullScreenPendingIntent, true);
    
NotificationManager notifyManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notifyManager.notify(notifyId, builder.build());
複製代碼

注意在Target SDk爲29及以上時,須要在AndroidManifest上增長USE_FULL_SCREEN_INTENT申明this

//AndroidManifest中
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
複製代碼

測試下來, 當手機處於亮屏狀態時, 會顯示一個通知欄, 當手機處於鎖屏或者滅屏狀態時,會亮屏並直接進入到CallActivity中

3. App沙盒化存儲

在Android10上, 當App的target sdk爲29及以上時或者在AndroidManifest中申明時, App即便有外部存儲的寫入權限, 也沒法直接經過路徑訪問外部存儲

疑惑

網上有些其餘的文章有一些說法

新的存儲權限取消並替換了READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE權限,若是要訪問沙盒外的媒體共享文件,好比照片,音樂,視頻等,須要申請新的媒體權限:READ_MEDIA_IMAGES,READ_MEDIA_VIDEO,READ_MEDIA_AUDIO,申請方法同原來的存儲權限

我在實際測試中並無找到這3個權限類型, 對此查看了官方文檔

官方文檔的原文爲:

Snipaste_2019-09-19_21-58-41.png

大體意思是當你訪問沙盒內部時,是不須要READ_EXTERNAL_STORAGE 或 WRITE_EXTERNAL_STORAGE權限的, 若要訪問外部存儲, 須要擁有READ_EXTERNAL_STORAGE 權限而且使用MediaStore來進行訪問

因此我以爲在Android 10上, 若要訪問外部文件, 仍是須要進行READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE 動態權限的申請的

適配方案

對應App的數據, 須要存儲到App的沙盒中, 對應的路徑爲下

文件類型 地址
視頻文件 context.getExternalFilesDir(Environment.DIRECTORY_MOVIES)
音頻文件 context.getExternalFilesDir(Environment.DIRECTORY_MUSIC)
圖片文件 context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
  • 存儲到沙盒中的這些地址的實際路徑在/data/user/0/包名/中,因此這些文件在App卸載以後會被清除
  • 在這些目錄裏的文件受系統保護, 其餘App沒法直接對其訪問

若須要將沙盒中的視頻或圖片文件保存在外部存儲時, 須要使用ContentValues和MediaStore來進行操做,對此我封裝了一個工具類:

public static boolean SavePictureFile(Context context, File file) {
        if (file == null) {
            return false;
        }
        Uri uri = insertFileIntoMediaStore(context, file, true);
        return SaveFile(context, file, uri);
}

public static boolean SaveVideoFile(Context context, File file) {
        if (file == null) {
            return false;
        }
        Uri uri = insertFileIntoMediaStore(context, file, false);
        return SaveFile(context, file, uri);
}

private static boolean SaveFile(Context context, File file, Uri uri) {
        if (uri == null) {
            LogUtil.e("url is null");
            return false;
        }
        LogUtil.i("SaveFile: " + file.getName());
        ContentResolver contentResolver = context.getContentResolver();

        ParcelFileDescriptor parcelFileDescriptor = null;
        try {
            parcelFileDescriptor = contentResolver.openFileDescriptor(uri, "w");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

        if (parcelFileDescriptor == null) {
            LogUtil.e("parcelFileDescriptor is null");
            return false;
        }

        FileOutputStream outputStream = new FileOutputStream(parcelFileDescriptor.getFileDescriptor());
        FileInputStream inputStream;
        try {
            inputStream = new FileInputStream(file);
        } catch (FileNotFoundException e) {
            LogUtil.e(e.toString());
            try {
                outputStream.close();
            } catch (IOException ex) {
                LogUtil.e(ex.toString());
            }
            return false;
        }

        try {
            copy(inputStream, outputStream);
        } catch (IOException e) {
            LogUtil.e(e.toString());
            return false;
        } finally {
            try {
                outputStream.close();
            } catch (IOException e) {
                LogUtil.e(e.toString());
            }
            try {
                inputStream.close();
            } catch (IOException e) {
                LogUtil.e(e.toString());
            }
        }

        return true;
    }

//注意當文件比較大時該方法耗時會比較大
private static void copy(InputStream ist, OutputStream ost) throws IOException {
        byte[] buffer = new byte[4096];
        int byteCount;
        while ((byteCount = ist.read(buffer)) != -1) {
            ost.write(buffer, 0, byteCount);
        }
        ost.flush();
}

//建立視頻或圖片的URI
private static Uri insertFileIntoMediaStore(Context context, File file, boolean isPicture) {
        ContentValues contentValues = new ContentValues();
        contentValues.put(MediaStore.Video.Media.DISPLAY_NAME, file.getName());
        contentValues.put(MediaStore.Video.Media.MIME_TYPE, isPicture ? "image/jpeg" : "video/mp4");
        if (Build.VERSION.SDK_INT >= 29) {
            contentValues.put(MediaStore.Video.Media.DATE_TAKEN, file.lastModified());
        }

        Uri uri = null;
        try {
            uri = context.getContentResolver().insert(
                    (isPicture ? MediaStore.Images.Media.EXTERNAL_CONTENT_URI : MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
                    , contentValues
            );
        } catch (Exception e) {
            e.printStackTrace();
        }
        return uri;
    }

複製代碼
  • Android 10 以前, 往系統文件夾寫入文件須要文件讀寫權限, Android10中, 能夠無需權限直接往系統文件夾寫入文件
  • 默認保存的地址, 視頻默認在Movies中, 圖片默認在Pictures中, 若想保持到對應的子文件夾中, 則須要如下設置
//注意MediaStore.Images.Media.RELATIVE_PATH須要compileSdkVersion=29, 
//故該方法只可在Android10的手機上執行

//圖片, 對應存儲的地址爲 Pictures/test
contentValues.put(MediaStore.Images.Media.RELATIVE_PATH,
Environment.DIRECTORY_PICTURES + File.separator + test);

//視頻, 對應存儲的地址爲 Movies/test
contentValues.put(MediaStore.Video.Media.RELATIVE_PATH,
Environment.DIRECTORY_MOVIES + File.separator + test);
複製代碼

結語

以上是我在適配中的總結,但願對你們的適配有所幫助,如有錯誤,敬請斧正。

相關文章
相關標籤/搜索