Sean's Note: Android Studio
顯示具有 Android Studio 標籤的文章。 顯示所有文章
顯示具有 Android Studio 標籤的文章。 顯示所有文章

2016年6月22日 星期三

Unit Tests for Beginners

Unit Tests 和 UI Tests 不同,Unit Tests 著重於測試程式中的一小部分的邏輯,例如 Class 或是 Class 中的 methods。而 Unit Tests 又可分為:

  • Local tests
    本地端的測試,程式碼只會運行在 JVM 上,所以執行效率最佳。多用於測試與 Android framework 沒有相依性的程式碼,或是其相依能被 mock objects 所取代。(Robolectric 也可以解決這個問題,見 Ref 3)

  • Instrumented tests
    跑在實體機器或模擬器上的測試。這些測試需要取得裝置上的一些資訊,如 Context 物件,或有其他不容易被 mock objects 所取代的相依類別。

建立 Local Unit Test Class

在 Android 中,建立單元測試需要用到 JUnit 這個已在 Java 世界中被廣泛被使用的 Unit Testing Framework,目前最新的版本是 JUnit 4,JUnit 4 不像前一版 JUnit 3 需要讓 test classes繼承 junit.framework.TestCase,更不限制 method 的命名要以 "test" 為前綴字。

測試的程式碼需要放在路徑 src/test/java 之下。

範例如下:
import org.junit.Test;
import java.util.regex.Pattern;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

public class EmailValidatorTest {

    @Test
    public void emailValidator_CorrectEmailSimple_ReturnsTrue() {
        assertThat(EmailValidator.isValidEmail("name@email.com"), is(true));
    }
    ...
}

在 gradle.build 裡宣告:

dependencies {
    // Required -- JUnit 4 framework
    testCompile 'junit:junit:4.12'
    // Optional -- Mockito framework
    testCompile 'org.mockito:mockito-core:1.+'
}


建立 Instrumented Test Class

需要用到 JUnit 4 test runner,而且測試的程式碼需要放在路徑 src/androidTest/java 之下。
此外 AndroidJUnitRunner 已經用來取代舊的 InstrumentationTestRunner 和 MultiDexTestRunner(自己繼承 AndroidJUnitRunner 來實作 AndroidJUnitMultiDexRunner )。若需要 Context 的話,可以透過 InstrumentationRegistry.getContext() 來取得物件(不用再繼承 InstrumentationTestCase 了)。

範例如下:
import android.os.Parcel;
import android.support.test.runner.AndroidJUnit4;
import android.util.Pair;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.List;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;

@RunWith(AndroidJUnit4.class)
@SmallTest
public class LogHistoryAndroidUnitTest {

    public static final String TEST_STRING = "This is a string";
    public static final long TEST_LONG = 12345678L;
    private LogHistory mLogHistory;

    @Before
    public void createLogHistory() {
        mLogHistory = new LogHistory();
    }

    @Test
    public void logHistory_ParcelableWriteRead() {
        // Set up the Parcelable object to send and receive.
        mLogHistory.addEntry(TEST_STRING, TEST_LONG);

        // Write the data.
        Parcel parcel = Parcel.obtain();
        mLogHistory.writeToParcel(parcel, mLogHistory.describeContents());

        // After you're done with writing, you need to reset the parcel for reading.
        parcel.setDataPosition(0);

        // Read the data.
        LogHistory createdFromParcel = LogHistory.CREATOR.createFromParcel(parcel);
        List> createdFromParcelData = createdFromParcel.getData();

        // Verify that the received data is correct.
        assertThat(createdFromParcelData.size(), is(1));
        assertThat(createdFromParcelData.get(0).first, is(TEST_STRING));
        assertThat(createdFromParcelData.get(0).second, is(TEST_LONG));
    }
}

在 gradle.build 裡宣告: (Note: 這裡可能會遇到 support-annotations 版本衝突的問題)
dependencies {
    androidTestCompile 'com.android.support:support-annotations:23.0.1'
    androidTestCompile 'com.android.support.test:runner:0.4.1'
    androidTestCompile 'com.android.support.test:rules:0.4.1'
    // Optional -- Hamcrest library
    androidTestCompile 'org.hamcrest:hamcrest-library:1.3'
    // Optional -- UI testing with Espresso
    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1'
    // Optional -- UI testing with UI Automator
    androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.1'
}

Ref:

[Android Library][Open-source] Butternife

Butternife 這個超方便的 library,透過 annotation 的方法來綁定 fields 和 methods,
免去了大家大量使用 findViewById 的方法。(許多知名的 APP 也都有在用這個 Library)

本來 bind 一個 view 要寫這樣:
TextView title;
title= (TextView) activity.findViewById(R.id.title);

用了 Butternife 後只要寫這樣:
@BindView(R.id.title) TextView title;

除此之外,Butternife 還有更多的功能,用法可以參考 Butternife 的官網:
http://jakewharton.github.io/butterknife

如果連 @BindView(R.id.XXX) 都懶得寫,可去下載 Android Studio 的這套 plugin:
ButterKnifeZelezny
https://github.com/avast/android-butterknife-zelezny

安裝完後,只要對 R.layout.XXX 按右鍵 -> Generate... -> Generate Butternife Injections 就可以
選擇要自動產生的 Bind codes 啦!

圖片來源: https://github.com/avast/android-butterknife-zelezny

2016年6月20日 星期一

Premultiplied pixels in Android bitmaps

簡單來說,Android 裡的 Bitmap 都是 premultiplied 過的,premultiplied 就是為了
加速 blending 兩張圖片的運算,預先將 ARGB 的 Alpha 資訊直接記錄在 RGB 中,
這樣的過程就是  premultiplied。
可以透過 Bitmap.isPremultiplied 來做檢查。
詳細可參考下面的連結。

Ref:
  1. The curious case of Android premultiplied alpha
    https://pspdfkit.com/blog/2016/a-curious-case-of-android-alpha/
  2. 透明像素-Premultiplied Alpha的秘密
    http://blog.csdn.net/zinking3/article/details/2260405

2016年6月14日 星期二

How to check AAR's dependencies?

以最近很火紅的 FireBase 為例,想要使用 Analytics 的功能,需要引用
com.google.firebase:firebase-analytics:9.0.2
等等? 有人說不是應該是 com.google.firebase:firebase-core:9.0.2 嗎?
其實兩者都對,com.google.firebase:firebase-core:9.0.2 的 dependency 就是
com.google.firebase:firebase-analytics:9.0.2,要怎麼看 AAR 的 dependencies 呢?
首先,Android Studio 把預載好的 AARs 放在以下的目錄:
C:\Users\{UserName}\AppData\Local\Android\sdk\extras\google\m2repository

所以我們找到了 firebase-core:9.0.2 的目錄:
C:\Users\{UserName}\AppData\Local\Android\sdk\extras\google\m2repository\com\google\firebase\firebase-core

資料夾裡有個神秘的檔案 "firebase-core-9.0.2.pom",打開看看:
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
xmlns="http://maven.apache.org/POM/4.0.0" 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.google.firebase</groupId>
  <artifactId>firebase-core</artifactId>
  <version>9.0.2</version>
  <packaging>aar</packaging>
  <dependencies>
    <dependency>
      <groupId>com.google.firebase</groupId>
      <artifactId>firebase-analytics</artifactId>
      <version>9.0.2</version>
      <scope>compile</scope>
      <type>aar</type>
    </dependency>
  </dependencies>
</project>

答案揭曉,原來 com.google.firebase:firebase-core:9.0.2 根本就是 dependent on com.google.firebase:firebase-analytics:9.0.2 嘛!

2016年6月6日 星期一

[Gradle] How to generate jar file with Android Studio

Android Studio 在 build 的時候,會在
{library project}\build\intermediates\bundles\release\ 下產生 classes.jar。
我們只要把它複製到該放的路徑下就好。

可以用 Gradle Task 來做:

task deleteJar(type: Delete) {
    delete 'libs/clgpuimage.jar'
}

task createJar(type: Copy) {
    from('build/intermediates/bundles/release/')
    into('libs/')
    include('classes.jar') // Only include "classes.jar" in the from directory.
    rename ('classes.jar', 'clgpuimage.jar')
}

createJar.dependsOn(deleteJar, build)

最後,對這個 library project 執行 "createJar" 的 task 即可。

2016年4月20日 星期三

Designtime Layout Attributes

從 Android Studio 0.2.11 開始,為 layout 與元件引進了設計期間(Designtime)的 attributes。
其中兩個 attributes 頗為實用:

tools:text

有時候我們不想要填寫 android:text,卻想要在設計期間看到字串以方便調整 layout 時,就可以使用 tools:text,這樣一來該字串就不會執行期間出現。

tools:listitem

在設計 ListView 的 UI 時,通常只看的到預設的 ListView,看不到自己所設計對應的 ListView Item,此時只要在 ListView 設定 tools:listitem="@layout/view_listview_item" 就可以看到了。

大部分的 android:xxx 的屬性都能搭配 tools:xxx 來使用,例如 tools:visibility 等等。
另外,在使用 tools:xxx 之前,不要忘記宣告了命名空間:
xmlns:tools="http://schemas.android.com/tools"

Ref:
  1. http://tools.android.com/tips/layout-designtime-attributes
  2. http://cloudchen.logdown.com/posts/247618/android-tools-attributes


2016年2月1日 星期一

How to handle app links?

App Link 可以用來讓程式 A 把 程式 B 的某個 Activity 叫起來。
首先,需要在 AndroidManifest.xml 裡的 Activity 區段宣告以下:
<activity ...>
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="myApp" />
        <data android:host="profilePage" />
    </intent-filter>
</activity>
一個 URI 是由以下所組成:
<scheme>://<host>:<port>[<path>|<pathPrefix>|<pathPattern>]

所以程式 A 就可以透過 URI myApp://profilePage 把程式 B 給叫起來:
// Application A start application B via the intent with URI. 
Intent i = new Intent();
i.setData(Uri.parse("myApp://profilePage"));
// Check whether the intent can be resolved.
if (i.resolveActivity(getPackageManager()) != null) {
  startActivity(i);
}

甚至 URI 後面也可以帶 query:
myApp://profilePage?uid=12&uname=Sean
再透過 Uri 的方法 getQueryParameterNames 和 getQueryParameter 來取得 key 和 value。


測試的方法

測試的方法有三種:
  1. 直接寫程式碼執行。
  2. 用 Android Studio 來執行
    Run -> Edit Configuration... -> Launch Options -> URL
    在 URL 的欄位輸入 URI 然後執行。
    用這個方法的缺點是只能測 action=android.intent.action.VIEW 而且 category=android.intent.category.BROWSER 的 activity。

  3. 透過 adb command line (推薦的方法)
    輸入 adb 指令:
    adb shell am start -a android.intent.action.VIEW -c android.intent.category.BROWSER -d myApp://profilePage

Ref:
  1. <data>
    https://developer.android.com/guide/topics/manifest/data-element.html
  2. Support HTTP URLs in Your App
    https://firebase.google.com/docs/app-indexing/android/app?hl=zh-tw#reference-the-noindexxml-file
  3. Testing URLs with Android Studio
    https://firebase.google.com/docs/app-indexing/android/test#lint-checks-for-links

2015年11月12日 星期四

How to check ProGuard obfuscation has worked?

用 Android Studio build 完 APK 檔之後,
在 C:\{Project}\{Application Project}\build\outputs\mapping\release 下會生成四個檔案:
  • dump.txt
    描述了 APK 檔裡所有 class 檔案的架構。
  • mapping.txt
    class, method, field 混淆前後的名稱。 
  • seeds.txt
    沒有被混淆的  class, method, field 名稱。 
  • usage.txt
    被 ProGuard 拿掉的 class, method, field。

How to use proguard for library project(module)?

發現如果在 application project 跟 library project 裡都有定義

buildTypes {
      release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
      }
}

就會有一堆 cannot find symbol class 的 errors。
解決的方法有兩種:
  1. 把 library project 的 proguard-rules.pro 裡的定義都搬到 application project 的 proguard-rules.pro,再把 library project 的 minifyEnabled 設為 false。
  2. 另一種就是把 library project 的 proguardFiles 改成 consumerProguardFiles 就行了 (minifyEnabled 也不須設定,minifyEnabled 是對應 proguardFiles 的)。
consumerProguardFiles 的定義是:
ProGuard rule files to be included in the published AAR.
These proguard rule files will then be used by any application project that consumes the AAR (if ProGuard is enabled).
This allows AAR to specify shrinking or obfuscation exclude rules.
This is only valid for Library project. This is ignored in Application project.

Ref:
  1. http://stackoverflow.com/questions/30820915/android-studio-proguard-handling-in-multi-library-projects
  2. http://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.BuildType.html


2015年10月16日 星期五

To get how many methods used in an APK.

GitHub 上有熱心的大大寫出了一個 tool (jar),可以用來查看 APK 用了多少 methods。
不過有點小麻煩的是得把 project 抓下來,自己 build 出 dex-method-counts.jar。
我把它  build 好放在 Google Drive 上方便懶人們直接使用:
https://drive.google.com/file/d/0B2EwSdT6jFo8ZWs2MlhxVzZwaU0/view?usp=sharing

Check following link for details:
https://github.com/mihaip/dex-method-counts

更有人把它進一步整成了 Android Studio 的 plugin:
https://github.com/KeepSafe/dexcount-gradle-plugin

2015年9月14日 星期一

Can I package Android resources to jar?

當然我們可以不用 jar,讓其他人直接 import 我們的 library project,
可是這樣程式碼不就被看光光了嗎?
但當我們的 library 有自己的 resources,而且想 build 出 jar 檔給其他人用時該怎麼辦?
我可以把 resources 也包進去嗎?
The answer is No!
但有其他替代的方法來解決這樣的問題。
畫了幾張圖來說明:
(一樣一開始準備一個要 build 出 jar 檔的 "myLib" project)
問題在於 Android 的每個 project 會產生自己的 R 檔,
在編譯的時候會自動把程式碼裡的 R.xxx 轉換成 id,
但 "myLib" 引用到的 id 不在於 jar 檔裡也不在於 apk 檔裡,
所以得把 "myLib" 用到的 resources 給搬到 "myAppTestbed" 主要應用程式的 project,
這樣 apk 檔裡就會有這個 id,然後再動態的去引用這些 resources。
Android 提供了一個 API Context.getResources().getIdentifier() 
可以在 runtime 的時候取得 resources。
流程如圖1 所示。

圖1. 

圖2. 
但是把 resources 都搬到 main project 也不是很好,一來開發 main project 的人
得手動搬一次,二來弄髒了 main project 的 resource folder。
所以我們可以參考 google-play-services_lib 的作法,
再另建一個 lib project "myLibWrapper"(名字可能取的不太好,大家可以自行決定),
這個 "myLibWrapper" 的主要功能有三個:
  1. 引用 myLib.jar。
  2. 存放 myLib.jar 所需的 resources。
  3. 讓 main project 引用。
這樣一來開發 main project 的人,只要 import "myLibWrapper",
並設定 dependencies 的關係就好,雖然迂迴了一下,但是簡單又乾淨。
詳細如圖3 的流程所示。
(當然弄成 aar 更簡單,不過這又是另一個議題了)

圖3.

2015年7月22日 星期三

[Android Studio] Code Completion Case Sensitive Setting

Android Studio 的 Code Completion 預設是 Case sensitive 的,
更改設定的方法:
Settings(or Preferences in mac) -> Editor -> Code Completion -> Case sensitive completion -> None

[Android Studio 1.2.2] Debugger stuck

最近開始改用 Android Studio,卻發現 debug 的時候常常卡住,
但 Android Studio 本身並沒有 hang, 後來發現網路上有很多人 report Google 這支 issue,
其中 issue 172523 裡有 Google 人員提到暫時的 workaround 方法:

"For anyone else who might encounter this issue, here is a summary:

The issue shows up in one of two ways: Studio will be responsive, but the debugger will be stuck at either "Collecting Data.." or "Waiting for last debugger command to complete..". This happens on both Dalivk and ART, so all versions of the platform are affected. The issue is more prevalent with Studio 1.2, but exists on all versions of Studio.

The correct fix for this issue is in the platform. The next version of M preview is likely to have this fix (in progress CL here: https://android-review.googlesource.com/#/c/152715/)

Until then we have some workarounds which reduce the probability of hitting this issue. So if you encounter this issue, you can try one of the following:

1. Change your breakpoint to only suspend the thread where it is hit rather than all threads. See comment #82 for more info on how to do this. The next release of Studio 1.2 and Studio 1.3 will be make this the default. (https://android-review.googlesource.com/#/c/152715/)

2. You can turn off various settings in the debugger that invoke methods: These include:
  a) inline debugging (https://www.jetbrains.com/idea/help/inline-debugging.html)
  b) "Enable 'toString()' object view" (Settings | Debugger | Data Views | Java)
  c) "Enable alternative view for Collections classes" (Settings | Debugger | Data Views | Java)

The 2nd option is more severe (it limits the amount of automation the debugger does for you), so we are not enabling that by default. However, if you still see the issue after changing the suspend policy to thread only, then unfortunately, you'll have to do the steps in 2 as well.

Finally, if you still see the issue after both, then that would be a new bug. Please file a new bug with a test case.

Thanks everyone for your patience and your help in providing us with repro cases and stack traces."


結論是 2.a 的 workaround 似乎可行。