編輯:關於android開發
Android 自定義ContentProvider ContentProvider 介紹 ContentProvider 優點 Android 自帶的ContentProvider ContentProvider Uri 結構 Google Uri 工具類 UriMatcher 解析 自定義 ContentProvider
有一種情景:假如A應用存儲的數據,B也用到。那麼,要怎麼共享數據呢?
其實有很多種解決辦法,比如存在SD卡上、網絡等等。
其實Android上的相冊、聯系人也都是Google給Android系統寫的App。也提供了對應的一套API給我們調用到其相應的數據,這就是ContentProvider。
ContentProvider是獲取數據和存儲都用的是統一的封裝接口,涉及到Uri的概念。
當然了,如果用到Uri了,我們可以使用Google提供的工具類 UriMatcher 和 ContentUris ,以下會講到這些。
如果想獲取一些系統自帶App的數據,可以參考:
android.provider
包下的ContentProvider。
content://com.camnter.content.provider/message/6
幾個例子:
content://com.camnter.content.provider/message
:表示操作message表的所有記錄。
content://com.camnter.content.provider/message/6
:表示操作message表的Id = 6的記錄。
content://com.camnter.content.provider/message/6/content
:表示操作message表的Id = 6的記錄的content字段。
UriMatcher :
public void addURI(String authority, String path, int code)
:添加Uri,Uri會被添加到一個ArrayList裡,提供match(Uri uri)
去匹配。 public int match(Uri uri)
:根據Uri匹配改Uri是否被添加。
ContentUris:
public static long parseId(Uri contentUri)
:解析Uri取得Id。 public static Uri.Builder appendId(Uri.Builder builder, long id)
:添加Id。 public static Uri withAppendedId(Uri contentUri, long id)
:添加Id。
ContentUris的源碼特別簡單,整個類就調用了Uri類的兩個方法 T T# 。
所以就看看UriMatcher源碼吧。
由於對其他類的依賴不多,我就自定義了一份UriMatcher。
import android.net.Uri;
import java.util.ArrayList;
import java.util.List;
/**
Utility class to aid in matching URIs in content providers.
To use this class, build up a tree of
UriMatcher
objects. For example:
private static final int PEOPLE = 1;
private static final int PEOPLE_ID = 2;
private static final int PEOPLE_PHONES = 3;
private static final int PEOPLE_PHONES_ID = 4;
private static final int PEOPLE_CONTACTMETHODS = 7;
private static final int PEOPLE_CONTACTMETHODS_ID = 8;
private static final int DELETED_PEOPLE = 20;
private static final int PHONES = 9;
private static final int PHONES_ID = 10;
private static final int PHONES_FILTER = 14;
private static final int CONTACTMETHODS = 18;
private static final int CONTACTMETHODS_ID = 19;
private static final int CALLS = 11;
private static final int CALLS_ID = 12;
private static final int CALLS_FILTER = 15;
private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static
{
sURIMatcher.addURI("contacts", "people", PEOPLE);
sURIMatcher.addURI("contacts", "people/#", PEOPLE_ID);
sURIMatcher.addURI("contacts", "people/#/phones", PEOPLE_PHONES);
sURIMatcher.addURI("contacts", "people/#/phones/#", PEOPLE_PHONES_ID);
sURIMatcher.addURI("contacts", "people/#/contact_methods", PEOPLE_CONTACTMETHODS);
sURIMatcher.addURI("contacts", "people/#/contact_methods/#", PEOPLE_CONTACTMETHODS_ID);
sURIMatcher.addURI("contacts", "deleted_people", DELETED_PEOPLE);
sURIMatcher.addURI("contacts", "phones", PHONES);
sURIMatcher.addURI("contacts", "phones/filter/*", PHONES_FILTER);
sURIMatcher.addURI("contacts", "phones/#", PHONES_ID);
sURIMatcher.addURI("contacts", "contact_methods", CONTACTMETHODS);
sURIMatcher.addURI("contacts", "contact_methods/#", CONTACTMETHODS_ID);
sURIMatcher.addURI("call_log", "calls", CALLS);
sURIMatcher.addURI("call_log", "calls/filter/*", CALLS_FILTER);
sURIMatcher.addURI("call_log", "calls/#", CALLS_ID);
}
Starting from API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, paths can start with a leading slash. For example:
sURIMatcher.addURI("contacts", "/people", PEOPLE);
Then when you need to match against a URI, call {@link #match}, providing the URL that you have been given. You can use the result to build a query, return a type, insert or delete a row, or whatever you need, without duplicating all of the if-else logic that you would otherwise need. For example:
public String getType(Uri url)
{
int match = sURIMatcher.match(url);
switch (match)
{
case PEOPLE:
return "vnd.android.cursor.dir/person";
case PEOPLE_ID:
return "vnd.android.cursor.item/person";
... snip ...
return "vnd.android.cursor.dir/snail-mail";
case PEOPLE_ADDRESS_ID:
return "vnd.android.cursor.item/snail-mail";
default:
return null;
}
}
instead of:
public String getType(Uri url)
{
List pathSegments = url.getPathSegments();
if (pathSegments.size() >= 2) {
if ("people".equals(pathSegments.get(1))) {
if (pathSegments.size() == 2) {
return "vnd.android.cursor.dir/person";
} else if (pathSegments.size() == 3) {
return "vnd.android.cursor.item/person";
... snip ...
return "vnd.android.cursor.dir/snail-mail";
} else if (pathSegments.size() == 3) {
return "vnd.android.cursor.item/snail-mail";
}
}
}
return null;
}
*/ public class UriMatcher { public static final int NO_MATCH = -1; /** * Creates the root node of the URI tree. * 創建Uri樹的根節點 * * @param code the code to match for the root URI */ public UriMatcher(int code) { mCode = code; mWhich = -1; mChildren = new ArrayList(); mText = null; } private UriMatcher() { mCode = NO_MATCH; mWhich = -1; mChildren = new ArrayList(); mText = null; } /** * Add a URI to match, and the code to return when this URI is * matched. URI nodes may be exact match string, the token "*" * that matches any text, or the token "#" that matches only * numbers. * 添加一個URI匹配,並且這裡添加code值會在這個URI匹配成功的時候返回。URI節點可能精確匹配字符串, * token為“*”的時候會匹配為text,token為“#”的時候只會匹配為數字 *
* Starting from API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, * this method will accept a leading slash in the path. * * @param authority the authority to match * ContentProvider的authority * * @param path the path to match. * may be used as a wild card for * any text, and # may be used as a wild card for numbers. * 匹配路徑,*可能是作為一個通配符用於任何文本,和#可能用作通配符數字。 * * @param code the code that is returned when a URI is matched * against the given components. Must be positive. * 該URI匹配成功時返回的code */ public void addURI(String authority, String path, int code) { if (code < 0) { throw new IllegalArgumentException("code " + code + " is invalid: it must be positive"); } String[] tokens = null; if (path != null) { String newPath = path; // Strip leading slash if present. if (path.length() > 0 && path.charAt(0) == '/') { newPath = path.substring(1); } tokens = newPath.split("/"); } int numTokens = tokens != null ? tokens.length : 0; UriMatcher node = this; for (int i = -1; i < numTokens; i++) { String token = i < 0 ? authority : tokens[i]; ArrayList
以上注釋還有使用,Google告訴我們怎麼使用UriMatcher的范例代碼。
代碼都重復看過,都寫上了注釋。
BaseContentProvider
public abstract class BaseContentProvider extends ContentProvider {
// 單一數據的MIME類型字符串應該以vnd.android.cursor.item/開頭
protected static final String MIME_SINGLE = "vnd.android.cursor.item/";
// 數據集的MIME類型字符串則應該以vnd.android.cursor.dir/開頭
protected static final String MIME_MULTIPLE = "vnd.android.cursor.dir/";
/**
* Implement this to initialize your content provider on startup.
* This method is called for all registered content providers on the
* application main thread at application launch time. It must not perform
* lengthy operations, or application startup will be delayed.
*
*
You should defer nontrivial initialization (such as opening, * upgrading, and scanning databases) until the content provider is used * (via {@link #query}, {@link #insert}, etc). Deferred initialization * keeps application startup fast, avoids unnecessary work if the provider * turns out not to be needed, and stops database errors (such as a full * disk) from halting application launch. *
*
If you use SQLite, {@link SQLiteOpenHelper} * is a helpful utility class that makes it easy to manage databases, * and will automatically defer opening until first use. If you do use * SQLiteOpenHelper, make sure to avoid calling * {@link SQLiteOpenHelper#getReadableDatabase} or * {@link SQLiteOpenHelper#getWritableDatabase} * from this method. (Instead, override * {@link SQLiteOpenHelper#onOpen} to initialize the * database when it is first opened.) * * @return true if the provider was successfully loaded, false otherwise */ @Override public abstract boolean onCreate(); /** * Implement this to handle query requests from clients. * This method can be called from multiple threads, as described in * Processes * and Threads. *
* Example client call:
*
// Request a specific record.
* Cursor managedCursor = managedQuery(
* ContentUris.withAppendedId(Contacts.People.CONTENT_URI, 2),
* projection, // Which columns to return.
* null, // WHERE clause.
* null, // WHERE clause value substitution
* People.NAME + " ASC"); // Sort order.
* Example implementation:
*
// SQLiteQueryBuilder is a helper class that creates the
* // proper SQL syntax for us.
* SQLiteQueryBuilder qBuilder = new SQLiteQueryBuilder();
*
* // Set the table we're querying. * qBuilder.setTables(DATABASE_TABLE_NAME); *
* // If the query ends in a specific record number, we're * // being asked for a specific record, so set the * // WHERE clause in our query. * if((URI_MATCHER.match(uri)) == SPECIFIC_MESSAGE){ * qBuilder.appendWhere("_id=" + uri.getPathLeafId()); * } *
* // Make the query. * Cursor c = qBuilder.query(mDb, * projection, * selection, * selectionArgs, * groupBy, * having, * sortOrder); * c.setNotificationUri(getContext().getContentResolver(), uri); * return c;
* * @param uri The URI to query. This will be the full URI sent by the client; * if the client is requesting a specific record, the URI will end in a record number * that the implementation should parse and add to a WHERE or HAVING clause, specifying * that _id value. * @param projection The list of columns to put into the cursor. If * {@code null} all columns are included. * @param selection A selection criteria to apply when filtering rows. * If {@code null} then all rows are included. * @param selectionArgs You may include ?s in selection, which will be replaced by * the values from selectionArgs, in order that they appear in the selection. * The values will be bound as Strings. * @param sortOrder How the rows in the cursor should be sorted. * If {@code null} then the provider is free to define the sort order. * @return a Cursor or {@code null}. */ @Override public abstract Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder); /** * Implement this to handle requests for the MIME type of the data at the * given URI. The returned MIME type should start with * vnd.android.cursor.item
for a single record, * or vnd.android.cursor.dir/
for multiple items. * This method can be called from multiple threads, as described in * Processes * and Threads. *
*
Note that there are no permissions needed for an application to * access this information; if your content provider requires read and/or * write permissions, or is not exported, all applications can still call * this method regardless of their access permissions. This allows them * to retrieve the MIME type for a URI when dispatching intents. * * @param uri the URI to query. * @return a MIME type string, or {@code null} if there is no type. */ @Override public abstract String getType(Uri uri); /** * Implement this to handle requests to insert a new row. * As a courtesy, call {@link ContentResolver#notifyChange(Uri, ContentObserver) notifyChange()} * after inserting. * This method can be called from multiple threads, as described in * Processes * and Threads. * * @param uri The content:// URI of the insertion request. This must not be {@code null}. * @param values A set of column_name/value pairs to add to the database. * This must not be {@code null}. * @return The URI for the newly inserted item. */ @Override public abstract Uri insert(Uri uri, ContentValues values); /** * Implement this to handle requests to delete one or more rows. * The implementation should apply the selection clause when performing * deletion, allowing the operation to affect multiple rows in a directory. * As a courtesy, call {@link ContentResolver#notifyChange(Uri, ContentObserver) notifyChange()} * after deleting. * This method can be called from multiple threads, as described in * Processes * and Threads. *
*
The implementation is responsible for parsing out a row ID at the end * of the URI, if a specific row is being deleted. That is, the client would * pass in
content://contacts/people/22
and the implementation is * responsible for parsing the record number (22) when creating a SQL statement. * * @param uri The full URI to query, including a row ID (if a specific record is requested). * @param selection An optional restriction to apply to rows when deleting. * @param selectionArgs * @return The number of rows affected. * @throws SQLException */ @Override public abstract int delete(Uri uri, String selection, String[] selectionArgs); /** * Implement this to handle requests to update one or more rows. * The implementation should update all rows matching the selection * to set the columns according to the provided values map. * As a courtesy, call {@link ContentResolver#notifyChange(Uri, ContentObserver) notifyChange()} * after updating. * This method can be called from multiple threads, as described in * Processes * and Threads. * * @param uri The URI to query. This can potentially have a record ID if this * is an update request for a specific record. * @param values A set of column_name/value pairs to update in the database. * This must not be {@code null}. * @param selection An optional filter to match rows to update. * @param selectionArgs * @return the number of rows affected. */ @Override public abstract int update(Uri uri, ContentValues values, String selection, String[] selectionArgs); }
MessageSQLiteHelper
public class MessageSQLiteHelper extends SQLiteOpenHelper {
private static final String TAG = "MessageSQLiteHelper";
private static final String DB_NAME = "message.db";
private static final int VERSION = 1;
public static final String TB_MESSAGE = "tb_message";
public static final String TB_MESSAGE_SQL = "CREATE TABLE IF NOT EXISTS " + TB_MESSAGE + "(_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," +
" content text)";
private static MessageSQLiteHelper ourInstance;
public MessageSQLiteHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
}
public static MessageSQLiteHelper getInstance(Context context) {
if (ourInstance == null) ourInstance = new MessageSQLiteHelper(context);
return ourInstance;
}
public MessageSQLiteHelper(Context context) {
this(context, DB_NAME, null, VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(TB_MESSAGE_SQL);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.i(TAG, "onUpgrade");
}
}
MessageContentProvider
public class MessageContentProvider extends BaseContentProvider {
// 主機名
private static final String AUTHORITY = "com.camnter.content.provider";
// Message uri
public static final Uri MESSAGE_URI = Uri.parse("content://" + AUTHORITY + "/message");
// 數據集的MIME類型字符串則應該以vnd.android.cursor.dir/開頭
private static final String TOPIC_SINGLE = MIME_SINGLE + "message";
// 單一數據的MIME類型字符串應該以vnd.android.cursor.item/開頭
private static final String TOPIC_MULTIPLE = MIME_MULTIPLE + "message";
// 有id匹配碼
private static final int MESSAGE = 6;
// 有無匹配碼
public static final int MESSAGES = 7;
// Message SQLite helper
private MessageSQLiteHelper messageSQLiteHelper;
private static final UriMatcher messageUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
// content://com.camnter.content.provider/message
messageUriMatcher.addURI(AUTHORITY, "message", MESSAGES);
// content://com.camnter.content.provider/message/#
messageUriMatcher.addURI(AUTHORITY, "message/#", MESSAGE);
}
@Override
public boolean onCreate() {
this.messageSQLiteHelper = MessageSQLiteHelper.getInstance(this.getContext());
return true;
}
@Override
public String getType(Uri uri) {
int match = messageUriMatcher.match(uri);
switch (match) {
case MESSAGE:
return TOPIC_SINGLE;
case MESSAGES:
return TOPIC_MULTIPLE;
default:
return null;
}
}
@Override
public Uri insert(Uri uri, ContentValues values) {
SQLiteDatabase db = this.messageSQLiteHelper.getWritableDatabase();
long id;
switch (messageUriMatcher.match(uri)) {
case MESSAGE:
id = db.insert(MessageSQLiteHelper.TB_MESSAGE, "content", values);
this.getContext().getContentResolver().notifyChange(uri, null);
return ContentUris.withAppendedId(uri, id);
case MESSAGES:
id = db.insert(MessageSQLiteHelper.TB_MESSAGE, "content", values);
String path = uri.toString();
this.getContext().getContentResolver().notifyChange(uri, null);
// 新id的Uri替換舊id的Uri
return Uri.parse(path.substring(0, path.lastIndexOf("/")) +"/"+ id);
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
SQLiteDatabase db = this.messageSQLiteHelper.getWritableDatabase();
int count;
switch (messageUriMatcher.match(uri)) {
case MESSAGE:
count = db.delete(MessageSQLiteHelper.TB_MESSAGE, selection, selectionArgs);
this.getContext().getContentResolver().notifyChange(uri, null);
break;
case MESSAGES:
long messageId = ContentUris.parseId(uri);
// 指定id
String where = "_id=" + messageId;
// 把其它條件附加上
where += !TextUtils.isEmpty(selection) ? " and (" + selection + ")" : "";
count = db.delete(MessageSQLiteHelper.TB_MESSAGE, where, selectionArgs);
this.getContext().getContentResolver().notifyChange(uri, null);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
return count;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
SQLiteDatabase db = this.messageSQLiteHelper.getWritableDatabase();
int count;
switch (messageUriMatcher.match(uri)) {
case MESSAGE:
long messageId = ContentUris.parseId(uri);
// 指定id
String where = "_id=" + messageId;
// 把其它條件附加上
where += !TextUtils.isEmpty(selection) ? " and (" + selection + ")" : "";
count = db.update(MessageSQLiteHelper.TB_MESSAGE, values, where, selectionArgs);
this.getContext().getContentResolver().notifyChange(uri, null);
break;
case MESSAGES:
count = db.update(MessageSQLiteHelper.TB_MESSAGE, values, selection, selectionArgs);
this.getContext().getContentResolver().notifyChange(uri, null);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
return count;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
SQLiteDatabase db = this.messageSQLiteHelper.getReadableDatabase();
Cursor cursor;
switch (messageUriMatcher.match(uri)) {
case MESSAGE:
long messageId = ContentUris.parseId(uri);
// 指定id
String where = "_id=" + messageId;
// 把其它條件附加上
where += !TextUtils.isEmpty(selection) ? " and (" + selection + ")" : "";
cursor = db.query(MessageSQLiteHelper.TB_MESSAGE, projection, where, selectionArgs, null, null, sortOrder);
this.getContext().getContentResolver().notifyChange(uri, null);
return cursor;
case MESSAGES:
cursor = db.query(MessageSQLiteHelper.TB_MESSAGE, projection, selection, selectionArgs, null, null, sortOrder);
this.getContext().getContentResolver().notifyChange(uri, null);
return cursor;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
}
}
CustomContentProviderActivity
public class CustomContentProviderActivity extends AppCompatActivity {
private RecyclerView providerRV;
private ProviderRecyclerViewAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(R.layout.activity_custom_content_provider);
this.getContentResolver().registerContentObserver(MessageContentProvider.MESSAGE_URI, true, new MessageProviderObserver(new Handler()));
this.providerRV = (RecyclerView) this.findViewById(R.id.provider_rv);
this.adapter = new ProviderRecyclerViewAdapter(this.getContentResolver(), MessageContentProvider.MESSAGE_URI);
this.providerRV.setAdapter(this.adapter);
this.initRecyclerView();
}
private void initRecyclerView() {
// 實例化LinearLayoutManager
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
// 設置垂直布局
linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
// 設置布局管理器
this.providerRV.setLayoutManager(linearLayoutManager);
this.providerRV.setItemAnimator(new DefaultItemAnimator());
this.providerRV.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST));
// 使RecyclerView保持固定的大小,該信息被用於自身的優化
this.providerRV.setHasFixedSize(true);
ArrayList allData = new ArrayList<>();
allData.add(new SQLiteData());
this.adapter.setList(allData);
this.adapter.notifyDataSetChanged();
}
private class MessageProviderObserver extends ContentObserver {
/**
* Creates a content observer.
*
* @param handler The handler to run {@link #onChange} on, or null if none.
*/
public MessageProviderObserver(Handler handler) {
super(handler);
}
@Override
public void onChange(boolean selfChange, Uri uri) {
super.onChange(selfChange, uri);
}
}
/**
* Provider RecyclerView Adapter
*/
private class ProviderRecyclerViewAdapter extends EasyRecyclerViewAdapter implements View.OnClickListener {
private static final int ITEM_PROVIDER_OPERATION = 0;
private static final int ITEM_PROVIDER_DATA = 1;
private ContentResolver resolver;
private Uri uri;
public ProviderRecyclerViewAdapter(ContentResolver resolver, Uri uri) {
this.resolver = resolver;
this.uri = uri;
}
@Override
public int[] getItemLayouts() {
return new int[]{R.layout.item_content_provider_operation, R.layout.item_content_provider_data};
}
@Override
public void onBindRecycleViewHolder(EasyRecyclerViewHolder easyRecyclerViewHolder, int position) {
int itemType = this.getRecycleViewItemType(position);
switch (itemType) {
case ITEM_PROVIDER_OPERATION:
easyRecyclerViewHolder.findViewById(R.id.provider_add_bt).setOnClickListener(this);
easyRecyclerViewHolder.findViewById(R.id.provider_del_bt).setOnClickListener(this);
easyRecyclerViewHolder.findViewById(R.id.provider_mod_bt).setOnClickListener(this);
easyRecyclerViewHolder.findViewById(R.id.provider_query_bt).setOnClickListener(this);
break;
case ITEM_PROVIDER_DATA:
ProviderData data = (ProviderData) this.getList().get(position);
TextView idTV = easyRecyclerViewHolder.findViewById(R.id.provider_id_tv);
TextView contentTV = easyRecyclerViewHolder.findViewById(R.id.provider_content_tv);
idTV.setText(data.id + "");
contentTV.setText(data.content + "");
break;
}
}
@Override
public int getRecycleViewItemType(int i) {
if (i == 0) {
return ITEM_PROVIDER_OPERATION;
} else {
return ITEM_PROVIDER_DATA;
}
}
/**
* Called when a view has been clicked.
*
* @param v The view that was clicked.
*/
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.provider_add_bt: {
ContentValues values = new ContentValues();
values.put("content", "Save you from anything");
this.resolver.insert(this.uri, values);
this.refresh();
break;
}
case R.id.provider_del_bt: {
this.resolver.delete(this.uri, null, null);
this.refresh();
break;
}
case R.id.provider_mod_bt: {
List allData = this.queryAll();
int firstId = allData.get(0).id;
ContentValues values = new ContentValues();
values.put("content", UUID.randomUUID().toString());
String path = this.uri.toString();
this.resolver.update(Uri.parse(path.substring(0, path.lastIndexOf("/")) +"/message/"+ firstId), values, null, null);
this.refresh();
break;
}
case R.id.provider_query_bt: {
this.refresh();
break;
}
}
}
private void refresh() {
List l = this.queryAll();
l.add(0,new ProviderData());
this.setList(l);
this.notifyDataSetChanged();
}
private List queryAll() {
List allData = new ArrayList<>();
Cursor result = this.resolver.query(this.uri, null, null, null, null);
for (result.moveToFirst(); !result.isAfterLast(); result.moveToNext()) {
ProviderData data = new ProviderData();
data.id = result.getInt(result.getColumnIndex("_id"));
data.content = result.getString(result.getColumnIndex("content"));
allData.add(data);
}
result.close();
return allData;
}
}
}
版權聲明:本文為博主原創文章,未經博主允許不得轉載。
Android中Canvas繪圖之Shader使用圖文詳解 概述 我們在用Android中的Canvas繪制各種圖形時,可以通過Paint.setShader(shad
使用Vitamio打造自己的Android萬能播放器—— 手勢控制亮度、音量、縮放 使用Vitamio打造自己的Android萬能播放器(1)——
Android junit單元測試,androidjunit軟件測試的分類* 黑盒測試 * 測試邏輯業務* 白盒測試 * 測試邏輯方法 根據測試粒度 * 方法測試:fun
Android熱補丁技術—dexposed原理簡析(手機淘寶采用方案) 本文由嵌入式企鵝圈原創團隊成員、阿裡資深工程師Hao分享。 上篇文章《Android無線開發的幾種