org.comixwall.pffw.GraphsBase.java Source code

Java tutorial

Introduction

Here is the source code for org.comixwall.pffw.GraphsBase.java

Source

/*
 * Copyright (C) 2017-2018 Soner Tari
 *
 * This file is part of PFFW.
 *
 * PFFW is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * PFFW is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with PFFW.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.comixwall.pffw;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.widget.SwipeRefreshLayout;
import android.util.Base64;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;

import org.json.JSONArray;
import org.json.JSONObject;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Iterator;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;

import static org.comixwall.pffw.MainActivity.controller;
import static org.comixwall.pffw.MainActivity.fragment;
import static org.comixwall.pffw.MainActivity.logger;
import static org.comixwall.pffw.Utils.getSslContext;

/**
 * Base class for all graphs fragments.
 */
public abstract class GraphsBase extends Fragment implements SwipeRefreshLayout.OnRefreshListener,
        RefreshTimer.OnTimeoutListener, ControllerTask.ControllerTaskListener {

    GraphsCache mModuleCache;

    View view;

    private RefreshTimer mTimer;
    private int mRefreshTimeout = 10;

    private SwipeRefreshLayout swipeRefresh;

    /**
     * Return value from the controller command.
     * This should contain KVPs from titles to graph hash names.
     */
    private JSONObject mGraphsJsonObject;

    /**
     * This is the name of the symon layout file.
     * Graphs are defined in such symon layout files.
     * Hence, this var is passed to the controller command as an argument.
     */
    String mLayout = "ifs";

    /**
     * Dimensions passed to the controller command for graph generation.
     */
    private int mGraphWidth;
    private int mGraphHeight;

    /**
     * We create our own SSL context which trusts the PFFW server certificate.
     */
    private SSLContext sslContext = null;

    /**
     * This is the host name verifier used in secure HTTP connections to the PFFW hosts.
     */
    private final HostnameVerifier hostnameVerifier = new HostnameVerifier() {
        public boolean verify(String hostname, SSLSession session) {
            return hostname.equals(controller.getHost()) || hostname.equals(controller.getHostname());
        }
    };

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

        init(inflater, container);

        swipeRefresh = view.findViewById(R.id.swipeRefresh);
        swipeRefresh.setOnRefreshListener(this);

        sslContext = getSslContext(this);

        return view;
    }

    /**
     * Initialize fragment layout and view vars.
     * Graphs fragments do their initialization here. In this method the fragment should:
     * <ul>
     * <li>Inflate its layout
     * <li>Assign to its view variables
     * <li>Create its cache
     * </ul>
     * This method is called the first thing as the fragment view is created.
     *
     * @param inflater See {@link #onCreateView}
     * @param container See {@link #onCreateView}
     */
    protected abstract void init(LayoutInflater inflater, ViewGroup container);

    @Override
    public void onPause() {
        super.onPause();

        mModuleCache.mGraphsJsonObject = mGraphsJsonObject;

        saveImages();

        // ATTENTION: It is very important to cancel the timer
        mTimer.cancel();
    }

    protected abstract void saveImages();

    /**
     * Resume fragment. If the mGraphsJsonObject var is null, we refresh the graphs.
     * Otherwise, we use the graphs available in the fragment cache.
     * <p>
     * We also make sure the static fragment var points to the current visible fragment.
     * <p>
     * All graphs pages refresh periodically, so this is where we start the refresh timer too.
     */
    @Override
    public void onResume() {
        super.onResume();

        fragment = this;

        mGraphsJsonObject = mModuleCache.mGraphsJsonObject;

        if (mGraphsJsonObject == null) {
            getGraphs();
        } else {
            restoreImages();
            updateImages();
        }

        // Schedule the timer here, not in onCreateView(), because mRefreshTimeout may be updated in loadURL()
        mTimer = new RefreshTimer((MainActivity) getActivity(), this);
        mTimer.start(mRefreshTimeout);
    }

    protected abstract void restoreImages();

    /**
     * Compute graph dimensions.
     * Graph width and height is determined based on the current size of the screen.
     * This size depends on the device and current rotation.
     * <p>
     * The width and height computed here are passed to the Controller to be used while creating the image.
     */
    @Override
    public void executePreTask() {
        // ATTENTION: Measured width of swipeRefresh may be 0 on rotation, which gives negative w otherwise
        int w = swipeRefresh.getMeasuredWidth() - 180;
        mGraphWidth = w > 0 ? w : 900;

        // Limix min/max width
        mGraphWidth = mGraphWidth > 2048 ? 2048 : mGraphWidth;
        mGraphWidth = mGraphWidth < 32 ? 32 : mGraphWidth;

        mGraphHeight = Math.round(mGraphWidth / 3f);

        // Limix min/max height
        mGraphHeight = mGraphHeight > 2048 ? 2048 : mGraphHeight;
        mGraphHeight = mGraphHeight < 32 ? 32 : mGraphHeight;
    }

    @Override
    public void preExecute() {
        swipeRefresh.setRefreshing(true);
    }

    /**
     * Run the controller task.
     * We fetch the graphs using secure http, or fall back to plain http if secure connection fails.
     * <p>
     * Note that the PFFW uses a self-signed server certificate. So the code should trust that certificate
     * and not reject the hostname.
     *
     * @return True on success, false on failure.
     */
    @Override
    public boolean executeTask() {
        Boolean retval = true;
        try {
            String output = controller.execute("symon", "RenderLayout", mLayout, mGraphWidth, mGraphHeight);

            JSONArray jsonArray = new JSONArray(output);
            mGraphsJsonObject = new JSONObject(jsonArray.get(0).toString());

            Iterator<String> it = mGraphsJsonObject.keys();
            while (it.hasNext()) {
                String title = it.next();
                String file = mGraphsJsonObject.getString(title);

                try {
                    InputStream stream = null;

                    try {
                        String outputGraph = controller.execute("symon", "GetGraph", file);
                        String base64Graph = new JSONArray(outputGraph).get(0).toString();
                        stream = new ByteArrayInputStream(Base64.decode(base64Graph, Base64.DEFAULT));

                    } catch (Exception e) {
                        e.printStackTrace();
                        logger.warning("SSH graph connection exception: " + e.toString());
                    }

                    // Try secure http if ssh fails
                    if (stream == null) {
                        // 1540861800_404e00f4044d07242a77f802e457f774
                        String hash = file.substring(file.indexOf('_') + 1);

                        try {
                            // Using https here gives: CertPathValidatorException: Trust anchor for certification path not found.
                            // So we should trust the PFFW server crt and hostname
                            URL secureUrl = new URL("https://" + controller.getHost() + "/symon/graph.php?" + hash);

                            HttpsURLConnection secureUrlConn = (HttpsURLConnection) secureUrl.openConnection();

                            // Tell the URLConnection to use a SocketFactory from our SSLContext
                            secureUrlConn.setSSLSocketFactory(sslContext.getSocketFactory());

                            // Install the PFFW host verifier
                            secureUrlConn.setHostnameVerifier(hostnameVerifier);

                            logger.finest("Using secure http: " + secureUrl.toString());

                            // ATTENTION: Setting a timeout value enables SocketTimeoutException, set both timeouts
                            secureUrlConn.setConnectTimeout(5000);
                            secureUrlConn.setReadTimeout(5000);
                            logger.finest("Secure URL connection timeout values: "
                                    + secureUrlConn.getConnectTimeout() + ", " + secureUrlConn.getReadTimeout());

                            stream = secureUrlConn.getInputStream();

                        } catch (Exception e) {
                            e.printStackTrace();
                            logger.warning("Secure URL connection exception: " + e.toString());
                        }

                        // Try plain http if secure http fails
                        if (stream == null) {
                            // ATTENTION: Don't use try-catch here, catch in the outer exception handling
                            URL plainUrl = new URL("http://" + controller.getHost() + "/symon/graph.php?" + hash);

                            HttpURLConnection plainUrlConn = (HttpURLConnection) plainUrl.openConnection();

                            logger.finest("Using plain http: " + plainUrlConn.toString());

                            // ATTENTION: Setting a timeout value enables SocketTimeoutException, set both timeouts
                            plainUrlConn.setConnectTimeout(5000);
                            plainUrlConn.setReadTimeout(5000);
                            logger.finest("Plain URL connection timeout values: " + plainUrlConn.getConnectTimeout()
                                    + ", " + plainUrlConn.getReadTimeout());

                            stream = plainUrlConn.getInputStream();
                        }
                    }

                    Bitmap bmp = BitmapFactory.decodeStream(stream);
                    setBitmap(title, bmp);

                } catch (Exception e) {
                    // We are especially interested in SocketTimeoutException, but catch all
                    e.printStackTrace();
                    logger.info("GraphsBase doInBackground exception: " + e.toString());
                    // We should break out of while loop on exception, because all conn attempts have failed
                    break;
                }
            }

            output = controller.execute("pf", "GetReloadRate");

            int timeout = Integer.parseInt(new JSONArray(output).get(0).toString());
            mRefreshTimeout = timeout < 10 ? 10 : timeout;

        } catch (Exception e) {
            e.printStackTrace();
            logger.warning("doInBackground exception: " + e.toString());
            retval = false;
        }
        return retval;
    }

    /**
     * Update the bmp variable with the downloaded bitmap.
     *
     * @param title The graph title.
     * @param bmp The graph.
     */
    protected abstract void setBitmap(String title, Bitmap bmp);

    /**
     * Update the graphs with the downloaded bitmaps.
     * This is where the bitmaps are really loaded to bitmap views, thus are displayed on the fragment layout.
     *
     * @param result Whether the graph download task was successful or not.
     */
    @Override
    public void postExecute(boolean result) {
        if (result) {
            updateImages();
        }

        swipeRefresh.setRefreshing(false);
    }

    protected abstract void updateImages();

    @Override
    public void executeOnCancelled() {
        swipeRefresh.setRefreshing(false);
    }

    private void getGraphs() {
        ControllerTask.run(this, this);
    }

    /**
     * Displays the graph in a dialog for pinch-zooming.
     */
    final View.OnClickListener onViewClick = (new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            logger.finest("Show Graph Dialog");

            android.app.FragmentTransaction ft = getActivity().getFragmentManager().beginTransaction();

            GraphDialog dialog = new GraphDialog();
            dialog.setBitmap(getBitmap(view));
            dialog.show(ft, "Graph Dialog");
        }
    });

    /**
     * Return the bitmap associated with the view.
     * Used by the graph dialog to load the bitmap the user has clicked on.
     *
     * @param view The view the user has clicked on.
     * @return The bitmap to display.
     */
    protected abstract Bitmap getBitmap(View view);

    /**
     * Refresh graphs periodically.
     */
    @Override
    public void onTimeout() {
        getGraphs();
    }

    /**
     * Refresh graphs upon swipe gesture.
     * <p>
     * ATTENTION: Do not check if a task is started upon a swipe gesture: No need to implement a return value for run().
     * Because we want the progress bar to be on if a controller task is running,
     * whether as a result of the last swipe gesture or not.
     */
    @Override
    public void onRefresh() {
        getGraphs();
    }

    /**
     * Handle app bar menu clicks.
     * This method is called by the handler of the activity, hence all fragments should implement it.
     * <p>
     * We currently have only refresh and logout options. Logout is handled by the activity.
     *
     * @param item The menu item clicked on.
     * @return See {@link Fragment#onOptionsItemSelected(MenuItem)}.
     */
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();

        if (id == R.id.menuRefresh) {
            getGraphs();
            return true;
        }

        return super.onOptionsItemSelected(item);
    }
}

/**
 * This is the cache class of the graphs pages.
 * Graphs fragments may have a different cache type with different vars based on their needs.
 */
class GraphsCache {
    JSONObject mGraphsJsonObject;
    Bitmap bmp;
}