1. 程式人生 > >使用Android內部的DownloadProvider下載檔案,並獲取cache許可權 .

使用Android內部的DownloadProvider下載檔案,並獲取cache許可權 .

Android內部提供了一個DownloadProvider,是一個非常完整的下載工具,提供了很好的外部介面可以被其他應用程式呼叫,來完成下載工作。同時也提供和很好的下載、通知、儲存等機制。
在Android的Browser等工具裡面都用到了這個DownloadProvider。

但是很遺憾的是,這個DownloadProvider不對app開發人員開放,只作為內部使用。

我們現在去探究如何將DownloadProvider拿來給自己用。

讓我們先找到DownloadProvider不能用的原因:
先找到它的原始碼,在這個位置:/packages/providers/DownloadProvider
開啟AndroidManifest.xml檔案,裡面有幾個自定義的許可權

    <!-- Allows access to the Download Manager -->
    <permission android:name="android.permission.ACCESS_DOWNLOAD_MANAGER"
        android:label="@string/permlab_downloadManager"
        android:description="@string/permdesc_downloadManager"
        android:protectionLevel="signatureOrSystem" />

    <!-- Allows advanced access to the Download Manager -->
    <permission android:name="android.permission.ACCESS_DOWNLOAD_MANAGER_ADVANCED"
        android:label="@string/permlab_downloadManagerAdvanced"
        android:description="@string/permdesc_downloadManagerAdvanced"
        android:protectionLevel="signatureOrSystem" />

    <!-- Allows filesystem access to /cache -->
    <permission android:name="android.permission.ACCESS_CACHE_FILESYSTEM"
        android:label="@string/permlab_cacheFilesystem"
        android:description="@string/permdesc_cacheFilesystem"
        android:protectionLevel="signature" />

    <!-- Allows to send download completed intents -->
    <permission android:name="android.permission.SEND_DOWNLOAD_COMPLETED_INTENTS"
        android:label="@string/permlab_downloadCompletedIntent"
        android:description="@string/permdesc_downloadCompletedIntent"
        android:protectionLevel="signature" />

這幾個許可權裡面都是android:protectionLevel="signatureOrSystem" 或者   android:protectionLevel="signature", 這個意思是隻有你的app擁有system許可權,或者和系統一樣的簽名,才能呼叫它。

這裡是問題的關鍵。那我們有兩種思路:
一種思路是:將這個protectionLevel改成normal,重新編譯DownloadProvider工程,讓其他app可以直接呼叫。
另一種思路是:將你自己的app弄成system許可權或者和系統一樣的簽名。

前一種思路已經完全成功了,第二種思路驗證了一部分。

先看第一種思路的辦法:
1)先將上面幾個許可權都改成:android:protectionLevel="normal"
2)重新編譯DownloadProvider
   mmm packages/providers/DownloadProvider
3) 將編譯後的apk替換現有的apk
   因為DownloadProvider.apk是系統app,你可以先給/system以root許可權,然後將這個app替換掉。 (作為一個使用者app安裝也可以,不過重啟以後就沒有了)
   使用類似 # mount -t ubifs -o remount ubi0:system /system   或者  # mount -o remount ubi0:system /system  給/system rw許可權。
   然後通過adb push 將DownloadProvider.apk push到 /system/app/下。系統會自動替換這個app。

4)寫一個工程來使用DownloadProvider.
   直接貼原始碼了:
   DownloadActivity.Java


package com.xxxx.usedownload;
 
import java.io.FileNotFoundException;
import java.NET.URI;

import android.app.Activity;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.webkit.URLUtil;

/**
 * @author lixinso
 * 使用DownloadProvider
 */
public class DownloadActivity extends Activity {
     @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
       
        //String url = "http://192.168.200.76:8080/webserver/dancing-skeleton.3gp";
        String contentDisposition = "attachment; filename=/"dancing-skeleton.3gp/"";
        String mimetype = "video/3GPP";
       
        String filename = URLUtil.guessFileName(url,contentDisposition, mimetype);
       
        URI uri = null;
        
        try {
            // Undo the percent-encoding that KURL may have done.
            String newUrl = new String(URLUtil.decode(url.getBytes()));
            // Parse the url into pieces
            WebAddress w = new WebAddress(newUrl);
            String frag = null;
            String query = null;
            String path = w.mPath;
            // Break the path into path, query, and fragment
            if (path.length() > 0) {
                // Strip the fragment
                int idx = path.lastIndexOf('#');
                if (idx != -1) {
                    frag = path.substring(idx + 1);
                    path = path.substring(0, idx);
                }
                idx = path.lastIndexOf('?');
                if (idx != -1) {
                    query = path.substring(idx + 1);
                    path = path.substring(0, idx);
                }
            }
            uri = new URI(w.mScheme, w.mAuthInfo, w.mHost, w.mPort, path,
                    query, frag);
        } catch (Exception e) {
            //Log.e(LOGTAG, "Could not parse url for download: " + url, e);
            return;
        }
       
        ContentValues values = new ContentValues();
        values.put("uri", uri.toString());
        values.put("useragent", "Mozilla/5.0 (linux; U; Android 1.5; en-us; SDK Build/CUPCAKE) AppleWebKit/528.5+ (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1");
        values.put("notificationpackage", getPackageName());
        values.put("notificationclass", "HelloWorld");
        values.put("visibility", 1);
        values.put("mimetype", mimetype);
        values.put("hint", filename);
        values.put("description", uri.getHost());
        values.put("total_bytes", 1349528);
        values.put("destination", 1);
       
       
        
        //這些引數參考:DownloadProvider工程中的:Helpers.java
        //public static DownloadFileInfo generateSaveFile(
        //        Context context,
        //      String url,
        //        String hint,
        //        String contentDisposition,
        //        String contentLocation,
        //        String mimeType,
        //        int destination,
        //        int contentLength) throws FileNotFoundException {
        //以及:  framework裡的Downloads.java;
       
       
        ContentResolver mResolver = getContentResolver();
        mResolver.insert(Uri.parse("content://downloads/download"), values);
       
    }
}

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.xxxx.usedownload"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".DownloadActivity"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
 
    </application>
    <uses-sdk android:minSdkVersion="7" />
   
    <uses-permission android:name="android.permission.ACCESS_CACHE_FILESYSTEM" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
    <uses-permission android:name="android.permission.ACCESS_DOWNLOAD_MANAGER" />
    <uses-permission android:name="android.permission.ACCESS_DOWNLOAD_MANAGER_ADVANCED" />
    <uses-permission android:name="android.permission.ACCESS_DRM" />
     <uses-permission android:name="android.permission.SEND_DOWNLOAD_COMPLETED_INTENTS" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.INSTALL_DRM" />
   
</manifest>

程式碼裡面引用了ParseException和WebAddress兩個類,可以從Android原始碼裡找到copy進來,在這裡frameworks/base/core/java/android/net。

程式碼裡面有幾個地方比較重要的:
a) 通過往DownloadProvider提供的ContentProvider “content://downloads/download” 中插入資料就能觸發DownloadProvider的執行。
b) values.put("destination", 1); 是下載檔案儲存在什麼地方, 如果沒有這個引數,預設儲存在sdcard的download 下面 (Constants.java 中的 DEFAULT_DL_SUBDIR = "/download" )
   如果指定為1,是往記憶體的 /cache目錄下存東西 (在/frameworks/base/core/java/android/provider/Downloads.java中定義, public static final int DESTINATION_CACHE_PARTITION = 1; )
b) 注意Manifest中的一堆許可權: ACCESS_DOWNLOAD_MANAGER是最基本的許可權,這樣可以使用DownloadProvider下載。
   如果需要destination=1,則需要 ACCESS_DOWNLOAD_MANAGER許可權。(Downloads.java中的註釋 : All file types are allowed, and only the initiating
     application can access the file (indirectly through a content provider). This requires the android.permission.ACCESS_DOWNLOAD_MANAGER_ADVANCED permission.)

如果沒有這個許可權,在往 content://downloads/download插入的時候有許可權問題報錯:
09-16 17:16:38.062: ERROR/DatabaseUtils(763): Writing exception to parcel
09-16 17:16:38.062: ERROR/DatabaseUtils(763): java.lang.SecurityException: unauthorized destination code
09-16 17:16:38.062: ERROR/DatabaseUtils(763):     at com.android.providers.downloads.DownloadProvider.insert(DownloadProvider.java:277)
09-16 17:16:38.062: ERROR/DatabaseUtils(763):     at android.content.ContentProvider$Transport.insert(ContentProvider.java:150)
09-16 17:16:38.062: ERROR/DatabaseUtils(763):     at android.content.ContentProviderNative.onTransact(ContentProviderNative.java:140)
09-16 17:16:38.062: ERROR/DatabaseUtils(763):     at android.os.Binder.execTransact(Binder.java:287)
09-16 17:16:38.062: ERROR/DatabaseUtils(763):     at dalvik.system.NativeStart.run(Native Method)
09-16 17:16:38.102: DEBUG/AndroidRuntime(4086): Shutting down VM

因為DownloadProvider.java中有這段程式碼:
        if (dest != null) {
            if (getContext().checkCallingPermission(Downloads.PERMISSION_ACCESS_ADVANCED)
                    != PackageManager.PERMISSION_GRANTED
                    && dest != Downloads.DESTINATION_EXTERNAL
                    && dest != Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE) {
                throw new SecurityException("unauthorized destination code");
            }
所以:要往/cache目錄下存東西,一定要記得這個許可權哦。

實際執行起來,只加這個許可權往/cache下存東西還不夠,就又把其他一堆許可權都加上了,具體哪些有用還沒細看。

5) 將這個app直接以普通app安裝上去,執行,可以看到下載成功到/cache裡了。


第二種思路就是想辦法獲得system許可權或者簽名:
這樣不修改DownloadProvider的程式碼,不動它。
而是將自己編寫的app做完以後放到/packages/app目錄下和整個系統一起編譯,將其編譯到img中的系統app下 這樣編譯完成以後執行,使用編譯的img執行模擬器。在模擬器中啟動自己寫的呼叫DownloadProvider的app,發現竟然也是可以呼叫的。
不過這種方法在模擬器上成功了,但是在真機上沒成功,可能還有些問題沒解決。第一種方法是完全成功的。