Posts match “ androiddev ” tag:

基本上使用 gradle wrapper 應該沒有這樣的問題, 不過由於 gradle 1.8 需要 plugin 0.6 以上, 如果 project 與其他人共用, 版本又沒統一的狀況下 (比如怕有地雷..) 可以透過判斷 gradle version 的小技巧, 讓 project 可以在 gradle 1.7 & 1.8 下都能編譯.

參考 https://gist.github.com/shakalaca/6779748 修改 project root 的 build.gradle, 在 dependencies 判斷使用的 plugin 版本即可.

搭配 gradle wrapper 使用效果更好, 如果先前用 1.7 編譯完全沒問題, 而 1.8 無法正常編譯, 修改 task wrapper 中的 gradleVersion 後, 執行一次 ./gradlew wrapper 即可退版, 等到之後 plugin 或者 gradle 有修正再切換過去.

如果不想在 apk 裡包入某個 jar 檔 (使用 device 上的, 或者某版本沒提供某種功能) 有兩種方式: 一個是加法, 一個是減法.

加法就是透過 configuration 的設定, 在 compile 時才把把該檔案加入 class path 參考: https://gist.github.com/shakalaca/6551508

減法就是在 dex task (compile task 之後) 開始處理 jar 檔前, 把 jar 檔從 library 路徑中移除: https://gist.github.com/shakalaca/6551576

一開始最直覺的方式就是加法, 但後來發現我需要的是在某個 build 不放置 jar 檔, 才有了減法的作法, 但感覺有些醜, 再想想有沒有更好的方式..

又到了難纏的狀況, 跟之前 Google Maps 的狀況很類似, 不過這次需要支援的檔案為 AndroidManifest.xml

GCM 需要在 AndroidManifest.xml 中設定權限, 而這個權限又得跟著 package name 跑, 所以不同的 flavour & buildType 就會有不同的設定值. 但問題是 gradle 並不支援 flavourBuildType (比如 amazonFreeDebug) 的目錄設定, 所以即使它可以幫你整合檔案裡的設定值, 但要怎麼擺放 AndroidManifest.xml 卻是個大問題.

這邊一樣提供 hack 的方法, 首先指定 flavour 所要使用的 AndroidManifest.xml 路徑

project.ext.flavor1 = [
    debugManifest: 'src/flavor1Debug/AndroidManifest.xml',
    releaseManifest: 'src/flavor1Release/AndroidManifest.xml'
]

project.ext.flavor2 = [
    debugManifest: 'src/flavor2Debug/AndroidManifest.xml',
    releaseManifest: 'src/flavor2Release/AndroidManifest.xml'
]

接著在 variant.processManifest 時想辦法讓 gradle 吃到這些檔案, 由於這個 case 的 debug & release 都是不同的 package name, 我們可以利用 flavour 本身並不具有資源設定值的特色, 把新的 AndroidManifest.xml 偷偷地塞進 flavour 的目錄裡, 名正言順的讓 gradle 編譯該 flavour. 最後記得刪除這個偷塞的檔案:

android.applicationVariants.all { variant ->
    variant.processManifest.doFirst {
        if (project.ext.has(variant.productFlavors.name)) {
            if (project.ext[variant.productFlavors.name].debugManifest != null &&
                project.ext[variant.productFlavors.name].releaseManifest != null) {
                def manifestDirectory = android.sourceSets[variant.productFlavors.name].manifest.srcFile.parentFile
                if (variant.buildType.name.equals("debug")) {
                    copy {
                        from project.ext[variant.productFlavors.name].debugManifest
                        into manifestDirectory
                    }
                } else if (variant.buildType.name.equals("release")) {
                    copy {
                        from project.ext[variant.productFlavors.name].releaseManifest
                        into manifestDirectory
                    }
                }
            }
        }
    }

    variant.processManifest.doLast {
        if (project.ext.has(variant.productFlavors.name)) {
            project.delete android.sourceSets[variant.productFlavors.name].manifest.srcFile
        }
    }
}

至於 AndroidManifest.xml, 只要留 GCM 相關設定即可, 其他 Activity 啥鬼的都不用放進去, 由 gradle 在編譯過程中自行與 main 整合.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.android.example.free.debug"
    android:versionCode="1"
    android:versionName="1.0" >

    <permission
        android:name="com.android.example.free.debug.permission.C2D_MESSAGE"
        android:protectionLevel="signature" />

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="com.android.example.free.debug.permission.C2D_MESSAGE" />
    <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />

</manifest>

收工, 跟平常一樣 assemble 即可, happy hacking ! 相關的 code 可以參考這 : https://github.com/shakalaca/learning_gradle_android/blob/bef7af864f1d89483df3c611d76a199815f65660/07_tricks/app/build.gradle

之前提到 Google Maps API (v2) 在整合至 gradle 時會碰到一些問題, 這邊提供一種可能的解法.

首先來看是怎樣的狀況, Google Maps API 在使用時得請求一組 API key, 而這組 key 得對應到一個 package name. 我自己會申請兩組, 一組給 debug 版用, 另一組給 release 版用, 這是因為 debug 版使用不同的 package name (透過 packageNameSuffix) 在測試時才能於同一支手機安裝 debug & release 版. 這樣的組合透過簡單的 overlay 應用即可輕鬆達成: 獨立出 debug (or release) 使用的 resource folder, 把 API key 放在 strings.xml 即可.

但如果你的 app 有免費版與付費版呢 ? 套用我的設定就有 4 種 package name 配 4 組 key, 理論上分 4 個 resource overlay 就可以解決. 可以問題來了, gradle 只能讓你設定 flavor 或者 buildtype 的 sourceSets, 例如 free, paid, debug, release, 但並不提供 freedebug, freerelease, paiddebug, paidrelease 的設定方式, 因此就算你把 folder 區分為前述的樣子, 或者異想天開在 sourceSets 分兩層方式設定 flavor.buildtype 的 res.srcDirs, 都無法順利分開 API key 所使用的 strings.xml.

目前看到的解法是透過修改 AndroidManifest.xml 的方式, 在 variant.processManifest.doLast 時複製一份方便修改的版本到編譯中介目錄, 然後置入正確 API key 的設定. 

我稍微研究 Android Tools Project Site 的文件, 試著找出另一種不用修改檔案的方式達到同樣的效果. 其中這一段吸引我的注意:

The following rules are used when dealing with all the sourcesets used to build a single APK:

  • All source code (src/*/java) are used together as multiple folders generating a single output.
  • Manifests are all merged together into a single manifest. This allows Product Flavors to have different components and/or permissions, similarly to Build Types.
  • All resources (Android res and assets) are used using overlay priority where the Build Type overrides the Product Flavor, which overrides the main sourceSet.
  • Each Build Variant generates its own R class (or other generated source code) from the resources. Nothing is shared between variants.

第三點提到 build type 的優先權最高, 如果我們在整合 resource 之前把 build type 的 folder 指定到我想要用的目錄呢 ? android gradle plugin 有提供 variant.mergeResources, 那麼試著在那之前把 build type 的 sourcesets 替換, 應該就行了吧 ? 花了點時間修改, 很幸運的跑起來沒有問題, 這邊提供給各位參考, 有使用 Google Maps API 且跟我有類似需求的朋友可以參考看看 :)

相關 code 可參考這 https://github.com/shakalaca/learning_gradle_android/blob/a7a37a7b94cc79fd5f6cac834935c178f50a4061/07_tricks/app/build.gradle

如果要定義額外的 resource file, 透過 project.ext.flavorname 定義, 其中 debugRes 為 debug 版的 resource, releaseRes 為 release 版的 resource, 沒有定義會自動使用 default.

延續之前討論的 rename APK 的問題, 那樣的寫法在 CLI 使用 gradle 時一切正常, 但如果把 project 匯入 Android Studio, 在 build 的時候會有奇怪的事情發生:

  • 如果跟我一樣在檔名安插 gitHash, commit 新的 change 之後在 Android Studio compile & run 時會抱怨檔案找不到, 此時會發現 Android Studio 要求的檔名為上一次的 gitHash 值, 但實際上新的檔案已經產生在 build/apk 目錄下.
  • 就算沒有使用動態檔名, 偶爾也會發生產生的檔名會回復成原本的名稱.

目前懷疑 Android Studio 會 cache 輸出的檔案路徑, 其實也算合理, 因為沒事除了切換 variant 檔名的確不會改變.

我自己採用折衷的方法: 把編譯好的 apk 移到另一個目錄並且更名. 一方面對 Android Studio 而言, 一切如常, 如果要調出檔案分享到指定的目錄找尋即可; 另一方面我還是可以享受編譯好檔案即更名的好處.

相關 code 可參考這裡 https://gist.github.com/shakalaca/6422811

gradle 在 zipAlign 為 true 時, 會產生兩個檔案: 一個為 unaligned 一個為 aligned (好像是廢話)

而在 android.applicationVariants 裡面, variant.packageApplication.outputFile 指向的是原始版本 (unaligned) 而 variant.outputFile 才是 zipAlign 過後的版本.

因此如果在 "保留兩種版本的檔案" 為前提的狀況下針對輸出的 apk 檔案更名, 應該要如連結中這麼做: variant.packageApplication.outputFile 一律更名, 而碰到 zipAlign 為 true 時, 額外更名 variant.outputFile.

相關 code 可參考這 https://gist.github.com/shakalaca/6414702

如果你有裝 Parallels Desktop or VMWare Fusion:

  • 點擊掛載 android_system_disk.vmdk, 找到 ramdisk 所在地開啟 terminal, 移出 ramdisk 到比如說 /tmp/test
  • cd /tmp/test; mkdir root; cd root; gunzip -c ../ramdisk | cpio -i
  • 做你想做的事情: 塞檔案, 編輯檔案, 比如編輯 default.prop, 把 ro.secure=1 改為 0
  • find . | cpio -o --format newc | gzip > ../ramdisk.new
  • 把 ramdisk.new 放回原來的目錄並更名為 ramdisk, 收工 !

其實只要是 ScrollView 或者 HorizontalScrollView 裡面塞 RelativeLayout 都會有這樣的問題..

最近把手邊的程式 targetSdkVersion 從 17 換到 19, 這才發現某些 layout 怪怪的. 我的 case 是這樣:

  • 在 ListView item 中使用 RelativeLayout 當 root view, layout height 設定為 wrap content
  • layout 中有一個 LinearLayout, 寬度固定高度為 match parent, 裡面包兩個 button, 透過 height="0dp" 與 weight="1" 均分高度
  • 因為 root view 為 wrap content, 所以該 LinearLayout 得對齊另一個固定高度 view 的上下緣方能填滿 root view

當 targetSdkVersion 為 17 時, 會是我想要的結果:

當 targetSdkVersion 為 19 時, button 擠在一起:

前者其實是因為 RelativeLayout 在計算 button 所需高度時, 因為 LinearLayout match parent/wrap content 的關係導致 overflow, 系統判定可以給予非常大的高度, 而又因為設定 weight 都是 1 的緣故, 造成按鈕均分的效果.

之後的版本在 View.javaRelativeLayout.java 多做了一些判斷 (找 sUseBrokenMakeMeasureSpec & mAllowBrokenMeasureSpecs) 所以如果 LinearLayout 或者 RelativeLayout 沒有指定高度, 呈現的結果一定會亂七八糟, 除非在 build 的時候指定 targetSdkVersion 為 17, 但這樣的犧牲有點大..

最後還是靠調整 layout files 解決這個狀況 :p

Ref: http://developer.android.com/reference/android/widget/RelativeLayout.html

最近寫 app 時碰到一個需求: layout 裡面有一個搜尋欄位 (EditText), 當開啟 activity 時, 希望那個 EditText 能取得焦點並且彈出虛擬鍵盤.

看似簡單的需求其實搞了一整晚, 解法有很多種, 端看 app 需求.

最簡單的方式是以 ScrollView 當底, 裡面的 layout 只要有放置 EditText, activity 開啟後便會自動取得 focus, 無需特別設定. 但如果你的 layout 裡面有多個 EditText 欄位, 肯定不希望一開啟就彈出虛擬鍵盤, 這時候可以透過兩種方式關閉. 一種是 AndroidManifest.xml 裡面在該 activity 的項目設定 android:windowSoftInputMode="stateHidden", 另一種是在程式裡面透過 getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN) 關閉虛擬鍵盤.

如果不想用 ScrollView 呢 ? 能否直接在 onResume() 時, 透過 requestFocus() 的方式讓虛擬鍵盤彈出來 ? 這說法有點怪, 因為原始的需求中只有一個 EditText, activity 打開本來就會 focus 到 EditText 上. 但有趣的地方在於即使 EditText 有 focus, 虛擬鍵盤仍舊不見蹤影. 依照前面的邏輯, 我們只要在 AndroidManifest.xml 裡面設定 android:windowSoftInputMode="stateVisible" 或者在 onResume() 透過 getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE) 便可以叫出虛擬鍵盤.

但事情沒有這麼簡單, 這麼做會帶出兩個問題:

  • 當程式轉換到下一個畫面, 透過 back 按鈕回到這個 fragment 時, 虛擬鍵盤不會再彈出.
  • 因為這是個 fragment, 所以當程式跳到別的畫面 (fragment) 時, 如果按 Home 或者 Multitask 鈕暫時跳出 app, 之後再由其他地方跳回 app, 虛擬鍵盤會莫名其妙的出現.

翻了下文件, 我們可以在 InputMethodManager 找到一個有趣的 method: showSoftInput 感覺上只要EditText 先 requestFocus(), 再把 EditText 與參數帶入, 就可以彈出虛擬鍵盤. 遺憾的是不管怎麼做 (onResume()), 虛擬鍵盤就是不會出來, 而透過 isActive() 會得到 false 的傳回值. 但是在已經出現的畫面中另外執行 showSoftInput() (比如 Button 的 clicklistener), 居然可以成功讓虛擬鍵盤彈出, 而呼叫 showSoftInput() 之前 isActive() 的回傳值竟然是 true ! 花了點時間在 AOSP 尋找答案, 推測在 app 開啟後 layout 畫完時, InputMethodService 尚未完全 ready, 如果太早 (onCreate/onResume/onStart) 對他進行相關請求, 基本上都會失敗. 而且悲劇的是目前也沒有 callback 可以讓 app 知道 InputMethdoService 什麼時候 ready, stackoverflow 上廣為流傳的 workaround 跟我的想法一樣:

mEditText.requestFocus();
mEditText.postDelayed(new Runnable() {
    @Override
    public void run() {
        InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
        imm.showSoftInput(mEditText, 0);
    }
}, 200);

不過我個人比較傾向另一種作法, 一樣是 onResume() 要求 focus, 但另外 setOnFocusChangeListener 讓 EditText 取得 focus 時彈出虛擬鍵盤:

private void requestSoftInput(View v) {
    mIsSoftInputFromImm = imm.isActive();
    if (mIsSoftInputFromImm) {
        imm.showSoftInput(v, 0);
    } else {
        getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
    }
}

@Override
public void onResume() {
    super.onResume();
    if (mEditText.hasFocus()) {
        requestSoftKeyboard(mEditText);
    } else {
        mEditText.requestFocus();
    }
}

@Override
public void onPause() {
    super.onPause();
    if (mIsSoftInputFromImm) {
        imm.hideSoftInputFromWindow(mEditText.getWindowToken(), 0);
    } else {
        getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
    }
}
  
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
    ...
    mEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
        @Override
        public void onFocusChange(View v, boolean hasFocus) {
            if (hasFocus) {
                requestSoftInput(v);
            }
        }
    });
}

這麼做有幾個好處:

  • 由系統自行決定調出虛擬鍵盤的時機, 而非透過 timer, 避免不必要的小問題.
  • 當程式由另一個 fragment 回來時, 仍舊可以藉由 focus 的取得彈出虛擬鍵盤.
  • 無論離開時有無顯示虛擬鍵盤, 回來時鍵盤一定會出現.
  • 切換 fragment 時不會因為錯誤的狀態導致虛擬鍵盤在奇怪的點彈出.

小小一個功能眉角還挺多的, 如果有朋友碰過類似的問題有更 solid 的 solution 也歡迎提供 :)

之前都是透過 gradle.properties 分開管理 release 用的簽章,好處是不用把敏感的 information 也丟到公開的 repo,但有個問題最近困擾著我:如果存放 keystore 的路徑有中文,gradle 在 sign apk 時無法正確讀取 keystore。試過很多亂七八糟的方法,最後是透過分開的 .gradle 檔案滿足這樣的需求。

Continue Reading →

與別人合作的 project 因為對方用的是 Eclipse (其實是只有我用 Android Studio),版本的設定會習慣放在 AndroidManifest.xml 裡面,但這麼一來如果 gradle 運行中需要使用到 versionName 或者 versionCode,就得額外設定並且注意是否與 AndroidManifest.xml 同步。這麼做除了讓專案的維護更麻煩以外,還得注意不同步的問題。

最早都是透過複雜的 script 分析 AndroidManifest.xml 把設定值取出,後來發現有個很好用的工具:com.android.builder.core.DefaultManifestParser

在你的 build.gradle 中最前面先

import com.android.builder.core.DefaultManifestParser

接著在 defaultConfig 裡面設定:

defaultConfig {
    def manifestParser = new DefaultManifestParser()

    versionName = manifestParser.getVersionName(android.sourceSets.main.manifest.srcFile)
    versionCode = manifestParser.getVersionCode(android.sourceSets.main.manifest.srcFile)
}

That’s it ! 唯一要注意的是,sourceSets 的設定要放在 defaultConfig 之前,否則 gradle 會跟你抱怨 manifest 找不到。

筆記一下,這邊是直接拆開原始的 recovery.img 後修改 ramdisk 再打包:

$MKBOOTIMG --base 0 \
--kernel recovery.img-zImage \
--ramdisk recovery.img-ramdisk.gz \
--cmdline 'console=ttyHSL0,115200,n8 androidboot.hardware=qcom user_debug=31 msm_rtb.filter=0x37 ehci-hcd.park=3 UNLOCKED=Y’ \
--pagesize 2048 \
--ramdisk_offset 0x02500000 \
--dt recovery.img-dt \
-o new-recovery.img

cmdline 後面的 UNLOCKED=Y 加上去後再配合 default.prop 塞 service.adb.root=1 才可以讓 adb shell 進去時為 root 權限。

檔案在這:asus_t00n_twrp_2_7_1_1.img

手機得先 root,在 shell 把映像檔寫入

dd if=asus_t00n_twrp_2_7_1_1.img of=/dev/block/platform/msm_sdcc.1/by-name/recovery

接著重開至 recovery,重開機後,當 ASUS logo 消失時等 60 秒左右,再按一下 power 按鈕,就可以看到 TWRP 選單囉。我目前還沒有驗證所有功能,有空再來試試看,請有興趣的記得先備份好手機資料。

警告:這篇是給 DEV 參考用,如果沒有相關基礎知識,使用後手機無法開機,本人不會幫忙解決。

雖然說手機尚未 unlock,隱藏的 partition 也無法透過 dd 寫入,但很久以前用過 Xperia Mini Pro 也是類似的狀況,但 XDA 的大神們還是想辦法搞了個 CWM recovery,理論上 ZenFone 也可以透過同樣的方式使用 CWM Recovery,所以花了點時間做個 POC (Proof of concept)。

Continue Reading →