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

2014-06-25

Android webview 處理 Line it intent

最近有個需求是要在 webview 上處理 LINE it 按鈕的連結,這裡做個筆記 :

1. 使用 shouldOverrideUrlLoading 攔截轉跳的 url 連結。
2. 攔截 intent:// 開頭的 url
3. 此時應該會有一個 intent 看起來像這樣 :

intent://msg/text/DUMMY_MESSAGE#Intent;scheme=line;action=android.intent.action.VIEW;category=android.intent.category.BROWSABLE;package=jp.naver.line.android;end

4. 兩行搞定

Intent iuri =Intent.parseUri(url, 0);
startActivity(iuri);

2014-03-10

Android DexClassLoader


本文述敘 DexClassLoader 之使用方法 ,概略內容如下 :

範例程式將在執行時下載 dex 檔案並於載入後呼叫 dex 提供的函數來開啟新的 Activity,此 Activity 將會利用 Toast 顯示一段啟動成功的訊息。

1. 製作 dex 檔案 :
將底下程式碼另存成檔案 PayloadActivity.java :
package com.example;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Toast;

public class PayloadActivity extends Activity {
  public static void starter(Activity parentActivity){
    Intent tmp_intent =new Intent(parentActivity, PayloadActivity.class);
    parentActivity.startActivity(tmp_intent);
  }
  
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Toast.makeText(this, "Instantiate PayloadAcitivty Successful", Toast.LENGTH_LONG).show();
  }
}
PayloadActivity class 提供靜態函數 starter,此函數被呼叫之後會啟動 Payload Activity。

把 PayloadActivity.java 編譯成 dex 檔案 (Mac OSX 環境)  :
首先將 java 檔案編譯成 class 檔案 (其中 path_to_sdk 指向 android sdk 之路徑):
javac ./PayloadActivity.java -cp /path_to_sdk/adt-bundle-mac-x86_64-20130917/sdk/platforms/android-10/android.jar -d .
將 class 檔案壓縮成 PayloadActivity.jar 檔案 :
jar cvfM ./PayloadActivity.jar com/
刪除執行編譯指令時產生的 com 資料夾 :
rm -rf ./com
將 jar 檔案轉成 dex 檔案 :
/path_to_sdk/adt-bundle-mac-x86_64-20130917/sdk/build-tools/android-4.3/dx --dex --output=PayloadActivity.dex.jar ./PayloadActivity.jar

將完成的 PayloadActivity.dex.jar 擺在網路空間。 ( 此範例假設可利用 url http://localhost/PayloadActivity.dex.jar 取得檔案 )

2. 程式執行時期下載 dex 檔案 :
 建立一個 com.example.dexclassloader 之 Android Application Project

主要啟動的 Activity 設定為 MainActivity

在 Eclipse 裡新創 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 dalvik.system.DexClassLoader;

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 {
 
 public static ClassLoader payloadClassLoader =null;
 private Smith<ClassLoader> sClassLoader =null;

 @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");
    
    //override class loader
    overrideClassLoader();
    
    //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 overrideClassLoader(){
  try{
         Context mBase =new Smith<Context>(this, "mBase").get();
         Object mPackageInfo =new Smith<Object>(mBase, "mPackageInfo").get();
         sClassLoader =new Smith<ClassLoader>(mPackageInfo, "mClassLoader");
 
         ClassLoader mClassLoader =sClassLoader.get();
 
         MyClassLoader cl =new MyClassLoader(mClassLoader);
         sClassLoader.set(cl);
  }catch(Exception e){
   
  }
 }
 
 private void loadAndInvokeMethod(){
  try{
         String libPath =getFilesDir()+"/PayloadActivity.dex.jar";
         File optimizedDexOutputPath = getDir("outdex", Context.MODE_PRIVATE);
         DexClassLoader dcl=new DexClassLoader(libPath, optimizedDexOutputPath.getAbsolutePath(), null, getClass().getClassLoader());
         payloadClassLoader =dcl;
 
         //LOAD CLASS
         Class<Object> payloadClass =(Class<Object>)dcl.loadClass("com.example.PayloadActivity");
         
         //LOAD METHOD
         Method starterFunc =payloadClass.getMethod("starter", Activity.class);
         
         //INVOKE METHOD
         starterFunc.invoke(null, this);
         
  }catch(Exception e){
   
  }
 }

}

觀察 MainActivity 的 onCreate 函數可知程式啟動時將從 url ( http://localhost/Payload.dex.jar ) 下載檔案 (downloadDexFile 函數),並將檔案暫存在裝置記憶體裡。

完成後使用 DexClassLoader 載入 dex 檔案並執行檔案裡提供的 starter 函數 (loadAndInvokeMethod 函數)。
下一節解說函數 overrideClassLoader 的功能。

3. 從 dex 檔案啟動 Activity (修改 ClassLoader) :

藉由 DexClassLoader 載入的程式在啟動 Activity 時 (Activity Class) 不會搜尋 DexClassLoader 所載入的 dex 檔案,因此會出現找不到 Class 的 Exception。為解決這個問題,透過修改 ClassLoader ,我們可以讓程式先搜尋 dex 檔案,若找不到目標則再搜尋預設的 jar 檔案。

底下是覆寫 loadClass 函數部分的程式碼 :

package com.example.dexclassloader;

public class MyClassLoader extends ClassLoader {
 public MyClassLoader(ClassLoader parent){
  super(parent);
 }
 
 @Override
 public Class<?> loadClass(String className) throws ClassNotFoundException{
  if (MainActivity.payloadClassLoader !=null){
   try{
                Class<?> c = MainActivity.payloadClassLoader.loadClass(className);
                if (c != null){
                  return c;
                }
   } catch (ClassNotFoundException e) {
   }
  }
  
  return super.loadClass(className);
 }
}

此外 overrideClassLoader 函數中的 Smith Class 可以幫助我們取得 ClassLoader,其程式碼可以參考這裡

4. AndroidManifest.xml

雖然可以透過 dex 檔案更新並且啟動 activity,但注意的是此 activity 在安裝 apk 之前,必須先定義在 apk 的 AndroidManifest.xml 檔案裡,AndroidManifest.xml 檔案內容如下 :
<?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" >
        <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://stackoverflow.com/questions/6857807/is-it-possible-to-dynamically-load-a-library-at-runtime-from-an-android-applicat
ref:http://stackoverflow.com/questions/2519760/android-how-to-use-dexclassloader-to-dynamically-replace-an-activity-or-service


2014-01-17

install build-essential


輸入底下指令即可 :
sudo apt-get install build-essential

底下是在沒有網路情況下的安裝步驟 :
1. 取得 deb 檔案 :
sudo apt-get -qq --print-uris install build-essential linux-headers-$(uname -r) | cut -d\' -f 2 > urls.txt

2. 將 urls.txt 檔案複製到有網路的裝置,輸入底下指令取得所有 deb 檔案 :
wget < urls.txt

3. 將所有 deb 檔案複製到原裝置的 apt-get 暫存資料夾 (假如是利用USB隨身碟) :
sudo cp /media/USERNAME/DRIVER_NAME/* /var/cache/apt/archives/

4. 安裝 build-essential :
sudo apt-get install build-essential linux-headers-$(uname -r)

ref:
http://askubuntu.com/questions/334136/how-do-i-install-build-essential-without-an-internet-connection