2012年9月30日 星期日

Android 開發筆記 - 簡易 手電筒(閃光燈) app 實作

返家過節,家人分享了 android app 手電筒給我把玩,說真的偶爾還挺實用的,甚至一些 feature phone 都也有這種設計(基本上是真的裝了一個燈)。但我仔細看了一下該 app 的權限,卻開了一堆有的沒有的權限,假設把這個行為丟給國內知名防毒大廠的偵測系統進行偵測,那應該就會被判斷成病毒了。所以我就順手練習一下了 XD


而 android app 的概念則是使用相機的閃光燈,讓閃光燈的狀態停留在 FLASH_MODE_TORCH 時,就可以當做手電筒使用了。然而真正實作上的細節,則必須先開啟相機才能使用閃光燈,也就是用閃光燈之前必須開啟鏡頭並且也會開始把鏡頭收到的影像傳給系統了。簡言之,使用閃光燈其實會操到鏡頭跟系統資料,等同於有資料不停地從鏡頭收進來。


簡單的實作:


layout/main.xml:


<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
       android:id="@+id/background"
       xmlns:tools="http://schemas.android.com/tools"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <SurfaceView
              android:id="@+id/camera_preview"
              android:layout_width="1dip"
              android:layout_height="1dip"/>


       <TextView
              android:layout_width="150dp"
              android:layout_height="150dp"
              android:layout_centerHorizontal="true"
              android:layout_centerVertical="true"
              android:gravity="center"
              android:text="@string/light"
              android:textColor="#AAA"
              android:textSize="50dp" />


</RelativeLayout>


AndroidManifest.xml:(事實上可以只需android.permission.CAMERA即可,其他只是最佳化的效果)


<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-feature android:name="android.hardware.camera" android:required="false"/>
<uses-feature android:name="android.hardware.touchscreen" android:required="false"/>


Code:


import java.io.IOException;


import android.graphics.Color;
import android.hardware.Camera;
import android.hardware.Camera.Parameters;
import android.os.Bundle;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.view.Menu;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.widget.RelativeLayout;


public class MyFlashLightActivity extends Activity implements SurfaceHolder.Callback {
       boolean have_light = false;


       Camera mCamera = null;
       SurfaceView mSurfaceView;
       SurfaceHolder mSurfaceHolder;
       WakeLock mWakeLock;

       @Override
       public void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              setContentView(R.layout.activity_my_flash_light);

              if( ( have_light = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH) ) ) {
                     try {
                            mCamera = Camera.open();
                            mSurfaceView = (SurfaceView) findViewById(R.id.PREVIEW);
                            if(mSurfaceView!= null) {
                                   mSurfaceHolder = mSurfaceView.getHolder();
                                   mSurfaceHolder.addCallback(this);
                                   mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);

                                   Parameters mParameters = mCamera.getParameters();
                                   mParameters.setFlashMode(Parameters.FLASH_MODE_TORCH);
                                   mCamera.setParameters(mParameters);
                                   mCamera.startPreview();


                            }
                     } catch( Exception e ) {
                            e.printStackTrace();
                     }
              } else {
                     RelativeLayout mRelativeLayout = (RelativeLayout)findViewById(R.id.background);
                     if(mRelativeLayout!=null)
                            mRelativeLayout.setBackgroundColor(Color.WHITE);
              }
       }


       @Override
       protected void onDestroy() {
              // TODO Auto-generated method stub
              super.onDestroy();
              if( have_light ) {
                     mCamera.stopPreview();
                     mCamera.release();
              }
              System.out.println("[D] finish");
       }


       @Override
       protected void onPause() {
              // TODO Auto-generated method stub
              super.onPause();
              if( mWakeLock != null )
                     mWakeLock.release();
       }


       @Override
       protected void onResume() {
              // TODO Auto-generated method stub
              super.onResume();
              if( mWakeLock == null ) {
                     PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
                     mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "WAKE_LOCK_TAG");
              }
              mWakeLock.acquire();
       }


       @Override
       public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {


              // TODO Auto-generated method stub
              System.out.println("[i] surfaceChanged");
       }


       @Override
       public void surfaceCreated(SurfaceHolder holder) {
              System.out.println("[i] surfaceCreated");
              try {
                     mCamera.setPreviewDisplay(holder);
                     holder.addCallback(MyFlashLightActivity.this);
              } catch (IOException e) {
                     e.printStackTrace();
              }
       }


       @Override
       public void surfaceDestroyed(SurfaceHolder holder) {
              // TODO Auto-generated method stub
              System.out.println("[i] surfaceDestroyed");


              mCamera.stopPreview();
              mSurfaceHolder = null;
       }
}


此外,有興趣的人,強烈建議去翻一下 Torch, an Android flashlight application 這個 open source ,算是找資料驗證時無意間發現的好物。


2012年9月29日 星期六

找尋支援 Apple NB 記憶體資訊

KTA-MB 1333


最近常看到 MBP 運作的很緩慢,查看記憶體使用量才發現,原先 4GB 的空間已經被用到不到 100MB 了。網路上隨意打滾一下,有的在露天賣 NB 記憶體,說啥有 Apple 認證,或是可以支援等小道消息,抑或看到網路購物直接打出 "Apple認證" 的關鍵字,但一條記憶體硬是比別人多 50%~100% 的價格啊(例如 NB DDR3-1333 8GB 一條現今約台幣1k,但有的主打 Apple 認證賣到一條 2k 左右,更誇張的還有一次賣兩條要價 4.5k 等等 )...


目前正在考慮 金士頓 記憶體,所幸從官網上可以搜尋:


search_ram_01


search_ram_02


如此一來,至少可以先驗證一下,那些露天賣家說的是真是假啦。


註:此例以 Macbook pro 13" late 2011 版本,在 Apple 官網上規格描述 MacBook Pro (13-inch, Late 2011) - Technical Specifications 所述,記憶體規格為 4GB (two 2GB SO-DIMMs) of 1333MHz DDR3 memory; two SO-DIMM slots support up to 8GB。網路上滿多人說可以插到 16GB (8GBx2) 來用,雖然金士頓官網有說 8GB 記憶體可以使用,但並沒有說 2條 8GB 記憶體可以跑 XD 所以...只能看網友測試或是自己親自下海囉...


2012年9月28日 星期五

Android 開發筆記 - 解決 HttpUriRequest/HttpGet/HttpPost 之 Host name may not be null

最近碰到一個 bug 卡關,那就是當我 new HttpGet("http://aaa_bbb.ccc.dddd") 出來,交由 HttpClient 執行時,卻會看到以下訊息:


java.lang.IllegalArgumentException: Host name may not be null
org.apache.http.HttpHost.<init>(HttpHost.java:83)
org.apache.http.impl.client.AbstractHttpClient.determineTarget(AbstractHttpClient.java:497)
org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:487)


當下讓我連問隔壁的同事幾次...難道 hostname 不能有底線?改成 new HttpGet("aaa_bbb.ccc.dddd") 則是:


java.lang.IllegalStateException: Target host must not be null, or set in parameters.
org.apache.http.impl.client.DefaultRequestDirector.determineRoute(DefaultRequestDirector.java:561)
org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:292)
org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:555)
org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:487)


原始程式碼:


String target = "http://aaa_bbb.ccc.dddd";


HttpClient mClient = new DefaultHttpClient();
HttpContext mHttpContext = new BasicHttpContext();
HttpUriRequest mHttpUriRequest = new HttpGet(target);
response = mClient.execute(mHttpUriRequest, mHttpContext);


解法:


String hostname = "aaa_bbb.ccc.dddd";
String target = "http://"+hostname;


HttpClient mClient = new DefaultHttpClient();
HttpContext mHttpContext = new BasicHttpContext();
HttpUriRequest mHttpUriRequest = new HttpGet(target);
response = mClient.execute(new HttpHost(hostname), mHttpUriRequest, mHttpContext);


不曉得這是不是一個 framework 的 bug?還是單純我操作錯誤呢...暫時先這樣解掉吧。


Android 開發筆記 - 解決/取消 EditText 自動 focus 問題

想必還滿常碰到一個 Activity 中,擺幾個 EditText 讓人輸入帳密來送出的表單吧!然而,當送出表單成功後,偶時會很偷懶直接把 mEditText.setText("Info") 且 mEditText.setEnable(false) 來處理,想說這樣又可以重複利用 XD 結果就會碰到開啟 Activity 後,自動 focus 在 EditText 並彈跳出 keyboard 的窘境了。如果動態進行 mEditText.setFocusable(false) 的方式,的確可以避開 focus 的問題,但很奇妙地再動態 mEditText.setFocusable(true) 時,卻會出錯而無法點選該欄位 Orz


最後,找到一些很折衷的辦法...那就是在 EditText 前,先讓某個處的 layout 可以被 focusable 就好 XD 這樣的解法真的是 It just works! 的狀態。


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:orientation="vertical" >

       <LinearLayout
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"
              android:focusable="true"
              android:focusableInTouchMode="true"
              android:gravity="center_vertical">
              <TextView
                     android:layout_margin="5dp"
                     android:layout_width="120dp"
                     android:layout_height="wrap_content"
                     android:text="@string/title_account"
                     android:textAppearance="?android:attr/textAppearanceLarge">
              </TextView>
              <EditText
                     android:id="@+id/edittext_account"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
                     android:hint="@string/account_hint"
                     android:ems="10" >
              </EditText>
       </LinearLayout>
       <LinearLayout
              android:id="@+id/linearlayout_password"
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"
              android:focusable="true"
              android:focusableInTouchMode="true"
              android:gravity="center_vertical">
              <TextView
                     android:layout_margin="5dp"
                     android:layout_width="120dp"
                     android:layout_height="wrap_content"
                     android:text="@string/title_password"
                     android:textAppearance="?android:attr/textAppearanceLarge">
              </TextView>


              <EditText
                     android:id="@+id/edittext_password"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
                     android:ems="10"
                     android:hint="@string/password_hint"
                     android:inputType="textPassword" >
              </EditText>
       </LinearLayout>
</LinearLayout>


2012年9月26日 星期三

Android 開發筆記 - 處理 API 回應 XML 資料的通用解法

如果 API 不是定義的很好,回應得資料格式不依,但至少有符合簡易的 XML 雛形,如:


<name>changyy</name>
<url>http://changyy.pixnet.net</url>
<style>blog</style>


並且很多支 API 格式都不一樣時,就會讓人想看看有沒統一解法,不小心就想到 Javascript property 的用法,可惜我對 Java 不熟,暫時就先用個 map 來處理了:


import java.io.IOException;
import java.io.StringReader;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;


import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;


public class QueryReturnObject {
       private Map<String,String> properties = new HashMap<String,String>();
       public QueryReturnObject(String xml) {
              parsing(xml);
       }
       public String getProperty(String key) {
              if( key == null || !properties.containsKey(key) )
                     return null;
              return properties.get(key);
       }
       public String toString() {
              if( properties.size() == 0 )
                     return "No Properties";
              String out = "";
              for( Iterator<Entry<String, String>> it = properties.entrySet().iterator() ; it.hasNext() ; ) {
                     Entry<String, String> item = it.next();
                     out += "Key=["+item.getKey()+"], Value=["+item.getValue()+"]\n";
              }
              return out;
       }
       void parsing(String raw) {
              properties.clear();
              try {
                     XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
                     XmlPullParser parser = factory.newPullParser();
                     parser.setInput( new StringReader ( raw ) );
                     int eventType = parser.getEventType();
                     String fieldName = null;
                     while( eventType != XmlPullParser.END_DOCUMENT ) {
                            switch(eventType) {
                                   case XmlPullParser.START_DOCUMENT:
                                          break;
                                   case XmlPullParser.START_TAG:
                                          fieldName = parser.getName();
                                          break;
                                   case XmlPullParser.END_TAG:
                                          fieldName = null;
                                          break;
                                   case XmlPullParser.TEXT:
                                          if( fieldName != null )
                                                 properties.put(fieldName, parser.getText());
                                          break;
                            }
                            eventType = parser.next();
                     }
              } catch (XmlPullParserException e) {
                     e.printStackTrace();
              } catch (IOException e) {
                     e.printStackTrace();
              }
       }
}


如此一來,任何 API 回應的資料,就直接用 data = new QueryReturnObject(response) 來使用,接著就可以用 data.getProperty("Key") 的方式來存取囉。


2012年9月24日 星期一

Android 開發筆記 - HTTP Post (File Uploading) Progress Report

之前研究 HTTP Post 的方法時,順手實作了支援 Cookie 等功能,久了之後就會想到如何監控上傳進度的部分,原理都很簡單,但要熟整各個 framework 才能方便進行。所幸網路上好心人士非常多,找到這篇 Android Multipart POST with Progress Bar 真的超佛心的,就順手修改一下一點架構。


實作概念:


繼承 org.apache.http.entity.mime.MultipartEntity 物件後,當寫出資料時,記錄已累積的寫出量,在搭配總共要送出的資料量,簡易的除法就能得知目前所進行的進度了。


我自己的粗略使用:


@ HttpPostMultipartEntity.java:


// 九成九一樣,單純改一些變數名稱
// src: http://toolongdidntread.com/android/android-multipart-post-with-progress-bar/
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;


import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntity;


public class HttpPostMultipartEntity extends MultipartEntity {
       private final HttpPostProgressHandler handler;
       public HttpPostMultipartEntity(final HttpPostProgressHandler handler) {
              super();
              this.handler = handler;
       }
       public HttpPostMultipartEntity(final HttpMultipartMode mode, final HttpPostProgressHandler handler) {
              super(mode);
              this.handler = handler;
       }
       public HttpPostMultipartEntity(final HttpMultipartMode mode, final String boundary, final Charset charset, final HttpPostProgressHandler handler) {
              super(mode,boundary,charset);
              this.handler = handler;
       }

       @Override
       public void writeTo(final OutputStream outstream) throws IOException {
              super.writeTo(new HttpPostOutputStream(outstream, this.handler));
       }

       public static class HttpPostOutputStream extends FilterOutputStream {
              private final HttpPostProgressHandler handler;
              private long transferred;

              public HttpPostOutputStream(final OutputStream out, final HttpPostProgressHandler handler) {
                     super(out);
                     this.handler = handler;
                     this.transferred = 0;
              }

              public void write(byte[] b, int off, int len) throws IOException {
                     out.write(b, off, len);
                     this.transferred += len;
                     if( this.handler != null )
                             this.handler.postStatusReport(this.transferred);

              }

              public void write(int b) throws IOException {
                     out.write(b);
                     this.transferred ++;
                     if( this.handler != null )

                            this.handler.postStatusReport(this.transferred);
              }
       }
}


@ HttpPostProgressHandler.java:


public interface HttpPostProgressHandler {
       void setPostDataSize(long size);
       void postStatusReport(long transferred);
}


HTTP POST Usage:


void doPost( String api_url, String file_path, HttpPostProgressHandler reporter ) {
       List<NameValuePair> mParams = new ArrayList<NameValuePair>();
       mParams.add( new BasicNameValuePair( "path", remote_path ) );
       mParams.add( new BasicNameValuePair( "mode", "upload_file") );
       mParams.add( new BasicNameValuePair( "name", filename) );
       mParams.add( new BasicNameValuePair( "code", edit_code ) );


       // multipart with args
       //MultipartEntity entity=new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE);
       HttpPostMultipartEntity entity = new HttpPostMultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE, reporter);
       for( NameValuePair mItem : mParams )
              try {
                     entity.addPart(item.getName(),new StringBody(mItem.getValue(), Charset.forName("UTF-8")));
              } catch (UnsupportedEncodingException e) {
                     e.printStackTrace();
              }

       File mFile = new File(file_path);
       if( ! mFile.exists() ) {
              System.out.println("File not found:"+local_path);
              return;
       }

       // add cookie
       CookieStore mCookieStore = BasicCookieStore()
       Cookie x = new BasicClientCookie("MyCookie", "MyCookieValue");
       ((BasicClientCookie)x).setPath("/");
       ((BasicClientCookie)x).setDomain("ServerDomain");
       cookieStore.addCookie( x );


       // add file
       entity.addPart( "file", new FileBody( mFile ) );


       // upload api
       HttpPost mHttpPost = new HttpPost(api_url);
       mHttpPost.setEntity(entity);


       // set post data total size
       if( reporter != null )
              reporter.setPostDataSize(entity.getContentLength());


       // use cookie
       HttpClient mHttpClient = new DefaultHttpClient();
       HttpContext mHttpContext = new BasicHttpContext();
       mHttpContext.setAttribute(ClientContext.COOKIE_STORE, cookieStore);

       try {
              HttpResponse response = mHttpClient.execute( mHttpPost, mHttpContext );
              HttpEntity result = response.getEntity();
              System.out.println( "Result:" + EntityUtils.toString(result) );
       } catch (Exception e) {
              e.printStackTrace();
       }
}


Main:


new Thread( new Runnable() {
       @Override
       public void run() {
              doPost(
                     "http://mytest/api/upload.php" ,
                     "/mnt/sdcard/test.png",
                     new HttpPostProgressHandler() {
                            long total_size = 0;


                            @Override
                            public void setPostDataSize(long size) {
                                   total_size = size;
                            }


                            @Override
                            public void postStatusReport(long transferred) {
                                   if(total_size == 0)
                                          return;
                                   System.out.println("Status:"+(float)transferred/total_size);
                            }
                     }
              );
       }
} ).start();


Others:


import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;


import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CookieStore;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.cookie.Cookie;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntity;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;


public class HttpQueryUsage {
       public static HttpPost createHttpPost(String url, List<NameValuePair> params, List<NameValuePair> files, HttpPostProgressHandler handler) {
              if( url == null )
                     return null;


              MultipartEntity entity=new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE);
              if( params != null && params.size() > 0)
                     for( NameValuePair item : params )
                            try {
                                   entity.addPart(item.getName(),new StringBody(item.getValue(), Charset.forName("UTF-8")));
                            } catch (UnsupportedEncodingException e) {
                                   e.printStackTrace();
                            }


              if( files != null && files.size() > 0 )
                     for( NameValuePair file : files ) {
                            try {
                                   File src = new File(file.getValue());
                                   if(!src.exists())
                                          continue;
                                   entity.addPart( file.getName(), new FileBody( src ) );
                            } catch (Exception e) {
                                   e.printStackTrace();
                            }
                     }

              HttpPost post = new HttpPost(url);
              post.setEntity(entity);


              if(handler!=null)
                     handler.setPostDataSize(entity.getContentLength());

              return post;
       }
       public static HttpGet createHttpGet(String url, List<NameValuePair> params) {
              if( url != null )
                     return new HttpGet( params == null || params.size() == 0 ? url : url + "?" + URLEncodedUtils.format(params, "utf-8") );
              return null;
       }
       public static HttpResponse executeQuery(HttpUriRequest request, CookieStore cookie_store) throws ClientProtocolException, IOException {
              HttpClient mClient = new DefaultHttpClient();
              if( cookie_store == null )
                     return mClient.execute(request);
              HttpContext mHttpContext = new BasicHttpContext();
              mHttpContext.setAttribute(ClientContext.COOKIE_STORE, cookie_store);
              return mClient.execute(request, mHttpContext);
       }
       public static void exampleGetUsage() {
              String url = "http://localhost/login.php";
              List<NameValuePair> params = new ArrayList<NameValuePair>();
              params.add( new BasicNameValuePair("user","username") );
              params.add( new BasicNameValuePair("passwd","password") );
              try {
                     CookieStore cookie_store = new BasicCookieStore();
                     HttpEntity result = HttpQueryUsage.executeQuery(HttpQueryUsage.createHttpGet(url, params), cookie_store).getEntity();
                     for( Cookie item : cookie_store.getCookies() )
                            System.out.println("CookieName: "+item.getName()+",CookieValue: "+item.getValue() );
                     if( result != null )
                            System.out.println("PageResult:"+EntityUtils.toString(result));
              } catch (Exception e) {
                     e.printStackTrace();
              }
       }
       public static void examplePostUsage() {
              String url = "http://localhost/login.php";
              List<NameValuePair> params = new ArrayList<NameValuePair>();
              params.add( new BasicNameValuePair("user","username") );
              params.add( new BasicNameValuePair("passwd","password") );
              try {
                     CookieStore cookie_store = new BasicCookieStore();
                     HttpEntity result = HttpQueryUsage.executeQuery(HttpQueryUsage.createHttpPost(url, params, null, null), cookie_store).getEntity();
                     for( Cookie item : cookie_store.getCookies() )
                            System.out.println("CookieName: "+item.getName()+",CookieValue: "+item.getValue() );
                     if( result != null )
                            System.out.println("PageResult:"+EntityUtils.toString(result));
              } catch (Exception e) {
                     e.printStackTrace();
              }
       }
       public static void examplePostFileUploadingUsage() {
              String url = "http://localhost/login.php";
              List<NameValuePair> files = new ArrayList<NameValuePair>();
              files.add( new BasicNameValuePair("file1","/mnt/sdcard/test.png") );
              try {
                     CookieStore cookie_store = new BasicCookieStore();
                     HttpEntity result = HttpQueryUsage.executeQuery(HttpQueryUsage.createHttpPost(url, null, files, null), cookie_store).getEntity();
                     for( Cookie item : cookie_store.getCookies() )
                            System.out.println("CookieName: "+item.getName()+",CookieValue: "+item.getValue() );
                     if( result != null )
                            System.out.println("PageResult:"+EntityUtils.toString(result));
              } catch (Exception e) {
                     e.printStackTrace();
              }
       }
}


2012年9月20日 星期四

Android 開發筆記 - 簡易 Java AES 加解密與 SHA1 筆記

寫 Mobile app 有時需要存一些敏感的資訊,如果只是當做一個認證用途,大概就用 MD5 或 SHA1 來使用,但如果需要保留的,大概就需要能夠加密後又解密的,這時候就可以考慮拿一把 key 進行 AES Encryption/Decryption 動作。


簡易 SHA1:


import java.security.MessageDigest;


public static String sha1(String input) {
       try {
              MessageDigest digest = MessageDigest.getInstance("SHA-1");
              digest.reset();
              byte[] out = digest.digest(input.getBytes("UTF-8"));
              return android.util.Base64.encodeToString(out, android.util.Base64.NO_WRAP);
       } catch (Exception e) {
              e.printStackTrace();
       }
       return null;
}


簡易 AES Encryption/Decryption (使用自製的 key):


import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import android.util.Base64;


public static String selfKey(String key) {   // key.length() must be 16, 24 or 32
       int length = key.length();
       if( length < 16 ) {
              for( int i=length ;i<16; ++i )
                     key += i%10;
              return key;
       } else if ( length < 24 ) {
              for( int i=length ;i<24; ++i )
                     key += i%10;
              return key;
       } else if ( length < 32 ) {
              for( int i=length ;i<32; ++i )
                     key += i%10;
              return key;
       }
       return key.substring(0, 32);
}


public static String selfEncode(String key, String value) {
       SecretKeySpec spec = new SecretKeySpec(selfKey(key).getBytes(), "AES");
       Cipher cipher;
       try {
              cipher = Cipher.getInstance("AES");
              cipher.init(Cipher.ENCRYPT_MODE, spec);
              return Base64.encodeToString(cipher.doFinal(value.getBytes()), android.util.Base64.NO_WRAP);
       } catch (Exception e) {
              e.printStackTrace();
       }
       return null;
}


public static String selfDecode(String key, String value) {
       SecretKeySpec spec = new SecretKeySpec(selfKey(key).getBytes(), "AES");
       Cipher cipher;
       try {
              cipher = Cipher.getInstance("AES");
              cipher.init(Cipher.DECRYPT_MODE, spec);
              return new String( cipher.doFinal(Base64.decode(value, android.util.Base64.NO_WRAP)) );
       } catch (Exception e) {
              e.printStackTrace();
       }
       return null;
}


Android 開發筆記 - 使用 HTTP Post 上傳檔案並支援 Cookie 資訊

file_upload


之前已寫過一篇 Android 開發筆記 - HTTP、HTTPS、GET、POST、Cookie 筆記,最近則需要用 POST 上傳檔案,就順手在記一下。首先需從 http://hc.apache.org/downloads.cgi 下載 HttpClient (4.2.1) 後,從裡頭取出 httpmime-4.2.1.jar 檔拖到 Package Explorer > Your Project > libs 後,在按右鍵 Build Path > Add to Build Path...,如此一來在 Android 裡就能使用 MultipartEntity 進行上傳檔案的行為。若不使用的話,大概就自行實作鄉對應的格式也行。


PHP CGI:


<?php
echo "\nCOOKIE:<br/>\n";
print_r( $_COOKIE );


echo "\n<br/>FILES:<br/>\n";
print_r( $_FILES );


echo "\n<br/>REQUEST:<br/>\n";
print_r( $_REQUEST );


Android/Java:


public static void upload_testing() {
       // config
       String server = "127.0.0.1";
       String cgi = "http://"+server+"/upload.php";
       String file_path = "/mnt/sdcard/test.png";


       // http request parameters
       List<NameValuePair> params = new ArrayList<NameValuePair>();
       params.add( new BasicNameValuePair("hello", "world") ); // $_REQUEST['hello'] = 'world'


       MultipartEntity entity=new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE);


       // build post parameters
       for( NameValuePair item : params )
              try {
                     entity.addPart(item.getName(),new StringBody(item.getValue(), Charset.forName("UTF-8")));
              } catch (UnsupportedEncodingException e) {
                     e.printStackTrace();
              }


       // add file data
       File src = new File(file_path);
       if( src.exists() )
              entity.addPart( "file", new FileBody( src ) ); // $_FILES['file']['name'] = 'test.png'


       // use cookie
       CookieStore cookies = new BasicCookieStore();
       Cookie mCookie = new BasicClientCookie("CookieName", "CookieValue"); // $_COOKIE['CookieName']
       ((BasicClientCookie)mCookie).setPath("/");
       ((BasicClientCookie)mCookie).setDomain(server);
       cookies.addCookie(mCookie);


       // build post query
       HttpPost post = new HttpPost(cgi);
       post.setEntity(entity);


       // do query with cookie
       HttpClient client = new DefaultHttpClient();
       HttpContext mHttpContext = new BasicHttpContext();
       mHttpContext.setAttribute(ClientContext.COOKIE_STORE, cookies);


       try {
              HttpResponse response = client.execute( post, mHttpContext );
              HttpEntity result = response.getEntity();
              System.out.println( EntityUtils.toString(result) );
       } catch (Exception e) {
              e.printStackTrace();
       }
}


成果:


2012年9月19日 星期三

[PHP] 使用 Yahoo! Content Analysis API (斷章取義)


Yahoo! Content Analysis API
 


簡易筆記:


<?php
$url = 'http://query.yahooapis.com/v1/public/yql';
$q = array();
$q['q']= 'select * from contentanalysis.analyze where text="你好嗎 台灣 生活樂趣";';


$ch = curl_init();
curl_setopt( $ch, CURLOPT_URL, $url );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
curl_setopt( $ch, CURLOPT_POST, true );
curl_setopt( $ch, CURLOPT_POSTFIELDS, http_build_query( $q ) );
echo curl_exec( $ch );
curl_close( $ch );


Android 開發筆記 - 取得 Mac Address 和 IP Address

網路服務常需要拿 IP 或 MAC Address 來做存取管控。筆記一下。


權限:


<uses-permissionandroid:name="android.permission.ACCESS_WIFI_STATE"/>


程式碼:


public static String getMacAddress(Context context) {
       WifiManager wifiMan = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
       WifiInfo wifiInf = wifiMan.getConnectionInfo();
       return wifiInf.getMacAddress();
}


public static String getIPAddress(Context context) {
       WifiManager wifiMan = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
       WifiInfo wifiInf = wifiMan.getConnectionInfo();
       long ip = wifiInf.getIpAddress();
       if( ip != 0 )
              return String.format( "%d.%d.%d.%d",
                     (ip & 0xff),
                     (ip >> 8 & 0xff),
                     (ip >> 16 & 0xff),
                     (ip >> 24 & 0xff));
       try {
              for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements();) {
                     NetworkInterface intf = en.nextElement();
                     for (Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements();) {
                            InetAddress inetAddress = enumIpAddr.nextElement();
                            if (!inetAddress.isLoopbackAddress()) {
                                   return inetAddress.getHostAddress().toString();
                            }
                     }
              }
       } catch (Exception e) {
        }
       return "0.0.0.0";
}


2012年9月18日 星期二

[Linux] 讓 GitWeb 的 Owner 顯示支援 Gitolite 的 Creator @ Ubuntu 12.04

最近裝了 3 次 Git + Gitweb + Gitolite ,最後就順手稍微修改 Gitweb 。由於 Gitolite 可以 remote create repos ,因此想要讓 Gitweb 顯示誰建立了 repo 。而 Gitolite 會在 repo 中建立一個 gl-creator 檔案記錄誰建立的,所以只需修改 Gitweb 顯示 owner 的片段程式即可。


目前用的 gitweb 版本:


$ sudo dpkg -l | grep gitweb
ii gitweb 1:1.7.9.5-1 fast, scalable, distributed revision control system (web interface)


修改片段:


$ sudo vim /usr/share/gitweb/index.cgi
sub git_get_project_owner {
       my $project = shift;
       my $owner;


       return undef unless $project;
       $git_dir = "$projectroot/$project";


       if (!defined $gitweb_project_owner) {
              git_get_project_list_from_file();
       }


       if (exists $gitweb_project_owner->{$project}) {
              $owner = $gitweb_project_owner->{$project};
       }
       if (!defined $owner){
              $owner = git_get_project_config('owner');
       }
       if (!defined $owner) {
              if( open(GLCreator, "$git_dir/gl-creator" ) ) {
                     $owner = '';
                     while(<GLCreator>) {
                            $owner .= $_;
                     }
                     close(GLCreator);
              }
       }
       if (!defined $owner) {
              $owner = get_file_owner("$git_dir");
       }


       return $owner;
}


2012年9月17日 星期一

Android 開發筆記 - 匯入圖片至模擬器中

gallery_apps


雖然 Android 開發是用實機才是王道,但有時就是想偷懶看模擬器跑的如何,這時就仍需要模擬器的環境。如果要處理照片、影片的應用時,就需要匯入一些圖片影片來測試。一開始若直接用 adb shell 或 DDMS push 資料至模擬器時,會顯示 failed to copy: Read-only file system:


$ adb push test.png /sdcard/
failed to copy 'test.png' to '/sdcard//test.png': Read-only file system


這時因為 / 目錄為 Read-Only file system,需要稍微處理一下:


$ adb shell mount -o remount rw /sdcard


接著就可以 DDMS 或 adb shell 匯入資料:


$ adb push images/ /sdcard/
push: images/1.png -> /mnt/sdcard/1.png
push: images/2.png -> /mnt/sdcard/2.png
push: images/3.png -> /mnt/sdcard/3.png
push: images/4.png -> /mnt/sdcard/4.png
push: images/5.png -> /mnt/sdcard/5.png


然而,匯入後仍無法被系統偵測,無論是寫程式還是開 Gallery app 也都一樣,但只需跑一下模擬器內建軟體 Dev Tools > Media Scanner 後,即可解決。


簡易的取出系統內所有 image 方式:


Cursor imgcursor = managedQuery(
       MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
       { MediaStore.Images.Media.DATA, MediaStore.Images.Media._ID }, // columns
       null,
       null,
       MediaStore.Images.Media._ID // order by
);
if( imgcursor != null ) {
       int count = imgcursor.getCount();
       int media_data_index = imgcursor.getColumnIndex(MediaStore.Images.Media.DATA);
       int media_id_index = imgcursor.getColumnIndex(MediaStore.Images.Media._ID);
       for( int i=0 ; i<count ; ++i ) {
              imgcursor.moveToPosition(i);
              String path = imgcursor.getString(media_data_index);
              Bitmap thumbnail = MediaStore.Images.Thumbnails.getThumbnail( getApplicationContext().getContentResolver(), media_id_index, MediaStore.Images.Thumbnails.MICRO_KIND, null);
              System.out.println("path :"+path);
       }
}


用 Intent  開啟單張圖片:


Intent intent = new Intent();
intent.setType("image/*");
intent.setAction(Intent.ACTION_GET_CONTENT);
startActivityForResult(Intent.createChooser(intent,"Pictures"), 1);


依檔案型態開啟對應程式:


Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse("file://" + path), "image/*");
startActivity(intent);


[Linux] 編譯 WebKitGtk+ @ Ubuntu 12.04

WebKitGtk+


順手記一下要裝的東西:


$ sudo apt-get install bison g++ flex gperf libjpeg-dev libpng-dev libglib2.0-dev libicu-dev libxml2-dev libpango1.0-dev libgail-3-dev libxt-dev libgl1-mesa-dev libsoup2.4-dev libsqlite3-dev libxslt-dev libgeoclue-dev libgstreamer-plugins-base0.10-dev


$ wget http://webkitgtk.org/releases/webkit-1.8.3.tar.xz
$ tar -xvf webkit-1.8.3.tar.gz
$ cd webkit-1.8.3
$ ./configure
$ make 


2012年9月15日 星期六

iOS 開發筆記 - 使用 Category 擴充 functions 與 variables

Category 對 Objective C 是一個很彈性的架構,它可以在不破壞 framework 架構下,彈性地新增一些好玩的 functions 和 variables。


以 UIImage 這個 class 來講,提供 getResizeImage 的函數:


@interface UIImage (MyLib)


- (UIImage *)getResizeImage:(CGSize)size;


@end


@implementation UIImage (MyLib)

- (UIImage *)getResizeImage:(CGSize)size {
        return nil;
}

@end


如此一來,所有的 UIImage 物件都可以使用 [obj getResizeImage:CGSizeMake(width, height)]; 來產生新的縮圖。


然而,有時候則需要搭配一些 variables 來記錄一些狀態,這時就會想到 @property 和 @synthesize 的搭配,可惜的在 Category 狀態下無法使用 @synthesize 來實作,取而代之的是 @dynamic 並且自行實作 setter/getter 函數,如:


UIImage+MyLib.h:


#import <objc/runtime.h>
#import <Foundation/Foundation.h>

@interface UIImage (MyLib)


@property (nonatomic, retain) NSMutableDictionary *otherInfo;


- (UIImage *)getResizeImage:(CGSize)size;


@end


UIImage+MyLib.m:


#import "UIImage+MyLib.h"


@implementation UIImage (MyLib)


@dynamic otherInfo;


NSString * const kOtherInfo = @"kOtherInfo";


- (void)setOtherInfo:(NSMutableDictionary *)obj {
        objc_setAssociatedObject( self, kOtherInfo, obj, OBJC_ASSOCIATION_RETAIN);
}


- (NSMutableDictionary *)otherInfo {
        return objc_getAssociatedObject(self, kOtherInfo);
}


- (UIImage *)getResizeImage:(CGSize)size {
        if( self.otherInfo )  {
        }
        return nil;
}
@end


如此一來,就能好好使用自定的函數跟變數囉。


其他參考資料:



2012年9月13日 星期四

申請 iOS Developer Program 之 Company 版

iOSDeveloperProgram02


申請 iOS Developer Program 流程說真的挺費時又耗工,比較大的變化是 2012.05 底左右,Apple 開始要求 Company 需提供 B&D D-U-N-S 編號,中譯為鄧白氏環球編碼 ,這編號常用於貿易公司,類似第三方機構驗證公司狀況,據說有這編號可以促進交易過程的流暢等等。


原先粗略猜測的流程:


取得 D&B D-U-N-S 帳號 (免錢30天, 付費7天) > 等待 D&B 資料庫與 Apple 資料庫同步(14天以內) > 註冊 Apple ID (數分鐘) > 填寫 iOS Develop Program 之 Company 版申請單(數分鐘) > 等待驗證通知(數天),之後傳真公司文件 > 等待 Apple 驗證公司資料(數天) > 信用卡繳款(數分鐘) > 開通(一天)


實際體驗流程:


註冊 Apple ID 、選擇購買 iOS Developer Program Company 版 (數分鐘) -> 資料填寫卡在 D-U-N-S Number (數天) -> 搞懂後從 Apple 提供的 D-U-N-S Lookup 頁面申請免費的 D-U-N-S Number (約一天收到 D&B 申請回音,約五天收 D&B 給的 D-U-N-S Nubmer ) -> 等待 D&B 全球資料庫同步 (大概5天就可以在 Apple D-U-N-S Lookup 查到資料,但致電Apple客服後仍發現無法進行,所以最後等了14天後,才在請Apple客服處理,也才發現因為之前 try 太多而被 ban 掉了) -> 隔天收到 email 通知,由於無法電話連絡上公司負責人,所以要求回電給 Apple 客服,純粹用電話口頭確認(約10分鐘) -> 進行後續 iOS Developer Program 申請流程、繳費 (約 10分鐘內搞定) -> 收到 iOS Developer Program 信件(啟動)通知 (無需激活,約四小時) 


Total 天數...若不包含查資料的話,大概 21 天內搞定,包含申請免費 D-U-N-S Number (5天)+等待D&B資料庫同步(14天)+等待電話驗證(1天)+等待 iOS Developer Program 啟用通知(1天)


首先要註冊一個公司用的 Apple ID ,接著在 iOS Developer Program 註冊網頁上選用 Company 版,接著需用英文填寫一些資料表:


First Name: 公司負責人名字
Last Name: 公司負責人姓氏
Title: 職稱(如董事長、執行長等等)
Phone: 公司負責人電話 (+886…, 常跟公司電話一致?!)
Email: 公司負責人電子信箱


公司資料:


Organization Type: Company
Legal Entity Name: CompanyName Inc.
D-U-N-S® Number: 9個數字
Website: 官方網站
Work Email: 官方信箱
Headquarters Phone: 總部電話


其中 D-U-N-S® Number 可真是個 Magic Number,網路上打滾了一會,在 www.dnb.com 可得免費申請(需耗時30天),也可以付錢快速取的。結果進去看後,才發現填的表格有的地區限制,僅限美國,結果就是香港就到 www.dnb.com.hk、英國就到 http://www.dnb.co.uk/ 申請等等,接著跑到台灣 www.dnb.com.tw 逛了一下,就看到取得一組最低要價年費 16,800 !! 當然,在 CocoaChina 討論版上,也可以看到一堆哀號聲 XD 普遍都花 1500 甚至數千塊人民幣。後來忍不住就致電給 Apple 客服,想多問看看後,才發現 Apple 有提供申請頁面:D-U-N-S Number Lookup,通常都是先查完沒看到才申請,所以就試看看這樣免費的申請吧!文件上說大概要等待 30 天才知道有沒通過。


若填寫的 D-U-N-S Number 跟所填的 Legal Entity Name 批配的話,就可以進入下一個階段,若一直無法過而顯示以下資訊,那代表 D-U-N-S 尚未同步,或是嘗試太多次而被 ban 掉囉:


Your D&B information was not accepted.


而沒被 ban 掉或是打電話給 Apple 客服請他們幫你解除後,正確申請應該會看到這個驗證流程:


iOSDeveloperProgram01


接著依著流程進行,就會看到一些畫面,以及最後等待公司驗證的資訊:


iOSDeveloperProgram03


iOSDeveloperProgram04


接著就會收到 Email 通知信:


iOSDeveloperProgram05


過個幾天,負責人就會收到電話通知,簡易的電話口頭確認後(無需傳真資料回美國),即可進行下一步:


iOSDeveloperProgram06


iOSDeveloperProgram07


信用卡繳款,別忘了公司統一編號用一下


iOSDeveloperProgram08


iOSDeveloperProgram09


幾小時後,可以收到信件通知


iOSDeveloperProgram10


接著登入後就等同開通,不需像一些文章說的還要去激活 XD


iOSDeveloperProgram11


2012年9月11日 星期二

[OSX] 免費的系統溫度查看軟體 - MacPorts XRG @ Mac OS X 10.8

XRG-View

最近天氣很熱,連 MBP 也熱當過幾次。所以就想找一套監控軟體看看到底多燙,最後想翻翻 MacPorts 找到 XRG 這套軟體囉。

$ sudo port install xrg

XRG-Preferences

此軟體可顯示的還不少(左下角有一堆選項),由於我只需要溫度就只勾幾項啦。使用 XRG 的主因之一是可以單純透過 MacPorts 安裝,想說這樣好像軟體比較有人認證過?安心一點點。不過說真的,就算看到溫度很高好像也沒啥解決方式 XD 只能吹冷氣了吧!?

2012年9月6日 星期四

[Windows] TortoiseGit : git did not exits cleanly(exit code 128) @ Windows 7

 + 


最近在 Windows 7 安裝 Git 時都會蹦出 libiconv-2.dll 找不到的訊息,當下草率解決,直到使用 TortoiseGit 時顯示 git did not exits cleanly(exit code 128) 時,才正式去處理 XD


簡單的說,當 TortoiseGit + PuTTYgen 無法正常存取 git 時,我則改用 MinGW32 的指令,才發現 libiconv-2.dll 的問題未完整解決 :p 總之,處理完 libiconv-2.dll 問題後,TortoiseGit 就能正常使用。


環境簡介,安裝順序:



  1. 安裝 msysgit - Git for Windows (msysGit-fullinstall-1.7.11-preview20120620.exe)

  2. 安裝 TortoiseGit (TortoiseGit-1.7.12.0-64bit.msi)


解法:


假設安裝 msysgit 目錄在 D:\msysgit 時,那只要把 D:\msysgit\mingw\bin\libiconv-2.dll 複製到 D:\msysgit\libexec\git-core\ 即可解決。


2012年9月5日 星期三

[Linux] 使用 Git + Gitolite + Gitweb 架設 Git Server @ Ubuntu 12.04

 


這半年算正式接觸 git 的使用,開始從 svn 轉過去了吧?! :P 接著則架設公司用的 Git Server 服務,就挑選了 Gitolite 套件,以打造像 github.com 服務,並透過 htpasswd 管理,提供簡易的 GitWeb 版,讓公司成員可以透過 Web 進行下載、瀏覽程式碼,而開發成員可以同時且分散式地開發程式。


在這邊就不多說 Git 的使用,而著重在 Gitolite 跟 GitWeb 的部份,其中 Gitolite 則是透過綁定一個系統帳號,而眾人存取都是透過 SSH Key-pair 來進行,只要把 keys 命名及綁定權限,就可以決定哪隻 key 可以讀寫、哪隻 key 只能讀取,達成簡易的權限管理。


Gitolite 的安裝及設定(採用 gitolite 系統帳號):


$ sudo apt-get install gitolite
$ sudo adduser gitolite
$ sudo su gitolite
$ whoami
gitolite


建立管理者(綁在一個 key):


$ cd /path
$ ssh-keygen -t rsa -P '' -f gitolite
$ ls
gitolite gitolite.pub 


初次設定 Gitolite:


$ whoami
gitolite
$ gl-setup /path/gitolite.pub
The default settings in the rc file (/home/gitolite/.gitolite.rc) are fine for most
people but if you wish to make any changes, you can do so now.
hit enter...

creating gitolite-admin...
Initialized empty Git repository in /home/gitolite/repositories/gitolite-admin.git/
creating testing...
Initialized empty Git repository in /home/gitolite/repositories/testing.git/
[master (root-commit) #######] start
2 files changed, 6 insertions(+)
create mode 100644 conf/gitolite.conf
create mode 100644 keydir/gitolite.pub


新增使用者帳號(請對方產生Key-pair後,把 public key 交給你,此例是 alice.pub):


$ git clone gitolite@localhost:gitolite-admin.git
(需使用當初登記為管理者的 key 來存取) 


$ cd gitolite-admin 
$ ls
conf keydir
$ cp /path/alice.pub keydir
$ git add keydir/alice.pub
$ git commit -m 'add users: alice' 
$ git push 


如此一來,該使用者 (alice.pub) 就能夠透過 git clone gitolite@localhost:testing.git 進行存取測試,且新增使用者可遠端進行


新增專案(proj.git) & 設定權限(alice):


$ cd ~/ && ls
projects.list  repositories
$ ls ~/repositories
gitolite-admin.git  testing.git


$ git clone gitolite@localhost:gitolite-admin.git


(需使用當初登記為管理者的 key 來存取) 


$ cd gitolite-admin
$ vim conf/gitolite.conf
repo gitolite-admin
    RW+ = gitolite

repo testing
    RW+ = @all

repo proj
    RW+ = alice
    R      = @all
$ git commit -a -m 'add projects: proj.git' 
$ git push 
...
...
remote: creating proj...
remote: Initialized empty Git repository in /home/gitolite/repositories/proj.git/

$ ls ~/repositories
gitolite-admin.git proj.git testing.git  


如此一來,Alice 就可以拿著自己的 key 用 git clone gitolite@localhost:proj.git 取出來讀寫了,並提供其他人讀取的權限,且新增專案、設定權限皆可遠端進行


架設 Gitweb:


$ sudo apt-get install apache2 gitweb
$ sudo usermod -a -G gitolite www-data
$ sudo vim /etc/gitweb.conf


$projectroot = "/home/gitolite/repositories"; 
$feature{'highlight'}{'default'} = [1];
# 提供系統 loadavg check,若系統繁忙,逛 gitweb 只會看到 503 - The load average on the server is too high 訊息
$maxload = 500; 


設定 repo 建立權限為 0750,如此一來 www-data 才可以存取:


$ vim /home/gitolite/.gitolite.rc
REPO_UMASK = 0027; 


接著設定 htpasswd 來管理 gitweb 的使用:


$ sudo htpasswd -cb /etc/appach2/gitweb.htpasswd ID PASSWD
$ sudo vim /etc/apache2/conf.d/gitweb
Alias /gitweb /usr/share/gitweb
<Directory /usr/share/gitweb>
  Options FollowSymLinks +ExecCGI
  AddHandler cgi-script .cgi
 
  AuthUserFile /etc/apache2/gitweb.htpasswd
  AuthName "GitWeb"
  AuthType Basic
  require valid-user
  Order allow,deny
  Allow from 127.0.0.0/255.0.0.0 10.0.0.0/8 192.168.0.0/16 ::1/128
  satisfy any
</Directory>


如此一來,公司內部(10.0.0.0/8, 192.168.0.0/16) 可以直接逛 gitweb,而外部連線進來則需要 id/passwd 的確認才能瀏覽。如果不想要把某些 project 被 gitweb 讀取的話,就把它 chmod 700 吧 ($ chmod 700 /home/gitolite/repositories/gitolite-admin.git )


@ 2012-09-06 補充:


Ubuntu 12.04 內建的 Gitolite 版本為 2.2-1,尚不支援 Admin Defined Commands (ADC) ?! 故先從 https://github.com/sitaramc/gitolite 取下後來安裝 Gitolite 3.x ,粗略升級流程:



    1. 移除系統 Gitolite


      • $ sudo apt-get remove gitolite


    2. 備份舊版 Gitolite & admin key-pair


      • $ sudo cp -r /home/gitolite /home/gitolite-2.2-1
        $ sudo chown -R gitolite:gitolite /home/gitolite-2.2-1 
        $ mv /home/gitolite-2.2-1/repositories/gitolite-admin.git /home/gitolite-2.2-1/gitolite-admin.git
        $ mv /home/gitolite-2.2-1/repositories/testing.git /home/gitolite-2.2-1/testing.git 


    3. 下載和安裝最新版 Gitolite (在此換掉原先的 gitolite.pub)


      • $ whoami
        gitolite
        $ rm -rf ~/* ~/.gitolite* ~/.ssh
        $ cd ~/
        $ git clone https://github.com/sitaramc/gitolite.git
        $ mkdir ~/.ssh ~/bin
        $ ssh-keygen -t rsa -P '' -f ~/.ssh/gitolite
        $ ls ~/.ssh
        gitolite    gitolite.pub
        $ mv ~/.ssh/gitolite ~/.ssh/id_rsa
        $ gitolite/install -to $HOME/bin
        $ ~/bin/gitolite setup -pk ~/ssh/gitolite.pub
        ...
        $ ls ~/
        bin  gitolite  projects.list  repositories 


    4. 設定 Gitolite.rc 與 Gitweb 的部份


      • $ vim ~/.gitolite.rc
        UMASK   =>   0027,    # = 0750 


    5. 恢復原先 repositories 跟 keys


      • $ cp -r /home/gitolite-2.2-1/repositories/* ~/repositories/ 
        $ cd ~/ 
        $ git clone /home/gitolite-2.2-1/gitolite-admin.git old-gitolite-admin
        $ git clone gitolite@localhost:gitolite-admin.git
        $ rm ~/old-gitolite-admin/keydir/gitolite.pub
        $ cp ~/old-gitolite-admin/keydir/* ~/gitolite-admin/keydir/
        $ cp ~/old-gitolite-admin/conf/gitolite.conf ~/gitolite-admin/conf/gitolite.conf
        $ cd ~/gitolite-admin
        $ git add .
        $ git commit -a -m 'restore init'
        $ git push 



測試 ADC(需設定 ~/.gitolite.rc 的 COMMANDS 清單,預設只有幾項 commands 可以用):


$ ssh gitolite@localhost help
hello gitolite, this is gitolite3 v3.04-15-gaec8c71 on git 1.7.9.5

list of remote commands available:

D
desc
help
info
perms
writable


遠端指定 repo myproj 進行刪除:


$ ssh gitolite@git-server D unlock myproj
$ ssh gitolite@git-server D rm myproj


遠端建立 repo myproj 方式,有兩種方式:



    1. 編輯 gitolite-admin/conf/gitolite.conf,直接新增未存在的 repo 後,git push 後則會自動建立

    2. 在 gitolite-admin/conf/gitolite.conf 給定 C 權限,如此一來,在 git clone 不存在的專案則會自動建立


      • $ vim gitolite-admin/conf/gitolite.conf

        @admin = user1 user2 user3

        repo [a-zA-Z0-9].*
            C  =  @admin
            RW+D  = CREATOR
            R  = @all 

      • $ git clone gitolite@git-server:myproject.git
        ...
        若 myproject.git 不存在,則會自動建立 myproject.git,並且建立者有 D 的權限,可以透過 ssh gitolite@git-server D rm myproj 來刪除



此外,公司常將一些內部使用的服務擺在防火牆後透,並透過 port forwarding 來處理對外連線,這時候需指定 port 連線,常用的有 ssh, git 跟 scp 指令,用法如下:


$ git clone ssh://gitolite@git-server:port/myproject.git
$ ssh -p port gitolite@git-server help
$ scp -r -P port input user@git-server:output 


@ 2012-09-07 搭配使用者自動管理專案之 ADC 常用方式:


編輯 gitolite-admin/conf/gitolite.conf ,提供使用者自動建立 public projects 和 private projects 功能:


repo priv/CREATOR/[a-zA-Z0-9].*
  C = @RDs
  RW+D = CREATOR
  RW = WRITERS
  R = READERS


# Update bin/Gitolite/Rc.m Or add into ~/.gitolte.rc :
# $REPOPATT_PATT        = qr(^\@?[[0-9a-zA-Z\(^][-0-9a-zA-Z._\@/+\\^$|()[\]*?!={},]*$);
repo ^(?!priv/)[0-9a-zA-Z]+$
  C = @RDs
  RW+D = CREATOR
  RW = WRITERS  
  R = @all


當使用者直接用 $ git clone gitolite@git-server:myproj.git 時,若 myproj.git 不存在則自動建立,此屬性屬於第二個,也就是對全體帳號預設都可以 Read 的;如果是用 $ git gitolite@git-server:priv/UserID/myprivate.git 時,這時 myprivate.git 預設是只有建立者可以讀取跟寫入。此外,這些專案的創立者也可以砍掉專案。


至於要對專案開放權限,則可以使用 perms 指令:


增加讀寫權限:


$ ssh gitolite@git-server perms myproject + WRITERS coworker_name


增加讀取權限:


$ ssh gitolite@git-server perms myproject + READERS coworker_name


移除讀寫(讀取)權限:


$ ssh gitolite@git-server perms myproject - WRITERS coworker_name
$ ssh gitolite@git-server perms myproject - READERS coworker_name 


列出權限清單:


$ ssh gitolite@git-server perms -l myproject


刪除專案:


$ ssh gitolite@git-server D unlock myproject
$ ssh gitolite@git-server D rm myproject


移到垃圾桶和還原操作:


$ ssh gitolite@git-server trash myproject
'myproject' moved to trashcan
$ ssh gitolite@git-server list-trash
myproject/2012-09-07_21:45:25
$ ssh gitolite@git-server restore myproject/2012-09-07_21:45:25
'myproject/2012-09-07_21:45:25' restored to 'myproject' 


2012年9月2日 星期日

使用 iTunes 批次更新歌曲資訊 @ Mac OS X

song_itunes02


用 Mac 有一陣子了,最近整理以前買的日劇音樂合輯共有兩片 CD,弄成 MP3 後,卻發現在 iTunes 上顯示為兩張專輯,仔細確認後,原來 iTunes 對歌曲進行分類專輯時,有用到歌曲的敘述,因此就碰到需要更改歌曲內容來達成擺在同一張專輯,故需要批次整理一堆歌曲的方式。


所幸內建 iTunes 可以處理,可以先把專輯名稱改成一樣,會強制把歌曲擺在同一張專輯,接著再把全選所有歌曲,透過簡介方式可以批次更改。


song_itunes01