2014-10-19

Android DexClassLoader 續

之前介紹的方法讓我們的程式可以在執行時期動態載入其它的程式碼,後來發現 Facebook 的 Buck 當中也有類似的功能 ( Exopackage ),但它實作的方法和之前介紹的方法不同,底下敘述使用 Buck 的方法,並套用在前一篇範例上的流程 :

1. 製作 dex 檔案 :
與前一篇介紹的方法相同

2. 程式執行時期下載 dex 檔案  :
建立一個 com.example.dexclassloader 之 Android Application Project 並將主要 Activity (MainActivity.java ) 替換如下 :
package com.example.dexclassloader;
 
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

import android.os.Bundle;
import android.app.Activity;
import android.content.Context;
import android.util.Log;
import android.view.Menu;
 
public class MainActivity extends Activity {
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
   
  Thread works =new Thread(){
   @Override
   public void run(){
    //download and save the jar
    downloadDexFile("http://localhost/PayloadActivity.dex.jar", "PayloadActivity.dex.jar");
    
    //install dex jar
    List<File> dexJars =new ArrayList<File>();
    File libFile =new File(getFilesDir()+"/PayloadActivity.dex.jar");
    dexJars.add(libFile);
    SystemClassLoaderAdder.installDexJars(getClassLoader(), getDir("outdex", Context.MODE_PRIVATE), dexJars);
     
    //load the jar
    loadAndInvokeMethod();
   }
  };
  works.start();
 }
 
 @Override
 public boolean onCreateOptionsMenu(Menu menu) {
  // Inflate the menu; this adds items to the action bar if it is present.
  getMenuInflater().inflate(R.menu.main, menu);
  return true;
 }
  
 private void downloadDexFile(String url, String filename){
     try {
 
         URL u = new URL(url);
         HttpURLConnection c = (HttpURLConnection) u.openConnection();
         c.setRequestMethod("GET");
         c.setDoOutput(true);
         c.connect();
          
         FileOutputStream f =openFileOutput(filename, MODE_PRIVATE);
         InputStream in = c.getInputStream();
 
         byte[] buffer = new byte[1024];
         int len1 = 0;
          
         while ((len1 = in.read(buffer)) > 0) {
             f.write(buffer, 0, len1);
         }
         f.close();
          
     } catch (Exception e) {
         //error dialog
         Log.d("mainactivity", "error -"+e.toString());
 
     }
 }
  
 private void loadAndInvokeMethod(){
  try{  
         //LOAD CLASS
         Class<?> payloadClass =this.getClassLoader().loadClass("com.example.PayloadActivity");
          
         //LOAD METHOD
         Method starterFunc =payloadClass.getMethod("starter", Activity.class);
          
         //INVOKE METHOD
         starterFunc.invoke(null, this);
          
  }catch(Exception e){
    
  }
 }
 
}
觀察得知原本 overrideClassLoader 函數的部分改為呼叫 SystemClassLoaderAdder.installDexJars 函數,此外呼叫載入的 dex 裡的類別函數方法也變得簡單。前一篇的作法是建立一個自己的 ClassLoader 去替換原本的 ClassLoader,這篇的作法是直接去修改現有的 ClassLoader 因此也比上一篇的方法少了 MyClassLoader.java 檔案。

(ps. 程式碼裡呼叫的 installDexJars 函數位於檔案 SystemClassLoaderAdder.java,可以在這裡取得)

3. MyApplication.java

完成步驟1, 2 之後程式就可以運作,但不算真正的完成。有時候啟動程式仍然會得到 java.lang.ClassNotFoundException 的錯誤訊息。

為何說是 "有時候" ,因為這跟使用者操作方式的流程有關,假若現在使用者在程式裡啟動了 Acitivty ,而這個 Activity 是屬於 dex 檔案後來載入的程式碼,啟動後在 Activity 的介面操作到一半切換到其它的 App,此時有可能因為系統記憶體不足,釋放原本在背景執行的 App 的記憶體,之後使用者欲切換到原來的 App,系統會自動重新啟動 App 並呼叫上次啟動的 Activity,此時的 ClassLoader 是系統建立的 ClassLoader 不是我們更改過的 ClassLoader 因此找不到 dex 檔案而發生 Class Not Found Exception。

幸好在系統啟動 Activity 之前我們還有機會讓系統執行我們的程式。那就是 Application 的 attachBaseContext 函數。

底下是新增加的程式 MyApplication.java :
package com.example.dexclassloader;

import android.app.Application;
import android.content.Context;
import android.content.res.Configuration;
import java.util.ArrayList;
import java.util.List;
import java.io.File;

public class MyApplication extends Application
{
  public void installPayloadDexFile(){

    String libPath =getFilesDir()+"/PayloadActivity.dex.jar";
    File libFile =new File(libPath);

    if (libFile.exists()==false)
      return;

    File optimizedDexOutputPath = getDir("outdex", Context.MODE_PRIVATE);
    List<File> dexJars = new ArrayList<File>();
    dexJars.add(libFile);
    SystemClassLoaderAdder.installDexJars(getClassLoader(), optimizedDexOutputPath, dexJars);

  }

  @Override
  public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
  }

  @Override
  protected final void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    installPayloadDexFile();
    
  }

  @Override
  public final void onCreate() {
    super.onCreate();
    installPayloadDexFile();
  }

  @Override
  public final void onTerminate() {
    super.onTerminate();

  }

  @Override
  public final void onLowMemory() {
    super.onLowMemory();

  }

  @Override
  public final void onTrimMemory(int level) {
    super.onTrimMemory(level);

  }

}

觀察可知程式在啟動 Activity 之前首先呼叫 MyApplication.attachBaseContext 然後檢查是否存在檔案 PayloadActivity.dex.jar ,若有則載入 dex 並修改目前的 ClassLoader。

4. AndroidManifest.xml

同前一篇介紹的,除了要預先定義 Activity 到 AndroidManifest.xml 之外,若要讓 Class MyApplication 生效,還須定義 MyApplication 在 application 標籤內 :
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.dexclassloader"
    android:versionCode="1"
    android:versionName="1.0" >
 
    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="18" />
 
    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" 
        android:name="com.example.dexclassloader.MyApplication" >
        <activity
            android:name="com.example.dexclassloader.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
 
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name="com.example.PayloadActivity" android:label="@string/app_name" />
    </application>
     
  <uses-permission android:name="android.permission.INTERNET" />
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
 
</manifest>

ref:http://facebook.github.io/buck/article/exopackage.html
ref:http://developer.android.com/reference/android/app/Application.html
ref:http://stackoverflow.com/questions/9873669/how-do-i-catch-content-provider-initialize


2014-10-16

Android : 建立並安裝 自我簽署憑証 ( Self-Signed Certificate authority )

在安全性的需求之下我們使用 https 代替 http 連線,而 server 端開啟 https 服務需要設定相關的憑証,這些憑証要向具有公信力的機購購買,但在開發時期我們可以自己產生,根據這裡的資料,製作 X.509 version 3 的 self-signed CA certificates 可以避免大多數行動裝置無法安裝的問題,底下步驟是敘述製作以及在 Android 上安裝 self-signed CA certificates 的流程。

A. 製作 self-signed CA certificates

1. 建立一個檔案,例如 openssl.cnf,其內容如下 (openssl.cnf 檔案的重點在於 basicConstraints = CA:true 以及 alt_names) :
[req]
  distinguished_name = req_distinguished_name
  req_extensions = v3_req

[req_distinguished_name]
  countryName = Country Name (2 letter code)
  countryName_default = US
  localityName = Locality Name (eg, city)
  organizationalUnitName = Organizational Unit Name (eg, section)
  commonName = Common Name (eg, YOUR name)
  commonName_max = 64
  emailAddress = Email Address
  emailAddress_max = 40

[v3_req] 
  basicConstraints = CA:true
  keyUsage = keyEncipherment, dataEncipherment
  extendedKeyUsage = serverAuth
  subjectAltName = @alt_names

[alt_names]
  DNS.1   = *.mydomain1.com.tw
  DNS.2   = *.mydomain2.com.tw
  IP.1    = 192.168.11.1
其中 alt_names 裡的 DNS 以及 IP 就輸入這個憑証欲綁定的 FQDN 或 IP。

2. 使用底下的命令建立 key 跟 certificate :
openssl req -x509 -nodes -days 365 -newkey rsa:4096 -keyout ./privatekey.key -out ./certificate.crt -extensions v3_req -config ./openssl.cnf
建立的 privatekey.key 跟 certificate.crt 是給開啟 https 服務的伺服器使用。

B. 將 certificate.crt 轉換成 DER 格式 (供 Android 安裝使用) 

openssl x509 -in ./certificate.crt -outform der -out ./certificate_der.crt
可將 certificate_der.crt 擺在網路上,讓 Android 裝置的瀏覽器將它下載到手機中,直接點擊下載的檔案就可以開始安裝憑証。
(或者也可以擺在自己的 http server 上,將 crt 的 MIME type 設定成 application/x-x509-ca-cert 讓瀏覽器直接安裝憑証,或是將 crt 的 MIME type 設定成 application/octet-stream 讓瀏覽器把它視為檔案下載。)

ref:https://developer.android.com/training/articles/security-ssl.html
ref:http://www-01.ibm.com/support/knowledgecenter/#!/SSZH4A_6.0.0/com.ibm.worklight.help.doc/admin/c_ssl_config.html
ref:http://www.xinotes.net/notes/note/1094/
ref:http://stackoverflow.com/questions/7229361/how-to-force-mime-type-of-file-download
ref:http://davdroid.bitfire.at/faq/entry/importing-a-certificate
ref:http://www.akadia.com/services/ssh_test_certificate.html