Bug 1455740 - Make GeckoViewExample more useful r=esawin,jchen,droeh draft
authorJames Willcox <snorp@snorp.net>
Fri, 30 Mar 2018 08:32:43 -0500
changeset 802295 279e7cb8bbbba0065bc263964997aac939eef99d
parent 801764 a9a50be22c48a420446b16b0bc70baf105e9d8d9
child 802296 9b076966f217ec4925fa41dc31a374eca0059c69
child 802398 022aeb62fe811cc6fe555d77d697329377f94286
push id111853
push userbmo:snorp@snorp.net
push dateThu, 31 May 2018 16:59:12 +0000
reviewersesawin, jchen, droeh
bugs1455740
milestone62.0a1
Bug 1455740 - Make GeckoViewExample more useful r=esawin,jchen,droeh This adds a location bar so you can enter arbitrary URLs, and also adds a menu for toggling various settings such as usage of E10s or tracking protection. MozReview-Commit-ID: FgBPR92dsfm
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestRunnerActivity.java
mobile/android/geckoview_example/build.gradle
mobile/android/geckoview_example/src/main/AndroidManifest.xml
mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/LocationView.java
mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/SessionActivity.java
mobile/android/geckoview_example/src/main/res/layout/geckoview_activity.xml
mobile/android/geckoview_example/src/main/res/menu/actions.xml
mobile/android/geckoview_example/src/main/res/values/strings.xml
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
@@ -51,10 +51,9 @@ class ContentDelegateTest : BaseSessionT
             override fun onExternalResponse(session: GeckoSession, response: GeckoSession.WebResponseInfo) {
                 assertThat("Uri should start with data:", response.uri, startsWith("data:"))
                 assertThat("Content type should match", response.contentType, equalTo("text/plain"))
                 assertThat("Content length should be non-zero", response.contentLength, greaterThan(0L))
                 assertThat("Filename should match", response.filename, equalTo("download.txt"))
             }
         })
     }
-
 }
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestRunnerActivity.java
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestRunnerActivity.java
@@ -89,20 +89,16 @@ public class TestRunnerActivity extends 
 
     private GeckoSession createSession() {
         return createSession(null);
     }
 
     private GeckoSession createSession(GeckoSessionSettings settings) {
         if (settings == null) {
             settings = new GeckoSessionSettings();
-
-            // We can't use e10s because we get deadlocked when quickly creating and
-            // destroying sessions. Bug 1348361.
-            settings.setBoolean(GeckoSessionSettings.USE_MULTIPROCESS, false);
         }
 
         final GeckoSession session = new GeckoSession(settings);
         session.setNavigationDelegate(mNavigationDelegate);
         return session;
     }
 
     @Override
--- a/mobile/android/geckoview_example/build.gradle
+++ b/mobile/android/geckoview_example/build.gradle
@@ -28,16 +28,17 @@ android {
     project.configureProductFlavors.delegate = it
     project.configureProductFlavors()
 }
 
 dependencies {
     testImplementation 'junit:junit:4.12'
 
     implementation "com.android.support:support-annotations:$support_library_version"
+    implementation "com.android.support:appcompat-v7:$support_library_version"
 
     androidTestImplementation 'com.android.support.test.espresso:espresso-core:2.2.2'
     androidTestImplementation 'com.android.support.test:runner:0.5'
     // Not defining this library again results in test-app assuming 23.1.1, and the following errors:
     // "Conflict with dependency 'com.android.support:support-annotations'. Resolved versions for app (23.4.0) and test app (23.1.1) differ."
     androidTestImplementation "com.android.support:support-annotations:$support_library_version"
 
     implementation project(path: ':geckoview')
--- a/mobile/android/geckoview_example/src/main/AndroidManifest.xml
+++ b/mobile/android/geckoview_example/src/main/AndroidManifest.xml
@@ -12,16 +12,17 @@
                  android:label="@string/app_name"
                  android:supportsRtl="true">
 
         <uses-library android:name="android.test.runner" />
 
         <activity android:name="org.mozilla.geckoview_example.GeckoViewActivity"
                   android:label="GeckoViewActivity"
                   android:windowSoftInputMode="stateUnspecified|adjustResize"
+                  android:theme="@style/Theme.AppCompat.Light.NoActionBar"
                   android:launchMode="singleTop">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
                 <category android:name="android.intent.category.MULTIWINDOW_LAUNCHER"/>
                 <category android:name="android.intent.category.APP_BROWSER" />
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
--- a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
@@ -1,62 +1,105 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.geckoview_example;
 
-import android.app.Activity;
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.process.GeckoProcessManager;
+import org.mozilla.geckoview.BasicSelectionActionDelegate;
+import org.mozilla.geckoview.GeckoResponse;
+import org.mozilla.geckoview.GeckoRuntime;
+import org.mozilla.geckoview.GeckoRuntimeSettings;
+import org.mozilla.geckoview.GeckoSession;
+import org.mozilla.geckoview.GeckoSession.TrackingProtectionDelegate;
+import org.mozilla.geckoview.GeckoSessionSettings;
+import org.mozilla.geckoview.GeckoView;
+
+import android.Manifest;
+import android.app.DownloadManager;
+import android.content.ActivityNotFoundException;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.Environment;
 import android.os.SystemClock;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
 import android.util.Log;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
 import android.view.WindowManager;
 
 import java.util.Locale;
+import java.util.LinkedList;
 
-import org.mozilla.geckoview.GeckoResponse;
-import org.mozilla.geckoview.GeckoSession;
-import org.mozilla.geckoview.GeckoSessionSettings;
-import org.mozilla.geckoview.GeckoSession.TrackingProtectionDelegate;
-import org.mozilla.geckoview.GeckoView;
-import org.mozilla.geckoview.GeckoRuntime;
-import org.mozilla.geckoview.GeckoRuntimeSettings;
-
-public class GeckoViewActivity extends Activity {
+public class GeckoViewActivity extends AppCompatActivity {
     private static final String LOGTAG = "GeckoViewActivity";
     private static final String DEFAULT_URL = "https://mozilla.org";
     private static final String USE_MULTIPROCESS_EXTRA = "use_multiprocess";
-    private static final String USE_REMOTE_DEBUGGER_EXTRA = "use_remote_debugger";
-    private static final String ACTION_SHUTDOWN =
-        "org.mozilla.geckoview_example.SHUTDOWN";
-    private boolean mKillProcessOnDestroy;
-
-    /* package */ static final int REQUEST_FILE_PICKER = 1;
+    private static final String SEARCH_URI_BASE = "https://www.google.com/search?q=";
+    private static final String ACTION_SHUTDOWN = "org.mozilla.geckoview_example.SHUTDOWN";
+    private static final int REQUEST_FILE_PICKER = 1;
     private static final int REQUEST_PERMISSIONS = 2;
+    private static final int REQUEST_WRITE_EXTERNAL_STORAGE = 3;
 
     private static GeckoRuntime sGeckoRuntime;
     private GeckoSession mGeckoSession;
     private GeckoView mGeckoView;
+    private boolean mUseMultiprocess;
+    private boolean mUseTrackingProtection;
+    private boolean mUsePrivateBrowsing;
+    private boolean mKillProcessOnDestroy;
+
+    private LocationView mLocationView;
+    private String mCurrentUri;
+    private boolean mCanGoBack;
+    private boolean mFullScreen;
+
+    private LinkedList<GeckoSession.WebResponseInfo> mPendingDownloads = new LinkedList<>();
+
+    private LocationView.CommitListener mCommitListener = new LocationView.CommitListener() {
+        @Override
+        public void onCommit(String text) {
+            if ((text.contains(".") || text.contains(":")) && !text.contains(" ")) {
+                mGeckoSession.loadUri(text);
+            } else {
+                mGeckoSession.loadUri(SEARCH_URI_BASE + text);
+            }
+            mGeckoView.requestFocus();
+        }
+    };
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         Log.i(LOGTAG, "zerdatime " + SystemClock.elapsedRealtime() +
               " - application start");
 
         setContentView(R.layout.geckoview_activity);
         mGeckoView = (GeckoView) findViewById(R.id.gecko_view);
 
-        final boolean useMultiprocess =
-            getIntent().getBooleanExtra(USE_MULTIPROCESS_EXTRA, true);
+        setSupportActionBar((Toolbar)findViewById(R.id.toolbar));
+
+        mLocationView = new LocationView(this);
+        getSupportActionBar().setCustomView(mLocationView,
+                new ActionBar.LayoutParams(ActionBar.LayoutParams.MATCH_PARENT,
+                        ActionBar.LayoutParams.WRAP_CONTENT));
+        getSupportActionBar().setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
+
+        mUseMultiprocess = getIntent().getBooleanExtra(USE_MULTIPROCESS_EXTRA, true);
 
         if (sGeckoRuntime == null) {
             final GeckoRuntimeSettings.Builder runtimeSettingsBuilder =
                 new GeckoRuntimeSettings.Builder();
 
             if (BuildConfig.DEBUG) {
                 // In debug builds, we want to load JavaScript resources fresh with
                 // each build.
@@ -64,94 +107,200 @@ public class GeckoViewActivity extends A
             }
 
             final Bundle extras = getIntent().getExtras();
             if (extras != null) {
                 runtimeSettingsBuilder.extras(extras);
             }
 
             runtimeSettingsBuilder
-                    .useContentProcessHint(useMultiprocess)
+                    .useContentProcessHint(mUseMultiprocess)
+                    .remoteDebuggingEnabled(true)
                     .nativeCrashReportingEnabled(true)
                     .javaCrashReportingEnabled(true);
 
             sGeckoRuntime = GeckoRuntime.create(this, runtimeSettingsBuilder.build());
         }
 
-        final GeckoSessionSettings sessionSettings = new GeckoSessionSettings();
-        sessionSettings.setBoolean(GeckoSessionSettings.USE_MULTIPROCESS,
-                                   useMultiprocess);
-        mGeckoSession = new GeckoSession(sessionSettings);
+        mGeckoSession = (GeckoSession)getIntent().getParcelableExtra("session");
+        if (mGeckoSession != null) {
+            connectSession(mGeckoSession);
+
+            if (!mGeckoSession.isOpen()) {
+                mGeckoSession.open(sGeckoRuntime);
+            }
+
+            mUseMultiprocess = mGeckoSession.getSettings().getBoolean(GeckoSessionSettings.USE_MULTIPROCESS);
+
+            mGeckoView.setSession(mGeckoSession);
+        } else {
+            mGeckoSession = createSession();
+            mGeckoView.setSession(mGeckoSession, sGeckoRuntime);
+            loadFromIntent(getIntent());
+        }
 
-        mGeckoView.setSession(mGeckoSession, sGeckoRuntime);
+        mLocationView.setCommitListener(mCommitListener);
+    }
+
+    private GeckoSession createSession() {
+        GeckoSession session = new GeckoSession();
+        session.getSettings().setBoolean(GeckoSessionSettings.USE_MULTIPROCESS, mUseMultiprocess);
+        session.getSettings().setBoolean(GeckoSessionSettings.USE_PRIVATE_MODE, mUsePrivateBrowsing);
+
+        connectSession(session);
 
-        mGeckoSession.setContentDelegate(new MyGeckoViewContent());
-        final MyTrackingProtection tp = new MyTrackingProtection();
-        mGeckoSession.setTrackingProtectionDelegate(tp);
-        mGeckoSession.setProgressDelegate(new MyGeckoViewProgress(tp));
-        mGeckoSession.setNavigationDelegate(new Navigation());
+        return session;
+    }
+
+    private void connectSession(GeckoSession session) {
+        session.setContentDelegate(new ExampleContentDelegate());
+        final ExampleTrackingProtectionDelegate tp = new ExampleTrackingProtectionDelegate();
+        session.setTrackingProtectionDelegate(tp);
+        session.setProgressDelegate(new ExampleProgressDelegate(tp));
+        session.setNavigationDelegate(new ExampleNavigationDelegate());
 
         final BasicGeckoViewPrompt prompt = new BasicGeckoViewPrompt(this);
         prompt.filePickerRequestCode = REQUEST_FILE_PICKER;
-        mGeckoSession.setPromptDelegate(prompt);
+        session.setPromptDelegate(prompt);
+
+        final ExamplePermissionDelegate permission = new ExamplePermissionDelegate();
+        permission.androidPermissionRequestCode = REQUEST_PERMISSIONS;
+        session.setPermissionDelegate(permission);
+
+        session.setSelectionActionDelegate(new BasicSelectionActionDelegate(this));
+
+        updateTrackingProtection(session);
+    }
+
+    private void recreateSession() {
+        mGeckoSession.close();
+
+        mGeckoSession = createSession();
+        mGeckoSession.open(sGeckoRuntime);
+        mGeckoView.setSession(mGeckoSession);
+        mGeckoSession.loadUri(mCurrentUri != null ? mCurrentUri : DEFAULT_URL);
+    }
 
-        final MyGeckoViewPermission permission = new MyGeckoViewPermission();
-        permission.androidPermissionRequestCode = REQUEST_PERMISSIONS;
-        mGeckoSession.setPermissionDelegate(permission);
+    private void updateTrackingProtection(GeckoSession session) {
+        if (mUseTrackingProtection) {
+            session.enableTrackingProtection(
+                    TrackingProtectionDelegate.CATEGORY_AD |
+                            TrackingProtectionDelegate.CATEGORY_ANALYTIC |
+                            TrackingProtectionDelegate.CATEGORY_SOCIAL
+            );
+        } else {
+            session.disableTrackingProtection();
+        }
+    }
+
+    @Override
+    protected void onPause() {
+        mGeckoSession.setActive(false);
+        super.onPause();
+    }
+
+    @Override
+    protected void onResume() {
+        mGeckoSession.setActive(true);
+        super.onResume();
+    }
+
+    @Override
+    public void onBackPressed() {
+        if (mFullScreen) {
+            mGeckoSession.exitFullScreen();
+            return;
+        }
 
-        mGeckoSession.enableTrackingProtection(
-              TrackingProtectionDelegate.CATEGORY_AD |
-              TrackingProtectionDelegate.CATEGORY_ANALYTIC |
-              TrackingProtectionDelegate.CATEGORY_SOCIAL
-        );
+        if (mCanGoBack && mGeckoSession != null) {
+            mGeckoSession.goBack();
+            return;
+        }
+
+        super.onBackPressed();
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        MenuInflater inflater = getMenuInflater();
+        inflater.inflate(R.menu.actions, menu);
+        return true;
+    }
+
+    @Override
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        menu.findItem(R.id.action_e10s).setChecked(mUseMultiprocess);
+        menu.findItem(R.id.action_tp).setChecked(mUseTrackingProtection);
+        menu.findItem(R.id.action_pb).setChecked(mUsePrivateBrowsing);
+        return true;
+    }
 
-        loadSettings(getIntent());
-        loadFromIntent(getIntent());
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case R.id.action_reload:
+                mGeckoSession.reload();
+                break;
+            case R.id.action_forward:
+                mGeckoSession.goForward();
+                break;
+            case R.id.action_e10s:
+                mUseMultiprocess = !mUseMultiprocess;
+                recreateSession();
+                break;
+            case R.id.action_tp:
+                mUseTrackingProtection = !mUseTrackingProtection;
+                updateTrackingProtection(mGeckoSession);
+                mGeckoSession.reload();
+                break;
+            case R.id.action_pb:
+                mUsePrivateBrowsing = !mUsePrivateBrowsing;
+                recreateSession();
+                break;
+            default:
+                return super.onOptionsItemSelected(item);
+        }
+
+        return true;
     }
 
     @Override
     public void onDestroy() {
-        super.onDestroy();
-
         if (mKillProcessOnDestroy) {
             android.os.Process.killProcess(android.os.Process.myPid());
         }
+
+        super.onDestroy();
     }
 
-    @Override
     protected void onNewIntent(final Intent intent) {
         super.onNewIntent(intent);
 
         if (ACTION_SHUTDOWN.equals(intent.getAction())) {
             mKillProcessOnDestroy = true;
             if (sGeckoRuntime != null) {
                 sGeckoRuntime.shutdown();
             }
             finish();
             return;
         }
 
         setIntent(intent);
 
-        loadSettings(intent);
         if (intent.getData() != null) {
             loadFromIntent(intent);
         }
     }
 
-    private void loadFromIntent(final Intent intent) {
+
+        private void loadFromIntent(final Intent intent) {
         final Uri uri = intent.getData();
         mGeckoSession.loadUri(uri != null ? uri.toString() : DEFAULT_URL);
     }
 
-    private void loadSettings(final Intent intent) {
-        sGeckoRuntime.getSettings().setRemoteDebuggingEnabled(
-            intent.getBooleanExtra(USE_REMOTE_DEBUGGER_EXTRA, false));
-    }
-
     @Override
     protected void onActivityResult(final int requestCode, final int resultCode,
                                     final Intent data) {
         if (requestCode == REQUEST_FILE_PICKER) {
             final BasicGeckoViewPrompt prompt = (BasicGeckoViewPrompt)
                     mGeckoSession.getPromptDelegate();
             prompt.onFileCallbackResult(resultCode, data);
         } else {
@@ -159,71 +308,112 @@ public class GeckoViewActivity extends A
         }
     }
 
     @Override
     public void onRequestPermissionsResult(final int requestCode,
                                            final String[] permissions,
                                            final int[] grantResults) {
         if (requestCode == REQUEST_PERMISSIONS) {
-            final MyGeckoViewPermission permission = (MyGeckoViewPermission)
+            final ExamplePermissionDelegate permission = (ExamplePermissionDelegate)
                     mGeckoSession.getPermissionDelegate();
             permission.onRequestPermissionsResult(permissions, grantResults);
+        } else if (requestCode == REQUEST_WRITE_EXTERNAL_STORAGE &&
+                   grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+            continueDownloads();
         } else {
             super.onRequestPermissionsResult(requestCode, permissions, grantResults);
         }
     }
 
-    private class MyGeckoViewContent implements GeckoSession.ContentDelegate {
+    private void continueDownloads() {
+        LinkedList<GeckoSession.WebResponseInfo> downloads = mPendingDownloads;
+        mPendingDownloads = new LinkedList<>();
+
+        for (GeckoSession.WebResponseInfo response : downloads) {
+            downloadFile(response);
+        }
+    }
+
+    private void downloadFile(GeckoSession.WebResponseInfo response) {
+        if (ContextCompat.checkSelfPermission(GeckoViewActivity.this,
+                Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+            mPendingDownloads.add(response);
+            ActivityCompat.requestPermissions(GeckoViewActivity.this,
+                    new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
+                    REQUEST_WRITE_EXTERNAL_STORAGE);
+            return;
+        }
+
+        final Uri uri = Uri.parse(response.uri);
+        final String filename = response.filename != null ? response.filename : uri.getLastPathSegment();
+
+        DownloadManager manager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
+        DownloadManager.Request req = new DownloadManager.Request(uri);
+        req.setMimeType(response.contentType);
+        req.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename);
+        req.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE);
+        manager.enqueue(req);
+    }
+
+    private class ExampleContentDelegate implements GeckoSession.ContentDelegate {
         @Override
         public void onTitleChange(GeckoSession session, String title) {
             Log.i(LOGTAG, "Content title changed to " + title);
         }
 
         @Override
         public void onFullScreen(final GeckoSession session, final boolean fullScreen) {
             getWindow().setFlags(fullScreen ? WindowManager.LayoutParams.FLAG_FULLSCREEN : 0,
                                  WindowManager.LayoutParams.FLAG_FULLSCREEN);
+            mFullScreen = fullScreen;
             if (fullScreen) {
-                getActionBar().hide();
+                getSupportActionBar().hide();
             } else {
-                getActionBar().show();
+                getSupportActionBar().show();
             }
         }
 
         @Override
         public void onFocusRequest(final GeckoSession session) {
             Log.i(LOGTAG, "Content requesting focus");
         }
 
         @Override
         public void onCloseRequest(final GeckoSession session) {
-            if (session != mGeckoSession) {
-                session.close();
+            if (session == mGeckoSession) {
+                finish();
             }
         }
 
         @Override
         public void onContextMenu(GeckoSession session, int screenX, int screenY,
                                   String uri, int elementType, String elementSrc) {
             Log.d(LOGTAG, "onContextMenu screenX=" + screenX +
                           " screenY=" + screenY + " uri=" + uri +
                           " elementType=" + elementType +
                           " elementSrc=" + elementSrc);
         }
 
         @Override
-        public void onExternalResponse(GeckoSession session, GeckoSession.WebResponseInfo request) {
+        public void onExternalResponse(GeckoSession session, GeckoSession.WebResponseInfo response) {
+            try {
+                Intent intent = new Intent(Intent.ACTION_VIEW);
+                intent.setDataAndTypeAndNormalize(Uri.parse(response.uri), response.contentType);
+                startActivity(intent);
+            } catch (ActivityNotFoundException e) {
+                downloadFile(response);
+            }
         }
     }
 
-    private class MyGeckoViewProgress implements GeckoSession.ProgressDelegate {
-        private MyTrackingProtection mTp;
+    private class ExampleProgressDelegate implements GeckoSession.ProgressDelegate {
+        private ExampleTrackingProtectionDelegate mTp;
 
-        private MyGeckoViewProgress(final MyTrackingProtection tp) {
+        private ExampleProgressDelegate(final ExampleTrackingProtectionDelegate tp) {
             mTp = tp;
         }
 
         @Override
         public void onPageStart(GeckoSession session, String url) {
             Log.i(LOGTAG, "Starting to load page at " + url);
             Log.i(LOGTAG, "zerdatime " + SystemClock.elapsedRealtime() +
                   " - page load start");
@@ -239,17 +429,17 @@ public class GeckoViewActivity extends A
         }
 
         @Override
         public void onSecurityChange(GeckoSession session, SecurityInformation securityInfo) {
             Log.i(LOGTAG, "Security status changed to " + securityInfo.securityMode);
         }
     }
 
-    private class MyGeckoViewPermission implements GeckoSession.PermissionDelegate {
+    private class ExamplePermissionDelegate implements GeckoSession.PermissionDelegate {
 
         public int androidPermissionRequestCode = 1;
         private Callback mCallback;
 
         public void onRequestPermissionsResult(final String[] permissions,
                                                final int[] grantResults) {
             if (mCallback == null) {
                 return;
@@ -345,23 +535,26 @@ public class GeckoViewActivity extends A
             String[] audioNames = normalizeMediaName(audio);
 
             final BasicGeckoViewPrompt prompt = (BasicGeckoViewPrompt)
                     mGeckoSession.getPromptDelegate();
             prompt.onMediaPrompt(session, title, video, audio, videoNames, audioNames, callback);
         }
     }
 
-    private class Navigation implements GeckoSession.NavigationDelegate {
+    private class ExampleNavigationDelegate implements GeckoSession.NavigationDelegate {
         @Override
         public void onLocationChange(GeckoSession session, final String url) {
+            mLocationView.setText(url);
+            mCurrentUri = url;
         }
 
         @Override
         public void onCanGoBack(GeckoSession session, boolean canGoBack) {
+            mCanGoBack = canGoBack;
         }
 
         @Override
         public void onCanGoForward(GeckoSession session, boolean value) {
         }
 
         @Override
         public void onLoadRequest(final GeckoSession session, final String uri,
@@ -369,21 +562,29 @@ public class GeckoViewActivity extends A
                                   GeckoResponse<Boolean> response) {
             Log.d(LOGTAG, "onLoadRequest=" + uri + " where=" + target +
                   " flags=" + flags);
             response.respond(false);
         }
 
         @Override
         public void onNewSession(final GeckoSession session, final String uri, GeckoResponse<GeckoSession> response) {
-            response.respond(null);
+            GeckoSession newSession = new GeckoSession(session.getSettings());
+            response.respond(newSession);
+
+            Intent intent = new Intent(GeckoViewActivity.this, SessionActivity.class);
+            intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+            intent.setAction(Intent.ACTION_VIEW);
+            intent.setData(Uri.parse(uri));
+            intent.putExtra("session", newSession);
+            startActivity(intent);
         }
     }
 
-    private class MyTrackingProtection implements GeckoSession.TrackingProtectionDelegate {
+    private class ExampleTrackingProtectionDelegate implements GeckoSession.TrackingProtectionDelegate {
         private int mBlockedAds = 0;
         private int mBlockedAnalytics = 0;
         private int mBlockedSocial = 0;
 
         private void clearCounters() {
             mBlockedAds = 0;
             mBlockedAnalytics = 0;
             mBlockedSocial = 0;
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/LocationView.java
@@ -0,0 +1,61 @@
+package org.mozilla.geckoview_example;
+
+import org.mozilla.geckoview.GeckoSession;
+
+import android.content.Context;
+import android.support.v7.widget.AppCompatEditText;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.widget.TextView;
+
+public class LocationView extends AppCompatEditText {
+
+    private CommitListener mCommitListener;
+    private FocusAndCommitListener mFocusCommitListener = new FocusAndCommitListener();
+
+    public interface CommitListener {
+        void onCommit(String text);
+    }
+
+    public LocationView(Context context) {
+        super(context);
+
+        this.setInputType(EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_URI);
+        this.setSingleLine(true);
+        this.setSelectAllOnFocus(true);
+        this.setHint(R.string.location_hint);
+
+        setOnFocusChangeListener(mFocusCommitListener);
+        setOnEditorActionListener(mFocusCommitListener);
+    }
+
+    public void setCommitListener(CommitListener listener) {
+        mCommitListener = listener;
+    }
+
+    private class FocusAndCommitListener implements OnFocusChangeListener, OnEditorActionListener {
+        private String mInitialText;
+        private boolean mCommitted;
+
+        @Override
+        public void onFocusChange(View view, boolean focused) {
+            if (focused) {
+                mInitialText = ((TextView)view).getText().toString();
+                mCommitted = false;
+            } else if (!mCommitted) {
+                setText(mInitialText);
+            }
+        }
+
+        @Override
+        public boolean onEditorAction(TextView textView, int i, KeyEvent keyEvent) {
+            if (mCommitListener != null) {
+                mCommitListener.onCommit(textView.getText().toString());
+            }
+
+            mCommitted = true;
+            return true;
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/SessionActivity.java
@@ -0,0 +1,4 @@
+package org.mozilla.geckoview_example;
+
+public class SessionActivity extends GeckoViewActivity {
+}
--- a/mobile/android/geckoview_example/src/main/res/layout/geckoview_activity.xml
+++ b/mobile/android/geckoview_example/src/main/res/layout/geckoview_activity.xml
@@ -1,13 +1,20 @@
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-              android:layout_width="fill_parent"
-              android:layout_height="fill_parent"
-              android:orientation="vertical">
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                android:id="@+id/main"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:orientation="vertical">
 
     <org.mozilla.geckoview.GeckoView
         android:id="@+id/gecko_view"
-        android:layout_width="fill_parent"
+        android:layout_width="match_parent"
         android:layout_height="match_parent"
+        android:layout_above="@id/toolbar"
         android:scrollbars="none"
         />
 
-</LinearLayout>
+    <android.support.v7.widget.Toolbar
+            android:id="@+id/toolbar"
+            android:layout_width="match_parent"
+            android:layout_height="?android:actionBarSize"
+            android:layout_alignParentBottom="true"/>
+</RelativeLayout>
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/menu/actions.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:title="@string/multiprocess" android:id="@+id/action_e10s" android:checkable="true"
+          android:showAsAction="never"/>
+    <item android:title="@string/tracking_protection" android:id="@+id/action_tp" android:showAsAction="never"
+          android:checkable="true"/>
+    <item android:title="@string/private_browsing" android:checkable="true" android:id="@+id/action_pb"/>
+    <item android:title="@string/forward" android:id="@+id/action_forward"/>
+    <item android:title="@string/reload" android:id="@+id/action_reload"/>
+</menu>
\ No newline at end of file
--- a/mobile/android/geckoview_example/src/main/res/values/strings.xml
+++ b/mobile/android/geckoview_example/src/main/res/values/strings.xml
@@ -1,15 +1,26 @@
 <resources>
     <string name="app_name">geckoview_example</string>
+    <string name="activity_label">GeckoView Example</string>
+    <string name="location_hint">Enter URL or search keywords...</string>
     <string name="username">Username</string>
     <string name="password">Password</string>
     <string name="clear_field">Clear</string>
     <string name="request_geolocation">Share location with "%1$s"?</string>
     <string name="request_notification">Allow notifications for "%1$s"?</string>
     <string name="request_video">Share video with "%1$s"</string>
     <string name="request_audio">Share audio with "%1$s"</string>
     <string name="request_media">Share video and audio with "%1$s"</string>
     <string name="media_back_camera">Back camera</string>
     <string name="media_front_camera">Front camera</string>
     <string name="media_microphone">Microphone</string>
     <string name="media_other">Unknown source</string>
+
+    <string name="crash_native">Native</string>
+    <string name="crash_java">Java</string>
+    <string name="crash_content_native">Content (Native)</string>
+    <string name="multiprocess">Multiprocess</string>
+    <string name="tracking_protection">Tracking Protection</string>
+    <string name="private_browsing">Private Browsing</string>
+    <string name="forward">Forward</string>
+    <string name="reload">Reload</string>
 </resources>