com.playhaven.android.view.HTMLView.java Source code

Java tutorial

Introduction

Here is the source code for com.playhaven.android.view.HTMLView.java

Source

/**
 * Copyright 2013 Medium Entertainment, Inc.
 *
 * 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.playhaven.android.view;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.AttributeSet;
import android.view.View;
import android.webkit.*;
import com.playhaven.android.Placement;
import com.playhaven.android.PlayHaven;
import com.playhaven.android.PlayHavenException;
import com.playhaven.android.cache.Cache;
import com.playhaven.android.data.*;
import com.playhaven.android.req.UrlRequest;
import com.playhaven.android.util.JsonUtil;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

/**
 * HTML Content Unit
 */
public class HTMLView extends WebView implements ChildView<HTMLView> {
    private final String DISPATCH_PREFIX = "ph://";
    private Placement mPlacement;
    private List<String> mImages;
    private ArrayList<Reward> mRewards;
    private ArrayList<Purchase> mPurchases;
    private ArrayList<DataCollectionField> mDataFields;

    // These are bits of Javascript we inject into the window.
    /** callback template arguments: callbackId, JSON, error */
    private final String CALLBACK_TEMPLATE = "javascript:PlayHaven.nativeAPI.callback(\"%s\", %s, %s)";
    private final String DISPATCH_PROTOCOL_TEMPLATE = "javascript:window.PlayHavenDispatchProtocolVersion=4";
    private final String COLLECT_FORM_DATA = "javascript:$.ajax({dataType: 'jsonp', jsonp: 'dcDataCallback', data: $('form').serialize(), url: 'ph://dcData'});";

    /**
     * These match the host portion of DISPATCH_PREFIX urls requested by the content
     * templates. They indicate different events in the content template that
     * need attention from the SDK.
     */
    public enum Dispatches {
        /**
         * closeButton hides the native emergency close button, and passes
         * notice of whether it was hidden back to the content template
         */
        closeButton,
        /**
         * dismiss triggers the contentDismissed listener
         */
        dismiss,
        /**
         * launch retrieves a URL from the server to be parsed using
         * Intent.ACTION_VIEW
         */
        launch,
        /**
         * loadContext passes the full "context" JSON blob to the
         * content template
         */
        loadContext,
        /**
         * purchase stores the purchase object (which is generated by the
         * content template) as mPurchases, for use with dismiss dispatch
         */
        purchase,
        /**
         * reward stores the reward object (which is generated by the
         * content template) as mRewards, for use with dismiss dispatch
         */
        reward,
        /**
         * subcontent takes a JSON blob generated by the content template
         * and uses that to get data for a new impression, currently a
         * more_games widget that follows a featured ad
         */
        subcontent,
        /**
         * No longer used
         */
        track,
        /**
         * This is one injected to let the Android SDK harvest data from the
         * opt-in data collection form.
         */
        dcData
    }

    /**
     * Makes the subcontent request, replaces the model in the placement, and
     * then reloads the WebView with the new stuff.
     */
    private class SubcontentRequest extends com.playhaven.android.req.SubcontentRequest {
        public SubcontentRequest(String dispatchContext) {
            super(dispatchContext);
        }

        @Override
        protected void handleResponse(String json) {
            mPlacement.setModel(json);
            load(JsonUtil.<String>getPath(mPlacement.getModel(), "$.response.url"));
        }
    }

    private WebChromeClient webChromeClient = new WebChromeClient() {
        @Override
        public boolean onConsoleMessage(ConsoleMessage message) {
            PlayHaven.v("ConsoleMessage: %s", message.message());
            return super.onConsoleMessage(message);
        }
    };

    private WebViewClient webViewClient = new WebViewClient() {
        @Override
        public void onLoadResource(WebView view, String url) {
            if (url.startsWith(DISPATCH_PREFIX)) {
                handleDispatch(url);
            }
        }

        @Override
        public void onPageFinished(WebView view, String url) {
            super.onPageFinished(view, url);
            HTMLView.this.setVisibility(android.view.View.VISIBLE);
        }

        @Override
        public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
            // Load images or the content template from the cache.
            if (mImages.contains(url)
                    || url.equals(JsonUtil.<String>getPath(mPlacement.getModel(), "$.response.url"))) {
                try {
                    // TODO: spaces will break Cache, fix encoding before now.
                    url = url.replace(" ", "%20");
                    Cache cache = new Cache(getContext());
                    File file = cache.getFile(new URL(url));
                    cache.close();
                    if (file != null && file.exists()) {
                        PlayHaven.v("Loading from cache: %s.", file.getAbsolutePath());
                        InputStream inputStream = new FileInputStream(file);
                        return new WebResourceResponse("", "UTF-8", inputStream);
                    }
                } catch (Exception e) {
                    PlayHaven.e("Could not load from cache: %s.", url);
                    PlayHaven.e(e);
                }
            }
            return null;
        }

        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            if (url.startsWith(DISPATCH_PREFIX)) {
                handleDispatch(url);
                return true;
            } else {
                return super.shouldOverrideUrlLoading(view, url);
            }
        }
    };

    public HTMLView(Context context) {
        super(context);
    }

    public HTMLView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * This switches on the host portion of a request prefixed with
     * DISPATCH_PREFIX in order to handle events from the content templates.
     *
     * @TODO this would be a good candidate for factoring out to a cleaner custom WebViewClient
     *
     * @param dispatchUrl
     */
    private void handleDispatch(String dispatchUrl) {
        Uri callbackUri = Uri.parse(dispatchUrl);
        String callbackId = callbackUri.getQueryParameter("callback");
        String callbackString = callbackUri.getHost();
        String dispatchContext = callbackUri.getQueryParameter("context");
        PlayHaven.d("Handling dispatch: %s of type %s", dispatchUrl, callbackString);

        switch (Dispatches.valueOf(callbackString)) {
        /**
         * closeButton hides the native emergency close button, and passes
         * notice of whether it was hidden back to the content template
         */
        case closeButton:
            String hidden = "true";
            try {
                hidden = new JSONObject(dispatchContext).getString("hidden");
            } catch (JSONException jse) {
                // Default to NOT hiding the emergency close button
                hidden = "false";
            }

            if ("true".equals(hidden)) {
                ((PlayHavenView) getParent()).setExitVisible(false);
            }

            // Tell the content template that we've hidden the emergency close button.
            this.loadUrl(String.format(CALLBACK_TEMPLATE, callbackId, "{'hidden':'" + hidden + "'}", null));
            break;
        /**
         * dismiss triggers the contentDismissed listener
         */
        case dismiss:
            PlayHavenView.DismissType dismiss = PlayHavenView.DismissType.NoThanks;
            if (mRewards != null)
                dismiss = PlayHavenView.DismissType.Reward;

            if (mDataFields != null)
                dismiss = PlayHavenView.DismissType.OptIn;

            if (mPurchases != null)
                dismiss = PlayHavenView.DismissType.Purchase;

            mPlacement.getListener().contentDismissed(mPlacement, dismiss, generateResponseBundle());

            // Unregister the web view client so that any future dispatches will be ignored.
            HTMLView.this.setWebViewClient(null);

            break;
        /**
         * launch retrieves a URL from the server to be parsed using
         * Intent.ACTION_VIEW
         */
        case launch:
            mPlacement.getListener().contentDismissed(mPlacement, PlayHavenView.DismissType.Launch, null);

            /*
             * We can't get this from the original model because we don't
             * know which one they picked (if this was a more_games template).
             */
            String url;
            try {
                url = new JSONObject(dispatchContext).getString("url");
            } catch (JSONException jse) {
                PlayHaven.e("Could not parse launch URL.");
                return;
            }

            UrlRequest urlRequest = new UrlRequest(url);
            ExecutorService pool = Executors.newSingleThreadExecutor();
            final Future<String> uriFuture = pool.submit(urlRequest);
            final String initialUrl = url;

            new Thread(new Runnable() {
                @Override
                public void run() {
                    // Wait for our final link.
                    String url = null;
                    try {
                        url = uriFuture.get();
                    } catch (Exception e) {
                        PlayHaven.v("Could not retrieve launch URL from server. Using initial url.");

                        // If the redirect failed, proceed with the original url.
                        url = initialUrl;
                    }

                    // Launch whatever it is. It might be a Play, web, or other link
                    Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    try {
                        HTMLView.this.getContext().startActivity(intent);
                    } catch (Exception e) {
                        PlayHaven.e("Unable to launch URI from template.");
                        e.printStackTrace();
                    }
                }
            }).start();
            break;
        /**
         * loadContext passes the full "context" JSON blob to the
         * content template
         */
        case loadContext:
            this.loadUrl(DISPATCH_PROTOCOL_TEMPLATE);
            net.minidev.json.JSONObject context = JsonUtil.getPath(mPlacement.getModel(), "$.response.context");

            /**
             * @playhaven.apihack KitKat+ changed how the webview is loaded
             */
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                this.evaluateJavascript(String.format(CALLBACK_TEMPLATE, callbackId, context, null), null);
            } else {
                this.loadUrl(String.format(CALLBACK_TEMPLATE, callbackId, context, null));
            }
            break;
        /**
         * purchase stores the purchase object (which is generated by the
         * content template) as mPurchases, for use with dismiss dispatch
         */
        case purchase:
            collectAttachments(dispatchContext);
            break;
        /**
         * reward stores the reward object (which is generated by the
         * content template) as mRewards, for use with dismiss dispatch
         */
        case reward:
            net.minidev.json.JSONObject rewardParam = JsonUtil.getPath(mPlacement.getModel(),
                    "$.response.context.content.open_dispatch.parameters");
            if (rewardParam == null || rewardParam.size() == 0) {
                // data_collection template sends a reward dispatch when it submits form data ...
                // @TODO: have templates return more than key/value pairs (eg class, pattern)
                this.loadUrl(COLLECT_FORM_DATA);
            }

            collectAttachments(dispatchContext);
            break;
        /**
         * subcontent takes a JSON blob generated by the content template
         * and uses that to get data for a new impression, currently a
         * more_games widget that follows a featured ad
         */
        case subcontent:
            SubcontentRequest subcontentRequest = new SubcontentRequest(dispatchContext);
            subcontentRequest.send(getContext());
            break;
        /**  @TODO Find out why this dispatch was abandoned in 1.12 */
        case track:
            PlayHaven.d("track callback not implemented.");
            break;
        /**
         * This is one injected to let the Android SDK harvest data from the
         * opt-in data collection form.
         */
        case dcData:
            try {
                mDataFields = DataCollectionField.fromUrl(callbackUri);
            } catch (PlayHavenException e) {
                e.printStackTrace();
            }
            break;
        default:
            break;
        }
    }

    /**
     * Parses rewards and purchases out of the model and stores them for
     * disbursal upon dismiss dispatch
    */
    public void collectAttachments(String dispatchContext) {
        if (JsonUtil.hasPath(dispatchContext, "$.purchases"))
            mPurchases = Purchase.fromJson(dispatchContext);

        if (JsonUtil.hasPath(dispatchContext, "$.rewards"))
            mRewards = Reward.fromJson(dispatchContext);
    }

    /**
     * Loads a url into the this webview, ensures
     * that load occurs on UI thread.
     * @param url to load
     */
    public void load(final String url) {
        this.post(new Runnable() {
            @SuppressLint("InlinedApi")
            @Override
            public void run() {
                String fileUrl = null;
                try {
                    Cache cache = new Cache(getContext());
                    File template = cache.getFile(new URL(url));
                    cache.close();
                    if (template != null && template.exists()) {
                        fileUrl = "file:///" + template.getAbsolutePath();
                    }
                } catch (Exception e) {
                    PlayHaven.e(e);
                }

                if (fileUrl != null && fileUrl.length() > 1) {
                    PlayHaven.v("Loading from cache: %s.", fileUrl);
                    HTMLView.this.loadUrl(fileUrl);
                } else {
                    HTMLView.this.loadUrl(url);
                }

                setBackgroundColor(0x00000000);
                /**
                 * @playhaven.apihack WebView has flickering & transparency issues when hardware acceleration is enabled.
                 */
                if (Build.VERSION.SDK_INT >= 11) {
                    setLayerType(View.LAYER_TYPE_SOFTWARE, null);
                }
            }
        });
    }

    @SuppressLint("SetJavaScriptEnabled")
    @Override
    public void setPlacement(Placement placement) {
        mPlacement = placement;
        this.getSettings().setJavaScriptEnabled(true);
        this.setWebViewClient(webViewClient);
        this.setWebChromeClient(webChromeClient);

        try {
            // If loading new placement into existing view, start over.
            mImages = null;

            /**
             * @playhaven.apihack After API 11, we can intercept requests for images.
             */
            if (Build.VERSION.SDK_INT >= 11) {
                mImages = JsonUrlExtractor.getImages(mPlacement.getModel());
            }
        } finally {
            if (mImages == null)
                mImages = new ArrayList<String>();
        }

        load(JsonUtil.<String>getPath(mPlacement.getModel(), "$.response.url"));
    }

    /**
     * Create a response bundle for passing back to the publisher
     *
     * @return bundle containing data
     */
    @Override
    public Bundle generateResponseBundle() {
        Bundle data = null;

        if (mRewards != null) {
            data = new Bundle();
            data.putParcelableArrayList(PlayHavenView.BUNDLE_DATA_REWARD, mRewards);
        }

        if (mDataFields != null) {
            if (data == null)
                data = new Bundle();
            data.putParcelableArrayList(PlayHavenView.BUNDLE_DATA_OPTIN, mDataFields);
        }

        if (mPurchases != null) {
            if (data == null)
                data = new Bundle();
            data.putParcelableArrayList(PlayHavenView.BUNDLE_DATA_PURCHASE, mPurchases);
        }

        return data;
    }

}