1. 程式人生 > >Android元件系列----ContentProvider內容提供者

Android元件系列----ContentProvider內容提供者

【宣告】 

歡迎轉載,但請保留文章原始出處→_→ 

生命壹號:http://www.cnblogs.com/smyhvae/

文章來源:http://www.cnblogs.com/smyhvae/p/4108017.html

 

【正文】


一、ContentProvider簡介:

ContentProvider內容提供者(四大元件之一)主要用於在不同的應用程式之間實現資料共享的功能

ContentProvider可以理解為一個Android應用對外開放的介面,只要是符合它所定義的Uri格式的請求,均可以正常訪問執行操作。其他的Android應用可以使用ContentResolver物件通過與ContentProvider同名的方

請求執行,被執行的就是ContentProvider中的同名方法。所以ContentProvider有很多對外可以訪問的方法,在ContentResolver中均有同名的方法,是一一對應的,來看 下面這一張圖:

12154217-a98af1e70c7e46cca16299e42ee30fb3

Android附帶了許多有用的ContentProvider,但是本文暫時不涉及到這麼多(本文將學習如何建立自己的ContentProvider)。Android中自帶的ContentProvider包括:

  • Browser:儲存如瀏覽器的資訊。
  • CallLog:儲存通話記錄等資訊。
  • Contacts Provider:儲存聯絡人(通訊錄)等資訊。
  • MediaStore:儲存媒體檔案的資訊。
  • Settings:儲存裝置的設定和首選項資訊。

此外,還有日曆、

ContentProvider的方法:

如果要建立自己的內容提供者,需要新建一個類繼承抽象類ContentProvider,並重寫其中的抽象方法。抽象方法如下:

複製程式碼
boolean onCreate()   
初始化提供者

Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)  
查詢資料,返回一個數據Cursor物件。其中引數selection和selectionArgs是外部程式提供的查詢條件

Uri insert(Uri uri, ContentValues values) 
插入一條資料。引數values是需要插入的值

int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) 根據條件更新資料 int delete(Uri uri, String selection, String[] selectionArgs) 根據條件刪除資料 String getType(Uri uri) 返回MIME型別對應內容的URI
複製程式碼

除了onCreate()和getType()方法外,其他的均為CRUD操作,這些方法中,Uri引數為與ContentProvider匹配的請求Uri,剩下的引數可以參見SQLite的CRUD操作,基本一致。 

備註:還有兩個非常有意思的方法,必須要提一下,call()和bulkInsert()方法,使用call,理論上可以在ContentResolver中執行ContentProvider暴露出來的任何方法,而bulkInsert()方法用於插入多條資料。

Uri:

在Android中,Uri是一種比較常見的資源訪問方式。而對於ContentProvider而言,Uri也是有固定格式的:<srandard_prefix>://<authority>/<data_path>/<id>

  • <srandard_prefix>:ContentProvider的srandard_prefix始終是content://。
  • <authority>:ContentProvider的名稱。
  • <data_path>:請求的資料型別。
  • <id>:指定請求的特定資料。

在ContentProvider的CRUD操作,均會傳遞一個Uri物件,通過這個物件來匹配對應的請求。那麼如何確定一個Uri執行哪項操作呢?需要用到一個UriMatcher物件,這個物件用來幫助內容提供者匹配Uri。它所提供的方法非常簡單,僅有兩個:

  • void addURI(String authority,String path,int code):新增一個Uri匹配項,authority為AndroidManifest.xml中註冊的ContentProvider中的authority屬性;path為一個路徑,可以設定萬用字元,#表示任意數字,*表示任意字元code為自定義的一個Uri程式碼。
  • int match(Uri uri):匹配傳遞的Uri,返回addURI()傳遞的code引數。

 

二、程式碼舉例:

最終所有工程檔案的目錄結構如下:

5b1e38fc-929c-4587-a2b2-87f060663db5

PersonDao是增刪改查資料庫的工具類,並在PersonContentProvider中得到呼叫。DBHelper用於初始化SQLite資料庫。

PersonContentProvider用於向外提供增刪改查的介面。並最終在ContentResolverTest的MyTest.java中進行單元測試,實現CRUD。

本文的核心類是:PersonContentProvider和MyTest

下面來看一下具體的實現步驟。

新建工程檔案ContetProviderTest01。

(1)新建類PersonDao:用於進行對SQLite的CRUD操作。程式碼如下:

PersonDao.java:

複製程式碼
 1 package com.example.contentprovidertest01.dao;
 2 
 3 import android.content.ContentValues;
 4 import android.content.Context;
 5 import android.database.Cursor;
 6 import android.database.sqlite.SQLiteDatabase;
 7 
 8 import com.example.contentprovidertest01.db.DBHelper;
 9 
10 public class PersonDao {
11     private DBHelper helper = null;
12 
13     public PersonDao(Context context) {
14         helper = new DBHelper(context);
15     }
16 
17     //方法:插入操作,返回的long型別為:插入當前行的行號
18     public long insertPerson(ContentValues values) {
19         long id = -1;
20         SQLiteDatabase database = null;
21         try {
22             database = helper.getWritableDatabase();
23             id = database.insert("person", null, values);
24         } catch (Exception e) {
25             e.printStackTrace();
26         } finally {
27             if (database != null) {
28                 database.close();
29             }
30         }
31         return id;
32     }
33 
34     public int deletePerson(String whereClause, String[] whereArgs) {
35         int count = -1;
36         SQLiteDatabase database = null;
37         try {
38             database = helper.getWritableDatabase();
39             count = database.delete("person", whereClause, whereArgs);
40         } catch (Exception e) {
41             e.printStackTrace();
42         } finally {
43             if (database != null) {
44                 database.close();
45             }
46         }
47         return count;
48     }
49 
50     public int updatePerson(ContentValues values, String whereClause,
51             String[] whereArgs) {
52         SQLiteDatabase database = null;
53         int count = -1;
54         try {
55             database = helper.getWritableDatabase();
56             count = database.update("person", values, whereClause, whereArgs);
57         } catch (Exception e) {
58             e.printStackTrace();
59         } finally {
60             if (null != database) {
61                 database.close();
62             }
63         }
64         return count;
65     }
66 
67     public Cursor queryPersons(String selection, String[] selectionArgs) {
68         SQLiteDatabase database = null;
69         Cursor cursor = null;
70         try {
71             database = helper.getReadableDatabase();
72             cursor = database.query(true, "person", null, selection,
73                     selectionArgs, null, null, null, null);
74         } catch (Exception e) {
75             e.printStackTrace();
76         } finally {
77             if (null != database) {
78                 // database.close();
79             }
80         }
81         return cursor;
82     }
83 
84 }
複製程式碼

(2)新建類DBHelper:用於初始化SQLiate資料庫

DBHelper.java:

複製程式碼
 1 package com.example.contentprovidertest01.db;
 2 
 3 import android.content.Context;
 4 import android.database.sqlite.SQLiteDatabase;
 5 import android.database.sqlite.SQLiteOpenHelper;
 6 
 7 public class DBHelper extends SQLiteOpenHelper {
 8 
 9     private static String name = "mydb.db"; // 資料庫的名字
10     private static int version = 1; // 資料庫的版本
11 
12     public DBHelper(Context context) {
13         super(context, name, null, version);
14     }
15 
16     @Override
17     public void onCreate(SQLiteDatabase db) {
18         // 只能支援基本資料型別:varchar int long float boolean text blob clob
19         // 建表語句執行
20         String sql = "create table person(id integer primary key autoincrement,name varchar(64),address varchar(64))";
21         db.execSQL(sql);
22     }
23 
24     @Override
25     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
26         // TODO Auto-generated method stub
27         String sql = "alter table person add sex varchar(8)";
28         db.execSQL(sql);
29     }
30 
31 }
複製程式碼

(3)【核心】新建類PersonContentProvider,繼承ContetProvider

PersonContentProvider.java:

複製程式碼
  1 package com.example.contentprovidertest01;
  2 
  3 import com.example.contentprovidertest01.dao.PersonDao;
  4 
  5 import android.content.ContentProvider;
  6 import android.content.ContentUris;
  7 import android.content.ContentValues;
  8 import android.content.UriMatcher;
  9 import android.database.Cursor;
 10 import android.net.Uri;
 11 import android.os.Bundle;
 12 import android.util.Log;
 13 
 14 public class PersonContentProvider extends ContentProvider {
 15 
 16     private final String TAG = "PersonContentProvider";
 17     private PersonDao personDao = null;
 18     private static final UriMatcher URI_MATCHER = new UriMatcher(
 19             UriMatcher.NO_MATCH);// 預設的規則是不匹配的
 20     private static final int PERSON = 1; // 操作單行記錄
 21     private static final int PERSONS = 2; // 操作多行記錄
 22     // 往UriMatcher中新增匹配規則。注意,這裡面的url不要寫錯了,我就是因為寫錯了,半天沒調試出來。哎···
 23     static {
 24         // 新增兩個URI篩選
 25         URI_MATCHER.addURI("com.example.contentprovidertest01.PersonContentProvider",
 26                 "person", PERSONS);
 27         // 使用萬用字元#,匹配任意數字
 28         URI_MATCHER.addURI("com.example.contentprovidertest01.PersonContentProvider",
 29                 "person/#", PERSON);
 30     }
 31 
 32     public PersonContentProvider() {
 33 
 34     }
 35 
 36     @Override
 37     public boolean onCreate() {
 38         // 初始化一個數據持久層
 39         personDao = new PersonDao(getContext());
 40         //Log.i(TAG, "--->>onCreate()被呼叫");
 41         return true;
 42     }
 43 
 44     @Override
 45     public Uri insert(Uri uri, ContentValues values) {
 46         Uri resultUri = null;
 47         // 解析Uri,返回Code
 48         int flag = URI_MATCHER.match(uri);
 49         switch (flag) {
 50         case PERSONS:
 51             //呼叫資料庫的訪問方法   
 52             long id = personDao.insertPerson(values); //執行插入操作的方法,返回插入當前行的行號
 53             resultUri = ContentUris.withAppendedId(uri, id);
 54             Log.i(TAG,"--->>插入成功, id=" + id);
 55             Log.i(TAG,"--->>插入成功, resultUri=" + resultUri.toString());
 56             System.out.println("insert success");
 57             break;
 58         }
 59         return resultUri;
 60     }
 61 
 62     //方法:刪除記錄。注:引數:selection和selectionArgs是查詢的條件,是由外部(另一個應用程式)傳進來的
 63     @Override
 64     public int delete(Uri uri, String selection, String[] selectionArgs) {
 65         int count = -1; //影響資料庫的行數
 66         try {
 67             int flag = URI_MATCHER.match(uri);
 68             switch (flag) {
 69             case PERSON:
 70                 // delete from student where id=?
 71                 // 單條資料,使用ContentUris工具類解析出結尾的Id
 72                 long id = ContentUris.parseId(uri);
 73                 String where_value = "id = ?";
 74                 String[] args = { String.valueOf(id) };
 75                 count = personDao.deletePerson(where_value, args);
 76                 break;
 77             case PERSONS:
 78                 count = personDao.deletePerson(selection, selectionArgs);
 79                 break;
 80             }
 81         } catch (Exception e) {
 82             e.printStackTrace();
 83         }
 84         Log.i(TAG, "--->>刪除成功,count=" + count);
 85         return count;
 86     }
 87 
 88     @Override
 89     public int update(Uri uri, ContentValues values, String selection,
 90             String[] selectionArgs) {
 91         int count = -1;
 92         try {
 93             int flag = URI_MATCHER.match(uri);
 94             switch (flag) {
 95             case PERSON:
 96                 long id = ContentUris.parseId(uri);
 97                 String where_value = " id = ?";
 98                 String[] args = { String.valueOf(id) };
 99                 count = personDao.updatePerson(values, where_value, args);
100                 break;
101             case PERSONS:
102                 count = personDao
103                         .updatePerson(values, selection, selectionArgs);
104                 break;
105             }
106         } catch (Exception e) {
107             e.printStackTrace();
108         }
109         Log.i(TAG, "--->>更新成功,count=" + count);
110         return count;
111     }
112 
113     @Override
114     public Cursor query(Uri uri, String[] projection, String selection,
115             String[] selectionArgs, String sortOrder) {
116         Cursor cursor = null;
117         try {
118             int flag = URI_MATCHER.match(uri);
119             switch (flag) {
120             case PERSON:
121                 long id = ContentUris.parseId(uri);
122                 String where_value = " id = ?";
123                 String[] args = { String.valueOf(id) };
124                 cursor = personDao.queryPersons(where_value, args);
125                 break;
126             case PERSONS:
127                 cursor = personDao.queryPersons(selection, selectionArgs);
128                 break;
129             }
130         } catch (Exception e) {
131             e.printStackTrace();
132         }
133         Log.i(TAG, "--->>查詢成功,Count=" + cursor.getCount());
134         return cursor;
135     }
136 
137     @Override
138     public String getType(Uri uri) {
139         int flag = URI_MATCHER.match(uri);
140         switch (flag) {
141         case PERSON:
142             return "vnd.android.cursor.item/person"; // 如果是單條記錄,則為vnd.android.cursor.item/
143                                                         // + path
144 
145         case PERSONS:
146             return "vnd.android.cursor.dir/persons"; // 如果是多條記錄,則為vnd.android.cursor.dir/
147                                                         // + path
148         }
149         return null;
150     }
151 
152     @Override
153     public Bundle call(String method, String arg, Bundle extras) {
154         Log.i(TAG, "--->>" + method);
155         Bundle bundle = new Bundle();
156         bundle.putString("returnCall", "call被執行了");
157         return bundle;
158     }
159 }
複製程式碼

18行的UriMatcher類的作用是:匹配內容uri,預設的規則是不匹配的。UriMatcher提供了一個addURI方法:

  • void android.content.UriMatcher.addURI(String authority, String path, int code)

這三個引數分別代表:許可權、路徑、和一個自定義程式碼。一般第一個引數是uri(包名.內容提供者的類名),第二個引數一般是資料庫的表名。

27行:匹配規則的解釋:*表示匹配任意字元,#表示匹配任意數字注:如果內部的匹配規則越多,越容易訪問。

138行的getType(Uri uri)方法:所有的內容提供者都必須提供的一個方法。用於獲取uri物件所對應的MIME型別。

然後,每編寫一個內容提供者,都必須在清單檔案中進行宣告。在AndroidManifest.xml中<application>節點中增加,格式如下:

<provider
    android:name=".內容提供者的類名"
    android:authorities="包名.內容提供者的類名" >
</provider>

第3行表示的是uri路徑,畢竟Contet Provider是通過路徑來訪問的。

所以在本程式中,在AndroidManifest.xml的<application>節點中增加如下程式碼:

<provider
  android:name=".PersonContentProvider"
  android:authorities="com.example.contentprovidertest01.PersonContentProvider" >
</provider>

(4)單元測試類:

這裡需要涉及到另外一個知識:ContentResolver內容訪問者

要想訪問ContentProvider,則必須使用ContentResolver。可以通過ContentResolver來操作ContentProvider所暴露處理的介面。一般使用Content.getContentResolver()方法獲取ContentResolver物件。第一段中已經提到:ContentProvider有很多對外可以訪問的方法,在ContentResolver中均有同名的方法,是一一對應的。所以它也存在insert、query、update、delete等方法。於是單元測試類可以這樣寫:(注:單元測試如果不清楚,可以參考另外一篇文章: JUnit單元測試的使用

MyTest.java:

複製程式碼
  1 package com.example.contentresolvertest;
  2 
  3 import android.content.ContentResolver;
  4 import android.content.ContentValues;
  5 import android.database.Cursor;
  6 import android.net.Uri;
  7 import android.os.Bundle;
  8 import android.test.AndroidTestCase;
  9 import android.util.Log;
 10 
 11 public class MyTest extends AndroidTestCase {
 12 
 13     public MyTest() {
 14         // TODO Auto-generated constructor stub
 15 
 16     }
 17 
 18     public void calltest() {
 19         ContentResolver contentResolver = getContext().getContentResolver();
 20         Uri uri = Uri
 21                 .parse("content://com.example.contentprovidertest01.PersonContentProvider/person");
 22         Bundle bundle = contentResolver.call(uri, "method", null, null);
 23         String returnCall = bundle.getString("returnCall");
 24         Log.i("main", "-------------->" + returnCall);
 25     }
 26 
 27     //測試方法:向資料庫中新增記錄。如果之前沒有資料庫,則會自動建立
 28     public void insert() {
 29         // 使用內容解析者ContentResolver訪問內容提供者ContentProvider
 30         ContentResolver contentResolver = getContext().getContentResolver();
 31         ContentValues values = new ContentValues();
 32         values.put("name", "生命貳號");
 33         values.put("address", "湖北");
 34         // content://authorities/person
 35         // http://
 36         Uri uri = Uri
 37                 .parse("content://com.example.contentprovidertest01.PersonContentProvider/person");
 38         contentResolver.insert(uri, values);
 39     }
 40 
 41     //測試方法:刪除單條記錄。如果要刪除所有記錄:content://com.example.contentprovidertest01.PersonContentProvider/person
 42     public void delete() {
 43         ContentResolver contentResolver = getContext().getContentResolver();
 44         Uri uri = Uri
 45                 .parse("content://com.example.contentprovidertest01.PersonContentProvider/person/2");//刪除id為1的記錄
 46         contentResolver.delete(uri, null, null);
 47     }
 48 
 49     //測試方法:根據條件刪除記錄。
 50     public void deletes() {
 51         ContentResolver contentResolver = getContext().getContentResolver();
 52         Uri uri = Uri
 53                 .parse("content://com.example.contentprovidertest01.PersonContentProvider/person");
 54         String where = "address=?"; 
 55         String[] where_args = { "HK" };
 56         contentResolver.delete(uri, where, where_args);  //第二個引數表示查詢的條件"address=?",第三個引數表示佔位符中的具體內容
 57     }
 58 
 59     //方法:根據id修改記錄。注:很少有批量修改的情況。
 60     public void update() {
 61         ContentResolver contentResolver = getContext().getContentResolver();
 62         Uri uri = Uri
 63                 .parse("content://com.example.contentprovidertest01.PersonContentProvider/person/2");
 64         ContentValues values = new ContentValues();
 65         values.put("name", "李四");
 66         values.put("address", "上海");
 67         contentResolver.update(uri, values, null, null);
 68     }
 69 
 70     //方法:根據條件來修改記錄。
 71     public void updates() {
 72         ContentResolver contentResolver = getContext().getContentResolver();
 73         Uri uri = Uri
 74                 .parse("content://com.example.contentprovidertest01.PersonContentProvider/person/student");
 75         ContentValues values = new ContentValues();
 76         values.put("name", "王五");
 77         values.put("address", "深圳");
 78         String where = "address=?";
 79         String[] where_args = { "beijing" };
 80         contentResolver.update(uri, values, where, where_args);
 81     }
 82 
 83     //測試方法:查詢所有記錄。如果要查詢單條記錄:content://com.example.contentprovidertest01.PersonContentProvider/person/1
 84     public void query() {
 85         ContentResolver contentResolver = getContext().getContentResolver();
 86         Uri uri = Uri
 87                 .parse("content://com.example.contentprovidertest01.PersonContentProvider/person");
 88         Cursor cursor = contentResolver.query(uri, null, null, null, null);
 89         while (cursor.moveToNext()) {
 90             Log.i("MyTest",
 91                     "--->>"
 92                             + cursor.getString(cursor.getColumnIndex("name")));
 93         }
 94     }
 95 
 96     //測試方法:根據條件查詢所有記錄。
 97     public void querys() {
 98         ContentResolver contentResolver = getContext().getContentResolver();
 99         Uri uri = Uri
100                 .parse("content://com.example.contentprovidertest01.PersonContentProvider/person");
101         String where = "address=?";
102         String[] where_args = { "深圳" };
103         Cursor cursor = contentResolver.query(uri, null, where, where_args,
104                 null);
105         while (cursor.moveToNext()) {
106             Log.i("main",
107                     "-------------->"
108                             + cursor.getString(cursor.getColumnIndex("name")));
109         }
110     }
111 
112 }
複製程式碼

既然ContetProvider實現的是跨應用訪問資料,那這個測試類Test.java就應該寫在另一個應用程式中才行。於是,我們新建另外一個工程檔案ContentResolverTest,在裡面新增單元測試,裡面的程式碼其實和上方的Test.java的程式碼是一模一樣的。執行單元測試,依然能在ContentResolverTest中實現對ContentProviderTest01中的CRUD.核心在於:使用應用1中的內容解析者ContentResolver訪問應用2中的內容提供者ContentProvider

現在執行ContentProviderTest01中的單元測試類:

1、執行insert()方法,實現插入操作。後臺列印如下:

7a532114-aa85-466f-8237-203e2b109eb2

上圖中紅框部分表明,這個uri就是代表內容提供者中,person表中,id為1的資料。

此時,開啟file Explorer,進行檢視,發現確實多了個檔案:

bb395fd7-353b-4240-86fe-a484de71fa26

注意:如果SQLite中之前沒有mydb.db這個資料庫,當實現插入操作時,會自動建立mydb.db這個資料庫,並自動建立person表(因為在PersonDao類中執行了getWritableDatabase()方法)。

現在將上圖中的mydb.db匯出,然後用SQLiteExpert軟體開啟,輸入sql查詢語句,就可以看到person表中的資料了:

a4b85f6c-ddf9-49a9-8d53-f6e0e32e93f9

如果再執行insert()方法,又會繼續新增一條記錄(id是自動增長的)。

2、執行query()方法,查詢所有記錄(目前一共兩條記錄)。後臺輸出效果如下:

c5324531-b69a-46f1-be87-295d5e23806b

經測試,其他方法也都是可以執行的。

事實證明,新建的另外一個工程檔案ContentResolverTest中,在裡面執行單元測試,也是可以執行的(單元測試的程式碼不變,實現的CRUD功能也一模一樣),也就是說,能夠對ContentProviderTest01中的SQLite進行CRUD操作。例如,執行query()方法,後臺輸出如下:

4180318a-9f6a-42ea-95de-cdc3548c7bd2

這樣,我們的目的也就達到了。

【特別注意】

需要特別注意的是,程式碼中uri不要寫錯了,這些錯誤一旦發生,很難被發現具體表現在:

1、清單檔案中:

<provider
    android:name=".內容提供者的類名" 
    android:authorities="包名.內容提供者的類名" >
</provider>

如:

        <provider
            android:name=".PersonContentProvider"
            android:authorities="com.example.contentprovidertest01.PersonContentProvider" >
        </provider>

2、ContentProvider類中的UriMatcher中的uri:

複製程式碼
 1     private static final UriMatcher URI_MATCHER = new UriMatcher(
 2             UriMatcher.NO_MATCH);// 預設的規則是不匹配的
 3     private static final int PERSON = 1; // 操作單行記錄
 4     private static final int PERSONS = 2; // 操作多行記錄
 5     // 往UriMatcher中新增匹配規則。注意,這裡面的url不要寫錯了,我就是因為寫錯了,半天沒調試出來。哎···
 6     static {
 7         // 新增兩個URI篩選
 8         URI_MATCHER.addURI("com.example.contentprovidertest01.PersonContentProvider",
 9                 "person", PERSONS);
10         // 使用萬用字元#,匹配任意數字
11         URI_MATCHER.addURI("com.example.contentprovidertest01.PersonContentProvider",
12                 "person/#", PERSON);
13     }
複製程式碼

3、ContentProvider類中的getType()方法裡面的程式碼:

複製程式碼
 1     @Override
 2     public String getType(Uri uri) {
 3         int flag = URI_MATCHER.match(uri);
 4         switch (flag) {
 5         case PERSON:
 6             return "vnd.android.cursor.item/person"; // 如果是單條記錄,則為vnd.android.cursor.item/
 7                                                         // + path
 8         case PERSONS:
 9             return "vnd.android.cursor.dir/persons"; // 如果是多條記錄,則為vnd.android.cursor.dir/
10                                                         // + path
11         }
12         return null;
13     }
複製程式碼

4、ContentResolver類中的uri:(以insert()方法為例)

複製程式碼
 1     //測試方法:向資料庫中新增記錄。如果之前沒有資料庫,則會自動建立
 2     public void insert() {
 3         // 使用內容解析者ContentResolver訪問內容提供者ContentProvider
 4         ContentResolver contentResolver = getContext().getContentResolver();
 5         ContentValues values = new ContentValues();
 6         values.put("name", "生命貳號");
 7         values.put("address", "湖北");
 8         // content://authorities/person
 9         // http://
10         Uri uri = Uri
11                 .parse("content://com.example.contentprovidertest01.PersonContentProvider/person");
12         contentResolver.insert(uri, values);
13     }
複製程式碼

【工程檔案】

連結:http://pan.baidu.com/s/1hq7VO12 

密碼:0a49