2012年3月31日 星期六

[TQC+ Android] 3-2 接收SMS Use BroadcastReceiver, SmsMessage, onNewIntent







設計接收SMS的程式, 程式一執行時顯示 "Waiting SMS", 在接收到由DDMS傳送的簡訊之後, 即會將儲存於本機內的圖片顯示於畫面上。現在來說明一下整個完成這項功能的處理流程, 首先進入Main Activity等待SMS 的訊息, 而我們已經在AndroidManifest.xml 靜態的註冊了一個BroacastReceiver 名稱為 SMSreceiver 它的功用是在處理System接收SMS的Action 發生
  
<action android:name="android.provider.Telephony.SMS_RECEIVED"/>


而SMSreceiver收到了receive SMS action之後就會啟動Image activity, 並把簡訊內容和電話號碼的資訊傳給它, 而根據題目的要求, 當Image被啟動的時候使用的Image圖片是交替顯示的, 並且請使用者預先在res/drawable 資料夾底下分別載入sdk.png 和 android_118.png , 以下是程式碼的部分。


package COM.TQC.GDD03;

import android.app.Activity;
import android.os.Bundle;


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


這個Activity很簡單, 就是一個mian Activity, 他的layout檔只放了一個TextView所以就不特別附上 ,下面要貼的是Image Activity的layout , 也相當的簡單。







接著是SMSreceiver class 它是一個BroadcastReceiver, 功能就是當有SMS傳進來時, 從簡訊中取得來電號碼和簡訊內容將它們放入 intent並啟動Image.class, 先來看看程式碼。
package COM.TQC.GDD03;


import java.util.Set;
import android.content.BroadcastReceiver; 
import android.content.Context; 
import android.content.Intent; 
import android.os.Bundle;
import android.telephony.SmsMessage;
import android.util.Log;


public class SMSreceiver extends BroadcastReceiver
{
 @Override 
   public void onReceive(Context context, Intent intent) 
   {       
    SmsMessage[] smsMessage = null;
      
      //先判斷這個intent是不是簡訊傳入的action
   if(intent.getAction().equals("android.provider.Telephony.SMS_RECEIVED"))
      {       
    Bundle bundle = intent.getExtras();
    
    //檢查一下返回的Bundle裡面有哪些的資訊, 檢查Key
    Set KeySet = bundle.keySet();
       
       for(String s : KeySet)
       {
          Log.d("We have key elements", "Key:"+s);
       }
              
    Object messages[] = (Object[]) bundle.get("pdus");
    smsMessage = new SmsMessage[messages.length];
    for (int n = 0; n < messages.length; n++)
    {
     smsMessage[n] = SmsMessage.createFromPdu((byte[]) messages[n]);
        //將原始的PDU格式的資料轉為smsMessage的格式, 由於傳入的簡訊由於長度的限制可能不止一封
    }
      }      
   
   Intent NewIntent = new Intent();
   NewIntent.setClass(context, Image.class);
   //指定目的地Activity
   
   NewIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
   //如果是由非Activity來啟動另一個Activity, 需要加入這個flag
   
   NewIntent.putExtra("Phone", smsMessage[0].getOriginatingAddress());
   //取出來電號碼, 因為輸入的訊息長度不會拆成多封簡訊所以這邊我只取第一封, 或許可以再做更嚴謹的判斷
   
   NewIntent.putExtra("Message", smsMessage[0].getDisplayMessageBody());
   //取出簡訊內容
   
   context.startActivity(NewIntent);
     
   } 
}





再來是被SMSreceiver 呼叫的 Image.java
package COM.TQC.GDD03;

import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import android.widget.ImageView;
import android.widget.TextView;


public class Image extends Activity 
{    
 Intent intent;
 int count;
 TextView tv;
 ImageView iv;
 
 /** Called when the activity is first created. */
 
 @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.image);
        
        intent = this.getIntent();
        handleIntent(intent);
        //將intent 送去handleIntent將newIntent中的資訊取出顯示
    }
 
 @Override
 protected void onNewIntent(Intent newIntent)
 {
  // TODO Auto-generated method stub
    
  super.onNewIntent(newIntent);    
  Log.d("Test","onNewIntent()");
  //如果Activity已經存在的話, 系統會將intent發送給 onNewIntent(), 而不是再去 call onCreate()
  
  setIntent(newIntent);
  //告知將換成newIntent
  
     handleIntent(newIntent);
     //將newIntent 送去handleIntent將newIntent中的資訊取出顯示
 }
 
 private void handleIntent(Intent intent)
 {
  tv = (TextView) this.findViewById(R.id.TextView1);
  iv = (ImageView) this.findViewById(R.id.imageView1);
  
  Bundle bundle = intent.getExtras();  
  SharedPreferences sPreferences = this.getSharedPreferences("SMSCount", MODE_PRIVATE);
  
  tv.setText("電話號碼:"+ bundle.getString("Phone") +", SMS內容:"+bundle.getString("Message"));
  count = sPreferences.getInt("Count", 0);
  Log.d("Test","Count:"+count);  
  
  if(count%2==0) iv.setImageResource(R.drawable.android_118);
  else iv.setImageResource(R.drawable.sdk);
  count++;
  //處理圖片交替顯示的部分
  
  sPreferences.edit().putInt("Count", count).commit();
  
 }
 
/*  另一個不要重複產生Instance的方法是在切換到另一個Activity之前
           在onStop()就call finish() destroy Instance
      
 @Override
 protected void onStop()
 {
  // TODO Auto-generated method stub    
    
  super.onStop();
  this.finish();
 } 
*/
}


當BroadcastReceiver每收到一個SMS的訊息, 它就會startActivity一次(也就是create Instance), 而這樣一來當我們沒有按下Back鍵回到main Activity, 而又收到SMS的話, 在Activity stack中就會產生多個 Image Activity, 這種情況顯然不是我們想見到的, 為了處理這種情況, 有簡單的兩種方法, 


1. 在onStop()的時候finish() 當前的Activity, 因為onStop() method 不管在我們按下Back鍵, 或是被強行跳出到新的Image Activity之前都會被執行的method。


2. 在AndoridManifest.xml中宣告Image Activity的lauchMode為singleTop, 因為singleTop的屬性規則是如果此Activity已經產生在Top of Activity stack的話, system就不會再替他create instance, 實際上只是不去call onCreate() method, 取而代之的是call onNewIntent() method, 所以我們要在程式中override onNewIntent() method, 把設定圖片來源的code 在這邊處理。




最後是AndroidManifest.xml的部分

    
    
        
            
                
                
            
        
                    
        
        
            
                
            
        
    
    
 


這邊要注意的地方就是先前提到的記得幫 Image Activity宣告lauchMode:singleTop, 另外還要幫SMSreceiver 在intent-filter中宣告 <action android:name="android.provider.Telephony.SMS_RECEIVED"/> , 最後別忘記因為我們會讀取到簡訊的內容, 所以記得別忘了user-permission: <uses-permission android:name="android.permission.RECEIVE_SMS"/>









P.S. 題目中所要求的Variable和Method皆會保留 , 也會根據題目所要求的流程去實作 , 縱使題目要求繞遠路....



2012年3月28日 星期三

[Android] 很簡單的使用搜尋功能 Use Global Search





提到使用Search Interface 去搜尋我們要的資料, 可搜尋的範圍是根據我們程式提供多少資料來源, 例如我們去查詢的資料來源為手機中的通訊錄, 簡訊夾, 或是一些應用程式, 大部分的資料來源都是從ContentProvider取得的, 而根據使用者的需要甚至可以擴大到當做翻譯字典甚至網路搜尋引擎, 不過包含的內容越多程式就越複雜, 那Android有提供方法讓我們不用在我們的App中撰寫複雜的程式碼卻也可以達到同樣搜尋的功效嗎?

很開心的是 Android的確有提供這樣的辦法協助開發人員輕鬆的使用這些搜尋功能, 它就是Global Search, 接下來看看一個簡單的使用範例。

package COM.TQC.GDD03;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;


public class GDD03 extends Activity
{  
  /** Called when the activity is first created. */
  @Override
  public void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);    
    
    setDefaultKeyMode(DEFAULT_KEYS_SEARCH_GLOBAL);
    //在鍵盤上打字時會自動的啟動Search UI, 並且預設為Global Search 
   
  }
  
  @Override
  public boolean onSearchRequested()
  {
 // TODO Auto-generated method stub  
     
  Log.d("Test","onSearchRequested()"); 
     
     startSearch("請輸入聯絡人姓名", false, null, true);
     /* 第四個參數代表是否啟用Global Search 功能, 在這個範例我們選擇 true
      * 如果要使用自定義的Local Search的話就代入 false
      */
  return true;  
  }
}


另外使用Global Search的功能是不用像Local Search那樣還需要額外的定義一個  searchable.xml , 也不用在AndroidManifest.xml中去額外的幫Activity定義 Search Action 和 searchable 的resource, 但是我們仍然能使用SEARCH BUTTON呼叫 Search UI, 也能夠讓 
onSearchRequested() Method 起作用, 這都是因為Global Search 的關係, 另外有一個值得一提的地方是使用鍵盤輸入會喚起Search UI, 但是從Log訊息中可以發現這個動作是不會去call  onSearchRequested() method 的(無論是Global Search or Local Search), 所以就算拿掉 override onSearchRequested() method的部分, 仍然可以靠 setDefaultKeyMode(DEFAULT_KEYS_SEARCH_GLOBAL)呼叫 Search UI 成功的完成Global search的動作。









2012年3月21日 星期三

[TQC+ Android] 3-1 通訊錄搜尋 Use SearchDialog, Searchable, SearchManager





設計通訊錄搜尋程式 , 具有搜尋管理員(SearchManager)的程式 , 在程式一開始執行即彈出搜尋管理員 , 當使用者輸入聯絡人姓名關鍵字 , 則將搜尋的結果顯示於TextView中  (請使用者預先在通訊錄下新增聯絡人)。


要在我們的App中加入搜尋功能的話 , 為了達到這個目的我們可以使用 The search dialog and widget. 這兩個是Android 所提供的 search User Interface, 而 SearchDialog被限制出現在 Top of Activity, 而 SearchWidget 可以被自由的擺放在Activity中。


而不管你使用的是 SearchDialog or SearchWidget, 當他們被使用者執行時, 系統會產生一個Intent並且儲存使用者所下達的Query, 並且把他們送達給一個真正在處理Search動作的Activity , 因為 SearchDialog  / SearchWidget 只是UI, 他們只提供一個給使用者操作的界面和傳遞資料, 不負責真正的Search動作, 此外真正在處理Search的 Activity也必須由我們自己實作, 而要完成在App中加入搜尋功能需要以下幾個部份:



  • 一個 Searchable 設定是一個xml檔案, 裡面的內容包含這個Search UI 將要提供的功能例如語音搜尋或是搜尋   建議 , 提示文字等。
  • 一個接收使用者Search Query的Activity, 也是真正在處理Searching 工作的Actitvity
  • SearchDialog 預設是隱藏的, 要提供使用者喚起SearchDialog的機制 , 例如預設的SearchButton, MenuItem , 或是任何你所提示的按鈕。


接下來會搭配著程式碼解說如何產生以上所提到的部分:


首先是 Searchable Configuration > res/xml/searchable.xml

 


請記得一定要擺在 res/xml下, 另外我們可以看到searchable elements
android:label  > 這是唯一被系統要求一定要定義的欄位 ,他是一個Application Name, 一般來說這個值在當我們為Quick Search Box開啟搜尋建議功能時 , 可以在系統的設定清單中的Searchable Items 裡找到它。
android:hint   >  這是系統建議我們設定的欄位 , 當Search UI裡還沒被輸入任何查詢字串時 , 他會顯示並且提示使用者該輸入的查詢字串。


關於Searchable的欄位還有很多 , 詳細的可以參考官方資料 <searchable>


接著再附上 strings.xml


  通訊錄搜尋
  Search
  Search contact list....




接下來的流程是這樣的 : 使用者送出Query > Receiving the query > 真正的Searching 工作 > 搜尋的結果呈現。


這四個步驟我們使用兩個Activity來完成 , 分別是一個 Query Activity , 它的工作就是喚出Search UI 給使用者輸入相關的Query資訊 , 並且將Query資訊送達到 Searchable Activity。 


Searchable Activity的工作就是 , Receiving the query 並且進行 Searching task , 然後把結果呈現出來 , 所以這麼看來Searchable Activity 是真正的主角。


首先先來看看Query Activity的程式碼 (根據這題的要求, 這個範例我們會使用的是 SearchDialog) : 


package COM.TQC.GDD03;

import android.app.Activity;
import android.os.Bundle;


public class GDD03 extends Activity
{  
     @Override
  protected void onCreate(Bundle savedInstanceState)
     {
   // TODO Auto-generated method stub
   
      super.onCreate(savedInstanceState);
   setContentView(R.layout.main);
   
   setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
   //當使用鍵盤輸入時會自動喚起Search UI, 將輸入的字鍵入, 並且預設搜尋手機裡的資源和所提供的資訊
   
   
   onSearchRequested();
   
   //一般來說如果我們目的只是要喚起Search UI的話
   //那直接進行 onSearchRequested(); 的呼叫即可
   //如果我們需要添加一些能協助搜尋的資訊時
   //就Override 這個 Function
       
  }
     
     @Override
     public boolean onSearchRequested()
     {
        // TODO Auto-generated method stub
      
         Bundle bundle = new Bundle();         
          
         //建立一個Bundle 攜帶想附加傳遞的資訊
         //有甚麼要提供的額外搜尋資訊都可以加入其中
         
         bundle.putString("Data", "搜尋結果");     
         
         startSearch("Search contact list", true, bundle, false);
         /* 這個Function 也可以達到呼叫 Search UI的功能
          * public void startSearch (String initialQuery, boolean selectInitialQuery, Bundle appSearchData, boolean globalSearch)
          * 
          * initialQuery : 預先顯示在Search UI的字串, 這樣就等於Search hint 起不了作用了
          * selectInitialQuery : If true, the intialQuery will be preselected, which means that any further typing will replace it. This is useful for cases where an entire pre-formed query is being inserted. If false, the selection point will be placed at the end of the inserted query. This is useful when the inserted query is text that the user entered, and the user would expect to be able to keep typing. This parameter is only meaningful if initialQuery is a non-empty string.
      appSearchData : 如果想加強搜尋結果的品質, 可以將一些想提供的額外資訊裝入Bundle, Bundle會 insert to Search intent. Null if no extra data is required.
      globalSearch : If false, this will only launch the search that has been specifically defined by the application (which is usually defined as a local search). If no default search is defined in the current application or activity, global search will be launched. If true, this will always launch a platform-global (e.g. web-based) search instead.
                 很怕翻譯錯語意 , 所以以原文呈現
          */         
         return true;  
     }
}


這個Activity 的任務較為簡單, 主要就是喚起 Search UI供使用者輸入 Query字串就好, 註解裡應該交代得蠻詳細了, 值得一提的是為了要讓 SEARCH button 能叫出Search UI, 也就是要讓onSearchRequested()起作用的話, 要事先在manifest.xml作宣告, 至於如何宣告在Searchable Activity 講完時會一起交待.


package COM.TQC.GDD03;


import android.app.Activity;
import android.app.SearchManager;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.widget.TextView;
import android.widget.Toast;


public class SearchActivity extends Activity
{ 
   private String queryAction;
   private TextView mTextView01;
   
   /** Called when the activity is first created. */
   @Override
   public void onCreate(Bundle savedInstanceState)
   {
     super.onCreate(savedInstanceState);
     setContentView(R.layout.main);
     
     mTextView01 = (TextView)this.findViewById(R.id.myTextView1);
     
     Intent intent = getIntent();
     queryAction = intent.getAction();
     
     if (Intent.ACTION_SEARCH.equals(queryAction)) // check 是否為 Search Action
     {
       String query = intent.getStringExtra(SearchManager.QUERY);
       //從intent中接收query字串
       
       query = query.trim();
       //刪掉空白字元, 怕使用者不經意連空白字元一起送出
       
       Toast t = Toast.makeText(this, "", Toast.LENGTH_SHORT);
       Bundle bundle = intent.getBundleExtra(SearchManager.APP_DATA);
       if(bundle != null)
       {
        t.setText("搜尋的字串為:"+ query +" 附加的資訊為:" + bundle.getString("data"));
       }
       else t.setText("搜尋的字串為:"+ query + " 沒有附加的資訊");
       t.show();
       
       findContact(query);
       // 將query字串丟到 findContact() function, 這個 function 作用就是在處理Searching      
     }  
     
   }

   private void findContact(String query)
   {
  // TODO Auto-generated method stub
    
    String selection = ContactsContract.Data.MIMETYPE
          + "='"
          + ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE
          + "'"
          + " AND "
          + ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME
          + " LIKE " + "'%" + query + "%'";
    
    Cursor cursor = getContentResolver().query(ContactsContract.Data.CONTENT_URI,
            new String[] {ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME} , selection , null , null);
    /*
     * 因為我們所要查詢手機中通訊錄的資料, 所以使用ContentResolver class 來存取Content model
     * 我們可以將 ContactsContract.Data.CONTENT_URI 這個參數視為想要存取的Table, 你也可以使用 null 他就會回傳所有的column 不過這樣比較沒有效率
     * new String[] {ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME} 為要回傳的Column set
     * selection 這個先前定義好的字串相當於SQL語法中的 WHERE
     * 
     * public final Cursor query (Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
     * 
     * uri : The URI, using the content:// scheme, for the content to retrieve.
    projection : A list of which columns to return. Passing null will return all columns, which is inefficient.
    selection A filter declaring which rows to return, formatted as an SQL WHERE clause (excluding the WHERE itself). Passing null will return all rows for the given URI.
    selectionArgs You may include ?s in selection, which will be replaced by the values from selectionArgs, in the order that they appear in the selection. The values will be bound as Strings.
    sortOrder How to order the rows, formatted as an SQL ORDER BY clause (excluding the ORDER BY itself). Passing null will use the default sort order, which may be unordered.
     * 
     */    
        
    int ColumnIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME);
    //取得我們想要的 Name 欄位在cursor中的column index
    
    StringBuffer name = new StringBuffer();
    
    if(cursor.getCount()>0)
    {      
     while(cursor.moveToNext())
     {      
      name.append(cursor.getString(ColumnIndex)+"\n");
     }
    }
    else name.append("無資料");
   
    mTextView01.setText(name.toString());   
   }   
}



首先要提到的就是當我們收到Search Intent之後如何從Intent中挖取我們要的資料, SearchManager, 有提到該使用哪些Data Key從Intent中獲得資料, 在成功取得搜尋結果後可以搭配ListView來顯示結果, 可以使用SimpleCusorAdapter, 不過這不是本題的重點, 想知道如何使用可以往前爬文, 或是在網誌搜尋關鍵字, 再來就是由於我們會存取到手機中的通訊錄資訊, 這部分還需要在Manifest.xml中宣告permission的權限.


現在我們要開始介紹Manifest的部分了, 這相當的重要

    
    
        
             
                
                          
             
            
         
        
             
              
               
                
       
    
    
 
從這個AndroidManifest.xml看得出來我們定義了兩個Activity, 一個是GDD03 也就是之前說的 Query Activity 這是我們用來喚起Search UI 並且把Query字串傳送給 SearchActivity 也就是之前所說一個真正在處裡搜尋的Searchable Activity.


GDD03 Activity 裡的meta-data中android:value 代表的要使用的Searchable Actvity名稱, 在這裡為我們的另一支程式 SearchActivity, 而 android:name="android.app.default_searchable"是固定的,  也因為這個宣告我們才有辦法在 GDD03 中才能使用 onSearchRequested()啟動Search UI, SEARCH button才會起作用。


而另一支Activity的宣告部分, 記得宣告它的Action為 <action android:name="android.intent.action.SEARCH"/> , 讓SearchActivity可以接受Search Intent, meta-data中的android:resource="@xml/searchable", 指定為我們放在 res/xml/searchable.xml 檔案, 也是設定Search configuration的來源, 另外 android:name="android.app.searchable" 這個屬性也是固定的. 最後記得宣告permission這樣我們才能存取手機中通訊錄的資料, <uses-permission android:name="android.permission.READ_CONTACTS"/>




值得一提的是如果有多個Activity都想要共用同一個SearchableActivity的話, 可以將 <meta-data android:name="android.app.default_searchable"                   android:value=".SearchActivity"/> , 插入到 Application 中 , 這麼一來Application中的Activity會自動得繼承meta-data元素 , 就像這樣

    
    
        
        
             
                
                          
                        
         
        
             
              
               
                
       
    
    
 




接下來要補充的是, 從接收Query字串到呈現搜尋結果為止我們使用了兩個Activity來完成, 如果只使用一個Activity來包辦所有工作呢? 也是可行的只是要注意的地方比較多, 我們接著探討這個作法.

首先我們會遇到一個問題, 當我們送出Query字串也就是會發送Search intent, 在之前兩個Activity的作法中, 這個Search intent會送到被指定的Searchable Activity, 這時候Searchable Activity收到相對應的intent 就會Create一個instance, 然後它會在Satck的最上層, 當我們看到搜尋的結果就會按Back鍵回到Query Activity繼續查詢動作, 這樣的流程似乎頗為合理, 但是當我們使用一個Activity包辦他們的工作時, 情況就不一樣了。

這個Activity (instance A)還是會發送出Search intent, 但是所指定的Searchable Activity就是自己本身, 當他又收到一個intent的時候, 會Create一個instance B 並且顯示搜尋結果, 造成instance隨著查詢次數增加, 其實在之前Activity的作法也會發生一樣的情況, 只要在Searchable Activity再進行搜尋動作, 他一樣會create an instance, 只是因為我們都會back到Query Activity 在進行查詢動作才把這個會產生許多instance的情況給避免了。

之所以會這樣是因為Activity launchMode 預設為 standard, 這個屬性是Multi-Instance也就是接收到Activity時他就會Create an instance, 讓每一個instance 處理一個 intent, 所以我們將launchMode的屬性設為別的值 android:launchMode="singleTop" 來解決這個問題, SingleTop 也許也會 Create an instance, 但是如果欲前往的Activity的instance已經存在stack的頂層時, 該instance將不會create a new instance, 取而代之的是它會去call onNewIntent() 來收下這個new intent, singleTop的規則正好符合我們的需求, 再接收到Search intent 時我們不必再去產生新的instance, 我們只要從這個Search intent中挖取query字串接下來一切的操作就像上一個作法一樣, 讓我們先來看看 AndroidManifest.xml的宣告。

    
        
                            
                
                
                
            
                         
                
    
    
    
 

除了新增  android:launchMode="singleTop" 的屬性之外, <intent-filter> 中也要記得加入 <action android:name="android.intent.action.SEARCH"/>, 此外順序是有差別的, 至於為什麼呢? 這我還沒找到相關的資料, 找到之後我會再補上 XDD, <meta-data> 裡的屬性也只留下定義作為Searchable Activity即可, 因為根據預設它已經有了enable onSearchRequested() 的功能了。
               
但是只要設定singleTop屬性是不夠的, 之前在說明提到過Activity會去call onNewIntent() 去接收這個new intent, 所以我們必須在程式碼中去override 這個 method, 以下是程式碼的部分。


package COM.TQC.GDD03;

import android.app.Activity;
import android.app.SearchManager;
import android.app.SearchManager.OnDismissListener;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.util.Log;
import android.widget.TextView;
import android.widget.Toast;


public class GDD03 extends Activity
{
  private String queryAction;
  private TextView mTextView01;
  
  SearchManager SManager;
  
  /** Called when the activity is first created. */
  @Override
  public void onCreate(Bundle savedInstanceState)
  {
 Log.d("Test","onCreate()");
 //onCreate() 只會被執行一次
   
 super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    
    SManager = (SearchManager) getSystemService(SEARCH_SERVICE);
    SManager.setOnDismissListener(new OnDismissListener()
    {
  @Override
  public void onDismiss()
  {
   // TODO Auto-generated method stub
   
   Log.d("Test","onDismiss()");
   continuteWork();
  }   
    });
    
    mTextView01 = (TextView)this.findViewById(R.id.myTextView1);
    
    setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
    
    onSearchRequested();    
  }

  

  @Override
  protected void onNewIntent(Intent newIntent)
  {
 // TODO Auto-generated method stub
   
 super.onNewIntent(newIntent);    
 Log.d("Test","onNewIntent()");
 //如果Activity已經存在的話, 系統會將intent發送給 onNewIntent(), 而不是再去 call onCreate()
 
 setIntent(newIntent);
 //告知將換成newIntent
 
    handleIntent(newIntent);
    //將newIntent 送去check是不是 search intent, 是的話就進行searching動作
  }

   private void handleIntent(Intent intent)
   {
  // TODO Auto-generated method stub
  
    queryAction = intent.getAction();   
     
     if(Intent.ACTION_SEARCH.equals(queryAction)) // check 是否為 Search Action
     {
       String query = intent.getStringExtra(SearchManager.QUERY);
       //從intent中接收query字串
       
       query = query.trim();
       //刪掉空白字元, 怕使用者不經意連空白字元一起送出
       
       Toast t = Toast.makeText(this, "", Toast.LENGTH_SHORT);
       Bundle bundle = intent.getBundleExtra(SearchManager.APP_DATA);
       if(bundle != null)
       {
        t.setText("搜尋的字串為:"+ query +" 附加的資訊為:" + bundle.getString("data"));
       }
       else t.setText("搜尋的字串為:"+ query + " 沒有附加的資訊");
       t.show();
       
       findContact(query);
       // 將query字串丟到 findContact() function, 這個 function 作用就是在處理Searching      
     } 
   }
   
   private void continuteWork()
   {
   // TODO Auto-generated method stub
      
      //使剛剛暫停的工作繼續執行
         Log.d("Test","continuteWork()");
   }
   
   private void pauseWork()
   {
   // TODO Auto-generated method stub
      
      //使暫停正在執行的工作 , 等候使用者輸入查詢字串
         Log.d("Test","pauseWork()");
   }

   @Override
   public boolean onSearchRequested()
   {
     // TODO Auto-generated method stub
    
   pauseWork();
   
      Bundle bundle = new Bundle();
      bundle.putString("Data", "搜尋結果");     
      
      startSearch("Search contact list", true, bundle, false);
     
      return true;  
   }
  
   private void findContact(String query)
   {
   // TODO Auto-generated method stub
   
   String selection = ContactsContract.Data.MIMETYPE
      + "='"
      + ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE
      + "'"
      + " AND "
      + ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME
      + " LIKE " + "'%" + query + "%'";
   
   Cursor cursor = getContentResolver().query(ContactsContract.Data.CONTENT_URI,
           new String[] {ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME} , selection , null , null);
    
   int ColumnIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME);
     
   StringBuffer name = new StringBuffer();
   
   if(cursor.getCount()>0)
   {      
    while(cursor.moveToNext())
    {      
     name.append(cursor.getString(ColumnIndex)+"\n");
    }
   }
   else name.append("無資料");
  
   mTextView01.setText(name.toString());   
   }
   
   @Override
   protected void onPause()
   {
  // TODO Auto-generated method stub
  Log.d("Test","onPause()");
  super.onPause();
   }

   @Override
   protected void onResume()
   {
  // TODO Auto-generated method stub
  Log.d("Test","onResume()");
  super.onResume();
   }
}



為了讓大家瞭解整個流程的LifeCycle 所以使用了Log來觀察整個流程, 從Log中可以看到, onCreate()只會被執行一次, 之後就會去call onNewIntent(Intent newIntent), 這全是因為 android:launchMode="singleTop" 的原因, 所以不會再去Create an instance了, 接著在onNewIntent() method中去判斷收到的intent 是不是我們要的Search intent 一切就大功告成了。


除此之外可以看到在onSearchRequseted()中加入了pauseWork() method, 這麼作的目的是因為當喚起了Search UI 之後, 就會自動的focus在 Search UI的輸入並且程式lifecycle 不會去call onPause(), 所以我們可能需要自己手動的暫停程式, 等到我們輸入搜尋字串或是自己取消Search UI之後再手動讓程式繼續執行, 接著我們藉由 Class SearchManager.OnDismissListener 的幫助來monitor Search的狀態, 因為當搜尋取消或是送Query字串後, SearchManager.OnDismissListener 是一定會被called, 所以我們將continuteWork() method 放到裡面等待執行










另外根據Android Developers的建議, 如果你開發的版本是在Android 3.0+ 的話, 建議是使用Search Widget , 這個部分有機會再發一篇文章介紹。 




P.S. 題目中所要求的Variable和Method皆會保留 , 也會根據題目所要求的流程去實作 , 縱使題目要求繞遠路....




相關文章 :  [Android] 很簡單的使用搜尋功能 Use Global Search









2012年3月10日 星期六

[TQC+ Android] 2-10 簡單記事 Use SimpleCursorAdapter , ListView , SQLiteDatabase





設計一個簡單記事本 , 按下Add時將EditText裡的值寫到資料庫中(MY_DB) , 新增成功後將值顯示在下方的 ListView , 點選到ListView任一筆的資料時將值帶入EditText並可Update或是Delete , 程式碼如下。




package COM.TQC.GDD02;
import android.app.Activity;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.CheckedTextView;
import android.widget.EditText;
import android.widget.ListView;


public class GDD02 extends Activity
{
 private static final String DBNAME = "MY_DB";
 private static final String TABLENAME = "MY_TABLE";
 private static final String FIELD01_NAME = "_id";
 private static final String FIELD02_NAME = "_text1";
 private SQLiteDatabase dataBase;
 private android.database.Cursor cursor;
 private int _id = -1;
 private EditText EditText01;
 private Button Button01;
 private Button Button02;
 private Button Button03;
 private ListView ListView01;
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        EditText01 = (EditText) findViewById(R.id.EditText01);
        Button01 = (Button) findViewById(R.id.BtnAdd);
        Button02 = (Button) findViewById(R.id.BtnUpdate);
        Button03 = (Button) findViewById(R.id.BtnDelete);     
        ListView01 = (ListView) findViewById(R.id.ListView01); 
        
        Button02.setEnabled(false);
        Button03.setEnabled(false);
        
        String CREATE_SQL = "create table if not exists "+TABLENAME+" ("+FIELD01_NAME+" INTEGER PRIMARY KEY, "+FIELD02_NAME+" TEXT)";
        dataBase = this.openOrCreateDatabase(DBNAME , MODE_PRIVATE , null);
        dataBase.execSQL(CREATE_SQL);
     updateAdapter();
     //Update ListView
        
        Button01.setOnClickListener(new View.OnClickListener()
        {
   @Override
   public void onClick(View arg0)
   {
    // TODO Auto-generated method stub
    if(!EditText01.getText().equals(""))
    {
     insert(EditText01.getText().toString());
    }
  }});
        
        Button02.setOnClickListener(new View.OnClickListener()
        {
   @Override
   public void onClick(View arg0)
   {
    // TODO Auto-generated method stub
    if(_id!=-1 && !EditText01.getText().equals(""))
    {
     update(_id, EditText01.getText().toString());
     Button02.setEnabled(false);
     Button03.setEnabled(false);
    }
    
  }});
        
        Button03.setOnClickListener(new View.OnClickListener()
        {
   @Override
   public void onClick(View arg0)
   {
    // TODO Auto-generated method stub
    if(_id!=-1)
    {
     delete(_id);
     Button02.setEnabled(false);
     Button03.setEnabled(false);
    }    
   }});
        
        ListView01.setOnItemClickListener(new android.widget.AdapterView.OnItemClickListener()
        {
   @Override
   public void onItemClick(android.widget.AdapterView arg0,
     View arg1, int arg2, long arg3)
   {
    // TODO Auto-generated method stub
    
    _id = (int) arg3;
    //arg3 is Primary Key, not arg2
    EditText01.setText(((CheckedTextView)arg1.findViewById(R.id.CheckedTextView01)).getText());
    Button02.setEnabled(true);
          Button03.setEnabled(true);
          Log.d("Test", "The position of the view in the adapter arg2:"+arg2);
          Log.d("Test", "The row id of the item that was clicked arg3:"+arg3);
   }});
    }
    
    private void insert(String text)
    {
     dataBase.execSQL("INSERT INTO "+TABLENAME+" ("+FIELD02_NAME+") "+"values ('"+text+"')");        
     updateAdapter();
     EditText01.setText("");
    }
    
    private void update(int id,String text)
    {
     dataBase.execSQL("UPDATE "+TABLENAME+" SET "+FIELD02_NAME+"='"+text+"' WHERE "+FIELD01_NAME+"="+id);        
     updateAdapter();
     EditText01.setText("");        
    }
    
    private void delete(int id)
    {
     dataBase.execSQL("DELETE FROM "+TABLENAME+" WHERE "+FIELD01_NAME+"="+id);        
     updateAdapter(); 
     Log.d("Test", "id:"+id);
     if(cursor.getCount()==0) _id=-1;
    }
    
    private void updateAdapter()
    {
     cursor = dataBase.rawQuery("SELECT * FROM "+TABLENAME, null);
     
     android.widget.SimpleCursorAdapter adapter = new android.widget.SimpleCursorAdapter(this, R.layout.list,
          cursor, new String[]
                { FIELD02_NAME }, new int[]
                { R.id.CheckedTextView01 });
     //R.layout.list 代表每一個Row view 的xml定義檔
     ListView01.setAdapter(adapter);
    }

 @Override
 protected void onDestroy()
 {
  // TODO Auto-generated method stub
  cursor.close();
  dataBase.close();
  super.onDestroy();  
 }    
}





接下來是main.xml


   
  
  
 
 
  
  
  
 
 
 





還有ListView中Row View的定義檔 list.xml


  
  








P.S. 題目中所要求的Variable和Method皆會保留 , 也會根據題目所要求的流程去實作 , 縱使題目要求繞遠路....


[TQC+ Android] 2-9 姓名清單 Use ArrayAdapter , Spinner , SQLiteDatabase





將string.xml裡的陣列資料寫入資料庫 , 再從資料庫裡依字母排序出來 , 並放在Spinner裡 ,  下面是程式碼。





package COM.TQC.GDD02;


import android.app.Activity;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.Spinner;

public class GDD02 extends Activity
{
 private static final String DBNAME = "MY_DB";
 private static final String TABLENAME = "MY_TABLE";
 private static final String FIELD01_NAME = "_id";
 private static final String FIELD02_NAME = "_text1";
 private SQLiteDatabase dataBase;
 private Spinner Spinner01;
 private String[] strNames , orderNames;
 private Cursor cursor;
 //資料筆數
 private int recordCount;
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
     super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        Spinner01 = (Spinner) findViewById(R.id.Spinner01); 
        Resources resources = getResources(); 
        strNames = resources.getStringArray(R.array.strNames);
               
        String CREATE_SQL = "create table if not exists "+TABLENAME+" ("+FIELD01_NAME+" INTEGER PRIMARY KEY, "+FIELD02_NAME+" TEXT)";
        dataBase = openOrCreateDatabase(DBNAME, MODE_PRIVATE, null);
        dataBase.execSQL(CREATE_SQL);
     
     for(int i=0; i0)
     {
      cursor.moveToFirst();
      orderNames = new String[strNames.length];
      for(int i=0; i < strNames.length ; i++)
      {
       orderNames[i] = cursor.getString(1);
       cursor.moveToNext();
      }
     }
     
     ArrayAdapter adapter = new ArrayAdapter(this,  
                android.R.layout.simple_spinner_item, orderNames);  
        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);  
        Spinner01.setAdapter(adapter);      
        Spinner01.setSelection(0);
        dataBase.execSQL("DROP TABLE "+TABLENAME);
        dataBase.close();
    }
}



main.xml
 
 





資料來源 , values/strings.xml
    
    姓名清單
 
  Ella
  Body
  David
  Andy
  Cindy
  GiGi
  Fancy
 





P.S. 題目中所要求的Variable和Method皆會保留 , 也會根據題目所要求的流程去實作 , 縱使題目要求繞遠路....



[Music] L.O.V.E. Cover by JCee 網路素人歌手





這次又要推薦一個網路的素人歌手 , 記得上次推薦的是阿福 , [Music] A-Fu (鄧福如) - B.o.B feat. Bruno Mars - Nothing on you , 最近無意間在看新聞時聽到了JCee的歌聲 , 一個神祕的網路歌手 , 他翻唱了幾首歌曲但是全程都用麥克風遮住了臉龐 , 重點事她的歌聲真的非常的清爽好聽 , 真的要分享一下她翻唱的L.O.V.E , 這首Frank Sinatra的西洋老歌~~~




L is for the way you look at me
O is for the only one I see
V is very, very extraordinary
E is even more than anyone that you adore can


Love is all that I can give to you
Love is more than just a game for two
Two in love can make it
Take my heart and please don't break it
Love was made for me and you


L is for the way you look at me
O is for the only one I see
V is very, very extraordinary
E is even more than anyone that you adore can


Love is all that I can give to you
Love is more than just a game for two
Two in love can make it
Take my heart and please don't break it
Love was made for me and you
Love was made for me and you
Love was made for me and you 






當然在這邊也要分享一下原唱Frank Sinatra的版本囉~


2012年3月3日 星期六

[TQC+ Android] 2-8 我是誰? Use SharedPreferences





第一次開啟程式時姓名 , 電話與住址是空的 , 再輸入姓名電話及住址後離開 , 接下來進入程式時會顯示所輸入的值 , 程式碼如下。




package COM.TQC.GDD02;

import android.app.Activity;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;

public class GDD02 extends Activity
{
 public static final String PREF_NAME = "GDD02_PREF";
 public static final String key01 = "key01";
 public static final String key02 = "key02";
 public static final String key03 = "key03";
 
 private EditText name;
 private EditText phone;
 private EditText address;
 private Button end;
 
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        name = (EditText) this.findViewById(R.id.name);
        phone = (EditText) this.findViewById(R.id.phone); 
        address = (EditText) this.findViewById(R.id.address); 
        
        end = (Button) this.findViewById(R.id.end); 
        end.setOnClickListener(new Button.OnClickListener()
        {
   @Override
   public void onClick(View v)
   {
    // TODO Auto-generated method stub
    finish();
   }         
        });        
    }
    
    @Override
 protected void onResume()
    {
  // TODO Auto-generated method stub
  super.onResume(); 
  
  SharedPreferences sf = getSharedPreferences(PREF_NAME, 0);
  name.setText(sf.getString(key01, ""));
  phone.setText(sf.getString(key02, ""));
  address.setText(sf.getString(key03, ""));
 }

 @Override
 protected void onStop()
 {
  // TODO Auto-generated method stub
  super.onStop();
  
  SharedPreferences sf = getSharedPreferences(PREF_NAME,0);  
  SharedPreferences.Editor edit = sf.edit();
  edit.putString(key01, name.getText().toString());
  edit.putString(key02, phone.getText().toString());
  edit.putString(key03, address.getText().toString());
  edit.commit();  
 } 
}


Next, layout.xml



    
    
        
    
    
    
    
    
    





P.S. 題目中所要求的Variable和Method皆會保留 , 也會根據題目所要求的流程去實作 , 縱使題目要求繞遠路....



Google Analytics