標籤雲

搜尋此網誌

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