/* * Copyright 2018 Google LLC * * 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.google.ads.consent; import android.annotation.TargetApi; import android.app.Dialog; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.text.TextUtils; import android.util.Base64; import android.view.ViewGroup; import android.webkit.WebResourceError; import android.webkit.WebResourceRequest; import android.webkit.WebView; import android.webkit.WebViewClient; import com.google.gson.Gson; import java.io.ByteArrayOutputStream; import java.net.URL; import java.util.HashMap; /** * A Google rendered form for collecting consent from a user. */ public class ConsentForm { private final ConsentFormListener listener; private final Context context; private final boolean personalizedAdsOption; private final boolean nonPersonalizedAdsOption; private final boolean adFreeOption; private final URL appPrivacyPolicyURL; private final Dialog dialog; private final WebView webView; private LoadState loadState; private enum LoadState { NOT_READY, LOADING, LOADED } private ConsentForm(Builder builder) { this.context = builder.context; if (builder.listener == null) { this.listener = new ConsentFormListener() {}; } else { this.listener = builder.listener; } this.personalizedAdsOption = builder.personalizedAdsOption; this.nonPersonalizedAdsOption = builder.nonPersonalizedAdsOption; this.adFreeOption = builder.adFreeOption; this.appPrivacyPolicyURL = builder.appPrivacyPolicyURL; this.dialog = new Dialog(context, android.R.style.Theme_Translucent_NoTitleBar); this.loadState = LoadState.NOT_READY; this.webView = new WebView(context); this.webView.setBackgroundColor(Color.TRANSPARENT); this.dialog.setContentView(webView); this.dialog.setCancelable(false); webView.getSettings().setJavaScriptEnabled(true); webView.setWebViewClient( new WebViewClient() { boolean isInternalRedirect; private boolean isConsentFormUrl(String url) { return !TextUtils.isEmpty(url) && url.startsWith("consent://"); } private void handleUrl(String url) { if (!isConsentFormUrl(url)) { return; } isInternalRedirect = true; Uri uri = Uri.parse(url); String action = uri.getQueryParameter("action"); String status = uri.getQueryParameter("status"); String browserUrl = uri.getQueryParameter("url"); switch (action) { case "load_complete": handleLoadComplete(status); break; case "dismiss": isInternalRedirect = false; handleDismiss(status); break; case "browser": handleOpenBrowser(browserUrl); break; default: // fall out } } @Override public void onLoadResource (WebView view, String url) { handleUrl(url); } @TargetApi(Build.VERSION_CODES.N) @Override public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { String url = request.getUrl().toString(); if (isConsentFormUrl(url)) { handleUrl(url); return true; } return false; } @SuppressWarnings("deprecation") @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if (isConsentFormUrl(url)) { handleUrl(url); return true; } return false; } @Override public void onPageFinished(WebView view, String url) { if (!isInternalRedirect) { updateDialogContent(view); } super.onPageFinished(view, url); } @Override public void onReceivedError( WebView view, WebResourceRequest request, WebResourceError error) { super.onReceivedError(view, request, error); loadState = LoadState.NOT_READY; listener.onConsentFormError(error.toString()); } }); } /** * Creates a new {@link Builder} for constructing a {@link ConsentForm}. */ public static class Builder { private final Context context; private ConsentFormListener listener; private boolean personalizedAdsOption; private boolean nonPersonalizedAdsOption; private boolean adFreeOption; private final URL appPrivacyPolicyURL; public Builder(Context context, URL appPrivacyPolicyURL) { this.context = context; this.personalizedAdsOption = false; this.nonPersonalizedAdsOption = false; this.adFreeOption = false; this.appPrivacyPolicyURL = appPrivacyPolicyURL; if (this.appPrivacyPolicyURL == null) { throw new IllegalArgumentException("Must provide valid app privacy policy url" + " to create a ConsentForm"); } } public Builder withListener(ConsentFormListener listener) { this.listener = listener; return this; } public Builder withPersonalizedAdsOption() { this.personalizedAdsOption = true; return this; } public Builder withNonPersonalizedAdsOption() { this.nonPersonalizedAdsOption = true; return this; } public Builder withAdFreeOption() { this.adFreeOption = true; return this; } public ConsentForm build() { return new ConsentForm(this); } } private static String getApplicationName(Context context) { return context.getApplicationInfo().loadLabel(context.getPackageManager()).toString(); } private static String getAppIconURIString(Context context) { Drawable iconDrawable = context.getPackageManager().getApplicationIcon(context .getApplicationInfo()); Bitmap bitmap = Bitmap.createBitmap(iconDrawable.getIntrinsicWidth(), iconDrawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); final Canvas canvas = new Canvas(bitmap); iconDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); iconDrawable.draw(canvas); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream); byte[] byteArray = byteArrayOutputStream.toByteArray(); return "data:image/png;base64," + Base64.encodeToString(byteArray, Base64.DEFAULT); } private static String createJavascriptCommand(String command, String argumentsJSON) { HashMap <String, Object> args = new HashMap < > (); args.put("info", argumentsJSON); HashMap <String, Object> wrappedArgs = new HashMap < > (); wrappedArgs.put("args", args); return String.format("javascript:%s(%s)", command, new Gson().toJson(wrappedArgs)); } private void updateDialogContent(WebView webView) { HashMap <String, Object> formInfo = new HashMap < > (); formInfo.put("app_name", getApplicationName(context)); formInfo.put("app_icon", getAppIconURIString(context)); formInfo.put("offer_personalized", this.personalizedAdsOption); formInfo.put("offer_non_personalized", this.nonPersonalizedAdsOption); formInfo.put("offer_ad_free", this.adFreeOption); formInfo.put("is_request_in_eea_or_unknown", ConsentInformation.getInstance(context).isRequestLocationInEeaOrUnknown()); formInfo.put("app_privacy_url", this.appPrivacyPolicyURL); ConsentData consentData = ConsentInformation.getInstance(context).loadConsentData(); formInfo.put("plat", consentData.getSDKPlatformString()); formInfo.put("consent_info", consentData); String argumentsJSON = new Gson().toJson(formInfo); String javascriptCommand = createJavascriptCommand("setUpConsentDialog", argumentsJSON); webView.loadUrl(javascriptCommand); } public void load() { if (this.loadState == LoadState.LOADING) { listener.onConsentFormError("Cannot simultaneously load multiple consent forms."); return; } if (this.loadState == LoadState.LOADED) { listener.onConsentFormLoaded(); return; } this.loadState = LoadState.LOADING; this.webView.loadUrl("file:///android_asset/consentform.html"); } private void handleLoadComplete(String status) { if (TextUtils.isEmpty(status)) { this.loadState = LoadState.NOT_READY; listener.onConsentFormError("No information"); } else if (status.contains("Error")) { this.loadState = LoadState.NOT_READY; listener.onConsentFormError(status); } else { this.loadState = LoadState.LOADED; listener.onConsentFormLoaded(); } } private void handleOpenBrowser(String urlString) { if (TextUtils.isEmpty(urlString)) { listener.onConsentFormError("No valid URL for browser navigation."); return; } try { Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(urlString)); context.startActivity(browserIntent); } catch (ActivityNotFoundException exception) { listener.onConsentFormError("No Activity found to handle browser intent."); } } private void handleDismiss(String status) { this.loadState = LoadState.NOT_READY; dialog.dismiss(); if (TextUtils.isEmpty(status)) { listener.onConsentFormError("No information provided."); return; } if (status.contains("Error")) { listener.onConsentFormError(status); return; } boolean userPrefersAdFree = false; ConsentStatus consentStatus; switch (status) { case "personalized": consentStatus = ConsentStatus.PERSONALIZED; break; case "non_personalized": consentStatus = ConsentStatus.NON_PERSONALIZED; break; case "ad_free": userPrefersAdFree = true; consentStatus = ConsentStatus.UNKNOWN; break; default: consentStatus = ConsentStatus.UNKNOWN; } ConsentInformation.getInstance(context).setConsentStatus(consentStatus, "form"); listener.onConsentFormClosed(consentStatus, userPrefersAdFree); } public void show() { if (this.loadState != LoadState.LOADED) { listener.onConsentFormError("Consent form is not ready to be displayed."); return; } if (ConsentInformation.getInstance(context).isTaggedForUnderAgeOfConsent()) { listener.onConsentFormError("Error: tagged for under age of consent"); return; } this.dialog.getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); this.dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); this.dialog.setOnShowListener(new DialogInterface.OnShowListener() { @Override public void onShow(DialogInterface dialog) { listener.onConsentFormOpened(); } }); this.dialog.show(); if (!this.dialog.isShowing()) { listener.onConsentFormError("Consent form could not be displayed."); } } public boolean isShowing() { return this.dialog.isShowing(); } }