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