Titanium Android native module

titanium本身有提供建立native module的指令,可透過指令產出基本可以使用的template,然後再做修改。

可參考官方的quick start,或者直接參考以下教學:

建立一個android native module

建立一個android module
1
ti create -p android -t module -d . -n test -u http:// --id com.example.test
build一個native java project
1
2
cd test/android
ant
將build好的zip,解壓縮至titanium的module路徑
1
unzip -o com.example.test-android-1.0.0.zip -d ~/Library/Application\ Support/Titanium/

KrollModule與KrollProxy

透過titanium的指令產生的native module,會建立出Module和Proxy兩個class,分別繼承KrollModuleTiViewProxy。其中會看到Kroll.methodKroll.getPropertyKroll.setProperty這些annotation,主要是會將method或者參數讓js去使用或存取寫入用。

取得剛建立好的native module
1
var test = require('com.example.test');
若是在KrollModule宣告的Kroll.method這類annotation,將可直接讓js module存取
java code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Kroll.module(name="Test", id="com.example.test")
public class TestModule extends KrollModule {

@Kroll.method
public String example() {

return "hello world";
}

@Kroll.getProperty
public String getExampleProp() {

return "hello world";
}

@Kroll.setProperty
public void setExampleProp(String value) {

Log.d(LCAT, "set example property: " + value)
}

//....
}
js code
1
2
3
Ti.API.info( test.example() );
Ti.API.info("module exampleProp is => " + test.exampleProp);
test.exampleProp = "This is a test value";
若透過KrollProxy宣告的Kroll.method這類annotation,會先建立一個instance,在讓js使用
java code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Kroll.proxy(creatableInModule=TestModule.class)
public class ExampleProxy extends TiViewProxy {

// ....

@Override
public TiUIView createView(Activity activity)
{

TiUIView view = new ExampleView(this);
view.getLayoutParams().autoFillsHeight = true;
view.getLayoutParams().autoFillsWidth = true;
return view;
}

// Methods
@Kroll.method
public void printMessage(String message)
{

Log.d(LCAT, "printing message: " + message);
}

@Kroll.getProperty @Kroll.method
public String getMessage()
{

return "Hello World from my module";
}

@Kroll.setProperty @Kroll.method
public void setMessage(String message)
{

Log.d(LCAT, "Tried setting module message to: " + message);
}

}
js code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (Ti.Platform.name == "android") {
var proxy = test.createExample({
message: "Creating an example Proxy",
backgroundColor: "red",
width: 100,
height: 100,
top: 100,
left: 150
});

proxy.printMessage("Hello world!");
proxy.message = "Hi world!. It's me again.";
proxy.printMessage("Hello world!");
win.add(proxy);
}
若是想傳入function可透過KrollFunction
java code
1
2
3
4
5
6
@Kroll.method 
public void testCallback(KrollFunction cb) {

KrollDict dict = new KrollDict();
dict.put("test", 123);
cb.callAsync( getKrollObject(), dict );
}
js code
1
2
3
4
5
6
7
8
9
test.testCallback(function( evt ){
//1
Ti.API.info(arguments.length);
Ti.API.info(evt);
//{"test":123}
Ti.API.info(JSON.stringify( evt ));
//123
Ti.API.info(evt.test);
});

以上的程式碼皆為片段。

在Titanium HTTPClient使用cookie與header

titanium內建的HTTPClient本身就有實作cookie的功能,使用起來跟瀏覽器一樣,會自動解析response header,及發送以設定的cookie,而不需要額外去解析header,和添加request的header,要清除cookie可以使用clearCookies,若要在client端設定cookie,可以使用setRequestHeader

清除與設定cookie

建立一個HTTPClient:

1
var client = Ti.Network.createHTTPClient({});

指定清除某個網域底下的cookie:

1
client.clearCookies( "www.example.com" );

由client設定cookie:

1
client.setRequestHeader( "cookie", "name=sparrow;" );

使用cookie常見問題

通常cookie能不能被存取,會依照expires、path、domain、HttpOnly、secure這幾項辨識:

  • expires : cookie的時效性,當時間超過expires設定的日期,這個cookie將會被捨棄。

    1
    expires=Sat, 17-Oct-2015 14:11:09 GMT;
  • path : 在domain下,cookie允許存取的路徑。

    1
    path=/;
  • domain : cookie允許存取的domain。

    1
    2
    3
    4
    domain=.google.com.tw;
    最前面的.指得是子網域皆可存取,例如:
    www.google.com.tw
    ad.google.com.tw
  • HttpOnly : 在瀏覽器下看到這個屬性時,只允許server端操作此cookie,換句話說client雖然存此cookie,卻無權操作,可防止透過xss將cookie偷走。

    1
    HttpOnly
  • secure :只允許在https上操作此cookie,在一般http接無法存取。

    1
    secure;

如果有用到nginx、apache這類的server去做proxy,需要檢查path或domain,才不會遇到cookie無效的狀況。

解析response的header

在android的device,如果想要取得header的內容可以使用allResponseHeaders,雖然titanium有提供getResponseHeader的功能,實際使用過都無法取得到值。

在onload取值:

1
2
3
4
client.onload = function(){

Ti.API.info( client.allResponseHeaders );
};

如果你想取得一個header的json,可以實作一個類似這種function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

/**
* @param {*} ary
* @type boolean
*/
var isArray = function( ary ){

return Object.prototype.toString.call( ary ) === "[object Array]";
};

/**
* @param {String} allResHeaders
* @type Object
*/
var getHeaders = function( allResHeaders ){

var result = {},

headerStrs = allResHeaders.split( "\n" );

for ( var index in headerStrs ) {

if ( headerStrs[index] ) {

var header = result[index].split( ":", 2 ),

name = header[0].toLowerCase(),

value = header[1];

if ( result[name] && !isArray( result[name] ) ) result[name] = [ result[name], value];

else if ( isArray( result[name] ) ) result[name].push( value );

else result[name] = value;

}
}

return result;
};

透過getHeaders取值:

1
2
var headers = getHeaders( client.allResponseHeaders );
Ti.API.info( headers );

Titanium相同檔名會拋出錯誤

大部分的程式,只要檔名大小寫不同,就會視為不同的檔案,但是在titanium編譯的時候,則會發生錯誤,因為titanium不允許在相同資料夾裡,有相同的檔案名稱。因為之前一直是用tishadow測試,都沒遇到這個問題,最後要build在device上,就遇到了以下錯誤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[ERROR] /home/sparrow/Works/test/build/android/bin/assets/Resources/actions/IndexAction.js: error: File is case-insensitive equivalent to: /home/sparrow/Works/ec2/test/build/android/bin/assets/Resources/actions/indexAction.js
[ERROR] Exception occured while building Android project:
[ERROR] Traceback (most recent call last):
[ERROR] File "/home/sparrow/.titanium/mobilesdk/linux/3.1.2.GA/android/builder.py", line 2621, in <module>
[ERROR] builder.build_and_run(True, avd_id, device_args=device_args, debugger_host=debugger_host, profiler_host=profiler_host)
[ERROR] File "/home/sparrow/.titanium/mobilesdk/linux/3.1.2.GA/android/builder.py", line 2400, in build_and_run
[ERROR] launched, launch_failed = self.package_and_deploy()
[ERROR] File "/home/sparrow/.titanium/mobilesdk/linux/3.1.2.GA/android/builder.py", line 1867, in package_and_deploy
[ERROR] unsigned_apk = self.create_unsigned_apk(ap_, webview_js_files)
[ERROR] File "/home/sparrow/.titanium/mobilesdk/linux/3.1.2.GA/android/builder.py", line 1679, in create_unsigned_apk
[ERROR] resources_zip = zipfile.ZipFile(resources_zip_file)
[ERROR] File "/usr/lib/python2.7/zipfile.py", line 701, in __init__
[ERROR] self.fp = open(file, modeDict[mode])
[ERROR] IOError: [Errno 2] No such file or directory: '/home/sparrow/Works/ec2/test/build/android/bin/app.ap_'
[ERROR] Build process exited with code 1
[ERROR] Project failed to build after 1m 40s 422ms

解決方法就是更換其中一個檔案的名稱,有了這次教訓…,以後盡量不使用相同檔案名稱。

titanium build failed(android)

今天在其他電腦裝titanium遇到以下錯誤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[TRACE] Writing out AndroidManifest.xml
[ERROR] Exception occured while building Android project:
[ERROR] Traceback (most recent call last):
[ERROR] File "/home/sparrow/.titanium/mobilesdk/linux/3.1.0.GA/android/builder.py", line 2480, in <module>
[ERROR] builder.build_and_run(False, avd_id)
[ERROR] File "/home/sparrow/.titanium/mobilesdk/linux/3.1.0.GA/android/builder.py", line 2264, in build_and_run
[ERROR] self.manifest_changed = self.generate_android_manifest(compiler)
[ERROR] File "/home/sparrow/.titanium/mobilesdk/linux/3.1.0.GA/android/builder.py", line 1404, in generate_android_manifest
[ERROR] '-I', self.android_jar], warning_regex=r'skipping')
[ERROR] File "/home/sparrow/.titanium/mobilesdk/linux/3.1.0.GA/android/run.py", line 38, in run
[ERROR] print "[DEBUG] %s" % subprocess.list2cmdline(args_to_log)
[ERROR] File "/usr/lib/python2.7/subprocess.py", line 587, in list2cmdline
[ERROR] needquote = (" " in arg) or ("\t" in arg) or not arg
[ERROR] TypeError: argument of type 'NoneType' is not iterable

查了~/.titanium/mobilesdk/linux/3.1.0.GA/android/run.py這隻程式,發現有些參數是空的。

然後在androidsdk.py找到這段,功能是取得android sdk的aapt路徑:

1
2
3
4
5
6
7
8
9
def get_aapt(self):  
# for aapt (and maybe eventually for others) we
# want to favor platform-tools over android-x/tools
# because of new resource qualifiers for honeycomb
sdk_platform_tools_dir = self.get_sdk_platform_tools_dir()
if not sdk_platform_tools_dir is None and os.path.exists(os.path.join(sdk_platform_tools_dir, 'aapt')):
return os.path.join(sdk_platform_tools_dir, 'aapt')

return self.get_platform_tool('aapt')

比對另一台電腦sdk的platform-tools(舊):

1
2
3
4
5
6
7
8
9
10
11
12
13
|-- platform-tools
| |-- aapt
| |-- adb
| |-- aidl
| |-- api
| |-- dexdump
| |-- dx
| |-- fastboot
| |-- lib
| |-- llvm-rs-cc
| |-- NOTICE.txt
| |-- renderscript
| `-- source.properties

新下載的sdk:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
├── platform-tools
│ ├── adb
│ ├── api
│ ├── fastboot
│ ├── NOTICE.txt
│ └── source.properties
├── build-tools
│ └── 17.0.0
│ ├── aapt
│ ├── aidl
│ ├── dexdump
│ ├── dx
│ ├── lib
│ ├── llvm-rs-cc
│ ├── NOTICE.txt
│ ├── renderscript
│ └── source.properties

所以新版的sdk目錄結構有變動,在加上titanium執行檔案有預設路徑,解決方式有下列幾種:

  1. 修改androidsdk.py裡面的get_dx、get_dx_jar、get_aapt…回傳路徑
  2. 搬移檔案build-tools檔案到platform-tools
  3. 直接使用link

我使用直接採用link的方式:

1
2
3
4
5
6
cd ~/tool/android-sdk/platform-tools
ln -s ../build-tools/17.0.0/aapt
ln -s ../build-tools/17.0.0/dx
ln -s ../build-tools/17.0.0/dexdump
ln -s ../build-tools/17.0.0/llvm-rs-cc
ln -s ../build-tools/17.0.0/aidl

最後還要還要記得設定jarsigner、keytool:

1
2
3
4
sudo update-alternatives --install "/usr/bin/jarsigner" "jarsigner" "/usr/lib/jvm/java-6-sun/bin/jarsigner" 1
sudo update-alternatives --install "/usr/bin/keytool" "keytool" "/usr/lib/jvm/java-6-sun/bin/keytool" 1
sudo update-alternatives --config jarsigner
sudo update-alternatives --config keytool

Titanium command line

titanium的環境都安裝好之後,就可以找到~/.titanium這路徑,然後在.bashrc加上以下配置。

1
alias titanium.py=$HOME/.titanium/mobilesdk/linux/3.0.0.GA/titanium.py

接著執行:

1
source .bashrc

然後建立一個project:

1
titanium.py create --platform=android --android=/path/to/android-sdk --id=com.mycompany.myApp --name=myApp

會建立出以下檔案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.
|-- build
| `-- android
| |-- AndroidManifest.xml
| |-- assets
| |-- bin
| |-- default.properties
| |-- gen
| |-- lib
| |-- res
| `-- src
|-- LICENSE
|-- manifest
|-- README
|-- Resources
| |-- android
| | |-- appicon.png
| | |-- default.png
| | `-- images
| |-- app.js
| |-- KS_nav_ui.png
| `-- KS_nav_views.png
`-- tiapp.xml

啟動android的模擬器:

1
titanium.py emulator --platform=android --android=/path/to/android-sdk

如果需要使用到google api,可等待建立後,至avd調整。

最後在把project run在模擬器上:

1
titanium.py run --platform=android --android=/path/to/android-sdk

可參考官方文件