標籤雲

搜尋此網誌

2018/08/07

AOSP - 系統內建 app 的 Android.mk 範例

有玩過 aosp 的人應該都有接觸過 mk 檔的撰寫
今天就來整理下如果要將 app 放進 aosp 裡面
Android.mk 檔要怎麼寫

最簡單的方式是先將 Module 編譯成 apk 檔
這樣子可以使用 gradle 編譯
dependency 也不用寫在 Android.mk 裡
非常簡單
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)

# Module name should match apk name to be installed
LOCAL_MODULE := XXXXXXX

LOCAL_MODULE_TAGS := optional

LOCAL_SRC_FILES := $(LOCAL_MODULE).apk

LOCAL_MODULE_CLASS := APPS

LOCAL_CERTIFICATE := platform

include $(BUILD_PREBUILT)

如果因為原始碼管理或是其他原因
你必須要把 Module 的專案原始碼放進 aosp 裡面的話
那麼可以參考一下下面的複雜版本

LOCAL_PATH:= $(call my-dir)

# compile time only jar names (for later use)
#COMPILE_LIBRARIES := compiler-a compiler-b

include $(CLEAR_VARS)

LOCAL_MODULE_TAGS := optional

# Name of the module to build
LOCAL_PACKAGE_NAME := XXXXXXX

# module src file path
LOCAL_SRC_FILES := \
        $(call all-java-files-under, java)

# path(s) of resources
#LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, res)

# List of static libraries to include in the package
#LOCAL_STATIC_JAVA_LIBRARIES := lib-jar-a lib-jar-b lib-jar-c

#LOCAL_STATIC_JAVA_AAR_LIBRARIES := lib-aar-a lib-aar-b lib-aar-c

# path of compile time libs
#LOCAL_CLASSPATH := $(addprefix $(LOCAL_PATH)/libs/,$(addsuffix .jar, $(COMPILE_LIBRARIES)))

# set as privileged application
#LOCAL_PRIVILEGED_MODULE := true

# use platform key to sign the module
LOCAL_CERTIFICATE := platform

# proguard enabled setting
LOCAL_PROGUARD_ENABLED := disabled

include $(BUILD_PACKAGE)

############## JAR/AAR Files ####################################
#include $(CLEAR_VARS)
#
## pre built all your jar and aar lib for your module 
## so you can use the lib name in LOCAL_STATIC_JAVA_LIBRARIES and LOCAL_STATIC_JAVA_AAR_LIBRARIES
#LOCAL_PREBUILT_STATIC_JAVA_LIBRARIES := lib-jar-a:libs/lib-jar-a.1.0.0.jar \
#    lib-jar-b:libs/lib-jar-b.1.0.0.jar \
#    lib-jar-c:libs/lib-jar-c.1.0.0.jar \
#    lib-aar-a:libs/lib-aar-a.1.0.0.aar \
#    lib-aar-b:libs/lib-aar-b.1.0.0.aar \
#    lib-aar-c:libs/lib-aar-c.1.0.0.aar
#
#include $(BUILD_MULTI_PREBUILT)

以上是參考不同範例綜合而成
若有錯誤及建議也希望不吝指教

Room Persistence Library 簡單介紹

使用 Room 需要在 app 的 build.gradle 中增加 dependencies

dependencies {
    def room_version = "1.1.1"

    implementation "android.arch.persistence.room:runtime:$room_version"
    annotationProcessor "android.arch.persistence.room:compiler:$room_version" // use kapt for Kotlin

    // optional: RxJava support for Room
    implementation "android.arch.persistence.room:rxjava2:$room_version"
    // optional: Guava support for Room, including Optional and ListenableFuture
    implementation "android.arch.persistence.room:guava:$room_version"
    // Test helpers
    testImplementation "android.arch.persistence.room:testing:$room_version"
}

Room 有三種主要元件

- Entity: 對應到 database 的 table

- DAO: 擁有存取 database 資料的 method

- Database: 是主要存取點與 db 的持有者, 需要滿足以下要件
使用 @Database 這個 annotation 並且在裡面宣告 Entities 的 list 來定義包含的 table
繼承 RoomDatabase 的 abstract class
擁有無參數的 abstract method 可以 return 用 @Dao 標記的 Data Access Object

另外, 由於 Database 是比較耗資源的類別, 所以建議是使用 singleton 模式來獲得實體

所以一個 Database 大概會長這樣
@Database(entities = {Alarm.class}, version = 1, exportSchema = false)
public abstract class XXXXXDB extends RoomDatabase {
 public static final String DB_NAME = "xxxxx_db";

 private static XXXXXDB INSTANCE;

 public static XXXXXDB getInstance(final Context context){
  if(INSTANCE == null){
   synchronized (AlarmDB.class) {
    if(INSTANCE == null) {
     INSTANCE = Room.databaseBuilder(context, XXXXXDB.class, DB_NAME).build();
    }
   }
  }
  return INSTANCE;
 }

 public static void destroyInstance() {
  if(INSTANCE != null){
   INSTANCE.close();
  }
  INSTANCE = null;
 }


 public abstract XXXXXDao xxxxxDao();

}

而 Data Access Object 則會長得像是這樣
@Dao
public interface XXXXXDao {
 @Query("SELECT * FROM "+XXXXX.TABLE_NAME+" ORDER BY "+XXXXX.Col.TIME+" DESC" )
 List queryXXXXXs();

 @Query("SELECT * FROM "+XXXXX.TABLE_NAME+" WHERE "+XXXXX.Col.ID+" = :id")
 Alarm queryXXXXX(long id);

 @Query("SELECT * FROM "+XXXXX.TABLE_NAME+" WHERE "+XXXXX.Col.TIME+" BETWEEN :afterTheTime AND :beforeTheTime")
 List queryXXXXXs(long afterTheTime, long beforeTheTime);

 //return long (the rowId)
 @Insert(onConflict = OnConflictStrategy.REPLACE)
 long insertXXXXX(XXXXX x);

 @Delete
 void deleteXXXXX(XXXXX x);
}

最後是 Entity object 的簡單範例
@Entity(tableName = XXXXX.TABLE_NAME)
public class XXXXX {
 private static final String TAG = XXXXX.class.getSimpleName();

 public static final String TABLE_NAME = "XXXXXs";

 public class Col {
  public static final String ID = "id";
  public static final String TARGET_DEVICE = "target_device_id";
  public static final String ACTION = "action";
  public static final String TIME = "time";
  public static final String TYPE = "type";
  public static final String CONTENT = "content";
 }
 public class Value {
  public static final String ACTION_AAA = "aaa";
  public static final String ACTION_BBB = "bbb";
 }

 @PrimaryKey
 @ColumnInfo(name= Col.ID)
 public long id;

 @ColumnInfo(name= Col.TARGET_DEVICE)
 public String targetDeviceId;

 @ColumnInfo(name= Col.ACTION)
 public String action;

 @ColumnInfo(name= Col.TIME)
 public long time;

 @ColumnInfo(name= Col.TYPE)
 public int type;

 @ColumnInfo(name= Col.CONTENT)
 public String content;

 @Ignore
 private int[] myInts;

 public Alarm() { }

}

另外由於檔案存取不允許在主 thread 進行
所以建議在 AsyncTask 或是 IntentService 裡面操作


相關資料:
Android Developers: Adding Components to your Project#Room
Google Developers Codelabs: Android Room with a View
Android Developers: Save data in a local database using Room

2016/07/13

Mac 使用 FAQ 與 terminal 指令

*手動產生 SSH key

1. 產生 public/private rsa key pair
ssh-keygen -t rsa
你會需要設定一組 password (passphrase)
完成後檔案會放置在 /Users/yourusername/.ssh/id_rsa
而公開鑰 (public key) 則是放在 /Users/yourusername/.ssh/id_rsa.pub

2. 將 id_rsa.pub 拷貝到剪貼簿
pbcopy < ~/.ssh/id_rsa.pub

*顯示隱藏檔案與資料夾

將 com.apple.finder 的 AppleShowAllFiles 屬性設為 YES
然後關閉所有現有 Finder
defaults write com.apple.finder AppleShowAllFiles YES
killall Finder
執行完後再打開 Finder 就可以了

相關資料:
Manually Generating Your SSH key in MAC OS X

2016/07/07

Flex Box 與常用 Styles

Flex Box

Flex Box 的 style 屬性有四個 flex, flexDirection, justifyContent, alignItems

其中 flex 代表該 element 應該擴展的大小, 0 代表不展開
另外三個則互相相關:

flexDirection 指定子 element 的排列方向, 'vertical'(column) 或 'horizontal'(row)
而 flexDirection 的值會影響 justifyContent 與 alignItems 兩個屬性所造成的效果
因為 justifyContent 代表內容物在主要軸上的分布方式
alignItems 代表內容物在次要軸上的對齊方式

其值範圍:

flexDirection enum('row', 'row-reverse', 'column', 'column-reverse')
justifyContent enum('flex-start', 'flex-end', 'center', 'space-between', 'space-around')

alignItems: 'flex-start'(對齊前端 default) / 'center'(置中) / 'flex-end'(對齊尾端) / 'stretch'(填滿)

在 React Native 中
flexDirection 預設值為 column
這表示 justifyContent 的方向是 vertical , alignItems 的方向是 horizontal

所以在這樣的情況下
justifyContent: flex-start
alignItems: stretch
代表的是子 element 的排列方式是 向上對齊、左右填滿

同樣的值在 flexDirection: row 設定下
flexDirection: row
justifyContent: flex-start
alignItems: stretch
子 element 的排列方式變成 靠左對齊、上下填滿

其實只要記住 justifyContent 預設(column)下是直式
那 alignItems 自然就是橫的
row 模式下則是相反
這樣就比較簡單了

以下列出常用 style 屬性:

Layout Props
flex number
flexDirection enum('row', 'row-reverse', 'column', 'column-reverse')
flexWrap enum('wrap', 'nowrap')
justifyContent enum('flex-start', 'flex-end', 'center', 'space-between', 'space-around')
alignItems enum('flex-start', 'flex-end', 'center', 'stretch')
alignSelf enum('auto', 'flex-start', 'flex-end', 'center', 'stretch')

width number (以及 maxWidth, minWidth)
height number (以及 maxHeight, minHeight)
margin number (以及 marginHorizontal, marginVertical, marginTop, marginBottom, marginLeft, marginRight)
padding number (以及 paddingHorizontal, paddingVertical, paddingTop, paddingBottom, paddingLeft, paddingRight)

backgroundColor string
opacity number 0~1
borderRadius number (以及 borderBottomLeftRadius, borderBottomRightRadius, borderTopLeftRadius, borderTopRightRadius)
borderWidth number (以及 borderTopWidth, borderBottomWidth, borderLeftWidth, borderRightWidth)
borderColor string (以及 borderTopColor, borderBottomColor, borderLeftColor, borderRightColor)

position enum('absolute', 'relative')
top number (以及 bottom, left, right)
zIndex number

Transforms
transform:[
{perspective: number},
{rotate: string}, {rotateX: string}, {rotateY: string}, {rotateZ: string}, (ex:'45deg')
{scale: number}, {scaleX: number}, {scaleY: number},
{translateX: number}, {translateY: number},
{skewX: string}, {skewY: string},
]
decomposedMatrix DecomposedMatrixPropType
transformMatrix TransformMatrixPropType

Shadow Props (ios)
shadowColor color
shadowOffset {width: number, height: number}
shadowOpacity number
shadowRadius number


相關資料:
React Native - Layout Props
React Native - Transforms
React Native - Shadow Props
React Native Express - Flexbox
React Native Express - View

2016/07/06

Component Lifecycle API

Component 的生命週期有
建構, 掛載(mount), 渲染(render), 更新(update), 卸載(unmount), 銷毀( destroy)等

你可以在各生命週期中插入執行自定的程式碼來達到更細膩的控制

Mounting Cycle

constructor(object props)
從父 element 初始化 props, 此時我們也可以順便初始化 state
在 ES5 用的是 getDefaultProps / getInitialState 的方法
var MyComponent = React.createClass({
  propTypes: {
    text: React.PropTypes.string,
    anotherProp: React.PropTypes.string.isRequired,
    //以此類推...
  },
  getDefaultProps: function() {
    return {
      anotherProp: 'default value'
    };
  },
  getInitialState: function(){
    return { 
      key1: 'value1', 
      key2: null
    };
  },
  //...
});

在 ES6 則是用 constructor 的方式
至於 defaultProps 是在 Component 內用一個 static 變數來處理
import React, { PropTypes, Component } from 'react';
class MyComponent extends Component {
  static defaultProps = {
    text: 'default value',
  };
  static propTypes = {
    text: PropTypes.string.isRequired,
  };
  constructor(props){
    super(props);
    //initial state
    this.state = { 
      key1: 'value1', 
      key2: null
    };
  }
  //...
}

componentWillMount()
在第一次 render 發生前被呼叫, 此方法只會被呼叫一次

render() -> React Element
render 必須 return 一個 React Element (或是 null 表示不顯示任何東西)
在第一次 render 之前這個 element 不會有任何 native UI

componentDidMount()
在第一次 render 發生後被呼叫, 此方法只會被呼叫一次
此時這個 element 的 native UI 已經完成 render, 可以透過 this.refs 直接操作
如果需要進行非同步 API 呼叫或是用 setTimeout 執行 delayed code, 一般是在這裡進行


Updating Cycle

componentWillReceiveProps(object nextProps)
父 component 傳入了新的 props, 所以 component 即將重新渲染 (re-render)
在 render 方法被呼叫前我們可以用 this.setState() 更新 component 內部狀態

shouldComponentUpdate(object nextProps, object nextState) -> boolean
根據新的 props 跟 state, 該 component 必須決定是否該重新渲染
基底類別的 shouldComponentUpdate 實作總是會回傳 true
我們可以 override 這個方法並檢查每個 props 跟 state 是否已被更動(例如對每個 key/value 做 equal 檢查), 若回傳 false 就不會重新渲染

componentWillUpdate(object nextProps, object nextState)
componentWillUpdate 代表 component 此時已經確定要重新渲染
這時我們就不該用 this.setState() 改變狀態, 因為已經在進行更新了

render() -> React Element
既然要重新渲染當然就會再呼叫 render

componentDidUpdate(object prevProps, object prevState)
重新渲染完成後, 該 component 已經更新了從 render() 回傳來的 React Element


最後是卸載的部分
Uumount

componentWillUnmount()
若有使用 Timer, 卸載前應該清除以避免 Fatal
clearInterval/clearTimeout/clearImmediate

相關資料:
React Native Express - Lifecycle API
React - Reusable Components
React Native 中组件的生命周期

2016/05/16

在 Android Studio 2.1 開啟對 Lambda expressions 的支援

Android N 與 Android Studio 2.1 引進對 Java 8 語言的部分支援
像是:
Default and static interface methods
Repeatable annotations
Lambda expressionsMethod References (這兩項可向下支援到 API level 23 以下)
(其他支援的 Java 8 API 請參考文末連結)

而要使用這些支援必須認識一下 Jack

Jack (Java Android Compiler Kit) 是 Android 新的 toolchain
用來把 java code 編譯成 Android dex bytecode
Jack 使用 Java 1.7 並移植了一些新功能
不但是 Open source,還縮短編譯時間
而這個 toolchain 取代了之前的 javac, ProGuard, dx 等工具
傳統 javac 工具鏈: javac (.java --> .class) --> dx (.class --> .dex)
新的 Jack 工具鏈: Jack (.java --> .jack --> .dex)

Jack 是 Android M 的 default build toolchain
所以不用下任何特殊的 command 就可以使用

順便認識認識 Jack Intermediate Library Linker (Jill)
它負責的是把 jar 檔轉換成新的 lib 格式 (.jack)

在 build.gradle (app) 使用 Jack 的方式
android {
    ...(省略)

    defaultConfig {
        ...(省略)

        jackOptions{
            enabled true
        }
    }
    compileOptions {
        targetCompatibility 1.8
        sourceCompatibility 1.8
    }
    ...(省略)
}
完成以上設定後就可以在專案內使用 lambda 了

同場加映:Lambda 表達式簡介

匿名類別 Anonymous Classes 常被使用於作為一個參數傳遞到 method 裡
但匿名類別(特別是只有一個方法的匿名類別) 寫起來常感覺有些繁瑣
而 Lambda Expressions 對於簡化只有單一方法的類別就可以發揮優點

Lambda 表達式語法:
* 逗號分隔的的參數
* 箭頭標記( -> )
* 表達式主體
- 若使用單行表達式則回傳結果值,或是直接使用 return,不過 return statement 不是表達式,如果 method 不是回傳 void 則 statement 需使用大括號( { } )

例如如果我們有一個 printPersons 方法,傳入 List<Person> 及 CheckPerson 介面的實體(其中有 test 方法)
可以輸出通過條件的 Person 訊息
public static void printPersons(
    List<Person> roster, CheckPerson tester) {
    for (Person p : roster) {
        if (tester.test(p)) {
            p.printPerson();
        }
    }
}
實際呼叫時 code 是像這樣:
printPersons(
    roster,
    new CheckPerson() {
        public boolean test(Person p) {
            return p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25;
        }
    }
);
改寫成 lambda 之後:
printPersons(
    roster,
    (Person p) -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25
);

相關資料:
android developers - Java 8 Language Features
Jack (Java Android Compiler Kit)

2016/04/18

adb monkey test

adb shell monkey
usage: monkey [-p ALLOWED_PACKAGE [-p ALLOWED_PACKAGE] ...]
              [-c MAIN_CATEGORY [-c MAIN_CATEGORY] ...]
              [--ignore-crashes] [--ignore-timeouts]
              [--ignore-security-exceptions]
              [--monitor-native-crashes] [--ignore-native-crashes]
              [--kill-process-after-error] [--hprof]

              [--pct-touch PERCENT] [--pct-motion PERCENT]
              [--pct-trackball PERCENT] [--pct-syskeys PERCENT]
              [--pct-nav PERCENT] [--pct-majornav PERCENT]
              [--pct-appswitch PERCENT] [--pct-flip PERCENT]
              [--pct-anyevent PERCENT] [--pct-pinchzoom PERCENT]
              [--pkg-blacklist-file PACKAGE_BLACKLIST_FILE]
              [--pkg-whitelist-file PACKAGE_WHITELIST_FILE]

              [--wait-dbg] [--dbg-no-events]
              [--setup scriptfile] [-f scriptfile [-f scriptfile] ...]
              [--port port]
              [-s SEED] [-v [-v] ...]
              [--throttle MILLISEC] [--randomize-throttle] 每個動作的等待時間
              [--profile-wait MILLISEC]
              [--device-sleep-time MILLISEC] 
              [--randomize-script]
              [--script-log]
              [--bugreport]
              [--version]
              COUNT
範例:
adb shell monkey -p com.company.packagename --throttle 700 --pct-touch 20 --pct-motion 40 --pct-majornav 40 -v 100

2016/02/23

Wi-Fi Peer-to-Peer (Wi-Fi P2P)

Android 的 Wi-Fi P2P framework 符合 Wi-Fi Direct™ 認證
它可以讓應用程式在藍牙有效範圍內快速找到附近的裝置並與之互動

Wi-Fi P2P API 包含以下部分:
- WifiP2pManager 包含了讓你發現、要求,及連接至 peer 的 method
(initialize(), connect(), cancelConnect(), requestConnectInfo(), createGroup(), removeGroup(), requestGroupInfo(), discoverPeers(), requestPeers())

- 不同的 Wi-Fi P2P Listener 則讓你獲得 WifiP2pManager method的成功或失敗通知
當我們呼叫 WifiP2pManager 的 method 也需要傳入 Listener
(Wi-Fi P2P Listeners: ActionListener, ChannelListener, ConnectionInfoListener, GroupInfoListener, PeerListListener)

- 還有 Intent,通知我們 Wi-Fi P2P framework 的特定事件,例如連線中斷或發現新的 peer
(Wi-Fi P2P Intents: WIFI_P2P_DISCOVERY_CHANGED_ACTION, WIFI_P2P_PEERS_CHANGED_ACTION, WIFI_P2P_STATE_CHANGED_ACTION, WIFI_P2P_THIS_DEVICE_CHANGED_ACTION)

以上這三個部分會經常同時用到
(例如我們呼叫 discoverPeers() 時需要傳入 WifiP2pManager.ActionListener
才能接收 ActionListener.onSuccess() 和 ActionListener.onFailure() 的通知
而 discoverPeers() 找到有 peer 改變時也會發出 WIFI_P2P_PEERS_CHANGED_ACTION intent)

* Set Up Application Permissions

首先我們必須宣告需要用到的 permission
雖然不會用到網路連線,但是因為 Wi-Fi P2P 有使用到 standard Java sockets
而這需要 INTERNET 的 permission 所以除了 ACCESS_WIFI_STATE, CHANGE_WIFI_STATE 之外我們還需要 INTERNET 這個 permission
<uses-permission
        android:required="true"
        android:name="android.permission.ACCESS_WIFI_STATE"/>
    <uses-permission
        android:required="true"
        android:name="android.permission.CHANGE_WIFI_STATE"/>
    <uses-permission
        android:required="true"
        android:name="android.permission.INTERNET"/>

* Set Up a Broadcast Receiver and Peer-to-Peer Manager

為了接收 Wi-Fi P2P 的 broadcast 我們必須過濾以下 Intent Filter
private final IntentFilter intentFilter = new IntentFilter();
WiFiDirectBroadcastReceiver receiver;
Channel mChannel;
...
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    //  Wi-Fi P2P 狀態已改變(啟用或停用)
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION);
    // 可用 peers 列表已改變
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION);
    // Wi-Fi P2P 連接狀態已改變
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);
    // 裝置細節已改變
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);
    ...
    mManager = (WifiP2pManager) getSystemService(Context.WIFI_P2P_SERVICE);
    mChannel = mManager.initialize(this, getMainLooper(), null);
}
/** 註冊 BroadcastReceiver */
@Override
public void onResume() {
    super.onResume();
    receiver = new WiFiDirectBroadcastReceiver(mManager, mChannel, this);
    registerReceiver(receiver, intentFilter);
}

@Override
public void onPause() {
    super.onPause();
    unregisterReceiver(receiver);
}

/**
 * A BroadcastReceiver that notifies of important Wi-Fi p2p events.
 */
public class WiFiDirectBroadcastReceiver extends BroadcastReceiver {

    private WifiP2pManager mManager;
    private Channel mChannel;
    private MyWiFiActivity mActivity;

    public WiFiDirectBroadcastReceiver(WifiP2pManager manager, Channel channel,
            MyWifiActivity activity) {
        super();
        this.mManager = manager;
        this.mChannel = channel;
        this.mActivity = activity;
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();

        if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION.equals(action)) {
            // 取得 WifiP2pManager.EXTRA_WIFI_STATE 並傳給 activity
            int state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1);
            if (state == WifiP2pManager.WIFI_P2P_STATE_ENABLED) {
                activity.setIsWifiP2pEnabled(true);
            } else {
                activity.setIsWifiP2pEnabled(false);
            }
        } else if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) {
            // peer list 改變了,用 WifiP2pManager 要求可用的 peer。這是非同步的呼叫而且
            // activity 將透過 PeerListListener.onPeersAvailable() 這個 callback 被通知
            if (mManager != null) {
                mManager.requestPeers(mChannel, peerListListener);
            }
            Log.d(WiFiDirectActivity.TAG, "P2P peers changed");
        } else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action)) {
            // 連線狀態改變了
            if (mManager == null) { return; }
            NetworkInfo networkInfo = (NetworkInfo) intent
                    .getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO);
            if (networkInfo.isConnected()) {
                // 與其他裝置連接了,要求連接資訊以取得 group owner IP
                //呼叫非同步的 requestConnectionInfo() 並傳入 connectionListener 參數
                mManager.requestConnectionInfo(mChannel, connectionListener);
            }
        } else if (WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION.equals(action)) {
            DeviceListFragment fragment = (DeviceListFragment) activity.getFragmentManager()
                    .findFragmentById(R.id.frag_list);
            fragment.updateThisDevice((WifiP2pDevice) intent.getParcelableExtra(
                    WifiP2pManager.EXTRA_WIFI_P2P_DEVICE));
        }
    }
}

* Initiate Peer Discovery

mManager.discoverPeers(mChannel, new WifiP2pManager.ActionListener() {
        @Override
        public void onSuccess() {
            // discovery 初始成功。discoverPeers 動作已開始
            // 但如果有發現 service 會收到 broadcast (在 onReceive 裡),所以這裡空白也可以
        }
        @Override
        public void onFailure(int reasonCode) {
            // discovery 初始失敗。錯誤處理
        }
});

* Fetch the List of Peers

實作 WifiP2pManager.PeerListListener 可以接收 peer list
private List peers = new ArrayList();
    ...
    private PeerListListener peerListListener = new PeerListListener() {
        @Override
        public void onPeersAvailable(WifiP2pDeviceList peerList) {
            // 舊的退出或新的加入
            peers.clear();
            peers.addAll(peerList.getDeviceList());

            // 如果我們有用 AdapterView 顯示 peer list,記得要 notifyDataSetChanged()
            ((WiFiPeerListAdapter) getListAdapter()).notifyDataSetChanged();
            if (peers.size() == 0) {
                Log.d(WiFiDirectActivity.TAG, "No devices found");
                return;
            }
        }
    }
如果收到 WIFI_P2P_PEERS_CHANGED_ACTION 的 broadcast
我們可以呼叫 requestPeers() 並傳入 peerListListener (或是在 broadcast 建構時就傳入也是一個方法)
public void onReceive(Context context, Intent intent) {
    ...
    else if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) {
        // 用 WifiP2pManager 要求可用的 peer。這是非同步的呼叫而且
        // activity 將透過 PeerListListener.onPeersAvailable() 這個 callback 被通知
        if (mManager != null) {
            mManager.requestPeers(mChannel, peerListListener);
        }
        Log.d(WiFiDirectActivity.TAG, "P2P peers changed");
    }...
}

* Connect to a Peer

建立一個 WifiP2pConfig 物件連線到 peer
並從 WifiP2pDevice 複製資料進去
然後呼叫 connect()
@Override
    public void connect() {
        // 選取網路上找到的第一個裝置
        WifiP2pDevice device = peers.get(0);

        WifiP2pConfig config = new WifiP2pConfig();
        config.deviceAddress = device.deviceAddress;
        config.wps.setup = WpsInfo.PBC;

        mManager.connect(mChannel, config, new ActionListener() {
            @Override
            public void onSuccess() {
                // WiFiDirectBroadcastReceiver 會通知我們所以這裡可以略過。
            }
            @Override
            public void onFailure(int reason) {
                Toast.makeText(WiFiDirectActivity.this, "Connect failed. Retry.",
                        Toast.LENGTH_SHORT).show();
            }
        });
    }

實作 WifiP2pManager.ActionListener 只能通知我們初始化的成功或失敗
實作 WifiP2pManager.ConnectionInfoListener 的 onConnectionInfoAvailable() callback 則可以通知我們連線狀態的改變
為了讓不同裝置連接到同一個裝置(例如多人遊戲或聊天 app ),其中一個裝置會被指定為 "group owner"
@Override
    public void onConnectionInfoAvailable(final WifiP2pInfo info) {
        // 來自 WifiP2pInfo 結構的 InetAddress
        InetAddress groupOwnerAddress = info.groupOwnerAddress.getHostAddress());
        // group 交涉動作後就可以知道誰是 group owner
        if (info.groupFormed && info.isGroupOwner) {
            // group owner 做該進行的事,例如建立 server thread 並接收傳入連接
        } else if (info.groupFormed) {
            // 其他裝置則成為 client。例如建立 client thread 並與 group owner 連接
        }
    }

現在我們再回到 BroadcastReceiver 的 onReceive
看看 WIFI_P2P_CONNECTION_CHANGED_ACTION 的處理
...
        } else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action)) {
            // 連線狀態改變了
            if (mManager == null) { return; }
            NetworkInfo networkInfo = (NetworkInfo) intent
                    .getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO);
            if (networkInfo.isConnected()) {
                // 與其他裝置連接了,要求連接資訊以取得 group owner IP
                //呼叫非同步的 requestConnectionInfo() 並傳入 connectionListener 參數
                mManager.requestConnectionInfo(mChannel, connectionListener);
            }
        } ...

* NSD & Wi-Fi P2P in combination

前篇已經看過了 NSD
就來看看如何使用 Wi-Fi P2P 來進行 NSD
達到就算沒有連接到網路或熱點也可以直接探索附近裝置的功能

manifest 的部分一樣需要宣告 ACCESS_WIFI_STATE, CHANGE_WIFI_STATE 及 INTERNET 這三個 permission

- Add a Local Service

我們需要為 service discovery 註冊我們提供的 local service
這樣 framework 才會在 service discovery 自動回應
而建立一個 local service 需要三個步驟:
1. 建立 WifiP2pServiceInfo object
2. 將 local service 的資訊填到 WifiP2pServiceInfo 裡
3. 呼叫 addLocalService() 為 service discovery 註冊 local service
private void startRegistration() {
        //  用一個 string map 存放我們的 service 訊息
        Map record = new HashMap();
        record.put("listenport", String.valueOf(SERVER_PORT));
        record.put("buddyname", "John Doe" + (int) (Math.random() * 1000));
        record.put("available", "visible");

        // Service information. 傳入 service name, service type, 及其他裝置連接時要提供的訊息
        WifiP2pDnsSdServiceInfo serviceInfo =
                WifiP2pDnsSdServiceInfo.newInstance("_test", "_presence._tcp", record);

        // 呼叫 addLocalService, 傳送 service info, network channel, 及 ActionListener
        mManager.addLocalService(channel, serviceInfo, new ActionListener() {
            @Override
            public void onSuccess() {
                // 加入成功!除非你想更新 UI 或紀錄狀態否則不用做其他事
            }
            @Override
            public void onFailure(int arg0) {
                // 加入失敗. 錯誤碼可能是 P2P_UNSUPPORTED, ERROR, or BUSY
            }
        });
    }

- Discover Nearby Services

android 使用 callback 機制通知應用程式有哪些可用的 service
使用 WifiP2pManager.DnsSdTxtRecordListener 查看紀錄
這紀錄可以被其他裝置 broadcast
當一個連線進來,複製裝置位置與其他任何有關的資訊到這個 method 以便稍後存取
final HashMap<String, String> buddies = new HashMap<String, String>();
...
private void discoverService() {
    DnsSdTxtRecordListener txtListener = new DnsSdTxtRecordListener() {
        @Override
        /* Callback 包含:
         * fullDomain: full domain name: 例如 "printer._ipp._tcp.local."
         * record: key/value 配對的文字紀錄資料
         * device: 執行 service 的裝置
         */
        public void onDnsSdTxtRecordAvailable(String fullDomain, Map record, WifiP2pDevice device) {
                Log.d(TAG, "DnsSdTxtRecord available -" + record.toString());
                //從自訂欄位 "buddyname" 取出資訊
                buddies.put(device.deviceAddress, record.get("buddyname"));
            }
        };
    ...
}
創建一個 WifiP2pManager.DnsSdServiceResponseListener 來接收 service 的相關訊息
前面的 code 例子用了一個 Map 物件把 buddyname 存入

一旦 DnsSdTxtRecordListener 與 DnsSdServiceResponseListener 這兩個 Listener 都實作了
用 setDnsSdResponseListeners() 把它們加到 WifiP2pManager 中
private void discoverService() {
...
    DnsSdServiceResponseListener servListener = new DnsSdServiceResponseListener() {
        @Override
        public void onDnsSdServiceAvailable(String instanceName, String registrationType,
                WifiP2pDevice resourceType) {

                // 從 DnsTxtRecord 更新裝置名稱為讓人看得懂的名稱
                resourceType.deviceName = buddies
                        .containsKey(resourceType.deviceAddress) ? buddies
                        .get(resourceType.deviceAddress) : resourceType.deviceName;

                // 把 WifiP2pDevice 加入自訂的 adapter 用來顯示 wifi 裝置
                WiFiDirectServicesList fragment = (WiFiDirectServicesList) getFragmentManager()
                        .findFragmentById(R.id.frag_peerlist);
                WiFiDevicesAdapter adapter = ((WiFiDevicesAdapter) fragment
                        .getListAdapter());

                adapter.add(resourceType);
                adapter.notifyDataSetChanged();
                Log.d(TAG, "onBonjourServiceAvailable " + instanceName);
        }
    };
    mManager.setDnsSdResponseListeners(channel, servListener, txtListener);
    ...
}

現在建立一個 serviceRequest 並且呼叫 addServiceRequest()
serviceRequest = WifiP2pDnsSdServiceRequest.newInstance();
        mManager.addServiceRequest(channel,
                serviceRequest,
                new ActionListener() {
                    @Override
                    public void onSuccess() {
                        // 成功!
                    }
                    @Override
                    public void onFailure(int code) {
                        // 失敗。確認錯誤碼並進行相關處理
                        // P2P_UNSUPPORTED - Wi-Fi P2P 不支援在此裝置上執行該應用程式
                        // ERROR - 因為內部錯誤造成操作失敗
                        // BUSY - 系統忙碌無法處理請求
                    }
                });
最後呼叫 discoverServices()
mManager.discoverServices(channel, new ActionListener() {
            @Override
            public void onSuccess() {
                // 成功!
            }
            @Override
            public void onFailure(int code) {
                // 失敗。確認錯誤碼並進行相關處理
                // P2P_UNSUPPORTED - Wi-Fi P2P 不支援在此裝置上執行該應用程式
                // ERROR - 因為內部錯誤造成操作失敗
                // BUSY - 系統忙碌無法處理請求
                if (code == WifiP2pManager.P2P_UNSUPPORTED) {
                    Log.d(TAG, "P2P isn't supported on this device.");
                else if(...)
                    ...
            }
        });

相關資料:
Connecting Devices Wirelessly
Wi-Fi P2P
Creating P2P Connections with Wi-Fi
Using Wi-Fi P2P for Service Discovery

Network Service Discovery (NSD)

* Register Your Service on the Network

要在 local 網路註冊 service
首先得創建一個 NsdServiceInfo 物件
這物件提供給 local 網路內的其他裝置一些資訊
讓它們決定是否與你的裝置連接
NsdManager mNsdManager;
public void registerService(int port) {
    // 創建 NsdServiceInfo 物件並填充它
    NsdServiceInfo serviceInfo  = new NsdServiceInfo();
    // 若在同一網路中其他裝置也有相同服務名稱,Android會自動把其中一台的 ServiceName 改變避免衝突
    // 例如 "NsdChat" 可能會被改名成 "NsdChat(1)" 這樣的名稱
    serviceInfo.setServiceName("NsdChat");
    // ServiceType 的語法為 "_<protocol>._<transportlayer>"
    // 所以 uses HTTP protocol running over TCP 寫成 "_http._tcp."
    serviceInfo.setServiceType("_http._tcp.");
    serviceInfo.setPort(port);

    mNsdManager = Context.getSystemService(Context.NSD_SERVICE);
    //註冊 service
    mNsdManager.registerService(
            serviceInfo, NsdManager.PROTOCOL_DNS_SD, mRegistrationListener);
}

為了避免你的 service 使用的 port 發生衝突,我們要取得一個可用 port
如下例是設定 port 為 0 讓 socket 可以使用任意可用的 port
public void initializeServerSocket() {
    // 初始化 server socket 於下個可用 port
    mServerSocket = new ServerSocket(0);
    // 儲存被選到的 port
    mLocalPort =  mServerSocket.getLocalPort();
    ...
}

在 registerService 前我們必須實作 RegistrationListener 以接收註冊 service 成功與否的訊息
因為 registerService 是非同步的,所以 service 建立後才要做的事情必須放在 onServiceRegistered 裡
public void initializeRegistrationListener() {
    mRegistrationListener = new NsdManager.RegistrationListener() {
        @Override
        public void onServiceRegistered(NsdServiceInfo NsdServiceInfo) {
            // 儲存 service name. 因為名稱有可能與原本設定的不同所以這裡要更新為實際名稱
            mServiceName = NsdServiceInfo.getServiceName();
        }
        @Override
        public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
            // 註冊失敗! 依錯誤碼進行處理
        }
        @Override
        public void onServiceUnregistered(NsdServiceInfo arg0) {
            // Service 反註冊。在我們呼叫 NsdManager.unregisterService() 後才可能被觸發
        }
        @Override
        public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
            // Service 反註冊失敗。依錯誤碼進行處理
        }
    };
}

* Discover Services on the Network

Service discovery 跟 service registration 一樣有兩個步驟:
建立 discovery 的 listener 跟 callback 然後呼叫非同步執行的 discoverServices()
public void initializeDiscoveryListener() {

    // new 一個 DiscoveryListener 實體
    mDiscoveryListener = new NsdManager.DiscoveryListener() {
        //  Discovery 開始時就會被呼叫
        @Override
        public void onDiscoveryStarted(String regType) {
            Log.d(TAG, "Service discovery started");
        }
        @Override
        public void onServiceFound(NsdServiceInfo service) {
            // 發現 Service
            Log.d(TAG, "Service discovery success" + service);
            if (!service.getServiceType().equals(SERVICE_TYPE)) {
                // 還記得嗎?Service type 是包含 protocol 跟 transport layer 的字串
                Log.d(TAG, "Unknown Service Type: " + service.getServiceType());
            } else if (service.getServiceName().equals(mServiceName)) {
                // service 名稱
                Log.d(TAG, "Same machine: " + mServiceName);
            } else if (service.getServiceName().contains("NsdChat")){
                mNsdManager.resolveService(service, mResolveListener);
            }
        }
        @Override
        public void onServiceLost(NsdServiceInfo service) {
            // 當網路失效
            Log.e(TAG, "service lost" + service);
        }
        @Override
        public void onDiscoveryStopped(String serviceType) {
            Log.i(TAG, "Discovery stopped: " + serviceType);
        }
        @Override
        public void onStartDiscoveryFailed(String serviceType, int errorCode) {
            Log.e(TAG, "Discovery failed: Error code:" + errorCode);
            mNsdManager.stopServiceDiscovery(this);
        }
        @Override
        public void onStopDiscoveryFailed(String serviceType, int errorCode) {
            Log.e(TAG, "Discovery failed: Error code:" + errorCode);
            mNsdManager.stopServiceDiscovery(this);
        }
    };
}
NSD API 透過 DiscoveryListener 去通知你 discovery 的狀態
注意上例在找到 service 時進行了一些檢查:
1. 找到的 service name 必須與 local 的 service name 比對以確定裝置是否接收到自己的 broadcast
2. service type 會被檢查以驗證你的應用程式可以連接
3. service name 會被檢查以確定連接到正確的應用程式
然而檢查 service name 並不是必要的,例如應用程式可能只想要連接到其他裝置上相同應用程式的實體
(像網路印表機就只需要檢查service type 是 "_ipp._tcp")

設定好 DiscoveryListener 後就可以呼叫 discoverServices()
mNsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, mDiscoveryListener);

* Connect to Services on the Network

當找到可以連接的網路服務,首先要用 resolveService() 判讀連線資訊
我們必須實作 NsdManager.ResolveListener 接收結果並取得包含連線資訊的 NsdServiceInfo
public void initializeResolveListener() {
    mResolveListener = new NsdManager.ResolveListener() {

        @Override
        public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
            // 解析失敗。使用錯誤碼進行除錯
            Log.e(TAG, "Resolve failed" + errorCode);
        }

        @Override
        public void onServiceResolved(NsdServiceInfo serviceInfo) {
            Log.e(TAG, "Resolve Succeeded. " + serviceInfo);

            if (serviceInfo.getServiceName().equals(mServiceName)) {
                Log.d(TAG, "Same IP.");
                return;
            }
            //
            mService = serviceInfo;
            int port = mService.getPort();
            InetAddress host = mService.getHost();
        }
    };
}

* Unregister Your Service on Application Close

在應用程式的生命週期裡適當的啟用和停用 NSD 功能是很重要的
當應用程式關閉時將 NSD 反註冊有助於防止其他應用程式認為它仍在活躍中並嘗試連接它
而 service discovery 是很耗效能的操作,所以應該在 activity pause 的時候停止、在 resume 時重新啟用
//In your application's Activity
    @Override
    protected void onPause() {
        if (mNsdHelper != null) {
            mNsdHelper.tearDown();
        }
        super.onPause();
    }
    @Override
    protected void onResume() {
        super.onResume();
        if (mNsdHelper != null) {
            mNsdHelper.registerService(mConnection.getLocalPort());
            mNsdHelper.discoverServices();
        }
    }
    @Override
    protected void onDestroy() {
        mNsdHelper.tearDown();
        mConnection.tearDown();
        super.onDestroy();
    }
    // NsdHelper's tearDown method
        public void tearDown() {
        mNsdManager.unregisterService(mRegistrationListener);
        mNsdManager.stopServiceDiscovery(mDiscoveryListener);
    }

相關資料:

Connecting Devices Wirelessly
YouTube-DevBytes: Network Service Discovery
Using Network Service Discovery

2016/02/19

使用 Dagger 2 解決 Dependency Injection

Dagger 2 勝過大多數其他依賴性注入框架的一個主要優點是它嚴格地生成實作(no reflection)
而這意味著它可以被使用在 Android application 中
但還是有一些該注意的事項

Dagger 依賴 ProGuard 去事後處理編譯好的 bytecode
這允許 Dagger 發布在 Android 跟 server 會使用不同的工具鏈去產生在這兩種環境都能有效率執行的 bytecode
此外,Dagger 有一個明確的目標是確保它生成的 java 原始碼始終都可以兼容 ProGuard 的優化

Dagger 的基本精神在於:
Multiple injection points: dependencies, being injected.
Multiple bindings: dependencies, being provided.
Multiple modules: a collection of bindings that implement a feature.
Multiple object graphs: a collection of modules that implement a scope.

而 Dagger 2 與前代的差別在於:
No reflection at all: graph validation, configurations and preconditions at compile time.
Easy debugging and fully traceable: entirely concrete call stack for provision and creation.
More performance: according to google they gained 13% of processor performance.
Code obfuscation: it uses method dispatch, like hand written code.

* Android Studio build.gradle

要在 android 使用 dagger2 必須先編輯專案跟 app 的 build.gradle 檔設定
否則 dagger2 無法正常運作

專案的 build.gradle
buildscript {
    //...

    dependencies {
        classpath 'com.android.tools.build:gradle:1.5.0'
        // 增加 apt 支援
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}

app 的 build.gradle
apply plugin: 'com.android.application'
//apply plugin for apt
apply plugin: 'com.neenbedankt.android-apt'

android {
    //...
}

dependencies {
    //...

    //加入 dagger, dagger-compiler, 與 annotation 的 lib dependency
    apt 'com.google.dagger:dagger-compiler:2.1'
    compile 'com.google.dagger:dagger:2.1'
    provided 'javax.annotation:jsr250-api:1.0'
}


* Declaring Dependencies

使用 javax.inject.Inject annotation (Dependency Injection for Java (JSR 330))

@Inject
基本上加了這個標註就表示我們需要依賴性
換句話說,我們用它來告訴 Dagger 這個標註的類別或屬性要是依賴性注入的一部分
因此 Dagger 會建構該類別的實體並滿足它的依賴性

class Thermosiphon implements Pump {
  private final Heater heater;

  @Inject
  Thermosiphon(Heater heater) {
    this.heater = heater;
  }
  ...
}
標註 @Inject 的建構子,需要實體時 Dagger 會取得必要參數並呼叫該建構子

標註 @Inject 的屬性
class CoffeeMaker {
  @Inject Heater heater;
  @Inject Pump pump;
  ...
}
如果 class 有標註 @Inject 的屬性,但沒有標註 @Inject 的建構子
若有需要 Dagger 會注入這些屬性,但並不會建立新的實體
所以加入一個標註的 @Inject 無參數的建構子,可以讓 Dagger 去建立實體

Dagger 還支援 method injection,但比較推薦用建構子或屬性的 injection
Dagger 無法建構裡面沒有任何 @Inject 標註的 class

* Satisfying Dependencies

@Provides 是定義在 modules 裡面的 method
它告訴 Dagger 我們想要如何建構並提供那些被提出的依賴性

預設情況下 Dagger 會滿足建立實體所需要的每一個依賴性
但 @Inject 在以下狀況會無效:
- 不能被建構的 interface
- 無法加上標註的第三方類別
- Configurable objects must be configured!

所以在 @Inject 有所不足或立場尷尬的時候
可以用 @Provides 標註的 method 來滿足依賴性
這個 method 的回傳型別定義了它能滿足哪個依賴性
所有的 @Provides method 都必須屬於 @Module 之下
(為了便於辨識, @Provides methods 命名都以 provide 前綴,而 module classes 則以 Module 後綴)

@Provides static Heater provideHeater() {
  return new ElectricHeater();
}
@Provides method 也可能有自己的依賴性,例如下例如果需要 pump 時會回傳一個 Thermosiphon
@Provides static Pump providePump(Thermosiphon pump) {
  return pump;
}

Module 代表的是有提供依賴性 method 的類別
所以我們定義一個類別並加上 @Module 標註,Dagger 會知道建構類別實體時要去那裡找到需要被滿足的依賴性
module 的一個重要功能是他們被設計為可以分開或組合在一起(multiple composed modules)
@Module
class DripCoffeeModule {
  @Provides static Heater provideHeater() {
    return new ElectricHeater();
  }

  @Provides static Pump providePump(Thermosiphon pump) {
    return pump;
  }
}

* Building the Graph

@Inject 與 @Provides 標註的類別透過它們的依賴性組成一個 graph(物件圖)
Android 的 Application 則是透過一組完整定義的根源(root)來存取這個 graph
在 Dagger 2,這個根是由沒有參數、而且會回傳需要型別的一些 method 組成的一個 interface 定義而成

藉由 @Component 標註以及傳入 modules 參數,Dagger 2 可以自動生成它的實作
而這個實作會與 interface 同名但使用 Dagger 前綴
對該實作呼叫 builder() 方法可以取得一個 builder 用來設定依賴性,之後再呼叫 build() 建立實體
(若該 module 有可存取的預設建構子,可以省略 build() 呼叫,因為 builder 可以直接建立實體)
@Component(modules = DripCoffeeModule.class)
interface CoffeeShop {
  CoffeeMaker maker();
}
CoffeeShop coffeeShop = DaggerCoffeeShop.builder()
    .dripCoffeeModule(new DripCoffeeModule())
    .build();
@Component 基本上是個注入者的角色,它可以找到你所需要的型別實體
或者說它是 @Inject 跟 @Module 之間的橋樑,因為它的主要責任就是把這兩樣連起來
我們可以把一個 interface 加上 @Component 標註,然後列出所有組成這個 component 的全部 @Module
如果缺少了任何 Module,編譯時就會出現錯誤
所有 component 都可以透過 module 知道它們提供依賴性的 scope


注意:如果你的 @Component 不是最上層的類別,那麼生成的 component 將會包含它的封閉類別的名稱並用底線相連

如下例會生成 DaggerFoo_Bar_BazComponent
class Foo {
  static class Bar {
    @Component
    interface BazComponent {}
  }
}
如果 module 的 @Provides methods 全都是 static 的,那它的實作不需要建立實體
而若該 interface 內所有依賴性都不須使用者建立依賴的實體就可以建構,那生成的實作會有 create 方法可用,而不用透過 builder
CoffeeShop coffeeShop = DaggerCoffeeShop.create();

現在我們的 CoffeeApp 可以簡易的使用 Dagger 生成的 CoffeeShop 實作(DaggerCoffeeShop)
來得到完整注入的 CoffeeMaker
public class CoffeeApp {
  public static void main(String[] args) {
    CoffeeShop coffeeShop = DaggerCoffeeShop.create();
    coffeeShop.maker().brew();
  }
}
前面例子說明了如何用較典型的 binding 建構一個 component
但還有一些機制可以促進物件圖的綁定
下面這些就可作為依賴性,並可以用來生成格式完整的 component
- 有用 @Component.modules 或 @Module.includes 直接參照的 @Module 並標註為 @Provides 的 method
- 任何有 @Inject 建構子並 unscoped 或有 @Scope 標註與其匹配的 component
- 具有 component 依賴性的 component 規定的 methods
- component 本身
- 包含任何 subcomponent 的不合規定 builders
- 上述任何 bindings 的 Provider 或 Lazy wrappers
- 任何型別的 MembersInjector

@Scope 是非常強大而有用的功能,所有物件都沒有必要知道如何去管理它們的實體
Dagger 2 有更具體的方式來透過自訂標註去做 scoping
一個 scope 的例子:
有一個標註 @PerActivity 的 class
這個物件只要我們的 activity 還活著它就活著
換句話來說,我們可以定義 scope 的大小(例如 @PerFragment, @PerUser...之類的)

* Singletons and Scoped Bindings

對於標註 @Singleton 的 @Provides method 或可被注入的類別
graph 會對所有 client 使用單一實體
@Provides @Singleton static Heater provideHeater() {
  return new ElectricHeater();
}
標註 @Singleton 的可被注入類別

可以用來提醒潛在維護者,該類別有可能被多執行序共享
@Singleton
class CoffeeMaker {
  ...
}

由於 Dagger 2 會把物件圖 scope 的實體與 component 實作實體互相關聯

因此這些組件本身必須宣告它們自己在哪個 scope
例如 @Singleton 跟 @RequestScoped 不會 binding 在同一個 component
因為不同 scope 有不同生命週期,所以無法同時生活在不同生命周期裡

要宣告 component 與哪個 scope 相關只要加上該 scope 的標註就可以

@Component(modules = DripCoffeeModule.class)
@Singleton
interface CoffeeShop {
  CoffeeMaker maker();
}


* Lazy injections

有時你會需要一個可以延遲實體化的物件
binding T 型別我們可以用 Lazy<T>
它會等到 Lazy<T> 首次呼叫 get() 方法才建立實體
如果 T 是一個 singleton,那麼 Lazy<T> 在所有注入的 graph 上會是同一個實體
否則每個注入都會取得各自的 Lazy<T> 實體

無論如何,之後任何 Lazy<T> 實體的呼叫都會回傳同一個基本的 T 實體
class GridingCoffeeMaker {
  @Inject Lazy lazyGrinder;

  public void brew() {
    while (needsGrinding()) {
      // Grinder created once on first call to .get() and cached.
      lazyGrinder.get().grind();
    }
  }
}

* Provider injections

有時你會需要注入單一值而取得多個實體
這有幾個方式可以做(例如 Factories, Builders)
其中一個選擇是用 Provider<T>
Provider<T> 在每次呼叫 get() 時都會使用 T 的 binding 邏輯
如果 binding 邏輯是 @Inject 的建構子,就會回傳一個新建構的實體
但 @Provides method 則不一定

class BigCoffeeMaker {
  @Inject Provider<filter> filterProvider;

  public void brew(int numberOfPots) {
  ...
    for (int p = 0; p < numberOfPots; p++) {
      maker.addFilter(filterProvider.get()); //new filter every time.
      maker.addCoffee(...);
      maker.percolate();
      ...
    }
  }
}
注入 Provider<T> 可能會創造出易混淆的程式碼而且在 graph 中造成範圍錯誤 (mis-scoped) 或建構錯誤 (mis-structured) 的物件
通常你會想使用 factory 或 Lazy<T> 或重整生命週期及建構程式的方法,去達成 只注入 T 的目的
但注入 Provider<T> 在某些情況下可能成為救命稻草
一個常見的情況是當你必須使用傳統的架構,不與你物件的生命週期有關連
(例如 servlet 被設計為 singleton,但只有在請求特定資料時才有效 )

* Qualifiers

當 class type 不足以識別依賴性時我們可以用 @Qualifier
例如在 Android 我們會需要不同類型的 context
所以我們可以定義 @ForApplication 跟 @ForActivity” 標註
那當注入一個 context 時我們可以使用這些標註去告訴 Dagger 哪一種 context 是我們所需要的
你可以用 @Qualifier 標在任何標註上

例如一個複雜的咖啡機可能會把加熱器分為水的跟熱盤用的
所以 qualifier annotation 派上用場了
下例是 javax.inject 裡的 @Named 標註
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {
  String value() default "";
}
你可以創造自己的 Qualifier 或使用原本的 @Named
但一個依賴性無法有多個 qualifier 標註
class ExpensiveCoffeeMaker {
  @Inject @Named("water") Heater waterHeater;
  @Inject @Named("hot plate") Heater hotPlateHeater;
  ...
}
@Provides @Named("hot plate") static Heater provideHotPlateHeater() {
  return new ElectricHeater(70);
}

@Provides @Named("water") static Heater provideWaterHeater() {
  return new ElectricHeater(93);
}
* Compile-time Validation

Dagger 的 annotation processor 是嚴格的
如果有任何不合法或不完整的 binding 就會造成 compiler error
下例 component 裡的 module,Executor 缺少了一個 binding
@Module
class DripCoffeeModule {
  @Provides static Heater provideHeater(Executor executor) {
    return new CpuHeater(executor);
  }
}
[ERROR] COMPILATION ERROR :
[ERROR] error: java.util.concurrent.Executor cannot be provided without an @Provides-annotated method.
這個問題只要在 component 裡的任意 modules 的 Executor 加上 @Provides 標註即可
雖然 @Inject, @Module 與 @Provides 標註是被各別驗證
所有 binding 間的關聯驗證發生在 @Component 層
Dagger 1 依賴嚴格的 @Module 層驗證,但 Dagger 2 這樣的驗證(及 @Module 的配置參數)有利於全物件圖驗證

* Compile-time Code Generation

Dagger 的 annotation processor 也會生成名稱像是 CoffeeMaker_Factory.java 或 CoffeeMaker_MembersInjector.java 這樣的原始檔
這些檔案是 Dagger 實作細節
不需要直接使用它們,因為它們可以在 injection 中逐步除錯
唯一需要參考的是 Dagger 前綴的那些 component 類別

相關資料:

YouTube-DAGGER 2 - A New Type of dependency injection (slides) (coffee example)
YouTube-Dependency Injection Using Dagger 2
Dagger
Dagger & Android
Tasting Dagger 2 on Android

2016/02/16

OpenGL ES 入門 - 使用 GLSurfaceView-2

在前面一篇我們定義了形狀並在 GLSurfaceView 利用 GLSurfaceView.Renderer 畫出
也學習到 Vertex Shader, Fragment Shader, Program

接下來要開始互動的部分

Projection and Camera Views
Projection 投影
負責依照座標及寬高計算物品顯示出的投射
如果沒有經過這個計算物品可能會由於 view window 的不等比例而變形
一般來說投影計算只需在 OpenGL view 建立時或是在 renderer 的 onSurfaceChanged() 時進行

Camera Views - 攝影機視角
OpenGL ES 並沒有定義一個實際的 camera 物件
但是提供了一個實用方法去模擬攝影機
Camera View 只需在當 GLSurfaceView 建立時進行計算
或是之後我們移動它時進行動態計算

在 GLSurfaceView.Renderer 的 onSurfaceChanged
用 Matrix.frustumM() 填充投影
// mMVPMatrix 是 "Model View Projection Matrix" 的縮寫
private final float[] mMVPMatrix = new float[16];
private final float[] mProjectionMatrix = new float[16];
private final float[] mViewMatrix = new float[16];

@Override
public void onSurfaceChanged(GL10 unused, int width, int height) {
    GLES20.glViewport(0, 0, width, height);

    float ratio = (float) width / height;

    // 投影矩陣會在物件的 onDrawFrame() 中套用到座標上(投影並不會讓物件顯示出來,必須搭配 camera )
    Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
}
camera 是透過 Matrix.setLookAtM() 去計算物件的變形
@Override
public void onDrawFrame(GL10 unused) {
    ...
    // 設定 camera 位置 (View matrix)
    Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
    // 計算投影與 view 變形
    Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
    // 畫出形狀
    mTriangle.draw(mMVPMatrix);
}
為了套用 Projection 跟 Camera,也必須修改一下其他程式
public class Triangle {

    private final String vertexShaderCode =
        // 這個矩陣變數提供了一個轉接去使用 vertex shader 操作物件座標 
        "uniform mat4 uMVPMatrix;" +
        "attribute vec4 vPosition;" +
        "void main() {" +
        // 矩陣必須包含作為 gl_Position 的變更者
        // 注意 uMVPMatrix *必須在前面* 才能確保矩陣的乘法結果正確
        "  gl_Position = uMVPMatrix * vPosition;" +
        "}";

    // 用來存取及設定 view 的變形
    private int mMVPMatrixHandle;
    ...
    public void draw(float[] mvpMatrix) { // 由計算過的變形矩陣傳入
        ...
        // 取得形狀的變形矩陣的 handle 
        mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
        // 套用投影與 view 變形
        GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
        // 畫出形狀
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
        // 停用 vertex array
        GLES20.glDisableVertexAttribArray(mPositionHandle);
    }
    ...
}

Adding Motion

要旋轉一個物品的話
我們必須建立另一個變形矩陣並把它結合到投影與攝影機的變形矩陣
(記得檢查一下你的 Renderer 是否有把 RenderMode 設為 GLSurfaceView.RENDERMODE_WHEN_DIRTY)
private float[] mRotationMatrix = new float[16];

public void onDrawFrame(GL10 gl) {
    float[] scratch = new float[16];
    ...
    // 建立旋轉變形
    long time = SystemClock.uptimeMillis() % 4000L;
    float angle = 0.090f * ((int) time);
    Matrix.setRotateM(mRotationMatrix, 0, angle, 0, 0, -1.0f);

    // 結合旋轉變形矩陣到投影與攝影機
    // 一樣的,mMVPMatrix *必須在最前面* 才能確保結果正確
    Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);

    mTriangle.draw(scratch);
}

Responding to Touch Events

為了讓 OpenGL ES app 能回應觸控事件
必須在 GLSurfaceView 裡實做 onTouchEvent()
private final float TOUCH_SCALE_FACTOR = 180.0f / 320;
private float mPreviousX;
private float mPreviousY;

@Override
public boolean onTouchEvent(MotionEvent e) {

    float x = e.getX();
    float y = e.getY();
    
    //這裡我們只關注 ACTION_MOVE 
    switch (e.getAction()) {
        case MotionEvent.ACTION_MOVE:

            float dx = x - mPreviousX;
            float dy = y - mPreviousY;

            // 反轉旋轉方向 above the mid-line
            if (y > getHeight() / 2) {
              dx = dx * -1 ;
            }

            // 反轉旋轉方向 to left of the mid-line
            if (x < getWidth() / 2) {
              dy = dy * -1 ;
            }

            mRenderer.setAngle(
                    mRenderer.getAngle() +
                    ((dx + dy) * TOUCH_SCALE_FACTOR));
            //要求重新渲染
            requestRender();
    }

    mPreviousX = x;
    mPreviousY = y;
    return true;
}
另外由於 Renderer 不是在 ActivityThread 上面運行 所以我們的 public 角度屬性必須加上 volatile 關鍵字
public class MyGLRenderer implements GLSurfaceView.Renderer {
    ...
    public volatile float mAngle;

    public float getAngle() {
        return mAngle;
    }

    public void setAngle(float angle) {
        mAngle = angle;
    }
    ...
    public void onDrawFrame(GL10 gl) {
        ...
        float[] scratch = new float[16];

        // 把前面自動旋轉的 code 給 comment 起來
        // long time = SystemClock.uptimeMillis() % 4000L;
        // float angle = 0.090f * ((int) time);
        Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0, -1.0f);

        // Combine the rotation matrix with the projection and camera view
        // Note that the mMVPMatrix factor *must be first* in order
        // for the matrix multiplication product to be correct.
        Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);

        // Draw triangle
        mTriangle.draw(scratch);
    }
    ...
}
相關資料:
android developer-Displaying Graphics with OpenGL ES
android developer-OpenGL ES
android developer-Applying Projection and Camera Views
android developer-Adding Motion
android developer-Responding to Touch Events

OpenGL ES 入門 - 使用 GLSurfaceView-1

OpenGL 全文為 Open Graphics Library (OpenGL®)
OpenGL ES 則是專門為行動裝置而推出的

要在 android app 裡面必須透過 view container 才能畫出 OpenGL ES 圖形
一個比較直接的方法是使用 GLSurfaceView 跟 GLSurfaceView.Renderer

GLSurfaceView - 是一個供 OpenGL 繪製圖形的全螢幕 view container
GLSurfaceView.Renderer - 控制甚麼要被劃在 view 裡面
(如果不需要全螢幕的 OpenGL ES 圖形而只需要螢幕中的一部分的話
應該考慮 TextureView )

而對於全部靠自己刻出來的開發者類型
也可以透過 SurfaceView 來做
但是這需要非常多的程式碼去達成

以下介紹使用 GLSurfaceView 跟 GLSurfaceView.Renderer 的方式

* 在 Manifest 中宣告使用 OpenGL ES

使用 OpenGL ES 2.0 API 的宣告方式
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
如果是 3.0 的話 android:glEsVersion 就是 0x00030000 (Android 4.3 (API level 18)後支援)
3.1 則是 0x00030001 (Android 5.0 (API level 21)後支援)
由於他們都向前相容(3.x版本可以相容2.0)

如果使用了材質壓縮 (texture compression) 的話也要宣告
<supports-gl-texture android:name="GL_OES_compressed_ETC1_RGB8_texture" />
<supports-gl-texture android:name="GL_OES_compressed_paletted_texture" />

* 建立使用 OpenGL ES 的 Activity

將 GLSurfaceView instance 做為 Activity 的 ContentView
public class OpenGLES20Activity extends Activity {
    private GLSurfaceView mGLView;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mGLView = new MyGLSurfaceView(this);
        setContentView(mGLView);
    }
}

* GLSurfaceView 與 GLSurfaceView.Renderer

class MyGLSurfaceView extends GLSurfaceView {
    private final MyGLRenderer mRenderer;
    public MyGLSurfaceView(Context context){
        super(context);
        // Create an OpenGL ES 2.0 context
        setEGLContextClientVersion(2);
        mRenderer = new MyGLRenderer();
        // Set the Renderer for drawing on the GLSurfaceView
        setRenderer(mRenderer);
        // Render the view only when there is a change in the drawing data
        setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
    }
}

Renderer 有三個方法會被 Android 使用到
onSurfaceCreated() - 會被呼叫一次去設定 view 的 OpenGL ES 環境
onDrawFrame() - view 的每次重劃都會被呼叫
onSurfaceChanged() - 如果 view 的幾何 (geometry) 改變時會被呼叫 (例如螢幕方向改變)

這裡畫了一個黑背景在螢幕上
public class MyGLRenderer implements GLSurfaceView.Renderer {
    public void onSurfaceCreated(GL10 unused, EGLConfig config) {
        // Set the background frame color
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    }
    public void onDrawFrame(GL10 unused) {
        // Redraw background color
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
    }
    public void onSurfaceChanged(GL10 unused, int width, int height) {
        GLES20.glViewport(0, 0, width, height);
    }
}

* 定義形狀

繪製物體使用的是座標
例如畫三角形,我們需要將它的頂點定義在 array 裡
而為了獲得最大效率,我們將座標寫入 ByteBuffer 傳給 OpenGL ES 處理

OpenGL ES 預設會把 [0,0,0](X,Y,Z) 放置在 GLSurfaceView 的中心
[1,1,0] 是右上方、[-1,-1,0] 是左下方
注意繪製形狀時須依照逆時針順序,順序決定了形狀的正反面所以很重要

public class Triangle {

    private FloatBuffer vertexBuffer;
    // 每個頂點的座標數
    static final int COORDS_PER_VERTEX = 3;
    static float triangleCoords[] = {   // 按逆時針順序:
             0.0f,  0.622008459f, 0.0f, // 頂點
            -0.5f, -0.311004243f, 0.0f, // 左下
             0.5f, -0.311004243f, 0.0f  // 右下
    };
    //顏色值array:紅, 綠, 藍, alpha
    float color[] = { 0.63671875f, 0.76953125f, 0.22265625f, 1.0f };

    public Triangle() {
        // 初始化 ByteBuffer
        ByteBuffer bb = ByteBuffer.allocateDirect(
                // (座標值數量 * 每個 float 有 4 bytes)
                triangleCoords.length * 4);
        // 使用裝置原生的 byte 順序
        bb.order(ByteOrder.nativeOrder());

        // 從 ByteBuffer 建立 FloatBuffer
        vertexBuffer = bb.asFloatBuffer();
        // 在 FloatBuffer 中添加我們的三角形座標
        vertexBuffer.put(triangleCoords);
        // FloatBuffer 讀取位置歸零
        vertexBuffer.position(0);
    }
}

而若要畫一個矩形則要用兩個畫在一起的三角形
public class Square {

    private FloatBuffer vertexBuffer;
    private ShortBuffer drawListBuffer;

    // 每個頂點的座標數
    static final int COORDS_PER_VERTEX = 3;
    static float squareCoords[] = {
            -0.5f,  0.5f, 0.0f,   // 左上
            -0.5f, -0.5f, 0.0f,   // 左下
             0.5f, -0.5f, 0.0f,   // 右下
             0.5f,  0.5f, 0.0f }; // 右上

    private short drawOrder[] = { 0, 1, 2, 0, 2, 3 }; // 繪製頂點的順序

    public Square() {
        ByteBuffer bb = ByteBuffer.allocateDirect(squareCoords.length * 4);
        bb.order(ByteOrder.nativeOrder());
        vertexBuffer = bb.asFloatBuffer();
        vertexBuffer.put(squareCoords);
        vertexBuffer.position(0);

        // 初始化 drawListBuffer 要用的 ByteBuffer
        ByteBuffer dlb = ByteBuffer.allocateDirect(
                // drawOrder的長度 * 每個 short 有 2 bytes)
                drawOrder.length * 2);
        dlb.order(ByteOrder.nativeOrder());
        drawListBuffer = dlb.asShortBuffer();
        drawListBuffer.put(drawOrder);
        drawListBuffer.position(0);
    }
}

* 繪製形狀

在繪圖前必須先將我們的形狀類別載入及初始化
除非在你執行的過程中該形狀會改變,否則我們應該在 renderer 裡面的 onSurfaceCreated() 初始化
這樣對記憶體及處理效能較好
public class MyGLRenderer implements GLSurfaceView.Renderer {

    ...
    private Triangle mTriangle;
    private Square   mSquare;

    public void onSurfaceCreated(GL10 unused, EGLConfig config) {
        ...
        // 初始化
        mTriangle = new Triangle();
        mSquare = new Square();
    }
    ...
}

繪製形狀需要提供很多細節
具體來說有:
Vertex Shader - 頂點著色器 負責渲染形狀的頂點
Fragment Shader - 片段著色器 對形狀的面進行材質或顏色的渲染
Program - 一個包含著色器的 OpenGL ES 物件

Vertex Shader 跟 Fragment Shader 最少各需要一個
編譯後加入 Program 負責畫出形狀

著色器裡面的 OpenGL Shading Language (GLSL) 必續先在 OpenGL ES 環境下編譯
public class Triangle {

    private final String vertexShaderCode =
        "attribute vec4 vPosition;" +
        "void main() {" +
        "  gl_Position = vPosition;" +
        "}";

    private final String fragmentShaderCode =
        "precision mediump float;" +
        "uniform vec4 vColor;" +
        "void main() {" +
        "  gl_FragColor = vColor;" +
        "}";
    ...
}

編譯並加入 program 物件的動作要在形狀的建構子裡面做,因為只須執行一次
(而由於這動作非常耗費 CPU 與時間,所以應該避免執行超過一次)
public static int loadShader(int type, String shaderCode){

    //依類型創建一個頂點著色器(GLES20.GL_VERTEX_SHADER)
    //或片段渲染器(GLES20.GL_FRAGMENT_SHADER)
    int shader = GLES20.glCreateShader(type);

    // 加入著色器原始碼及編譯
    GLES20.glShaderSource(shader, shaderCode);
    GLES20.glCompileShader(shader);

    return shader;
}
public class Triangle() {
    ...

    private final int mProgram;

    public Triangle() {
        ...

        int vertexShader = MyGLRenderer.loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode);
        int fragmentShader = MyGLRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);

        // 建立 OpenGL ES 的 program
        mProgram = GLES20.glCreateProgram();

        // 加入 vertex shader 與 fragment shader 到 program
        GLES20.glAttachShader(mProgram, vertexShader);
        GLES20.glAttachShader(mProgram, fragmentShader);

        // 建立 OpenGL ES 的可執行 program
        GLES20.glLinkProgram(mProgram);
    }
}
這時我們就可以把形狀給畫出來
我們必須給 OpenGL ES 一些參數去告訴 rendering pipeline 怎麼畫
因為這些參數因為不同形狀而有所相異,所以我們可以把它放在形狀的 class 裡面

private int mPositionHandle;
private int mColorHandle;

private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;
private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex

public void draw() {
    // 將 program 加入 OpenGL ES 環境
    GLES20.glUseProgram(mProgram);

    // 取得頂點著色器的 vPosition 的 handle 
    mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
    // 啟用頂點的 handle
    GLES20.glEnableVertexAttribArray(mPositionHandle);
    // 準備三角形座標資料
    GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,
                                 GLES20.GL_FLOAT, false, vertexStride, vertexBuffer);

    // 取得片段著色器 vColor 的 handle 
    mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");
    // 設定顏色
    GLES20.glUniform4fv(mColorHandle, 1, color, 0);
    // 畫出三角形
    GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
    // 停用頂點 array
    GLES20.glDisableVertexAttribArray(mPositionHandle);
}

public void onDrawFrame(GL10 unused) {
    ...
    mTriangle.draw();
}


相關資料:
android developer-Displaying Graphics with OpenGL ES
android developer-OpenGL ES
android developer-Building an OpenGL ES Environment
android developer-Defining Shapes
android developer-Drawing Shapes

2016/02/15

System Permissions 系統權限

在 android 的安全架構下
沒有一個應用程式是預設有權限去執行任何影響到其他程式、系統、使用者
他們都運行在自己的 process sandbox 中
如果需要用到 basic sandbox 提供的權限以外的操作就要透過要求 permissions
而由於沙箱並不依賴建構app的技術,所以不管是用 Java, native, and hybrid 建構出來的應用程式都擁有同樣的安全性

不同的 app 在 device 上會有不同的 Linux user ID (UID) 以確保它們在不同的 process 上運行
但同一個 signature 的不同 app
可以藉由在 AndroidManifest 檔案中宣告 manifest tag 的 android:sharedUserId 屬性
去設定為同一個 string 標籤
這樣系統(在安全性上)會將這兩個 app 視為同一個應用程式

Declaring Permissions

System permissions 分為兩種:
Normal - 對使用者的隱私沒有直接影響。系統會自動給予權限而無須詢問使用者。
Dangerous - 訪問用戶的私密資料。用戶必須明確地給予批准。
不論是哪一種 permission 都需要在 AndroidManifest 檔案中宣告

<manifest ...>
    <uses-permission android:name="android.permission.SEND_SMS" />

    <application ...>
        ...
    </application ...>
</manifest ...>

Requesting Permissions at Run Time
Android 6.0 (API level 23) 之後對於牽涉到 user 隱私的行為更加重視
所以相關動作的權限都需要經過 user 的同意

Android 6.0 (API level 23) 之後關於權限的請求時機:
Device: Android 5.1 以下 target SDK: 22 以下
在安裝時必須授予權限。

Device: Android 6.0 以上 target SDK: 23 以上
應用程式運行時,在需要權限的每一個當下要求批准。

因此我們必須在執行需要該權限的操作時
每次都必須先檢查權限(因為用戶隨時可以自由撤銷許可)
而官方建議我們使用 SupportLibrary 來做會比較簡單

* 檢查權限
可以使用 android.support.v4.content.ContextCompat.checkSelfPermission(android.content.Context, String)
int permissionCheck = ContextCompat.checkSelfPermission(MyActivity.this,
        Manifest.permission.WRITE_CALENDAR);
// 結果為 PackageManager.PERMISSION_DENIED (Constant Value: -1) 
// 或 PackageManager.PERMISSION_GRANTED (Constant Value: 0)

* 請求權限

在請求權限時應該向使用者說明 app 需要該權限的原因
說明應該簡短扼要,以免使用者覺得太麻煩而移除你的 app
尤其是當使用者之前反對了你的權限請求,他更需要了解 app 為何需要這權限
android 提供了一個方法
ActivityCompat.shouldShowRequestPermissionRationale (Activity activity, String permission)
如果之前使用者被詢問過該權限而且請求被駁回,回傳值為 true
如果使用者駁回權限並勾選 "Don't ask again",則會回傳 false
(若該設備的權限政策禁止 app 擁有該權限也會回傳 false)

如果 app 沒有取得需要的權限
必須用 ActivityCompat.requestPermissions (Activity activity, String[] permissions, int requestCode)請求權限
該 activity 必須實作 ActivityCompat.OnRequestPermissionsResultCallback
然後從 onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) 中拿到請求的結果
(有些權限需要重啟 app,這時系統在把結果傳回 onRequestPermissionsResult 之前會 recreate activity stack)

int permissionCheck = ContextCompat.checkSelfPermission(MyActivity.this, Manifest.permission.READ_CONTACTS)
if(permissionCheck  != PackageManager.PERMISSION_GRANTED) {
    // 如果要提供使用者任何說明,應該做的是在呼叫 requestPermissions() 之前
    // 並嘗試在使用者閱讀完說明後再次要求權限
    if (ActivityCompat.shouldShowRequestPermissionRationale(MyActivity.this, Manifest.permission.READ_CONTACTS)) {
        // Show an explanation to the user *asynchronously*
        // don't block this thread waiting for the user's response!
    } else {
        // No explanation needed, we can request the permission.
        ActivityCompat.requestPermissions(MyActivity.this,
                new String[]{Manifest.permission.READ_CONTACTS},
                MY_PERMISSIONS_REQUEST_READ_CONTACTS  //an app-defined int constant
        );
        // 當呼叫 requestPermissions() 時,系統會顯示一個標準對話框。您的應用程序無法配置或改變該對話框。
    }
}

* 處理請求權限的結果

當使用者回應系統跳出的權限請求對話框,onRequestPermissionsResult() 會被呼叫並傳回結果
(requestPermissions 時傳入的 requestCode 也會回來)

@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
    switch (requestCode) {
        case MY_PERMISSIONS_REQUEST_READ_CONTACTS: {
            // 如果權限請求被取消了,grantResults array 的 length 會是 0
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // 權限請求通過的處理
            } else {
                // 權限請求駁回的處理(如告知使用者那些功能無法使用之類的)
            }
            return;
        }
        ...(其他 requestCode case)
    }
}
權限的請求是以群組 (permission group) 為單位
同一群組的權限不會分開詢問,因為系統會自動同意相同群組的權限
(但是權限群組有可能會有更動,所以不能依賴這個而只能每個權限都做詢問處理)

而如果使用者點選了"不再詢問我"的選項
以後該權限的請求對話框就不會再出現而會自動回傳 PackageManager.PERMISSION_DENIED 拒絕權限請求
所以權限請求駁回的時候
最好還是做好對應的回饋與引導讓使用者可以把權限重新打開比較適當

Permissions Best Practices

* 考慮使用 Intent

如果不需要自訂的介面或整合功能(或是該功能不是你 app 的主要功能)
這些需要權限的操作可以考慮使用 intent
讓系統或其他 app 來完成
(例如相機可以利用 ACTION_IMAGE_CAPTURE 這個 Intent action
他可以把結果透過 onActivityResult() 傳回)

* 別把使用者淹沒在權限請求中

若功能是你的主要功能,可以在 app 開啟時就請求權限(或是在你的 app 的功能導覽之後請求)
不然就該需要時才請求
別讓使用者一開始就被一堆權限請求壓垮

* 解釋為何需要這權限

在呼叫 requestPermissions() 前先跟使用者解釋你為何需要這權限是不錯的方法
官方也建議將這些解釋放在你 app 的功能導覽中(如果有的話)
但有些使用者會直接略過導覽,所以在需要用到相關功能時還是必須撰寫請求權限的 code

* 在新舊兩種權限模式下都要進行測試

在 API level 23 之後的建議步驟:
1- 找出你的 app 目前需要的權限及相關的程式位置
2- 測試受保護權限的功能和資料的 user flow
3- 測試准許或撤銷權限的各種組合
(如果有些權限准許、有些權限被撤銷,確保程式能夠處理)
4- 使用 adb 用 command line 管理權限:
依群組列出權限與狀態
$ adb shell pm list permissions -d -g
准許或撤銷權限
$ adb shell pm [grant|revoke]  ...
5- 分析你的 app 功能使用權限的狀況

附:Android 6.0.1 權限群組
$ adb shell pm list permissions -d -g
Dangerous Permissions:

group:com.google.android.gms.permission.CAR_INFORMATION
  permission:com.google.android.gms.permission.CAR_VENDOR_EXTENSION
  permission:com.google.android.gms.permission.CAR_MILEAGE
  permission:com.google.android.gms.permission.CAR_FUEL

group:android.permission-group.CONTACTS
  permission:android.permission.WRITE_CONTACTS
  permission:android.permission.GET_ACCOUNTS
  permission:android.permission.READ_CONTACTS

group:android.permission-group.PHONE
  permission:android.permission.READ_CALL_LOG
  permission:android.permission.READ_PHONE_STATE
  permission:android.permission.CALL_PHONE
  permission:android.permission.WRITE_CALL_LOG
  permission:android.permission.USE_SIP
  permission:android.permission.PROCESS_OUTGOING_CALLS
  permission:com.android.voicemail.permission.ADD_VOICEMAIL

group:android.permission-group.CALENDAR
  permission:android.permission.READ_CALENDAR
  permission:android.permission.WRITE_CALENDAR

group:android.permission-group.CAMERA
  permission:android.permission.CAMERA

group:android.permission-group.SENSORS
  permission:android.permission.BODY_SENSORS

group:android.permission-group.LOCATION
  permission:android.permission.ACCESS_FINE_LOCATION
  permission:com.google.android.gms.permission.CAR_SPEED
  permission:android.permission.ACCESS_COARSE_LOCATION

group:android.permission-group.STORAGE
  permission:android.permission.READ_EXTERNAL_STORAGE
  permission:android.permission.WRITE_EXTERNAL_STORAGE

group:android.permission-group.MICROPHONE
  permission:android.permission.RECORD_AUDIO

group:android.permission-group.SMS
  permission:android.permission.READ_SMS
  permission:android.permission.RECEIVE_WAP_PUSH
  permission:android.permission.RECEIVE_MMS
  permission:android.permission.RECEIVE_SMS
  permission:android.permission.SEND_SMS
  permission:android.permission.READ_CELL_BROADCASTS

相關資料:
android developer-Declaring Permissions
android developer-Requesting Permissions at Run Time
android developer-Permissions Best Practices
android developer-System Permissions
AOSP-android developer-Security

2016/01/19

Android Studio 的 dependencies setting 與 library module 輸出設定

Android Studio 的 dependencies setting 約有下面幾種類型:

1. 將某目錄下所有符合條件的檔案加入
下例即是將 libs 下的所有 .jar 檔加入
compile fileTree(include: ['*.jar'], dir: 'libs')

2. 從 marven central 中加入
寫法一般是 packageName:projectName:Version
若使用本機的 aar 檔記得加 @aar
compile 'com.android.support:appcompat-v7:23.0.1'
compile 'com.journeyapps:zxing-android-embedded:3.1.0@aar'

3. 加入某個匯入的 library module
寫法是 compile(project(':projectName'))
compile(project(':zxing-android-embedded')) { transitive = true }

4. 加入 aar 檔
下例是將 libs 下的特定 aar 檔案加入
dependencies {
    compile(name: 'nicelibrary-v1.0.3', ext: 'aar')
    ...
}
repositories {
    flatDir {
        dirs 'libs'
    }
    mavenCentral()
}

5. 加入某路徑下的 jar 檔
compile files('yyyy/xxxxxx.jar')

如果設定成功
在專案路徑下的 build/intermediates/

而在 gradle 中的輸出設定
正常狀況是輸出為 apk
寫法是這樣
apply plugin: 'com.android.application'

輸出為 aar 的話 (即作為 library module 的 project)
apply plugin: 'com.android.library'

輸出方式為:
1. 叫出 android studio 的 Gradle 面板(一般是在右側)
2. 找到要輸出的 Module 在 build 下面找到 assembleRelease
3. 按右鍵選擇 "Run"
若成功輸出
路徑為 build\outputs\aar
若 proguard 失敗就檢查 proguard 設定

相關資料: Android Studio 打包及引用 aar

2015/12/07

在 View 顯示在畫面上之前得知尺寸

View 在被 inflate 出來後一直到被顯示在畫面上之前
如果我們想要利用 getWidth(), getHeight() 或 getMeasuredWidth(), getMeasuredHeight() 取得寬高值的話
拿到的結果會不如預期

這問題我相信很多人都碰過
今天在網路上剛好看到一些資訊就順手記在這裡吧

原來 View 在顯示到畫面上之前會經過 measure() 跟 requestLayout() 這兩個步驟
所以問題就在 measure 這個地方

View.measure (int widthMeasureSpec, int heightMeasureSpec)
用來決定一個 View 的大小
需要傳入寬與高的 MeasureSpec 常數,正常狀態下會由其 parent 提供
而查詢 View.MeasureSpec 後得知共有三種值:
* UNSPECIFIED 其值為 0 (0x00000000),其 parent 沒有對寬/高給予限制
* EXACTLY 其值為 1073741824 (0x40000000),其 parent 已經給了寬/高值
* AT_MOST 其值為 -2147483648 (0x80000000),在限制大小內可以為任意寬/高

所以如果我們只是想要預先知道這個 View 的尺寸
一般簡單情況下用 0 (即為 MeasureSpec.UNSPECIFIED) 就可以了
不過由於這不是真正放到 parent 後的大小,所以不保證是最後的大小歐

另外在 API level 17 級以下的版本裡,如果 View 的是用 RelativeLayout 的話
在 makeMeasureSpec 時 (onMeasure 裡面會呼叫到) 會有 bug 導致錯誤
所以請確定 API level 大於 17 或不是使用 RelativeLAyout 作為 view 的根

相關資料:
developer.android.com - View
developer.android.com - View.MeasureSpec

Best Practices for Background Jobs - Scheduling Repeating Alarms

鬧鐘 (基於 AlarmManager class) 提供一個以時間為基礎進行操作的方式
有一些特點:
1. 在設定時間或週期啟動 Intent
2. 搭配 broadcast receiver 來啟動 service 及進行其他操作
3. 由於它在 app 之外運行,即使 app 沒有運行或休眠中我們仍可以用來觸發事件
4. 因為可以週期喚醒,所以 app 不用常駐而可以節省系統資源(比 Handler + Timer + Thread 省資源)

repeating alarm 是一個靈活度有限的簡單機制
如果需要網路操作的話這可能不會是一個最佳選擇
設計不當的話甚至會榨乾電池及造成系統負載

常見的狀況是在 app 外觸發操作去跟 server 進行資料同步
但如果你對 server 有足夠的掌控權的話應該使用 Google Cloud Messaging (GCM) 搭配 sync adapter 會比較好
sync adapter 比 AlarmManager 更有靈活性

官方建議了一些關於 alarm 的 Best practices:
* 對任何 network requests 加入隨機性觸發
- 當 alarm 觸發時執行任何 local (不須網路的)工作
- 同時安排一個在隨機時間觸發的 alarm 去執行 network requests
* alarm 越少、越不頻繁,越好
* 若無必要不要喚醒 device
* 使用 setInexactRepeating() 取代 setRepeating()
android 可以把不同 app 的 repeating alarm 同步及同時觸發,這會減少系統喚醒 device 的次數並改善電池消耗
(在 Android 4.4 (API Level 19)之後,所有的 repeating alarms 都是 inexact)
* 盡量避免根據時鐘的時間設定 alarm
定義精確時間的 alarm 比較無法展延,可能的話使用 ELAPSED_REALTIME 這個 alarm type

一個 repeating alarm 有以下幾個屬性:
* alarm type
- ELAPSED_REALTIME (建議選項,適合間隔時間的鬧鐘,以 time since system boot 作為參照,所以不受時區影響)
- ELAPSED_REALTIME_WAKEUP (WAKEUP 版本會在 screen off 狀況下喚醒 CPU,以確保能準時觸發)
- RTC (real time clock) 適合固定時間的鬧鐘或與地區相關的運用,使用 UTC (wall clock)為參照
- RTC_WAKEUP (RTC 的 WAKEUP 版本)

* trigger time - 如果時間已經過了,alarm 會立即觸發
* interval(間隔)
* PendingIntent - alarm 觸發時會啟動。你的第二個 alarm 也會使用同一個 PendingIntent(把原本的蓋過)

接下來看一下官方提供的範例

ELAPSED_REALTIME_WAKEUP:
30 分鐘後喚醒設備並啟動鬧鐘,每 30 分鐘重複一次
// 每半小時喚醒還是蠻耗電的,官方強烈希望你把間隔拉長一點
alarmMgr.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
        AlarmManager.INTERVAL_HALF_HOUR,
        AlarmManager.INTERVAL_HALF_HOUR, alarmIntent);
一分鐘後啟動的單次鬧鐘
alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
        SystemClock.elapsedRealtime() + 60 * 1000, alarmIntent);

RTC_WAKEUP:
鬧鐘訂於大約下午兩點,並重複於每日同一時間
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
calendar.set(Calendar.HOUR_OF_DAY, 14);

// setInexactRepeating() 必須傳入 AlarmManager interval 常數,這裡用的是 AlarmManager.INTERVAL_DAY
// 注意時間會是大約值,無法非常精確
alarmMgr.setInexactRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(),
        AlarmManager.INTERVAL_DAY, alarmIntent);
喚醒設備於當日 8:30 am,每 20 分鐘重複
private AlarmManager alarmMgr;
private PendingIntent alarmIntent;
...
alarmMgr = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(context, AlarmReceiver.class);
alarmIntent = PendingIntent.getBroadcast(context, 0, intent, 0);

Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
calendar.set(Calendar.HOUR_OF_DAY, 8);
calendar.set(Calendar.MINUTE, 30);

// 使用 setRepeating 可以精確指定鬧鐘響的時間,重複時間就不用 AlarmManager 常數,而是直接給間隔的毫秒值
// 再提醒一次:在 Android 4.4 (API Level 19)之後,所有的 repeating alarms 都是 inexact
alarmMgr.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), 1000 * 60 * 20, alarmIntent);

Cancel an Alarm
在 PendingIntent 裡我們可以取出 alarmMgr 再用 alarmMgr.cancel 取消鬧鐘
//如果鬧鐘已經設定了就取消
if (alarmMgr!= null) {
    alarmMgr.cancel(alarmIntent);
}

Start an Alarm When the Device Boots
當關機時,所有鬧鐘都會被取消,所以我們可以在開機時重設鬧鐘
首先當然我們要宣告接收 BOOT_COMPLETED 事件,還有該事件的 receiver
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
...
<receiver android:name=".SampleBootReceiver"
        android:enabled="false">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED"></action>
    </intent-filter>
</receiver/>
然後實作這個 BroadcastReceiver 去接收 BOOT_COMPLETED
public class SampleBootReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getAction().equals("android.intent.action.BOOT_COMPLETED")) {
            // Set the alarm here.
        }
    }
}
由於上面在 manifest 中把 receiver 的 android:enabled 設為 false 以避免不必要的通知
所以我們在需要接收通知時在程式中將它 enable
ComponentName receiver = new ComponentName(context, SampleBootReceiver.class);
PackageManager pm = context.getPackageManager();

pm.setComponentEnabledSetting(receiver,
        PackageManager.COMPONENT_ENABLED_STATE_ENABLED, //或 PackageManager.COMPONENT_ENABLED_STATE_DISABLED
        PackageManager.DONT_KILL_APP);
一旦設為 PackageManager.COMPONENT_ENABLED_STATE_ENABLED 後,因為會覆蓋原本 manifest 的設定,即使重新開機它也會保持在 enable
所以當使用者取消鬧鐘(或我們不再需要接收這事件的情況下)可以再用一樣方法將 enabled 設回 PackageManager.COMPONENT_ENABLED_STATE_DISABLED

相關資料:
Managing Device Awake State
Scheduling Repeating Alarms

Best Practices for Background Jobs - Keeping the Device Awake

當 android device 被閒置的時候
它會先變暗,關閉螢幕,最後關閉 CPU 以節省電源
但有時我們需要螢幕長時間開啟(如遊戲或影片)
或不需要螢幕開啟但是希望保持 CPU 運作直到某些操作完成
就需要一些方法

Keep the Screen On
設定 FLAG_KEEP_SCREEN_ON (只能在 Activity 而不能在 service 或其他 app component)
public class MainActivity extends Activity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
  }
這不需要特別的 permission 而且使用者在不同 app 間切換時我們不需要擔心資源會被釋放掉
另一個方法是在 layout xml 檔案中使用 android:keepScreenOn 屬性
這等同於使用 FLAG_KEEP_SCREEN_ON
但是若之後需要將 flag 移掉的話用 FLAG_KEEP_SCREEN_ON 會比較方便
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:keepScreenOn="true">
    ...
</RelativeLayout>

Keep the CPU On
如果要保持 CPU 持續運作以完成某些工作
可以用 WAKE_LOCK 這個 PowerManager 的 system service
但因為這很容易讓電池電量被榨乾
所以官方強烈建議只在必要時適當使用(例如不應該在 activity 中使用,要保持 activity 螢幕開啟應使用 FLAG_KEEP_SCREEN_ON)

其他可選的方案有:
1. 如果 app 要進行長時間的 HTTP download,使用 DownloadManager
2. 如果 app 要從外部 server 同步資料,那就建立一個 sync adapter
3. 如果 app 要依賴 background service,使用 repeating alarms 或 Google Cloud Messaging 來觸發

要使用 WAKE_LOCK 首先要宣告 uses-permission
<uses-permission android:name="android.permission.WAKE_LOCK" />

然後建議的方法是使用 WakefulBroadcastReceiver 來管理 WAKE_LOCK
不過如果不是用這方法可以直接用 code 設定
PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyWakelockTag");
wakeLock.acquire();
//釋放 wake lock
wakelock.release()

接下來就來介紹 WakefulBroadcastReceiver

WakefulBroadcastReceiver 是一種特別的 broadcast receiver
它負責創建並管理我們 app 中的 PARTIAL_WAKE_LOCK
將工作傳給 service (通常是 IntentService)並確保 device 不會休眠
如果我們沒有持有 wake lock 就把工作丟給 service 就無法確保 device 不會在工作完成前就休眠

使用 WakefulBroadcastReceiver 的第一步是在 manifest 宣告
<receiver android:name=".MyWakefulReceiver"></receiver>

接著使用 startWakefulService 就可以在 service 啟動時持有 wake lock
傳入的 Intent 包含一個 extra 用來識別 wake lock

public class MyWakefulReceiver extends WakefulBroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {

        // Start the service, keeping the device awake while the service is
        // launching. This is the Intent to deliver to the service.
        Intent service = new Intent(context, MyIntentService.class);
        startWakefulService(context, service);
    }
}

當 service 完成時會呼叫 WakefulBroadcastReceiver 的 completeWakefulIntent() 以釋放 wake lock
public class MyIntentService extends IntentService {
    public static final int NOTIFICATION_ID = 1;
    private NotificationManager mNotificationManager;
    NotificationCompat.Builder builder;

    public MyIntentService() {
        super("MyIntentService");
    }
    @Override
    protected void onHandleIntent(Intent intent) {
        Bundle extras = intent.getExtras();
        // 進行一些工作
        ...(省略)
        // 最後釋放 WakefulBroadcastReceiver 提供的 wake lock
        MyWakefulReceiver.completeWakefulIntent(intent);
    }
}

相關資料:
Managing Device Awake State
Keeping the Device Awake

2015/12/04

Best Practices for Background Jobs - CursorLoader

查詢 ContentProvider 是可能造成 UI 停頓的工作
官方建議我們可以使用 CursorLoader 在背景進行非同步的查詢
然後再將結果送回 activity
而當資料改變時 CursorLoader 也會自動重新進行查詢
(以下的範例是使用 support library 的 CursorLoader 版本)

要使用 CursorLoader 必須實作 LoaderManager.LoaderCallbacks interface
為了獲得像 Fragment 一樣的 CursorLoader 支援,範例這裡我們繼承 FragmentActivity

然後在得到查詢條件後使用 LoaderManager.initLoader() 進行初始化
(LoaderManager 透過 Fragment 的 getLoaderManager() 或 support 版的 getSupportLoaderManager())
如果不須查詢條件也可以直接在 onCreate 或 onCreateView 裡直接初始化
public class PhotoThumbnailFragment extends FragmentActivity implements
        LoaderManager.LoaderCallbacks<cursor> {
    ...
    private static final int URL_LOADER = 0; //loader 的給定 ID

    public String[] mFromColumns = { DataProviderContract.IMAGE_PICTURENAME_COLUMN };
    public int[] mToFields = { R.id.PictureName };
     ...
    public View onCreateView(LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
        ...
        // 初始化 CursorLoader. The URL_LOADER 最後會傳入 onCreateLoader()
        getSupportLoaderManager().initLoader(URL_LOADER, null, this);
        ...
        ListView mListView = (ListView) findViewById(R.id.dataList);

        SimpleCursorAdapter mAdapter =
            new SimpleCursorAdapter(
                    this,                // Current context
                    R.layout.list_item,  // Layout for a single row
                    null,                // No Cursor yet
                    mFromColumns,        // Cursor columns to use
                    mToFields,           // Layout fields to use
                    0                    // No flags
            );
        mListView.setAdapter(mAdapter);
    }
    ...
    // 當系統把Loader 初始化並準備好進行查詢時會呼叫 onCreateLoader 這個 Callback
    @Override
    public Loader<cursor> onCreateLoader(int loaderID, Bundle bundle){
        //依照 loaderID 進行處理
        switch (loaderID) {
            case URL_LOADER:
                // 回傳 CursorLoader,當查詢完成時會呼叫 onLoadFinished()
                return new CursorLoader(
                        this,           // Parent activity context
                        mDataUrl,       // Table to query
                        mProjection,    // Projection to return
                        null,           // No selection clause
                        null,           // No selection arguments
                        null            // Default sort order
                );
            default:
                // 不合理的 id 就回傳 null
                return null;
        }
    }
    ...
    // 完成查詢時會呼叫 onLoadFinished
    @Override
    public void onLoadFinished(Loader<cursor> loader, Cursor cursor) {
        ...
        //更新 cursor
        mAdapter.changeCursor(cursor);
    }
    ...
    // 如果資料改變則 onLoaderReset 會被呼叫
    @Override
    public void onLoaderReset(Loader<cursor> loader) {
        // 把 adapter 中 Cursor 的 reference 清掉,防止 memory leaks.
        mAdapter.changeCursor(null);
    }
}

相關資料:
Loading Data in the Background
Running a Query with a CursorLoader

2015/12/03

Best Practices for Background Jobs - IntentService

IntentService 是執行簡單後端操作的建議選項
其不受 UI 生命週期的影響,所以可以運行在一些 AsyncTask 會被關閉的情況
但限制有:
1. 它不會直接與 UI 進行交互,要顯示結果的話請送到 Activity
2. 同時只能有一個 IntentService 在跑,多個的話會排隊等待
3. IntentService 無法被中斷

使用條件為
1. extends IntentService
2. Override onHandleIntent(Intent)
public class RSSPullService extends IntentService {
    @Override
    protected void onHandleIntent(Intent workIntent) {
        // 從參數的 intent 中取出資料
        String dataString = workIntent.getDataString();
        // 根據取出的資料進行一些工作
        ...(省略)
        // 把結果放入 intent 中,然後 sendBroadcast 出去
        Intent localIntent = new Intent(Constants.BROADCAST_ACTION)
                .putExtra(Constants.EXTENDED_DATA_STATUS, status);
        LocalBroadcastManager.getInstance(this).sendBroadcast(localIntent);
    }
}
public final class Constants {
    ...
    // 定義用來收結果的 Intent action
    public static final String BROADCAST_ACTION =
        "com.example.android.threadsample.BROADCAST";
    // 結果的資料欄位
    public static final String EXTENDED_DATA_STATUS =
        "com.example.android.threadsample.STATUS";
    ...
}
在一般的 Service 類別會有的 onStartCommand() 之類的方法會被自動調用
所以不需要去 Override

另外還需要在 manifest 進行宣告
<application
    android:icon="@drawable/icon"
    android:label="@string/app_name">
    ...
    <!-- android:exported 設為 "false" 所以只能在這個 app 內被使用-->
    <service
        android:name=".RSSPullService"
        android:exported="false"/>
    ...
<application/>
這裡沒有宣告 intent-filter,是因為下面會明確呼叫這個 intent
這也意謂只有同一個 app 內相同 user ID 才能存取它
//建立 Intent 並放入 data
mServiceIntent = new Intent(getActivity(), RSSPullService.class);
mServiceIntent.setData(Uri.parse(dataUrl));
// 執行 IntentService (在 onHandleIntent() 裡面的 code 就會開始執行)
getActivity().startService(mServiceIntent);

從前面的 code 知道執行結果會透過 sendBroadcast 發出來
所以我們要用 BroadcastReceiver 收
private class DownloadStateReceiver extends BroadcastReceiver {
    // 防止實體化
    private DownloadStateReceiver() {}

    @Override
    public void onReceive(Context context, Intent intent) {
        // 當 BroadcastReceiver 收到註冊的 Intent 時會執行 onReceive
        ...
    }
}
並在 activity 中定義 IntentFilter
// Class that displays photos
public class DisplayActivity extends FragmentActivity {
    ...
    public void onCreate(Bundle stateBundle) {
        ...
        super.onCreate(stateBundle);
        ...
        // 定義 IntentFilter
        IntentFilter statusIntentFilter = new IntentFilter(Constants.ACTION_ZOOM_IMAGE);

        IntentFilter mStatusIntentFilter = new IntentFilter(Constants.BROADCAST_ACTION);
        // Adds a data filter for the HTTP scheme
        mStatusIntentFilter.addDataScheme("http");

        ...

        //註冊 BroadcastReceiver 跟 IntentFilter
        DownloadStateReceiver mDownloadStateReceiver = new DownloadStateReceiver();
        LocalBroadcastManager.getInstance(this).registerReceiver(
                mDownloadStateReceiver, mStatusIntentFilter);

        LocalBroadcastManager.getInstance(this).registerReceiver(
                mDownloadStateReceiver, mIntentFilter);
        ...
BroadcastReceiver 並不會讓 Activity 自動顯示在 foreground
所以如果要通知使用者可以用 Notification
(請不要採取收到 Broadcast 就去開啟新 Activity 的做法)

相關資料:
Creating a Background Service
Sending Work Requests to the Background Service
Reporting Work Status

2015/11/27

Data Binding-3


Data Objects

POJO(plain old Java object) 可以用來作為 data binding
但是 POJO 的變動不會導致 UI 的更新
如果要真正發揮 data binding 的能力可以使用以下三種通知機制之一
Observable objects, observable fields, observable collections

Observable Objects

實作 Observable interface 的類別允許 binding 附加一個 listener 來監聽這個物件的屬性改變
Observable interface 有 add/remove listener 的機制
但是通知的部分則是由開發者自己決定
BaseObservable 已經實作了註冊 listener 的機制
通知則要在 getter 上加一個 Bindable 標記並在 setter 內通知
private static class User extends BaseObservable {
   private String firstName;
   private String lastName;
   @Bindable
   public String getFirstName() {
       return this.firstName;
   }
   @Bindable
   public String getLastName() {
       return this.lastName;
   }
   public void setFirstName(String firstName) {
       this.firstName = firstName;
       //加了 Bindable 標記會在編譯時在 module package 下生成一個 BR class
       notifyPropertyChanged(BR.firstName);
   }
   public void setLastName(String lastName) {
       this.lastName = lastName;
       notifyPropertyChanged(BR.lastName);
   }
}

如果 data classes 的 base class 無法是 BaseObservable
可以實作 Observable interface 利用 PropertyChangeRegistry 去達成目的

Observable fields

如果覺得建立 Observable classes 的方式有點麻煩
我們也可以使用 ObservableField 與其兄弟姊妹
ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat, ObservableDouble, ObservableParcelable

ObservableField 是內含單一屬性欄位的 observable object
而 primitive 的版本則是在存取時避免了 boxing 跟 unboxing 動作
如果要使用 Observable fields 記得要將其宣告為 public final
private static class User {
   public final ObservableField<string> firstName = new ObservableField<>();
   public final ObservableField<string> lastName = new ObservableField<>();
   public final ObservableInt age = new ObservableInt();
}
使用的話是透過 set 跟 get method
user.firstName.set("Google");
int age = user.age.get();

Observable collections

動態資料結構的話
Observable collections 允許使用 key 來存取資料

如果 key 是 reference type 的話
可以用 ObservableArrayMap
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);

在 layout 檔中的使用方法也是透過 key 來存取
<data>
    <import type="android.databinding.ObservableMap"/>
    <variable name="user" type="ObservableMap&lt;String, Object>"/>
</data>
...(省略)
<TextView
   android:text='@{user["lastName"]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
<TextView
   android:text='@{String.valueOf(1 + (Integer)user["age"])}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

如果 key 是 integer 的話
就使用 ObservableArrayList

ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);
在 layout 檔中是透過索引來存取
<data>
    <import type="android.databinding.ObservableList"/>
    <import type="com.example.my.app.Fields"/>
    <variable name="user" type="ObservableList&lt;Object>"/>
</data>
...(省略)
<TextView
   android:text='@{user[Fields.LAST_NAME]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
<TextView
   android:text='@{String.valueOf(1 + (Integer)user[Fields.AGE])}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

Generated Binding

自動產生的 binding 類別負責連接 layout variables 跟 Views
(如前篇所述,其名稱跟 package 可以自訂)
所有生成的 binding class 都繼承自 ViewDataBinding

Creating

binding 應在 inflate 後馬上建立以確保 View hierarchy 裡的 view 的 binding 及 layout 裡面的 expressions 能有效運作
而 binding 的方式不只一種
較常見的是使用 Binding class 的 static methods 將 inflate 跟 binding 一步完成
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);
若是 layout 已經 inflate 出來了
那就只要進行 binding 就好
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);

所以前篇範例中原本的
ActivityMainBinding testbinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
由於 binding 的部分都在 include 進來的 content_main.xml 中(其 id 我們取為 contentMain)
可以改為
setContentView(R.layout.activity_main);
ContentMainBinding testbinding = ContentMainBinding.bind(findViewById(R.id.contentMain));
把 setContentView 跟 bind 分開

有的時候我們無法提前知道要用哪個 binding 類別
這時可以用 DataBindingUtil class
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId,
    parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);

Views With IDs and Variables

layout 裡的每個 view 都會照它們的 id 生成一個 public final 欄位(沒有給 id 的就不會有)
這會比我們自己一個個去 findViewById 更快
(也就是說原則上不需要 data binding 的 view 就不用給 id,但當然這並非絕對)
而每個變數欄位都會生成 setter 跟 getter

ViewStubs

ViewStubs 跟正常的 view 有些不同
它們一開始是不可見的,直到被設為 visible 或是被 inflate 的時候,它們才會被 inflate 出來

因為原本在 View hierarchy 中不存在,所以 binding object 的 view 當然也不存在
而 Views 是 final 的,所以會先用 ViewStubProxy 代替 ViewStub
以便在 inflate 之後能操控 ViewStub
在 inflate 一個 layout 以後,新的 layout 必須做 binding 的動作
因此 ViewStubProxy 必須監聽 ViewStub.OnInflateListener 並在這時建立 binding
又 ViewStub 跟 ViewStubProxy 不會同時存在
所以 ViewStubProxy 也允許設定 OnInflateListener (在 binding 建立後會被呼叫)


Advanced Binding

Dynamic Variables

有時我們無法肯定哪個 binding 要被用
例如 RecyclerView.Adapter 可能不會只使用某一特定 layout
但卻必須把 value 在 onBindViewHolder(VH, int) 給綁定
這時我們可以透過 BindingHolder.getBinding() 來傳回 ViewDataBinding 物件
public void onBindViewHolder(BindingHolder holder, int position) {
   final T item = mItems.get(position);
   holder.getBinding().setVariable(BR.item, item);
   holder.getBinding().executePendingBindings();
}

Immediate Binding

當變數或 observable 改變時
binding 會排程在下一個 frame 前更新
若要強制馬上執行可以用 executePendingBindings()

Background Thread

若資料不是 collection 型態,可以在 background thread 改變資料
data binding 會驗證每個欄位以避免錯誤並更新數值

Attribute Setters

當綁定的資料改變時,binding class 會呼叫 setter
由於 data binding framework 會自動挑選最適合的 setter 來設值
例如 android:text 一般是用 setText(String)
但如果 expression 回傳的值是 int,則會尋找是否有 setText(int)
因此若回傳值不是預期的型別,記得要先轉型
不過即使沒有符合名稱的屬性,data binding 還是會運作
如下例 app:scrimColor 跟 app:drawerListener 都是自訂的屬性
<android.support.v4.widget.DrawerLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:scrimColor="@{@color/scrim}"
    app:drawerListener="@{fragment.drawerListener}"/>

Renamed Setters

有些 setter 跟屬性的名稱並不相符
這時可以加上 BindingMethods 標記
例如 android:tint 其實是對應到 setImageTintList(ColorStateList)
開發者不應該要處理這種事情而應該由 android framework 來實作
@BindingMethods({
    @BindingMethod(type = "android.widget.ImageView",
                   attribute = "android:tint",
                   method = "setImageTintList"),
})

Custom Setters

有些屬性需要自訂 binding 邏輯
例如沒有 setter 是對應到 android:paddingLeft 而是 setPadding(left, top, right, bottom)
我們可以用加了 BindingAdapter 標記的 static method 來自定 setter
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
   view.setPadding(padding,
                   view.getPaddingTop(),
                   view.getPaddingRight(),
                   view.getPaddingBottom());
}

BindingAdapter 也可以用來指定自訂屬性的對應 setter
@BindingAdapter({"bind:imageUrl", "bind:error"})
public static void loadImage(ImageView view, String url, Drawable error) {
   Picasso.with(view.getContext()).load(url).error(error).into(view);
}
<ImageView app:imageUrl=“@{venue.imageUrl}”
app:error=“@{@drawable/venueError}”/>
如上例 loadImage 在設定 imageUrl 跟 error 屬性時都會被呼叫
且 imageUrl 是一個 string 而 error 是一個 drawable
(注意到 namespaces 是被忽略的,所以我們也可以覆蓋 android 這個 namespace 裡的屬性 setter)

Binding adapter method 還可以將原本的舊值取來用
(不過這不是必要的參數)
記住如果要用的話記得所有的舊值要在新值前面
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int oldPadding, int newPadding) {
   if (oldPadding != newPadding) {
       view.setPadding(newPadding,
                       view.getPaddingTop(),
                       view.getPaddingRight(),
                       view.getPaddingBottom());
   }
}

Event handler 的部分只限於用 interface 或只有一個 abstract method 的 abstract class
@BindingAdapter("android:onLayoutChange")
public static void setOnLayoutChangeListener(View view, View.OnLayoutChangeListener oldValue,
       View.OnLayoutChangeListener newValue) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        if (oldValue != null) {
            view.removeOnLayoutChangeListener(oldValue);
        }
        if (newValue != null) {
            view.addOnLayoutChangeListener(newValue);
        }
    }
}
如果 listener 有多個 method
就必須分成不同 listeners
例如 View.OnAttachStateChangeListener 有 onViewAttachedToWindow() 跟 onViewDetachedFromWindow() 兩個 method
所以要先建立兩個 interface 把它們分開
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
    void onViewDetachedFromWindow(View v);
}

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
    void onViewAttachedToWindow(View v);
}
然後因為改變其中一個 listener 會影響到另一個
所以我們必須寫成三個 binding adapter (還真麻煩ㄆ)
@BindingAdapter("android:onViewAttachedToWindow")
public static void setListener(View view, OnViewAttachedToWindow attached) {
    setListener(view, null, attached);
}

@BindingAdapter("android:onViewDetachedFromWindow")
public static void setListener(View view, OnViewDetachedFromWindow detached) {
    setListener(view, detached, null);
}

@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"})
public static void setListener(View view, final OnViewDetachedFromWindow detach, final OnViewAttachedToWindow attach) {

    if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
        final OnAttachStateChangeListener newListener;
        if (detach == null && attach == null) {
            newListener = null;
        } else {
            newListener = new OnAttachStateChangeListener() {
                @Override
                public void onViewAttachedToWindow(View v) {
                    if (attach != null) {
                        attach.onViewAttachedToWindow(v);
                    }
                }
                @Override
                public void onViewDetachedFromWindow(View v) {
                    if (detach != null) {
                        detach.onViewDetachedFromWindow(v);
                    }
                }
            };
        }
        final OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view,
                newListener, R.id.onAttachStateChangeListener);
        if (oldListener != null) {
            view.removeOnAttachStateChangeListener(oldListener);
        }
        if (newListener != null) {
            view.addOnAttachStateChangeListener(newListener);
        }
    }
}
android.databinding.adapters.ListenerUtil 類別可以有助於追蹤到原有的 listeners
讓我們可以在 Binding Adaper 裡面將它移除

官方提到這個例子比正常的狀況複雜
因為 View 的 listener 一般是用 add 跟 remove 而不是 setter

由於我們加上 @TargetApi(VERSION_CODES.HONEYCOMB_MR1) 標記
data binding code generator 知道這只會在 Honeycomb MR1 上執行
新的系統版本會使用 addOnAttachStateChangeListener(View.OnAttachStateChangeListener)
addOnAttachStateChangeListener(View.OnAttachStateChangeListener)

Converters

前面提到 data binding 會自動挑選適合的 setter
<TextView
   android:text='@{userMap["lastName"]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
但如果型別會造成混亂
就必須自訂
<View
   android:background="@{isError ? @color/red : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
background 要求的是 Drawable 型別
但是拿到的卻是 int
所以我們必須使用 BindingConversion 標記
先把值轉成 ColorDrawable
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
   return new ColorDrawable(color);
}
不過不允許混用型別
像下面這樣就是不行的
<View
   android:background="@{isError ? @drawable/error : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

相關資料:
Data Binding Guide
Presentation - A Brief Intro of Android Data Binding