2014年3月20日 星期四

iOS 開發筆記 - 在 Compiler Time 使用 __has_include 偵測 Framework 是否存在

__has_include Framework

以 FacebookSDK 來說,之前一直都用 github.com/facebook/facebook-ios-sdk 來使用,最近發現 source tree 預設有一些 bug 無法在 Xcode 5.1 編譯成功,自行修又容易碰到維護問題,於是就跑去下載 Facebook 官方打包好的 facebook-ios-sdk-current.pkg 來用了。

之前用 Facebook source code 來編譯時,在 header file 的使用:

#import "Facebook.h"

然而,現在改用 FacebookSDK 時,則該使用

#import <FacebookSDK/FacebookSDK.h>

因此想要在 Compiler Time 來確認,因此來增加彈性。

所幸有找到類似堪用的方式(Include File Checking Macros):

#if defined(__has_include)
#if __has_include("FacebookSDK/FacebookSDK.h")
#import <FacebookSDK/FacebookSDK.h>
#else
#import "Facebook.h"
#endif
#endif


此外,在 Project 的 Build Settings,就可以這樣通用設定:
  • Framework Search Path: /path/sdk/FacebookSDK
  • Library Search Paths: ${SRCROOT}
  • User Header Search Path: /path/sdk/facebook-ios-sdk
其中 /path/sdk/FacebookSDK 指的是 facebook-ios-sdk-current.pkg 安裝位置(只要把pkg裡的 FacebookSDK.framework 拖進去會自動設定好),而 Library Search Paths 和 User Header Search Path 的設定則是為了採用 source tree 方式。

2014年3月18日 星期二

[PHP] CodeIgniter 處理多層子目錄 Routing 問題

[PHP] CodeIgniter 處理多層子目錄 Routing 問題

過去使用 CodeIgniter 一直以為在 application/controller 裡的目錄結構可以無限延伸使用,如:

application/conotrollers/service/dashboard/product.php
application/conotrollers/service/api/product.php
application/conotrollers/service/welcome.php
application/conotrollers/welcome.php


當瀏覽 hostname/ 可以由 conotrollers/welcome.php 處理,瀏覽 hostname/service/welcome 則由 conotrollers/service/welcome.php 處理,一切正常。

但瀏覽 hostname/service/api/product 和 hostname/service/dashboard/product 時,卻噴 404 Page Not Found 訊息。

一開始以為 nginx rules 設定錯誤,追一下 CodeIgniter 的 source code 後,發現 CodeIgniter 的程式碼沒有用遞迴或等價方式去搜尋子目錄,再透過相關關鍵字才發現,關於多層子目錄的需求,則只能透過指定 routing 的設定方式進行,但對應到還是一層目錄結構:

例如 hostname/service/dashboard/product 用法:

$ vim application/config/routes.php

$route['service/(:any)/(:any)'] = 'service_$1/$2';


而目錄結構更新為:

application/conotrollers/service_dashboard/product.php
application/conotrollers/service_api/product.php
application/conotrollers/welcome.php


簡言之,就是以 application/controllers 為基準,頂多再加一層 subdir 而已,而想要 uri 有多層的含義,只能自定 route 來達到。

2014年3月16日 星期日

[OSX] Xcode 5.1 - Unused Entity Issue: Unused Variable @ Mac OS X 10.9.2

Unused Entity Issue - Unused Variable

上次手滑更新 Xcode 後,發現新版 Facebook SDK 3.13 (2014/03/06 15:39:14) 編譯時會出現 Unused Entity Issue: Unused Variable 的錯誤,且看到有人提出了,暫時就先自己處理一下吧 :P

2014年3月15日 星期六

存在感、表現慾望與建設性

cocoaheads-taipei

因高手的介紹,第一次在台北參加小型聚會 CocoaHeads.tw 2014 3月聚會,場地是由知名 PicCollage app 的公司 Cardinal Blue 所提供的,也是他們家上班場所,十分舒適啊!

cocoaheads-taipei 2014-03

經過這次小型聚會,讓我想起不少點滴,像是以前在研究單位時,每兩周的技術分享一樣,真的,一個人所能接收的訊息、機會都跟工作有關,有道是男怕入錯行,不是沒有原因的 XD 這次聽到 KKBOX iOS developer 分享 CarPlay 挺有趣的。

心得:
  1. Cardinal Blue 辦公室真漂亮
  2. 不少人正積極尋找舞台,力求發揮(iOS app 派的商業特質?)
  3. 工作著實地影響一個人精進,若在工作上無法吸收到新技術,下班再去做就真的太累了 XD 此時就比較適合挑其他人生目標、舞台?
  4. 早期 iOS app 適合單打獨鬥,小功能就容易吸引人,現況適合打群架,要豐富的應用(如軟硬結合)才比較能有話題。此外,一旦開發週期拉長,產品無法及時曝光商業價值就降低
  5. 雖然 idea 的新穎性是個創業的因素,我自己第一份工作也常會因為看的多&遠,常常對別人開槍,後來某天後來有了新體悟:同樣的路,走 100 遍各有各的滋味 :) 此外,市場規模、地域性仍是個關鍵點,如同台灣現況很夯的 web service (團購、群眾募資等)不少也是從國外引進來... :P 所以,開槍時若多一點建設性方向的分享,應該會提升交流品質
最後,則是著實地提醒自己,該排好時程精實執行!

2014年3月14日 星期五

Android 開發筆記 - 使用 Google Cloud Messaging for Android (GCM)

原來 Android Cloud to Device Messaging Framework (C2DM) has been officially deprecated as of June 26, 2012 ... 現在改用 Google Cloud Messaging for Android
Google Cloud Messaging for Android (GCM) is a free service that helps developers send data from servers to their Android applications on Android devices, and upstream messages from the user's device back to the cloud. This could be a lightweight message telling the Android application that there is new data to be fetched from the server (for instance, a "new email" notification informing the application that it is out of sync with the back end), or it could be a message containing up to 4kb of payload data (so apps like instant messaging can consume the message directly). The GCM service handles all aspects of queueing of messages and delivery to the target Android application running on the target device.
一遍後,其實 GCM 的架構跟 Apple Push Notification service (APNs) 很像,主要都是先由 Mobile device 跟 Android server (Apple server) 註冊一個 token,接著再請 Mobile device 將此 token 交給 Service Provide。而 Service Provider 就利用這個 token 跟 Mobile device 傳訊息。比較不一樣的是 GCM 提供的架構多了一些設計,詳細請參考官網資料,在此僅簡易筆記一下。

Google Developer console:
  1. 首先,先到 Google Developer Console 建立一個 project,可得到 Service Provider ID
  2. 啟用 GCM service
  3. 建立 keys,APIs & auth > Credentials > Create a new key > Server key  (測試上 IP 設定為 0.0.0.0/0)
  4. 得到一組 API key,之後就是透過這個 key 跟 Mobile device's token 丟訊息
Android app:
  1. 下載 Google Play service library 並安置好
  2. 在 AndroidManifest.xml 宣告相關權限、並新增 <receiver> 來接收 Service Provider 的訊息
  3. 接著就是程式啟動時,透過 Google library 跟 GCM 註冊一個 token,此時需要指定 Service Provider ID 資訊
  4. 將 token 交給 Service Provider,如此一來 service provider 就可以傳訊息
  5. 實作好 <receiver>,當訊息進來時,啟動一個 service 發 notification 出去
接著是一些片段範例:
  • Service Provider ID: Your-Sender-ID
  • API KEY: API_Key
  • Android App Package Name: com.example.gcm
  • Mobile device token: MobileDeviceToken
  • Message:hello world
  • Payload: {"to":"MobileDeviceToken","data":{"message":"hello world","action":"com.example.gcm"}}
Service Provider 發訊息給指定的 Mobile device:

$ curl --header "Authorization: key=API_Key" --header Content-Type:"application/json" https://android.googleapis.com/gcm/send  -d '{"to":"MobileDeviceToken","data":{"message":"hello world","action":"com.example.gcm"}}'

Android app - AndroidManifest.xml:

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

    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="15" />

    <!--  ADD FOR Google Cloud Messaging ### Begin -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<permission android:name="com.example.gcm.permission.C2D_MESSAGE"
        android:protectionLevel="signature" />
<uses-permission android:name="com.example.gcm.permission.C2D_MESSAGE" />
    <!--  ADD FOR Google Cloud Messaging ### End -->


    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.example.gcm.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
   
        <!--  ADD FOR Google Cloud Messaging ### Begin -->
        <meta-data android:name="com.google.android.gms.version"
        android:value="@integer/google_play_services_version" />
        <receiver
            android:name=".GcmBroadcastReceiver"
            android:permission="com.google.android.c2dm.permission.SEND" >
            <intent-filter>
                <action android:name="com.google.android.c2dm.intent.RECEIVE" />
                <category android:name="com.example.gcm" />
            </intent-filter>
        </receiver>
        <service android:name=".GcmIntentService" />
        <!--  ADD FOR Google Cloud Messaging ### End -->

   
    </application>
</manifest>


Android app - Receiver:

package com.example.gcm;

import com.google.android.gms.gcm.GoogleCloudMessaging;

import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.content.WakefulBroadcastReceiver;
import android.util.Log;

public class GcmBroadcastReceiver extends WakefulBroadcastReceiver {
private static final String TAG = "GCM";

@Override
public void onReceive(Context context, Intent intent) {
// TODO Auto-generated method stub
Log.i(TAG, "GcmBroadcastReceiver");

GoogleCloudMessaging gcm = GoogleCloudMessaging.getInstance(context);
String messageType = gcm.getMessageType(intent);
Log.i(TAG, "GcmBroadcastReceiver messageType:"+messageType);

Bundle extras = intent.getExtras();
if (!extras.isEmpty()) {
if (GoogleCloudMessaging.MESSAGE_TYPE_SEND_ERROR.equals(messageType)) {

} else if (GoogleCloudMessaging.MESSAGE_TYPE_DELETED.equals(messageType)) {

} else if (GoogleCloudMessaging.MESSAGE_TYPE_MESSAGE.equals(messageType)) {

}
Log.i(TAG, "GcmBroadcastReceiver Received: " + extras.toString());
}
}
}


Android app - MainActivity:

public class MainActivity extends ActionBarActivity {
private static final String TAG = "GCM";
public static final String EXTRA_MESSAGE = "message";
public static final String PROPERTY_REG_ID = "registration_id";
private static final String PROPERTY_APP_VERSION = "appVersion";
private final static int PLAY_SERVICES_RESOLUTION_REQUEST = 9000;
String SENDER_ID = "Your-Sender-ID";

GoogleCloudMessaging gcm;
AtomicInteger msgId = new AtomicInteger();
String regid;
Context context;

private boolean checkPlayServices() {
   int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
   if (resultCode != ConnectionResult.SUCCESS) {
       if (GooglePlayServicesUtil.isUserRecoverableError(resultCode)) {
           GooglePlayServicesUtil.getErrorDialog(resultCode, this,
                   PLAY_SERVICES_RESOLUTION_REQUEST).show();
       } else {
           Log.i(TAG, "This device is not supported.");
           finish();
       }
       return false;
   }
   return true;
}

private String getRegistrationId(Context context) {
   final SharedPreferences prefs = getGCMPreferences(context);
   String registrationId = prefs.getString(PROPERTY_REG_ID, "");
   if (registrationId.isEmpty()) {
       Log.i(TAG, "Registration not found.");
       return "";
   }
   // Check if app was updated; if so, it must clear the registration ID
   // since the existing regID is not guaranteed to work with the new
   // app version.
   int registeredVersion = prefs.getInt(PROPERTY_APP_VERSION, Integer.MIN_VALUE);
   int currentVersion = getAppVersion(context);
   if (registeredVersion != currentVersion) {
       Log.i(TAG, "App version changed.");
       return "";
   }
   return registrationId;
}

private SharedPreferences getGCMPreferences(Context context) {
   // This sample app persists the registration ID in shared preferences, but
   // how you store the regID in your app is up to you.
   return getSharedPreferences(MainActivity.class.getSimpleName(),
           Context.MODE_PRIVATE);
}

private static int getAppVersion(Context context) {
   try {
       PackageInfo packageInfo = context.getPackageManager()
               .getPackageInfo(context.getPackageName(), 0);
       return packageInfo.versionCode;
   } catch (NameNotFoundException e) {
       // should never happen
       throw new RuntimeException("Could not get package name: " + e);
   }
}
private void registerInBackground() {
new AsyncTask<Object, Object, Object>() {

@Override
protected Object doInBackground(final Object... arg0) {
// TODO Auto-generated method stub
String msg = "";
           try {
               if (gcm == null) {
                   gcm = GoogleCloudMessaging.getInstance(context);
               }
               regid = gcm.register(SENDER_ID);
               msg = "Device registered, registration ID=" + regid;

               // You should send the registration ID to your server over HTTP,
               // so it can use GCM/HTTP or CCS to send messages to your app.
               // The request to your server should be authenticated if your app
               // is using accounts.
               sendRegistrationIdToBackend();

               // For this demo: we don't need to send it because the device
               // will send upstream messages to a server that echo back the
               // message using the 'from' address in the message.

               // Persist the regID - no need to register again.
               storeRegistrationId(context, regid);
           } catch (IOException ex) {
               msg = "Error :" + ex.getMessage();
               // If there is an error, don't just keep trying to register.
               // Require the user to click a button again, or perform
               // exponential back-off.
           }
           return msg;
}

}.execute(null, null, null);
}

private void sendRegistrationIdToBackend() {
   // Your implementation here.
}

private void storeRegistrationId(Context context, String regId) {
   final SharedPreferences prefs = getGCMPreferences(context);
   int appVersion = getAppVersion(context);
   Log.i(TAG, "Saving regId on app version " + appVersion);
   Log.i(TAG, "Saving regId: " + regId);
   SharedPreferences.Editor editor = prefs.edit();
   editor.putString(PROPERTY_REG_ID, regId);
   editor.putInt(PROPERTY_APP_VERSION, appVersion);
   editor.commit();
}

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

        context = getApplicationContext();

        // Check device for Play Services APK. If check succeeds, proceed with
        //  GCM registration.
        if (checkPlayServices()) {
            gcm = GoogleCloudMessaging.getInstance(this);
            regid = getRegistrationId(context);

            if (regid.isEmpty()) {
                registerInBackground();
            }
        } else {
            Log.i(TAG, "No valid Google Play Services APK found.");
        }

if (savedInstanceState == null) {
getSupportFragmentManager().beginTransaction()
.add(R.id.container, new PlaceholderFragment()).commit();
}
}
// ...
}


更完整的範例請參考官網:GCM Client