2012年5月13日 星期日

回家走走吧~

2012-05-13


最近發生了一連串的事情,回家走走吧 :) 母親節快樂。


2012年5月9日 星期三

Android 開發筆記 - 使用 ListView 和 ListActivity

simple_list_item_1 simple_list_item_2


對 HTML 來說,所謂的 ListView 大概稱得上網頁上常看到的 ul、ol、dt 或是 table 吧!這東西真的是萬用的好工具,最直觀的排版,正所謂規規矩矩,不美也不會太醜。最近我終於開始在 Android 上使用這個東西了,光這功能就能讓你完成九成的工作了。更多 ListViewListActivity 的資訊,請參考官網的介紹,這邊僅簡易的筆記。


在 MyActivity 上呼叫自訂的 MyListActivity 的方式:


public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);


        Intent mIntent = new Intent();
        mIntent.setClass(this, MyListActivity.class);
        startActivity( mIntent );
}


在 MyListActivity 中,使用簡易的排版。範例一,使用系統內建 android.R.layout.simple_list_item_1 排版:


simple_list_item_1


 程式碼:


package com.example.study;


import android.app.ListActivity;
import android.os.Bundle;
import android.widget.ArrayAdapter;


public class MyListActivity extends ListActivity {
        @Override
        public void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);

                String[] values = new String[] { "Android", "iPhone", "WindowsMobile", "Blackberry", "WebOS", "Ubuntu", "Windows7", "Max OS X", "Linux", "OS/2" };
                setListAdapter(
                        new ArrayAdapter<String>(
                                this, 
                                android.R.layout.simple_list_item_1,
                                values) 
                );
        }
}


範例二,使用系統內建 android.R.layout.simple_list_item_2 排版:


simple_list_item_2


程式碼:


package com.example.study;


import android.app.ListActivity;
import android.os.Bundle;
import java.util.ArrayList;
import java.util.HashMap;
import android.widget.SimpleAdapter;


public class ProgramListActivity extends ListActivity {
        @Override
        public void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);


                final String ID_TITLE = "TITLE", ID_SUBTITLE = "SUBTITLE";

                ArrayList<HashMap<String,String>> myListData = new ArrayList<HashMap<String,String>>();
                String[] titles = new String[]{ "Title1" , "Title2", "Title3" };
                String[] subtitles = new String[]{ "SubTitle1" , "SubTitle2", "SubTitle3" };

                for( int i=0;i<titles.length ; ++i) {
                        HashMap<String,String> item = new HashMap<String,String>();
                        item.put(ID_TITLE,titles[i]);
                        item.put(ID_SUBTITLE,subtitles[i]);
                        myListData.add(item);
                }

                setListAdapter( new SimpleAdapter(
                        this,
                        myListData,
                        android.R.layout.simple_list_item_2,
                        new String[] { ID_TITLE, ID_SUBTITLE },
                        new int[] { android.R.id.text1, android.R.id.text2 } )
                );
        }
}


其中 android.R.layout.simple_list_item_1.xml 和 android.R.layout.simple_list_item_2.xml 的資料,可以查閱自己用的 SDK 囉,例如使用 Android 2.2 時,在 Eclipse 的 Package Explorer 中,可以瀏覽 Android 2.2 -> android.jar -> res.layout 並在裡頭找到。


此處提的範例都是 ListActivity 的,如果你在你的 Activity 的 layout 上擺了一個 ListView 的話,那只需要先用 findViewById 後,就可以仿照 ListActivity 把資料餵進去,如:


ListView mListView = (ListView)findViewById(R.id.listView1);
if( mListView != null ) {
        mListView.setAdapter(
                new SimpleAdapter(
                        this,
                        mDataSource ,
                        android.R.layout.simple_list_item_2,
                        new String[] { ID_TITLE, ID_DETAL },
                        new int[] { android.R.id.text1, android.R.id.text2 }
                )
        );
}


Android 開發筆記 - 使用 Content Provider


來源:Calendar Provider data model


由於安全設計關係,預設 app 操作使用的 sqlite db 都是在 sandbox 環境,換句話說,也就是僅 app 自身可獨立存取得而已,其他 app 是不能存取的。因此,如果想要提供自身的 sqlite db 資料給其他程式使用,除了寫 Android Service 架構來達到 IPC 或寫 Socket 程式溝通資料外,還可以採用 Content Provider 的架構。當然,在此不討論 root 後突破權限管理的機制 XD 舉例來說,Android 系統內建 Calendar app,而使用 Android 會綁定一個 Gmail 帳號,那有沒辦法取得此帳號的行事曆呢?此時可以透過 Content Provider 來進行操作。


首先,想要讀寫 Calendar 資料,那在 app 的 AndroidManifest.xml 權限上,需要加上 android.permission.READ_CALENDAR 和 android.permission.WRITE_CALENDAR,這樣的設計是告知使用者此程式將會存取 calendar 資料。


接著 app 中,可以透過 ContentResolver 物件,向 Calendar app 進行存取,如 content://CONTENT_URI/DBTable,此例為 Calendars.CONTENT_URI 等於 content://calendar/calendars,但建議使用 Calendars.CONTENT_URI 變數來存取,因為在各個 Android 版本中,可能因為軟體升級或架構改變而儲存位置變更,因此透過 Calendars.CONTENT_URI 存取最為恰當(有些版本為 content://com.android.calendar/calendars)。


片段程式碼:


// Projection array. Creating indices for this array instead of doing
// dynamic lookups improves performance.
public static final String[] EVENT_PROJECTION = new String[] {
        Calendars._ID, // 0
        Calendars.ACCOUNT_NAME, // 1
        Calendars.CALENDAR_DISPLAY_NAME, // 2
        Calendars.OWNER_ACCOUNT // 3
};

// The indices for the projection array above.
private static final int PROJECTION_ID_INDEX = 0;
private static final int PROJECTION_ACCOUNT_NAME_INDEX = 1;
private static final int PROJECTION_DISPLAY_NAME_INDEX = 2;
private static final int PROJECTION_OWNER_ACCOUNT_INDEX = 3;


// Run query
Cursor cur = null;
ContentResolver cr = getContentResolver();
Uri uri = Calendars.CONTENT_URI;
String selection = "((" + Calendars.ACCOUNT_NAME + " = ?) AND ("
        + Calendars.ACCOUNT_TYPE + " = ?) AND ("
        + Calendars.OWNER_ACCOUNT + " = ?))";
String[] selectionArgs = new String[] {"sampleuser@gmail.com","com.google","sampleuser@gmail.com"};
// Submit the query and get a Cursor object back.
cur = cr.query(uri, EVENT_PROJECTION, selection, selectionArgs, null);


此例 SQL 語法 = "SELECT Calendars._ID, Calendars.ACCOUNT_NAME, Calendars.CALENDAR_DISPLAY_NAME, Calendars.OWNER_ACCOUNT FROM Calendars WHERE Calendars.ACCOUNT_NAME = sampleuser@gmail.com AND Calendars.ACCOUNT_TYPE = com.google AND Calendars.OWNER_ACCOUNT = sampleuser@gmail.com";。


剩下的就跟操作 SQLite 一樣:


// Use the cursor to step through the returned records
while (cur.moveToNext()) {
        long calID = 0;
        String displayName = null;
        String accountName = null;
        String ownerName = null;

        // Get the field values
        calID = cur.getLong(PROJECTION_ID_INDEX);
        displayName = cur.getString(PROJECTION_DISPLAY_NAME_INDEX);
        accountName = cur.getString(PROJECTION_ACCOUNT_NAME_INDEX);
        ownerName = cur.getString(PROJECTION_OWNER_ACCOUNT_INDEX);

        // Do something with the values...


        ...
}


此外,如果要更新某一筆資料,也一樣透過 URI 來進行操作,僅需在最後加上 Record ID,如 content://CONTENT_URI/DBTable/RecordID,在進行資料的操作:


long calID = 2;
ContentValues values = new ContentValues();
// The new display name for the calendar
values.put(Calendars.CALENDAR_DISPLAY_NAME, "Trevor's Calendar");
Uri updateUri = ContentUris.withAppendedId(Calendars.CONTENT_URI, calID);
int rows = getContentResolver().update(updateUri, values, null, null);


此例 SQL 約略為 "UPDATE Calendars SET Calendars.CALENDAR_DISPLAY_NAME = Trevor's Calendar WHERE Calendars._ID = 2";。


當然也免不了新增資料:


long calID = 3;
long startMillis = 0;
long endMillis = 0;
Calendar beginTime = Calendar.getInstance();
beginTime.set(2012, 9, 14, 7, 30);
startMillis = beginTime.getTimeInMillis();
Calendar endTime = Calendar.getInstance();
endTime.set(2012, 9, 14, 8, 45);
endMillis = endTime.getTimeInMillis();
...
ContentResolver cr = getContentResolver();
ContentValues values = new ContentValues();
values.put(Events.DTSTART, startMillis);
values.put(Events.DTEND, endMillis);
values.put(Events.TITLE, "Jazzercise");
values.put(Events.DESCRIPTION, "Group workout");
values.put(Events.CALENDAR_ID, calID);
values.put(Events.EVENT_TIMEZONE, "America/Los_Angeles");
Uri uri = cr.insert(Events.CONTENT_URI, values);


// get the event ID that is the last element in the Uri
long eventID = Long.parseLong(uri.getLastPathSegment());
//
// ... do something with event ID
//
//


以上在 Android Developers 官網都有非常詳細的描述,在此僅簡易筆記一下,更多的 Content Provider 請參閱 package android.provider,裡頭可以看到系統內建有哪些資料可以存取使用囉!


Android 開發筆記 - 使用 SQLite / SQLiteDatabase / SQLiteOpenHelper

MyDBHelper


SQLite 這幾年來已經成了一種標準用法,無論是嵌入式或是 PC 上的軟體,很容易看的到 SQLite 的蹤跡了。


對 Android 系統架構我還沒那麼熟,粗略知道它有 SQLiteDatabase 和 SQLiteOpenHelper 兩個 class 可供 SQLite 操作使用。網路上翻到的資料,大概要先撰寫一隻 SQLiteOpenHelper,此角色是用來初始化資料庫,包含建 Table 、資料庫升級等對應動作。有些人習慣把資料庫的操作都寫在同一個 class (extends SQLiteOpenHelper)中,由於我經驗尚淺,我不曉得這樣的做法是不是正確的,因為觀看一些物件特性及機制似乎不太恰當。以下用簡易範例當作筆記。


新增 MyDBHelper 並繼承 android.database.sqlite.SQLiteOpenHelper,接著實作 void onCreate(SQLiteDatabase db) 和 void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) 後,就算完成 SQLiteOpenHelper 的工作了:


package com.example.study;


import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteOpenHelper;


public class MyDBHelper extends SQLiteOpenHelper {
        final private static int _DB_VERSION = 1;
        final private static String _DB_DATABASE_NAME = "MyDatabases.db";
        public MyDBHelper(Context context) {
                super(context,_DB_DATABASE_NAME,null,_DB_VERSION);
        }
        public MyDBHelper(Context context, String name, CursorFactory factory, int version) {
                super(context, name, factory, version);
                // TODO Auto-generated constructor stub
        }


        @Override
        public void onCreate(SQLiteDatabase db) {
                // TODO Auto-generated method stub
                db.execSQL(
                        "CREATE TABLE MyTable (" +
                                " _ID INTEGER PRIMARY KEY, " +
                                " _DATA VARCHAR(50) NOT NULL, " +
                                " _DATETIME DATETIME NULL " +
                        ")"
                );
        }


        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
                // TODO Auto-generated method stub
                db.execSQL("DROP TABLE IF EXISTS MyTable");
                onCreate(db);
        }
}


接著在 MyTestingActivity 中,就可以操作:


package com.example.study;


import android.app.Activity;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.util.Log;


public class SQLiteDBTestingActivity extends Activity {
        /** Called when the activity is first created. */
        @Override
        public void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.main);

                // Init
                MyDBHelper mHelper = new MyDBHelper(this);
                SQLiteDatabase mDB = null;

                // Insert by raw SQL
                mDB = mHelper.getWritableDatabase();
                mDB.execSQL( "INSERT INTO MyTable (_DATA,_DATETIME) VALUES ('Hello World', datetime('now'))");
                mDB.close();

                // Insert by object method
                mDB = mHelper.getWritableDatabase();
                ContentValues values = new ContentValues();
                values.put("_DATA","YoHO");
                values.put("_DATETIME","2012-05-09 20:30:00");
                mDB.insertOrThrow("MyTable",null,values);
                mDB.close();

                // Query by raw SQL
                mDB = mHelper.getWritableDatabase(); // mDB = mHelper.getReadableDatabase();
                Cursor cursor = mDB.rawQuery("SELECT _ID, _DATA, _DATETIME FROM MyTable", null);
                cursor.moveToFirst();
                while(!cursor.isAfterLast()) {
                        Log.e("SQLiteDBTestingActivity","_ID = "+cursor.getInt(0));
                        Log.e("SQLiteDBTestingActivity","_DATA = "+cursor.getString(1));
                        Log.e("SQLiteDBTestingActivity","_DATETIME = "+cursor.getString(2).substring(0, 16));
                        cursor.moveToNext();
                }
                startManagingCursor(cursor);
                cursor.close();
                mDB.close();
        }
}


執行結果:


output


從上述片段程式來看,可以看到一開始 SQLiteOpenHelper 初始化必須傳入一個 Context,並且執行資料庫操作是透過 getWritableDatabase() 或 getReadableDatabase() 而來的,且查詢時會透過 Cursor 甚至 Activity 的 startManagingCursor 等動作,結束時還需做一些 close 的資源回收,因此,個人覺得把這一系列的動作都包進 SQLiteOpenHelper 似乎不太恰當。目前經驗尚淺,或許需要多累積一些實作經驗才會明瞭吧!


此外,可以透過 DDMS 將模擬器內的 SQLiteDB 取出來 (/data/data/com.example.study/databases/MyDatabases.db),並且用 sqlite3 等相仿的 cmd 去操作:


$ sqlite3 MyDatabases.db
SQLite version 3.6.22
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .table
MyTable android_metadata
sqlite> SELECT * FROM MyTable;
1|Hello World|2012-05-09 12:41:07
2|YoHO|2012-05-09 20:30:00
sqlite>


2012年5月7日 星期一

[Linux] 免費編輯 PDF 文件 (刪除、新增頁面) @ Ubuntu 10.04

pdfeditor


最近偶爾會編輯一下 PDF 檔案,由於在 Windows 上似乎沒有幾套免費的 PDF 編輯軟體,有的免費的不支援存檔或是增減頁數,於是我只好默默開啟 Ubuntu 後,安裝 PDF Editor 啦 XD


$ sudo apt-get install pdfedit


接著就可以開啟想要更改的 pdf 檔案,例如把其中一頁給刪掉 (Remove Page)


remove page


如果編輯文件時,底下的視窗出現類似的資訊:


PDFedit
Loaded file : test.pdf
> removePageWithConditionalRefresh()
! In script '/usr/share/pdfedit/page.qs', line 118:
! Error. Exception in Pdf.removePage : Document is read-only


此時要先透過 [Tools] -> [Delinearize] 重新選這個檔案(此例為test.pdf),再另存起來(如 test2.pdf),接著開啟 test2.pdf 就能夠編輯。


pdfedit_add_pages


此外,也可以新增 pdf 檔案進來 [Tools] -> [Insert Pages from other document],接著開啟想要匯入的檔案後,可以把想要的 page 匯入,並調整頁數位置。雖然匯入的格式必須是 PDF 檔案,但也還算方便啦。