com.gmail.jiangyang5157.cardboard.net.DescriptionRequest.java Source code

Java tutorial

Introduction

Here is the source code for com.gmail.jiangyang5157.cardboard.net.DescriptionRequest.java

Source

/*
Reference: com.android.volley.toolbox.*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gmail.jiangyang5157.cardboard.net;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;
import android.widget.ImageView;

import com.android.volley.DefaultRetryPolicy;
import com.android.volley.NetworkResponse;
import com.android.volley.ParseError;
import com.android.volley.Request;
import com.android.volley.Response;
import com.android.volley.VolleyLog;
import com.android.volley.toolbox.HttpHeaderParser;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;

import java.io.UnsupportedEncodingException;
import java.util.Map;

/**
 * parseNetworkResponse returns either bitmap or string
 *
 * @author Yang
 * @since 8/12/2016
 */
public class DescriptionRequest extends Request<Object> {
    private static final String TAG = "[DescriptionRequest]";

    private Map<String, String> responseHeaders;

    protected static final int RESPONSE_TYPE_BITMAP = 1;
    protected static final int RESPONSE_TYPE_STRING = 2;
    protected int responseType;

    /**
     * Socket timeout in milliseconds for image requests
     */
    public static final int DEFAULT_IMAGE_TIMEOUT_MS = 1000;

    /**
     * Default number of retries for image requests
     */
    public static final int DEFAULT_IMAGE_MAX_RETRIES = 2;

    /**
     * Default backoff multiplier for image requests
     */
    public static final float DEFAULT_IMAGE_BACKOFF_MULT = 2f;

    private final Response.Listener<Object> mListener;
    private final Bitmap.Config mDecodeConfig;
    private final int mMaxWidth;
    private final int mMaxHeight;
    private ImageView.ScaleType mScaleType;

    /**
     * Decoding lock so that we don't decode more than one image at a time (to avoid OOM's)
     */
    private static final Object sDecodeLock = new Object();

    /**
     * Creates a new image request, decoding to a maximum specified width and
     * height. If both width and height are zero, the image will be decoded to
     * its natural size. If one of the two is nonzero, that dimension will be
     * clamped and the other one will be set to preserve the image's aspect
     * ratio. If both width and height are nonzero, the image will be decoded to
     * be fit in the rectangle of dimensions width x height while keeping its
     * aspect ratio.
     *
     * @param url           URL of the image
     * @param listener      Listener to receive the decoded bitmap
     * @param maxWidth      Maximum width to decode this bitmap to, or zero for none
     * @param maxHeight     Maximum height to decode this bitmap to, or zero for
     *                      none
     * @param scaleType     The ImageViews ScaleType used to calculate the needed image size.
     * @param decodeConfig  Format to decode the bitmap to
     * @param errorListener Error listener, or null to ignore errors
     */
    public DescriptionRequest(String url, Response.Listener<Object> listener, int maxWidth, int maxHeight,
            ImageView.ScaleType scaleType, Bitmap.Config decodeConfig, Response.ErrorListener errorListener) {
        super(Method.GET, url, errorListener);
        setRetryPolicy(new DefaultRetryPolicy(DEFAULT_IMAGE_TIMEOUT_MS, DEFAULT_IMAGE_MAX_RETRIES,
                DEFAULT_IMAGE_BACKOFF_MULT));
        mListener = listener;
        mDecodeConfig = decodeConfig;
        mMaxWidth = maxWidth;
        mMaxHeight = maxHeight;
        mScaleType = scaleType;
    }

    @Override
    public Priority getPriority() {
        return Priority.LOW;
    }

    /**
     * Scales one side of a rectangle to fit aspect ratio.
     *
     * @param maxPrimary      Maximum size of the primary dimension (i.e. width for
     *                        max width), or zero to maintain aspect ratio with secondary
     *                        dimension
     * @param maxSecondary    Maximum size of the secondary dimension, or zero to
     *                        maintain aspect ratio with primary dimension
     * @param actualPrimary   Actual size of the primary dimension
     * @param actualSecondary Actual size of the secondary dimension
     * @param scaleType       The ScaleType used to calculate the needed image size.
     */
    private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary, int actualSecondary,
            ImageView.ScaleType scaleType) {

        // If no dominant value at all, just return the actual.
        if ((maxPrimary == 0) && (maxSecondary == 0)) {
            return actualPrimary;
        }

        // If ScaleType.FIT_XY fill the whole rectangle, ignore ratio.
        if (scaleType == ImageView.ScaleType.FIT_XY) {
            if (maxPrimary == 0) {
                return actualPrimary;
            }
            return maxPrimary;
        }

        // If primary is unspecified, scale primary to match secondary's scaling ratio.
        if (maxPrimary == 0) {
            double ratio = (double) maxSecondary / (double) actualSecondary;
            return (int) (actualPrimary * ratio);
        }

        if (maxSecondary == 0) {
            return maxPrimary;
        }

        double ratio = (double) actualSecondary / (double) actualPrimary;
        int resized = maxPrimary;

        // If ScaleType.CENTER_CROP fill the whole rectangle, preserve aspect ratio.
        if (scaleType == ImageView.ScaleType.CENTER_CROP) {
            if ((resized * ratio) < maxSecondary) {
                resized = (int) (maxSecondary / ratio);
            }
            return resized;
        }

        if ((resized * ratio) > maxSecondary) {
            resized = (int) (maxSecondary / ratio);
        }
        return resized;
    }

    @Override
    protected Response<Object> parseNetworkResponse(NetworkResponse response) {
        responseHeaders = response.headers;
        //        for (Map.Entry<String, String> entry : responseHeaders.entrySet()) {
        //            Log.d(TAG, "headers: key/value: " + entry.getKey() + ", " + entry.getValue());
        //        }
        String contentType = responseHeaders.get("Content-Type");
        //        Log.d(TAG, "parseNetworkResponse.contentType: " + contentType);
        if (contentType.startsWith("image/jpeg")) {
            responseType = RESPONSE_TYPE_BITMAP;
            // Serialize all decode on a global lock to reduce concurrent heap usage.
            synchronized (sDecodeLock) {
                try {
                    return doBitmapParse(response);
                } catch (OutOfMemoryError e) {
                    VolleyLog.e("Caught OOM for %d byte image, url=%s", response.data.length, getUrl());
                    return Response.error(new ParseError(e));
                }
            }
        } else if (contentType.startsWith("text/html")) {
            responseType = RESPONSE_TYPE_STRING;
            return doHtmlParse(response);
        } else if (contentType.startsWith("application/json")) {
            responseType = RESPONSE_TYPE_STRING;
            return doJsonParse(response);
        } else {
            Log.w(TAG, "Unsupported contentType: " + contentType + " from " + getUrl());
            return Response.error(new ParseError());
        }
    }

    private String getParsedString(NetworkResponse response) {
        String parsed;
        try {
            parsed = new String(response.data, HttpHeaderParser.parseCharset(responseHeaders));
        } catch (UnsupportedEncodingException e) {
            parsed = new String(response.data);
        }
        return parsed;
    }

    private Response<Object> doJsonParse(NetworkResponse response) {
        String parsed = getParsedString(response);

        if (getUrl().contains(".wikipedia.org/")) {
            try {
                JSONObject jsonObject = new JSONObject(parsed);
                //                    Log.d(TAG, "JSONObject: " + jsonObject.toString());
                JSONObject query = jsonObject.getJSONObject("query");
                //                    Log.d(TAG, "JSONObject query: " + query.toString());
                JSONArray pageids = query.getJSONArray("pageids");
                //                    Log.d(TAG, "JSONArray pageids: " + pageids.toString());
                String pageId = pageids.getString(0);
                //                    Log.d(TAG, "String pageId: " + pageId.toString());
                JSONObject pages = query.getJSONObject("pages");
                //                    Log.d(TAG, "JSONObject pages: " + pages.toString());
                JSONObject page = pages.getJSONObject(pageId);
                //                    Log.d(TAG, "JSONObject page: " + page.toString());
                String extract = (String) page.get("extract");
                //                    Log.d(TAG, "String extract: " + extract);
                return Response.success(extract, HttpHeaderParser.parseCacheHeaders(response));
            } catch (JSONException e) {
                // expected exception "No Value For"
                Log.w(TAG, "JSONException: " + e.getMessage());
                return Response.error(new ParseError());
            }
        } else {
            return Response.error(new ParseError());
        }
    }

    private Response<Object> doHtmlParse(NetworkResponse response) {
        String parsed = getParsedString(response);

        Document doc;
        try {
            doc = Jsoup.parse(parsed);
        } catch (VerifyError | NoClassDefFoundError e) {
            // http://stackoverflow.com/questions/38059373/java-lang-verifyerror-when-downloading-data-with-jsoup-in-android-n
            // TODO: 10/1/2016 upgrade Jsoup
            return Response.error(new ParseError(response));
        }

        // for <title>Hello World</title>
        String content = doc.title();

        // for <meta property="og:description" content="Hello world." />
        Element mataPropertyOgDescription = doc.select("meta[property^=og:description]").first();
        if (mataPropertyOgDescription != null) {
            content += "\n\n" + mataPropertyOgDescription.attr("content");
        }
        return Response.success(content, HttpHeaderParser.parseCacheHeaders(response));
    }

    /**
     * The real guts of parseNetworkResponse. Broken out for readability.
     */
    private Response<Object> doBitmapParse(NetworkResponse response) {
        byte[] data = response.data;
        BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
        Bitmap bitmap;
        if (mMaxWidth == 0 && mMaxHeight == 0) {
            decodeOptions.inPreferredConfig = mDecodeConfig;
            bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
        } else {
            // If we have to resize this image, first get the natural bounds.
            decodeOptions.inJustDecodeBounds = true;
            BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
            int actualWidth = decodeOptions.outWidth;
            int actualHeight = decodeOptions.outHeight;

            // Then compute the dimensions we would ideally like to decode to.
            int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight, actualWidth, actualHeight, mScaleType);
            int desiredHeight = getResizedDimension(mMaxHeight, mMaxWidth, actualHeight, actualWidth, mScaleType);

            // Decode to the nearest power of two scaling factor.
            decodeOptions.inJustDecodeBounds = false;
            decodeOptions.inSampleSize = findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight);
            Bitmap tempBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);

            // If necessary, scale down to the maximal acceptable size.
            if (tempBitmap != null
                    && (tempBitmap.getWidth() > desiredWidth || tempBitmap.getHeight() > desiredHeight)) {
                bitmap = Bitmap.createScaledBitmap(tempBitmap, desiredWidth, desiredHeight, true);
                tempBitmap.recycle();
            } else {
                bitmap = tempBitmap;
            }
        }

        if (bitmap == null) {
            return Response.error(new ParseError(response));
        } else {
            return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response));
        }
    }

    @Override
    protected void deliverResponse(Object response) {
        mListener.onResponse(response);
    }

    /**
     * Returns the largest power-of-two divisor for use in downscaling a bitmap
     * that will not result in the scaling past the desired dimensions.
     *
     * @param actualWidth   Actual width of the bitmap
     * @param actualHeight  Actual height of the bitmap
     * @param desiredWidth  Desired width of the bitmap
     * @param desiredHeight Desired height of the bitmap
     */
    // Visible for testing.
    static int findBestSampleSize(int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) {
        double wr = (double) actualWidth / desiredWidth;
        double hr = (double) actualHeight / desiredHeight;
        double ratio = Math.min(wr, hr);
        float n = 1.0f;
        while ((n * 2) <= ratio) {
            n *= 2;
        }

        return (int) n;
    }

    public Map<String, String> getResponseHeaders() {
        return responseHeaders;
    }
}