1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.ondevicepersonalization.services.display; 18 19 import android.adservices.ondevicepersonalization.RenderOutputParcel; 20 import android.adservices.ondevicepersonalization.RequestLogRecord; 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.content.ComponentName; 24 import android.content.Context; 25 import android.content.pm.PackageManager; 26 import android.hardware.display.DisplayManager; 27 import android.os.IBinder; 28 import android.os.PersistableBundle; 29 import android.view.Display; 30 import android.view.SurfaceControlViewHost; 31 import android.view.SurfaceControlViewHost.SurfacePackage; 32 import android.view.WindowManager; 33 import android.webkit.WebSettings; 34 import android.webkit.WebView; 35 36 import com.android.odp.module.common.PackageUtils; 37 import com.android.ondevicepersonalization.internal.util.LoggerFactory; 38 import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors; 39 import com.android.ondevicepersonalization.services.data.vendor.OnDevicePersonalizationVendorDataDao; 40 import com.android.ondevicepersonalization.services.display.velocity.VelocityEngineFactory; 41 42 import com.google.common.util.concurrent.ListenableFuture; 43 import com.google.common.util.concurrent.SettableFuture; 44 45 import org.apache.velocity.Template; 46 import org.apache.velocity.app.VelocityEngine; 47 48 import java.io.File; 49 import java.io.IOException; 50 import java.io.PrintWriter; 51 import java.io.StringWriter; 52 import java.nio.charset.StandardCharsets; 53 54 /** Helper class to display personalized content. */ 55 public class DisplayHelper { 56 private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger(); 57 private static final String TAG = "DisplayHelper"; 58 @NonNull private final Context mContext; 59 DisplayHelper(Context context)60 public DisplayHelper(Context context) { 61 mContext = context; 62 } 63 64 /** Generates an HTML string from the template data in RenderOutputParcel. */ generateHtml( @onNull RenderOutputParcel renderContentResult, @NonNull ComponentName service)65 @NonNull public String generateHtml( 66 @NonNull RenderOutputParcel renderContentResult, 67 @NonNull ComponentName service) { 68 // If htmlContent is provided, do not render the template. 69 String htmlContent = renderContentResult.getContent(); 70 if (null != htmlContent && !htmlContent.isEmpty()) { 71 return htmlContent; 72 } 73 PersistableBundle templateParams = renderContentResult.getTemplateParams(); 74 String templateId = renderContentResult.getTemplateId(); 75 if (null == templateParams || null == templateId) { 76 throw new IllegalArgumentException( 77 "Valid rendering output not provided for generateHtml"); 78 } 79 try { 80 byte[] templateBytes = OnDevicePersonalizationVendorDataDao.getInstance( 81 mContext, 82 service, 83 PackageUtils.getCertDigest(mContext, service.getPackageName())) 84 .readSingleVendorDataRow(templateId); 85 if (null == templateBytes) { 86 throw new IllegalArgumentException( 87 "Provided templateId not found during generateHtml"); 88 } 89 String templateContent = new String(templateBytes, StandardCharsets.UTF_8); 90 // Move the template into a temp file to pass to Velocity. 91 String templateFileName = createTempTemplateFile( 92 templateContent, service.getPackageName()); 93 VelocityEngine ve = VelocityEngineFactory.getVelocityEngine(mContext); 94 Template template = ve.getTemplate(templateFileName); 95 org.apache.velocity.context.Context ctx = 96 VelocityEngineFactory.createVelocityContext(templateParams); 97 98 StringWriter writer = new StringWriter(); 99 template.merge(ctx, writer); 100 return writer.toString(); 101 } catch (PackageManager.NameNotFoundException | IOException e) { 102 throw new RuntimeException(e); 103 } 104 } 105 createTempTemplateFile(String templateContent, String templateId)106 private String createTempTemplateFile(String templateContent, String templateId) 107 throws IOException { 108 File temp = File.createTempFile(templateId, ".vm", mContext.getCacheDir()); 109 try (PrintWriter out = new PrintWriter(temp)) { 110 out.print(templateContent); 111 } 112 temp.deleteOnExit(); 113 return temp.getName(); 114 } 115 116 /** Creates a webview and displays the provided HTML. */ displayHtml( @onNull String html, @Nullable RequestLogRecord logRecord, long queryId, @NonNull ComponentName service, @NonNull IBinder hostToken, int displayId, int width, int height)117 @NonNull public ListenableFuture<SurfacePackage> displayHtml( 118 @NonNull String html, @Nullable RequestLogRecord logRecord, 119 long queryId, @NonNull ComponentName service, 120 @NonNull IBinder hostToken, int displayId, int width, int height) { 121 SettableFuture<SurfacePackage> result = SettableFuture.create(); 122 try { 123 sLogger.d(TAG + ": displayHtml"); 124 OnDevicePersonalizationExecutors.getHandlerForMainThread().post(() -> { 125 createWebView(html, logRecord, queryId, service, 126 hostToken, displayId, width, height, result); 127 }); 128 } catch (Exception e) { 129 result.setException(e); 130 } 131 return result; 132 } 133 createWebView( @onNull String html, @Nullable RequestLogRecord logRecord, long queryId, @NonNull ComponentName service, @NonNull IBinder hostToken, int displayId, int width, int height, @NonNull SettableFuture<SurfacePackage> resultFuture)134 private void createWebView( 135 @NonNull String html, @Nullable RequestLogRecord logRecord, long queryId, 136 @NonNull ComponentName service, 137 @NonNull IBinder hostToken, int displayId, int width, int height, 138 @NonNull SettableFuture<SurfacePackage> resultFuture) { 139 try { 140 sLogger.d(TAG + ": createWebView() started"); 141 Display display = mContext.getSystemService(DisplayManager.class).getDisplay(displayId); 142 Context displayContext = mContext.createDisplayContext(display); 143 Context windowContext = displayContext.createWindowContext( 144 WindowManager.LayoutParams.TYPE_APPLICATION_PANEL, null); 145 146 WebView webView = new WebView(windowContext); 147 webView.setWebViewClient( 148 new OdpWebViewClient(mContext, service, queryId, logRecord)); 149 WebSettings webViewSettings = webView.getSettings(); 150 // Do not allow using file:// or content:// URLs. 151 webViewSettings.setAllowFileAccess(false); 152 webViewSettings.setAllowContentAccess(false); 153 webView.loadData(html, "text/html; charset=utf-8", "UTF-8"); 154 155 SurfaceControlViewHost host = new SurfaceControlViewHost( 156 windowContext, display, hostToken); 157 host.setView(webView, width, height); 158 SurfacePackage surfacePackage = host.getSurfacePackage(); 159 sLogger.d(TAG + ": createWebView success: " + surfacePackage); 160 resultFuture.set(surfacePackage); 161 } catch (Exception e) { 162 sLogger.d(TAG + ": createWebView failed", e); 163 resultFuture.setException(e); 164 } 165 } 166 } 167