1 /*
2  * Copyright 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 package com.example.androidx.webkit;
17 
18 import android.annotation.SuppressLint;
19 import android.app.Activity;
20 import android.net.Uri;
21 import android.os.Bundle;
22 import android.text.SpannableString;
23 import android.text.Spanned;
24 import android.text.TextUtils;
25 import android.text.style.AbsoluteSizeSpan;
26 import android.webkit.WebView;
27 import android.webkit.WebViewClient;
28 import android.widget.ArrayAdapter;
29 import android.widget.CheckBox;
30 import android.widget.Spinner;
31 import android.widget.TextView;
32 
33 import androidx.appcompat.app.AppCompatActivity;
34 import androidx.webkit.WebMessageCompat;
35 import androidx.webkit.WebMessagePortCompat;
36 import androidx.webkit.WebViewCompat;
37 import androidx.webkit.WebViewFeature;
38 
39 import com.google.common.base.Charsets;
40 import com.google.common.io.ByteStreams;
41 
42 import org.jspecify.annotations.NonNull;
43 import org.jspecify.annotations.Nullable;
44 
45 import java.io.IOException;
46 import java.io.InputStream;
47 import java.nio.ByteBuffer;
48 
49 /**
50  * An {@link Activity} to exercise WebMessageCompat related functionality.
51  */
52 public class WebMessageCompatActivity extends AppCompatActivity {
53     private static final String TYPE_STRING = "String";
54     private static final String TYPE_ARRAY_BUFFER = "ArrayBuffer";
55     private static final String[] MESSAGE_TYPES = {TYPE_STRING, TYPE_ARRAY_BUFFER};
56     private static final boolean ARRAY_BUFFER_FEATURE_ENABLED =
57             WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER);
58 
59     private WebView mWebView;
60     private TextView mTextView, mPerfTextView;
61     private CheckBox mCheckBox;
62     private WebMessagePortCompat mPort;
63     private Spinner mSpinner;
64     private int mMessageCount = 0;
65     private int mExpectedCount = 0;
66     private long mTimeStamp;
67 
createNativeTitle()68     static CharSequence createNativeTitle() {
69         final String title = "Native View\n";
70         SpannableString ss = new SpannableString(title);
71         ss.setSpan(new AbsoluteSizeSpan(55, true), 0, title.length() - 1,
72                 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
73         return ss;
74     }
75 
76     @SuppressLint("SetJavascriptEnabled")
77     @Override
onCreate(@ullable Bundle savedInstanceState)78     protected void onCreate(@Nullable Bundle savedInstanceState) {
79         super.onCreate(savedInstanceState);
80         setContentView(R.layout.activity_web_message_compat);
81         setTitle(R.string.web_message_compat_activity_title);
82         WebkitHelpers.appendWebViewVersionToTitle(this);
83         if (!WebViewFeature.isFeatureSupported(WebViewFeature.POST_WEB_MESSAGE)) {
84             WebkitHelpers.showMessageInActivity(WebMessageCompatActivity.this,
85                     R.string.webkit_api_not_available);
86             return;
87         }
88 
89         mWebView = findViewById(R.id.webview);
90         mWebView.getSettings().setJavaScriptEnabled(true);
91         mWebView.setWebViewClient(new MyWebViewClient());
92         mTextView = findViewById(R.id.textview);
93         mPerfTextView = findViewById(R.id.textview_perf);
94         mCheckBox = findViewById(R.id.checkbox_window_message);
95         mSpinner = findViewById(R.id.message_type_spinner);
96         final ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
97                 android.R.layout.simple_spinner_dropdown_item, MESSAGE_TYPES);
98         mSpinner.setAdapter(adapter);
99         // If GET_PAYLOAD feature is not supported, disable the type selection spinner.
100         mSpinner.setEnabled(ARRAY_BUFFER_FEATURE_ENABLED);
101 
102         try (InputStream is = getAssets().open("www/web_message_compat.html")) {
103             String webContent = new String(ByteStreams.toByteArray(is), Charsets.UTF_8);
104             mWebView.loadDataWithBaseURL("https://example.com", webContent, "text/html", null,
105                     null);
106         } catch (IOException e) {
107             throw new RuntimeException(e);
108         }
109         findViewById(R.id.button_send).setOnClickListener(view -> {
110             mExpectedCount = mMessageCount + 5000;
111             mTimeStamp = System.currentTimeMillis();
112             mSpinner.setEnabled(false);
113             sendMessage();
114         });
115     }
116 
sendMessage()117     private void sendMessage() {
118         if (mMessageCount >= mExpectedCount) {
119             return;
120         }
121         final String selectedType = (String) mSpinner.getSelectedItem();
122         final WebMessageCompat message;
123         switch (selectedType) {
124             case TYPE_STRING:
125                 message = new WebMessageCompat(String.valueOf(mMessageCount + 1));
126                 break;
127             case TYPE_ARRAY_BUFFER:
128                 byte[] bytes =
129                         ByteBuffer.allocate(Integer.BYTES).putInt(mMessageCount + 1).array();
130                 message = new WebMessageCompat(bytes);
131                 break;
132             default:
133                 // Should never happen.
134                 throw new RuntimeException("Invalid type.");
135         }
136         if (mCheckBox.isChecked()) {
137             WebViewCompat.postWebMessage(mWebView, message, Uri.EMPTY);
138         } else {
139             mPort.postMessage(message);
140         }
141     }
142 
refreshNativeText()143     private void refreshNativeText() {
144         mTextView.setText(TextUtils.concat(createNativeTitle(), String.valueOf(mMessageCount),
145                 " messages received"));
146     }
147 
refreshPerfText()148     private void refreshPerfText() {
149         mPerfTextView.setText("Average time over 5000 messages: "
150                 + (System.currentTimeMillis() - mTimeStamp) / 5000.0 + " ms");
151     }
152 
setupMessagePort()153     private void setupMessagePort() {
154         WebMessagePortCompat[] ports = WebViewCompat.createWebMessageChannel(mWebView);
155         WebViewCompat.postWebMessage(mWebView,
156                 new WebMessageCompat("setup", new WebMessagePortCompat[]{ports[0]}), Uri.EMPTY);
157         mPort = ports[1];
158         mPort.setWebMessageCallback(new WebMessagePortCompat.WebMessageCallbackCompat() {
159             @Override
160             public void onMessage(@NonNull WebMessagePortCompat port,
161                     @Nullable WebMessageCompat message) {
162                 switch (message.getType()) {
163                     case WebMessageCompat.TYPE_STRING:
164                         mMessageCount = Integer.parseInt(message.getData());
165                         break;
166                     case WebMessageCompat.TYPE_ARRAY_BUFFER:
167                         mMessageCount = ByteBuffer.wrap(message.getArrayBuffer()).getInt();
168                         break;
169                     default:
170                         // Should never happen
171                         throw new RuntimeException("Invalid type: " + message.getType());
172                 }
173                 if (mMessageCount % 100 == 0) {
174                     refreshNativeText();
175                 }
176                 if (mMessageCount == mExpectedCount) {
177                     refreshPerfText();
178                     mSpinner.setEnabled(ARRAY_BUFFER_FEATURE_ENABLED);
179                 }
180                 sendMessage();
181             }
182         });
183     }
184 
185     private class MyWebViewClient extends WebViewClient {
186         @Override
onPageFinished(WebView view, String url)187         public void onPageFinished(WebView view, String url) {
188             super.onPageFinished(view, url);
189             setupMessagePort();
190             view.setWebViewClient(null);
191         }
192     }
193 }
194