Java tutorial
/** * 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; } }