1 // Copyright (c) 2013-2014 Intel Corporation. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 package org.xwalk.core.internal;
7 import android.app.Activity;
8 import android.app.AlertDialog;
9 import android.app.ApplicationErrorReport;
10 import android.content.Context;
11 import android.content.DialogInterface;
12 import android.content.Intent;
13 import android.graphics.Rect;
14 import android.net.Uri;
15 import android.os.Bundle;
16 import android.os.Environment;
17 import android.os.Looper;
18 import android.util.AttributeSet;
19 import android.view.KeyEvent;
20 import android.view.ViewGroup;
21 import android.webkit.ValueCallback;
22 import android.widget.FrameLayout;
25 import java.io.PrintWriter;
26 import java.io.StringWriter;
27 import java.lang.ref.WeakReference;
28 import java.net.MalformedURLException;
31 import org.chromium.base.ActivityState;
32 import org.chromium.base.ApplicationStatus;
33 import org.chromium.base.ApplicationStatus.ActivityStateListener;
34 import org.chromium.base.CommandLine;
35 import org.xwalk.core.internal.extension.BuiltinXWalkExtensions;
38 * <p>XWalkViewInternal represents an Android view for web apps/pages. Thus most of attributes
39 * for Android view are valid for this class. Since it internally uses
40 * <a href="http://developer.android.com/reference/android/view/SurfaceView.html">
41 * android.view.SurfaceView</a> for rendering web pages by default, it can't be resized,
42 * rotated, transformed and animated due to the limitations of SurfaceView.
43 * Alternatively, if the preference key {@link XWalkPreferencesInternal#ANIMATABLE_XWALK_VIEW}
44 * is set to True, XWalkViewInternal can be transformed and animated because
45 * <a href="http://developer.android.com/reference/android/view/TextureView.html">
46 * TextureView</a> is intentionally used to render web pages for animation support.
47 * Besides, XWalkViewInternal won't be rendered if it's invisible.</p>
49 * <p>XWalkViewInternal needs hardware acceleration to render web pages. As a result, the
50 * AndroidManifest.xml of the caller's app must be appended with the attribute
51 * "android:hardwareAccelerated" and its value must be set as "true".</p>
53 * <application android:name="android.app.Application" android:label="XWalkUsers"
54 * android:hardwareAccelerated="true">
57 * <p>Crosswalk provides 2 major callback classes, namely {@link XWalkResourceClientInternal} and
58 * {@link XWalkUIClientInternal} for listening to the events related to resource loading and UI.
59 * By default, Crosswalk has a default implementation. Callers can override them if needed.</p>
61 * <p>Unlike other Android views, this class has to listen to system events like intents and activity result.
62 * The web engine inside this view need to get and handle them.
63 * With contianer activity's lifecycle change, XWalkViewInternal will pause all timers and other
64 * components like videos when activity paused, resume back them when activity resumed.
65 * When activity is about to destroy, XWalkViewInternal will destroy itself as well.
66 * Embedders can also call onHide() and pauseTimers() to explicitly pause XWalkViewInternal.
67 * Similarily with onShow(), resumeTimers() and onDestroy().
72 * import android.app.Activity;
73 * import android.os.Bundle;
75 * import org.xwalk.core.internal.XWalkResourceClientInternal;
76 * import org.xwalk.core.internal.XWalkUIClientInternal;
77 * import org.xwalk.core.internal.XWalkViewInternal;
79 * public class MyActivity extends Activity {
80 * XWalkViewInternal mXwalkView;
82 * class MyResourceClient extends XWalkResourceClientInternal {
83 * MyResourceClient(XWalkViewInternal view) {
88 * WebResourceResponse shouldInterceptLoadRequest(XWalkViewInternal view, String url) {
94 * class MyUIClient extends XWalkUIClientInternal {
95 * MyUIClient(XWalkViewInternal view) {
100 * void onFullscreenToggled(XWalkViewInternal view, String url) {
107 * protected void onCreate(Bundle savedInstanceState) {
108 * mXwalkView = new XWalkViewInternal(this, null);
109 * setContentView(mXwalkView);
110 * mXwalkView.setResourceClient(new MyResourceClient(mXwalkView));
111 * mXwalkView.setUIClient(new MyUIClient(mXwalkView));
112 * mXwalkView.load("http://www.crosswalk-project.org", null);
116 * protected void onActivityResult(int requestCode, int resultCode, Intent data) {
117 * if (mXwalkView != null) {
118 * mXwalkView.onActivityResult(requestCode, resultCode, data);
123 * protected void onNewIntent(Intent intent) {
124 * if (mXwalkView != null) {
125 * mXwalkView.onNewIntent(intent);
131 @XWalkAPI(extendClass = FrameLayout.class, createExternally = true)
132 public class XWalkViewInternal extends android.widget.FrameLayout {
134 private class XWalkActivityStateListener implements ActivityStateListener {
135 WeakReference<XWalkViewInternal> mXWalkViewRef;
137 XWalkActivityStateListener(XWalkViewInternal view) {
138 mXWalkViewRef = new WeakReference<XWalkViewInternal>(view);
142 public void onActivityStateChange(Activity activity, int newState) {
143 XWalkViewInternal view = mXWalkViewRef.get();
144 if (view == null) return;
145 view.onActivityStateChange(activity, newState);
149 static final String PLAYSTORE_DETAIL_URI = "market://details?id=";
151 private XWalkContent mContent;
152 private Activity mActivity;
153 private Context mContext;
154 private boolean mIsHidden;
155 private XWalkActivityStateListener mActivityStateListener;
158 * Normal reload mode as default.
162 public static final int RELOAD_NORMAL = 0;
164 * Reload mode with bypassing the cache.
168 public static final int RELOAD_IGNORE_CACHE = 1;
171 * Constructor for inflating via XML.
172 * @param context a Context object used to access application assets.
173 * @param attrs an AttributeSet passed to our parent.
176 @XWalkAPI(preWrapperLines = {
177 " super(${param1}, ${param2});"},
179 " if (bridge == null) return;",
180 " addView((FrameLayout)bridge, new FrameLayout.LayoutParams(",
181 " FrameLayout.LayoutParams.MATCH_PARENT,",
182 " FrameLayout.LayoutParams.MATCH_PARENT));"})
183 public XWalkViewInternal(Context context, AttributeSet attrs) {
184 super(convertContext(context), attrs);
187 mActivity = (Activity) context;
188 mContext = getContext();
189 init(mContext, attrs);
193 * Constructor for Crosswalk runtime. In shared mode, context isi
194 * different from activity. In embedded mode, they're same.
195 * @param context a Context object used to access application assets
196 * @param activity the activity for this XWalkViewInternal.
199 @XWalkAPI(preWrapperLines = {
200 " super(${param1}, null);"},
202 " if (bridge == null) return;",
203 " addView((FrameLayout)bridge, new FrameLayout.LayoutParams(",
204 " FrameLayout.LayoutParams.MATCH_PARENT,",
205 " FrameLayout.LayoutParams.MATCH_PARENT));"})
206 public XWalkViewInternal(Context context, Activity activity) {
207 super(convertContext(context), null);
210 // Make sure mActivity is initialized before calling 'init' method.
211 mActivity = activity;
212 mContext = getContext();
213 init(mContext, null);
216 private static Context convertContext(Context context) {
217 Context ret = context;
218 Context bridgeContext = ReflectionHelper.getBridgeContext();
219 if (bridgeContext == null || context == null ||
220 bridgeContext.getPackageName().equals(context.getPackageName())) {
221 // Not acrossing package
224 ret = new MixedContext(bridgeContext, context);
230 * Get the current activity passed from callers. It's never null.
231 * @return the activity instance passed from callers.
235 public Activity getActivity() {
236 if (mActivity != null) {
238 } else if (getContext() instanceof Activity) {
239 return (Activity)getContext();
242 // Never achieve here.
247 // TODO(yongsheng): we should remove this since we have getContext()?
251 public Context getViewContext() {
255 public void completeWindowCreation(XWalkViewInternal newXWalkView) {
256 mContent.supplyContentsForPopup(newXWalkView == null ? null : newXWalkView.mContent);
259 private void init(Context context, AttributeSet attrs) {
260 // Initialize chromium resources. Assign them the correct ids in
262 XWalkInternalResources.resetIds(context);
264 // Intialize library, paks and others.
266 XWalkViewDelegate.init(this);
267 mActivityStateListener = new XWalkActivityStateListener(this);
268 ApplicationStatus.registerStateListenerForActivity(
269 mActivityStateListener, getActivity());
270 } catch (Throwable e) {
271 // Try to find if there is UnsatisfiedLinkError in the cause chain of the met Throwable.
272 Throwable linkError = e;
274 if (linkError == null) throw new RuntimeException(e);
275 if (linkError instanceof UnsatisfiedLinkError) break;
276 if (linkError.getCause() == null ||
277 linkError.getCause().equals(linkError)) {
278 throw new RuntimeException(e);
280 linkError = linkError.getCause();
282 final UnsatisfiedLinkError err = (UnsatisfiedLinkError) linkError;
283 final Activity activity = getActivity();
284 final String packageName = context.getPackageName();
285 String missingArch = XWalkViewDelegate.isRunningOnIA() ? "Intel" : "ARM";
286 final String message =
287 context.getString(R.string.cpu_arch_mismatch_message, missingArch);
289 AlertDialog.Builder builder = new AlertDialog.Builder(activity);
290 builder.setTitle(R.string.cpu_arch_mismatch_title)
292 .setOnCancelListener(new DialogInterface.OnCancelListener() {
294 public void onCancel(DialogInterface dialog) {
297 }).setPositiveButton(R.string.goto_store_button_label,
298 new DialogInterface.OnClickListener() {
300 public void onClick(DialogInterface dialog, int which) {
301 activity.startActivity(new Intent(Intent.ACTION_VIEW,
302 Uri.parse(PLAYSTORE_DETAIL_URI + packageName)));
305 }).setNeutralButton(R.string.report_feedback_button_label,
306 new DialogInterface.OnClickListener() {
308 public void onClick(DialogInterface dialog, int which) {
309 ApplicationErrorReport report = new ApplicationErrorReport();
310 report.type = ApplicationErrorReport.TYPE_CRASH;
311 report.packageName = report.processName = packageName;
313 ApplicationErrorReport.CrashInfo crash =
314 new ApplicationErrorReport.CrashInfo();
315 crash.exceptionClassName = err.getClass().getSimpleName();
316 crash.exceptionMessage = "CPU architecture mismatch";
317 StringWriter writer = new StringWriter();
318 PrintWriter print = new PrintWriter(writer);
319 err.printStackTrace(print);
320 crash.stackTrace = writer.toString();
321 StackTraceElement stack = err.getStackTrace()[0];
322 crash.throwClassName = stack.getClassName();
323 crash.throwFileName = stack.getFileName();
324 crash.throwLineNumber = stack.getLineNumber();
325 crash.throwMethodName = stack.getMethodName();
327 report.crashInfo = crash;
328 report.systemApp = false;
329 report.time = System.currentTimeMillis();
331 Intent intent = new Intent(Intent.ACTION_APP_ERROR);
332 intent.putExtra(Intent.EXTRA_BUG_REPORT, report);
333 activity.startActivity(intent);
337 builder.create().show();
341 initXWalkContent(context, attrs);
344 private void initXWalkContent(Context context, AttributeSet attrs) {
346 mContent = new XWalkContent(context, attrs, this);
348 new FrameLayout.LayoutParams(
349 FrameLayout.LayoutParams.MATCH_PARENT,
350 FrameLayout.LayoutParams.MATCH_PARENT));
353 // Set default XWalkClientImpl.
354 setXWalkClient(new XWalkClient(this));
355 // Set default XWalkWebChromeClient and DownloadListener. The default actions
356 // are provided via the following clients if special actions are not needed.
357 setXWalkWebChromeClient(new XWalkWebChromeClient(this));
359 // Set with internal implementation. Could be overwritten by embedders'
361 setUIClient(new XWalkUIClientInternal(this));
362 setResourceClient(new XWalkResourceClientInternal(this));
364 setDownloadListener(new XWalkDownloadListenerImpl(context));
365 setNavigationHandler(new XWalkNavigationHandlerImpl(context));
366 setNotificationService(new XWalkNotificationServiceImpl(context, this));
368 if (!CommandLine.getInstance().hasSwitch("disable-xwalk-extensions")) {
369 BuiltinXWalkExtensions.load(context, getActivity());
371 XWalkPreferencesInternal.setValue(XWalkPreferencesInternal.ENABLE_EXTENSIONS, false);
374 XWalkPathHelper.initialize();
375 XWalkPathHelper.setCacheDirectory(
376 mContext.getApplicationContext().getCacheDir().getPath());
378 String state = Environment.getExternalStorageState();
379 if (Environment.MEDIA_MOUNTED.equals(state) ||
380 Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
381 File extCacheDir = mContext.getApplicationContext().getExternalCacheDir();
382 if (null != extCacheDir) {
383 XWalkPathHelper.setExternalCacheDirectory(extCacheDir.getPath());
389 * Load a web page/app from a given base URL or a content.
390 * If url is null or empty and content is null or empty, then this function
392 * If content is not null, load the web page/app from the content.
393 * If content is not null and the url is not set, return "about:blank" ifi
394 * calling {@link XWalkViewInternal#getUrl()}.
395 * If content is null, try to load the content from the url.
397 * It supports URL schemes like 'http:', 'https:' and 'file:'.
398 * It can also load files from Android assets, e.g. 'file:///android_asset/'.
399 * @param url the url for web page/app.
400 * @param content the content for the web page/app. Could be empty.
404 public void load(String url, String content) {
405 if (mContent == null) return;
407 mContent.loadUrl(url, content);
411 * Load a web app from a given manifest.json file. If content is not null,
412 * load the manifest.json from the content. If content is null, try to load
413 * the manifest.json from the url. Note that url should not be null if the
414 * launched path defined in manifest.json is relative.
416 * It supports URL schemes like 'http:', 'https:' and 'file:'.
417 * It can also load files from Android assets, e.g. 'file:///android_asset/'.
418 * @param url the url for manifest.json.
419 * @param content the content for manifest.json.
423 public void loadAppFromManifest(String url, String content) {
424 if (mContent == null) return;
426 mContent.loadAppFromManifest(url, content);
430 * Reload a web app with a given mode.
431 * @param mode the reload mode.
435 public void reload(int mode) {
436 if (mContent == null) return;
438 mContent.reload(mode);
442 * Stop current loading progress.
446 public void stopLoading() {
447 if (mContent == null) return;
449 mContent.stopLoading();
453 * Get the url of current web page/app. This may be different from what's passed
455 * @return the url for current web page/app.
459 public String getUrl() {
460 if (mContent == null) return null;
462 return mContent.getUrl();
466 * Get the title of current web page/app. This may be different from what's passed
468 * @return the title for current web page/app.
472 public String getTitle() {
473 if (mContent == null) return null;
475 return mContent.getTitle();
479 * Get the original url specified by caller.
480 * @return the original url.
484 public String getOriginalUrl() {
485 if (mContent == null) return null;
487 return mContent.getOriginalUrl();
491 * Get the navigation history for current XWalkViewInternal. It's synchronized with
492 * this XWalkViewInternal if any backward/forward and navigation operations.
493 * @return the navigation history.
497 public XWalkNavigationHistoryInternal getNavigationHistory() {
498 if (mContent == null) return null;
500 return mContent.getNavigationHistory();
504 * Injects the supplied Java object into this XWalkViewInternal.
505 * Each method defined in the class of the object should be
506 * marked with {@link JavascriptInterface} if it's called by JavaScript.
507 * @param object the supplied Java object, called by JavaScript.
508 * @param name the name injected in JavaScript.
512 public void addJavascriptInterface(Object object, String name) {
513 if (mContent == null) return;
515 mContent.addJavascriptInterface(object, name);
519 * Evaluate a fragment of JavaScript code and get the result via callback.
520 * @param script the JavaScript string.
521 * @param callback the callback to handle the evaluated result.
525 public void evaluateJavascript(String script, ValueCallback<String> callback) {
526 if (mContent == null) return;
528 mContent.evaluateJavascript(script, callback);
532 * Clear the resource cache. Note that the cache is per-application, so this
533 * will clear the cache for all XWalkViews used.
534 * @param includeDiskFiles indicate whether to clear disk files for cache.
538 public void clearCache(boolean includeDiskFiles) {
539 if (mContent == null) return;
541 mContent.clearCache(includeDiskFiles);
545 * Indicate that a HTML element is occupying the whole screen.
546 * @return true if any HTML element is occupying the whole screen.
550 public boolean hasEnteredFullscreen() {
551 if (mContent == null) return false;
553 return mContent.hasEnteredFullscreen();
557 * Leave fullscreen mode if it's. Do nothing if it's not
562 public void leaveFullscreen() {
563 if (mContent == null) return;
565 mContent.exitFullscreen();
569 * Pause all layout, parsing and JavaScript timers for all XWalkViewInternal instances.
570 * It will be called when the container Activity get paused. It can also be explicitly
571 * called to pause timers.
573 * Note that it will globally impact all XWalkViewInternal instances, not limited to
574 * just this XWalkViewInternal.
579 public void pauseTimers() {
580 if (mContent == null) return;
582 mContent.pauseTimers();
586 * Resume all layout, parsing and JavaScript timers for all XWalkViewInternal instances.
587 * It will be called when the container Activity get resumed. It can also be explicitly
588 * called to resume timers.
590 * Note that it will globally impact all XWalkViewInternal instances, not limited to
591 * just this XWalkViewInternal.
596 public void resumeTimers() {
597 if (mContent == null) return;
599 mContent.resumeTimers();
603 * Pause many other things except JavaScript timers inside rendering engine,
604 * like video player, modal dialogs, etc. See {@link #pauseTimers} about pausing
606 * It will be called when the container Activity get paused. It can also be explicitly
607 * called to pause above things.
611 public void onHide() {
612 if (mContent == null || mIsHidden) return;
618 * Resume video player, modal dialogs. Embedders are in charge of calling
619 * this during resuming this activity if they call onHide.
620 * Typically it should be called when the activity for this view is resumed.
621 * It will be called when the container Activity get resumed. It can also be explicitly
622 * called to resume above things.
626 public void onShow() {
627 if (mContent == null || !mIsHidden ) return;
633 * Release internal resources occupied by this XWalkViewInternal.
634 * It will be called when the container Activity get destroyed. It can also be explicitly
635 * called to release resources.
639 public void onDestroy() {
644 * Pass through activity result to XWalkViewInternal. Many internal facilities need this
645 * to handle activity result like JavaScript dialog, Crosswalk extensions, etc.
646 * See <a href="http://developer.android.com/reference/android/app/Activity.html">
647 * android.app.Activity.onActivityResult()</a>.
648 * @param requestCode passed from android.app.Activity.onActivityResult().
649 * @param resultCode passed from android.app.Activity.onActivityResult().
650 * @param data passed from android.app.Activity.onActivityResult().
654 public void onActivityResult(int requestCode, int resultCode, Intent data) {
655 if (mContent == null) return;
656 mContent.onActivityResult(requestCode, resultCode, data);
660 * Pass through intents to XWalkViewInternal. Many internal facilities need this
661 * to receive the intents like web notification. See
662 * <a href="http://developer.android.com/reference/android/app/Activity.html">
663 * android.app.Activity.onNewIntent()</a>.
664 * @param intent passed from android.app.Activity.onNewIntent().
668 public boolean onNewIntent(Intent intent) {
669 if (mContent == null) return false;
670 return mContent.onNewIntent(intent);
674 * Save current internal state of this XWalkViewInternal. This can help restore this state
675 * afterwards restoring.
676 * @param outState the saved state for restoring.
680 public boolean saveState(Bundle outState) {
681 if (mContent == null) return false;
682 mContent.saveState(outState);
687 * Restore the state from the saved bundle data.
688 * @param inState the state saved from saveState().
689 * @return true if it can restore the state.
693 public boolean restoreState(Bundle inState) {
694 if (mContent == null) return false;
695 if (mContent.restoreState(inState) != null) return true;
700 * Get the API version of Crosswalk embedding API.
701 * @return the string of API level.
704 // TODO(yongsheng): make it static?
706 public String getAPIVersion() {
711 * Get the Crosswalk version.
712 * @return the string of Crosswalk.
715 // TODO(yongsheng): make it static?
717 public String getXWalkVersion() {
718 if (mContent == null) return null;
719 return mContent.getXWalkVersion();
723 * Embedders use this to customize their handlers to events/callbacks related
725 * @param client the XWalkUIClientInternal defined by callers.
729 public void setUIClient(XWalkUIClientInternal client) {
730 if (mContent == null) return;
732 mContent.setUIClient(client);
736 * Embedders use this to customize their handlers to events/callbacks related
737 * to resource loading.
738 * @param client the XWalkResourceClientInternal defined by callers.
742 public void setResourceClient(XWalkResourceClientInternal client) {
743 if (mContent == null) return;
745 mContent.setResourceClient(client);
748 // TODO(yongsheng): this is not public.
752 public XWalkSettings getSettings() {
753 if (mContent == null) return null;
755 return mContent.getSettings();
759 * This method is used by Cordova for hacking.
760 * TODO(yongsheng): remove this and related test cases?
765 public void setNetworkAvailable(boolean networkUp) {
766 if (mContent == null) return;
768 mContent.setNetworkAvailable(networkUp);
772 * Enables remote debugging and returns the URL at which the dev tools
773 * server is listening for commands.
774 * The allowedUid argument can be used to specify the uid of the process
775 * that is permitted to connect.
777 public void enableRemoteDebugging(int allowedUid) {
778 if (mContent == null) return;
780 mContent.enableRemoteDebugging(allowedUid);
784 * Get the websocket url for remote debugging.
785 * @return the web socket url to remote debug this xwalk view.
786 * null will be returned if remote debugging is not enabled.
790 public URL getRemoteDebuggingUrl() {
791 if (mContent == null) return null;
793 String wsUrl = mContent.getRemoteDebuggingUrl();
794 if (wsUrl == null || wsUrl.isEmpty()) return null;
797 return new URL(wsUrl);
798 } catch (MalformedURLException e) {
804 * It's used for Presentation API.
807 public int getContentID() {
808 if (mContent == null) return -1;
809 return mContent.getRoutingID();
812 boolean canGoBack() {
813 if (mContent == null) return false;
815 return mContent.canGoBack();
819 if (mContent == null) return;
824 boolean canGoForward() {
825 if (mContent == null) return false;
827 return mContent.canGoForward();
831 if (mContent == null) return;
833 mContent.goForward();
836 void clearHistory() {
837 if (mContent == null) return;
839 mContent.clearHistory();
843 if (mContent == null) return;
844 ApplicationStatus.unregisterActivityStateListener(mActivityStateListener);
845 mActivityStateListener = null;
847 disableRemoteDebugging();
850 // Enables remote debugging and returns the URL at which the dev tools server is listening
851 // for commands. Only the current process is allowed to connect to the server.
852 void enableRemoteDebugging() {
853 enableRemoteDebugging(mContext.getApplicationInfo().uid);
856 void disableRemoteDebugging() {
857 if (mContent == null) return;
859 mContent.disableRemoteDebugging();
862 private static void checkThreadSafety() {
863 if (Looper.myLooper() != Looper.getMainLooper()) {
864 Throwable throwable = new Throwable(
865 "Warning: A XWalkViewInternal method was called on thread '" +
866 Thread.currentThread().getName() + "'. " +
867 "All XWalkViewInternal methods must be called on the UI thread. ");
868 throw new RuntimeException(throwable);
872 boolean isOwnerActivityRunning() {
873 int status = ApplicationStatus.getStateForActivity(getActivity());
874 if (status == ActivityState.DESTROYED) return false;
878 void navigateTo(int offset) {
879 if (mContent == null) return;
880 mContent.navigateTo(offset);
883 void setOverlayVideoMode(boolean enabled) {
884 mContent.setOverlayVideoMode(enabled);
887 // Below methods are for test shell and instrumentation tests.
891 public void setXWalkClient(XWalkClient client) {
892 if (mContent == null) return;
894 mContent.setXWalkClient(client);
900 public void setXWalkWebChromeClient(XWalkWebChromeClient client) {
901 if (mContent == null) return;
903 mContent.setXWalkWebChromeClient(client);
909 public void setDownloadListener(DownloadListener listener) {
910 if (mContent == null) return;
912 mContent.setDownloadListener(listener);
918 public void setNavigationHandler(XWalkNavigationHandler handler) {
919 if (mContent == null) return;
921 mContent.setNavigationHandler(handler);
927 public void setNotificationService(XWalkNotificationService service) {
928 if (mContent == null) return;
930 mContent.setNotificationService(service);
937 public boolean dispatchKeyEvent(KeyEvent event) {
938 if (event.getAction() == KeyEvent.ACTION_UP &&
939 event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
940 // If there's navigation happens when app is fullscreen,
941 // the content will still be fullscreen after navigation.
942 // In such case, the back key will exit fullscreen first.
943 if (hasEnteredFullscreen()) {
946 } else if (canGoBack()) {
951 return super.dispatchKeyEvent(event);
954 private void onActivityStateChange(Activity activity, int newState) {
955 assert(getActivity() == activity);
957 case ActivityState.STARTED:
960 case ActivityState.PAUSED:
963 case ActivityState.RESUMED:
966 case ActivityState.DESTROYED:
969 case ActivityState.STOPPED: