image/svg+xml $ $ ing$ ing$ ces$ ces$ Res Res ea ea Res->ea ou ou Res->ou r r ea->r ch ch ea->ch r->ces$ r->ch ch->$ ch->ing$ T T T->ea ou->r

Le protocole HTTP en quelques mots

Remarque : la 1ère version de HTTP est simple et peut être implantée "à la main" en utilisant des sockets TCP. Les versions suivantes (2 et 3) sont beaucoup plus complexes, l'utilisation d'une bibliothèque implantant le protocole est incontournable.

Page pour en savoir plus sur le protocole HTTP

Exemple d'utilisation de HttpURLConnection

Réalisation d'une activité manipulant des compteurs d'entiers avec l'API web countapi.xyz.

Support de deux opérations :

  1. Récupération de la valeur d'un compteur
  2. Incrémentation de la valeur d'un compteur

Implantation de méthodes statiques réalisant ces deux opérations avec HttpURLConnection :

// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License

package fr.upem.coursand.webcounter;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

/** Definition of useful methods to handle the counter API from CountApi.xyz on Android
 *  We use the HttpURLConnection API froom the java.net package
 */
public class CountApiUtils {
    public static final int BUFFER_SIZE = 2048;
    public static final Charset CHARSET = StandardCharsets.UTF_8;

    public static final String NAMESPACE = "coursand";

    /** This call does not follow the REST philosophy since it supports the GET method;
     *  whereas the GET method should not induce server-side modifications
     */
    public static final String INCREMENT_URL = "https://api.countapi.xyz/hit/:namespace/:key";

    public static final String GET_URL = "https://api.countapi.xyz/get/:namespace/:key";

    public static JSONObject makeHttpRequestWithJSONResult(String url) throws IOException, JSONException {
        URL u = new URL(url);
        HttpURLConnection connection = (HttpURLConnection)u.openConnection();
        connection.connect();
        if (connection.getResponseCode() == 200) {
            try (InputStream input = connection.getInputStream()) {
                // Accumulate all the data in memory
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                byte[] buffer = new byte[BUFFER_SIZE];
                for (int r = input.read(buffer); r != -1; r = input.read(buffer))
                    baos.write(buffer, 0, r); // write into the baos the read buffer
                byte[] data = baos.toByteArray();
                // Convert using the default charset to a string
                String dataStr = new String(data, 0, data.length, CHARSET);
                // Parse the string as JSON
                JSONObject json = new JSONObject(dataStr);
                return json;
            }
        } else
            throw new IOException("HTTP error " + connection.getResponseCode());
    }

    public static class CounterApiException extends Exception {
        public CounterApiException(Throwable cause) { super(cause); }
    }

    private static String makeURL(String base, String key) {
        String url = base.replace(":namespace", NAMESPACE).replace(":key", key);
        return url;
    }

    /** Increment a counter and return the value of the incremented counter */
    public static int incrementCounter(String key) throws CounterApiException {
        try {
            JSONObject result = makeHttpRequestWithJSONResult(makeURL(INCREMENT_URL, key));
            return result.getInt("value");
        } catch (IOException|JSONException e) {
            throw new CounterApiException(e);
        }
    }

    /** Fetch the value of a counter (return -1 if the counter does not exist) */
    public static int fetchCounter(String key) throws CounterApiException {
        try {
            JSONObject result = makeHttpRequestWithJSONResult(makeURL(GET_URL, key));
            if (result.get("value") == null) return -1; // non existing value
            return result.getInt("value");
        } catch (IOException|JSONException e) {
            throw new CounterApiException(e);
        }
    }


}

Ensuite nous réalisons l'activité WebCounterActivity utilisant les deux méthodes. Les méthodes réalisées sont synchrones et bloquent donc lors de la communication HTTP : nous les appelons depuis la méthode doInBackground d'une AsyncTask afin qu'elles soient exécutées dans une thread secondaire.

⚠ La classe telle qu'elle est implantée ne supporte pas la rotation par destruction et recréation d'activité

// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License

package fr.upem.coursand.webcounter;

import android.Manifest;
import android.os.AsyncTask;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import java.util.Date;

import fr.upem.coursand.R;
import fr.upem.coursand.launcher.ActivityMetadata;

/** Activity handling a web counter with 2 operations: get and increment
 * TODO: support screen rotation (the activity is destroyed and restarted)
 */
@ActivityMetadata(permissions={Manifest.permission.INTERNET})
public class WebCounterActivity extends AppCompatActivity {

    private EditText counterKeyEditText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_web_counter);
        counterKeyEditText = findViewById(R.id.counterKeyView);
    }

    public void appendText(String message) {
        TextView tv = findViewById(R.id.counterResultView);
        tv.append(new Date().toString() +  ": " + message);
    }

    private String getKey() {
        String key = counterKeyEditText.getText().toString().trim();
        if (key.isEmpty()) {
            Toast.makeText(this, "Key must not be empty", Toast.LENGTH_SHORT).show();
            return null;
        } else
            return key;
    }

    /** Get the counter value */
    public void onGetClick(View v) {
        AsyncTask<String, Void, Integer> task = new AsyncTask<String, Void, Integer>() {
            private CountApiUtils.CounterApiException exception = null;
            private String key;

            @Override
            protected Integer doInBackground(String... strings) {
                this.key = strings[0];
                int result = -1;
                try {
                    result = CountApiUtils.fetchCounter(this.key);
                } catch (CountApiUtils.CounterApiException e) {
                    exception = e;
                }
                return result;
            }

            @Override
            protected void onPostExecute(Integer result) {
                if (result == -1 && exception == null) appendText("Key " + key + " does not exist\n");
                else if (result == -1 && exception != null) appendText("Exception encountered: " + exception.toString() + "\n");
                else appendText(key + "=" + result + "\n");
            }
        };
        String key = getKey();
        if (key != null) task.execute(key);
    }

    /** Get the counter value */
    public void onIncrementClick(View v) {
        AsyncTask<String, Void, Integer> task = new AsyncTask<String, Void, Integer>() {
            private CountApiUtils.CounterApiException exception = null;
            private String key;

            @Override
            protected Integer doInBackground(String... strings) {
                this.key = strings[0];
                int result = -1;
                try {
                    result = CountApiUtils.incrementCounter(this.key);
                } catch (CountApiUtils.CounterApiException e) {
                    exception = e;
                }
                return result;
            }

            @Override
            protected void onPostExecute(Integer result) {
                if (result == -1 && exception != null) appendText("Exception encountered: " + exception.toString() + "\n");
                else appendText(key + " incremented to " + result + "\n");
            }
        };
        String key = getKey();
        if (key != null) task.execute(key);
    }
}

Exemple d'utilisation de OkHttp

Réalisation d'une activité réalisant une requête HTTP paramétrable

Utilisation de OkHttp en mode asynchrone :

// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License

package fr.upem.coursand.webrequest;

import android.Manifest;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.preference.PreferenceManager;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.RadioGroup;
import android.widget.TextView;

import org.jetbrains.annotations.NotNull;
import org.json.JSONObject;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import fr.upem.coursand.R;
import fr.upem.coursand.launcher.ActivityMetadata;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.FormBody;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;

/** An activity allowing to do customized HTTP requests */
@ActivityMetadata(permissions={Manifest.permission.INTERNET})
public class WebRequestActivity extends AppCompatActivity {

    /** Value to use to specify where to put a joined file */
    public static final String FILE_PLACEHOLDER = "@file";

    private EditText urlView;
    private EditText methodView;
    private EditText headersView;
    private RadioGroup encodingView;
    private EditText bodyView;
    private TextView fileExplanationView;
    private TextView resultView;

    // If a file is joined
    private Uri fileUri;

    // current HTTP call
    private Call currentCall;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_web_request);

        // assign the fields (should be more easier using Kotlin or the ButterKnife library
        urlView = findViewById(R.id.urlView);
        methodView = findViewById(R.id.methodView);
        headersView = findViewById(R.id.headersView);
        encodingView = findViewById(R.id.encodingView);
        bodyView = findViewById(R.id.requestBodyView);
        fileExplanationView = findViewById(R.id.fileExplanationView);
        bodyView = findViewById(R.id.requestBodyView);
        resultView = findViewById(R.id.requestResultView);

        // prefill the fields using the preferences
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
        urlView.setText(prefs.getString("url", ""));
        methodView.setText(prefs.getString("method", "POST"));
        headersView.setText(prefs.getString("headers", ""));
        switch(prefs.getString("encoding", "urlEncoded")) {
            case "urlEncoded":
                encodingView.check(R.id.urlEncodedRadioButton);
                break;
            case "formdata":
                encodingView.check(R.id.formDataRadioButton);
                break;
            case "json":
                encodingView.check(R.id.jsonRadioButton);
                break;
            case "raw":
                encodingView.check(R.id.rawRadioButton);
                break;
        }
        bodyView.setText(prefs.getString("body", ""));

        // if a file is joined with ACTION_SEND
        if (getIntent().getAction() != null && getIntent().getAction().equals(Intent.ACTION_SEND)) {
            Uri uri = getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
            if (uri != null) {
                fileUri = uri;
                fileExplanationView.setText("Use @file as value to include " + fileUri);
                fileExplanationView.setVisibility(View.VISIBLE);
            }
        }
    }

    /** Extract the map of parameters in a string */
    public Map<String, String> extractParams(String s) {
        Map<String, String> params = new HashMap<>();
        for (String line: s.split("\n")) {
            String[] components = line.split(":", 2);
            if (components.length == 2) {
                params.put(components[0].trim(), components[1].trim());
            }
        }
        return params;
    }

    /** Build the request using the values of fields */
    protected Request buildRequest() {
        Request.Builder rb = new Request.Builder()
                .url(urlView.getText().toString());
        RequestBody body = null; // to be built
        // add headers
        for (Map.Entry<String, String> entry: extractParams(headersView.getText().toString()).entrySet())
            rb.addHeader(entry.getKey(), entry.getValue());

        // manage encoding for the body
        String encoding = null;
        switch(encodingView.getCheckedRadioButtonId()) {
            case R.id.urlEncodedRadioButton:
                encoding = "urlEncoded";
                FormBody.Builder fbb = new FormBody.Builder();
                for (Map.Entry<String, String> entry: extractParams(bodyView.getText().toString()).entrySet())
                    fbb.add(entry.getKey(), entry.getValue());
                body = fbb.build();
                break;
            case R.id.formDataRadioButton:
                encoding = "formdata";
                MultipartBody.Builder mbb = new MultipartBody.Builder()
                        .setType(MultipartBody.FORM);
                for (Map.Entry<String, String> entry: extractParams(bodyView.getText().toString()).entrySet())
                    if (entry.getValue().equals(FILE_PLACEHOLDER) && fileUri != null) {
                        // special treatment to join a file
                        // convert Uri to File
                        mbb.addFormDataPart(entry.getKey(), "file", new UriRequestBody(getContentResolver(), fileUri));
                    } else {
                        mbb.addFormDataPart(entry.getKey(), entry.getValue());
                    }
                body = mbb.build();
                break;
            case R.id.jsonRadioButton:
                encoding = "json";
                JSONObject json = new JSONObject(extractParams(bodyView.getText().toString()));
                body = RequestBody.create(MediaType.parse("application/json"), json.toString());
                break;
            case R.id.rawRadioButton:
                encoding = "raw";
                body = RequestBody.create(MediaType.parse("text/plain"), bodyView.getText().toString());
                break;
        }

        // save into the preferences
        SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(this).edit();
        editor.putString("url", urlView.getText().toString());
        editor.putString("method", methodView.getText().toString());
        editor.putString("encoding", encoding);
        editor.putString("body", bodyView.getText().toString());
        editor.commit();

        return rb.build();
    }

    /** When one click on the send button */
    public void onSendClick(View v) {
        // cancel the possible previous call
        if (currentCall != null) currentCall.cancel();
        currentCall = null;
        // Build the request
        Request r = buildRequest();
        OkHttpClient client = new OkHttpClient();
        currentCall = client.newCall(r);
        currentCall.enqueue(new Callback() {
            @Override
            public void onFailure(@NotNull Call call, @NotNull IOException e) {
                runOnUiThread( () -> {
                    // the call failed
                    resultView.setText("HTTP call failed with exception " + e);
                    currentCall = null;
                });
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                runOnUiThread( () -> {
                    String r = "Result code: " + response.code() + "\n";
                    // add the headers
                    for (kotlin.Pair<?,?> header: response.headers())
                        r += header.getFirst().toString() + ": " + header.getSecond().toString() + "\n";
                    r += "\n";
                    // read the body
                    try {
                        r += "\n" + response.body().string(); // could be dangerous if the body is too largge for memory
                    } catch (IOException e) {
                        r += "Cannot read the body due to exception " + e;
                    }
                    resultView.setText(r);
                    currentCall = null;
                });
            }
        });
    }

    @Override
    protected void onStop() {
        super.onStop();
        // cancel the current call if not null
        if (currentCall != null) {
            currentCall.cancel();
            currentCall = null;
        }
    }
}

Exemple d'utilisation de websockets avec OkHttp

Réalisation d'une activité avec envoi et réception de messages texte (encodage UTF-8) avec le protocole Websocket (utilisation de la bibliothèque OkHttp)

Content not available