Android 插件開發指南
本節提供如何在 Android 平台上實作原生插件程式碼的詳細資訊。
在閱讀本文之前,請參閱插件開發指南,以了解插件的結構及其常見的 JavaScript 介面。本節將繼續示範範例 echo 插件,該插件可從 Cordova webview 與原生平台進行雙向通訊。如需其他範例,另請參閱CordovaPlugin.java中的註解。
Android 插件基於 Cordova-Android,後者是從具有原生橋接器的 Android WebView 建構而成。Android 插件的原生部分至少包含一個 Java 類別,該類別會擴充 CordovaPlugin
類別,並覆寫其 execute
方法之一。
插件類別對應
插件的 JavaScript 介面使用 cordova.exec
方法,如下所示
exec(<successFunction>, <failFunction>, <service>, <action>, [<args>]);
這會將請求從 WebView 封送到 Android 原生端,有效地使用 args
陣列中傳遞的其他引數,在 service
類別上呼叫 action
方法。
無論您是以 Java 檔案還是以其自身的 jar 檔案來散佈插件,都必須在 Cordova-Android 應用程式的 res/xml/config.xml
檔案中指定該插件。如需瞭解如何使用 plugin.xml
檔案注入此 feature
元素,請參閱應用程式插件。
<feature name="<service_name>">
<param name="android-package" value="<full_name_including_namespace>" />
</feature>
服務名稱與 JavaScript exec
呼叫中使用的名稱相符。該值為 Java 類別的完整命名空間識別碼。否則,插件可能會編譯,但仍然無法供 Cordova 使用。
插件初始化和生命週期
每個 WebView
的生命週期都會建立一個插件物件的執行個體。除非在 config.xml
中將 onload
name
屬性設定為 "true"
的 <param>
,否則在 JavaScript 首次呼叫參考插件之前,不會具現化插件。例如:
<feature name="Echo">
<param name="android-package" value="<full_name_including_namespace>" />
<param name="onload" value="true" />
</feature>
插件應該使用 initialize
方法來進行其啟動邏輯。
@Override
public void initialize(CordovaInterface cordova, CordovaWebView webView) {
super.initialize(cordova, webView);
// your init code here
}
插件也可以存取 Android 生命周期事件,並可透過擴充提供的其中一種方法 (onResume
、onDestroy
等) 來處理這些事件。具有長時間執行請求、背景活動 (例如媒體播放)、監聽器或內部狀態的插件應該實作 onReset()
方法。當 WebView
導覽至新頁面或重新整理時 (會重新載入 JavaScript),會執行此方法。
撰寫 Android Java 插件
JavaScript 呼叫會將插件請求觸發到原生端,而對應的 Java 插件會在 config.xml
檔案中正確對應,但最終的 Android Java 插件類別看起來是什麼樣子?透過 JavaScript 的 exec
函式分派給插件的任何內容,都會傳遞到插件類別的 execute
方法中。大多數 execute
實作看起來像這樣
@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
if ("beep".equals(action)) {
this.beep(args.getLong(0));
callbackContext.success();
return true;
}
return false; // Returning false results in a "MethodNotFound" error.
}
JavaScript exec
函式的 action
參數對應於要使用選用參數來分派的私有類別方法。
在攔截例外狀況並傳回錯誤時,為了清楚起見,傳回給 JavaScript 的錯誤應盡可能與 Java 的例外狀況名稱相符。
執行緒
插件的 JavaScript 不會在 WebView
介面的主執行緒中執行;而是會在 WebCore
執行緒上執行,execute
方法也會如此。如果您需要與使用者介面互動,應該使用 Activity 的 runOnUiThread
方法,如下所示
@Override
public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException {
if ("beep".equals(action)) {
final long duration = args.getLong(0);
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
...
callbackContext.success(); // Thread-safe.
}
});
return true;
}
return false;
}
如果您不需要在 UI 執行緒上執行,但也不想封鎖 WebCore
執行緒,則應該使用 Cordova ExecutorService
來執行您的程式碼,該執行緒是透過 cordova.getThreadPool()
取得的,如下所示
@Override
public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException {
if ("beep".equals(action)) {
final long duration = args.getLong(0);
cordova.getThreadPool().execute(new Runnable() {
public void run() {
...
callbackContext.success(); // Thread-safe.
}
});
return true;
}
return false;
}
新增相依性程式庫
如果您的 Android 插件有額外的相依性,則必須以兩種方式之一在 plugin.xml
中列出。
慣用的方式是使用 <framework />
標籤 (如需詳細資訊,請參閱插件規格)。以這種方式指定程式庫允許透過 Gradle 的 相依性管理邏輯來解析它們。這允許常用的程式庫 (例如 gson、android-support-v4 和 google-play-services) 由多個插件使用,而不會發生衝突。
第二個選項是使用 <lib-file />
標籤來指定 jar 檔案的位置 (如需詳細資訊,請參閱插件規格)。只有在您確定沒有其他插件會依賴您參考的程式庫時,才應該使用這種方法 (例如,如果程式庫是您的插件專屬)。否則,如果另一個插件新增相同的程式庫,您可能會導致插件的使用者發生組建錯誤。值得注意的是,Cordova 應用程式開發人員不一定是原生開發人員,因此原生平台組建錯誤可能會特別令人沮喪。
Echo Android 插件範例
為了符合應用程式插件中描述的 JavaScript 介面 echo 功能,請使用 plugin.xml
將 feature
規格插入到本機平台的 config.xml
檔案
<platform name="android">
<config-file target="config.xml" parent="/*">
<feature name="Echo">
<param name="android-package" value="org.apache.cordova.plugin.Echo"/>
</feature>
</config-file>
<source-file src="src/android/Echo.java" target-dir="src/org/apache/cordova/plugin" />
</platform>
然後,將下列內容新增至 src/android/Echo.java
檔案
package org.apache.cordova.plugin;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CallbackContext;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
/**
* This class echoes a string called from JavaScript.
*/
public class Echo extends CordovaPlugin {
@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
if (action.equals("echo")) {
String message = args.getString(0);
this.echo(message, callbackContext);
return true;
}
return false;
}
private void echo(String message, CallbackContext callbackContext) {
if (message != null && message.length() > 0) {
callbackContext.success(message);
} else {
callbackContext.error("Expected one non-empty string argument.");
}
}
}
檔案頂端必要的匯入會從 CordovaPlugin
擴充類別,該類別的 execute()
方法會覆寫以接收來自 exec()
的訊息。execute()
方法會先測試 action
的值,在此案例中只有一個有效的 echo
值。任何其他動作都會傳回 false
,並導致 INVALID_ACTION
錯誤,該錯誤會轉換為在 JavaScript 端呼叫的錯誤回呼。
接下來,此方法會使用 args
物件的 getString
方法來擷取 echo 字串,指定傳遞給此方法的第一個參數。在將值傳遞至私有 echo
方法之後,會檢查其參數以確保其不是 null
或空字串,在這種情況下,callbackContext.error()
會呼叫 JavaScript 的錯誤回呼。如果各種檢查都通過,則 callbackContext.success()
會將原始 message
字串作為參數傳遞回 JavaScript 的成功回呼。
Android 整合
Android 具有 Intent 系統,該系統允許進程彼此通訊。插件可以存取 CordovaInterface
物件,該物件可以存取執行應用程式的 Android Activity。這是啟動新 Android Intent 所需的 Context。CordovaInterface
允許插件啟動 Activity 以取得結果,並在 Intent 返回應用程式時設定回呼插件。
從 Cordova 2.0 開始,插件無法再直接存取 Context,而且舊版 ctx
成員已遭到取代。所有 ctx
方法都存在於 Context 上,因此 getContext()
和 getActivity()
都可以傳回所需的物件。
Android 權限
直到最近,Android 權限都是在安裝時而不是在執行時處理的。需要在使用權限的應用程式上宣告這些權限,而且這些權限需要新增至 Android Manifest。這可以透過使用 config.xml
在 AndroidManifest.xml
檔案中插入這些權限來完成。以下範例使用聯絡人權限。
<config-file target="AndroidManifest.xml" parent="/*">
<uses-permission android:name="android.permission.READ_CONTACTS" />
</config-file>
執行階段權限 (Cordova-Android 5.0.0+)
Android 6.0「Marshmallow」引入了新的權限模型,使用者可以在其中根據需要開啟和關閉權限。這表示應用程式必須處理這些權限變更才能具有未來相容性,而這也是 Cordova-Android 5.0.0 版本的主要重點。
可以在 Android 開發人員文件中這裡找到需要在執行階段處理的權限。
就插件而言,可以透過呼叫權限方法來要求權限;其簽名如下
cordova.requestPermission(CordovaPlugin plugin, int requestCode, String permission);
為了減少冗長,標準做法是將其指派給本機靜態變數
public static final String READ = Manifest.permission.READ_CONTACTS;
將 requestCode 定義如下也是標準做法
public static final int SEARCH_REQ_CODE = 0;
然後,在 exec 方法中,應該檢查權限
if(cordova.hasPermission(READ))
{
search(executeArgs);
}
else
{
getReadPermission(SEARCH_REQ_CODE);
}
在此案例中,我們只呼叫 requestPermission
protected void getReadPermission(int requestCode)
{
cordova.requestPermission(this, requestCode, READ);
}
這會呼叫 Activity 並導致出現提示,要求權限。使用者擁有權限後,必須使用 onRequestPermissionResult
方法來處理結果,每個插件都應該覆寫該方法。以下可以找到範例
public void onRequestPermissionResult(int requestCode, String[] permissions,
int[] grantResults) throws JSONException
{
for(int r:grantResults)
{
if(r == PackageManager.PERMISSION_DENIED)
{
this.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, PERMISSION_DENIED_ERROR));
return;
}
}
switch(requestCode)
{
case SEARCH_REQ_CODE:
search(executeArgs);
break;
case SAVE_REQ_CODE:
save(executeArgs);
break;
case REMOVE_REQ_CODE:
remove(executeArgs);
break;
}
}
上述 switch 陳述式會從提示傳回,並根據傳入的 requestCode
呼叫個別的方法。應該注意的是,如果執行未正確處理,權限提示可能會堆疊,並且應該避免這種情況。
除了要求單一權限的權限之外,也可以透過定義 permissions
陣列來要求整個群組的權限,就像使用地理位置插件一樣
String [] permissions = { Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION };
然後,在要求權限時,只需要執行以下操作
cordova.requestPermissions(this, 0, permissions);
這會要求陣列中指定的權限。建議提供可公開存取的 permissions
陣列,因為使用您的插件作為相依性的插件可以使用此陣列,雖然這不是必要的。
偵錯 Android 插件
雖然建議使用 Android Studio,但可以使用 Eclipse 或 Android Studio 進行 Android 偵錯。由於目前 Cordova-Android 是以程式庫專案的形式使用,而插件則支援為原始碼,因此可以像原生 Android 應用程式一樣偵錯 Cordova 應用程式內部的 Java 程式碼。
啟動其他 Activity
如果您的插件啟動一個將 Cordova Activity 推送到背景的 Activity,則需要特別注意。如果裝置記憶體不足,Android 作業系統會銷毀背景中的 Activity。在這種情況下,CordovaPlugin
實例也會被銷毀。如果您的插件正在等待從它啟動的 Activity 取得結果,當 Cordova Activity 回到前景並取得結果時,將會建立一個新的插件實例。然而,插件的狀態不會自動儲存或還原,插件的 CallbackContext
也會遺失。您的 CordovaPlugin
可以實作兩種方法來處理這種情況
/**
* Called when the Activity is being destroyed (e.g. if a plugin calls out to an
* external Activity and the OS kills the CordovaActivity in the background).
* The plugin should save its state in this method only if it is awaiting the
* result of an external Activity and needs to preserve some information so as
* to handle that result; onRestoreStateForActivityResult() will only be called
* if the plugin is the recipient of an Activity result
*
* @return Bundle containing the state of the plugin or null if state does not
* need to be saved
*/
public Bundle onSaveInstanceState() {}
/**
* Called when a plugin is the recipient of an Activity result after the
* CordovaActivity has been destroyed. The Bundle will be the same as the one
* the plugin returned in onSaveInstanceState()
*
* @param state Bundle containing the state of the plugin
* @param callbackContext Replacement Context to return the plugin result to
*/
public void onRestoreStateForActivityResult(Bundle state, CallbackContext callbackContext) {}
重要的是要注意,上述方法僅應在您的插件啟動一個 Activity 來取得結果時使用,並且僅應還原處理該 Activity 結果所需的狀態。插件的狀態不會被還原,除非是在使用 CordovaInterface
的 startActivityForResult()
方法請求 Activity 結果,且 Cordova Activity 在背景中被作業系統銷毀的情況下。
作為 onRestoreStateForActivityResult()
的一部分,您的插件將會被傳遞一個替換的 CallbackContext。重要的是要了解這個 CallbackContext 與被 Activity 銷毀的那個不是同一個。原始的回呼會遺失,並且不會在 javascript 應用程式中觸發。相反,這個替換的 CallbackContext
會在應用程式恢復時觸發的 resume
事件中回傳結果。resume
事件的有效負載遵循以下結構
{
action: "resume",
pendingResult: {
pluginServiceName: string,
pluginStatus: string,
result: any
}
}
pluginServiceName
將會與您 plugin.xml 中的 name 元素 相符。pluginStatus
將會是一個描述傳遞給 CallbackContext 的 PluginResult 狀態的字串。請參閱 PluginResult.java 以取得與插件狀態相對應的字串值。result
將會是插件傳遞給 CallbackContext 的任何結果(例如,字串、數字、JSON 物件等)。
這個 resume
有效負載將會被傳遞給 javascript 應用程式已註冊的任何 resume
事件回呼。這表示結果會直接傳送到 Cordova 應用程式;您的插件在應用程式接收到結果之前,不會有機會使用 javascript 來處理結果。因此,您應該盡可能使原生程式碼回傳的結果完整,並且在啟動 Activity 時不要依賴任何 javascript 回呼。
請務必溝通 Cordova 應用程式應如何解讀在 resume
事件中接收到的結果。維護自己的狀態並記住他們發出的請求和提供的參數(如果需要),是 Cordova 應用程式的責任。但是,您仍然應該清楚地在插件的 API 中溝通 pluginStatus
值的含義以及 resume
欄位中回傳的資料類型。
啟動 Activity 的完整事件序列如下
- Cordova 應用程式呼叫您的插件
- 您的插件啟動一個 Activity 來取得結果
- Android 作業系統銷毀 Cordova Activity 和您的插件實例
onSaveInstanceState()
被呼叫
- 使用者與您的 Activity 互動,並且該 Activity 完成
- Cordova Activity 被重新建立,並且接收到 Activity 結果
onRestoreStateForActivityResult()
被呼叫
onActivityResult()
被呼叫,並且您的插件將結果傳遞給新的 CallbackContextresume
事件被觸發,並且 Cordova 應用程式接收到該事件
Android 提供了一個開發人員設定,用於偵錯記憶體不足時 Activity 的銷毀。在您的裝置或模擬器的「開發人員選項」選單中啟用「不保留活動」設定,以模擬記憶體不足的情況。如果您的插件啟動外部 Activity,您應該始終在啟用此設定的情況下進行一些測試,以確保您能正確處理記憶體不足的情況。