內容提供者ContentProvider,是Android 的四大組件之一。 ...
內容提供者ContentProvider,是Android 的四大組件之一。內容提供者是應用程式之間共用數據的介面。應用程式創建的資料庫,預設情況下是私有的,別的應用程式訪問不到數據,如果想把數據對外提供,就要用到內容提供。ContentProvider屏蔽了數據存儲的細節,內部實現對用戶完全透明, 用戶只需要關心操作數據的uri就可以了,ContentProvider可以實現不同app之間共用。 Sql也有增刪改查的方法,但是sql只能查詢本應用下的資料庫。 而ContentProvider 還可以去增刪改查本地文件/xml文件的讀取等。Android 系統將這種機制應用到方方面面,比如:聯繫人(通訊錄應用程式)Provider 專為不同應用程式提供聯繫人數據;簡訊(簡訊應用程式)Provider 專為不同應用程式提供系統簡訊信息。當應用繼承ContentProvider 類,並重寫該類用於提供數據和存儲數據的方法,就可以向其他應用共用其數據。雖然使用其他方法也可以對外共用數據,但數據訪問方式會因數據存儲的方式而不同,如:採用文件方式對外共用數據,需要進行文件操作讀寫數據;採用SharedPreferences 共用數據,需要使用SharedPreferences API 讀寫數據。而使用ContentProvider 共用數據的好處是統一了數據訪問方式。總之,內容提供者管理了對結構化數據最常見的就是資料庫中數據的訪問,操作內容提供者是不同進程之間以資料庫數據形式交互數據的標準方式。
自定義的內容提供者包括內容提供者和訪問者兩個部分。
內容提供者,擁有自己的資料庫,將資料庫暴露出來供訪問者修改。ContenProvider的編寫基本步驟:
1. 寫一個類繼承 ContentProvider;
2. 重寫一系列的方法,包括資料庫操作的空實現;
3. 在內容提供者代碼內部定義UriMatcher -用於判斷uri是否匹配
static UriMatcher mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); static { mUriMatcher.addURI("在清單文件裡面定義的authorities", "自定義匹配字元串", 成功返回的標識);
}
4. 在增刪改查執行的時候判斷uri是否合法,在內容提供者內部實現對資料庫的增刪改查;
5. 清單文件的下麵聲明provider,這裡需要指定主機名,也就是對外提供的Uri,當訪問者在內容解析者中傳入同一個uri時,才可以訪問到資料庫;
<provider
android:name="com.itheima.db.BankDBBackdoor"
android:authorities="自定義主機名" >
</provider>
訪問者,存在於另外一個工程中,可以對內提供者的資料庫進行操作。
1. 創建內容提供者解析器
ContentResolver resolver = 上下文.getContentResolver();
2. 定義要訪問的Uri路徑
Uri uri = Uri.parse("content://自定義主機名/自定義匹配字元串") // “content://”是標準寫法
3. 利用內容提供者解析器進行增刪改查,實現對資料庫的操作
內容提供者Uri 的書寫模板: content:// 主機名authority/path/id。具體的書寫規範如下所示:
1. "content://" 這是個固定寫法,用來說明一個ContentProvider 控制這些數據。
2. 主機名或授權Authority:它定義了是哪個ContentProvider 提供這些數據。
3. path:路徑,URI 下的某一個Item。
4. ID:通常定義Uri 時使用”#”號占位符代替, 使用時替換成對應的數字。#表示數據id,#代表任意數字,*用來來匹配任意文本
使用內容提供者操作系統簡訊和操作系統聯繫人是我們開發中經常遇到的需求,而自定義內容提供者對外提供數據反而使用的場景並不多,除非我們開發的簡訊或者聯繫人應用。一個小細節是,由於讀取和插入系統簡訊資料庫都涉及到可能侵犯用戶隱私,因此創建的工程必須添加相關的許可權。下麵就分別講解一下使用內容提供者操作系統資料庫和自定義內容提供者。
用內容提供者操作系統簡訊只需要關註的到系統簡訊資料庫的一張表,最長用的數據有body,date,type,address,各自的含義也較為直觀。body表示簡訊的內容,date表示發送簡訊或收到簡訊的時間,type表示是受到簡訊還是發送簡訊,address表示收到的簡訊來自於哪個手機號。讀取和插入系統簡訊資料庫需要添加如下許可權:
<uses-permission android:name="android.permission.READ_SMS"/>
<uses-permission android:name="android.permission.WRITE_SMS"/>
使用內容觀察者操作系統聯繫人時需要關註三張張數據表,rawcontact表 ,data表和 mimetype表。其中raw_contacts 表存放的是聯繫人id 信息,一個聯繫人就是表中的一行記錄。ontact_id在表單里應該是唯一的存在, 所以在插入的時候需要先查詢最後一條id是多少,然後在此基礎上加一。data 表中存放的是raw_contacts中的每一條id 對應的具體信息,一個聯繫人可能有電話、郵件、姓名等多條信息,每一條信息在該表中都是一行記錄。為了區分不同信息的類型,因此還有一個mimetypes 表,該表存儲的是常量數據,不同類型的信息由mimetype_id 來標識。現在很多App 都可以對系統聯繫人進行操作,這樣就可以直接將號碼添加到系統聯繫人中,可以關聯/備份/恢復系統聯繫人。讀取聯繫人信息的基本步驟是,首先查詢rawcontact表,獲取聯繫人的contactid,在rawcontact表中並不是每一個contact_id對應一條信息,而是一個contact_id對應多條信息,這樣可以存儲更多的信息。其次查詢根據contact_id查詢data表,獲取聯繫人的數據 data1、mimetype,前者存儲相關數據,後者存儲該數據對應得數據。最後根據mimetype類型確定數據類型。修改聯繫人的操作是往raw_contacts 表中插入一個id,值為n+1,作為一條新的記錄,然後往data1 表中插入具體的數據,其中id 必須為n+1。讀取和插入系統聯繫人資料庫需要添加如下許可權:
<uses-permission android:readPermission="android.permission.READ_CONTACTS"
<uses-permission android:writePermission="android.permission.WRITE_CONTACTS"
那麼怎麼才能獲取簡訊和聯繫人的Uri呢。這時就需要看源碼了。打開Android 系統源碼,其中TelephonyProvider 就是簡訊的內容提供者文件。打開TelephonyProvider 下的src 文件,查看java 文件,其中的SmsProvider.java 即簡訊息內容提供者邏輯代碼。通過查找系統源碼,可以確定簡訊息內容提供者的Uri 應該為:”content://sms”。用同樣的方法,可以查到聯繫人內容提供者的Uri路徑。打開Android 源碼,查看packages\providers\路徑下的文件,其中ContactsProvider 就是聯繫人的內容提供者。打開ContactsProvider2.java文件,查看此內容提供者的uri 路徑。根據源碼,確定內容提供者的Uri 信息為:操作raw_contacts 表的Uri:content://com.android.contacts/raw_contacts。操作data 表的Uri:content://com.android.contacts/data。其實,平常寫代碼時不必要這麼複雜,直接把Uri路徑拿來就可以用了。另外要註意的是,由於聯繫人資料庫使用了視圖,所以操作資料庫表時,看到的表欄位名稱和真實操作的有所不同。比如:data 表在查詢的時候沒有mimetype_id 欄位,取代的是mimetype 欄位。
再來講一講內容觀察者。內容提供者相當於一個監聽。觀察資料庫內容是否發生改變,如果改變,通知觀察者。內容觀察者ContentObserver,目的是觀察(捕捉)特定Uri 引起的資料庫的變化,繼而做一些相應的處理,它類似於資料庫技術中的觸發器(Trigger),當ContentObserver 所觀察的Uri 發生變化時,便會觸發它。觸發器分為表觸發器、行觸發器,相應的ContentObserver 也分為“表ContentObserver”、“行ContentObserver”,當然這是與它所監聽的Uri MIME Type 有關的。
內容觀察者的使用步驟:
1. 在內容提供者類增加通知方法
getContext().getContentResolver().notifyChange(uri, null);
2. 在觀察者類註冊觀察
//定義觀察的uri ,和內容提供者的Uri一直
Uri uri = Uri.parse("content://...");
//註冊觀察者
getContentResolver().registerContentObserver(uri, true, ContentObserver);
最後通過案例來演示今天的知識點。
案例一: 內容提供者操作簡訊
java代碼
/* * 添加許可權 * <uses-permission android:name="android.permission.READ_SMS"/> * <uses-permission android:name="android.permission.WRITE_SMS"/> */ import android.app.Activity; import android.content.ContentResolver; import android.content.ContentValues; import android.net.Uri; import android.os.Bundle; import android.view.View; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } /**利用記憶體提供者添加簡訊*/ public void add(View view) { Uri uri = Uri.parse("content://sms"); ContentResolver resolver = getContentResolver(); ContentValues values = new ContentValues(); values.put("address", "00000"); values.put("date", System.currentTimeMillis()); values.put("type", 1); // 表示收簡訊還是發簡訊 values.put("body", "看到簡訊表示利用內容提供者添加簡訊成功"); resolver.insert(uri, values); } /**利用記憶體提供者刪除簡訊*/ public void delete(View view) { Uri uri = Uri.parse("content://sms"); ContentResolver resolver = getContentResolver(); resolver.delete(uri, "address = ?", new String[] {"00000"}); } }
佈局文件中只定義了兩個按鈕
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="add" android:text="添加簡訊" /> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="delete" android:text="刪除簡訊" /> </LinearLayout>
效果展示:
添加短息:
刪除簡訊:
案例二: 內容提供者操作聯繫人
JAVA代碼,主程式:
import java.util.List; import android.app.Activity; import android.os.Bundle; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.ListView; import android.widget.TextView; import com.example.contact.domain.ContactInfo; import com.example.contact.utils.ContactUtils; //添加許可權<uses-permission android:name="android.permission.READ_CONTACTS"/> public class MainActivity extends Activity { private static final String TAG = "MainActivity"; private ListView listview; private List<ContactInfo> contactlist; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); listview = (ListView) findViewById(R.id.listview); contactlist = ContactUtils.getContact(this); Log.i(TAG, "info" + contactlist.size()); for (ContactInfo info : contactlist) { Log.i(TAG, "info" + info.toString()); } listview.setAdapter(new ContactAdapter()); } public class ContactAdapter extends BaseAdapter { private ContactInfo contactInfo; @Override public int getCount() { return contactlist.size(); } @Override public Object getItem(int position) { return null; } @Override public long getItemId(int position) { return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { viewHolder holder; if (convertView == null) { holder = new viewHolder(); convertView = View.inflate(MainActivity.this, R.layout.item_contact, null); holder.tv_text = (TextView) convertView .findViewById(R.id.tv_text); convertView.setTag(holder); } else { holder = (viewHolder) convertView.getTag(); } contactInfo = contactlist.get(position); holder.tv_text.setText(contactInfo.toString()); return convertView; } } public class viewHolder { TextView tv_text; } }
聯繫人工具類:
public class ContactInfo { private String name; private String phone; private String email; private String qq; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getQq() { return qq; } public void setQq(String qq) { this.qq = qq; } @Override public String toString() { return "ContactInfo [name=" + name + ", phone=" + phone + ", email=" + email + ", qq=" + qq + "]"; } }
獲取系統聯繫人:
import java.util.ArrayList; import java.util.List; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.net.Uri; import com.example.contact.domain.ContactInfo; public class ContactUtils { public static List<ContactInfo> getContact(Context context) { List<ContactInfo> list = new ArrayList<ContactInfo>(); ContentResolver resolver = context.getContentResolver(); Uri uri = Uri.parse("content://com.android.contacts/raw_contacts"); Uri datauri = Uri.parse("content://com.android.contacts/data"); Cursor cursor = resolver.query(uri, new String[] { "contact_id" }, null, null, null); while (cursor.moveToNext()) { String id = cursor.getString(0); System.out.println("00"+id); if (id != null) { ContactInfo info = new ContactInfo(); Cursor datacursor = resolver.query(datauri, new String[] { "data1", "mimetype" }, "raw_contact_id = ?", new String[] { id }, null); while (datacursor.moveToNext()) { String data1 = datacursor.getString(0); String mimetype = datacursor.getString(1); if ("vnd.android.cursor.item/name".equals(mimetype)) { info.setName(data1); } else if ("vnd.android.cursor.item/im".equals(mimetype)) { info.setQq(data1); } else if ("vnd.android.cursor.item/email_v2" .equals(mimetype)) { info.setEmail(data1); } else if ("vnd.android.cursor.item/phone_v2" .equals(mimetype)) { info.setPhone(data1); } } datacursor.close(); list.add(info); } } cursor.close(); return list; } }
佈局文件:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity" > <ListView android:id="@+id/listview" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>
成果展示:
案例三: 內容提供者修改資料庫。通過上述知識我們知道當需要對另外一個應用程式的資料庫操作的時候可以用到內容提供者,那到底內容提供者內部是如何工作的呢?在這裡給出一個案例來說明內容提供者的工作機制。首先,創建一個工程,在這個工程中新建一個資料庫,單獨寫一個類繼承ContentProvider,我們稱之為後門程式,重寫其中一系列的方法。其中包括對數據增刪改查。但此處的資料庫操作方法是空實現,當我們在另一工程中為內容解析者指定同一Uri路徑時。調用內容解析者的增刪改查方法時,會自動對該數據哭庫操作。同時,還可以在後門程式中設置內容監聽者,這樣可以隨時觀察到數據的變化。
內容提供者的主程式,不用操作任何邏輯:
import android.os.Bundle; import android.app.Activity; import android.view.Menu; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }
內容提供者中創建資料庫:
import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.database.sqlite.SQLiteOpenHelper; public class MyDBOpenhelper extends SQLiteOpenHelper { public MyDBOpenhelper(Context context) { super(context, "test.db", null, 1); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL("create table account (_id integer primary key autoincrement,name varchar(20),number varchar(20))"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { } }
寫一個類ContentProvider:
import android.content.ContentProvider; import android.content.ContentValues; import android.content.UriMatcher; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; public class BackDoor extends ContentProvider { private static final int SUCCESS = 1; /** 判斷Uri規則 */ static UriMatcher mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); static { mUriMatcher.addURI("com.exmple.text", "account", SUCCESS); //uri規則可自己定義,但一定和清單文件一直 } @Override public boolean onCreate() { return false; } /** 增刪改查為空實現 */ @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { int code = mUriMatcher.match(uri); // 判斷Uri是否合法 if (code == SUCCESS) { System.out.println("查詢數據"); MyDBOpenhelper helper = new MyDBOpenhelper(getContext()); SQLiteDatabase db = helper.getReadableDatabase(); return db.query("account", projection, selection, selectionArgs, null, null, sortOrder); } else { throw new IllegalArgumentException("路徑不正確"); } } @Override public String getType(Uri uri) { return null; } @Override public Uri insert(Uri uri, ContentValues values) { int code = mUriMatcher.match(uri); if (code == SUCCESS) { System.out.println("添加數據"); MyDBOpenhelper helper = new MyDBOpenhelper(getContext()); SQLiteDatabase db = helper.getWritableDatabase(); db.insert("account", null, values); getContext().getContentResolver().notifyChange(uri, null); // 內容觀察者檢測資料庫是否更改 } else { throw new IllegalArgumentException("路徑不正確"); } return null; } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { int code = mUriMatcher.match(uri); if (code == SUCCESS) { System.out.println("刪除數據"); MyDBOpenhelper helper = new MyDBOpenhelper(getContext()); SQLiteDatabase db = helper.getWritableDatabase(); db.delete("account", selection, selectionArgs); getContext().getContentResolver().notifyChange(uri, null); // 內容觀察者檢測資料庫是否更改 } else { throw new IllegalArgumentException("路徑不正確"); } return 0; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { int code = mUriMatcher.match(uri); if (code == SUCCESS) { System.out.println("更新數據"); MyDBOpenhelper helper = new MyDBOpenhelper(getContext()); SQLiteDatabase db = helper.getWritableDatabase(); db.update("account", values, selection, selectionArgs); getContext().getContentResolver().notifyChange(uri, null); // 內容觀察者檢測資料庫是否更改 } else { throw new IllegalArgumentException("路徑不正確"); } return 0; } }
配置文件中添加provider節點:
<!-- 註冊內容提供者數據 --> <provider android:name="com.example.provider.BackDoor" android:authorities="com.exmple.text" > </provider>
調用者的主程式:
import android.app.Activity; import android.content.ContentResolver; import android.content.ContentValues; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.view.View; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } /** * 利用後門程式 添加一條數據 */ public void insert(View view) { ContentResolver resolver = getContentResolver(); Uri uri = Uri.parse("content://com.exmple.text/account"); ContentValues values = new ContentValues(); values.put("name", "zhangsan"); values.put("number", 10000); resolver.insert(uri, values); } /** * 利用後門程式 刪除一條數據 */ public void delete(View view) { ContentResolver resolver = getContentResolver(); Uri uri = Uri.parse("content://com.exmple.text/account"); resolver.delete(uri, "name=?", new String[] { "zhangsan" }); } /** * 利用後門程式 修改數據 */ public void update(View view) { ContentResolver resolver = getContentResolver(); Uri uri = Uri.parse("content://com.exmple.text/account"); ContentValues values = new ContentValues(); values.put("number", 20000); resolver.update(uri, values, "name=?", new String[] { "zhangsan" }); } /** * 利用後門程式 查詢數據 */ public void query(View view) { ContentResolver resolver = getContentResolver(); Uri uri = Uri.parse("content://com.exmple.text/account"); Cursor cursor = resolver.query(uri, new String[] { "name", "number" }, null, null, null); while (cursor.moveToNext()) { String name = cursor.getString(0); float number = cursor.getFloat(1); System.out.println("name:" + name + "----" + "number:" + number); } cursor.close(); } }
調用者的佈局文件:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context=".MainActivity" > <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="insert" android:text="增" /> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="delete" android:text="刪" /> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="update" android:text="改" /> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="query" android:text="查" /> </LinearLayout>
運行結果: