-
Notifications
You must be signed in to change notification settings - Fork 278
/
WebViewFallbackActivity.java
331 lines (291 loc) · 13.5 KB
/
WebViewFallbackActivity.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
// Copyright 2019 Google Inc. All Rights Reserved.
//
// 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.androidbrowserhelper.trusted;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Color;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.webkit.RenderProcessGoneDetail;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceRequest;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.FrameLayout;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.core.content.ContextCompat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class WebViewFallbackActivity extends Activity {
private static final String TAG = WebViewFallbackActivity.class.getSimpleName();
private static final String KEY_PREFIX =
"com.google.browser.examples.twawebviewfallback.WebViewFallbackActivity.";
private static final String KEY_LAUNCH_URI = KEY_PREFIX + "LAUNCH_URL";
private static final String KEY_NAVIGATION_BAR_COLOR = KEY_PREFIX + "KEY_NAVIGATION_BAR_COLOR";
private static final String KEY_STATUS_BAR_COLOR = KEY_PREFIX + "KEY_STATUS_BAR_COLOR";
private static final String KEY_EXTRA_ORIGINS = KEY_PREFIX + "KEY_EXTRA_ORIGINS";
private Uri mLaunchUrl;
private int mStatusBarColor;
private WebView mWebView;
private List<Uri> mExtraOrigins = new ArrayList<>();
public static Intent createLaunchIntent(
Context context,
Uri launchUrl,
LauncherActivityMetadata launcherActivityMetadata) {
Intent intent = new Intent(context, WebViewFallbackActivity.class);
intent.putExtra(WebViewFallbackActivity.KEY_LAUNCH_URI, launchUrl);
intent.putExtra(WebViewFallbackActivity.KEY_STATUS_BAR_COLOR,
ContextCompat.getColor(context, launcherActivityMetadata.statusBarColorId));
intent.putExtra(WebViewFallbackActivity.KEY_NAVIGATION_BAR_COLOR,
ContextCompat.getColor(context, launcherActivityMetadata.navigationBarColorId));
if (launcherActivityMetadata.additionalTrustedOrigins != null) {
ArrayList<String> extraOrigins =
new ArrayList<>(launcherActivityMetadata.additionalTrustedOrigins);
intent.putStringArrayListExtra(WebViewFallbackActivity.KEY_EXTRA_ORIGINS, extraOrigins);
}
return intent;
}
@SuppressLint("SetJavaScriptEnabled")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.mLaunchUrl = this.getIntent().getParcelableExtra(KEY_LAUNCH_URI);
if (!"https".equals(this.mLaunchUrl.getScheme())) {
throw new IllegalArgumentException("launchUrl scheme must be 'https'");
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (getIntent().hasExtra(KEY_NAVIGATION_BAR_COLOR)) {
int navigationBarColor = this.getIntent().getIntExtra(KEY_NAVIGATION_BAR_COLOR, 0);
getWindow().setNavigationBarColor(navigationBarColor);
}
}
if (getIntent().hasExtra(KEY_STATUS_BAR_COLOR)) {
mStatusBarColor = this.getIntent().getIntExtra(KEY_STATUS_BAR_COLOR, 0);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
getWindow().setStatusBarColor(mStatusBarColor);
}
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mStatusBarColor = getWindow().getStatusBarColor();
} else {
mStatusBarColor = Color.WHITE;
}
}
if (getIntent().hasExtra(KEY_EXTRA_ORIGINS)) {
List<String> extraOrigins = getIntent().getStringArrayListExtra(KEY_EXTRA_ORIGINS);
if (extraOrigins != null) {
for (String extraOrigin : extraOrigins) {
Uri extraOriginUri = Uri.parse(extraOrigin);
if (!"https".equalsIgnoreCase(extraOriginUri.getScheme())) {
Log.w(TAG, "Only 'https' origins are accepted. Ignoring extra origin: "
+ extraOrigin);
continue;
}
mExtraOrigins.add(extraOriginUri);
}
}
}
mWebView = new WebView(this);
mWebView.setWebViewClient(createWebViewClient());
mWebView.setWebChromeClient(createWebViewChromeClient());
WebSettings webSettings = mWebView.getSettings();
setupWebSettings(webSettings);
ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
setContentView(mWebView, layoutParams);
if (savedInstanceState != null) {
mWebView.restoreState(savedInstanceState);
return;
}
// Applications running in a Trusted Web Activity are supposed to have
// android-app://<package-name> as the referrer.
Map<String, String> headers = new HashMap<>();
headers.put("Referer", "android-app://" + getPackageName() + "/");
mWebView.loadUrl(mLaunchUrl.toString(), headers);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if ((keyCode == KeyEvent.KEYCODE_BACK) && mWebView.canGoBack()) {
mWebView.goBack();
return true;
}
return super.onKeyDown(keyCode, event);
}
@Override
protected void onPause() {
super.onPause();
if (mWebView != null) {
mWebView.onPause();
}
}
@Override
protected void onResume() {
super.onResume();
if (mWebView != null) {
mWebView.onResume();
}
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
if (mWebView != null) {
mWebView.saveState(outState);
}
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
private WebViewClient createWebViewClient() {
return new WebViewClient() {
@Override
public boolean onRenderProcessGone(
WebView view, RenderProcessGoneDetail detail) {
ViewGroup vg = (ViewGroup) view.getParent();
// Remove crashed WebView from the hierarchy
// and ensure it is destroyed.
vg.removeView(view);
view.destroy();
// Create a new instance, and ensure it also
// handles crashes - in this case, re-using
// the current WebViewClient
mWebView = new WebView(view.getContext());
mWebView.setWebViewClient(this);
WebSettings webSettings = mWebView.getSettings();
setupWebSettings(webSettings);
vg.addView(mWebView);
// With the crash recovered, decide what to do next.
// We are sending a toast and loading the origin
// URL, in this example.
Toast.makeText(view.getContext(), "Recovering from crash",
Toast.LENGTH_LONG).show();
mWebView.loadUrl(mLaunchUrl.toString());
return true;
}
private boolean shouldOverrideUrlLoading(Uri navigationUrl) {
Uri launchUrl = WebViewFallbackActivity.this.mLaunchUrl;
// If the user is navigation to a different origin, use CCT to handle the navigation
//
// URIs with the `data` scheme are handled in the WebView.
// The "Demo" item in https://jakearchibald.github.io/svgomg/ is one example of this
// usage
if (!"data".equals(navigationUrl.getScheme()) &&
!uriOriginsMatch(navigationUrl, launchUrl) &&
!matchExtraOrigins(navigationUrl)) {
// A Custom Tab is an ACTION_VIEW Intent with special extras that cause the
// handler for that Intent to change its behaviour. This Intent should be
// able to trigger both browsers or platform-specific apps that handle those
// Intents.
// However, some URLs, like the data:// schema must be handled by the
// WebView itself. If an URL can't be handled by any app we allow the WebView
// to try to handle it.
try {
CustomTabsIntent intent = new CustomTabsIntent.Builder()
.setToolbarColor(mStatusBarColor)
.build();
intent.launchUrl(WebViewFallbackActivity.this, navigationUrl);
return true;
} catch (ActivityNotFoundException ex) {
Log.e(TAG, String.format(
"ActivityNotFoundException while launching '%s'", navigationUrl));
return false;
}
}
return false;
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
return this.shouldOverrideUrlLoading(Uri.parse(url));
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
return this.shouldOverrideUrlLoading(request.getUrl());
}
private boolean matchExtraOrigins(Uri navigationUri) {
for (Uri uri : mExtraOrigins) {
if (uriOriginsMatch(uri, navigationUri)) {
return true;
}
}
return false;
}
private boolean uriOriginsMatch(Uri uriA, Uri uriB) {
return uriA.getScheme().equalsIgnoreCase(uriB.getScheme()) &&
uriA.getHost().equalsIgnoreCase(uriB.getHost()) &&
uriA.getPort() == uriB.getPort();
}
};
}
private WebChromeClient createWebViewChromeClient() {
return new WebChromeClient() {
private View fullScreenView;
private int originalOrientation;
@Override
public void onShowCustomView(View paramView, CustomViewCallback paramCustomViewCallback) {
// Make sure that we don't have another fullscreen shown already.
if (this.fullScreenView != null) {
onHideCustomView();
}
// Save the fullscreen view in order to be able to remove it later when requested.
this.fullScreenView = paramView;
// Save the current orientation in order to be able to return the state after
// exiting the fullscreen.
this.originalOrientation = getRequestedOrientation();
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().addContentView(this.fullScreenView,
new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER));
}
@Override
public void onHideCustomView() {
// If we don't have a fullscreen then no-op.
if (fullScreenView == null) {
return;
}
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
((ViewGroup) fullScreenView.getParent()).removeView(fullScreenView);
this.fullScreenView = null;
setRequestedOrientation(this.originalOrientation);
}
};
}
@SuppressLint("SetJavaScriptEnabled")
private static void setupWebSettings(WebSettings webSettings) {
// Those settings are disabled by default.
webSettings.setJavaScriptEnabled(true);
webSettings.setDomStorageEnabled(true);
webSettings.setDatabaseEnabled(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
webSettings.setMediaPlaybackRequiresUserGesture(false);
}
}
}