2010年9月30日 星期四

[轉貼] [Android] JAR Library: Export and Import

原文標題: [Android] JAR Library: Export and Import
原文作者: web_surf@163.com
發表日期: 2010-06-18 09:42
原文來源: https://bbs.et8.net/bbs/showthread.php?t=616987

1.Export Library
* Prepare source code
- Create an Android project
- Create source code, and fix all bug
- Remove res/*
- Remove unused source files
- Edit AndroidManifest.xml to remove the statements that referes to resource, such as android:icon="@drwable/icon", android:label="@string/app_name"
    * Export library
    - On Package Explorer Panel of Eclipse, right-click the project, and select Export
    - Select Jave -> JAR file, then press "Next". At this time a dialog pops up.
    - On the right panel of "Select the resources to export:" group, unselect all items (such as .classpath, .project, AndroidManifest.xlm, default.properties), then press "Next"
    Press "Next"
    Press "Finish"

    Then the library is created.
    (Reference: Eclipse export jar files http://hi.baidu.com/etrigger/blog/item/e1fed134468b2fb2d0a2d3ad.html)
    * xxx

    2.Import Library
    You can use a third party JAR in your application by adding it to your Eclipse project as follows:
    - In the Package Explorer panel, right-click on your project and select Properties.
    - Select Java Build Path, then the tab Libraries.
    - Press the Add External JARs... button and select the JAR file

    Alternatively, if you want to include third party JARs with your package, create a new directory for them within your project and select Add Library... instead.
    It is not necessary to put external JARs in the assets folder.
    (Reference: http://code.google.com/intl/zh-CN/android/kb/commontasks.html)


    簡單的作法是靠Eclipse的Project->Export, 僅選取src的部份, 匯出即可
    Import還是靠Eclipse的Project->Properties->Java Build Path->Libraries->Add External JARs即可

    2010年9月20日 星期一

    [Android] ContentProvider的權限設定

    看起來沒啥, 結果快搞死我了...
    廢話不多說, 情境就是如果Android的process間要共享資源, 通常會實做ContentProvider+SQLite, 再export出適合的uri
    如果想要對ContentProvider設定讀/寫的權限, 要從Manifest.xml下手, 以下範例:

    ContentProvider(提供讀/寫的權限)
    <manifest ...>
      <application ...>
      <provider 
        android:name="{PROVIDER_CLASS}"
        android:authorities="{AUTHORITY}"
        android:multiprocess="true"                
        android:readPermission="{CONTENTPROVIDER_PACKAGE}.{READ_PERMISSION}"
        android:writePermission="{CONTENTPROVIDER_PACKAGE}.{WRITE_PERMISSION}>
      </provider>
      <activity ...></activity>
    </application>
    </manifest> 
    

    ContentResolver(設定為只有讀的權限)
    <manifest ...>
      <application ...>
        <activity ...></activity>
      </application>
      <uses-permission android:name="{CONTENTPROVIDER_PACKAGE}.{READ_PERMISSION}" />
      <permission-tree android:name="{CONTENTPROVIDER_PACKAGE}.{READ_PERMISSION}" />
      <permission 
      android:protectionLevel="dangerous" 
      android:name="{CONTENTPROVIDER_PACKAGE}.{READ_PERMISSION}" />
    </manifest>


    實例說明:
    ContentProvider.apk
    /src/demo/content/provider/myObj.java
    /src/demo/content/provider/myObjProvider.java
    /src/demo/content/provider/myContentProvider.java
    /res/main.xml
    AndroidManifest.xml

    myObj.java
    package demo.content.provider;
    
    import android.net.Uri;
    import android.provider.BaseColumns;
    
    public class myObj implements BaseColumns {
      public static final String AUTHORITY = "demo.content.provider";
      public static final String PATH_SINGLE = "obj/";
      public static final String PATH_MULTIPLE = "objs";
    
      public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" +  PATH_MULTIPLE);
      public static final String NAME = "name";
    }
    

    myObjProvider.java
    package demo.content.provider;
    
    import java.util.HashMap;
    import android.content.ContentProvider;
    import android.content.ContentUris;
    import android.content.ContentValues;
    import android.content.Context;
    import android.content.UriMatcher;
    import android.database.Cursor;
    import android.database.sqlite.SQLiteDatabase;
    import android.database.sqlite.SQLiteOpenHelper;
    import android.database.sqlite.SQLiteQueryBuilder;
    import android.net.Uri;
    import android.provider.BaseColumns;
    
    public class myObjProvider extends ContentProvider {
      private static final int OBJS = 1;
      private static final int OBJ = 2;
      public static final String DATABASE_NAME = "ContentProvider.db";
      public static final String TABLE_NAME = "obj";
      public static final int DATABASE_VERSION = 1;
    
      private static UriMatcher URI_MATCHER = null;
      private static HashMap<String, String> PROJECTION_MAP;
      private static SQLiteDatabase db;
    
      static {
        URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
        URI_MATCHER.addURI(myObj.AUTHORITY, myObj.PATH_MULTIPLE, OBJS);
        URI_MATCHER.addURI(myObj.AUTHORITY, myObj.PATH_SINGLE, OBJ);
        PROJECTION_MAP  = new HashMap<String, String>();
        PROJECTION_MAP.put(BaseColumns._ID, "_id");
        PROJECTION_MAP.put(myObj.NAME, "name");
      }
    
      @Override
      public boolean onCreate() {
        DbHelper mDbHelper = new DbHelper(getContext());
        return (db = mDbHelper.getWritableDatabase()) != null ? true : false;
      }
    
    
      //
      // DbHelper
      //
      private static class DbHelper extends SQLiteOpenHelper {
        private static final String DATABASE_CREATE = "CREATE TABLE " +
          myObjProvider.TABLE_NAME +
          "(_id INTEGER PRIMARY KEY, " +
          "name TEXT UNIQUE NOT NULL)";
    
        public DbHelper(Context context) {
          super(context, DATABASE_NAME, null, DATABASE_VERSION);
        }
    
        @Override
        public void onCreate(SQLiteDatabase db) {
          db.execSQL(DbHelper.DATABASE_CREATE);
        }
    
        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        }
    
        @Override
        public void onOpen(SQLiteDatabase db) {
          super.onOpen(db);
        }
      }
    
      //
      // implement abstract methods
      //
      public int delete(Uri uri, String selection, String[] selectionArgs) {
        int count = 0;
    
        switch (myObjProvider.URI_MATCHER.match(uri)) {
          case OBJS:
            count = db.delete(TABLE_NAME, selection, selectionArgs);
          break;
          case OBJ:
            String segment = uri.getPathSegments().get(1);
            String where = "";
    
            if (selection.length() != 0) {
              where = " AND (" + selection + ");";
            }
            count = db.delete(TABLE_NAME, "_id=" + segment + where, selectionArgs);
          break;
          default:
        }
    
        return count;
      }
    
    
      public String getType(Uri uri) {
        return null;
      }
    
      public Uri insert(Uri uri, ContentValues values) {
        long rowId = 0L;
        ContentValues cv = null;
    
        if (values != null) {
          cv = new ContentValues(values);
        } else {
          cv = new ContentValues();
        }
    
        rowId = db.insert(TABLE_NAME, null, cv);
        Uri result = ContentUris.withAppendedId(uri, rowId);
    
        return result;
      }
    
      public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
        String orderBy = null;
    
        switch (myObjProvider.URI_MATCHER.match(uri)) {
          case OBJS:
            queryBuilder.setTables(myObjProvider.TABLE_NAME);
            queryBuilder.setProjectionMap(myObjProvider.PROJECTION_MAP);
          break;
          case OBJ:
            queryBuilder.setTables(myObjProvider.TABLE_NAME);
            queryBuilder.appendWhere("_id=" + uri.getPathSegments().get(1));
          break;
          default:
        }
    
        Cursor c = queryBuilder.query(db, projection, selection, selectionArgs, null, null, orderBy);
        c.setNotificationUri(this.getContext().getContentResolver(), uri);
      
        return c;
      }
    
      public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        /*
        int count = 0;
        switch (myObjProvider.URI_MATCHER.match(uri)) {
        case OBJ:
        count = db.update(myObjProvider.TABLE_NAME, values, selection, selectionArgs);
        break;
        case OBJS:
        String segment = uri.getPathSegments().get(1);
        String where = ""; 
        if (!TextUtils.isEmpty(selection)) {
        where = " AND (" + selection + ")";
        }   
        count = db.update(myObjProvider.TABLE_NAME, values, "_id=" + segment + where, selectionArgs);
        break;
        default:
        }   
    
        return count;
        */
        return 0;
      }
    }
    

    myContentProvider.java
    package demo.content.provider;
    
    import android.app.ListActivity;
    import android.content.ContentResolver;
    import android.content.ContentValues;
    import android.database.Cursor;
    import android.os.Bundle;
    import android.view.View;
    import android.view.View.OnClickListener;
    import android.widget.Button;
    import android.widget.EditText;
    import android.widget.SimpleCursorAdapter;
    
    public class myContentProvider extends ListActivity {
      private EditText addName;
      private Button addButton;
    
      @Override
      public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    
        addName = (EditText) findViewById(R.id.add_name);
        addButton = (Button) findViewById(R.id.add_button);
        addButton.setOnClickListener(new OnClickListener() {
          public void onClick(View v){
            add();
            fillAdapter();
          }
        });
      }
    
      @Override
      public void onStart() {
        super.onStart();
        fillAdapter();
       }
    
      private void fillAdapter() {
        String[] projection = new String[]{myObj._ID, myObj.NAME};
        ContentResolver resolver = this.getContentResolver();
        Cursor mCursor = resolver.query(myObj.CONTENT_URI, projection, null, null, null);
        startManagingCursor(mCursor);
        String[] from = new String[]{myObj.NAME};
        int[] to = new int[]{android.R.id.text1};
    
        SimpleCursorAdapter adapter = new SimpleCursorAdapter(this, android.R.layout.simple_list_item_1, mCursor, from, to);
        setListAdapter(adapter);
      }
    
      private void add() {
        ContentValues cv = new ContentValues();
        cv.put(myObj.NAME, addName.getText().toString());
        getContentResolver().insert(myObj.CONTENT_URI, cv);
      }
    }
    

    main.xml
    <?xml version="1.0" encoding="utf-8"?>
    <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
      android:layout_width="fill_parent" android:layout_height="wrap_content"
      android:scrollbars="none">
    
      <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent">
    
       <TextView android:layout_width="wrap_content"
         android:layout_height="wrap_content" android:layout_marginLeft="10px"
         android:layout_marginBottom="5px" android:text="-Add NEW Item-" />
       <EditText android:id="@+id/add_name"
         android:layout_width="fill_parent"
         android:layout_height="wrap_content" android:layout_marginLeft="10px"
         android:layout_marginBottom="5px" android:text="" />
       <Button android:id="@+id/add_button"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content" android:layout_marginLeft="10px"
         android:layout_marginBottom="15px" android:text="Add" />
       <ListView
         android:id="@+id/android:list"  
         android:layout_width="fill_parent" 
         android:layout_height="wrap_content" />
      </LinearLayout>
    </ScrollView>
    

    AndroidManifest.xml
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="demo.content.provider"
      android:versionCode="1"
      android:versionName="1.0">
      <application android:icon="@drawable/icon" android:label="@string/app_name">
        <provider android:name="myObjProvider"
          android:authorities="demo.content.provider"
          android:multiprocess="true"
          android:readPermission="demo.content.provider.PERMISSION.READ"
          android:writePermission="demo.content.provider.PERMISSION.WRITE">
        </provider>
    
        <activity android:name=".myContentProvider"
          android:label="@string/app_name">
          <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
          </intent-filter>
        </activity>
      </application>
    </manifest> 
    


    ContentResolver.apk(設定為有讀/寫的權限)
    package demo.content.resolver;
    
    import android.app.ListActivity;
    import android.database.Cursor;
    import android.net.Uri;
    import android.os.Bundle;
    import android.content.ContentResolver;
    import android.content.ContentValues;
    import android.widget.SimpleCursorAdapter;
    
    public class myContentResolver extends ListActivity {
      /** Called when the activity is first created. */
      @Override
      public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    
        String[] projection = new String[]{"_id", "name"};
        Uri uri = Uri.parse("content://demo.content.provider/objs");
    
        ContentResolver resolver = this.getContentResolver();
    
        //
        // insert
        //
        ContentValues cv = new ContentValues();
        cv.put("name", "insert_value_by_content_resolver");
        resolver.insert(uri, cv);
    
        //
        // query
        // 
        Cursor mCursor = resolver.query(uri, projection, null, null, null);
        startManagingCursor(mCursor);
        String[] from = new String[]{"name"};
        int[] to = new int[]{android.R.id.text1};
        SimpleCursorAdapter adapter = new SimpleCursorAdapter(this, android.R.layout.simple_list_item_1, mCursor, from, to);
        setListAdapter(adapter);
      }
    }
    

    main.xml
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
      android:orientation="vertical"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent">
      <ListView
        android:id="@+id/android:list"  
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content"  />
    </LinearLayout>
    

    AndroidManifest.xml
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="demo.content.resolver"
      android:versionCode="1"
      android:versionName="1.0">
      <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".myContentResolver"
          android:label="@string/app_name">
          <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
          </intent-filter>
        </activity>
      </application>
      <uses-permission android:name="demo.content.provider.PERMISSION.READ"></uses-permission>
      <uses-permission android:name="demo.content.provider.PERMISSION.WRITE"></uses-permission>
      <permission-tree android:name="demo.content.provider.PERMISSION.READ"></permission-tree>
      <permission-tree android:name="demo.content.provider.PERMISSION.WRITE"></permission-tree>
      <permission android:protectionLevel="dangerous" android:name="demo.content.provider.PERMISSION.READ"></permission>
      <permission android:protectionLevel="dangerous" android:name="demo.content.provider.PERMISSION.WRITE"></permission>
    </manifest>
    


    * myContentProvider.apk here
    * myContentResolver.apk here

    2010年9月18日 星期六

    [Android] 不同的apk有相同的uid

    在Android上執行不同的apk會跑在不同的sandbox上, 並且以不同的身份(uid)在執行, 但問題是我今天想要用system身份做某些事或是想要在不同的app分享資源時, 就會需要能對apk指定uid, 方法有兩種分別說明如下:

    方法一
    1.修改AndroidManifest.xml, 新增android:sharedUserId="android.uid.system"屬性(使用system身份)
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
    android:sharedUserId="android.uid.system">
    2.修改Android.mk文件,加入LOCAL_CERTIFICATE := platform這一行
    3.重新編譯apk

    方法二
    1.如方法一修改AndroidManifest.xml, 新增android:sharedUserId="android.uid.system"屬性(使用system身份)
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
    android:sharedUserId="android.uid.system">
    2.使用Eclipse編譯出apk
    3.刪除該apk中META-INF目錄下的CERT.SF和CERT.RSA兩個檔案
    4.以系統的public/private key重新簽署打包apk, signapk的用法為
    Usage: signapk [-w] publickey.x509[.pem] privatekey.pk8 input.jar output.jar
    5.實際執行命令如
    $java -jar /path/to/android/out/host/linux-x86/framework/signapk.jar /path/to/android/build/target/product/security/platform.x509.pem /path/to/android/build/target/product/security/platform.pk8 /path/to/input.apk /path/to/output.apk


    如果想要不同的apk以相同的uid身份在執行, 則在這些不同的APK中AndroidManifest.xml的語法為
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
    android:sharedUserId="{MY.URI.FORMAT}">

    reference:
    Android中如何修改系統時間(應用程序獲得系統權限)
    Jollen的Android系統管理雜記,#1:關於 android.uid.system與AID_SYSTEM

    2010年9月15日 星期三

    [Tips] Print to PDF

    有時候會需要把網頁(或任何文件)印成PDF, 在Windows我喜歡用Bullzip, 原因是免費/沒有廣告; 在Ubuntu, 則只需要
    $ sudo apt-get install cups-pdf

    預設輸出位置會在~/PDF,可修改成任意位置(例如從~/PDF到~/Desktop)
    $ sudo vim /etc/cups/cups-pdf.conf
    Out ${HOME}/PDF
    #Out ${HOME}/Desktop
    
    $ sudo /etc/init.d/cupsys restart

    簡單/快速/免設定, that's it

    2010年9月1日 星期三

    [應用程式] git via http

    在git協同工作模式下,會根據不同角色設定RW和RO的權限,一般來說透過ssh的存取通常為RW,透過curl/http(s)通常為RO(若不使用WebDAV),前面介紹過以Gitosis管理ssh存取的安裝設定方式,本篇紀錄透過http存取的安裝設定方式

    1. http via gitweb,如git.kernel.org

    下載安裝gitweb
    $ sudo apt-get install gitweb

    修改gitweb設定檔,主要是設定repository的目錄(本篇沒有修改)
    $ sudo vim /etc/gitweb.conf
    $projectroot = "/var/cache/git"

    複製gitweb目錄
    $ sudo cp /usr/share/gitweb /var/www/
    $ sudo cp /usr/lib/cgi-bin /var/www/gitweb

    用htpasswd設定foobar密碼(第一次建立加上 -c)
    $ sudo /usr/local/apache2/bin/htpasswd -c /usr/local/apache2/conf/git-auth-file foobar

    修改httpd.conf內容
    $ sudo vim /usr/local/apache2/conf/httpd.conf
    Alias /gitweb /var/www/gitweb
    <Directory /var/www/gitweb>
      SetEnv GITWEB_CONFIG /etc/gitweb.conf
      Options ExecCGI
      AddHandler cgi-script cgi
      DirectoryIndex /gitweb/cgi-bin/gitweb.cgi
      AuthType Basic
      AuthName "gitweb"
      AuthUserFile /usr/local/apache2/conf/git-auth-file
      Require valid-user
      Order allow,deny
      Allow from all
      AllowOverride None
    </Directory>
    

    在git-server上建立kernel_source,詳細過程參考Gitosis HOWTO
    $ sudo vim /path/to/gitosis-admin/gitosis.conf
    [repo kernel_source]
    owner = foo@git-client-foo
    gitweb = yes
    description = kernel source development
    

    從git-server上將repository取回
    $ git clone git@git-server:kernel_source
    $ cd kernel_source
    $ touch README
    $ git add README
    $ git commit -am '[add] README'
    $ git push origin master

    建立kernel_source.git在/var/cache/git目錄下
    $ sudo git clone --bare kernel_source /var/cache/git/kernel_source.git

    修改讀取權限
    $ cd /var/cache/git/kernel_source.git
    $ sudo git update-server-info

    用瀏覽器看結果
    http://git-server/gitweb

    修改gitweb的頁面中description和owner欄位
    $ sudo vim /var/cache/git/kernel_source.git/description
    this is kernel source

    $ sudo vim /var/cache/git/kernel_source.git/config
    [gitweb]
    owner = "foobar"



    2. http via curl

    如果沒有建立git-server,一樣可以建立用http存取的git服務
    $ mkdir kernel_source
    $ cd kernel_source
    $ git init
    $ touch README
    $ git add README
    $ git commit -am '[add] README'

    建立kernel_source.git在/var/www/html目錄下(步驟與上面相同, 只有目錄不同)
    $ sudo git clone --bare kernel_source /var/www/html/kernel_source.git

    修改讀取權限(步驟與上面相同, 只有目錄不同)
    $ cd /var/www/html/kernel_source.git
    $ sudo git update-server-info

    用htpasswd設定foobar密碼(第一次建立加上 -c)
    $ sudo /usr/local/apache2/bin/htpasswd -c /usr/local/apache2/conf/git-auth-file foobar

    為了要能讓用http抓下的code可以push回去,必須開啟webdav,而首先要先確定apache是否有載入dav和dav_fs模組,可以透過apachectl查看目前已載入的模組
    $ /path/to/apache2/bin/apachectl -t -D DUMP_MODULES
    dav_module (shared)
     dav_fs_module (shared)
     dav_lock_module (shared)
     dav_svn_module (shared)
    

    如果沒有載入,一種就是用a2enmod指令手動載入需要的模組
    $ a2enmod dav
    $ a2enmod dav_fs
    或是重新config和install,例如
    $ configure --enable-dav --enable-dav-fs

    當dav的模組都載入後,修改httpd.conf內容
    DavLockDB /var/lock/dav/lockdb
    <Directory "/var/www/html/kernel_source.git">
      Dav on
      Options FollowSymLinks Indexes ExecCGI
      AuthType Basic
      AuthName "gitweb"
      AuthUserFile /usr/local/apache2/conf/git-auth-file
      Require valid-user
      Order allow,deny
      Allow from all
      AllowOverride None
    </Directory>

    由於加上apache的basic authentication, 因此在使用curl時需先加上帳號密碼的驗證
    $ vim ~/.netrc
    # add
    machine git-server
    login foobar
    password ooxx

    測試看看是否可正常連線
    $ curl --netrc --location -v http://git-server/kernel_source.git

    如果看到類似這樣的內容就是正確的結果了
    <html>
    <head>
    <title>Index of /kernel_source.git</title>
    </head>
    <body>
    <h1>Index of /kernel_source.git</h1>
    <ul><li><a href="/"> Parent Directory</a></li>
    <li><a href="HEAD"> HEAD</a></li>
    <li><a href="branches/"> branches/</a></li>
    <li><a href="config"> config</a></li>
    <li><a href="description"> description</a></li>
    <li><a href="hooks/"> hooks/</a></li>
    <li><a href="info/"> info/</a></li>
    <li><a href="objects/"> objects/</a></li>
    <li><a href="packed-refs"> packed-refs</a></li>
    <li><a href="refs/"> refs/</a></li>
    </ul>
    </body></html>

    使用curl讀取kernel_source.git
    $ git clone http://git-server/kernel_source.git


    由於我還不知道本地repository更新後, 要如何更新repository.git的內容, 所以我每次都要砍掉重練, 而有鑑於砍掉重練的步驟頗重複, 所以我寫了一個update_repo_git.sh處理
    #!/bin/bash
    git_repo_path="/home/foobar/git"
    git_http_path="/var/www/html"
    git_web_path="/var/cache/gitweb"
    
    while [ $# -gt 0 ]
    do
      # git http access
      rm -Rf "$git_http_path"/"${1}".git
      git clone --bare "$git_repo_path"/"${1}" "$git_http_path"/"${1}".git
      cd "$git_http_path"/"${1}".git
      git update-server-info
    
      # change folder permission for webdav
      chown -R daemon:daemon "$git_http_path"/"${1}".git
    
      # gitweb access
      rm -Rf "$git_web_path"/"${1}".git
      git clone --bare "$git_repo_path"/"${1}" "$git_web_path"/"${1}".git
      cd "$git_web_path"/"${1}".git
      git update-server-info
      shift
    done

    未來若是repository時, 要同步更新gitweb和web上的repository(可同時加上多個repo名稱)
    $ ./update_repo_git.sh kernel_source
    就可透過gitweb查看,只要在瀏覽器的網址列輸入http://git.server/gitweb即可
    並可透過http的方式下載,只要下git clone http://git.server/kernel_source.git即可


    參考資料:
    * Learn GIT series – Part 2: Install GITWEB to host repository on Apache web-server
    * Debian Linux 架設 Gitweb
    * Debian Linux 架設使用 HTTP 存取 的 Git Server