Trasformare un sito web in un’app Android

Spesso, una volta messo on line un sito web o una web app, sorge l’esigenza di fornire ai propri utenti una versione mobile delle funzionalità esistenti. Non si possono certo ignorare le potenzialità di un’ampio bacino di utenza come quello che attinge al mondo degli smartphone, ma ciò implica anche tempi e costi aggiuntivi: pensiamo, ad esempio, alla necessità di dover implementare un backend di webservice dai quali un’app per smartphone possa reperire informazioni, aggiornare dei record, caricare file ecc. Tuttavia, se la nostra webapp è già stata pensata per gli utenti mobile e dispone di un layout responsive, trasformarla in un’app Android potrebbe non essere un compito così arduo. Certo è, pero, che dovremo rinunciare a un look & feel nativo e che gli utenti più evoluti potrebbero non apprezzare del tutto (ma in quel caso avremmo guadagnato tempo per lo sviluppo del backend e avremmo raggiunto già un buon numero di utenti…).

Carichiamo una webview a tutto schermo

Il primo passo per realizzare il nostro scopo e creare un’activity a tutto schermo. In questo caso ho scelto il template FullscreenActivity di Android Studio, accessibile da File/New/New project, dopo aver scelto la denominazione del progetto e l’Api level  target (nel mio caso Api 17, Android 4.2 – Jelly Bean).  Aggiungiamo subito i permessi al manifest:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

che ci serviranno, rispettivamente, per far caricare alla webview l’url della nostra webapp e per accedere in lettura ai file, qualore fosse necessario caricarne qualcuno in una form di upload.

Nel file di layout fullscreen_activity.xml, all’interno della directory res,  inseriamo la nostra webview:

<WebView
    android:id="@+id/vw"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:keepScreenOn="true"/>

e aggiungiamo  al progetto una classe di comodo che chiameremo Utils, strutturata in questo modo:

public class Utils {

    public final static int FILECHOOSER_RESULTCODE=1;
    public static final int INPUT_FILE_REQUEST_CODE = 1;

    private static ValueCallback<Uri> uploadMessage;
    private static Uri capturedImageURI = null;
    private static ValueCallback<Uri[]> filePathCallback;
    private static String cameraPhotoPath;

    public static ValueCallback<Uri> getUploadMessage() {
        return uploadMessage;
    }

    public static void setUploadMessage(ValueCallback<Uri> uploadMessage) {
        Utils.uploadMessage = uploadMessage;
    }

    public static Uri getCapturedImageURI() {
        return capturedImageURI;
    }

    public static void setCapturedImageURI(Uri capturedImageURI) {
        Utils.capturedImageURI = capturedImageURI;
    }

    public static ValueCallback<Uri[]> getFilePathCallback() {
        return filePathCallback;
    }

    public static void setFilePathCallback(ValueCallback<Uri[]> filePathCallback) {
        Utils.filePathCallback = filePathCallback;
    }

    public static String getCameraPhotoPath() {
        return cameraPhotoPath;
    }

    public static void setCameraPhotoPath(String cameraPhotoPath) {
        Utils.cameraPhotoPath = cameraPhotoPath;
    }
}

contenente le costanti e le variabili condivise dalle classi della nostra applicazione.

Estendiamo la classe WebViewClient

Prima di caricare l’url del nostro sito nella webview, abbiamo bisogno di mostrare un progress dialog, in modo da mostrare all’utente in attesa che il caricamento dei contenuti è in corso. Inoltre dovremo eseguire l’override del metodo shouldOverrideUrlLoading, in modo da non far aprire il browser predefinito:

import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.webkit.WebView;
import android.webkit.WebViewClient;

/**
 * Created by oloap on 15/03/2018.
 */

public class MyWebClient extends WebViewClient {
    ProgressDialog progressDialog;

    Activity activity;

    public MyWebClient(Activity container) {
        activity = container;
    }

    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {

        //restituendo false, l'url verrà caricato nella nostra webview
        return false;

    }


    public void onPageStarted(WebView view, String url, Bitmap favicon) {


        if (progressDialog == null) {
            progressDialog = new ProgressDialog(activity);
            progressDialog.setMessage("Loading...");
            progressDialog.show();
        }
    }


    public void onPageFinished(WebView view, String url) {
        try {

            if (progressDialog.isShowing()) {
                progressDialog.dismiss();
                progressDialog = null;
                activity.getParent().recreate();
            }
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }
}

Estendiamo la classe WebChromeClient

A questo punto, poiché la nostra webview non deve semplicemente renderizzare codice html, ma deve utilizzare tutte le funzionalità possibili (ad esempio, caricare la favicon del sito), dobbiamo impostrarne il chrome client. Quest’ultimo andrà esteso per gestire al meglio il file upload (in questo caso specifico, gestiamo l’upload delle immagini). Creiamo quindi la classe WebChromeClient:

public class MyChromeClient extends WebChromeClient

e passiamo al costruttore tanto la main activity, quanto la webview:

private Activity activity;
private WebView webView;
public MyChromeClient(Activity container, WebView _webView) {
    activity = container;
    webView = _webView;
}

Implementiamo i metodi openFileChooser eseguendo un overload per gestire diverse versioni di Android:

//openFileChooser per le altre versioni di Android
public void openFileChooser(ValueCallback<Uri> uploadMsg,
                            String acceptType,
                            String capture) {

    openFileChooser(uploadMsg, acceptType);
}

// openFileChooser for Android < 3.0
public void openFileChooser(ValueCallback<Uri> uploadMsg) {
    openFileChooser(uploadMsg, "");
}

// openFileChooser for Android 3.0+
public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType) {

    Utils.setUploadMessage(uploadMsg);
   
    // Crea ExampleFolder nella sdcard
    File imageStorageDir = new File(
            Environment.getExternalStoragePublicDirectory(
                    Environment.DIRECTORY_PICTURES)
            , "ExampleFolder");

    if (!imageStorageDir.exists()) {
        // Create AndroidExampleFolder at sdcard
        imageStorageDir.mkdirs();
    }

    //Crea il percorso dell'immagine scattata con la video camera 
    File file = new File(
            imageStorageDir + File.separator + "IMG_"
                    + String.valueOf(System.currentTimeMillis())
    );

    Utils.setCapturedImageURI(Uri.fromFile(file));

    //Intent della video camera
    final Intent captureIntent = new Intent(
            android.provider.MediaStore.ACTION_IMAGE_CAPTURE);

    captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, Utils.getCapturedImageURI());

    Intent i = new Intent(Intent.ACTION_GET_CONTENT);
    i.addCategory(Intent.CATEGORY_OPENABLE);
    i.setType("image/*");

    // Create file chooser intent
    Intent chooserIntent = Intent.createChooser(i, "Image Chooser");

    //Imposta l'intent della video camera per la finestra di selezione dei file
    chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS
            , new Parcelable[] { captureIntent });

    //Ad immagine selezionata chiamiamo il callback onActivityResult di FullscreenActivity
    activity.startActivityForResult(chooserIntent, Utils.FILECHOOSER_RESULTCODE);


}

// For Android 5.0
public boolean onShowFileChooser(WebView view, ValueCallback<Uri[]> filePath, WebChromeClient.FileChooserParams fileChooserParams) {
    // Double check that we don't have any existing callbacks
    if (Utils.getFilePathCallback() != null) {
        Utils.getFilePathCallback().onReceiveValue(null);
    }
    Utils.setFilePathCallback(filePath);

    Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    if (takePictureIntent.resolveActivity(activity.getPackageManager()) != null) {
        // Create the File where the photo should go
        File photoFile = null;
        try {
            photoFile = createImageFile();
            takePictureIntent.putExtra("PhotoPath", Utils.getCameraPhotoPath());
        } catch (IOException ex) {
            // Error occurred while creating the File
            Log.e(TAG, "Unable to create Image File", ex);
        }

        // Continue only if the File was successfully created
        if (photoFile != null) {
            Utils.setCameraPhotoPath("file:" + photoFile.getAbsolutePath());
            takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT,
                    Uri.fromFile(photoFile));
        } else {
            takePictureIntent = null;
        }
    }

    Intent contentSelectionIntent = new Intent(Intent.ACTION_GET_CONTENT);
    contentSelectionIntent.addCategory(Intent.CATEGORY_OPENABLE);
    contentSelectionIntent.setType("image/*");

    Intent[] intentArray;
    if (takePictureIntent != null) {
        intentArray = new Intent[]{takePictureIntent};
    } else {
        intentArray = new Intent[0];
    }

    Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER);
    chooserIntent.putExtra(Intent.EXTRA_INTENT, contentSelectionIntent);
    chooserIntent.putExtra(Intent.EXTRA_TITLE, "Image Chooser");
    chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentArray);

    activity.startActivityForResult(chooserIntent, Utils.INPUT_FILE_REQUEST_CODE);

    return true;

}

Carichiamo la home del sito

Adesso che abbiamo preparato tutti gli strumenti necessari, facciamo in modo che la nostra webview carichi la home del nostro sito e gestisca la finestra di selezione dei file. Per fare ciò, nel metodo onCreate di FullscreenActivity inseriamo il seguente codice:

this.webView = (WebView) findViewById(R.id.vw);
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
MyWebClient webViewClient = new MyWebClient(this);

webView.loadUrl(txtUrl.getText().toString());
webView.setWebViewClient(webViewClient);

webView.setWebChromeClient(new MyChromeClient(this, webView));
if (Build.VERSION.SDK_INT >= 19) {
    webView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
}
else if(Build.VERSION.SDK_INT >=11 && Build.VERSION.SDK_INT < 19) {
    webView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}

Gestiamo il tasto “indietro” e la chiusura del file chooser

Non possiamo certamente lasciare che l’utente esca dalla main activity, ma dobbiamo permettergli di navigare agilmente tra le varie schermate della nostra web app. Gestiremo quindi l’evento onKeyDown in questo modo

public boolean onKeyDown(int keyCode, KeyEvent event) {
    //Verifichiamo che l'evento sia scatenato dal tasto indietro e che nella webview ci sia una history di navigazione
    if ((keyCode == KeyEvent.KEYCODE_BACK) && webView.canGoBack()) {
        webView.goBack();
        return true;
    }
    
    //altrimenti eseguiamo il comportamento di default
    return super.onKeyDown(keyCode, event);
}

Infine, eseguiremo l’override del metodo onActivityResult, per gestire il messaggio di avvenuta selezione e il caricamento di un file:

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {

        if (requestCode != Utils.INPUT_FILE_REQUEST_CODE || Utils.getFilePathCallback() == null) {
            super.onActivityResult(requestCode, resultCode, data);
            return;
        }

        Uri[] results = null;

        // Check that the response is a good one
        if (resultCode == Activity.RESULT_OK) {
            if (data == null) {
                // If there is not data, then we may have taken a photo
                if (Utils.getCameraPhotoPath() != null) {
                    results = new Uri[]{Uri.parse(Utils.getCameraPhotoPath())};
                }
            } else {
                String dataString = data.getDataString();
                if (dataString != null) {
                    results = new Uri[]{Uri.parse(dataString)};
                }
            }
        }

        Utils.getFilePathCallback().onReceiveValue(results);
        Utils.setFilePathCallback(null);

    } else if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
        if (requestCode != Utils.FILECHOOSER_RESULTCODE || Utils.getUploadMessage() == null) {
            super.onActivityResult(requestCode, resultCode, data);
            return;
        }

        if (requestCode == Utils.FILECHOOSER_RESULTCODE) {

            if (Utils.getUploadMessage() == null) {
                return;

            }

            Uri result = null;

            try {
                if (resultCode != RESULT_OK) {

                    result = null;

                } else {

                    
                    result = data == null ? Utils.getCapturedImageURI() : data.getData();
                }
            } catch (Exception e) {
                Toast.makeText(getApplicationContext(), "activity :" + e,
                        Toast.LENGTH_LONG).show();
            }

            Utils.getUploadMessage().onReceiveValue(result);
            Utils.setUploadMessage(null);

        }
    }

    return;
}

Fatto ciò, siamo pronti a pubblicare la nostra app sul play store!

Conclusioni

Abbiamo visto che per trasformare un sito web in un’app android, è necessario caricare la homepage in una webview, abilitando l’esecuzione di javascript ed estendendo le classi WebClient e WebChromeClient per gestire il caricamento di un file. Inoltre, ne abbiamo gestito la navigazione, impedendo al tasto indietro di uscire dalla main activity e abbiamo gestito il callback del file chooser per notificare all’utente del caricamento effettuato. I sorgenti di questa guida sono disponibili su questo repository github (con la piccola differenza che l’url da caricare viene gestito tramite un EditText).