1. 程式人生 > >【我的Android進階之旅】Android 7.0報異常:java.lang.SecurityException: COLUMN_LOCAL_FILENAME is deprecated;

【我的Android進階之旅】Android 7.0報異常:java.lang.SecurityException: COLUMN_LOCAL_FILENAME is deprecated;

之前開發的一個和第三方合作的apk,在之前公司的 Android 5.1 系統的手錶上執行正常,今天在公司新開發的 Android 7.1系統的手錶上執行的時候,使用 DownloadManager 下載之後,查詢下載狀態的時候,報了異常

java.lang.SecurityException: COLUMN_LOCAL_FILENAME is deprecated; use ContentResolver.openFileDescriptor() instead

異常詳細資訊如下:

03-17 15:59:43.288 31487-31487/com.netease.xtc.cloudmusic E/CloudMusicDownloadService: DownloadChangeObserver.onChange() throwable = java.lang.SecurityException: COLUMN_LOCAL_FILENAME is deprecated; use ContentResolver.openFileDescriptor() instead
                                                                                           at android.app.DownloadManager$CursorTranslator.getString(DownloadManager.java:1545
) at com.netease.xtc.cloudmusic.services.download.CloudMusicDownloadService.queryDownloadStatus(CloudMusicDownloadService.java:634) at com.netease.xtc.cloudmusic.services.download.CloudMusicDownloadService.access$900
(CloudMusicDownloadService.java:55) at com.netease.xtc.cloudmusic.services.download.CloudMusicDownloadService$DownloadChangeObserver$3.call(CloudMusicDownloadService.java:590) at com.netease.xtc.cloudmusic.services.download.CloudMusicDownloadService$DownloadChangeObserver$3.
call(CloudMusicDownloadService.java:587) at rx.Observable.unsafeSubscribe(Observable.java:8666) at rx.internal.operators.OperatorSubscribeOn$1.call(OperatorSubscribeOn.java:94) at rx.internal.schedulers.CachedThreadScheduler$EventLoopWorker$1.call(CachedThreadScheduler.java:220) at rx.internal.schedulers.ScheduledAction.run(ScheduledAction.java:55) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:428) at java.util.concurrent.FutureTask.run(FutureTask.java:237) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:272) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607) at java.lang.Thread.run(Thread.java:761)

跳轉到報錯的地方,如下圖所示:
這裡寫圖片描述

在執行下面兩行程式碼的時候報錯了。

  int fileNameIdx = c.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME);
  String fileName = c.getString(fileNameIdx);

而且 DownloadManager.COLUMN_LOCAL_FILENAME 已經是被廢棄的常量了,點選檢視原始碼如下所示:

 /**
     * Path to the downloaded file on disk.
     * <p>
     * Note that apps may not have filesystem permissions to directly access
     * this path. Instead of trying to open this path directly, apps should use
     * {@link ContentResolver#openFileDescriptor(Uri, String)} to gain access.
     *
     * @deprecated apps should transition to using
     *             {@link ContentResolver#openFileDescriptor(Uri, String)}
     *             instead.
     */
    @Deprecated
    public final static String COLUMN_LOCAL_FILENAME = "local_filename";

Android 在 Android 7.0 或更高版本開發的應用在嘗試訪問DownloadManager.COLUMN_LOCAL_FILENAME 時會觸發java.lang.SecurityException。

Android官方建議我們使用 ContentResolver#openFileDescriptor(Uri, String)來獲取檔案相關資訊。原始碼如下所示:

 /**
     * Open a raw file descriptor to access data under a URI.  This
     * is like {@link #openAssetFileDescriptor(Uri, String)}, but uses the
     * underlying {@link ContentProvider#openFile}
     * ContentProvider.openFile()} method, so will <em>not</em> work with
     * providers that return sub-sections of files.  If at all possible,
     * you should use {@link #openAssetFileDescriptor(Uri, String)}.  You
     * will receive a FileNotFoundException exception if the provider returns a
     * sub-section of a file.
     *
     * <h5>Accepts the following URI schemes:</h5>
     * <ul>
     * <li>content ({@link #SCHEME_CONTENT})</li>
     * <li>file ({@link #SCHEME_FILE})</li>
     * </ul>
     *
     * <p>See {@link #openAssetFileDescriptor(Uri, String)} for more information
     * on these schemes.
     * <p>
     * If opening with the exclusive "r" or "w" modes, the returned
     * ParcelFileDescriptor could be a pipe or socket pair to enable streaming
     * of data. Opening with the "rw" mode implies a file on disk that supports
     * seeking. If possible, always use an exclusive mode to give the underlying
     * {@link ContentProvider} the most flexibility.
     * <p>
     * If you are writing a file, and need to communicate an error to the
     * provider, use {@link ParcelFileDescriptor#closeWithError(String)}.
     *
     * @param uri The desired URI to open.
     * @param mode The file mode to use, as per {@link ContentProvider#openFile
     * ContentProvider.openFile}.
     * @return Returns a new ParcelFileDescriptor pointing to the file.  You
     * own this descriptor and are responsible for closing it when done.
     * @throws FileNotFoundException Throws FileNotFoundException if no
     * file exists under the URI or the mode is invalid.
     * @see #openAssetFileDescriptor(Uri, String)
     */
    public final @Nullable ParcelFileDescriptor openFileDescriptor(@NonNull Uri uri,
            @NonNull String mode) throws FileNotFoundException {
        return openFileDescriptor(uri, mode, null);
    }

這裡寫圖片描述

檢視Android 官方文件,關於Android 7.0 的許可權管理更改,如下圖所示:

這裡寫圖片描述

因此為了解決這個異常,我們有以下兩個方法解決。

方法一、使用 ContentResolver#openFileDescriptor(Uri, String)來替代訪問由 DownloadManager 公開的檔案。

方法二、使用DownloadManager.COLUMN_LOCAL_URI查出檔案Uri,然後使用Uri new一個File,再獲取File的相關資訊,如下所示:

int fileUriIdx = c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI);
String fileUri = c.getString(fileUriIdx);
String fileName = null;
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
    if (fileUri != null) {
        fileName = Uri.parse(fileUri).getPath();
    }
} else {
    //Android 7.0以上的方式:請求獲取寫入許可權,這一步報錯
    //過時的方式:DownloadManager.COLUMN_LOCAL_FILENAME
    int fileNameIdx = c.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME);
    fileName = c.getString(fileNameIdx);
}

執行之後,不會報錯,並且能夠獲取到正常的檔名。

這裡寫圖片描述