分類
Android Uncategorized

Android 存取 全域資料 (access global data in android application)

建立 Application 的子類別,並將其設計成類似於 Singleton Pattern
全域資料放到這個子類別中,並提供存取方法。

package com.foxx.utility;
import android.app.Application;
public class ApplicationEx extends Application
{
    private static ApplicationEx sUniqueInstance;
    public static ApplicationEx getInstance()
    {
        return sUniqueInstance;
    }
    @Override
    public void onCreate()
    {
        super.onCreate();
        sUniqueInstance = this;
        sUniqueInstance.initData();
    }
    private void initData()
    {
        //init some data here
    }
}

第14行複寫 Application 的 onCreate 方法,該方法的 comment 敘述了幾個重點

/**
     * Called when the application is starting, before any activity, service,
     * or receiver objects (excluding content providers) have been created.
     * Implementations should be as quick as possible (for example using
     * lazy initialization of state) since the time spent in this function
     * directly impacts the performance of starting the first activity,
     * service, or receiver in a process.
     * If you override this method, be sure to call super.onCreate().
     */
    public void onCreate() {
    }

1.onCreate()會在所有 activity , service , receiver 建立之前被呼叫。

2.onCreate()會影響啟動第一個元件的時間。

3.onCreate()記得呼叫 super.onCreate()。

4.記得在AndroidManifest.xml 指定使用 Application name

<application
    ...
    android:name="com.foxx.utility.ApplicationEx"
    ...
>

顯而易見的好處是當你需要 context 的實體完成一些工作(如 getResource()),可以直接呼叫 getInstance().getResources() 來完成。
不需要 Context 當作參數傳來傳去。
使用範例

ApplicationEx.getInstance().getResources();
ApplicationEx.getInstance().getFilesDir();

 
Note 1:
ApplicationEx 並不需要私有化其建構式,e.g.

private ApplicationEx(){
...
}

如果多做這一步反而會產生 IllegalAccessException: access to constructor not allowed 的異常。
因為使用 static 保證只有一個實體被建立,因此這種方法可以確保正確同步化。
Note 2:
經測試無法當做建立Dialog的參數用,e.g.

new Dialog(ApplicationEx.getInstance());
new Dialog(ApplicationEx.getInstance().getApplicationContext());

Dialog建構式的參數必須為 Activity 的 Context
 

分類
Design Pattern

Singleton Pattern (單例模式)

Singleton Pattern :

限制類別只能產生一個實體並提供全域存取方法。


 
Singleton Pattern 大致上可以分為 2 種形式,飢餓型以及懶人型,而不論哪種形式都相當簡單。
飢餓型,在第3行宣告 sUniqueInstance 變數立即產生實體。

public class HungerSingleton
{
    private static HungerSingleton sUniqueInstance = new HungerSingleton();
    private HungerSingleton() {
    }
    public static HungerSingleton getInstance()
    {
        return sUniqueInstance;
    }
}

 


 
懶人型,在第3行宣告 sUniqueInstance 變數並不馬上建立實體,而是等到需要實體才建立(第12行)。

public class LazySingleton
{
    private static LazySingleton sUniqueInstance;
    private LazySingleton(){
    }
    public static LazySingleton getInstance()
    {
        if (sUniqueInstance == null) {
            sUniqueInstance = new LazySingleton();
        }
        return sUniqueInstance;
    }
}

 
使用 Singleton Pattern 需要注意的是多執行緒的問題。
因為使用靜態初始化的關係,HungerSingleton 可以確保只產生一個 instance。
LazySingleton 由於使用判斷 null 值來產生實體,當 2 條執行緒同時執行到第11,12行,就有可能產生 2 個 Singleton ,就不是單例了。
最簡單的解決方式可以加上 synchronized 來限制同時只能有一條執行緒進入 getInstance 方法 如下

    public static synchronized LazySingleton getInstance()
    {
        if (sUniqueInstance == null) {
            sUniqueInstance = new LazySingleton();
        }
        return sUniqueInstance;
    }

另一種比較麻煩的解決方法為DCL(double checked locking), 但這種解法有個限制就是只能支援 jvm 1.5之後的版本。

public class DCLSingleton {
    private static volatile DCLSingleton sUniqueInstance;
    private static Object mLock = new Object();
    public static DCLSingleton getInstance() {
        if (sUniqueInstance == null) {
            synchronized (mLock) {
                if (sUniqueInstance == null) {
                    sUniqueInstance = new DCLSingleton();
                }
            }
        }
        return sUniqueInstance;
    }
}

如果情況予許還是選擇 HungerSingleton 來避免可能產生多個實體的情況,但 HungerSingleton 有另外一個缺點。
因為沒有實現 lazy load , 若其本身佔用大量記憶體,會在程序開始的時候造成浪費記憶體的情況。
可以使用靜態內部類別(static inner class)來實現 lazy load + HungerSingleton

public class InnerStaticHungerSingleton
{
    private InnerStaticHungerSingleton()
    {
      //Prevents utility class being instantiated
    }
    private static class InnerInstance{
        private static final InnerStaticHungerSingleton UNIQUE_INSTANCE = new InnerStaticHungerSingleton();
    }
    public static InnerStaticHungerSingleton getInstance()
    {
        return InnerInstance.UNIQUE_INSTANCE;
    }
}

 


另外 LazySingleton 實現了 lazy load 作法。也就是為了節省記憶體使用空間,只有在變數需要實體才產生。
除了 lazy load 可以節省記憶體之外,還有另一個技術 cache 可以達到相同的效果。
simple cache in java

public class SimpleCache {
    private Map<String, Object> mCache = new HashMap<String, Object>();
    private static final String KEY_OF_DATA = "keyOfData";
    public Object getCache() {
        Object data = mCache.get(KEY_OF_DATA);
        if (data == null) {
            data = new Object();
            mCache.put(KEY_OF_DATA, data);
        }
        return data;
    }
}

簡單的 cache 實作,主要使用 Map 來達成,第一次呼叫 getCache 會進入第10行,並把 data 存入 map 中,隨後需要使用的時候再從 map 取出即可。
不需要重複產生實體,其實再複雜一點的變化型就是 Flyweight Pattern(享元模式),其作用也是解決需要大量細微性物件時,記憶體不夠的狀態。
 
 
 
 
 
 

分類
Refactoring

重構專案紀錄(TwoFlashLight)(part4) : Refactor class(MainActivity.java)

(Source Code: https://github.com/testfoxx/TwoFlashLight.git)
完成 FP 類別的重構後,只剩下 MainActivity,開始修改它吧

Remove useless comment

首先把註解的程式碼移除,移除前 MainActivity 的總行數為 558 行,移除後MainActivity的總行數只剩 282 行。
為了確保沒有移除到不應該移除的部份,先跑 UnitTest 看看,結果如下。

test:
[getlibpath] Library dependencies:
[getlibpath] No Libraries
     [echo] Running tests...
     [echo] Running tests ...
     [exec]
     [exec] com.twoflashlight.test.UnitTest:..
     [exec] Test results for InstrumentationTestRunner=..
     [exec] Time: 2.222
     [exec]
     [exec] OK (2 tests)
...
...
BUILD SUCCESSFUL

成功通過測試,把本次的修改 merge 到遠端 Server 吧。
也許會覺得這次的修改部份不多,可以一併重構其他部份再merge,但使用版本控制就是盡量保持小部份但穩定的merge。
好處是萬一日後真的出現 bug,尋找 commit 可以切割的比較乾淨。
接著來看看變數的命名和存取權限部份。
 

Rename variable

重構前

...
public class MainActivity extends Activity
{
    static Camera c;
    static Parameters p;
    public LayoutParams lp;
    public PowerManager powerm;
    public PowerManager.WakeLock WL;
    public static int width;
    public static int height;
    public int moveX;
    public int backLightIndex;
    public int moveY;
    public int frontLightIndex;
    AdView adview;
    int testnum;
    boolean isCameraUse;
...

從 static Camera c 開始,單一字母的變數命名方式很難看出該變數代表的意義,把 c 改為 mCamera。
變數名稱前綴加上m,代表該變數為類別成員。
看看 c 的存取範圍,c 只有被 MainActivity 內部存取,因此可以縮減存取範圍為 private。
相同的方式來處理 static Parameters p , 將 p 改為 private Parameters mParameters;
其中 AdView adview , int testnum ,boolean isCameraUse 這3個變數並沒有被引用,因此把他們刪除掉。
重構完,如下

...
public class MainActivity extends Activity
{
    private Camera mCamera;
    private Parameters mParameters;
    private LayoutParams mLayoutParams;
    private PowerManager mPowerManager;
    private PowerManager.WakeLock mWakeLock;
    private int mScreenWidth;
    private int mScreenHeight;
    private int mXCoordinatesOfScreen;
    private int mYCorrdinatesOfScreen;
    private int mBackLightIndex;
    private int mFrontLightIndex;
...

執行 UnitTest 看看測試結果。

test:
[getlibpath] Library dependencies:
[getlibpath] No Libraries
     [echo] Running tests...
     [echo] Running tests ...
     [exec]
     [exec] com.twoflashlight.test.UnitTest:.INSTRUMENTATION_RESULT: shortMsg=java.lang.RuntimeException
     [exec] INSTRUMENTATION_RESULT: longMsg=java.lang.RuntimeException: Fail to connect to camera service
     [exec] INSTRUMENTATION_CODE: 0
BUILD FAILED

發生 build failed , 觀察訊息看起來問題出在 RuntimeException : Fail to connect to camera service。
google 錯誤訊息 ,參考 http://stackoverflow.com/questions/23904459/android-java-lang-runtimeexception-fail-to-connect-to-camera-service
原因為當 Camera 使用後必須在 onPause()中 呼叫 release() 把資源釋放掉,最好是在 onResume()中 呼叫 open ()。
參考 http://developer.android.com/intl/zh-tw/reference/android/hardware/Camera.html#getNumberOfCameras()
了解原因之後就來修改 bug 吧,接下來先考慮甚麼時候要開啟 Camera , 根據 activity 的生命週期,
當 user 啟動 activity 或是暫停 activty 或是開啟另一個activity後,
除了 destroy 該 activity 之外,onResume() 一定都會被呼叫到,所以我們可以統一將初始化的動作放在 onResume()並針對不同狀態作處理。
加入 2 個方法如下,首先 initCamera 會初始化 mCamera 物件並設定其閃燈模式,第 12 行的 try catch 為了避免有些裝置沒有後置鏡頭其回傳值為 null 的情況。

    @Override
    protected void onResume()
    {
        super.onResume();
        initCamera();
        detBackLight();
        detFrontLight();
    }
    private void initCamera()
    {
        try {
            mCamera = Camera.open();
        } catch (RuntimeException e) {
            e.printStackTrace();
        }
        if (mCamera != null) {
            mParameters = mCamera.getParameters();
            mParameters.setFlashMode(Parameters.FLASH_MODE_TORCH);
            mCamera.setParameters(mParameters);
        }
    }

接著考慮甚麼時候需要把 camera release,雖然 android developer 建議在 onPause() 中 release , 但因為我們希望 camera 被移動到背景時也能正常工作。
因此 release 的時間點就在於 onDestroy() 方法中。
加入 onDestroy()

    @Override
    protected void onDestroy()
    {
        super.onPause();
        mCamera.release();
        mCamera = null;
    }

完成後,執行 UnitTest,結果如下

test:
[getlibpath] Library dependencies:
[getlibpath] No Libraries
     [echo] Running tests...
     [echo] Running tests ...
     [exec]
     [exec] com.twoflashlight.test.UnitTest:..
     [exec] Test results for InstrumentationTestRunner=..
     [exec] Time: 2.095
     [exec]
     [exec] OK (2 tests)
...
BUILD SUCCESSFUL

修正bug之後,針對方法來重構吧,首先從 showAdmob() 開始,
重構 showAdmob() 前,先來考慮 SRP 原則,只考慮 showAdmob 和 MainActivity 2者即可,
showAdmob() 主要有4個步驟
1.建立adView 物件
2.建立廣告layout
3.廣告layout加入adView物件
4.adview物件讀取廣告
如下

    private void showAdmob()
    {
        AdView adView = new AdView(this, AdSize.BANNER, "a150c81d1f5acca");
        LinearLayout layout = (LinearLayout) findViewById(R.id.AdLayout);
        layout.addView(adView);
        adView.loadAd(new AdRequest());
    }

 
照目前的設計,當 showAdmob()發生變化時,一定需要改動MainActivity,比如說 “a150c81d1f5acca” 需要改為別的號碼,或者想把 layout 改為別的顯示畫面。
這種高耦合的設計是違反 SRP 的。確實,顯示廣告是 MainActivity 的工作,但這不一定需要由 MainActivity 來完成。
我們可以建立另一個 AdmobManager,把顯示廣告的工作內容交給它,讓 MainActivity 簡單的呼叫 AdmobManager 的方法即可。
除了減低耦合以外,若日後有其他Activity 需要顯示廣告,只要呼叫 AdmobManager 即可,不必複製貼上同樣的內容,也可減少重複的程式碼。
Extract Class
建立 AdmobManager 並把 showAdmob() 搬移進來,AdmobManager 如下

package com.twoflashlight.utility;
import android.app.Activity;
import android.widget.LinearLayout;
import com.google.ads.AdRequest;
import com.google.ads.AdSize;
import com.google.ads.AdView;
import com.tylerfoxx.twoflash.R;
public class AdmobManager
{
    private static final String AD_UNIT_ID = "a150c81d1f5acca";
    public static void showAdmob(Activity activity)
    {
        AdView adView = new AdView(activity, AdSize.BANNER, AD_UNIT_ID);
        LinearLayout layout = (LinearLayout) activity.findViewById(R.id.AdLayout);
        layout.addView(adView);
        adView.loadAd(new AdRequest());
    }
}

在 MainActivity 的 init() 改為呼叫 AdmobManager.showAdmob(),如下

void init()
{
    ...
    AdmobManager.showAdmob(this);
    ...
}

完成後,執行UnitTest,結果如下

test:
[getlibpath] Library dependencies:
[getlibpath] No Libraries
     [echo] Running tests...
     [echo] Running tests ...
     [exec]
     [exec] com.twoflashlight.test.UnitTest:..
     [exec] Test results for InstrumentationTestRunner=..
     [exec] Time: 2.563
     [exec]
     [exec] OK (2 tests)
...
BUILD SUCCESSFUL

成功通過測試。
接著重構 init() ,首先修改存取權限為 private ,接著把方法重構為以下內容,其中 ScreenWakeLockManager 為延續 AdmobManager 的作法,
把 WakeLock 的實作內容提取到 ScreenWakeLockManager 。

    private void init()
    {
        countBackLightIndex();
        countFrontLightIndex();
        AdmobManager.showAdmob(this);
        ScreenWakeLockManager.makeScreenWakeLock(this);
        setScreenBrightness(0.1f);
    }
    private void countFrontLightIndex()
    {
        mScreenHeight = (getWindowManager().getDefaultDisplay().getHeight());
        mYCorrdinatesOfScreen = mScreenHeight / 2;
        mFrontLightIndex = 5;
    }
    private void countBackLightIndex()
    {
        mScreenWidth = (getWindowManager().getDefaultDisplay().getWidth());
        mXCoordinatesOfScreen = mScreenWidth / 2;
        mBackLightIndex = 1;
    }
    private void setScreenBrightness(float brightness)
    {
        mLayoutParams = getWindow().getAttributes();
        mLayoutParams.screenBrightness = brightness;
        getWindow().setAttributes(mLayoutParams);
    }

ScreenWakeLockManager 如下

package com.twoflashlight.utility;
import android.app.Activity;
import android.content.Context;
import android.os.PowerManager;
public class ScreenWakeLockManager
{
    private static final String DEBUG_TRACE_IDENTIFY = "ScreenWakeLockManager";
    public static void makeScreenWakeLock(Activity activity){
        PowerManager powerManager = (PowerManager) activity.getSystemService(Context.POWER_SERVICE);
        powerManager.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, DEBUG_TRACE_IDENTIFY).acquire();
    }
}

完成重構後,跑 UnitTest ,測試結果如下

test:
[getlibpath] Library dependencies:
[getlibpath] No Libraries
     [echo] Running tests...
     [echo] Running tests ...
     [exec]
     [exec] com.twoflashlight.test.UnitTest:..
     [exec] Test results for InstrumentationTestRunner=..
     [exec] Time: 3.595
     [exec]
     [exec] OK (2 tests)
...
BUILD SUCCESSFUL

成功通過。
接著是public void finishApp(), 重構後如下

    private void showExitDialog()
    {
        Builder dialog = new AlertDialog.Builder(MainActivity.this);
        dialog.setTitle("Warning");
        dialog.setMessage("Exit Application??");
        dialog.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which)
            {
                nmShow();
                MainActivity.this.finish();
            }
        });
        dialog.setNegativeButton("No", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which)
            {
            }
        });
        dialog.show();
    }

Run UnitTest!! e.g.

test:
[getlibpath] Library dependencies:
[getlibpath] No Libraries
     [echo] Running tests...
     [echo] Running tests ...
     [exec]
     [exec] com.twoflashlight.test.UnitTest:..
     [exec] Test results for InstrumentationTestRunner=..
     [exec] Time: 1.727
     [exec]
     [exec] OK (2 tests)
...
BUILD SUCCESSFUL

Success!!
接著是 void nmShow() , 重構後如下

private void showNotificationToTitleBar()
    {
        Intent intent = new Intent(this, MainActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        PendingIntent pendingIntent = PendingIntent.getActivity(MainActivity.this, 0, intent, 0);
        Notification notification = new Notification();
        notification.tickerText = "TwoFlashLight!!!";
        notification.defaults = Notification.DEFAULT_ALL;
        notification.setLatestEventInfo(MainActivity.this, "TwoFlashLight", "Welcome to use!!!", pendingIntent);
        NotificationManager notificatoinManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        notificatoinManager.notify(0, notification);
    }

run UnitTest

test:
[getlibpath] Library dependencies:
[getlibpath] No Libraries
     [echo] Running tests...
     [echo] Running tests ...
     [exec]
     [exec] com.twoflashlight.test.UnitTest:..
     [exec] Test results for InstrumentationTestRunner=..
     [exec] Time: 3.88
     [exec]
     [exec] OK (2 tests)
...
BUILD SUCCESSFUL

Success!!
接著是 public boolean onTouchEvent()
重構後,改變為3個方法,改變為3個方法的原因為增加可讀性以及保持方法的抽象層次。

    public boolean onTouchEvent(MotionEvent event)
    {
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                adjustLight(event);
                break;
        }
        return true;
    }
    private void adjustLight(MotionEvent event)
    {
        getCoordinatesOnScreen(event);
        detBackLight();
        detFrontLight();
    }
    private void getCoordinatesOnScreen(MotionEvent event)
    {
        mXCoordinatesOfScreen = (int) event.getX();
        mYCorrdinatesOfScreen = (int) event.getY();
    }

run UnitTest

test:
[getlibpath] Library dependencies:
[getlibpath] No Libraries
     [echo] Running tests...
     [echo] Running tests ...
     [exec]
     [exec] com.twoflashlight.test.UnitTest:..
     [exec] Test results for InstrumentationTestRunner=..
     [exec] Time: 2.689
     [exec]
     [exec] OK (2 tests)
...
BUILD SUCCESSFUL

重構public boolean onKeyDone()
重構變動的行數不多,主要是把 magic number 改為可讀性高的 KeyEvent.KEYCODE_BACK

    @Override
    public boolean onKeyDown(int keycode, KeyEvent event)
    {
        super.onKeyDown(keycode, event);
        TraceLogger.print("keycode:" + keycode);
        switch (keycode) {
            case KeyEvent.KEYCODE_BACK:
                showExitDialog();
                return true;
        }
        return false;
    }

由於變動到按鍵功能,必須增加按下 back key 的測項到 UnitTest 中。
但這時候出現另一個問題,要如何測試 dialog?
按下 back key 會呼叫 showExitDialog() 進而顯示 dialog , 但我們必須按下 dialog 上的按鈕(Yes / No),才有辦法確認功能是否正常。
但因為 dialog 是私有方法的區域變數,除非修改 MainActivity 讓 dialog 可以存取,否則無法在外部取得該 dialog。
這裡推薦一個好用的開源測試工具,Robotium。它把複雜測試指令包裝起來方便讓人呼叫,以本次的測試為例。
當按下 back key 之後,出現 dialog,可以輸入有關按鈕的文字,讓Robotium自動去尋找符合的元件,如下
第3行送出 back key 指令以顯示dialog
第4行尋找含有 “Warning” 的元件(dialog)
第5行點擊含有 “No” 的元件(No button)
即可測試按下 no 的功能是否正常。

    private void testExitDialogFunction()
    {
        mMainActivity.sendKey(KeyEvent.KEYCODE_BACK);
        mMainActivity.searchText("Warning");
        mMainActivity.clickLongOnText("No");
        ...
    }

最後修改 UnitTest 如下,也加入模擬改變亮度測項(testChangeLightFunction)。

package com.twoflashlight.test;
import android.test.ActivityInstrumentationTestCase2;
import android.view.KeyEvent;
import com.robotium.solo.Solo;
import com.twoflashlight.main.MainActivity;
public class UnitTest extends ActivityInstrumentationTestCase2<MainActivity>
{
    private static final int MOVE_STEP_ON_SCREEN = 20;
    private Solo mMainActivity;
    public UnitTest() {
        super(MainActivity.class);
    }
    @Override
    protected void setUp() throws Exception
    {
        super.setUp();
        mMainActivity = new Solo(getInstrumentation(), getActivity());
    }
    private void testChangeLightFunction()
    {
        testChangeBackLightToMin();
        testChangeFrontLightToMax();
        testChangeBackLightToMax();
        testChangeFrontLightToMin();
    }
    private void testChangeBackLightToMax()
    {
        float startX = 0;
        float endX = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay()
                .getWidth();
        float startY = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay()
                .getHeight() / 2;
        float endY = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay()
                .getHeight() / 2;
        mMainActivity.drag(startX, endX, startY, endY, MOVE_STEP_ON_SCREEN);
    }
    private void testChangeBackLightToMin()
    {
        float startX = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay()
                .getWidth();
        float endX = 0;
        float startY = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay()
                .getHeight() / 2;
        float endY = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay()
                .getHeight() / 2;
        mMainActivity.drag(startX, endX, startY, endY, MOVE_STEP_ON_SCREEN);
    }
    private void testChangeFrontLightToMax()
    {
        float startY = 0;
        float endY = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay()
                .getHeight();
        float startX = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay()
                .getWidth() / 2;
        float endX = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay()
                .getWidth() / 2;
        mMainActivity.drag(startX, endX, startY, endY, MOVE_STEP_ON_SCREEN);
    }
    private void testChangeFrontLightToMin()
    {
        float endY = 0;
        float startY = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay()
                .getHeight();
        float startX = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay()
                .getWidth() / 2;
        float endX = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay()
                .getWidth() / 2;
        mMainActivity.drag(startX, endX, startY, endY, MOVE_STEP_ON_SCREEN);
    }
    private void testExitDialogFunction()
    {
        mMainActivity.sendKey(KeyEvent.KEYCODE_BACK);
        mMainActivity.searchText("Warning");
        mMainActivity.clickLongOnText("No");
        mMainActivity.sendKey(KeyEvent.KEYCODE_BACK);
        mMainActivity.searchText("Warning");
        mMainActivity.clickLongOnText("Yes");
    }
    public void testMainActivityFunction()
    {
        testChangeLightFunction();
        testExitDialogFunction();
        assertNotNull(mMainActivity);
    }
}

完成後執行 UnitTest,

test:
[getlibpath] Library dependencies:
[getlibpath] No Libraries
     [echo] Running tests...
     [echo] Running tests ...
     [exec]
     [exec] com.twoflashlight.test.UnitTest:.
     [exec] Test results for InstrumentationTestRunner=.
     [exec] Time: 16.019
     [exec]
     [exec] OK (1 test)
...
BUILD SUCCESSFUL

接著看看 UnitTest 的覆蓋率。

[EMMA v2.0.5312 report, generated Thu Oct 22 13:31:18 CST 2015]
-------------------------------------------------------------------------------
OVERALL COVERAGE SUMMARY:
[class, %]	[method, %]	[block, %]	[line, %]	[name]
100% (6/6)	86%  (24/28)	95%  (476/500)	91%  (112/123)	all classes
OVERALL STATS SUMMARY:
total packages:	2
total classes:	6
total methods:	28
total executable files:	4
total executable lines:	123
COVERAGE BREAKDOWN BY PACKAGE:
[class, %]	[method, %]	[block, %]	[line, %]	[name]
100% (3/3)	50%  (3/6)!	80%  (37/46)	77%  (10/13)!	com.twoflashlight.utility
100% (3/3)	95%  (21/22)	97%  (439/454)	93%  (102/110)	com.twoflashlight.main
-------------------------------------------------------------------------------

class 的覆蓋率 100% ,方法覆蓋率 86%,方法的覆蓋率包含類別建構式,在 com.twoflashlight.utility 內的 class 都是僅只有 static method , 不必產生其類別實體即可呼叫。
接著只剩下 detBackLight 和 detFrontLight 這2個方法等待重構。
準備重構 detBackLight 和 detFrontLight,首先更改2個方法的名稱為 changeBackLight 和 changeFrontLight 比較適合方法的意圖。
接著根據方法內容提取出不同功能的部份以 changeFrontLight 為例,
在方法內部其實可以再分為2個部份為
1.計算 front light 索引
2.設定螢幕亮度
於是我們再提取出這兩個方法如下。

    private void changeFrontLight()
    {
        calculateFrontLightIndex();
        setScreenBrightness();
    }

2個方法內容為

    private void calculateFrontLightIndex()
    {
        ArrayList<Integer> screenHeightQuarterIndex = new ArrayList<Integer>();
        int screenHeightQuarterMax = 10;
        for (int i = 0; i < screenHeightQuarterMax; i++) {
            screenHeightQuarterIndex.add(mScreenHeight * i / screenHeightQuarterMax);
        }
        for (int i = 0; i < screenHeightQuarterIndex.size() - 1; i++) {
            if (mYCorrdinatesOfScreen >= screenHeightQuarterIndex.get(i)
                    && mYCorrdinatesOfScreen <= screenHeightQuarterIndex.get(i + 1)) {
                mFrontLightIndex = i;
            }
        }
        mLayoutParams.screenBrightness = (float) mFrontLightIndex / screenHeightQuarterMax;
    }
    private void setScreenBrightness()
    {
        float screenBrightnessMin = 0.1f;
        if (mLayoutParams.screenBrightness <= screenBrightnessMin) {
            mLayoutParams.screenBrightness = screenBrightnessMin;
        }
        getWindow().setAttributes(mLayoutParams);
    }

接著使用同樣的方式重構 changeBackLight()
重構 changeBackLight 完成如下

    private void changeBackLight()
    {
        calculateBackLightIndex();
        setBackLightFlashMode();
    }
    private void setBackLightFlashMode()
    {
        if (mParameters != null) {
            if (mBackLightIndex == BackLightIndex.OFF.getIndex()) {
                mParameters.setFlashMode(Parameters.FLASH_MODE_OFF);
            } else if (mBackLightIndex == BackLightIndex.MACRO.getIndex()) {
                mParameters.setFocusMode("macro");
                mParameters.setFlashMode(Parameters.FLASH_MODE_TORCH);
            } else if (mBackLightIndex == BackLightIndex.AUTO.getIndex()) {
                mParameters.setFocusMode("auto");
                mParameters.setFlashMode(Parameters.FLASH_MODE_TORCH);
            }
            try {
                mCamera.setParameters(mParameters);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    private void calculateBackLightIndex()
    {
        ArrayList<Integer> screenWidthQuarterIndex = new ArrayList<Integer>();
        int screenWidthQuarterMax = 4;
        for (int i = 0; i < screenWidthQuarterMax; ++i) {
            screenWidthQuarterIndex.add(mScreenWidth * i / screenWidthQuarterMax - 1);
        }
        for (int i = 0; i < screenWidthQuarterIndex.size() - 1; ++i) {
            if (mXCoordinatesOfScreen >= screenWidthQuarterIndex.get(i)
                    && mXCoordinatesOfScreen <= screenWidthQuarterIndex.get(i + 1)) {
                mBackLightIndex = i;
            }
        }
    }

完成後一樣需要 run UnitTest

test:
[getlibpath] Library dependencies:
[getlibpath] No Libraries
     [echo] Running tests...
     [echo] Running tests ...
     [exec]
     [exec] com.twoflashlight.test.UnitTest:.
     [exec] Test results for InstrumentationTestRunner=.
     [exec] Time: 15.171
     [exec]
     [exec] OK (1 test)
...
BUILD SUCCESSFUL

接著進行本地化資源的動作,如果將 UI 上的顯示文字寫死在 java code 中,就無法針對不同的國家顯示不同的文字。
因此我們必須對資源進行本地化。android 系統預設會到 res/values/strings.xml 中尋找字串資源,預設的strings.xml如下

<resources>
    <string name="app_name">TwoFlashLight</string>
    <string name="menu_settings">Settings</string>
    <string name="title_activity_main">Two Flash Light</string>
    <string name="operate_hint">Drag around the screen , adjust the brightness of the back\n\n
	    Drag up or down the screen , adjust the front luminance\n\nPress the Home key for background use\n\n
	    please press the back key to close app</string>
    <string name="exit_dialog_title">Warning!!</string>
    <string name="exit_dialog_message">Exit Application??</string>
    <string name="exit_dialog_yes">Yes</string>
    <string name="exit_dialog_no">No</string>
    <string name="notification_text">TwoFlashLight!!!</string>
    <string name="notification_welcomeUse">Welcome to Use!!!</string>
</resources>

 
若需要針對不同國家進行資源的設定必須在 res/ 資料夾下建立相對應的資料夾,比如我們想針對繁體字串資源的設定就建立
res/values-zh-rTW 資料夾,並在資料夾中新建 strings.xml 如下,

<resources>
    <string name="app_name">雙面手電筒</string>
    <string name="menu_settings">設定</string>
    <string name="title_activity_main">雙面手電筒</string>
    <string name="operate_hint">左右拖曳畫面,調整背面亮度\n\n上下拖曳畫面,調整正面亮度\n\n背景使用請按Home鍵\n\n關閉程式請按返回鍵</string>
    <string name="exit_dialog_title">警告!!</string>
    <string name="exit_dialog_message">離開應用程式??</string>
    <string name="exit_dialog_yes">是</string>
    <string name="exit_dialog_no">否</string>
    <string name="notification_text">雙面手電筒!!!</string>
    <string name="notification_welcomeUse">歡迎使用!!!</string>
</resources>

完成後也必須修改 UnitTest,只要修改 testExitDialogFunction()即可

    private void testExitDialogFunction()
    {
        mMainActivity.sendKey(KeyEvent.KEYCODE_BACK);
        mMainActivity.searchText(mMainActivity.getString(R.string.exit_dialog_title));
        mMainActivity.clickLongOnText(mMainActivity.getString(R.string.exit_dialog_no));
        mMainActivity.sendKey(KeyEvent.KEYCODE_BACK);
        mMainActivity.searchText(mMainActivity.getString(R.string.exit_dialog_title));
        mMainActivity.clickLongOnText(mMainActivity.getString(R.string.exit_dialog_yes));
    }

因為已經針對不同的語系設定不同的資源,所以測項也不可寫死在 java code。
Run UnitTest!!

test:
[getlibpath] Library dependencies:
[getlibpath] No Libraries
     [echo] Running tests...
     [echo] Running tests ...
     [exec]
     [exec] com.twoflashlight.test.UnitTest:.
     [exec] Test results for InstrumentationTestRunner=.
     [exec] Time: 15.668
     [exec]
     [exec] OK (1 test)
...
BUILD SUCCESSFUL

 
 

分類
Refactoring

重構專案紀錄(TwoFlashLight)(part3) : Refactor class(FP.java)

(Source Code: https://github.com/testfoxx/TwoFlashLight.git)
去除掉沒有使用的類別之後,我們把目標轉向 FP.java
首先觀察類別名稱,FP 沒有傳達出任何有關這個類別的意義,於是再看看 FP 的內容。
發現有個 TLog 的常數以及 p 的靜態方法,先從 TLog 開始,
追蹤 TLog 的引用位置,發現只有在 p 的方法中有被引用,所以修改其存取範圍為 private , 看起來它似乎只是被拿來當識別用。
於是修改 TLog 的名稱為 LOG_IDENTIFY。
接著 p 方法,其引用位置在 MainActivity 的 detBlackLight , detFrontLight , onKeyDown的三個方法中。
它接受一個參數並呼叫 Log.v 並把其參數內容列印出來,看起來是當作列印訊息用。因此可以把 p 修改為 print 。
最後看起來 FP 的用途就是追蹤並列印訊息,所以把 FP 改為 TraceLogger 。
重構前

package com.twoflashlight.utility;
import android.util.Log;
public class FP {
	public static final String TLog = "Trace Log";
	public static void p(String msg){
		Log.v(TLog, msg);
	}
}

重構後

package com.twoflashlight.utility;
import android.util.Log;
public class TraceLogger {
	private static final String LOG_IDENTIFY = "Trace Log";
	public static void print(String msg) {
		Log.v(LOG_IDENTIFY, msg);
	}
}

重構應用的技術相當簡單, 主要是rename class,rename variable,rename method,雖然簡單卻可以提昇整個類別的可讀性。
因為重構改變 FP 的內部功能,所以我們必須增加新的測項到 UnitTest。
在 UnitTest.java 加入新的測項如下:

public void testTraceLoggerPrint()
{
    TraceLogger.print("Test message from UnitTest");
}

由於 TraceLogger 的 print 方法沒有回傳值,也只能對其方法進行呼叫測試。
在 local 端執行 UnitTest 。執行結果如下

test:
[getlibpath] Library dependencies:
[getlibpath] No Libraries
     [echo] Running tests...
     [echo] Running tests ...
     [exec]
     [exec] com.twoflashlight.test.UnitTest:..
     [exec] Test results for InstrumentationTestRunner=..
     [exec] Time: 1.623
     [exec]
     [exec] OK (2 tests)
...

由於這次加入 testTraceLoggerprint() 測項,因此在 [exec] OK 有2個 test 通過。
完成 local 端的測試,後即可 merge 到遠端 Server。
 
refactoring part4

分類
Refactoring

重構專案紀錄(TwoFlashLight)(part2) : Remove unuse class

(Source Code: https://github.com/testfoxx/TwoFlashLight.git)
Remove unuse class
專案中的類別都需要維護的成本,沒有被引用的類別除了會增加維護的時間和人力以外,也很容易會造成 bug 的源頭,因為你不知道到底需不需要他們。
不過現在有了 version control ,你不需要擔心日後找不到他們。所以放心刪除吧。
尋找 unuse class的方法有很多,當類別不多時可以考慮一個一個找,當然也有一堆套件方便工作。
首先你可以藉由 ide 幫你找,使用 eclipse 在類別的名稱上按下 ctrl + alt + h , 或是點擊滑鼠右鍵選擇 open call hierarchy,都會幫你列出該類別被引用的位置。
但是類別中如果有 static 方法,因為 static 方法不需要建立類別實體就可呼叫。因此也需要對 static 方法實施上述的工作。
另一個快速的方式可以直接刪除或改名該類別,看看其他類別是否有出現error的狀況。
重構步驟為:
首先對 com.twoflashlight.utility 內的類別進行測試,除了 FP 這個類別以外其他的類別都沒有被引用,只留下 FP 並刪除其他類別。
接著對 com.twoflashlight.main 進行測試,除了 MainActivity 這個類別以外其他的類別也都沒有被引用,刪除其他類別。
刪除掉沒有使用的類別後,TwoFlashLight 只剩下2個類別。
最後在 local 端執行 UnitTest,結果如下

test:
[getlibpath] Library dependencies:
[getlibpath] No Libraries
     [echo] Running tests...
     [echo] Running tests ...
     [exec]
     [exec] com.twoflashlight.test.UnitTest:.
     [exec] Test results for InstrumentationTestRunner=.
     [exec] Time: 7.231
     [exec]
     [exec] OK (1 test)
...

通過 local 端 UnitTest 後,即可 merge 到遠端 Server 上。
 
refactoring part3

分類
Refactoring

重構專案紀錄(TwoFlashLight)(part1) : Rename Package

(Source Code: https://github.com/testfoxx/TwoFlashLight.git)
先從程式碼的層面一步步開始重構吧,首先是 package 和 class 的名稱沒有符合 coding standard。
格式良好的 coding standard 會讓 code 看起來更簡潔更有規範,相對提高了可讀性。
 

Rename package

觀察 com/tylerfoxx/twoflash 中的 class , 其中 MainActivity 會被當作起始點啟動,所以先暫時把 package name 改為含有主要(main)的名稱。
之後重構分離 class 可以再建立其他的package 放入 class 也沒關係。將 package name 從 com/tylerfoxx/twoflash 改為 com/twoflashlight/main
觀察 FoXxLib 中的 class,發現 class 負責的工作似乎都不相關,看起來並沒有一定的關聯性,所以先改名為 utility 名稱。
將 package name 從 FoXxLib 改為 com/twoflashlight/utility。
完成修改後可先在 local 端啟動 UnitTest,避免讓本次重構產生其他的bug,確定 UnitTest 過關之後再 git merge 到遠端 Server (github)。
本地端啟動UnitTest的步驟為:
Step 1. 開啟終端機到TwoFlashLight/Two_FlashLight/test
Step 2. 確認已經有裝置連線後輸入 ant clean emma debug install test
Step 3. 最後在終端機會顯示這次的UnitTest狀態,如下

test:
[getlibpath] Library dependencies:
[getlibpath] No Libraries
     [echo] Running tests...
     [echo] Running tests ...
     [exec]
     [exec] com.twoflashlight.test.UnitTest:.
     [exec] Test results for InstrumentationTestRunner=.
     [exec] Time: 7.372
     [exec]
     [exec] OK (1 test)
     [exec]
     [exec]
...

結果代表本次的 UnitTest 成功通過。UnitTest 的類別位於 src/com/twoflashlight/test/UnitTest.java。
測試內容相當簡單,啟動MainActivity並測試其不為空值(null)。
當然,若重構的過程有修改到其他部份,也必須增加到 UnitTest 才行。
local 端測試 UnitTest 並成功通過後就可 git push 到遠端 Server上,再到 github 手動 merge 本次修改,每次完成 merge,jenkins 都會自動 build project。
除了即時通知本次的 merge 會不會造成 build fail , 也可以使用套件分析程式碼以了解專案的狀態。

分類
Refactoring

重構專案紀錄(TwoFlashLight)

Two Flash Light 是我在多年前寫的小型Android project,當初寫的時間很趕加上也沒什麼設計模式,原則和重構等等的概念。
因此無論是從具體的程式碼實作或是高層的抽象設計都是一塌糊塗。(不過它仍然活在google play上好好的)。
由於種種的缺陷因此把它拿來當作重構的目標,似乎再適合也不過。
接下來重構的過程都會一步一步紀錄在這裡,也會將它開源到github上。(https://github.com/testfoxx/TwoFlashLight.git)
確定重構前已經建立好一切的準備。包含version control tool(git) , CI Server(Jenkins)等等。
git clone 之後會是一個TwoFlashLight的資料夾,source code 的部份放在 Two_FlashLight 資料夾中。
相關的測試類別放在 TwoFlashLight/Two_FlashLight/test。
要說明的是重構前必須建立測試環境,以避免重構失敗所造成的影響。
使用工具為 android 的 InstrumentationTestRunner 搭配 jenkins emma plugin
emma plugin 可以讓你了解目前的測試覆蓋率有多少。每一次 merge code 都會更新覆蓋率。
 
OK~ 那麼就開始嘗試重構吧。
refactoring part1
refactoring part2
refactoring part3
refactoring part4
 
 
 
 
 

分類
Android Uncategorized

Android emulator Management

1.查詢可使用的android version
開啟Terminal並使用 android sdk 中的tools/android

android list targets

記下想使用的id即可
2.建立emulator,建立名稱為 id_14_android_19 的emulator,其使用的id為14

android create avd -n id_14_android_19 -t 14

3.啟動emulator,若os為32bit , 需要加上 -force-32bit

emulator -avd id_14_android_19

4.查詢系統中的emulator,emulator 的config 都會留存在~/.android/avd/ 中,只要查詢該資料夾即可知道有哪些emulator。

ls ~/.android/avd/

5.刪除emulator

android delete avd -n nameofemulator

 
詳細內容可參考:
http://developer.android.com/intl/zh-tw/tools/devices/managing-avds-cmdline.html
http://developer.android.com/intl/zh-tw/tools/devices/emulator.html
http://developer.android.com/intl/zh-tw/tools/help/emulator.html
 
 

分類
Android git 使用紀錄 Jenkins Uncategorized

合併 Jenkins 和 GitHub for Andriod Project

Jenkins 除了可以和 Local 端專案整合,也可和github一起工作。
這篇紀錄如何整合jenkins和github。
1.先在github上建立空專案。
2.在Build Server(jenkins),先 git clone github下來,並建立 android project,注意要使用 ant 成功建立專案(ant release or ant debug),
注意,不要加入local.properties 和 ant.properties,可以加入.gitignore來忽略。
完成後git push到github上
3.在jenkins建立專案並設定git path為github的路徑。
4.若有需要先在github上merge第3步驟的push commit,完成後jenkins 可以build成功。
其他詳細可參考 https://wiki.jenkins-ci.org/display/JENKINS/Building+an+Android+app+and+test+project

分類
gerrit

修改 gerrit 資料庫以更改使用者信件驗證,權限

Descriptoin:

註冊完成gerrit 帳號後,在預設的情況下必須透過 email 來驗證帳號,否則無法進行其他動作。
但若 server 本身並無 mail server 的功能,即無法發送接收信件,或是其他因素無法使用email,即無法驗證帳號。
 

Solution:

可修改 gerrit 使用的 database, 由於 database 存放所有 gerrit 使用的資料,不僅可以修改驗證還可以修改使用者權限等等。
但此方法必須具有登入並 root server 的權限才可。
 
Step 1.
登入 gerrit server 並停止 gerrit 服務。

sudo sh /your_gerrit_shell_path/gerrit.sh stop

 
Step 2.
利用 gerrit.war 執行 gsql 指令,以登入 sql。

java -jar gerrit.war gsql

如果出現以下error,必須注意先移動到相關 review_site 資料夾再執行 gerrit.war,

fatal: not a Gerrit site: '/xxx'
fatal: Perhaps you need to run init first?

以我為例,我的gerrit.war 是位於 review_site 的上一層資料夾,所以必須先移動到 review_site,再執行 gerrit.war

cd /xxx/review_site
sudo java -jar ../gerrit.war gsql

成功的話應該可以登入到 sql 中,接著準備修改相關資料

➜  review_site  sudo java -jar ../gerrit-2.1.8.war gsql
Welcome to Gerrit Code Review 2.1.8
(MySQL 5.5.38-0ubuntu0.12.04.1)
Type '\h' for help.  Type '\r' to clear the buffer.
gerrit>

Step 3.
由於資料表相當多,這邊只介紹如何手動加入驗證 email, 以及修改使用者權限,有興趣的話可以修改其它數值玩玩看。
手動加入驗證 email 必須修改 account_external_ids 資料表
使用 show databases; 查詢所有資料庫,使用 show tables;  查詢某資料庫中所有資料表。使用 use databasename; 選擇資料庫。

gerrit> show databases;
 Database
 ------------------
 information_schema
 reviewdb
(2 rows; 27 ms)
gerrit> use reviewdb;
UPDATE 0; 1 ms
gerrit> show tables;
 Tables_in_reviewdb
 ----------------------------
 account_agreements
 account_diff_preferences
 account_external_ids
 account_group_agreements
 account_group_id
 account_group_includes
 account_group_includes_audit
 account_group_members
 account_group_members_audit
 account_group_names
 account_groups
 account_id
 account_patch_reviews
 account_project_watches
 account_ssh_keys
 accounts
 approval_categories
 approval_category_values
 change_id
 change_message_id
 change_messages
 changes
 contributor_agreement_id
 contributor_agreements
 patch_comments
 patch_set_ancestors
 patch_set_approvals
 patch_sets
 projects
 ref_rights
 schema_version
 starred_changes
 system_config
 tracking_ids
(34 rows; 24 ms)
gerrit>

從上圖中可以看到 account_external_ids 資料表是位於 reviewdb 資料庫。

修改使用者驗證信箱

接著看看 account _external_ids 資料表的內容,我們的目標為修改 testuser,讓其驗證信箱為驗證完成狀態。

gerrit> select * from account_external_ids;
 account_id | email_address    | password | external_id
 -----------+------------------+----------+-----------------
 2          | NULL             | NULL     | gerrit:testuser
 1          | NULL             | NULL     | gerrit:xxx
 2          | NULL             | NULL     | username:testuser
 1          | NULL             | NULL     | username:xxx
(4 rows; 2 ms)

我們的目標就是修改 testuser 的 email_address , 讓它不可為NULL。
使用以下指令修改欄位值

gerrit> update account_external_ids set email_address='example@example' where account_id=2;
UPDATE 2; 5 ms

修改完成後再查看account_external_ids table

gerrit> select * from account_external_ids;
 account_id | email_address    | password | external_id
 -----------+------------------+----------+-----------------
 2          | example@example  | NULL     | gerrit:testuser
 1          | NULL             | NULL     | gerrit:xxx
 2          | example@example  | NULL     | username:testuser
 1          | NULL             | NULL     | username:xxx
(4 rows; 2 ms)

離開sql , 重啟 gerrit 看到修改後的結果。

gerrit> \q
Bye
sudo /etc/init.d/apache2 restart
sudo sh /your_gerrit_shell_path/gerrit.sh run

 

修改使用者權限為 admin

首先確認使用者的 account_id, 下表還有一些內容因為用不到所以忽略,只要找到 user_name 和所對應的 account_id 即可,可以發現 testuser 的 account_id 為 2。

gerrit> select * from accounts;
full_name      | account_id
---------------------------
| testuser     | 2
(2 rows; 2 ms)(2 rows; 2 ms)

查詢 admin 對應的 group_id,可以確認 admin 所屬的 group_id 為 1。

gerrit> select * from account_groups;
 name                  | owner_group_id | description                           | group_type | external_name | visible_to_all | email_only_authors | group_id
 ----------------------+----------------+-------------------------------------------+------------+---------------+----------------+--------------------+------
 Administrators        | 1              | Gerrit Site Administrators            | INTERNAL   | NULL          | N              | N                  | 1
 Anonymous Users       | 1              | Any user, signed-in or not            | SYSTEM     | NULL          | N              | N                  | 2
 Registered Users      | 1              | Any signed-in user                    | SYSTEM     | NULL          | N              | N                  | 3
(6 rows; 3 ms)

查詢 account_id 所屬的 group_id

gerrit> select * from account_group_members;
 account_id | group_id
 -----------+---------
 1          | 1
 2          | NULL

可以看到 account_id 為 2 的部份,其所屬的 group_id 為 NULL,所以只要把 NULL 修改為 1 ,就能讓 testuser 權限變更為 admin。
使用 update 來更改數值

gerrit> update account_group_members set group_id=1 where account_id=2;
UPDATE 1; 30 ms

重新檢查一次 account_group_members , 已經完成修改。

gerrit> select * from account_group_members;
 account_id | group_id
 -----------+---------
 1          | 1
 2          | 1
(2 rows; 1 ms)

離開 sql , 重啟 gerrit,進入 gerrit 頁面就可看到成果。

gerrit> \q
Bye
sudo /etc/init.d/apache2 restart
sudo sh /your_gerrit_shell_path/gerrit.sh run