2011年12月29日 星期四

Android - 開機(boot complete)後自動執行程式(activity/service)

如果希望android裝置一開機時,就能執行某支程式,無論是activity或是service,流程大致如下:

(1) 需要一個BroadcastReceiver

(2) AndroidManifest.xml對這個Receiver定義intent filter去接收android.intent.action.BOOT_COMPLETED

(3) 在AndroidManifest.xml宣告擁有權限android.permission.RECEIVE_BOOT_COMPLETED

(4) 當Recevier收到intent後,去執行activity或是service


一開就就執行某service 的程式範例如下:
AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.test.testboot"
    android:versionCode="1"
    android:versionName="1.0" >

    <application android:icon="@drawable/ic_launcher"
                 android:label="@string/app_name" >
        <receiver android:name=".util.CommonReceiver">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"/>
            </intent-filter>
        </receiver>
        <service android:name=".service.UpdaterInitService"/>          
    </application>

    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />   
       
</manifest>


Receiver程式如下:
public class CommonReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
       
        if(action.equals(Intent.ACTION_BOOT_COMPLETED)) {
            Intent startIntent = new Intent();
            startIntent.setClass(context, TestService.class);
            context.startService(startIntent);
        }
    }
}

如此一來,開機時系統會broadcast intent "ACTION_BOOT_COMPLETED",我們的Receiver一收到,就會啟動一條TestService

Android - FLAG_RECEIVER_REGISTERED_ONLY介紹

一般而言,sendBroadcast()一個Intent時,有兩種receiver可以收到。

(1) component型式的BroadcastReceiver, 要收的Intent會定義在AndroidManifest.xml
(2) 在java code中動態定義的BroadcastReceiver,用IntentFilter去定義要收那些Intent的Action,再利用registerReceiver()指定IntentFilter給Receiver

如果你不希望sendBroadcast()時,所有的Receiver都被Launch而動作的話,可以利用
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
這樣就只有第2種Receiver會有反應,AndroidManifest.xml所定義的是不會有反應的。

例如在有關耳機控制的程式中,HeadsetObserver.java就是利用

Intent intent = new Intent(Intent.ACTION_HEADSET_PLUG);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
ActivityManagerNative.broadcastStickyIntent(intent, null);

來確保只有第2種Receiver會有反應。有關broadcastStickyIntent()可參考另一篇文章介紹

Android - sendStickyBroadcast(1) - 簡介

sendBroadcast()的用法大家比較常用,這邊不介紹。我們介紹sendStickyBroadcast()的用法,請先看官方說明。基本上是說,sticky intent在被broadcast出去之後,還會留在系統內,不會消失。如果有某個component有register這個Intent可以被receive的話,當這個component被執行時,還是可以receive這個「早就broadcast過的Intent」。sendStickyBroadcast()讓有component去register一個receiver收這個Intent時,馬上就可以收到。

也就是說,如果是sendBroadcast()只能實現,先執行component,再broadcast Intent,component才能收到這個Intent。但是改用 sendStickyBroadcast()則可以實現先broadcast Intent,再執行component,然後component去收Intent的順序

下面的範例需要2個Activity

Activity1.java
Activity2.java

我 們在Activity1中先sendStickyBroadcast(), 然後按去start Activity2,會發現Activity2可以收到剛才那個Intent。如果一開始是用sendBroadcast()的話,會發現按下start Activity2,是沒有辦法印出"This is Activity2 onReceive()"這段LOG的。

AndroidManifest.xml內容如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.test.stickyintent"
    android:versionCode="1"
    android:versionName="1.0" >

    <application
        android:icon="@drawable/ic_launcher">
        <activity
            android:name=".Activity1" >
            <intent-filter >
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
       
        <activity
            android:name=".Activity2" >
        </activity>           
    </application>

    <uses-permission android:name="android.permission.BROADCAST_STICKY"/>   
       
</manifest>


Activity1.java 程式如下:
public class Activity1 extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main1);
       
        OnClickListener lis = new OnClickListener(){
            @Override
            public void onClick(View arg0) {
                switch (arg0.getId()) {
                case R.id.btn1:
                    btn1();
                    break;
                case R.id.btn2:
                    btn2();
                    break;
                }
            }   
        };
       
        Button btn1 = (Button) findViewById(R.id.btn1);
        Button btn2 = (Button) findViewById(R.id.btn2);
        btn1.setOnClickListener(lis);
        btn2.setOnClickListener(lis);
    }
   
    private void btn1(){
        Intent it = new Intent("com.test.stickyintent");
        sendStickyBroadcast(it);       
    }
   
    private void btn2(){
        Intent it = new Intent(this, Activity2.class);
        startActivity(it);
    }
}


Activity2.java程式如下:
public class Activity2 extends Activity {
    /** Called when the activity is first created. */
    private IntentFilter mIntentFilter;
   
    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            Log.d("gill", "This is Activity2 onReceive()");
        }
    };
   
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d("gill","Activity2 onCreate()");
       
        setContentView(R.layout.main2);
        mIntentFilter = new IntentFilter();    
        mIntentFilter.addAction("com.test.stickyintent");   
    }
   
    @Override 
    protected void onResume() { 
        super.onResume(); 
        Log.d("gill","Activity2 onResume()");
        registerReceiver(mReceiver, mIntentFilter);  
    }
   
    @Override
    protected void onPause() {
        super.onPause();
        Log.d("gill","Activity2 onRause()");
        unregisterReceiver(mReceiver);
    }
}
程式畫面如下:






















執行結果LOG如下:
D/gill    ( 2443): Activity2 onCreate()
D/gill    ( 2443): Activity2 onResume()
D/gill    ( 2443): This is Activity2 onReceive()

Android - HoneyComb的notification builder

在3.0之後,針對status bar notification,google已經不建議使用
Notification notification = new Notification(icon, tickerText, when);
這樣的用法,
應該要改用 Notification.Builder 來取代。
詳細使用參考網頁

程式範例如下:
NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
Notification.Builder mNotificationBuilder = new Notification.Builder(this);

Intent anotherIntent = new Intent(context, AnotherActivity.class);

mNotificationBuilder.setSmallIcon(R.drawable.icon)
    .setAutoCancel(false)
    .setContentTitle("Title")
    .setContentText("Notification Message Content")
    .setContentIntent(PendingIntent.getActivity(context, 0, anotherIntent, 0));

mNotificationManager.notify(R.drawable.icon, mNotificationBuilder.getNotification());

執行結果如下

Java - 檔案型態 get the file type

如果你想要得知某個檔案的型態,程式如下:

        FileNameMap fileNameMap = URLConnection.getFileNameMap();
        String type = fileNameMap.getContentTypeFor("/mnt/sdcard/a.jpg");
        Log.d("gill","file type = " + type);

你可以從log看到
D/gill    ( 5710): file type = image/jpeg

這個檔案是image且是jpeg的型態

Java - 檔案處理(2) - 複製檔案

在JDK1.4之前,複製檔案可以利用

(1) FileInputStream
(2) FileOutputStream
(3) read()

這三個關鍵方法來完成,程式如下:
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            fis  = new FileInputStream("/mnt/sdcard/xxx.apk");
            fos = new FileOutputStream("/mnt/sdcard/xxx2.apk");
            byte[] buf = new byte[1024];
            int i = 0;
            while ((i = fis.read(buf)) != -1) {
                fos.write(buf, 0, i);
            }
            if (fis != null) fis.close();
            if (fos != null) fos.close();
        }
        catch (Exception e) {
            e.printStackTrace();
        }

到了JDK1.4之後,有更具效率的新套件可以用。我們改用FileChannel的方式來實作程式:
        FileChannel fis = null;
        FileChannel fos = null;
        try {
            fis  = new FileInputStream("/mnt/sdcard/xxx.apk").getChannel();
            fos = new FileOutputStream("/mnt/sdcard/xxx2.apk").getChannel();
            fos.transferFrom(fis, 0, fis.size());
            if (fis != null) fis.close();
            if (fos != null) fos.close();
        }
        catch (Exception e) {
            e.printStackTrace();
        }

Java - 檔案處理(1) - 如何複製貼上檔案(copy paste)

如果想要把某一個檔案,複製到另一個地方,程式如下:

        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            fis  = new FileInputStream("/mnt/sdcard/sample.apk");
            fos = new FileOutputStream("/mnt/sdcard/sample.apk");
            byte[] buf = new byte[1024];
            int i = 0;
            while ((i = fis.read(buf)) != -1) {
                fos.write(buf, 0, i);
            }
            if (fis != null) fis.close();
            if (fos != null) fos.close();
        }
        catch (Exception e) {
            e.printStackTrace();
        }

Android - 利用DownloadManager下載檔案

在Android2.3中,官網已經提供透過http下載檔案的API,請看官網說明
這裡直接使用範例

package com.test.testdownload;

import android.app.Activity;
import android.app.DownloadManager;
import android.app.DownloadManager.Request;
import android.net.Uri;
import android.os.Bundle;

public class Main extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
       
        DownloadManager dm = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
       
        //指定檔案來源,必需是http,hppts會有錯誤
        Request request = new Request(
        Uri.parse("http://www.ballpure.com/uploads4/userup/1107/25003614Z55.jpg"));
       
        //存檔路徑及檔名
        Uri uri = Uri.parse("file:///mnt/sdcard/download_name.jpg");
        request.setDestinationUri(uri);
       
        //開始下載
        dm.enqueue(request);
    }
}

AndroidManifest.xml 如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.test.testdownload"
      android:versionCode="1"
      android:versionName="1.0">
    <uses-sdk android:minSdkVersion="9" />

    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".Main"
                  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="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
   
</manifest>

Java - CRC32驗證程式或檔案

如果想要驗證你的程式或是檔案有沒有被更改過時,CRC32是一個很簡單的方式。比如你有一支程式可以幫手機進行檔案的更新,通常作法是先讓使用者 下載新的程式,然後再安裝。但是如果下載後,過了一段時間,程式檔案已經發生損毀,使用者不知道還進行安裝,有可能發生不可預期的錯誤。

因 此,應該在安裝前先檢查一下準備安裝的程式是不是和原本下載的程式一模一樣。CRC32的運作流程是,首先去網路上找一個CRC32的軟體,它可以計算出 你的程式的checksum,然後安裝前再用你自己寫的CRC32程式碼再計算一次當下的程式的checksum, 這兩個checksum如果相同,就代表程式沒有變動過。

用java計算 checksum程式如下:

先要
import java.util.zip.CRC32;
import java.util.zip.CheckedInputStream;

然後如下:

try {
            CheckedInputStream cis = null;
            cis = new CheckedInputStream(new FileInputStream("/data/test.apk), new CRC32());
            byte[] buf = new byte[128];
            while(cis.read(buf) >= 0) {
            }

            checksum = cis.getChecksum().getValue();

        } catch (Exception e) {
            e.printStackTrace();
        }

假設你原本用網路上抓下來的軟體計算出來的checksum是1000,
然後  checksum = cis.getChecksum().getValue();  這裡計算出來的也是1000,
表示目前這個test.apk和原本提供給使用者的程式是相同的。如果計算出來不是1000,就表示test.apk和當時的程式已經不同了。

Android - 取得系統時間 - Calendar

如果要取得目前的系統時間,可用
import java.util.Calendar;
               
long time=System.currentTimeMillis();
final Calendar mCalendar=Calendar.getInstance();
mCalendar.setTimeInMillis(time);
int mHour=mCalendar.get(Calendar.HOUR);                        //小時
int mMinuts=mCalendar.get(Calendar.MINUTE);                 //分
int mSecond = mCalendar.get(Calendar.SECOND);             //秒
int mMilliSecond = mCalendar.get(Calendar.MILLISECOND);  //千分之1秒

Android - 按鈕平均分配寬度 (rid of button padding)

如果想在畫面的「某一列」,放3個按鈕,而這3個按鈕要平均分配寬度,程式如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="
horizontal" android:layout_width="fill_parent"
    android:layout_height="fill_parent">

    <Button android:id="@+id/button1" android:layout_height="wrap_content" android:text="11111"
            android:layout_width="fill_parent" android:layout_weight="1"/>
           
    <Button android:id="@+id/button2" android:layout_height="wrap_content" android:text="2222222"
            android:layout_width="fill_parent" android:layout_weight="1"/>
           
    <Button android:id="@+id/button3" android:layout_height="wrap_content" android:text="3333333333"
            android:layout_width="fill_parent" android:layout_weight="1"/>

</LinearLayout>
如果不要按鈕之間有padding,有幾種作法

(1) 把padding設負值

(2) 給Button 設android:background="#000cab" 隨便給一個顏色,但是高度會變小,要自己設一下

(3) 自己做一張.9.png圖,給Button 設android:background="你的圖"

Android - 動畫TranslateAnimation 和 AnimationSet 的使用

下面範例是利用 TranslateAnimationAnimationSet 來製造ImageView上下移動的動畫。主要觀念為
(1)使用TranslateAnimation設定動畫時間、移動位置、重覆…等效果
(2)AnimationSet可以把多個動畫集合後,一次交付給某元件執行。本例就是下、上、下、上等四個動畫

程式如下:
public class Main extends Activity {
    ImageView img;
    AnimationSet animSet;
    TranslateAnimation anim;

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

        img = (ImageView) findViewById(R.id.img);
        img.setImageResource(R.drawable.icon);
        animSet = new AnimationSet(true);

        int[] step = {100,-100,100,-100 };
        for (int index = 0; index < 4; index++) {
            anim = new TranslateAnimation(0, 0, 0, step[index]);

            //動畫週期為0.5秒
            anim.setDuration(500);

            //避免元件又回到初始位置
            anim.setFillAfter(true);

            //因為這裡用了四個動畫,所以必須為每個動畫設置初始時間
            //0秒、0.5秒、1秒、1.5秒 為四個動畫初始執行的時間
            anim.setStartOffset(500 * index);

            // 把動畫集合起來
            animSet.addAnimation(anim);
        }

        // img元件開始執行動畫
        img.startAnimation(animSet);
    }
}

Android - Thread 和 Handler (1)

大家都知道
(1) 為了避免ANR,耗時的工作不要放在 UI Thread,要另外拉一條Thread或是用Service
(2) 在Thread工作後如果想要更改UI,要利用Handler來處理畫面
(3) 要注意,不要在Thread的run()裡面才new handler,應該在先前就要new好了。因為handler是為了綁定某個thread,為了處理畫面,應該讓handler綁定main thread。


Java - String split 拆解字串用法(含dot)

如果想要把字串,依據某字串拆解開來,可以使用split。程式如下:

String tempString="aaa:bbbb:ccc:ddddd";
String[] splitStringArray = tempString.split(":");

for (String splitString:splitStringArray){
    Log.d("gill", splitString);
}

則輸出
aaa
bbbb
ccc
ddddd

如果想要拆解的依據是特殊字元的話,則要在前面加上\ 
例如:
\b  \t  \n  \f  \r  \"  \'  \\

最後要注意的是,如果要拆解的依據是句點,也就是 "."
則要使用
\\.

Java - 字串 包含另一 字串 的檢查 indexof

如果要檢查某一字串是否有出現在某一字串,可以用string類別內的indexof,如果回傳值為-1,就表示沒有出現。如果不為-1,就表示有出現。舉例來說

string longString = "abcdefg";
string shortString = "cd";
string errorString="ck";

int havePosition = longString.indexof(shortString);
int noPosition = longString.indexof(errorString);

havePosition的值會等於2,即第1次出現的索引位置。
noPosition的值會等於-1,表示longString裡面不包含errorString這個字串。

Android - 取消Activity動畫

如果程式會連續開啟相同畫面的Activity,卻又不希望每次都有動畫出現,可以在onCreate()加上

getWindow().setWindowAnimations(0);


另外如果是用intent呼叫Activity時,也可以使用

Intent it = new Intent("com.gill.test.intentactivity");
it.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivity(it);

Eclipse hotkey (熱鍵、快捷鍵)

ctrl + z 回復上一次步驟
ctrl + y 重做下一次步驟

ctrl + / 註解 (取消註解)

ctrl + k 快速搜尋關鍵字 (如果先用ctrl + f 然後再找下一筆)
ctrl + f 搜尋關鍵字

ctrl + shift + o 自動匯入所欠缺的類別

ctrl + l 移至指定行數

ctrl + 滑鼠左鍵 跳至定義位置
alt + ← 跳到上一次游標所在位置
alt + → 跳到下一次游標所在位置
(這個功能常配合ctrl + 滑鼠左鍵,當查完定義後,要再回到原程式位置)

ctrl + shift + s 全部儲存

ctrl + shift + p 跳至匹配的括號
ctrl + q 跳至上一次編輯的位置

-----------------------------------------------------------------------------------------------------

ctrl + shift + L 顯示按鍵輔助

alt + / 程式碼輔助

alt + ↑ 上移 (快速的移動一行或一個段落)
alt + ↓ 下移 (快速的移動一行或一個段落)

ctrl + shift + k 快速搜尋關鍵字 (如果先用ctrl + f 然後再找上一筆)
ctrl + o 搜尋所有位於function名的關鍵字
ctrl + s 儲存

ctrl + D 單行刪除

ctrl + j 向下增量搜尋 (每打1個字母就開始向下搜尋)
ctrl + shift +j 向上增量搜尋 (每打1個字母就開始向上搜尋)

ctrl + shift + f 格式化程式碼

紅色字體為較少見,但是實用的hotkey

Android - 產生scroll bar讓使用者往下拉

我們可以把所有的元件放到ScrollView裡面,這樣就可以在畫面產生scroll bar讓使用者往下拉。但是,ScrollView裡面只可以作用於一個元件,最常見的做法就是,在ScrollView裡面放一個LinearLayout,再把所有的元件放在這個LinearLayout裡面就可以了。

xml範例如下:

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

   <ScrollView   
             android:layout_width="fill_parent"   
             android:layout_height="wrap_content" > 

   
    <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="fill_parent"
        android:layout_height="wrap_content"
        android:text="This is a TextView" />

    <CheckBox
        android:layout_height="wrap_content"
        android:layout_width="fill_parent"
        android:text="This is a CheckBox" />

    </LinearLayout>

  </ScrollView>

</LinearLayout>

Android - Layout Tool

想要簡單而且很快的繪製程式的畫面,可以使用一個叫的工具。無論是改變元件大小或是位置,都相單快速。就像VB等開發工具。可以從這裡下載程式。畫好畫面只要按下「Generate」就可以產生layout的 xml檔案。












Android - View常見的attribute

layout_marginTop = "10px" 本元件和上元件距離10px
layout_marginBottom = "10px" 本元件和下元件距離10px
layout_marginLeft = "10px" 本元件和左元件距離10px
layout_marginRight = "10px" 本元件和右元件距離10px

singleLine="true"  文字內容最多只有一行,常用於TextView元件

layout_above = "@id/xxxxx" 本元件放在xxxxx元件的上方
layout_below = "@id/xxxxx" 本元件放在xxxxx元件的下方
layout_toLeftOf = "@id/xxxxx" 本元件放在xxxxx元件的左方
layout_toRightOf = "@id/xxxxx" 本元件放在xxxxx元件的右方

layout_alignTop = "@id/xxxxx" 本元件頂部和xxxxx元件頂部切齊
layout_alignBottom = "@id/xxxxx" 本元件底部和xxxxx元件底部切齊
layout_alignLeft = "@id/xxxxx" 本元件左側和xxxxx元件左側切齊
layout_alignRight = "@id/xxxxx" 本元件右側和xxxxx元件右側切齊

layout_alignParentTop = "true" 本元件頂部和外層父元件頂部切齊
layout_alignParentBottom = "true" 本元件底部和外層父元件底部切齊
layout_alignParentLeft = "true" 本元件左側和外層父元件左側切齊
layout_alignParentRight = "true" 本元件右側和外層父元右側切齊

layout_centerHorizontal = "true" 本元件位於水平方向的中央
layout_centerVertical = "true" 本元件位於垂直方向的中央
layout_centerInParent = "true" 本元件位於外層父元件內,水平和垂直方向的中央

background="@drawable/background_pic" 指定background_pic為背景圖片
background="#000000" 指定背景為黑色

visibility="gone" 該元件不可視,而且不佔用面積

Android - Drawable 轉成 Bitmap

在Drawable資料夾中有一檔案為download.png,如果要轉成 Bitmap格式,可用程式:

    Bitmap bm = BitmapFactory.decodeResource(getResources(), R.drawable.download);

Android - IntentService使用

請先看有關IntentService官網介紹,另外這裡這裡也 有詳細說明原理。這裡用程式實作,並發現,一般的Service執行完後並沒有停止,在 Settings->Applications->Running services中可以發現依然存在,但是如果程式改用IntentService,則會發現執行程式後,在 Settings->Applications->Running services 是看不到的。

程式有2個檔案如下:
Main.java = 一個Activity的主畫面,按下畫面的按鈕可以呼叫Service
OriginalService.java = 繼承IntentService

Main.java 如下:
public class Main extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
       
        Button btn = (Button) findViewById(R.id.btn1);    
        OnClickListener lis = new OnClickListener() {       
            public void onClick(View v) {
                Intent it = new Intent();
                it.setClass(Main.this, OriginalService.class);
                startService(it);
            }
        };
        btn.setOnClickListener(lis);
       
    }
}


//OriginalService.java 如下:
public class OriginalService extends IntentService {

    //這個constructor一定要寫,不然在runtime的時候,會出現
    // java.lang.InstantiationException 的錯誤
    public OriginalService(){
        super("OriginalService");
    }
   
    public OriginalService(String name) {
        super(name);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d("gill", "onStartCommand, the hashcode = " + this.toString());
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        //這裡寫上任何想要在背景耗時的作業,比如網路下載,讀取資料庫,播放音樂音效…
    }

}

Android - database query (2) boolean 布林值查詢

在sqlite中,存入欄位的布林值,並不是用true或是false,而是轉成字串的"1"和"0"來代替。
假設有一table如下:

name    |    isBoy
-----------------------------
Gill              1
Mary           0
Tom            1
Jenny           0

所以在下查詢條件想要找出所有男生姓名時,可以如下程式:
        Cursor cursor = this.getContentResolver().query(myURI,
                     new String[] {"name"},
                     "isBoy=?",new String[] {"1"}, null);

其中的myURL依據你自已資料來源來決定其URI。布林值要用"1"或是"0"去查,如果用"true"或是"false"是查不到的。

Android - database query (1) 模糊查詢

大家應該都知道如何使用query查詢條件,例如此篇文章所述。如果需要進行模糊查詢,則是把

=?改成是 like ?


例如table如下:
studentName |  studentNumber |  studentScore

Gill                      1                                60
Tom                    2                                100
John                    3                                70
Mary                   4                                80
Gary                    5                                30

如果想要找出名字中間有ar的資料,並依分數由高往低排序,程式寫法如下:

String likeString = "%" + "ar" + "%";

cursor = getContentResolver().query(uri, new String[]{"studentName,studentNumber,studentScore"}, "studentName like?", new String[]{likeString}, "studentScore desc");

Android - 偵測某網址是否可以正常連線 (HttpURLConnection)

如果你想要測試某個網址是否可以正常的連線,你可以使用:

int count = 0;
while (count < 5) { 
    try {
        URL myUrl = new URL("http://www.google.com");
        HttpURLConnection myConnection =
                                         (HttpURLConnection) myUrl.openConnection();
        myConnection.setConnectTimeout(5000);
        int state = myConnection.getResponseCode();
        if (state == 200) {
            Log.d("gill","the address is available!");
        }
        break;
    } catch (Exception ex) {
        count = count + 1;
    }
    Log.d("gill","the address is unavailable!");
}

getResponseCode() will return:
200 : connect normally
404 : can't connect
-1 : can't detect the result

設定setConnectTimeout(5000)表示連線5秒還沒成功就放棄。

2011年12月28日 星期三

Android - 偵測螢幕方向 (ORIENTATION)

如果你想偵測目前手機的方向是直式還是橫式,程式如下:

        if (getResources().getConfiguration().orientation ==
            Configuration.ORIENTATION_PORTRAIT){
            //直式
        }
        if (getResources().getConfiguration().orientation ==
            Configuration.ORIENTATION_LANDSCAPE){
            //橫式
        }

Android - 取得螢幕大小(screen size)

在Activity底下,使用下列程式碼

Display display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
int width = display.getWidth();
int height = display.getHeight();

Android - Bitmap 轉 Drawable

要把Bitmap轉成Drawable可以接受的格式時,假設myBitmap為一Bitmap物件,程式如下:

Drawable myDrawable = new BitmapDrawable(myBitmap);

Android - ANR出現的時機

ANR詳細說明請參考官網,會出現ANR的2個時機是

(1)程式對使用者的輸入(按實體按鍵或是螢幕),超過5秒沒有反應

(2)BroadReceiver超過10秒還沒結束。

BroadReceiver 因為這個特性,所以不應該在收到Broadcast後做太多費時的工作。正確在收到Receiver後,service或是透過notification 去執行就要去執行某個activity....等其他相對應的工作。注意,不要直接執行activity而要透過notification,讓使用者自行 決定是否要去執行activity。否則activity忽然跑出來螢幕上會讓使用者原本操作的程式被中斷,這是不好的使用者操作經驗,要適情況避免!

避免ANR並給使用者好的操作方式 ,建議三點:
(1)呈現等待的畫面 (例如: progress bar)
(2)使用子線程
(3) 如果是在程式初始畫面即有大量運算或等待的情況時,要用asynchronously的方式來改變UI。舉例來說程式一開始執行時會透過網路下載30張小 圖片到畫面上時,可以每下載一張圖片完成,就先把這張圖片秀到畫面上,而不是30張都下載完了才一口氣全部秀到畫面上。

Android - Activity設計建議(2)

(6)如果你的activity會送資料給其它程式,應該設計一個選項讓user知道。利如Gallery會有Share的功能,按下後可以把資料 送給Facebook,Mail,Messages,Twitter....等。這個時候要特別注意建議(2)有可能發生的錯誤。當想要啟動一個 Activity時,如果會再次返回原Activity並得到一些資料,可以使用startActivityResult()。如果不需要在返回時取得資 料就可以用startActivity()


(7)你的Activity可以依特殊需求設計成Widget,而且Widget除了可以放在Home上面之外,也可以放在其它的application上,如果一來可以依需求不停的update內容。


(8) 如果你的Activity會被其它application執行,為了讓activity不會影響該application返回(按下return key)的行為,launch mode盡量使用standard 或 singleTop 而不要使用 singleTask or singleInstanc。Launch mode四者的差別請參考http://slashgill.blogspot.com/search/label/launchMode


(9) 當使用者由Activity A 按下notification到Activity B後,若按下 Back Key 時應該要能正確返回 Activity A。這時候為了避免Activity B按下Back Key會執行該application的其它activity(如果這些activity曾被執行而留在activity stack裡面的話),因此要把Activity B的taskAffinity設定空字串(不可以不設定)


(10)相較於上面的另一種作法是用在執行Activity B的時候,先設定FLAG_ACTIVITY_CLEAR_TOP和ACTIVITY_NEW_TASK,這樣到時候從Activity B按下Back Key時,就會回到Activity A


(11)盡可能不要改變Back Key的行為,才不會影響使用者原本的操作習慣。

Android - Activity設計建議(1)

有關設計建議的詳細內容請參考官網說明

(1)如果你的activity只想被自己設計的application使用,而不想被別的application使用,那就不要建立intent filter,直接使用explicit intents,例如:

   Intent it = new Intent();
   it.setClass(this,MyActivity.class);
   startActivity(it);

這樣就沒有別的applicaton會因為誤發了一個intent而把MyActivity給執行起來。使用intent filter會發佈到Android系統上,讓別的application也有機會執行。


(2)使用intent要特別注意,如果這個intent要呼叫的apk是第三方程式(意即可能被移除),那startActivity()很有可能會產生Exception。兩種解決方式

   (1) try{
           startActivity();
       }catch(ActivityNotFoundException e){
           //do something if the apk is uninstalled
       }

   (2) 利用package manager確定這個apk是否還在Android裝置裡面,做法可參考這篇文章



(3)如果你的Activity可以單獨執行,應該讓使用者在Launcher可以執行它。也就是application中應該有一個activity要有
            <intent-filter>
                <action android:name="android.intent.action.MAIN"></action>
                <category android:name="android.intent.category.DEFAULT"></category>
            </intent-filter>



(4)相較於上者,如果你有一個不常用的小程式,尤其是系統內也內建有類似功能的程式,或者這個小程式是依附在其他程式底下,那也許不用大費周章的為他在Launcher設立一個可以執行的icon。只要有intent filter可以在適當的時候被呼叫起來就好了。



(5) 相較於(3)和(4),如果你有兩支activity可獨立執行,但是有很多資料、程式碼是可以共用的,那可以考慮寫成一支application但是有 兩個可以在Launcher執行的icon,像是Camera 和 Camcorder。在AndroidManifest.xml中,這兩支activity都有

                  <action android:name="android.intent.action.MAIN" />
                  <category android:name="android.intent.category.DEFAULT" />

但是要注意的是要把taskAffinity分開,兩者才不會互相影響。例如
android:taskAffinity="android.task.camera" 和 android:taskAffinity="android.task.Camcorder",

有關taskAffinity可以參考官網的說明

Android - adb shell am broadcast指令

adb shell am broadcast 後面的參數有

[-a <ACTION>]
[-d <DATA_URI>]
[-t <MIME_TYPE>]
[-c <CATEGORY> [-c <CATEGORY>] ...]
[-e|--es <EXTRA_KEY> <EXTRA_STRING_VALUE> ...]
[--ez <EXTRA_KEY> <EXTRA_BOOLEAN_VALUE> ...]
[-e|--ei <EXTRA_KEY> <EXTRA_INT_VALUE> ...]
[-n <COMPONENT>]
[-f <FLAGS>] [<URI>] 

例如: adb shell am broadcast -a com.gill.test.broadcast --es TestString "this is test string" --ez TestBoolean true --ei TestInt 100

Android - Json解析 (Json String parser)

JSON為新一代的資料傳輸格式,這裡有一篇文章對XML和JSON進行了清楚的比較。下面的程式透過JSONObjectJSONArray展示出如何解析放在SD卡裡面的JSON資料,檔名取為json_file。
下面為json_file的內容
{
    "student num":"3",
    "student":
    [
      {"name":"Gill",
       "age":"15"},
      {"name":"Tom",
       "age":"14"},
      {"name":"John",
       "age":"17"}
    ]
}

讀取JSON的觀念是針對每個Key去讀取其Value的,可以想像{}裡面就是一個JSONObject。而因為JSON裡面的資料可以像陣列般包含多項內容,這時候就用getJSONArray一口氣全部抓出來。大致上的流程是

(1)建立字串:內容為JSON所有的資料

(2)利用上述字串建立JSONObject

(3)利用JSONObjec取得資料 : 像是利用getInt(),getString()....等

(4)如果要解析的資料是陣列式的 : 先利用getJSONArray取出JSONARRAY後,再配合for迴圈把每個細項也就是JSONObject建立起來出來。有了JSONObject要讀資料,就和步驟(3)一樣。


程式碼如下:
public class Main extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        startParser();
    }

    public void startParser() {
        try {
            String jsonString = readJsonFile();       
            JSONObject jsonResponse = new JSONObject(jsonString);             
     
            //直接由JSONObject取出資料
            Integer studentNum = jsonResponse.getInt("student num");
            Log.d("Test","The number of student is :" + studentNum);
           
            //處理陣列型式的資料要多一個步驟
            //就是先建立JSONArray,再搭配for迴圈建立JSONObject
            JSONArray jsonArray = jsonResponse.getJSONArray("student");
            for (int i = 0; i < jsonArray.length(); i++) {
                JSONObject jsonObject = jsonArray.getJSONObject(i);
                Log.d("Test","The name value is :" + jsonObject.getString("name"));
                Log.d("Test","The age value is :" + jsonObject.getString("age"));
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
   
    //這裡先將檔案json_file裡的內容讀出,再轉換成字串
    private String readJsonFile(){
        FileInputStream fin;
        String returnString = "";
        try {
            fin = new FileInputStream(new File("sdcard/tmp/json_file"));
            byte[] buffer = new byte[fin.available()];
            while (fin.read(buffer) != -1) ;
            returnString = new String(buffer);

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return returnString;
    }
}

執行結果如下:

Android - xml 解析 (XmlPullParser)

某些需要從網站下載xml資料,進行解析的程式。個人建議將解析的流程寫在一個Service裡面,如此一來才不會影響使用者當下操作的畫面。當解析完畢時,再將所需的資料丟給所需要的Activity或是DB或是進行處理。下面程式利用XmlPullParserFactoryXmlPullParser來對SD卡裡面的XML格式資料進行解析。採用XmlPullParser的好處是,如果網站下載的xml太大,而我們只需要前面一部份的資料時,就可以隨時停止解析。但是SAXParser則沒有辦法解析到一半就停止。
舉 例來說: 為了避免使用者等待太久,網頁的呈現一開始只有一部份資料,下方也許有一個Button叫「更多資料」。按下Button後,整個畫面會往下呈現全部資 料。像這種情形,就可以利用XmlPullParser在一開始先解析一部份的資料就好,以避免因解析大量XML而導致等待時間太久。


/**
* Start to parse the contents of xml file.
* The order of getEventType() is as follows,
* 1) XmlPullParser.START_DOCUMENT
* 2) XmlPullParser.START_TAG
* 3) XmlPullParser.TEXT
* 4) XmlPullParser.END_TAG
* 5) XmlPullParser.END_DOCUMENT
* The item2,itm3 and item 4 may be a loop
*/
public void startParser() {
   XmlPullParserFactory parserFactory;
   XmlPullParser pullParser;
   try {
       parserFactory = XmlPullParserFactory.newInstance();
       parserFactory.setNamespaceAware(true);
       pullParser = parserFactory.newPullParser();

       FileReader fileReader = new FileReader("sdcard/tmp/xml_file.xml");
       pullParser.setInput(fileReader);

       int eventType = pullParser.getEventType();

       int dataNum = 0 ;
       while (eventType != XmlPullParser.END_DOCUMENT) {

           if (eventType == XmlPullParser.START_TAG){
               Log.d("Test","the tag name is :" + pullParser.getName());
           }else if(eventType == XmlPullParser.TEXT ){
              Log.d("Test","the text value is :" + pullParser.getText());
              dataNum = dataNum + 1;    //讀到一筆資料了
           }
  
           if(dataNum <= 10){
               eventType = pullParser.next(); //往下一個Tag進行分析
           }else{
               break;    //讀到10筆資料後,就離開while迴圈
           }

       }
   } catch (XmlPullParserException e) {
       e.printStackTrace();
   } catch (IOException e) {
       e.printStackTrace();
   }
}

XmlPullParser詳細用法請參考原文:http://developer.android.com/reference/org/xmlpull/v1/XmlPullParser.html

Android - Theme 應用

Android提供了很多Theme可以應用,請看程式:
frameworks/base/core /res/res/values/themes.xml
有取消標題、狀態欄、背景為黑、背景為白…等一堆。

具體用方是在AndroidManifest.xml的activity裡加上
android:theme="@android:style/Theme.Translucent"

其 中一種叫就Theme.Translucent,官方說法為Default theme for translucent activities, that is windows that allow you to see through them to the windows behind.  This sets up the translucent flag and appropriate animations for your windows.

現在舉一種應用。假設Activity1 按下Button後來到 Activity2,而Activity2的畫面是屬於比較耗時的,例如ListView或是要載入圖檔。正常情況下在Activity1的畫面換成 Activity2前,會發現螢幕畫面會先是黑色(很快,也許不到1秒),才出現Activity2。如果把Activity2設為 Theme.Translucent就不會有這個現象,這樣給使用者的感覺會好一點。但其實這是一種假象,真實情況是(時間是假設的):

沒有設Theme.Translucent: 按下Button時:Activity1 待了1秒->  黑背景0.5秒 -> 出現Activity2
設Theme.Translucent: 按下Button時:Activity1 待了1秒->  透明背景0.5秒 -> 出現Activity2

但是有設Theme.Translucent的情況下,因為沒有黑背景,所以感覺上是由Activity1直接切換到Activity2。但其實使用者並沒有發現,其實按下Button後有停留在Activity1一下子。

另外如果使用Theme.Dialog.Alert之類而出現
Error: Resource is not public. (at 'theme' with value '@android:style/Theme.Dialog.Alert')的錯誤,因為在frameworks/base/core /res/res/values/public.xml找不到這個資料

只要加個*改成
  '@*android:style/Theme.Dialog.Alert'
就可以使用了。

Android - adb logcat 實用的招式

詳細的用法請看原文:
http://developer.android.com/guide/developing/tools/adb.html

其中有一種用法對於了解較為大型的程式流程非常有用,原文中的例子是:
adb logcat ActivityManager:I MyApp:D *:S 
 
依個人經驗,在開發程式的時候,建議在每一個method()的第一行都下一個Log.d(),方便日後trace code。而且交接給別人維護時,別人只要執行一下程式順便把log印出來,馬上就能了解程式流程和架構。

舉例你有5個.java檔,分別是
A.java
B.java
C.java
D.java
E.java

裡面你可以為每支java都設立各自的tag,如:
TAG_A
TAG_B
TAG_C
TAG_D
TAG_E

在每一個程式的每一個method()都寫上Log.d("TAG_A",method name()); 其中TAG_A依不同程式TAG替換掉,而method_name()則依不同的method name替換掉。而adb logcat改下:

adb logcat TAG_A:D TAG_B:D TAG_C:D TAG_D:D TAG_E:D *:S >> log_file

或是

adb logcat | grep "TAG_"

如此一來,log_file裡面就不會有一大堆有的沒的其它不相關程式的log,而只會單純的顯示A~E.java這五支程式的log。又因為每支程式的method()第一行都有log.d(),所以就會變成光看log_file 就大致了解這個程式的流程了。

了解這種用法並加以變化應用,對學習較複雜的大型程式,非常有用!


另外,為了知道log發生的時間點,可以利用-v Time這個參數,比如:
adb logcat -v Time,則log會類似如下:
01-11 03:21:55.164 D/NotificationService(  237): mIntentReceiver() Intent.ACTION_BATTERY_CHANGED
01-11 03:21:55.164 D/NotificationService(  237): batteryCharging=true, level=100, status=5, health=2
01-11 03:21:55.164 D/PowerUI (  313): onReceive , action=android.intent.action.BATTERY_CHANGED

Eclipse - Questoid SQLite Browser

如果利用Eclpse有關Sqlite的程式,建議可以安裝一個視覺化的工具Questoid SQLite Browser,有助於開發上的效率。步驟如下:

(1) 先到Oracle 下載1.6.x版的jre,執行bin檔會得到一個資料夾,改名為jre後,放到eclipse的目錄底下
(2)下載Questoid SQLite Browser,並將該jar放到eclipse底下的plugins資料夾內。
(3) 重開eclipse
(4) 利用File Explorer找到該資料庫,然後使用sqlite browser開啟,如下圖:

(5)可清楚看到table資料

Ubuntu如何執行bin檔

In command line,
chmod a+x XXX.bin

then
./XXX.bin

Android - Downloads.java和DownloadProvider的應用

想要讓自己的apk達到從網路下載東西(預設放到SD 卡的download底下)的功能,可以利用DownloadProvider。總共有3個主要步驟

(1)DownloadProvider必需是system權限才能執行的程式,所以我們必需讓自己的apk的USER是system。
(2)了解Download東西其實是透過改變data/data/com.android.providers.downloads/databases裡面的downloads.db裡面的downloads這一個table,來達成的。
(3)AndroidManifest.xml給予適當的權限。

接下來實作測試程式碼
public class Main extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
      
        ContentValues values = new ContentValues();
        String url = "http://lh4.ggpht.com/_cCOjVNtItLY/TLTC9OVLHeI/AAAAAAAAIOw/7PP-Ih9vOY4/s640/P1030438.JPG";
        values.put("uri", url);
        values.put("mimetype", "image/jpeg");
       
        ContentResolver mResolver = getContentResolver();
        mResolver.insert(Uri.parse("content://downloads/download"), values);  
    }
}

針對"content://downloads/download"所對應到的table寫入一筆準備download的資料,就會觸發系統下載的動作。其中必填欄位最少要對uri和mimetype給值。整個Android到底對應那些mimetype可以參考
frameworks/base/core/java/android/webkit/MimeTypeMap.java

而想知道table到底有那些欄位,可以利用adb shell進入後
# cd data/data/com.android.providers.downloads/databases
# sqlite3 downloads.db
SQLite version 3.6.22
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite>.headers on
sqlite> select * from downloads;
_id|uri|method|entity|no_integrity|hint|otaupdate|_data|mimetype|destination|no_system|visibility|control|status|numfailed|lastmod|notificationpackage|notificationclass|notificationextras|cookiedata|useragent|referer|total_bytes|current_bytes|etag|uid|otheruid|title|description|restriction|auth_header_name|auth_header_value|scanned
所有的欄位就出現了。這麼多欄位,有那些是比較重要的(比較需要填寫的),可以參考/packages/providers/DownloadProvider/Helpers.java


在AndroidManifest.xml中,需要給予的權限有
    <uses-permission android:name="android.permission.ACCESS_DOWNLOAD_MANAGER" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />


完成上述之後就等於完成了第2和第3項的條件。最後一項是要設定apk的USER是system。
這裡有兩個步驟
(1)在AndroidManifest.xml設定
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.test.testdownload"
      android:versionCode="1"
      android:versionName="1.0"
      android:sharedUserId="android.uid.system" >

(2)另外,這支程式要在source code底下利用mm的方式產生出來,所以在Android.mk中要加上
LOCAL_CERTIFICATE := platform

最後mm出來的apk放到手機上執行,就會發現程式download東西了。

Android - 播放notification sound

如果要在程式中播放notification 預設的音效,程式如下:

public class Main extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        Uri mySoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
        RingtoneManager.getRingtone(this, mySoundUri ).play();
    }
}

Android - adb shell呈現系統目前資訊 (dumpsys)

不裝任何apk也想知道目前的電池狀況嗎?
直接下指令就可以了

adb shell dumpsys battery

就會秀出:

Current Battery Service state:
  AC powered: false
  USB powered: true
  status: 2
  health: 2
  present: true
  level: 26
  scale: 100
  voltage:3747
  temperature: 318
  technology: Li-ion


其它有關adb shell dumpsys 後面的參數還有很多用法,總共如下:
  SurfaceFlinger
  accessibility
  account
  activity
  alarm
  appwidget
  audio
  backup
  battery
  batteryinfo
  bluetooth
  bluetooth_a2dp
  bluetooth_dg_service
  bluetooth_fm_receiver_service
  clipboard
  connectivity
  content
  cpuinfo
  device_policy
  devicestoragemonitor
  diskstats
  dropbox
  entropy
  hardware
  htc_checkin
  htchardware
  input_method
  iphonesubinfo
  isms
  location
  media.audio_flinger
  media.audio_policy
  media.camera
  media.player
  meminfo
  mount
  netstat
  network_management
  notification
  package
  permission
  phone
  power
  search
  sensor
  simphonebook
  statusbar
  telephony.registry
  throttle
  uimode
  usagestats
  usbnet
  userbehavior
  vibrator
  wallpaper
  wifi
  window


如果什麼參數都不加,直接打adb shell dumpsys,就會出現全部的資訊了。

gcc4.4降級gcc4.3

有的時候用make編譯source code時,會出現一些warning甚至是錯誤,告知gcc版本4.4可能修改過某些東西,所以make終止。如果要把gcc4.4降至gcc4.3,通常可以解決這些問題,步驟如下:

apt-get install gcc-4.3 g++-4.3

cd /usr/bin

sudo rm gcc
sudo rm g++

sudo ln -s gcc-4.3 gcc
sudo ln -s g++-4.3 g++

gcc --version
確定是不是gcc4.3版本後,重新make

Android - Intent呼叫第三方程式 (setComponent)

只要利用adb logcat ,再搭配使用setComponet(),就可以輕易的呼叫第三方程式(不在自己的application內)。
詳細用法參考原文:
http://developer.android.com/reference/android/content/Intent.html#setComponent%28android.content.ComponentName%29

比如我自己的程式想執行Android裡面的Settings,先用adb logcat看系統是如何呼叫Settings的

I/ActivityManager(   60): Starting activity: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.android.settings/.Settings }
I/ActivityManager(   60): Displayed activity com.android.settings/.Settings: 1205 ms (total 1205 ms)

只要有這個cmp就可以呼叫Settings了

public class Main extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Intent i = new Intent();
        ComponentName comp = new ComponentName("com.android.settings", "com.android.settings.Settings");
        i.setComponent(comp);
        startActivity(i);   
    }
}

另外有些程式要被執行,Intent還要多加搭配Intent.setData()或者是Intent.setAction()等方式。比如:想要開啟Browser,而且是開啟tw.yahoo.com的網頁,程式碼如下:
        Intent i = new Intent();
        ComponentName comp = new ComponentName("com.android.browser", "com.android.browser.BrowserActivity");
        i.setComponent(comp);
        Uri uri = Uri.parse("http://tw.yahoo.com");
        i.setData(uri);
        startActivity(i);

Android - 解析度 - resolution dpi分別

在Android1.5(含)之前drawable資料夾只有一個,現在分成3個

(1) drawable-ldpi,裡面建議存放低解析的圖片,如QVGA (240x320)

(2) drawable-mdpi,裡面建議存放中等解析的圖片,如HVGA (320x480)
 
(3) drawable-hdpi,裡面建議存放高等解析的圖片如WVGA (480x800),FWVGA (480x854)

(4) drawable-xdpi,裡面存放更大的尺寸(for tablet)

目前市面上典型的分類是
  • QVGA (240x320, low density, small screen)
  • WQVGA400 (240x400, low density, normal screen)
  • WQVGA432 (240x432, low density, normal screen)
  • HVGA (320x480, medium density, normal screen)
  • WVGA800 (480x800, high density, normal screen)
  • WVGA854 (480x854 high density, normal screen)
  • WXGA720 (1280x720, extra-high density, normal screen)
  • WSVGA (1024x600, medium density, large screen)
  • WXGA (1280x800, medium density, xlarge screen)



系統會根據不同Device的解析度到這幾個資料夾裡面去找合適的圖片。


下圖是官網公佈的資訊:
http://developer.android.com/guide/practices/screens_support.html


Table 3. Various screen configurations available from emulator skins in the Android SDK (indicated in bold) and other representative resolutions.


Low density (120), ldpi Medium density (160), mdpi High density (240), hdpi Extra high density (320), xhdpi
Small screen QVGA (240x320)

480x640

Normal screen WQVGA400 (240x400) WQVGA432 (240x432) HVGA (320x480) WVGA800 (480x800) WVGA854 (480x854)
600x1024
640x960
Large screen WVGA800** (480x800) WVGA854** (480x854) WVGA800* (480x800) WVGA854* (480x854)
600x1024




Extra Large screen 1024x600 WXGA (1280x800)
1024x768
1280x768
1536x1152
1920x1152
1920x1200
2048x1536
2560x1536
2560x1600


Android - Lifecycle (1) - activity

請看官網的說明
http://developer.android.com/reference/android/app/Activity.html

簡單的程式測式碼如下:
public class TestRotate extends Activity {

    public void onCreate(Bundle savedInstanceState) {
        Log.d("TestRotate","create");
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
   
    public void onStart(){
        Log.d("TestRotate","onStart");
        super.onStart();
    }

    public void onResume(){
        Log.d("TestRotate","onResume");
        super.onResume();
    }

    public void onRestart(){
        Log.d("TestRotate","onRestart");
        super.onRestart();
    }
   
   
    public void onPause(){
        Log.d("TestRotate","onPause");
        super.onPause();
    }

    public void onStop(){
        Log.d("TestRotate","onStop");
        super.onStop();
    }

    public void onDestroy(){
        Log.d("TestRotate","onDestroy");
        super.onDestroy();
    }

}


來看看一個Activity在硬體按鈕操作下的流程

程式第一次執行:
onCreate
onStart
onResume

按下home key:
onPause
onStop

再次程式執行:
onRestart
onStart
onResume

按下return Key:
onPause
onStop
onDestroy

接下來看看啟動Auto-rotate screen時,旋轉了畫面的流程:
onPause
onStop
onDestroy
onCreate
onStart
onResume

請注意,正常情況下如果旋轉畫面,其實是Destroy這個Activity後,再重啟一個Activity。換言之,有可能會讓原本的資料流失,如果要避免這個現象,可以在AndroidManifest.xml中,修改activity的tag如下:

        <activity android:name=".Test"
                  android:label="@string/app_name"
                  android:configChanges="orientation|keyboardHidden"
                  >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

加上 android:configChanges="orientation",則程式不會重新經歷onPause到onResume的動作。而後面的keyboardHidden則是當使用者開啟手機的實體鍵盤時,不會重新經歷onPause到onResume的動作。

Android - ContentProvider - Sqlite應用 (1)

對於ContentProvider的原理,請看原文:
http://developer.android.com/guide/topics/providers/content-providers.html

網 路上也有很多人解釋ContentProvider,這裡不多說。我的實作測試程式和網路上不同,從Android Source Code可以發現/packages/providers底下有很多個資料夾。這些大部份都不會有Acitvity,因為這類型的apk單純就只是一個介 面給其它的apk呼叫用的。所以如果實作程式將Content Provider和含Activity合成同一支apk,就比較感覺不到效果。因此測試含有2支apk

(1)MyDataBaseProvider.apk:內含2個java檔
     1. MyContentProvider.java : 繼承ContentProvider,需要實作6個function
         query()
         insert()
         update()
         delete()
         getType()
         onCreate()  
     2. MyOpenHelper.java : 繼承 SQLiteOpenHelper,這是為了方便產生DB用途

(2)UseProvider.apk :裡面有一個Main.java。這支apk和MyDataBaseProvider分開獨立的。利用系統的ContentResolver找出適合的ContentProvider溝通(本範例也就是存取DB資料)

理論上,應該是先有ContentProvider介面設計好之後,未來有需要的apk就都可以透過它去存取資料。

MyContentProvider.java的程式碼:
public class MyContentProvider extends ContentProvider {
    private MyOpenHelper myOpenHelper;
    private static final String[] MYCOLUMN = new String[]{ "mycolumn"};
   
    @Override
    public boolean onCreate() {
        myOpenHelper = new MyOpenHelper(getContext());
        return true;
    }
    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder) {
        SQLiteDatabase myDatabase = myOpenHelper.getWritableDatabase();
        Cursor c = myDatabase.query(uri.getLastPathSegment(), MYCOLUMN, null, null, null, null, null);    
        return c;
    }
    @Override
    public String getType(Uri uri) {
        return null;
    }
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        return null;
    }
    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        return 0;
    }
    @Override
    public int update(Uri uri, ContentValues values, String selection,
            String[] selectionArgs) {
        return 0;
    }

}

其中的getLastPathSegment目的是取出uri最後一個字段,有可能是Table的名稱也有可能是單筆資料的row id。因此可以確保異動的資料對象到底是整張Table還是某一筆資料。

MyOpenHelper.java的程式碼:
public class MyOpenHelper extends SQLiteOpenHelper {

    // 定義DB相關資訊
    private static final String DATABASE_NAME = "MyDatabase";
    private static final int DATABASE_VERSION = 1;
    private static final String TABLE_NAME = "MyTable";

    public MyOpenHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        // 這一個onCreate是一個callback function
        // 之後程式呼叫到SQLiteOpenHelper裡的getWritableDatabase()時,
        // 會檢查有無DB,沒有就先create "MyDatabase"的DB
        // 然後再檢查是否要callback這個onCreate()而產生一個"MyTable"的table,並塞入一筆資料
        db.execSQL("CREATE TABLE " + TABLE_NAME
                + " (id integer primary key autoincrement, mycolumn text);");
        String mySql = "insert into " + TABLE_NAME
                + " (mycolumn) values('gill');";
        db.execSQL(mySql);
    }
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // 如果DB版本更新時需要進行什麼動作,就寫在這裡
    }
}

AndroidManifest.xml如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.test.mydatabaseprovider"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <provider android:name="MyContentProvider"
            android:authorities="com.gill.test.authorities">
        </provider>   
    </application>
</manifest>

這邊的authorities值隨便你取,其它apk要透過這個authorities和要存取的table name組出一個URI,才可以存取DB內該table資料。

public class Main extends Activity {
    // AUTHORITY要看該Provider的AndroidManifest.xml中的定義
    public static final String AUTHORITY = "com.gill.test.authorities";
    // URI = AUTHORITY再加table name
    public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY
            + "/MyTable");
    private static final String[] MYCOLUMN = new String[] { "mycolumn" };

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

        // 透過getContentResolver由系統依據uri,決定要執行那一個ContentProvider
        // 所以這裡不用直接呼叫MyDataBaseProvider也可以取得資料
        Cursor myCursor = getContentResolver().query(CONTENT_URI, MYCOLUMN, null,
                null, null);
       
        myCursor.moveToFirst(); //一定要寫,不然會出錯
        TextView txt = (TextView) findViewById(R.id.txt);
        txt.setText(myCursor.getString(0));
    }
}

安裝這兩支apk後,執行UseMyProvider,畫面如下:

gill是來自於DB




















如果想知道DB位置和裡面的table及資料,可以利用adb shell進入後,
# cd data/data/com.test.mydatabaseprovider/databases
# ls    
MyDatabase
# sqlite3 MyDatabase
SQLite version 3.6.22
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .table
MyTable           android_metadata
sqlite> .header on
sqlite> select * from MyTable;
id|mycolumn
1|gill

就可以看到欄位名稱和裡面的值了。