יום שלישי, ינואר 11

חיבור לאינטרנט חלק 1: Apache HttpClient

זה הפוסט הראשון בסדרה שאני מתכנן בנושא קישוריות לקוח (client) לאינטרנט, וצריכת שרותים (Internet Services).  נתחיל היום ביצירת תקשורת HTTP ונתרגל פעולת Get פשוטה - נביא עמוד HTML ונשפוך אותו למסך כטקסט רגיל.
 פרוטוקול ה-HTTP הוא הבסיס לכל. אין צורך לרדת מתחתיו: אין צורך לממש פתיחת  sockets או לדאוג ליצירת חיבורים באופן עצמאי. ה-HTTP כבר דואג להכל.  
לא אכנס  להסבר מפורט על פרוטוקול ה-HTTP עצמו (מומלץ לקרוא, יש המון חומר ברשת), רק אזכיר שהוא הפרוטוקול המרכזי באינטרנט, ורב השרותים רצים מעליו. הוא אינו תלוי פלטפורמה, כך שהוא מאפשר למערכות מסוגים שונים התומכות ב-HTTP להתחבר.
ה-sdk של אנדרואיד כולל תמיכה ב-API של Apache, ה-HttpClient, וגם ב-java.net API. אנחנו נשתמש ב-Apache. הוא פשוט  יותר למימוש תוכניות יציבות ואמינות מה-java.net API u ואכן google ממליצים להשתמש בו.
כשהתחלתי לכתוב את הפוסט הזה, קפצתי לביקור  באתר הרשמי של Apache, באיזור של ה-Http.  מתואר שם תהליך יצירת קישוריות ה-Http עם ה-HttpClient, והוא כולל שישה שלבים:

  1. צור instance של HttpClient.
  2. צור instance של אחת המתודות - כלומר Get או Post וכדומה.
  3. הפעל את המתודה execute.
  4. קרא את ה-response - התשובה שמתקבלת מהצד השני.
  5. סגור את החיבור
  6. טפל בתשובה שהתקבלה.
שימו את השלבים האלה בצד לעת עתה, (או כמו שאומרים באיטליה - Let's park it here), ונמשיך הלאה. נחזור אליהם בהמשך.
ניגש לראות את הדוגמא.

מה הדוגמא עושה
  •  הצגת עמוד הבית של Apache ע"י פעולת Get
    • מדפיסה את דף ה-HTML מאתר הבית של Apache, כמות שהוא, בפורמט טקסט, לתוך TextView עם ScrollView.
    • מבצעת את הבאת הדף הנ"ל  מתוך Thread נפרד, והתשובה שמתקבלת נשלחת ל-Handler שרץ ב-UI.
  • הצגת אותו עמוד עם WebView browser:   נביא את אותו דף אינטרנט גם  תוך שימוש ב-WebView. ה-WebView  הוא view (או ווידג'ט) שמציג דפי אינטרנט כמו browser. זו הזדמנות להציג אותו כאן!
  • יצירת Tabs: נפריד את מסכי ה-Text וה-WebView ע"י יצירת שני טאבים.
  •  כפתור הפעלה בתפריט: כפתור של OptionsMenu מפעיל את הגישה לאינטרנט.
ועוד שאלה קטנה לפני שנמשיך: למה צריך בכלל API, ואי אפשר להסתפק ב-Browser ו- WebView.
שאלה מעניינת. לא אענה עליה כעת. תוכלו למצוא תשובות לשאלה זו  (בן השאר) בפרזנטצית וידאו  די מעניינת של גוגל על שרותי אינטרנט ו- REST.

צילומי המסכים
מצ"ב צילום של שני המסכים, כלומר שני ה-Tabs:

תמונת מסך הטקסט שהתקבל עם פעולת ה-Get

תמונת ה-WebView


ניגש לקוד

שלושה קבצים יעניינו אותנו:
  1. ה-Manifest.xml שם צריך להוסיף license לגישה לאינטרנט.
  2. ה-layout.xml -  נראה את ה-WebView ווידגט וסידור ה-Tabs.
  3. וקובץ ה-java כמובן.

הקובץ הראשון: ה-Manifest.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.ahs.androheb.httpexample"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".HttpExample"
                  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-sdk android:minSdkVersion="8" />
   <uses-permission android:name="android.permission.INTERNET" /> 
</manifest> 


שום דבר מיוחד, מלבד השורה הצהובה המכילה את הרישיון שצריך לשתול כדי לאפשר גישה לאינטרנט.

הקובץ השני - main.xml קובץ ה-Layout.
  1.  <TabHost xmlns:android="http://schemas.android.com/apk/res/android"
  2.      android:id="@+id/tabhost"
  3.      android:layout_width="fill_parent"
  4.      android:layout_height="fill_parent">
  5.      <LinearLayout
  6.          android:orientation="vertical"
  7.          android:layout_width="fill_parent"
  8.          android:layout_height="fill_parent">
  9.          <TabWidget android:id="@android:id/tabs"
  10.              android:layout_width="fill_parent"
  11.              android:layout_height="wrap_content"
  12.          />
  13.          <FrameLayout android:id="@android:id/tabcontent"
  14.              android:layout_width="fill_parent"
  15.              android:layout_height="fill_parent">
  16. <ScrollView
  17. android:layout_width="fill_parent"
  18.     android:layout_height="wrap_content">
  19. <TextView
  20. android:id="@+id/text_view_output"  
  21.     android:layout_width="wrap_content" 
  22.     android:layout_height="wrap_content" 
  23.     android:text="Output is not ready yet"
  24.     />
  25.  </ScrollView>   
  26.     <WebView android:id="@+id/webkit"
  27. android:layout_width="wrap_content"
  28. android:layout_height="wrap_content"
  29. />

  30.          </FrameLayout>
  31.      </LinearLayout>
  32.  </TabHost>


התיחסות לקובץ ה-layout:

  • TabHost(שורה 1) - זהו ה-container שמכיל את כל המרכיבים שבונים את ה-Tabs
    •  את הכפתורים - TabWidget
    •  ואת תוכנם - בתוך ה-FrameLayout.
 הוא עוטף את כל ה-layout. ושני הרכיבים הבאים הם הבנים שלו.
  • TabWidget (שורה 9) - זהו ה-container שמכיל את פס כפתורי הטאב.
  • FrameLayout (שורה 13) - ה-container שמכיל את התוכן של הטאבים. 
הסבר נוסף על נושא ה-Tab נמצא בפוסט מיוחד לנושא - מיצאו אותו.
כעת אפשר להכניס ווידגטים ל-Tabs. נכניס שני ווידג'טים לשני Tabs נפרדים:
  • שורות 16-25 קשורות ל-TextView לשם נישפוך את הדף שיתקבל.  ה-container שעוטף אותו הוא מסוג Scrollview שיאפשר גילגול.
  • שורות 26-29 - הכנסת ה-WebView.

ומכאן למנה העיקרית - HttpExample.java:





package com.ahs.androheb.httpexample;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;

import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.webkit.WebView;
import android.widget.TabHost;
import android.widget.TextView;

public class HttpExample extends Activity {
TextView textView;
WebView browser;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
     textView= (TextView)findViewById(R.id.text_view_output);
   browser=(WebView)findViewById(R.id.webkit);
   TabHost tabs=(TabHost)findViewById(R.id.tabhost);
   tabs.setup();
   TabHost.TabSpec spec;
   spec=tabs.newTabSpec("tag1");
   spec.setContent(R.id.text_view_output);
   spec.setIndicator("Plain Text");
   tabs.addTab(spec);
   spec=tabs.newTabSpec("tag2");
   spec.setContent(R.id.webkit);
   spec.setIndicator("webkit");
   tabs.addTab(spec);
    }
    Handler handler = new Handler(){
         public void handleMessage(Message msg) {
            String recString = msg.getData().getString("myPage");
           textView.setText(recString); 
         }
    }; 
    
    public void myHttpGet(){ 
     new Thread() {
    public void run() {
     String TAG = "run";
       String url = "http://www.apache.org/";
         browser.loadUrl(url);
          Looper.prepare();
   try {
       HttpClient client = new DefaultHttpClient();
       HttpGet request = new HttpGet();
       request.setURI(new URI(url));
       HttpResponse response = client.execute(request);
       responseProcess(response);
   }catch(URISyntaxException e){
      Log.e(TAG,"myHttpGetHttpGet URISyntaxException");
      alertDialogSet("URISyntaxException  connecting to" + url);
   }catch(IOException e){
      Log.e(TAG,"myHttpGetHttpGet IOException");
        alertDialogSet("IOException Problem connecting to" + url);
   }catch(IllegalStateException e){
      Log.e(TAG,"myHttpGetHttpGet IllegalStateException");
      alertDialogSet("myHttpGetHttpGet IllegalStateException" + url);
   }
   Looper.loop();
   Looper.myLooper().quit();
   }
  }.start();
   }
    private void responseProcess(HttpResponse response){
Handler mHandler = handler;
BufferedReader inBuff = null;
final String TAG = "responseProcess";
try{
 inBuff = new BufferedReader (new InputStreamReader(response.getEntity().getContent()));
   StringBuffer stringBuf = new StringBuffer("");
   String line = null;
   String NewLine = System.getProperty("line.separator");
   while ((line = inBuff.readLine()) != null) {
       stringBuf.append(line + NewLine);
   }
   inBuff.close();
   String page = stringBuf.toString();
   Message msg = mHandler.obtainMessage();
   Bundle bundle = new Bundle();
   bundle.putString("myPage", page);
   msg.setData(bundle);
   mHandler.sendMessage(msg);
}catch(IOException e){
    Log.e(TAG," IOException");
  alertDialogSet("responseProcess IOException");
}
finally {
if (inBuff != null) {
try {
inBuff.close();
        }catch (IOException e) {
          Log.e(TAG,"IOException closing buffer");
        }
    }
}
    }
    private void alertDialogSet(String msg){
   AlertDialog.Builder builder = new AlertDialog.Builder(this);
   builder.setMessage(msg);
   builder.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
        public void onClick(DialogInterface dialog, int id) {
        }
     });
 AlertDialog alert = builder.create();
 alert.show();
    }

   static final int start = Menu.FIRST; 
    @Override 
    public boolean onCreateOptionsMenu(Menu menu) {
     menu.add(Menu.NONE, start, Menu.NONE, "Start");
     return(super.onCreateOptionsMenu(menu));
    }
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        super.onOptionsItemSelected(item);
        switch (item.getItemId()) {
            case start:
             myHttpGet();
            return(true);
        }
        return(false);
    }

}

בתוך ה-HttpExample class ישנן 7 מתודות:


  1. onCreate - יוצרת את ה-view ואת ה-tabs.
  2. ה-handleMessage בתוך ה-Handler class - מעבירה לתצוגה ב-TextView את הדף שהתקבל  ע"י ה-Thread ונשלח ל-Handler.
  3. myHttpGet - מטפלת בהבאת הדף עבור ה-WebView ובפעולת ה- Http Get. רצה ב-Thread. מטפלת בתשובה שהגיעה. מבצעת סידור מינימליסטי של הנתונים ושולחת ל-UI Thread. רצה ב-Thread עצמאי.
  4. responseProcess - מתודת עזר של myHttpGet, רצה בתוך ה-Thread. שולפת את העמוד שהגיע, ושולחת אותו ל-Handler.
  5. alertDialogSet - מתודת עזר של myHttpGet להצגת דיאלוג במקרה של Exception.
  6. onCreateOptionsMenu - זהו אחד משני ה-callbacks של ה-optionsMenu. מופעל עם לחיצה על כפתור Menu. יוצר את תוכן התפריט.
  7. onOptionsItemSelected - זהו ה-callback השני של ה-optionsMenu. מופעל עם בחירת הפריט בתפריט. מפעיל את המתודה המתאימה לפריט שנבחר.
אגב, שימו לב להתווספות ספריות ה-Apache  - ראו למעלה בצהוב.

נעבור על המתודות ונוסיף פרוט לגביהן.

ה-onCreate

המתודה מתחלקת לשני חלקים, שנצבעו בצבעים שונים:

  1. טיפול ב-view ויצירת התצוגה.
  2. יצירת ה-Tabs.
בחלק הראשון מאתחלים את ה-TextView וה-WebView שנשלפים מתוך ה-R class.
בחלק השני יוצרים את ה-Tabs - שני כפתורי ה-Tab שהוגדרו ב-main.xml. הסבר נוסף על Tabs אפשר למצוא בפוסט המיוחד לעניין.

 @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
     textView= (TextView)findViewById(R.id.text_view_output);
    browser=(WebView)findViewById(R.id.webkit);
    TabHost tabs=(TabHost)findViewById(R.id.tabhost);
    tabs.setup();
    TabHost.TabSpec spec;
    spec=tabs.newTabSpec("tag1");
    spec.setContent(R.id.text_view_output);
    spec.setIndicator("Plain Text");
    tabs.addTab(spec);
    spec=tabs.newTabSpec("tag2");
    spec.setContent(R.id.webkit);
    spec.setIndicator("webkit");
    tabs.addTab(spec);
    }


handleMessage

 המתודה handleMessage מקבלת את כל הדף כ-String ושמה אותו ב-TextView.

 Handler handler = new Handler(){
         public void handleMessage(Message msg) {
            String recString = msg.getData().getString("myPage");
           textView.setText(recString); 
         }
    }; 


myHttpGet

זהו החלק העיקרי של הדיון היום.
מחולקת לשלושה חלקים צבועים בצבע שונה:
חלק 1: הפעלת המתודה loadUrl של ה-WebView. כאן נטען העמוד לתצוגה ב-WebView.
חלק 2: הבאת הדף תוך שימוש ב-Apache HttpClient.


זוכרים את  ששת השלבים שצוינו במדריך של Apache אותם הזכרתי למעלה? נבחן אותם כאן מול הקוד - ראה שורות ממוספרות למטה.

  1. צור instance של HttpClient. - זה מתבצע בשורה 1.
  2. צור instance של אחת המתודות - כלומר Get או Post וכדומה. שורה 2 - יצירת instance של Get.
  3. הפעל את המתודה execute. שורה 4. לפני כן, הכנת ה-request שכולל אך ורק את כתובת הדף.
  4. קרא את ה-response - התשובה שמתקבלת מהצד השני.  שורה 4, גם קוראת את ה-response.
  5. סגור את החיבור - גם בזה מטפלת ה-execute.
  6. טפל בתשובה שהתקבלה. שורה 5, קריאה למתודת עזר responseProcess.
חלק 3: טיפול בשלושה סוגי -exceptions.
  1. URISyntaxException - תו לא חוקי ב-URI.
  2. IllegalStateException -  יופיע במקרה של כתובת שגויה.
  3. IOException - זוהי בעיה שקשורה בתקשורת - Transport, כך שבהרבה מקרים ניתן להתגבר עליה על ידי נסיונות חיבור נוספים. ה-HttpClient בעצמו מבצע  אוטומטית נסיונות נוספים להתקשרות לפני שליחת ה-exception.
הערה: ה-IOException  הוא היחידי שיופיע גם כשהמערכת תקינה, אבל יש בעיות תקשורת כגון רשת לא זמינה. נסיונות התקשרות נוספים יכולים כאמור לפתור את הבעיה.

    
במקרה של exception, יתבצעו שני תהליכים:

  1. תשלח הודעה ל-Logger
  2. וגם יופעל alertDialog.

שימו לב למתודות ה-Looper (צבועות בכתום). בלעדיהן לא ניתן היה להפעיל את ה=alertDialog מתוך ה-Thread.  ה-Looper class נועד לאפשר קבלת הודעות לתוך Thread, וגם לשליחת dialog מתוכו. לצורך זה יש להוסיף את 3 השורות הכתומות: ה-Looper.prepare לפני הקריאה, ואת השורות שאחריה.

    public void myHttpGet(){ 
      new Thread() {
     public void run() {
     String TAG = "run";
         String url = "http://www.apache.org/";
          browser.loadUrl(url);
          Looper.prepare();
    try {
  1.         HttpClient client = new DefaultHttpClient();
  2.         HttpGet request = new HttpGet();
  3.         request.setURI(new URI(url));
  4.         HttpResponse response = client.execute(request);
  5.         responseProcess(response);
    }catch(URISyntaxException e){
        Log.e(TAG,"myHttpGetHttpGet URISyntaxException");
        alertDialogSet("URISyntaxException  connecting to" + url);
    }catch(IOException e){
        Log.e(TAG,"myHttpGetHttpGet IOException");
         alertDialogSet("IOException Problem connecting to" + url);
    }catch(IllegalStateException e){
        Log.e(TAG,"myHttpGetHttpGet IllegalStateException");
       alertDialogSet("myHttpGetHttpGet IllegalStateException" + url);
    }
   Looper.loop();
   Looper.myLooper().quit();
    }
   }.start();
   }




responseProcess







זוהי מתודת עזר שמטפלת בדף שהתקבל. היא ממש לא מתוחכמת:  היא לא שולפת את האינפורמציה מעמוד ה-HTML  אלא מעבירה אותו כמו שהוא, רק מחדירה תווי "שורה חדשה" בין השורות.



היא מופעלת מתוך ה-Thread.




ארבעה חלקים למתודה, גם הפעם מסומנים בצבע שונה:
יצירת buffer והעברת התגובה שהתקבלה (ה-response) לתוכו. 
קריאה מתוך ה-buffer הנ"ל, שורה אחרי שורה, והכנסת תו "שורה חדשה" בסוף כל שורה. שמירה ב-buffer נוסף -stringBuf.
שליחת הדף שהתקבל ל-Handler לצורך טיפול מחוץ ל-Thread.
טיפול ב-Exceptions.
סגירת ה-buffer.














   private void responseProcess(HttpResponse response){
Handler mHandler = handler;
BufferedReader inBuff = null;
final String TAG = "responseProcess";
try{ 
  inBuff = new BufferedReader (new InputStreamReader(response.getEntity().getContent()));
    StringBuffer stringBuf = new StringBuffer("");
    String line = null;
    String NewLine = System.getProperty("line.separator");
    while ((line = inBuff.readLine()) != null) {
        stringBuf.append(line + NewLine);
    }
    inBuff.close();
    String page = stringBuf.toString();
    Message msg = mHandler.obtainMessage();
    Bundle bundle = new Bundle();
    bundle.putString("myPage", page);
    msg.setData(bundle);
    mHandler.sendMessage(msg);
}catch(IOException e){
     Log.e(TAG," IOException");
    alertDialogSet("responseProcess IOException");
}
finally {
  if (inBuff != null) {
  try {
  inBuff.close();
         }catch (IOException e) {
            Log.e(TAG,"IOException closing buffer");
         }
     }
}
    }







alertDialogSet




מתודה שנותנת שרות הפעלת alertDialog למספר מקרים של exception. שימו לב להמצאות כפתור ביטול ל-dialog.









  private void alertDialogSet(String msg){
      AlertDialog.Builder builder = new AlertDialog.Builder(this);
      builder.setMessage(msg);
      builder.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
        public void onClick(DialogInterface dialog, int id) {
        }
     });
  AlertDialog alert = builder.create();
  alert.show();
    }


onCreateOptionsMenu ו- 
onOptionsItemSelected 

  • מטפלות בתפריט הראשי - ה-Options Menu.
  • בתפריט מוגדר כפתור אחד בלבד: start, והוא מפעיל את myHttpGet.

static final int start = Menu.FIRST; 
    @Override 
    public boolean onCreateOptionsMenu(Menu menu) {
     menu.add(Menu.NONE, start, Menu.NONE, "Start");
     return(super.onCreateOptionsMenu(menu));
    }
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        super.onOptionsItemSelected(item);
        switch (item.getItemId()) {
            case start:
             myHttpGet();
            return(true);
        }
        return(false);
    }




5 תגובות:

  1. פוסט מצויין ומובן מאוד, תודה.

    השבמחק
  2. תודה רבה, פוסט מפורט וקריא.

    השבמחק
  3. תודה רבה על האינפורמציה.
    אני לא יודע אם השיטה שיישמת במדריך הזה מתאימה לפרוייקט שאני רוצה לעשות.
    אני רוצה שכל פעם לשלוח לדוגגמא סטרינג/CLASS מהאנדרואיד לSERVLET.
    עכשיו השאלה אם החיבור הוא חיבור רציף?או שכל פעם אני אצטרך לעשות DOGET מחדש.כי אם נגיד התחברתי לשרת עם שם משתמש וסיסמא איך אני אדע שהתחברתי?
    קראתי באינטרנט על הנושא הזה הבנתי שיש כמה אפשרויות יש של חיבור WEBSOCKET HTTPCLIENT וכו.
    אשמח אם תוכל להסביר בקצרה מה הדרך המתאימה לי, או לפחות לכוון אותי
    תודה מראש.

    השבמחק