星期六, 8月 30, 2014

Creating a Content Provider (一)

Before You Start Building

在開始建立provider之前,有以下幾件事要做。

  1. 決定是否需要一個content provider。如果你需要提供一個或多個以下的議題,你就需要建立一個content provider。
    • 想提供複雜的資料或檔案給其他應用程式。
    • 想允許從你的應用程式複製複雜資料到其他應用程式。
    • 想利用搜尋框架提供客製化搜尋。
  2. 如果你還沒準備好,請先看Content Provider Basics。
接下來,跟隨以下這些步驟建立provider。
  1. 對於你的資料設計raw storage。Content provider有兩種方式提供資料。
    1. File data
      資料通常進入檔案,如照片、視訊、音樂,儲存這些檔案在應用程式私有的空間。回應其它應用程式對於檔案的請求,你的provider可以提供檔案的處理。
    2. "Structured" data
      資料通常進入資料庫、陣列或類似的結構。資料的儲存格式是行和列的資料表,行表示值組,如一個人或一個項目,列表示對於值組的一些資料,如人的姓名或項目的價錢。一般儲存此型態的資料的方式是在SQLite資料庫,但是你也可以使用任何型態的永久性儲存,詳細說明在Designing Data Storage。
  2. 定義實際的ContentProvider類別和它需要的方法的實作。該類別是你的資料和Android系統間的介面。
  3. 定義provider的authority字串、content URI、列名稱。如果你想要provider的應用程式能處理intent,也要定義intent action、extras data、flags。也要定義想存取你的資料的應用程式將請求的權限。你應該考慮定義這些所有數值為常數,放在獨立的contract類別,往後,你可以顯示該類別給其他開發者。
  4. 加入其他可選部分。例如樣品資料或是AbstractThreadedSyncAdapter的實作,AbstractThreadedSyncAdapter實作可以在provider與雲端資料之間同步資料。


Designing Data Storage

Content provider是資料儲存結構化模式的介面。建立介面之前,必須決定如何儲存資料。可以儲存成任何你喜歡的模式,然後再設計讀取和寫入資料所必要的介面。

以下是在Android下可用的資料儲存技術
  • Android系統包含了SQLite資料庫API。SQLiteOpenHelper類別幫助你建立資料庫,且SQLiteDatabase類別是存取資料的基本類別。
  • 對於儲存成檔案的資料,Android提供多種檔案導向的API。如果你正在設計提供多媒體相關資料如音樂視訊的provider,你可以結合資料表和檔案。
  • 對於以網路為基礎的資料的運作,使用java.net及android.net中的類別。你也可以同步network-based資料到本地資料儲存如資料庫,然後提供資料表或檔案,

Data design considerations

以下有一些設計provider的資料結構的訣竅
  • 資料表資料應該始終有一個"主鍵"欄位,用來讓provider維護每行。也可以使用此欄位row與其他資料表的相關的row連結(如使用外鍵一樣)。雖然主鍵可以用任何名字命名,使用BaseColumns._ID還是最佳的選擇,因為連結查詢結果到ListView需要有欄位名稱為_ID。
  • 如果想要提供Bitmap圖片或其他非常大的檔案導向資料,應將資料儲存在檔案然後鍵接的提供它,而不是直接地儲存在資料表中。如果如此做的話,應該告訴你的provider的使用者,他們需使用ContentResolver檔案方法來存取資料。
  • 使用Binary Large OBject(BLOB)資料型態,它有多種大小和不同的結構。例如,可以使用BLOB欄位儲存protocol buffer或JSON Structure。你也可以使用BLOB實現綱目獨立(schema independent)的資料表。在這種資料表中,你定義了一個關鍵欄位、一個MIME型態欄位及一個或多個通用欄位做為BLOB,意味著,在BLOB欄位中的資料是由在MIME型態的欄位的值表示的。Contacts Provider的"data"ContactsProvider.Data是schema-independent資料表的例子。


Designing Content URIs

content URI是一種URI用來辨識provider中的資料,Content URI包含整個provider的名稱(它的authority),只到的檔案或資料表的名稱(path),可選的id部分指出在資料表中個別row。ContentProvider的每一個資料存取的方法都有一個content URI作為參數,此允許你決定存取table row或檔案。


Designing an authority

一個provider通常有一個個別的authority,它作為Android內部的名稱。避免與其他provider衝突,你應該使用網路域名所有權作為你的authority的基礎,因為這與Android package名稱的建議一樣,你可以將你的provider authority定義為包含provider的package的名稱的延伸。例如,你的Android package名稱為com.example.<appname>,可以給你的provider authority為com.example.<appname>.provider。


Designing a path structure

開發人員通常藉由從authority附加路徑建立content URI,以其指出個別的資料表。例如,假如你有兩個資料表table1table2,由先前的例子結合authority產生content URIs為com.example.<appnmae>.provider/table1及con.example.<appname>.provider.table2。


Handling content URI IDs

按照慣例,provider藉由在content URI末端附加上每個row的ID值,提供存取資料表中的個別row。provider的ID值與資料表的_ID欄位相配,並執行被請求的存取。

為應用程式存取provider,此慣例有助於一般設計樣式。應用程式針對provider查詢,並利用CursorAdapter將查詢結果Cursor顯示在ListView,CursorAdapter的定義是在Cursor中需要有一個欄位為_ID。

接著使用者從使用者介面中挑選被顯示的row,查看或修改資料。應用程式從支持ListView的Cursor取得相對應的row,取得此row的_ID值,附加到content URI並送出存取provider的請求。provider接著查詢或修改使用者挑出來的row。


Content URI patterns

幫助你針對傳入的content URI選擇採取動作,provider API包含了便利的類別UriMatcher,UriMatcher可以將content Uri樣式成整數值。你可以在switch描述中使用整數值,採取預期的動作。

content URI pattern匹配content URIs

  • * :Matches a string of any valid characters of any length
  • # :Matches a string of numeric characters of any length
以設計及編碼content URI的處理作為例子,考慮有authority的provider-com.example.app.provider,用它來辨認以下的content URI以指出資料表。
  • content://com.example.app.provider/table1:名稱為table1的資料表。
  • content://com.example.app.provider/table2/dataset1:名稱為dataset1的資料表。
  • content://com.example.app.provider/table2/dataset2:名稱為dataset2的資料表。
  • content://com.example.app.provider/atble3:名稱為table3的資料表。
如果這些content URI有row id附加在它們上,provider也能辨認。例如,com.example.app.provider/table3/1被辨認為table3的id為1的row。

以下content URI patterns也是:

content://com.example.app.provider/*  -  匹配在provider中的任何content URI。
content://com.example.app.provider/table2/*  -  匹配在資料表dataset1及dataset2中的content URI,但不匹配在資料表table1及table3中的content URI。

content://com.example.app.provider/table3/#  -  匹配在資料表table3中的單一row的content URI,例如,content://com.example.app.provider/table3/6為由6確定row。

以下程式片段顯示在UriMatcher中的方法如何運作,這程式處理對於整個資料表的content URI不同於處理資料表中單一row的content URI。藉由使用content URI patttern針對資料表的content://<authority>/<path>以及針對單一row的content://<authority>/<path>/<id>。

addURI()方法是用來將authority和path對應到整數數值,match()方法回傳對應到一個Uri其整數數值。

public class ExampleProvider extends ContentProvider {
...
    // Creates a UriMatcher object.
    private static final UriMatcher sUriMatcher;
...
    /*
     * The calls to addURI() go here, for all of the content URI patterns that the provider
     * should recognize. For this snippet, only the calls for table 3 are shown.
     */
...
    /*
     * Sets the integer value for multiple rows in table 3 to 1. Notice that no wildcard is used
     * in the path
     */
    sUriMatcher.addURI("com.example.app.provider", "table3", 1);

    /*
     * Sets the code for a single row to 2. In this case, the "#" wildcard is
     * used. "content://com.example.app.provider/table3/3" matches, but
     * "content://com.example.app.provider/table3 doesn't.
     */
    sUriMatcher.addURI("com.example.app.provider", "table3/#", 2);
...
    // Implements ContentProvider.query()
    public Cursor query(
        Uri uri,
        String[] projection,
        String selection,
        String[] selectionArgs,
        String sortOrder) {
...
        /*
         * Choose the table to query and a sort order based on the code returned for the incoming
         * URI. Here, too, only the statements for table 3 are shown.
         */
        switch (sUriMatcher.match(uri)) {


            // If the incoming URI was for all of table3
            case 1:

                if (TextUtils.isEmpty(sortOrder)) sortOrder = "_ID ASC";
                break;

            // If the incoming URI was for a single row
            case 2:

                /*
                 * Because this URI was for a single row, the _ID value part is
                 * present. Get the last path segment from the URI; this is the _ID value.
                 * Then, append the value to the WHERE clause for the query
                 */
                selection = selection + "_ID = " uri.getLastPathSegment();
                break;

            default:
            ...
                // If the URI is not recognized, you should do some error handling here.
        }
        // call the code to actually do the query
    }
其他類別如contentUris,它提供了便利的方法,用來處理content URI的id部分。類別Uri及Uri.Builder,它提供便利方法,用來處理解析已存在的Uri物件及建立新的Uri。

星期四, 8月 28, 2014

Content Provider Basics (二)


Displaying query results

ContentResolver.query()客戶端方法始終回傳一個Cursor,cursor包含符合查詢條件的行和查詢中projection指定的欄位,Cursor物件提供隨機讀取它包含的行和欄位。使用Cursor方法,你可以遍歷在查詢結果中的各行、決定每個欄位的資料型態、從欄位中取值出來及檢查查詢結果的其他特性。一些Cursor在provider的資料改變時會自動地更新物件,或在Cursor改變時觸發一些方法,或兩者都會。

如果沒有行符合selection條件,provider回傳Cursor物件而Cursor.getCount()為0(一個空的Cursor)。

如果內部錯誤發生,查詢結果會根據特別的provider,它可能會選擇回傳null,或是丟出Exception。

因為一個Cursor是一個行列表,顯示Cursor內容的一個好方式是透過SimpleCursorAdapter將Cursor連結到ListView。

以下片段接續了上面的片段,建立SimpleCursorAdapter物件包含藉由查詢描述擷取出的Cursor,接著設定此物件為ListView的Adapter。

// Defines a list of columns to retrieve from the Cursor and load into an output row
String[] mWordListColumns =
{
    UserDictionary.Words.WORD,   // Contract class constant containing the word column name
    UserDictionary.Words.LOCALE  // Contract class constant containing the locale column name
};
// Defines a list of View IDs that will receive the Cursor columns for each row
int[] mWordListItems = { R.id.dictWord, R.id.locale};
// Creates a new SimpleCursorAdapter
mCursorAdapter = new SimpleCursorAdapter(
    getApplicationContext(),               // The application's Context object
    R.layout.wordlistrow,                  // A layout in XML for one row in the ListView
    mCursor,                               // The result from the query
    mWordListColumns,                      // A string array of column names in the cursor
    mWordListItems,                        // An integer array of view IDs in the row layout
    0);                                    // Flags (usually none are needed)
// Sets the adapter for the ListView
mWordList.setAdapter(mCursorAdapter);
注意 -回到有Cursor的ListView,cursor必須包含名稱為_ID的欄位。因為如此,查詢描述顯示擷取"words"資料表的_ID欄位,儘管如此,ListView不會顯示它。此限制也說明了為什麼大部分的provider對於每個資料表都有_ID欄位了。


Getting data from query results

比起簡單地顯示查詢結果,妳可能會使用它們完成其他任務。例如,你可以從user dictionary擷取拼法,接著在其他provider中尋找它們。要完成這項功能,你要遍歷Cursor中的所有行。

// Determine the column index of the column named "word"
int index = mCursor.getColumnIndex(UserDictionary.Words.WORD);
/*
 * Only executes if the cursor is valid. The User Dictionary Provider returns null if
 * an internal error occurs. Other providers may throw an Exception instead of returning null.
 */
if (mCursor != null) {
    /*
     * Moves to the next row in the cursor. Before the first movement in the cursor, the
     * "row pointer" is -1, and if you try to retrieve data at that position you will get an
     * exception.
     */
    while (mCursor.moveToNext()) {

        // Gets the value from the column.
        newWord = mCursor.getString(index);

        // Insert code here to process the retrieved word.

        ...

        // end of while loop
    }
} else {

    // Insert code here to report an error if the cursor is null or the provider threw an exception.
}
Cursor實現包含許多"get"方法擷取來自物件不同資料型態的資料。例如,前面的片段使用getString()。它們也有getType()方法,回傳欄位的資料型態。


Content Provider Permissions

一個provider的應用程式可以指定權限讓其他應用程式要存取provider資料必須有該權限,這些權限確保了使用者知道應用程式將會試著存取什麼樣的資料。根據provider的需求,其他應用程式為了存取provider資料,請求它們需要的權限。終端使用者在安裝應用程式時會看到請求的權限。

取得存取provider所需的權限,應用程式在它的manifest文件中用<uses-permission>標籤請求。

以下為請求讀取User Dictionary Provider權限

 <uses-permission android:name="android.permission.READ_USER_DICTIONARY">


Inserting, Updating, and Deleting Data

如同我們從provider擷取資料出來的方法,你也可以使用provider客戶端與provider的ContentProvider間的互動修改資料。你以參數呼叫ContentResolver方法,參數會被傳遞到相對應的ContentProvider方法。provider和provider客戶端會自動地處理安全性與處理程序間的溝通。


Inserting data

將資料插入provider,呼叫ContentResolver.insert()方法。此方法插入新的行到provider並回傳該行的content Uri。以下片段插入新的單字到User Dictionary Provider。

// Defines a new Uri object that receives the result of the insertion
Uri mNewUri;
...
// Defines an object to contain the new values to insert
ContentValues mNewValues = new ContentValues();
/*
 * Sets the values of each column and inserts the word. The arguments to the "put"
 * method are "column name" and "value"
 */
mNewValues.put(UserDictionary.Words.APP_ID, "example.user");
mNewValues.put(UserDictionary.Words.LOCALE, "en_US");
mNewValues.put(UserDictionary.Words.WORD, "insert");
mNewValues.put(UserDictionary.Words.FREQUENCY, "100");

mNewUri = getContentResolver().insert(
    UserDictionary.Word.CONTENT_URI,   // the user dictionary content URI
    mNewValues                          // the values to insert
);
新行的資料進入一個單一ContentValue物件,它格式有點類似包含一行的cursor。此物件中的欄位不需要有相同的資料型態,且如果不想要指定數值,你可以使用ContentValue.putNull()設定欄位為null。

不需要加入_ID欄位,因為此欄位是自動維護的。provider會分配給被加入的每一行一個_ID唯一值。provider通常使用此值作為資料表的主鍵。

回傳到mNewValues的content Uri用來辨識新加入的行,如以下格式

content://user_dictionary/words/<id_value>
<id_value>為新行的_ID值,大部分的provider能自動地偵測此格式的content Uri,然後執行在特定行上的操作。

從回傳的Uri中取得_ID值,呼叫ContentUris,parseId()。


Updating data

 更新一個row,你可以使用ContentValues物件與更新資料一起,就如同在插入資料時所做的一樣,selection條件也和在query時一樣。在客戶端使用的方法是ContentResolver.update(),只需要針對欲更新欄位,加數值到ContentValues物件。如果想要清除欄位的內容,把數值設為null。

以下片段將所有的row它的locale為"en"改變為null,回傳值為有改變的row個數。

// Defines an object to contain the updated values
ContentValues mUpdateValues = new ContentValues();
// Defines selection criteria for the rows you want to update
String mSelectionClause = UserDictionary.Words.LOCALE +  "LIKE ?";
String[] mSelectionArgs = {"en_%"};
// Defines a variable to contain the number of updated rows
int mRowsUpdated = 0;
...
/*
 * Sets the updated value and updates the selected words.
 */
mUpdateValues.putNull(UserDictionary.Words.LOCALE);

mRowsUpdated = getContentResolver().update(
    UserDictionary.Words.CONTENT_URI,   // the user dictionary content URI
    mUpdateValues                       // the columns to update
    mSelectionClause                    // the column to select on
    mSelectionArgs                      // the value to compare to
);
當呼叫ContentResolver.update()時,也應該將使用者輸入做消毒,就像在Protecting against malicious input。


Deleting data

刪除rows類似於擷取row資料,針對想刪除的row指定selection條件,且客戶端方法回傳被刪除的row的號碼。以下片段為刪除appid符合"user"的row,方法回傳被刪除的row的號碼。

// Defines selection criteria for the rows you want to delete
String mSelectionClause = UserDictionary.Words.APP_ID + " LIKE ?";
String[] mSelectionArgs = {"user"};
// Defines a variable to contain the number of rows deleted
int mRowsDeleted = 0;
...
// Deletes the words that match the selection criteria
mRowsDeleted = getContentResolver().delete(
    UserDictionary.Words.CONTENT_URI,   // the user dictionary content URI
    mSelectionClause                    // the column to select on
    mSelectionArgs                      // the value to compare to
);
當呼叫ContentResolver.delete()時,也應該將使用者輸入做消毒,就像在Protecting against malicious input。


Provider Data Types

Content providers提供許多不同的資料型態。User Dictionary Provider只有文字型態,但provider可以提供以下型態

  • integer
  • long integer (long)
  • floating point
  • long floating point (double)
provider常使用的它資料型態是Binary Large OBject (BLOB)被實作為64KB的byte array。可用的資料型態可到Cursor類別看看"get"方法。

一個provider中,每個欄位的資料型態通常被列在它的文件中。

provider也為每個content URI維護MIME型態資訊。


Alternative Forms of Provider Access

應用程式發展過程中,以下三種provider存取的替代形式是重要的:
  • Batch access - 你可以在ContentProviderOperation類別中建立呼叫存取方法的批次處理,接著運用在ContentResolver.applyBatch()。
  • Asynchronous queries - 應該獨立的執行緒中執行查詢,一種達成方式是使用CursorLoader物件。
  • Data access vis intents - 雖然你無法直接地傳送一個intent,你可以傳送intent到provider的應用程式,應用程式有最佳的條件去修改provider資料。


星期三, 8月 27, 2014

Content Providers Basics (一)

如何存取Content provider中的資料


Overview

一個content provider提供資料給外部應用程式是一個或多個資料表,類似關聯式資料庫的資料表。一行表示是provider蒐集某些類型資料的一個實例,一行中的每個列表示對於一個實例資料的個別片段。

例如,在Android平台中的內建的provider之一的user dictionay,user dictionary儲存了使用者想保留的非標準單字的拼法,以下表格說明在provider的資料表中資料的樣子。

Table 1: Sample user dictionary table.
wordapp idfrequencylocale_ID
mapreduceuser1100en_US1
precompileruser14200fr_FR2
appletuser2225fr_CA3
constuser1255pt_BR4
intuser5100en_UK5
在資料表中,每一行表示每個單字的實例,單字可能不存在標準字典中。每一列表示對於此單字的一些資料,如locale。列的標頭表示列的名稱,儲存在provider中。參考row的locale,就參考到locale列。對於此資料表來說,_ID列就如"主鍵"列一樣,由provider自動地維護。

注意 - provider不要求要有主鍵,且如果資料表有主鍵也不一定要用_ID為名稱。然而,如果你想要從provider將資料綁定在ListView上,其中一個列名稱一定要是_ID。詳細原因請繼續往下到Displaying query results。


Accessing a provider

應用程式用一個ContentResolver客戶端物件來存取provider中的資料。ContentResovler物件有方法,它們會呼叫在provider物件中同名的方法。ContentResolver方法提供基本的"CRUD"(create、read、update及delete)功能。

在客戶端應用程式的處理程序中的ContentResolver物件和在擁有provider的應用程式中的ContentProvider物件會自動地處置處理程序間的通信,ContentProvider也會扮演一個在資料的資料庫與資料的外部顯示如資料表之間的抽象層。

注意 -存取provider時,你的應用程式必須在manifest文件中請求特定的權限,詳細說明在繼續往下到Content Providers Permission。

舉例,從User Dictionary Provider中得到單字及它們的locale的列表,你會呼叫ContentResolver.query()。此query()方法呼叫由User Dictionary Provider定義的query()方法。以下顯示ContentResolver.query()的呼叫。

// Queries the user dictionary and returns results
mCursor = getContentResolver().query(
    UserDictionary.Words.CONTENT_URI,   // The content URI of the words table
    mProjection,                        // The columns to return for each row
    mSelectionClause                    // Selection criteria
    mSelectionArgs,                     // Selection criteria
    mSortOrder);                        // The sort order for the returned rows
表格2顯示query()中的參數對應到SQL SELECT描述。

Table 2: Query() compared to SQL query.
query() argumentSELECT keyword/parameterNotes
UriFROM table_nameUri maps to the table in the provider named table_name.
projectioncol,col,col,...projection is an array of columns that should be included for each row retrieved.
selectionWHERE col =valueselection specifies the criteria for selecting rows.
selectionArgs(No exact equivalent. Selection arguments replace? placeholders in the selection clause.)
sortOrderORDER BYcol,col,...sortOrder specifies the order in which rows appear in the returnedCursor.


Content URIs

content URI是辨識provider中資料的一種URI,Content URIs包含整個provider的符號名稱(它的authority)且一個名稱點出一個資料表(一個path)。當你呼叫一個客戶端方法存取一個provider的一個資料表時,對於資料表來說,content URI是其中一個參數。

在先前的程式碼中,常數CONTENT_URI含有user dictionary的"word"資料表的content URI,ContentResolver物件解析出URI的authority,並利用它藉由與已知的provider的系統資料表的authority比較去"Resolve"provider,接著ContentResolver發送查詢參數給正確的provider。

ContentResolver利用content URI的一部分路徑來選擇存取的資料表。一個provider通常為每個資料表有一個path

在先前的程式碼中,"word"資料表的URI為

content://user_dictionary/words
user_dictionary字串是provider的authority,且words字串是資料表的路徑,字串content://(the scheme)總是存在的,且用來識別這是一個content URI。

許多provider允許你藉由在URI的末端附加上ID值來存取資料表中的單一行。例如,擷取自user dictionary它的_ID為4的一整行,可以使用以下的content URI

Uri singleUri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI,4);
當你已經擷取出一組行並想要更新或刪除它們其中的一行時,你可以使用id值達成。

注意 -Uri和Uri.Binder類別包含從字串建立良好格式的Uri的便利方法,ContentUris包含在一個Uri上附加id值得便利方法,閒錢的片段是使用withAppendedId()方法附加id值到USER DICTIONARY的content URI。


Retrieving Data from the Provider

接下來是要了解如何從一個provider中擷取資料。

擷取來自provider的資料,遵循這些基本步驟

  1. 對provider請求讀取權限
  2. 定義寄給provider的查詢的程式碼

Requesting read access permission

擷取來自provider的資料,對於provider你的應用程式需要"read access permission",無法在執行時請求此權限,你必須使用<uses-permission>標籤及確切由provider定義的名稱,在manifest文件指定你需要此權限。當你在manifest文件中指定此標籤,你實際上為你的應用程式"請求"

此權限。當使用者安裝你的應用程式,他們默默地授予此請求。

找出對於你使用的provider的read access permission的確切名稱,以及provider的其他存取權限名稱,請查看p該rovider的文件。

詳細在存取provider的權限的角色細節,請往下到Content Provider Permissions。

USER DICTIONARY PROVIDER定義了android.permission.READ_USER_DICTIONARY權限在manifeest文件中,所以應用成是想要讀取來自provider的資料必須請求此權限。


Constructing the query

擷取來自provider的資料的下一步是建購查詢(query),第一個片段是為存取User Dictionary Provider而定義一些變數。


// A "projection" defines the columns that will be returned for each row
String[] mProjection =
{
    UserDictionary.Words._ID,    // Contract class constant for the _ID column name
    UserDictionary.Words.WORD,   // Contract class constant for the word column name
    UserDictionary.Words.LOCALE  // Contract class constant for the locale column name
};
// Defines a string to contain the selection clause
String mSelectionClause = null;
// Initializes an array to contain selection arguments
String[] mSelectionArgs = {""};
下一個片段顯示如何利用ContentResolver.query(),以User Dictionary Provider為例。provider客戶端查詢與SQL查詢相似,它包含一組回傳欄位集合,一組selection條件及排序。

查詢回傳的欄位集合是被稱為projection(如變數mProjection)。

擷取指定的行的表示式被分為selection clause和selection argument,selection clause是結合了邏輯和布林的表示式、欄位名稱及值(如變數mSelectionClause),如果你只訂了可更換參數?取代值,query方法就會從selection argument陣列中取出值來取代它(如變數mSelectionArgs)。

下一個片段,如果使用者不輸入單字,selection clause設定為null,查詢會回傳所有在provider中的單字。如果使用者輸入單字,selection clause設定為UserDictionary.Words.WORD + " = ?"且selection argument陣列的第一個元素設定為使用者輸入的單字。

/*
 * This defines a one-element String array to contain the selection argument.
 */
String[] mSelectionArgs = {""};
// Gets a word from the UI
mSearchString = mSearchWord.getText().toString();
// Remember to insert code here to check for invalid or malicious input.
// If the word is the empty string, gets everything
if (TextUtils.isEmpty(mSearchString)) {
    // Setting the selection clause to null will return all words
    mSelectionClause = null;
    mSelectionArgs[0] = "";
} else {
    // Constructs a selection clause that matches the word that the user entered.
    mSelectionClause = UserDictionary.Words.WORD + " = ?";

    // Moves the user's input string to the selection arguments.
    mSelectionArgs[0] = mSearchString;
}
// Does a query against the table and returns a Cursor object
mCursor = getContentResolver().query(
    UserDictionary.Words.CONTENT_URI,  // The content URI of the words table
    mProjection,                       // The columns to return for each row
    mSelectionClause                   // Either null, or the word the user entered
    mSelectionArgs,                    // Either empty, or the string the user entered
    mSortOrder);                       // The sort order for the returned rows
// Some providers return null if an error occurs, others throw an exception
if (null == mCursor) {
    /*
     * Insert code here to handle the error. Be sure not to use the cursor! You may want to
     * call android.util.Log.e() to log this error.
     *
     */
// If the Cursor is empty, the provider found no matches
} else if (mCursor.getCount() < 1) {

    /*
     * Insert code here to notify the user that the search was unsuccessful. This isn't necessarily
     * an error. You may want to offer the user the option to insert a new row, or re-type the
     * search term.
     */
} else {
    // Insert code here to do something with the results
}
此查詢類似SQL描述。

SELECT _ID, word, locale FROM words WHERE word = <userinput> ORDER BY word ASC;

Protecting against malicious input

如果由content provider管理的資料是在SQL資料庫中,包含外部不可信的資料進入SQL描述可以導致SQL injection。

考慮此selection clause

// Constructs a selection clause by concatenating the user's input to the column name
String mSelectionClause =  "var = " + mUserInput;
如果你這麼做,你正允許使用者串連惡意SQL到你的SQL描述上。例如,使用者可以對mUserInput輸入"nothing; DROP TABLE *;",它會導致selection clause為var = nothing; DROP TABLE *;。因為selection clause被視為SQL描述,這可能導致provider刪除在底層SQLite資料庫中的所有資料表(除非provider被設定嘗試捕捉SQL injection)。

為了避免此問題,使用有利用?作為可更換變數的selection clause和selection arguments的獨立陣列。當你這麼做時,使用者輸入是直接綁在查詢上,而不是被直譯為SQL描述的一部份。因為它不被視為SQL,使用者輸入無法注入惡意SQL,取代使用串連方式來包含使用者輸入,使用此selection clause。

// Constructs a selection clause with a replaceable parameter
String mSelectionClause =  "var = ?";
設定selection arguments陣列如下

// Defines an array to contain the selection arguments
String[] selectionArgs = {""};



星期一, 8月 25, 2014

Content Providers

Content providers管理對一個結構性的資料集合的存取,它們封裝資料並為定義資料安全提供機制。Content providers是在一個處理程序中的資料與另一處理程序中運行的程式之間相互連結的標準介面。

當你想要存取在content providers中的資料時,你在你的應用程式的Context中使用ContentResolver物件作為client與provider通信。ContentResolver物件與Provider物件通信,該provider物件是一個實現ContentProvider類別的實例。該provider物件接收來自clients的資料請求並執行請求的動作,回傳結果。

如果你無意圖與其他應用程式分享你的資料,那你就不需要開發你自己的provider。然而,在應用程式本身提供自定義搜尋方案你就需要自己的provider。如果你想要從你的應用程式複製及貼上複雜資料或檔案到另一應用程式,你也需要自己的provider。

Android自己包含content provider,它用來管理資料如音頻、視頻、圖片及個人聯絡人資訊。你可以在參考文件android.provider中看到一些provider列表。在某些限制下,任何Android應用程式皆能存取這些provider。

星期日, 8月 24, 2014

Making ListView Scrolling Smooth

平滑地捲動ListView的關鍵是保持應用程式主執行緒(UI thread)遠離沉重的處理,確保你的任何硬碟存取、網路存取或SQL存取在獨立分開的執行緒中。


Use a Background Thread

使用背景執行續("worker thread")移除主執行緒的負擔,如此它可以專注在繪製使用者介面。在許多的例子中,使用AsyncTask提供一個簡單的方法來執行主執行續以外的工作,AsyncTask會自動地將所有execute()排隊並執行它們。

以下的例子,AsyncTask被用來在背景執行緒中載入圖片,載入結束,接著將圖片應用在使用者介面。

// Using an AsyncTask to load the slow images in a background thread
new AsyncTask<ViewHolder, Void, Bitmap>() {
    private ViewHolder v;

    @Override
    protected Bitmap doInBackground(ViewHolder... params) {
        v = params[0];
        return mFakeImageLoader.getImage();
    }

    @Override
    protected void onPostExecute(Bitmap result) {
        super.onPostExecute(result);
        if (v.position == position) {
            // If this item hasn't been recycled already, hide the
            // progress and set and show the image
            v.progress.setVisibility(View.GONE);
            v.icon.setVisibility(View.VISIBLE);
            v.icon.setImageBitmap(result);
        }
    }
}.execute(holder);
Android3.0開始,AsyncTask有額外的功能可用,所以你可以啟用它運行跨越多處理核心。取代呼叫execute(),你可以指定executeOnExecutor()及多請求可以同時在多個處理核心上被執行。


Hold View Objects in a View Holder


在ListView捲動期間,你的程式可能會經常地呼叫findViewById(),它可能會拖慢執行效率,即使Adapter供回收回傳了膨大的視圖,你仍然需要照料項目及更新它們。重複findViewById()的使用的一個方法是使用"view holder"設計模式。

一個ViewHolder物件儲存了每一個在layout中被標記的視圖元件,所以你可以直接存取它們,不需要重複地找它們。首先,你需要建立一個類別去保留你的具體視圖集。

static class ViewHolder {
  TextView text;
  TextView timestamp;
  ImageView icon;
  ProgressBar progress;
  int position;
}
接著填充ViewHolder並將它儲存在Layout內。

ViewHolder holder = new ViewHolder();
holder.icon = (ImageView) convertView.findViewById(R.id.listitem_image);
holder.text = (TextView) convertView.findViewById(R.id.listitem_text);
holder.timestamp = (TextView) convertView.findViewById(R.id.listitem_timestamp);
holder.progress = (ProgressBar) convertView.findViewById(R.id.progress_spinner);
convertView.setTag(holder);
現在你可以輕鬆地存取每個視圖,不需要look-up,以保留有價值的處理器週期。





星期六, 8月 23, 2014

ListView

ListView是一種view group,顯示一系列可捲動的項目。利用Adapter自動地將列表項插入列表中,adapter會從資料來源例如陣列或資料庫查詢中拉出資料並將每個項目轉換被插入ListView中的視圖。

如何利用adapter動態地插入視圖的介紹請點這裡


Using a Loader

利用CursorLoader異步查詢Cursor是標準的方法,以避免查詢阻塞應用程式的主執行緒。當CursorLoader接收了Cursor結果,LoaderManager.LoaderCallbacks接收回調給onLoadFinished()方法,此方法是你利用新的cursor更新adapter的地方,接著list view顯示結果。

關於利用loader異步加載資料的梗多資訊請點這裡


Example

以下例子使用LiatActivity,ListActivity是預設包含ListView作為它唯一的Layout的Activity。此例為執行查詢Contacts Provider列出姓名和電話號碼。

此Activity執行LoaderCallbacks介面為了使用CursorLoader動態地加載資料到list view。

public class ListViewLoader extends ListActivity
        implements LoaderManager.LoaderCallbacks<Cursor> {

    // This is the Adapter being used to display the list's data
    SimpleCursorAdapter mAdapter;

    // These are the Contacts rows that we will retrieve
    static final String[] PROJECTION = new String[] {ContactsContract.Data._ID,
            ContactsContract.Data.DISPLAY_NAME};

    // This is the select criteria
    static final String SELECTION = "((" + 
            ContactsContract.Data.DISPLAY_NAME + " NOTNULL) AND (" +
            ContactsContract.Data.DISPLAY_NAME + " != '' ))";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Create a progress bar to display while the list loads
        ProgressBar progressBar = new ProgressBar(this);
        progressBar.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
                LayoutParams.WRAP_CONTENT, Gravity.CENTER));
        progressBar.setIndeterminate(true);
        getListView().setEmptyView(progressBar);

        // Must add the progress bar to the root of the layout
        ViewGroup root = (ViewGroup) findViewById(android.R.id.content);
        root.addView(progressBar);

        // For the cursor adapter, specify which columns go into which views
        String[] fromColumns = {ContactsContract.Data.DISPLAY_NAME};
        int[] toViews = {android.R.id.text1}; // The TextView in simple_list_item_1

        // Create an empty adapter we will use to display the loaded data.
        // We pass null for the cursor, then update it in onLoadFinished()
        mAdapter = new SimpleCursorAdapter(this, 
                android.R.layout.simple_list_item_1, null,
                fromColumns, toViews, 0);
        setListAdapter(mAdapter);

        // Prepare the loader.  Either re-connect with an existing one,
        // or start a new one.
        getLoaderManager().initLoader(0, null, this);
    }

    // Called when a new Loader needs to be created
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        // Now create and return a CursorLoader that will take care of
        // creating a Cursor for the data being displayed.
        return new CursorLoader(this, ContactsContract.Data.CONTENT_URI,
                PROJECTION, SELECTION, null, null);
    }

    // Called when a previously created loader has finished loading
    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
        // Swap the new cursor in.  (The framework will take care of closing the
        // old cursor once we return.)
        mAdapter.swapCursor(data);
    }

    // Called when a previously created loader is reset, making the data unavailable
    public void onLoaderReset(Loader<Cursor> loader) {
        // This is called when the last Cursor provided to onLoadFinished()
        // above is about to be closed.  We need to make sure we are no
        // longer using it.
        mAdapter.swapCursor(null);
    }

    @Override 
    public void onListItemClick(ListView l, View v, int position, long id) {
        // Do something when a list item is clicked
    }
}