Bug 1386192 - Test Leanplum Custom Message for Onboarding; r?nechen draft
authorPetru Lingurar <petru.lingurar@softvision.ro>
Wed, 04 Jul 2018 12:56:20 +0300
changeset 813965 b8d7c5ce4fe05fe86d8a3e4d7a78c761bebb2078
parent 813837 987ea0d6a000b95cf93928b25a74a7fb1dfe37b2
push id115069
push userplingurar@mozilla.com
push dateWed, 04 Jul 2018 09:59:22 +0000
reviewersnechen
bugs1386192
milestone63.0a1
Bug 1386192 - Test Leanplum Custom Message for Onboarding; r?nechen Created LeanPlumVariables to allow LeanPlum overwriting the values used for populating the OnBoarding screens. By simply adding the @Variable annotation to it's fields, on the first run of the app, they will appear in "LeanPlum dashboard - Variables" and will allow overwriting for future runs. The OnBoarding process will now try to use LeanPlum values if possible. Because connecting to LeanPlum and downloading the Variables might take a few seconds we use a delay of up to 3 seconds until starting to show the Onboarding screens. The default values will still be used if: - if the LP experiment is not available - if no internet connection - if more than 3 seconds have passed and LP didn't finish it's download Added two new events that could be tracked to Leanplum MmaDelegate.ONBOARDING_DEFAULT_VALUES and MmaDelegate.ONBOARDING_REMOTE_VALUES to inform if showing the Onboarding with server values was possible or not. Because of the 3 seconds delay until showing the Onboarding panels leaking the could be possible. Used WeakReferences for both the Activity in OnboardingHelper and the OnboardingHelper in MmaLeanplumImp to avoid it. MozReview-Commit-ID: H30e9Ng7jrM
mobile/android/app/build.gradle
mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
mobile/android/base/java/org/mozilla/gecko/firstrun/FirstRunPanelConfigProviderStrategy.java
mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunAnimationContainer.java
mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPager.java
mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPagerConfig.java
mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPanel.java
mobile/android/base/java/org/mozilla/gecko/firstrun/LastPanel.java
mobile/android/base/java/org/mozilla/gecko/firstrun/LocalFirstRunPanelProvider.java
mobile/android/base/java/org/mozilla/gecko/firstrun/OnboardingHelper.java
mobile/android/base/java/org/mozilla/gecko/firstrun/PanelConfig.java
mobile/android/base/java/org/mozilla/gecko/firstrun/RemoteFirstRunPanelConfig.java
mobile/android/base/java/org/mozilla/gecko/firstrun/SyncPanel.java
mobile/android/base/java/org/mozilla/gecko/mma/LeanplumVariables.java
mobile/android/base/java/org/mozilla/gecko/mma/MmaDelegate.java
mobile/android/base/java/org/mozilla/gecko/mma/MmaInterface.java
mobile/android/base/java/org/mozilla/gecko/mma/MmaLeanplumImp.java
mobile/android/base/java/org/mozilla/gecko/mma/MmaStubImp.java
mobile/android/base/java/org/mozilla/gecko/switchboard/AsyncConfigLoader.java
mobile/android/base/java/org/mozilla/gecko/switchboard/SwitchBoard.java
mobile/android/base/java/org/mozilla/gecko/widget/SplashScreen.java
mobile/android/services/src/main/java/org/mozilla/gecko/fxa/EnvironmentUtils.java
mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseRobocopTest.java
--- a/mobile/android/app/build.gradle
+++ b/mobile/android/app/build.gradle
@@ -140,16 +140,17 @@ android {
                 } else {
                     exclude 'org/mozilla/gecko/adjust/AdjustHelper.java'
                 }
 
                 if (mozconfig.substs.MOZ_ANDROID_MMA) {
                     exclude 'org/mozilla/gecko/mma/MmaStubImp.java'
                 } else {
                     exclude 'org/mozilla/gecko/mma/MmaLeanplumImp.java'
+                    exclude 'org/mozilla/gecko/mma/LeanplumVariables.java'
                 }
 
                 if (!mozconfig.substs.MOZ_ANDROID_GCM) {
                     exclude 'org/mozilla/gecko/gcm/**/*.java'
                     exclude 'org/mozilla/gecko/push/**/*.java'
                 }
             }
 
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -36,17 +36,16 @@ import android.os.Build;
 import android.os.Bundle;
 import android.os.Environment;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.StrictMode;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.annotation.StringRes;
-import android.support.annotation.UiThread;
 import android.support.design.widget.Snackbar;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentManager;
 import android.support.v4.app.NotificationCompat;
 import android.support.v4.content.res.ResourcesCompat;
 import android.support.v4.view.MenuItemCompat;
 import android.text.TextUtils;
 import android.util.AttributeSet;
@@ -89,17 +88,17 @@ import org.mozilla.gecko.db.SuggestedSit
 import org.mozilla.gecko.delegates.BookmarkStateChangeDelegate;
 import org.mozilla.gecko.delegates.BrowserAppDelegate;
 import org.mozilla.gecko.delegates.OfflineTabStatusDelegate;
 import org.mozilla.gecko.delegates.ScreenshotDelegate;
 import org.mozilla.gecko.distribution.Distribution;
 import org.mozilla.gecko.distribution.DistributionStoreCallback;
 import org.mozilla.gecko.dlc.DownloadContentService;
 import org.mozilla.gecko.extensions.ExtensionPermissionsHelper;
-import org.mozilla.gecko.firstrun.FirstrunAnimationContainer;
+import org.mozilla.gecko.firstrun.OnboardingHelper;
 import org.mozilla.gecko.gfx.DynamicToolbarAnimator;
 import org.mozilla.gecko.gfx.DynamicToolbarAnimator.PinReason;
 import org.mozilla.gecko.home.BrowserSearch;
 import org.mozilla.gecko.home.HomeBanner;
 import org.mozilla.gecko.home.HomeConfig;
 import org.mozilla.gecko.home.HomeConfig.PanelType;
 import org.mozilla.gecko.home.HomeConfigPrefsBackend;
 import org.mozilla.gecko.home.HomeFragment;
@@ -157,16 +156,17 @@ import org.mozilla.gecko.util.ActivityUt
 import org.mozilla.gecko.util.ContextUtils;
 import org.mozilla.gecko.util.DrawableUtil;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.GamepadUtils;
 import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.util.IntentUtils;
 import org.mozilla.gecko.util.MenuUtils;
+import org.mozilla.gecko.util.NetworkUtils;
 import org.mozilla.gecko.util.PrefUtils;
 import org.mozilla.gecko.util.ShortcutUtils;
 import org.mozilla.gecko.util.StringUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.util.WindowUtil;
 import org.mozilla.gecko.widget.ActionModePresenter;
 import org.mozilla.gecko.widget.AnchoredPopup;
 import org.mozilla.gecko.widget.AnimatedProgressBar;
@@ -181,17 +181,16 @@ import java.lang.reflect.Method;
 import java.net.URLEncoder;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
-import java.util.UUID;
 import java.util.regex.Pattern;
 
 import static org.mozilla.gecko.mma.MmaDelegate.NEW_TAB;
 
 public class BrowserApp extends GeckoApp
                         implements ActionModePresenter,
                                    AnchoredPopup.OnVisibilityChangeListener,
                                    BookmarkEditFragment.Callbacks,
@@ -199,17 +198,18 @@ public class BrowserApp extends GeckoApp
                                    BrowserSearch.OnSearchListener,
                                    DynamicToolbarAnimator.ToolbarChromeProxy,
                                    LayoutInflater.Factory,
                                    LightweightTheme.OnChangeListener,
                                    OnUrlOpenListener,
                                    OnUrlOpenInBackgroundListener,
                                    PropertyAnimator.PropertyAnimationListener,
                                    TabsPanel.TabsLayoutChangeListener,
-                                   View.OnKeyListener {
+                                   View.OnKeyListener,
+                                   OnboardingHelper.OnboardingListener {
     private static final String LOGTAG = "GeckoBrowserApp";
 
     private static final int TABS_ANIMATION_DURATION = 450;
 
     // Intent String extras used to specify custom Switchboard configurations.
     private static final String INTENT_KEY_SWITCHBOARD_SERVER = "switchboard-server";
 
     // TODO: Replace with kinto endpoint.
@@ -228,38 +228,30 @@ public class BrowserApp extends GeckoApp
     public static final int ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_GOTO_BOOKMARKS = 3002;
     public static final int ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_IGNORE = 3003;
     public static final int ACTIVITY_REQUEST_TRIPLE_READERVIEW = 4001;
     public static final int ACTIVITY_RESULT_TRIPLE_READERVIEW_ADD_BOOKMARK = 4002;
     public static final int ACTIVITY_RESULT_TRIPLE_READERVIEW_IGNORE = 4003;
 
     public static final String ACTION_VIEW_MULTIPLE = AppConstants.ANDROID_PACKAGE_NAME + ".action.VIEW_MULTIPLE";
 
-    @RobocopTarget
-    public static final String EXTRA_SKIP_STARTPANE = "skipstartpane";
     private static final String EOL_NOTIFIED = "eol_notified";
 
-    /**
-     * Be aware of {@link org.mozilla.gecko.fxa.EnvironmentUtils.GECKO_PREFS_FIRSTRUN_UUID}.
-     */
-    private static final String FIRSTRUN_UUID = "firstrun_uuid";
-
     private BrowserSearch mBrowserSearch;
     private View mBrowserSearchContainer;
 
     public ViewGroup mBrowserChrome;
     public ViewFlipper mActionBarFlipper;
     public ActionModeCompatView mActionBar;
     private VideoPlayer mVideoPlayer;
     private BrowserToolbar mBrowserToolbar;
     private View doorhangerOverlay;
     // We can't name the TabStrip class because it's not included on API 9.
     private TabStripInterface mTabStrip;
     private AnimatedProgressBar mProgressView;
-    private FirstrunAnimationContainer mFirstrunAnimationContainer;
     private HomeScreen mHomeScreen;
     private TabsPanel mTabsPanel;
 
     private boolean showSplashScreen = false;
     private SplashScreen splashScreen;
     /**
      * Container for the home screen implementation. This will be populated with any valid
      * home screen implementation (currently that is just the HomePager, but that will be extended
@@ -428,16 +420,17 @@ public class BrowserApp extends GeckoApp
             new PostUpdateHandler(),
             mTelemetryCorePingDelegate,
             new OfflineTabStatusDelegate(),
             new AdjustBrowserAppDelegate(mTelemetryCorePingDelegate)
     ));
 
     @NonNull
     private SearchEngineManager mSearchEngineManager; // Contains reference to Context - DO NOT LEAK!
+    private OnboardingHelper mOnboardingHelper;       // Contains reference to Context - DO NOT LEAK!
 
     private boolean mHasResumed;
 
     @Override
     public View onCreateView(final View parent, final String name, final Context context, final AttributeSet attrs) {
         final View view;
         if (BrowserToolbar.class.getName().equals(name)) {
             view = BrowserToolbar.create(context, attrs);
@@ -744,16 +737,17 @@ public class BrowserApp extends GeckoApp
         app.prepareLightweightTheme();
 
         super.onCreate(savedInstanceState);
 
         if (mIsAbortingAppLaunch) {
           return;
         }
 
+        mOnboardingHelper = new OnboardingHelper(this, safeStartingIntent);
         initSwitchboardAndMma(this, safeStartingIntent, isInAutomation);
         initTelemetryUploader(isInAutomation);
 
         mBrowserChrome = (ViewGroup) findViewById(R.id.browser_chrome);
         mActionBarFlipper = (ViewFlipper) findViewById(R.id.browser_actionbar);
         mActionBar = (ActionModeCompatView) findViewById(R.id.actionbar);
 
         mVideoPlayer = (VideoPlayer) findViewById(R.id.video_player);
@@ -1009,24 +1003,26 @@ public class BrowserApp extends GeckoApp
             return;
         } else if (!AppConstants.MOZ_SWITCHBOARD) {
             Log.d(LOGTAG, "Switchboard compile-time disabled");
             return;
         }
 
         final String serverExtra = intent.getStringExtra(INTENT_KEY_SWITCHBOARD_SERVER);
         final String serverUrl = TextUtils.isEmpty(serverExtra) ? SWITCHBOARD_SERVER : serverExtra;
-        new AsyncConfigLoader(context, serverUrl) {
+        final SwitchBoard.ConfigStatusListener configStatuslistener = mOnboardingHelper;
+        final MmaDelegate.MmaVariablesChangedListener variablesChangedListener = mOnboardingHelper;
+        new AsyncConfigLoader(context, serverUrl, configStatuslistener) {
             @Override
             protected Void doInBackground(Void... params) {
                 super.doInBackground(params);
-                SwitchBoard.loadConfig(context, serverUrl);
+                SwitchBoard.loadConfig(context, serverUrl, configStatuslistener);
                 if (GeckoPreferences.isMmaAvailableAndEnabled(context)) {
                     // Do LeanPlum start/init here
-                    MmaDelegate.init(BrowserApp.this);
+                    MmaDelegate.init(BrowserApp.this, variablesChangedListener);
                 }
                 return null;
             }
         }.execute();
     }
 
     private static void initTelemetryUploader(final boolean isInAutomation) {
         TelemetryUploadService.setDisabled(isInAutomation);
@@ -1084,116 +1080,16 @@ public class BrowserApp extends GeckoApp
                                 .putBoolean(EOL_NOTIFIED, true)
                                 .apply();
             }
         } finally {
             StrictMode.setThreadPolicy(savedPolicy);
         }
     }
 
-    /**
-     * Code to actually show the first run pager, separated
-     * for distribution purposes.
-     */
-    @UiThread
-    private void checkFirstrunInternal() {
-        showFirstrunPager();
-
-        if (HardwareUtils.isTablet()) {
-            mTabStrip.setOnTabChangedListener(new TabStripInterface.OnTabAddedOrRemovedListener() {
-                @Override
-                public void onTabChanged() {
-                    hideFirstrunPager(TelemetryContract.Method.BUTTON);
-                    mTabStrip.setOnTabChangedListener(null);
-                }
-            });
-        }
-    }
-
-    /**
-     * Check and show the firstrun pane if the browser has never been launched and
-     * is not opening an external link from another application.
-     *
-     * @param context Context of application; used to show firstrun pane if appropriate
-     * @param intent Intent that launched this activity
-     */
-    private void checkFirstrun(Context context, SafeIntent intent) {
-        if (getProfile().inGuestMode()) {
-            // We do not want to show any first run tour for guest profiles.
-            return;
-        }
-
-        if (intent.getBooleanExtra(EXTRA_SKIP_STARTPANE, false)) {
-            // Note that we don't set the pref, so subsequent launches can result
-            // in the firstrun pane being shown.
-            return;
-        }
-        final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
-
-        try {
-            final SharedPreferences prefs = GeckoSharedPrefs.forProfile(this);
-
-            if (prefs.getBoolean(FirstrunAnimationContainer.PREF_FIRSTRUN_ENABLED_OLD, true) &&
-                prefs.getBoolean(FirstrunAnimationContainer.PREF_FIRSTRUN_ENABLED, true)) {
-                showSplashScreen = false;
-                if (!Intent.ACTION_VIEW.equals(intent.getAction())) {
-                    // Check to see if a distribution has turned off the first run pager.
-                    final Distribution distribution = Distribution.getInstance(BrowserApp.this);
-                    if (!distribution.shouldWaitForSystemDistribution()) {
-                        checkFirstrunInternal();
-                    } else {
-                        distribution.addOnDistributionReadyCallback(new Distribution.ReadyCallback() {
-                            @Override
-                            public void distributionNotFound() {
-                                ThreadUtils.postToUiThread(new Runnable() {
-                                    @Override
-                                    public void run() {
-                                        checkFirstrunInternal();
-                                    }
-                                });
-                            }
-
-                            @Override
-                            public void distributionFound(final Distribution distribution) {
-                                // Check preference again in case distribution turned it off.
-                                if (prefs.getBoolean(FirstrunAnimationContainer.PREF_FIRSTRUN_ENABLED, true)) {
-                                    ThreadUtils.postToUiThread(new Runnable() {
-                                        @Override
-                                        public void run() {
-                                            checkFirstrunInternal();
-                                        }
-                                    });
-                                }
-                            }
-
-                            @Override
-                            public void distributionArrivedLate(final Distribution distribution) {
-                            }
-                        });
-                    }
-                }
-
-                prefs.edit()
-                        // Don't bother trying again to show the v1 minimal first run.
-                        .putBoolean(FirstrunAnimationContainer.PREF_FIRSTRUN_ENABLED, false)
-                        // Generate a unique identify for the current first run.
-                        // See Bug 1429735 for why we care to do this.
-                        .putString(FIRSTRUN_UUID, UUID.randomUUID().toString())
-                        .apply();
-
-                // We have no intention of stopping this session. The FIRSTRUN session
-                // ends when the browsing session/activity has ended. All events
-                // during firstrun will be tagged as FIRSTRUN.
-                Telemetry.startUISession(TelemetryContract.Session.FIRSTRUN);
-            }
-        } finally {
-            StrictMode.setThreadPolicy(savedPolicy);
-        }
-    }
-
     private Class<?> getMediaPlayerManager() {
         if (AppConstants.MOZ_MEDIA_PLAYER) {
             try {
                 return Class.forName("org.mozilla.gecko.MediaPlayerManager");
             } catch (Exception ex) {
                 // Ignore failures
                 Log.e(LOGTAG, "No native casting support", ex);
             }
@@ -1241,17 +1137,17 @@ public class BrowserApp extends GeckoApp
     }
 
     @Override
     public void onAttachedToWindow() {
         final SafeIntent intent = new SafeIntent(getIntent());
 
         if (!IntentUtils.getIsInAutomationFromEnvironment(intent)) {
             // We can't show the first run experience until Gecko has finished initialization (bug 1077583).
-            checkFirstrun(this, intent);
+            mOnboardingHelper.checkFirstRun();
         }
     }
 
     @Override
     protected void processTabQueue() {
         if (TabQueueHelper.TAB_QUEUE_ENABLED && mInitialized) {
             ThreadUtils.postToBackgroundThread(new Runnable() {
                 @Override
@@ -2655,19 +2551,20 @@ public class BrowserApp extends GeckoApp
         mBrowserToolbar.cancelEdit();
     }
 
     private boolean isHomePagerVisible() {
         return (mHomeScreen != null && mHomeScreen.isVisible()
                 && mHomeScreenContainer != null && mHomeScreenContainer.getVisibility() == View.VISIBLE);
     }
 
-    private boolean isFirstrunVisible() {
-        return (mFirstrunAnimationContainer != null && mFirstrunAnimationContainer.isVisible()
-                && mHomeScreenContainer != null && mHomeScreenContainer.getVisibility() == View.VISIBLE);
+    private SplashScreen getSplashScreen() {
+        final ViewGroup main = (ViewGroup) findViewById(R.id.gecko_layout);
+        final View splashLayout = LayoutInflater.from(this).inflate(R.layout.splash_screen, main);
+        return (SplashScreen) splashLayout.findViewById(R.id.splash_root);
     }
 
     /**
      * Enters editing mode with the current tab's URL. There might be no
      * tabs loaded by the time the user enters editing mode e.g. just after
      * the app starts. In this case, we simply fallback to an empty URL.
      */
     private void enterEditingMode() {
@@ -2910,18 +2807,21 @@ public class BrowserApp extends GeckoApp
             return;
         }
 
         // History will only store that we were visiting about:home, however the specific panel
         // isn't stored. (We are able to navigate directly to homepanels using an about:home?panel=...
         // URL, but the reverse doesn't apply: manually switching panels doesn't update the URL.)
         // Hence we need to restore the panel, in addition to panel state, here.
         if (isAboutHome(tab)) {
-            // For some reason(e.g. from SearchWidget) we are showing the splash schreen. We should hide it now.
-            if (splashScreen != null && splashScreen.getVisibility() == View.VISIBLE) {
+            // For some reason(e.g. from SearchWidget) we are showing the splash schreen.
+            // If we are not waiting for the onboarding screens we should hide it now.
+            if (!mOnboardingHelper.isPreparing() &&
+                    splashScreen != null &&
+                    splashScreen.getVisibility() == View.VISIBLE) {
                 // Below line will be run when LOCATION_CHANGE. Which means the page load is almost completed.
                 splashScreen.hide();
             }
 
             String panelId = AboutPages.getPanelIdFromAboutHomeUrl(tab.getURL());
             Bundle panelRestoreData = null;
             if (panelId == null) {
                 // No panel was specified in the URL. Try loading the most recent
@@ -2950,20 +2850,19 @@ public class BrowserApp extends GeckoApp
             }
             showSplashScreen = false;
         } else {
             // The tab going to load is not about page. It's a web page.
             // If showSplashScreen is true, it means the app is first launched. We want to show the SlashScreen
             // But if GeckoThread.isRunning, the will be 0 sec for web rendering.
             // In that case, we don't want to show the SlashScreen/
             if (showSplashScreen && !GeckoThread.isRunning()) {
-
-                final ViewGroup main = (ViewGroup) findViewById(R.id.gecko_layout);
-                final View splashLayout = LayoutInflater.from(this).inflate(R.layout.splash_screen, main);
-                splashScreen = (SplashScreen) splashLayout.findViewById(R.id.splash_root);
+                if (splashScreen == null) {
+                    splashScreen = getSplashScreen();
+                }
 
                 showSplashScreen = false;
             } else if (splashScreen != null) {
                 // Below line will be run when LOCATION_CHANGE. Which means the page load is almost completed.
                 splashScreen.hide();
             }
             hideHomePager();
         }
@@ -3018,36 +2917,16 @@ public class BrowserApp extends GeckoApp
                 for (final BrowserAppDelegate delegate : delegates) {
                     delegate.onActivityResult(this, requestCode, resultCode, data);
                 }
 
                 super.onActivityResult(requestCode, resultCode, data);
         }
     }
 
-    private void showFirstrunPager() {
-
-        if (mFirstrunAnimationContainer == null) {
-            final ViewStub firstrunPagerStub = (ViewStub) findViewById(R.id.firstrun_pager_stub);
-            mFirstrunAnimationContainer = (FirstrunAnimationContainer) firstrunPagerStub.inflate();
-            mFirstrunAnimationContainer.load(getApplicationContext(), getSupportFragmentManager());
-            mFirstrunAnimationContainer.registerOnFinishListener(new FirstrunAnimationContainer.OnFinishListener() {
-                @Override
-                public void onFinish() {
-                    if (mFirstrunAnimationContainer.showBrowserHint() &&
-                        !Tabs.hasHomepage(BrowserApp.this)) {
-                        enterEditingMode();
-                    }
-                }
-            });
-        }
-
-        mHomeScreenContainer.setVisibility(View.VISIBLE);
-    }
-
     private void showHomePager(String panelId, Bundle panelRestoreData) {
         showHomePagerWithAnimator(panelId, panelRestoreData, null);
     }
 
     private void showHomePagerWithAnimator(String panelId, Bundle panelRestoreData, PropertyAnimator animator) {
         if (isHomePagerVisible()) {
             // Home pager already visible, make sure it shows the correct panel.
             mHomeScreen.showPanel(panelId, panelRestoreData);
@@ -3169,25 +3048,22 @@ public class BrowserApp extends GeckoApp
     }
 
     /**
      * Hide the Onboarding pager on user action, and don't show any onFinish hints.
      * @param method TelemetryContract method by which action was taken
      * @return boolean of whether pager was visible
      */
     private boolean hideFirstrunPager(TelemetryContract.Method method) {
-        if (!isFirstrunVisible()) {
+        if (!mOnboardingHelper.hideOnboarding()) {
             return false;
         }
 
         Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, method, "firstrun-pane");
 
-        // Don't show any onFinish actions when hiding from this Activity.
-        mFirstrunAnimationContainer.registerOnFinishListener(null);
-        mFirstrunAnimationContainer.hide();
         return true;
     }
 
     /**
      * Hides the HomePager, using the url of the currently selected tab as the url to be
      * loaded.
      */
     private void hideHomePager() {
@@ -4629,9 +4505,40 @@ public class BrowserApp extends GeckoApp
     public void onLightweightThemeReset() {
         refreshStatusBarColor();
     }
 
     private void refreshStatusBarColor() {
         final boolean isPrivate = mBrowserToolbar.isPrivateMode();
         WindowUtil.setStatusBarColor(BrowserApp.this, isPrivate);
     }
+
+    @Override
+    public void onOnboardingProcessStarted() {
+        if (splashScreen == null) {
+            splashScreen = getSplashScreen();
+        }
+
+        splashScreen.show(OnboardingHelper.DELAY_SHOW_DEFAULT_ONBOARDING);
+    }
+
+    @Override
+    public void onOnboardingScreensVisible() {
+        mHomeScreenContainer.setVisibility(View.VISIBLE);
+
+        if (HardwareUtils.isTablet()) {
+            mTabStrip.setOnTabChangedListener(new BrowserApp.TabStripInterface.OnTabAddedOrRemovedListener() {
+                @Override
+                public void onTabChanged() {
+                    hideFirstrunPager(TelemetryContract.Method.BUTTON);
+                    mTabStrip.setOnTabChangedListener(null);
+                }
+            });
+        }
+    }
+
+    @Override
+    public void onFinishedOnboarding(final boolean showBrowserHint) {
+        if (showBrowserHint && !Tabs.hasHomepage(this)) {
+            enterEditingMode();
+        }
+    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -141,17 +141,17 @@ public abstract class GeckoApp extends G
     public static final String PREFS_CLEANUP_TEMP_FILES    = "cleanupTempFiles";
 
     /**
      * Used with SharedPreferences, per profile, to determine if this is the first run of
      * the application. When accessing SharedPreferences, the default value of true should be used.
 
      * Originally, this was only used for the telemetry core ping logic. To avoid
      * having to write custom migration logic, we just keep the original pref key.
-     * Be aware of {@link org.mozilla.gecko.fxa.EnvironmentUtils.GECKO_PREFS_IS_FIRST_RUN}.
+     * Be aware of {@link org.mozilla.gecko.fxa.EnvironmentUtils#GECKO_PREFS_IS_FIRST_RUN}.
      */
     public static final String PREFS_IS_FIRST_RUN = "telemetry-isFirstRun";
 
     public static final String SAVED_STATE_IN_BACKGROUND   = "inBackground";
     public static final String SAVED_STATE_PRIVATE_SESSION = "privateSession";
 
     // Delay before running one-time "cleanup" tasks that may be needed
     // after a version upgrade.
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstRunPanelConfigProviderStrategy.java
@@ -0,0 +1,13 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.gecko.firstrun;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+
+public interface FirstRunPanelConfigProviderStrategy {
+    PanelConfig getPanelConfig(@NonNull Context context, PanelConfig.TYPE panelConfigType, final boolean useLocalValues);
+}
--- a/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunAnimationContainer.java
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunAnimationContainer.java
@@ -10,56 +10,59 @@ import android.support.v4.app.FragmentMa
 import android.util.AttributeSet;
 
 import android.view.View;
 import android.widget.LinearLayout;
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ObjectAnimator;
 import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.Experiments;
 import org.mozilla.gecko.mma.MmaDelegate;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 
 /**
  * A container for the pager and the entire first run experience.
  * This is used for animation purposes.
  */
 public class FirstrunAnimationContainer extends LinearLayout {
     // See bug 1330714. Need NON_PREF_PREFIX to set from distribution.
     public static final String PREF_FIRSTRUN_ENABLED_OLD = "startpane_enabled";
     // After 57, the pref name will be changed. Thus all user since 57 will check this new pref.
     public static final String PREF_FIRSTRUN_ENABLED = GeckoPreferences.NON_PREF_PREFIX + "startpane_enabled_after_57";
 
-    public static interface OnFinishListener {
-        public void onFinish();
+    public interface OnFinishListener {
+        void onFinish();
     }
 
     private FirstrunPager pager;
     private boolean visible;
     private OnFinishListener onFinishListener;
 
     public FirstrunAnimationContainer(Context context) {
         this(context, null);
     }
     public FirstrunAnimationContainer(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
-    public void load(Context appContext, FragmentManager fm) {
+    public void load(Context appContext, FragmentManager fm, final boolean useLocalValues) {
         visible = true;
-        pager = (FirstrunPager) findViewById(R.id.firstrun_pager);
-        pager.load(appContext, fm, new OnFinishListener() {
+        pager = findViewById(R.id.firstrun_pager);
+        pager.load(appContext, fm, useLocalValues, new OnFinishListener() {
             @Override
             public void onFinish() {
                 hide();
             }
         });
+
+        if (useLocalValues) {
+            MmaDelegate.track(MmaDelegate.ONBOARDING_DEFAULT_VALUES);
+        } else {
+            MmaDelegate.track(MmaDelegate.ONBOARDING_REMOTE_VALUES);
+        }
     }
 
     public boolean isVisible() {
         return visible;
     }
 
     public void hide() {
 
--- a/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPager.java
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPager.java
@@ -58,25 +58,26 @@ public class FirstrunPager extends RtlVi
                     setCurrentItem(index, true);
                 }
             });
         }
 
         super.addView(child, index, params);
     }
 
-    public void load(Context appContext, FragmentManager fm, final FirstrunAnimationContainer.OnFinishListener onFinishListener) {
+    public void load(Context appContext, FragmentManager fm, final boolean useLocalValues,
+                     final FirstrunAnimationContainer.OnFinishListener onFinishListener) {
         final List<FirstrunPagerConfig.FirstrunPanelConfig> panels;
 
-        if (Restrictions.isRestrictedProfile(context)) {
-            panels = FirstrunPagerConfig.getRestricted();
-        } else if (FirefoxAccounts.firefoxAccountsExist(context)) {
-            panels = FirstrunPagerConfig.forFxAUser(appContext);
+        if (Restrictions.isRestrictedProfile(appContext)) {
+            panels = FirstrunPagerConfig.getRestricted(appContext);
+        } else if (FirefoxAccounts.firefoxAccountsExist(appContext)) {
+            panels = FirstrunPagerConfig.forFxAUser(appContext, useLocalValues);
         } else {
-            panels = FirstrunPagerConfig.getDefault(appContext);
+            panels = FirstrunPagerConfig.getDefault(appContext, useLocalValues);
         }
 
         setAdapter(new ViewPagerAdapter(fm, panels));
         this.pagerNavigation = new FirstrunPanel.PagerNavigation() {
             @Override
             public void next() {
                 final int currentPage = FirstrunPager.this.getCurrentItem();
                 if (currentPage < FirstrunPager.this.getAdapter().getCount() - 1) {
@@ -139,17 +140,17 @@ public class FirstrunPager extends RtlVi
         private final List<FirstrunPagerConfig.FirstrunPanelConfig> panels;
         private final Fragment[] fragments;
 
         public ViewPagerAdapter(FragmentManager fm, List<FirstrunPagerConfig.FirstrunPanelConfig> panels) {
             super(fm);
             this.panels = panels;
             this.fragments = new Fragment[panels.size()];
             for (FirstrunPagerConfig.FirstrunPanelConfig panel : panels) {
-                mDecor.onAddPagerView(context.getString(panel.getTitleRes()));
+                mDecor.onAddPagerView(panel.getTitle());
             }
 
             if (panels.size() > 0) {
                 mDecor.onPageSelected(0);
             }
         }
 
         @Override
@@ -167,12 +168,12 @@ public class FirstrunPager extends RtlVi
         @Override
         public int getCount() {
             return panels.size();
         }
 
         @Override
         public CharSequence getPageTitle(int i) {
             // Unused now that we use TabMenuStrip.
-            return context.getString(panels.get(i).getTitleRes()).toUpperCase();
+            return panels.get(i).getTitle().toUpperCase();
         }
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPagerConfig.java
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPagerConfig.java
@@ -1,100 +1,99 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.gecko.firstrun;
 
 import android.content.Context;
+import android.graphics.Bitmap;
 import android.os.Bundle;
-import android.util.Log;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.Experiments;
+import android.support.annotation.NonNull;
+
+import org.mozilla.gecko.mma.MmaDelegate;
 
 import java.util.LinkedList;
 import java.util.List;
 
-public class FirstrunPagerConfig {
-    public static final String LOGTAG = "FirstrunPagerConfig";
+class FirstrunPagerConfig {
+    static final String LOGTAG = "FirstrunPagerConfig";
 
-    public static final String KEY_IMAGE = "imageRes";
-    public static final String KEY_TEXT = "textRes";
-    public static final String KEY_SUBTEXT = "subtextRes";
+    static final String KEY_IMAGE = "panelImage";
+    static final String KEY_MESSAGE = "panelMessage";
+    static final String KEY_SUBTEXT = "panelDescription";
 
-   public static List<FirstrunPanelConfig> getDefault(Context context) {
+    static List<FirstrunPanelConfig> getDefault(Context context, final boolean useLocalValues) {
         final List<FirstrunPanelConfig> panels = new LinkedList<>();
-       panels.add(SimplePanelConfigs.welcomePanelConfig);
-       panels.add(SimplePanelConfigs.privatePanelConfig);
-       panels.add(SimplePanelConfigs.customizePanelConfig);
-       panels.add(SimplePanelConfigs.syncPanelConfig);
+        panels.add(FirstrunPanelConfig.getConfiguredPanel(context, PanelConfig.TYPE.WELCOME, useLocalValues));
+        panels.add(FirstrunPanelConfig.getConfiguredPanel(context, PanelConfig.TYPE.PRIVACY, useLocalValues));
+        panels.add(FirstrunPanelConfig.getConfiguredPanel(context, PanelConfig.TYPE.CUSTOMIZE, useLocalValues));
+        panels.add(FirstrunPanelConfig.getConfiguredPanel(context, PanelConfig.TYPE.SYNC, useLocalValues));
 
         return panels;
     }
 
-    public static List<FirstrunPanelConfig> forFxAUser(Context context) {
+    static List<FirstrunPanelConfig> forFxAUser(Context context, final boolean useLocalValues) {
         final List<FirstrunPanelConfig> panels = new LinkedList<>();
-        panels.add(SimplePanelConfigs.welcomePanelConfig);
-        panels.add(SimplePanelConfigs.privatePanelConfig);
-        panels.add(SimplePanelConfigs.customizeLastPanelConfig);
+        panels.add(FirstrunPanelConfig.getConfiguredPanel(context, PanelConfig.TYPE.WELCOME, useLocalValues));
+        panels.add(FirstrunPanelConfig.getConfiguredPanel(context, PanelConfig.TYPE.PRIVACY, useLocalValues));
+        panels.add(FirstrunPanelConfig.getConfiguredPanel(context, PanelConfig.TYPE.LAST_CUSTOMIZE, useLocalValues));
 
         return panels;
     }
 
-    public static List<FirstrunPanelConfig> getRestricted() {
+    static List<FirstrunPanelConfig> getRestricted(Context context) {
         final List<FirstrunPanelConfig> panels = new LinkedList<>();
-        panels.add(new FirstrunPanelConfig(RestrictedWelcomePanel.class.getName(), RestrictedWelcomePanel.TITLE_RES));
+        panels.add(new FirstrunPanelConfig(RestrictedWelcomePanel.class.getName(),
+                context.getString(RestrictedWelcomePanel.TITLE_RES)));
         return panels;
     }
 
-    public static class FirstrunPanelConfig {
-
+    static class FirstrunPanelConfig {
         private String classname;
-        private int titleRes;
+        private String title;
         private Bundle args;
 
-        public FirstrunPanelConfig(String resource, int titleRes) {
-            this(resource, titleRes, -1, -1, -1, true);
+        FirstrunPanelConfig(String resource, String title) {
+            this(resource, title, null, null, null, true);
         }
 
-        public FirstrunPanelConfig(String classname, int titleRes, int imageRes, int textRes, int subtextRes) {
-            this(classname, titleRes, imageRes, textRes, subtextRes, false);
-        }
-
-        private FirstrunPanelConfig(String classname, int titleRes, int imageRes, int textRes, int subtextRes, boolean isCustom) {
+        private FirstrunPanelConfig(String classname, String title, Bitmap image, String message,
+                                    String subtext, boolean isCustom) {
             this.classname = classname;
-            this.titleRes = titleRes;
+            this.title = title;
 
             if (!isCustom) {
-                this.args = new Bundle();
-                this.args.putInt(KEY_IMAGE, imageRes);
-                this.args.putInt(KEY_TEXT, textRes);
-                this.args.putInt(KEY_SUBTEXT, subtextRes);
+                args = new Bundle();
+                args.putParcelable(KEY_IMAGE, image);
+                args.putString(KEY_MESSAGE, message);
+                args.putString(KEY_SUBTEXT, subtext);
             }
         }
 
-        public String getClassname() {
-            return this.classname;
+        static FirstrunPanelConfig getConfiguredPanel(@NonNull Context context,
+                                                      PanelConfig.TYPE wantedPanelConfig,
+                                                      final boolean useLocalValues) {
+            PanelConfig panelConfig;
+            if (useLocalValues) {
+                panelConfig = new LocalFirstRunPanelProvider().getPanelConfig(context, wantedPanelConfig, useLocalValues);
+            } else {
+                panelConfig = new RemoteFirstRunPanelConfig().getPanelConfig(context, wantedPanelConfig, useLocalValues);
+            }
+            return new FirstrunPanelConfig(panelConfig.getClassName(), panelConfig.getTitle(),
+                    panelConfig.getImage(), panelConfig.getMessage(), panelConfig.getText(), false);
         }
 
-        public int getTitleRes() {
-            return this.titleRes;
+
+        String getClassname() {
+            return classname;
         }
 
-        public Bundle getArgs() {
+        String getTitle() {
+            return title;
+        }
+
+        Bundle getArgs() {
             return args;
         }
     }
-
-    private static class SimplePanelConfigs {
-        public static final FirstrunPanelConfig welcomePanelConfig = new FirstrunPanelConfig(FirstrunPanel.class.getName(), R.string.firstrun_panel_title_welcome, R.drawable.firstrun_welcome, R.string.firstrun_urlbar_message, R.string.firstrun_urlbar_subtext);
-        public static final FirstrunPanelConfig privatePanelConfig = new FirstrunPanelConfig(FirstrunPanel.class.getName(), R.string.firstrun_panel_title_privacy, R.drawable.firstrun_private, R.string.firstrun_privacy_message, R.string.firstrun_privacy_subtext);
-        public static final FirstrunPanelConfig customizePanelConfig = new FirstrunPanelConfig(FirstrunPanel.class.getName(), R.string.firstrun_panel_title_customize, R.drawable.firstrun_data, R.string.firstrun_customize_message, R.string.firstrun_customize_subtext);
-        public static final FirstrunPanelConfig customizeLastPanelConfig = new FirstrunPanelConfig(LastPanel.class.getName(), R.string.firstrun_panel_title_customize, R.drawable.firstrun_data, R.string.firstrun_customize_message, R.string.firstrun_customize_subtext);
-
-        public static final FirstrunPanelConfig syncPanelConfig = new FirstrunPanelConfig(SyncPanel.class.getName(), R.string.firstrun_sync_title, R.drawable.firstrun_sync, R.string.firstrun_sync_message, R.string.firstrun_sync_subtext);
-
-    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPanel.java
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPanel.java
@@ -1,49 +1,50 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.gecko.firstrun;
 
+import android.graphics.Bitmap;
 import android.os.Bundle;
 import android.support.v4.app.Fragment;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ImageView;
 import android.widget.TextView;
+
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 
 /**
  * Base class for our first run pages. We call these FirstrunPanel for consistency
  * with HomePager/HomePanel.
  *
  * @see FirstrunPager for the containing pager.
  */
 public class FirstrunPanel extends Fragment {
 
-    public static final int TITLE_RES = -1;
     protected boolean showBrowserHint = true;
 
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance) {
         final ViewGroup root = (ViewGroup) inflater.inflate(R.layout.firstrun_basepanel_checkable_fragment, container, false);
         final Bundle args = getArguments();
         if (args != null) {
-            final int imageRes = args.getInt(FirstrunPagerConfig.KEY_IMAGE);
-            final int textRes = args.getInt(FirstrunPagerConfig.KEY_TEXT);
-            final int subtextRes = args.getInt(FirstrunPagerConfig.KEY_SUBTEXT);
+            final Bitmap image = args.getParcelable(FirstrunPagerConfig.KEY_IMAGE);
+            final String message = args.getString(FirstrunPagerConfig.KEY_MESSAGE);
+            final String subtext = args.getString(FirstrunPagerConfig.KEY_SUBTEXT);
 
-            ((ImageView) root.findViewById(R.id.firstrun_image)).setImageResource(imageRes);
-            ((TextView) root.findViewById(R.id.firstrun_text)).setText(textRes);
-            ((TextView) root.findViewById(R.id.firstrun_subtext)).setText(subtextRes);
+            ((ImageView) root.findViewById(R.id.firstrun_image)).setImageBitmap(image);
+            ((TextView) root.findViewById(R.id.firstrun_text)).setText(message);
+            ((TextView) root.findViewById(R.id.firstrun_subtext)).setText(subtext);
         }
 
         root.findViewById(R.id.firstrun_link).setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
                 Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "firstrun-next");
                 pagerNavigation.next();
             }
--- a/mobile/android/base/java/org/mozilla/gecko/firstrun/LastPanel.java
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/LastPanel.java
@@ -1,15 +1,16 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.gecko.firstrun;
 
+import android.graphics.Bitmap;
 import android.os.Bundle;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ImageView;
 import android.widget.TextView;
 
 import org.mozilla.gecko.R;
@@ -17,31 +18,29 @@ import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 
 public class LastPanel extends FirstrunPanel {
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance) {
         final ViewGroup root = (ViewGroup) inflater.inflate(R.layout.firstrun_basepanel_checkable_fragment, container, false);
         final Bundle args = getArguments();
         if (args != null) {
-            final int imageRes = args.getInt(FirstrunPagerConfig.KEY_IMAGE);
-            final int textRes = args.getInt(FirstrunPagerConfig.KEY_TEXT);
-            final int subtextRes = args.getInt(FirstrunPagerConfig.KEY_SUBTEXT);
+            final Bitmap image = args.getParcelable(FirstrunPagerConfig.KEY_IMAGE);
+            final String message = args.getString(FirstrunPagerConfig.KEY_MESSAGE);
+            final String subtext = args.getString(FirstrunPagerConfig.KEY_SUBTEXT);
 
-            ((ImageView) root.findViewById(R.id.firstrun_image)).setImageResource(imageRes);
-            ((TextView) root.findViewById(R.id.firstrun_text)).setText(textRes);
-            ((TextView) root.findViewById(R.id.firstrun_subtext)).setText(subtextRes);
+            ((ImageView) root.findViewById(R.id.firstrun_image)).setImageBitmap(image);
+            ((TextView) root.findViewById(R.id.firstrun_text)).setText(message);
+            ((TextView) root.findViewById(R.id.firstrun_subtext)).setText(subtext);
             ((TextView) root.findViewById(R.id.firstrun_link)).setText(R.string.firstrun_welcome_button_browser);
-
         }
 
         root.findViewById(R.id.firstrun_link).setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
                 Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "firstrun-next");
                 close();
             }
         });
 
-
         return root;
     }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/LocalFirstRunPanelProvider.java
@@ -0,0 +1,47 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.gecko.firstrun;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.BitmapFactory;
+import android.support.annotation.NonNull;
+
+import org.mozilla.gecko.R;
+
+public class LocalFirstRunPanelProvider implements FirstRunPanelConfigProviderStrategy {
+    public PanelConfig getPanelConfig(@NonNull Context context, PanelConfig.TYPE type, final boolean useLocalValues) {
+        final Resources resources = context.getResources();
+        switch (type) {
+            case WELCOME:
+                return new PanelConfig(type, useLocalValues, resources.getString(R.string.firstrun_panel_title_welcome),
+                        resources.getString(R.string.firstrun_urlbar_message),
+                        resources.getString(R.string.firstrun_urlbar_subtext),
+                        BitmapFactory.decodeResource(resources, R.drawable.firstrun_welcome));
+            case PRIVACY:
+                return new PanelConfig(type, useLocalValues, resources.getString(R.string.firstrun_panel_title_privacy),
+                        resources.getString(R.string.firstrun_privacy_message),
+                        resources.getString(R.string.firstrun_privacy_subtext),
+                        BitmapFactory.decodeResource(resources, R.drawable.firstrun_private));
+            case CUSTOMIZE:
+            case LAST_CUSTOMIZE:
+                return new PanelConfig(type, useLocalValues, resources.getString(R.string.firstrun_panel_title_customize),
+                        resources.getString(R.string.firstrun_customize_message),
+                        resources.getString(R.string.firstrun_customize_subtext),
+                        BitmapFactory.decodeResource(resources, R.drawable.firstrun_data));
+            case SYNC:
+                return new PanelConfig(type, useLocalValues, resources.getString(R.string.firstrun_sync_title),
+                        resources.getString(R.string.firstrun_sync_message),
+                        resources.getString(R.string.firstrun_sync_subtext),
+                        BitmapFactory.decodeResource(resources, R.drawable.firstrun_sync));
+            default:    // This will also be the case for "WELCOME"
+                return new PanelConfig(type, useLocalValues, resources.getString(R.string.firstrun_panel_title_welcome),
+                        resources.getString(R.string.firstrun_urlbar_message),
+                        resources.getString(R.string.firstrun_urlbar_subtext),
+                        BitmapFactory.decodeResource(resources, R.drawable.firstrun_welcome));
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/OnboardingHelper.java
@@ -0,0 +1,354 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.gecko.firstrun;
+
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.StrictMode;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.UiThread;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.view.ViewStub;
+
+import org.mozilla.gecko.GeckoSharedPrefs;
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.Telemetry;
+import org.mozilla.gecko.TelemetryContract;
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.distribution.Distribution;
+import org.mozilla.gecko.mma.MmaDelegate;
+import org.mozilla.gecko.mozglue.SafeIntent;
+import org.mozilla.gecko.switchboard.SwitchBoard;
+import org.mozilla.gecko.util.NetworkUtils;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import java.lang.ref.WeakReference;
+import java.util.UUID;
+
+/**
+ * Helper class of an an {@link AppCompatActivity} for managing showing the Onboarding screens.
+ * <br>The user class will have to implement {@link OnboardingListener}.
+ */
+public class OnboardingHelper implements MmaDelegate.MmaVariablesChangedListener,
+                                         SwitchBoard.ConfigStatusListener {
+    private static final String LOGTAG = "OnboardingHelper";
+    private static final boolean DEBUG = false;
+
+    @RobocopTarget
+    public static final String EXTRA_SKIP_STARTPANE = "skipstartpane";
+
+    /** Be aware of {@link org.mozilla.gecko.fxa.EnvironmentUtils#GECKO_PREFS_FIRSTRUN_UUID}. */
+    private static final String FIRSTRUN_UUID = "firstrun_uuid";
+
+    // Speculative timeout for showing the Onboarding screens with the default local values.
+    public static final int DELAY_SHOW_DEFAULT_ONBOARDING = 3 * 1000;
+
+    private WeakReference<AppCompatActivity> activityRef;
+    private OnboardingListener listener;
+    private SafeIntent activityStartingIntent;
+    private FirstrunAnimationContainer firstrunAnimationContainer;
+    private Runnable showOnboarding;
+    private boolean onboardingIsPreparing;
+    private boolean abortOnboarding;
+    private long startTimeForCheckingOnlineVariables;
+
+    public OnboardingHelper(
+            @NonNull final AppCompatActivity activity,
+            @NonNull final SafeIntent activityStartingIntent)
+            throws IllegalArgumentException {
+
+        if (!(activity instanceof OnboardingListener)) {
+            final String activityClass = activity.getClass().getSimpleName();
+            final String listenerInterface = OnboardingListener.class.getSimpleName();
+            throw new IllegalArgumentException(
+                    String.format("%s does not implement %s", activityClass, listenerInterface));
+        }
+
+        this.activityRef = new WeakReference<>(activity);
+        this.listener = (OnboardingListener) activity;
+        this.activityStartingIntent = activityStartingIntent;
+    }
+
+    /**
+     * Check and show the firstrun pane if the browser has never been launched and
+     * is not opening an external link from another application.
+     */
+    public void checkFirstRun() {
+        if (GeckoThread.getActiveProfile().inGuestMode()) {
+            // We do not want to show any first run tour for guest profiles.
+            return;
+        }
+
+        if (activityStartingIntent.getBooleanExtra(EXTRA_SKIP_STARTPANE, false)) {
+            // Note that we don't set the pref, so subsequent launches can result
+            // in the firstrun pane being shown.
+            return;
+        }
+
+        final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
+
+        try {
+            AppCompatActivity activity = activityRef.get();
+            if (activity == null) {
+                return;
+            }
+            final SharedPreferences prefs = GeckoSharedPrefs.forProfile(activity);
+
+            if (prefs.getBoolean(FirstrunAnimationContainer.PREF_FIRSTRUN_ENABLED_OLD, true) &&
+                    prefs.getBoolean(FirstrunAnimationContainer.PREF_FIRSTRUN_ENABLED, true)) {
+
+                onboardingIsPreparing = true;
+                listener.onOnboardingProcessStarted();
+
+                // Allow the activity to be gc'ed while waiting for the distribution
+                activity = null;
+
+                if (!Intent.ACTION_VIEW.equals(activityStartingIntent.getAction())) {
+                    // Check to see if a distribution has turned off the first run pager.
+                    final Distribution distribution = Distribution.getInstance(activityRef.get());
+                    if (!distribution.shouldWaitForSystemDistribution()) {
+                        checkFirstrunInternal();
+                    } else {
+                        distribution.addOnDistributionReadyCallback(new Distribution.ReadyCallback() {
+                            @Override
+                            public void distributionNotFound() {
+                                ThreadUtils.postToUiThread(new Runnable() {
+                                    @Override
+                                    public void run() {
+                                        checkFirstrunInternal();
+                                    }
+                                });
+                            }
+
+                            @Override
+                            public void distributionFound(final Distribution distribution) {
+                                // Check preference again in case distribution turned it off.
+                                if (prefs.getBoolean(FirstrunAnimationContainer.PREF_FIRSTRUN_ENABLED, true)) {
+                                    ThreadUtils.postToUiThread(new Runnable() {
+                                        @Override
+                                        public void run() {
+                                            checkFirstrunInternal();
+                                        }
+                                    });
+                                }
+                            }
+
+                            @Override
+                            public void distributionArrivedLate(final Distribution distribution) {
+                            }
+                        });
+                    }
+                }
+
+                // We have no intention of stopping this session. The FIRSTRUN session
+                // ends when the browsing session/activity has ended. All events
+                // during firstrun will be tagged as FIRSTRUN.
+                Telemetry.startUISession(TelemetryContract.Session.FIRSTRUN);
+            }
+        } finally {
+            StrictMode.setThreadPolicy(savedPolicy);
+        }
+    }
+
+    /**
+     * Call this to prevent or finish displaying of the Onboarding process.<br>
+     * If it has not yet been shown to the user and now it has been prevented to,
+     * showing the Onboarding screens will be retried at the next app start.
+     *
+     * @return whether Onboarding was prevented / finished early or not.
+     */
+    public boolean hideOnboarding() {
+        abortOnboarding = true;
+
+        if (DEBUG) {
+            Log.d(LOGTAG, "hideOnboarding");
+        }
+
+        if (isPreparing()) {
+            onboardingIsPreparing = false;
+            // Cancel showing Onboarding. Will retry automatically at the next app startup.
+            ThreadUtils.removeCallbacksFromUiThread(showOnboarding);
+            return true;
+        }
+
+        if (isOnboardingVisible()) {
+            onboardingIsPreparing = false;
+            firstrunAnimationContainer.registerOnFinishListener(null);
+            firstrunAnimationContainer.hide();
+            return true;
+        }
+
+        return false;
+    }
+
+    private boolean isOnboardingVisible() {
+        return firstrunAnimationContainer != null && firstrunAnimationContainer.isVisible();
+    }
+
+    /**
+     * Get if we are in the process of preparing the Onboarding screens.<br>
+     * If the Onboarding screens should be shown to the user, they will be so after a small delay -
+     * up to {@link #DELAY_SHOW_DEFAULT_ONBOARDING} necessary for downloading the data
+     * needed to populate the screens.
+     *
+     * @return <code>true</code> - we are preparing for showing Onboarding but haven't yet done
+     *         <code>false</code> - Onboarding has been displayed
+     */
+    public boolean isPreparing() {
+        return onboardingIsPreparing;
+    }
+
+    /**
+     * Code to actually show the first run pager, separated for distribution purposes.<br>
+     * If network is available it will first try to use server values for populating the
+     * onboarding screens. If that isn't possible the default local values will be used.
+     */
+    @UiThread
+    private void checkFirstrunInternal() {
+        final AppCompatActivity activity = activityRef.get();
+        if (activity == null) {
+            return;
+        }
+
+        if (abortOnboarding) {
+            return;
+        }
+
+        if (NetworkUtils.isConnected(activity)) {
+            showOnboarding = new Runnable() {
+                @Override
+                public void run() {
+                    showFirstrunPager(true);
+                }
+            };
+
+            if (DEBUG) {
+                startTimeForCheckingOnlineVariables = System.currentTimeMillis();
+            }
+
+            ThreadUtils.postDelayedToUiThread(showOnboarding, DELAY_SHOW_DEFAULT_ONBOARDING);
+        } else {
+            showFirstrunPager(true);
+        }
+    }
+
+    private void showFirstrunPager(final boolean useLocalValues) {
+        final AppCompatActivity activity = activityRef.get();
+        if (activity == null) {
+            return;
+        }
+
+        onboardingIsPreparing = false;
+
+        if (firstrunAnimationContainer == null) {
+            final ViewStub firstrunPagerStub = (ViewStub) activity.findViewById(R.id.firstrun_pager_stub);
+            firstrunAnimationContainer = (FirstrunAnimationContainer) firstrunPagerStub.inflate();
+        }
+
+        if (DEBUG) {
+            final StringBuilder logMessage =
+                    new StringBuilder("Will show Onboarding using ")
+                            .append((useLocalValues ? "local" : "server"))
+                            .append(" values");
+            Log.d(LOGTAG, logMessage.toString());
+        }
+
+        firstrunAnimationContainer.load
+                (activity.getApplicationContext(), activity.getSupportFragmentManager(), useLocalValues);
+        firstrunAnimationContainer.registerOnFinishListener(new FirstrunAnimationContainer.OnFinishListener() {
+            @Override
+            public void onFinish() {
+                listener.onFinishedOnboarding(firstrunAnimationContainer.showBrowserHint());
+            }
+        });
+
+        listener.onOnboardingScreensVisible();
+        saveOnboardingShownStatus();
+    }
+
+    // The Onboarding screens should only be shown one time.
+    private void saveOnboardingShownStatus() {
+        // The method is called serially from showFirstrunPager()
+        // which stores a hard reference to the activity so it's safe to use it directly
+        final SharedPreferences prefs = GeckoSharedPrefs.forProfile(activityRef.get());
+
+        prefs.edit()
+                // Don't bother trying again to show the v1 minimal first run.
+                .putBoolean(FirstrunAnimationContainer.PREF_FIRSTRUN_ENABLED, false)
+                // Generate a unique identifier for the current first run.
+                // See Bug 1429735 for why we care to do this.
+                .putString(FIRSTRUN_UUID, UUID.randomUUID().toString())
+                .apply();
+    }
+
+    /**
+     * Try showing the Onboarding screens even before #DELAY_SHOW_DEFAULT_ONBOARDING.<br>
+     * If they have already been shown calling this method has no effect.
+     */
+    private void tryShowOnboarding(final boolean shouldUseLocalValues) {
+        final AppCompatActivity activity = activityRef.get();
+        if (activity == null) {
+            return;
+        }
+
+        if (isPreparing()) {
+            ThreadUtils.removeCallbacksFromUiThread(showOnboarding);
+            showFirstrunPager(shouldUseLocalValues);
+        }
+    }
+
+    @Override
+    @MainThread
+    public void onRemoteVariablesChanged() {
+        if (DEBUG) {
+            final long timeElapsed = System.currentTimeMillis() - startTimeForCheckingOnlineVariables;
+            Log.d(LOGTAG, String.format("Got online variables after: %d millis", timeElapsed));
+        }
+
+        tryShowOnboarding(false);
+    }
+
+    @Override
+    @MainThread
+    public void onRemoteVariablesUnavailable() {
+        tryShowOnboarding(true);
+    }
+
+    @Override
+    @MainThread
+    public void onExperimentsConfigLoaded() {
+        final AppCompatActivity activity = activityRef.get();
+        if (activity == null) {
+            return;
+        }
+
+        // Only if the Mma experiment is available we should continue to wait for server values.
+        if (!MmaDelegate.isMmaExperimentEnabled(activity)) {
+            tryShowOnboarding(true);
+        }
+    }
+
+    @Override
+    @MainThread
+    public void onExperimentsConfigLoadFailed() {
+        tryShowOnboarding(true);
+    }
+
+    /**
+     * Informs about the status of the onboarding process.
+     */
+    public interface OnboardingListener {
+
+        void onOnboardingProcessStarted();
+
+        void onOnboardingScreensVisible();
+
+        void onFinishedOnboarding(final boolean showBrowserHint);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/PanelConfig.java
@@ -0,0 +1,72 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.gecko.firstrun;
+
+import android.graphics.Bitmap;
+
+/**
+ * Onboarding screens configuration object.
+ */
+public class PanelConfig {
+    public enum TYPE {
+        WELCOME, PRIVACY, CUSTOMIZE, LAST_CUSTOMIZE, SYNC
+    }
+
+    private final TYPE type;
+    private final boolean useLocalValues;
+    private final String title;
+    private final String message;
+    private final String text;
+    private final Bitmap image;
+
+    public PanelConfig(TYPE type, boolean useLocalValues, String title, String message, String text, Bitmap image) {
+        this.type = type;
+        this.useLocalValues = useLocalValues;
+        this.title = title;
+        this.message = message;
+        this.text = text;
+        this.image = image;
+    }
+
+    public String getClassName() {
+        switch (type) {
+            case WELCOME:
+            case PRIVACY:
+            case CUSTOMIZE:
+                return FirstrunPanel.class.getName();
+            case LAST_CUSTOMIZE:
+                return LastPanel.class.getName();
+            case SYNC:
+                return SyncPanel.class.getName();
+            default:    // Return the default Panel, same as for "WELCOME"
+                return FirstrunPanel.class.getName();
+        }
+    }
+
+    public TYPE getType() {
+        return type;
+    }
+
+    public boolean isUsingLocalValues() {
+        return useLocalValues;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public String getText() {
+        return text;
+    }
+
+    public Bitmap getImage() {
+        return image;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/RemoteFirstRunPanelConfig.java
@@ -0,0 +1,18 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.gecko.firstrun;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+
+import org.mozilla.gecko.mma.MmaDelegate;
+
+public class RemoteFirstRunPanelConfig implements FirstRunPanelConfigProviderStrategy {
+    @Override
+    public PanelConfig getPanelConfig(@NonNull Context context, PanelConfig.TYPE type, final boolean useLocalValues) {
+        return MmaDelegate.getPanelConfig(context, type, useLocalValues);
+    }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/firstrun/SyncPanel.java
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/SyncPanel.java
@@ -1,16 +1,17 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.gecko.firstrun;
 
 import android.content.Intent;
+import android.graphics.Bitmap;
 import android.os.Bundle;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ImageView;
 import android.widget.TextView;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
@@ -19,23 +20,23 @@ import org.mozilla.gecko.fxa.FxAccountCo
 import org.mozilla.gecko.fxa.activities.FxAccountWebFlowActivity;
 
 public class SyncPanel extends FirstrunPanel {
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance) {
         final ViewGroup root = (ViewGroup) inflater.inflate(R.layout.firstrun_sync_fragment, container, false);
         final Bundle args = getArguments();
         if (args != null) {
-            final int imageRes = args.getInt(FirstrunPagerConfig.KEY_IMAGE);
-            final int textRes = args.getInt(FirstrunPagerConfig.KEY_TEXT);
-            final int subtextRes = args.getInt(FirstrunPagerConfig.KEY_SUBTEXT);
+            final Bitmap image = args.getParcelable(FirstrunPagerConfig.KEY_IMAGE);
+            final String message = args.getString(FirstrunPagerConfig.KEY_MESSAGE);
+            final String subtext = args.getString(FirstrunPagerConfig.KEY_SUBTEXT);
 
-            ((ImageView) root.findViewById(R.id.firstrun_image)).setImageResource(imageRes);
-            ((TextView) root.findViewById(R.id.firstrun_text)).setText(textRes);
-            ((TextView) root.findViewById(R.id.firstrun_subtext)).setText(subtextRes);
+            ((ImageView) root.findViewById(R.id.firstrun_image)).setImageBitmap(image);
+            ((TextView) root.findViewById(R.id.firstrun_text)).setText(message);
+            ((TextView) root.findViewById(R.id.firstrun_subtext)).setText(subtext);
         }
 
         root.findViewById(R.id.welcome_account).setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
                 Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "firstrun-sync");
                 showBrowserHint = false;
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/mma/LeanplumVariables.java
@@ -0,0 +1,122 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.gecko.mma;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+
+import com.leanplum.annotations.Variable;
+
+import org.mozilla.gecko.R;
+
+import java.lang.reflect.Field;
+
+/**
+ * Unified repo for all LeanPlum variables.<br>
+ * <ul>To make them appear in the LP dashboard and get new values from the server
+ *      <li>they must be annotated with {@link com.leanplum.annotations.Variable}.</li>
+ *      <li>they need to be parsed with {@link com.leanplum.annotations.Parser} after {@link com.leanplum.Leanplum#setApplicationContext(Context)}</li>
+ * </ul>
+ * Although some fields are public (LP SDK limitation) they are not to be written into.
+ *
+ * @see <a href="https://docs.leanplum.com/reference#defining-variables">Official LP variables documentation</a>
+ */
+public class LeanplumVariables {
+    private static LeanplumVariables INSTANCE;
+    private static Resources appResources;
+
+    private static final String FIRSTRUN_WELCOME_PANEL_GROUP_NAME = "FirstRun Welcome Panel";
+    private static final String FIRSTRUN_PRIVACY_PANEL_GROUP_NAME = "FirstRun Privacy Panel";
+    private static final String FIRSTRUN_CUSTOMIZE_PANEL_GROUP_NAME = "FirstRun Customize Panel";
+    private static final String FIRSTRUN_SYNC_PANEL_GROUP_NAME = "FirstRun Sync Panel";
+
+    @Variable(group = FIRSTRUN_WELCOME_PANEL_GROUP_NAME) public static String welcomePanelTitle;
+    @Variable(group = FIRSTRUN_WELCOME_PANEL_GROUP_NAME) public static String welcomePanelMessage;
+    @Variable(group = FIRSTRUN_WELCOME_PANEL_GROUP_NAME) public static String welcomePanelSubtext;
+    @DrawableRes private static int welcomeDrawableId;
+
+    @Variable(group = FIRSTRUN_PRIVACY_PANEL_GROUP_NAME) public static String privacyPanelTitle;
+    @Variable(group = FIRSTRUN_PRIVACY_PANEL_GROUP_NAME) public static String privacyPanelMessage;
+    @Variable(group = FIRSTRUN_PRIVACY_PANEL_GROUP_NAME) public static String privacyPanelSubtext;
+    @DrawableRes private static int privacyDrawableId;
+
+    @Variable(group = FIRSTRUN_CUSTOMIZE_PANEL_GROUP_NAME) public static String customizePanelTitle;
+    @Variable(group = FIRSTRUN_CUSTOMIZE_PANEL_GROUP_NAME) public static String customizePanelMessage;
+    @Variable(group = FIRSTRUN_CUSTOMIZE_PANEL_GROUP_NAME) public static String customizePanelSubtext;
+    @DrawableRes private static int customizingDrawableId;
+
+    @Variable(group = FIRSTRUN_SYNC_PANEL_GROUP_NAME) public static String syncPanelTitle;
+    @Variable(group = FIRSTRUN_SYNC_PANEL_GROUP_NAME) public static String syncPanelMessage;
+    @Variable(group = FIRSTRUN_SYNC_PANEL_GROUP_NAME) public static String syncPanelSubtext;
+    @DrawableRes private static int syncDrawableId;
+
+    /**
+     * Allows constructing and/or returning an already constructed instance of this class
+     * which has all it's fields populated with values from Resources.<br><br>
+     *
+     * An instance of this class needs exist to allow overwriting it's fields with downloaded values from LeanPlum
+     * @see com.leanplum.annotations.Parser#defineFileVariable(Object, String, String, Field)
+     */
+    public static LeanplumVariables getInstance(Context appContext) {
+        // Simple Singleton as we don't expect concurrency problems.
+        if (INSTANCE == null) {
+            INSTANCE = new LeanplumVariables(appContext);
+        }
+
+        return INSTANCE;
+    }
+
+    /**
+     * Allows setting default values for instance variables.
+     * @param context used to access application resources
+     */
+    private LeanplumVariables(@NonNull Context context) {
+        appResources = context.getResources();
+        welcomePanelTitle = appResources.getString(R.string.firstrun_panel_title_welcome);
+        welcomePanelMessage = appResources.getString(R.string.firstrun_urlbar_message);
+        welcomePanelSubtext = appResources.getString(R.string.firstrun_urlbar_subtext);
+        welcomeDrawableId = R.drawable.firstrun_welcome;
+
+        privacyPanelTitle = appResources.getString(R.string.firstrun_panel_title_privacy);
+        privacyPanelMessage = appResources.getString(R.string.firstrun_privacy_message);
+        privacyPanelSubtext = appResources.getString(R.string.firstrun_privacy_subtext);
+        privacyDrawableId = R.drawable.firstrun_private;
+
+        customizePanelTitle = appResources.getString(R.string.firstrun_panel_title_customize);
+        customizePanelMessage = appResources.getString(R.string.firstrun_customize_message);
+        customizePanelSubtext = appResources.getString(R.string.firstrun_customize_subtext);
+        customizingDrawableId = R.drawable.firstrun_data;
+
+        syncPanelTitle = appResources.getString(R.string.firstrun_sync_title);
+        syncPanelMessage = appResources.getString(R.string.firstrun_sync_message);
+        syncPanelSubtext = appResources.getString(R.string.firstrun_sync_subtext);
+        syncDrawableId = R.drawable.firstrun_sync;
+    }
+
+    public static Bitmap getWelcomeImage() {
+        return getBitmapFromMmaVar(welcomeDrawableId);
+    }
+
+    public static Bitmap getPrivacyImage() {
+        return getBitmapFromMmaVar(privacyDrawableId);
+    }
+
+    public static Bitmap getCustomizingImage() {
+        return getBitmapFromMmaVar(customizingDrawableId);
+    }
+
+    public static Bitmap getSyncImage() {
+        return getBitmapFromMmaVar(syncDrawableId);
+    }
+
+    private static Bitmap getBitmapFromMmaVar(@DrawableRes final int drawableRes) {
+        return BitmapFactory.decodeResource(appResources, drawableRes);
+    }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/mma/MmaDelegate.java
+++ b/mobile/android/base/java/org/mozilla/gecko/mma/MmaDelegate.java
@@ -19,20 +19,22 @@ import android.text.TextUtils;
 
 import org.mozilla.gecko.Experiments;
 import org.mozilla.gecko.MmaConstants;
 import org.mozilla.gecko.PrefsHelper;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.activitystream.homepanel.ActivityStreamConfiguration;
+import org.mozilla.gecko.firstrun.PanelConfig;
 import org.mozilla.gecko.fxa.FirefoxAccounts;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 import org.mozilla.gecko.switchboard.SwitchBoard;
 import org.mozilla.gecko.util.ContextUtils;
+import org.mozilla.gecko.util.ThreadUtils;
 
 import java.util.HashMap;
 import java.util.Map;
 import java.util.UUID;
 
 
 public class MmaDelegate {
 
@@ -42,16 +44,18 @@ public class MmaDelegate {
     public static final String SAVED_BOOKMARK = "E_Saved_Bookmark";
     public static final String OPENED_BOOKMARK = "E_Opened_Bookmark";
     public static final String INTERACT_WITH_SEARCH_URL_AREA = "E_Interact_With_Search_URL_Area";
     public static final String SCREENSHOT = "E_Screenshot";
     public static final String SAVED_LOGIN_AND_PASSWORD = "E_Saved_Login_And_Password";
     public static final String RESUMED_FROM_BACKGROUND = "E_Resumed_From_Background";
     public static final String NEW_TAB = "E_Opened_New_Tab";
     public static final String DISMISS_ONBOARDING = "E_Dismiss_Onboarding";
+    public static final String ONBOARDING_DEFAULT_VALUES = "E_Onboarding_With_Default_Values";
+    public static final String ONBOARDING_REMOTE_VALUES = "E_Onboarding_With_Remote_Values";
 
     private static final String LAUNCH_BUT_NOT_DEFAULT_BROWSER = "E_Launch_But_Not_Default_Browser";
     private static final String LAUNCH_BROWSER = "E_Launch_Browser";
     private static final String CHANGED_DEFAULT_TO_FENNEC = "E_Changed_Default_To_Fennec";
     private static final String INSTALLED_FOCUS = "E_Just_Installed_Focus";
     private static final String INSTALLED_KLAR = "E_Just_Installed_Klar";
 
     private static final String USER_ATT_FOCUS_INSTALLED = "Focus Installed";
@@ -70,33 +74,40 @@ public class MmaDelegate {
     public static final String KEY_ANDROID_PREF_STRING_LEANPLUM_DEVICE_ID = "android.not_a_preference.leanplum.device_id";
     private static final String KEY_ANDROID_PREF_BOOLEAN_FENNEC_IS_DEFAULT = "android.not_a_preference.fennec.default.browser.status";
 
     private static final String DEBUG_LEANPLUM_DEVICE_ID = "8effda84-99df-11e7-abc4-cec278b6b50a";
 
     private static final MmaInterface mmaHelper = MmaConstants.getMma();
     private static Context applicationContext;
 
-    public static void init(Activity activity) {
+    public static void init(final Activity activity,
+                            final MmaVariablesChangedListener remoteVariablesListener) {
         applicationContext = activity.getApplicationContext();
         // Since user attributes are gathered in Fennec, not in MMA implementation,
         // we gather the information here then pass to mmaHelper.init()
         // Note that generateUserAttribute always return a non null HashMap.
         final Map<String, Object> attributes = gatherUserAttributes(activity);
         final String deviceId = getDeviceId(activity);
         mmaHelper.setDeviceId(deviceId);
         PrefsHelper.setPref(GeckoPreferences.PREFS_MMA_DEVICE_ID, deviceId);
         // above two config setup required to be invoked before mmaHelper.init.
         mmaHelper.init(activity, attributes);
 
         if (!isDefaultBrowser(activity)) {
             mmaHelper.event(MmaDelegate.LAUNCH_BUT_NOT_DEFAULT_BROWSER);
         }
         mmaHelper.event(MmaDelegate.LAUNCH_BROWSER);
 
+        ThreadUtils.postToUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mmaHelper.listenOnceForVariableChanges(remoteVariablesListener);
+            }
+        });
     }
 
     public static void stop() {
         mmaHelper.stop();
     }
 
     /* This method must be called at background thread to avoid performance issues in some API level */
     @NonNull
@@ -206,22 +217,32 @@ public class MmaDelegate {
         if (isMmaAllowed(context)) {
             mmaHelper.setCustomIcon(R.drawable.ic_status_logo);
             return mmaHelper.handleGcmMessage(context, from, bundle);
         } else {
             return false;
         }
     }
 
+    public static PanelConfig getPanelConfig(@NonNull Context context, PanelConfig.TYPE panelConfigType, final boolean useLocalValues) {
+        return mmaHelper.getPanelConfig(context, panelConfigType, useLocalValues);
+    }
+
     private static String getDeviceId(Activity activity) {
         if (SwitchBoard.isInExperiment(activity, Experiments.LEANPLUM_DEBUG)) {
             return DEBUG_LEANPLUM_DEVICE_ID;
         }
 
         final SharedPreferences sharedPreferences = activity.getPreferences(Context.MODE_PRIVATE);
         String deviceId = sharedPreferences.getString(KEY_ANDROID_PREF_STRING_LEANPLUM_DEVICE_ID, null);
         if (deviceId == null) {
             deviceId = UUID.randomUUID().toString();
             sharedPreferences.edit().putString(KEY_ANDROID_PREF_STRING_LEANPLUM_DEVICE_ID, deviceId).apply();
         }
         return deviceId;
     }
+
+    public interface MmaVariablesChangedListener {
+        void onRemoteVariablesChanged();
+
+        void onRemoteVariablesUnavailable();
+    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/mma/MmaInterface.java
+++ b/mobile/android/base/java/org/mozilla/gecko/mma/MmaInterface.java
@@ -8,16 +8,18 @@ package org.mozilla.gecko.mma;
 
 import android.app.Activity;
 import android.content.Context;
 import android.os.Bundle;
 import android.support.annotation.CheckResult;
 import android.support.annotation.DrawableRes;
 import android.support.annotation.NonNull;
 
+import org.mozilla.gecko.firstrun.PanelConfig;
+
 import java.util.Map;
 
 
 public interface MmaInterface {
 
     void init(Activity Activity, Map<String, ?> attributes);
 
     void setCustomIcon(@DrawableRes int iconResId);
@@ -28,9 +30,13 @@ public interface MmaInterface {
 
     void event(String mmaEvent, double value);
 
     void stop();
 
     @CheckResult boolean handleGcmMessage(Context context, String from, Bundle bundle);
 
     void setDeviceId(@NonNull String deviceId);
+
+    PanelConfig getPanelConfig(@NonNull Context context, PanelConfig.TYPE panelConfigType, final boolean useLocalValues);
+
+    void listenOnceForVariableChanges(@NonNull final MmaDelegate.MmaVariablesChangedListener listener);
 }
--- a/mobile/android/base/java/org/mozilla/gecko/mma/MmaLeanplumImp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/mma/MmaLeanplumImp.java
@@ -4,35 +4,36 @@
  * 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.gecko.mma;
 
 import android.app.Activity;
 import android.app.Notification;
 import android.content.Context;
-import android.content.SharedPreferences;
 import android.os.Bundle;
 import android.support.annotation.DrawableRes;
 import android.support.annotation.NonNull;
 import android.support.v4.app.NotificationCompat;
 
 import com.leanplum.Leanplum;
 import com.leanplum.LeanplumActivityHelper;
 import com.leanplum.LeanplumPushNotificationCustomizer;
 import com.leanplum.LeanplumPushService;
+import com.leanplum.annotations.Parser;
+import com.leanplum.callbacks.VariablesChangedCallback;
 import com.leanplum.internal.Constants;
 import com.leanplum.internal.LeanplumInternal;
-import com.leanplum.internal.VarCache;
 
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.MmaConstants;
+import org.mozilla.gecko.firstrun.PanelConfig;
 
+import java.lang.ref.WeakReference;
 import java.util.Map;
-import java.util.UUID;
 
 
 public class MmaLeanplumImp implements MmaInterface {
 
 
     @Override
     public void init(final Activity activity, Map<String, ?> attributes) {
         if (activity == null) {
@@ -42,16 +43,18 @@ public class MmaLeanplumImp implements M
         // Need to call this in the eventuality that in this app session stop() has been called.
         // It will allow LeanPlum to communicate again with the servers.
         Leanplum.setIsTestModeEnabled(false);
 
         Leanplum.setApplicationContext(activity.getApplicationContext());
 
         LeanplumActivityHelper.enableLifecycleCallbacks(activity.getApplication());
 
+        Parser.parseVariables(LeanplumVariables.getInstance(activity.getApplicationContext()));
+
         if (AppConstants.MOZILLA_OFFICIAL) {
             Leanplum.setAppIdForProductionMode(MmaConstants.MOZ_LEANPLUM_SDK_CLIENTID, MmaConstants.MOZ_LEANPLUM_SDK_KEY);
         } else {
             Leanplum.setAppIdForDevelopmentMode(MmaConstants.MOZ_LEANPLUM_SDK_CLIENTID, MmaConstants.MOZ_LEANPLUM_SDK_KEY);
         }
 
         LeanplumPushService.setGcmSenderId(AppConstants.MOZ_ANDROID_GCM_SENDERIDS);
 
@@ -140,9 +143,50 @@ public class MmaLeanplumImp implements M
         return false;
     }
 
     @Override
     public void setDeviceId(@NonNull String deviceId) {
         Leanplum.setDeviceId(deviceId);
     }
 
+    @Override
+    public PanelConfig getPanelConfig(@NonNull Context context, PanelConfig.TYPE type, final boolean useLocalValues) {
+        if (useLocalValues) {
+            throw new UnsupportedOperationException("Cannot build remote panel config with local values");
+        }
+
+        switch (type) {
+            case WELCOME:
+                return new PanelConfig(type, useLocalValues, LeanplumVariables.welcomePanelTitle, LeanplumVariables.welcomePanelMessage,
+                        LeanplumVariables.welcomePanelSubtext, LeanplumVariables.getWelcomeImage());
+            case PRIVACY:
+                return new PanelConfig(type, useLocalValues, LeanplumVariables.privacyPanelTitle, LeanplumVariables.privacyPanelMessage,
+                        LeanplumVariables.privacyPanelSubtext, LeanplumVariables.getPrivacyImage());
+            case CUSTOMIZE:
+            case LAST_CUSTOMIZE:
+                return new PanelConfig(type, useLocalValues, LeanplumVariables.customizePanelTitle, LeanplumVariables.customizePanelMessage,
+                        LeanplumVariables.customizePanelSubtext, LeanplumVariables.getCustomizingImage());
+            case SYNC:
+                return new PanelConfig(type, useLocalValues, LeanplumVariables.syncPanelTitle, LeanplumVariables.syncPanelMessage,
+                        LeanplumVariables.syncPanelSubtext, LeanplumVariables.getSyncImage());
+            default:    // This will also be the case for "WELCOME"
+                return new PanelConfig(type, useLocalValues, LeanplumVariables.welcomePanelTitle, LeanplumVariables.welcomePanelMessage,
+                        LeanplumVariables.welcomePanelSubtext, LeanplumVariables.getWelcomeImage());
+        }
+    }
+
+    @Override
+    public void listenOnceForVariableChanges(@NonNull final MmaDelegate.MmaVariablesChangedListener listener) {
+        final WeakReference<MmaDelegate.MmaVariablesChangedListener> listenerRef = new WeakReference<>(listener);
+
+        Leanplum.addVariablesChangedHandler(new VariablesChangedCallback() {
+            @Override
+            public void variablesChanged() {
+                Leanplum.removeVariablesChangedHandler(this);
+                MmaDelegate.MmaVariablesChangedListener variablesChangesListener = listenerRef.get();
+                if (variablesChangesListener != null) {
+                    variablesChangesListener.onRemoteVariablesChanged();
+                }
+            }
+        });
+    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/mma/MmaStubImp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/mma/MmaStubImp.java
@@ -7,16 +7,18 @@
 package org.mozilla.gecko.mma;
 
 import android.app.Activity;
 import android.content.Context;
 import android.os.Bundle;
 import android.support.annotation.DrawableRes;
 import android.support.annotation.NonNull;
 
+import org.mozilla.gecko.firstrun.PanelConfig;
+
 import java.util.Map;
 
 
 public class MmaStubImp implements MmaInterface {
     @Override
     public void init(Activity activity, Map<String, ?> attributes) {
 
     }
@@ -51,9 +53,18 @@ public class MmaStubImp implements MmaIn
         return false;
     }
 
     @Override
     public void setDeviceId(@NonNull String deviceId) {
 
     }
 
+    @Override
+    public PanelConfig getPanelConfig(@NonNull Context context, PanelConfig.TYPE panelConfigType, boolean useLocalValues) {
+        return null;
+    }
+
+    @Override
+    public void listenOnceForVariableChanges(@NonNull MmaDelegate.MmaVariablesChangedListener listener) {
+        listener.onRemoteVariablesUnavailable();
+    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/switchboard/AsyncConfigLoader.java
+++ b/mobile/android/base/java/org/mozilla/gecko/switchboard/AsyncConfigLoader.java
@@ -28,27 +28,30 @@ import android.os.AsyncTask;
  *
  * @author Philipp Berner
  *
  */
 public class AsyncConfigLoader extends AsyncTask<Void, Void, Void> {
 
     private Context context;
     private String defaultServerUrl;
+    private SwitchBoard.ConfigStatusListener listener;
 
     /**
      * Sets the params for async loading either SwitchBoard.updateConfigServerUrl()
      * or SwitchBoard.loadConfig.
      * Loads config with a custom UUID
      * @param c Application context
      * @param defaultServerUrl Default URL endpoint for Switchboard config.
      */
-    public AsyncConfigLoader(Context c, String defaultServerUrl) {
+    public AsyncConfigLoader(Context c, String defaultServerUrl,
+                             SwitchBoard.ConfigStatusListener listener) {
         this.context = c;
         this.defaultServerUrl = defaultServerUrl;
+        this.listener = listener;
     }
 
     @Override
     protected Void doInBackground(Void... params) {
-        SwitchBoard.loadConfig(context, defaultServerUrl);
+        SwitchBoard.loadConfig(context, defaultServerUrl, listener);
         return null;
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/switchboard/SwitchBoard.java
+++ b/mobile/android/base/java/org/mozilla/gecko/switchboard/SwitchBoard.java
@@ -33,16 +33,17 @@ import org.json.JSONException;
 import org.json.JSONObject;
 import org.json.JSONArray;
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.search.SearchEngineManager;
 import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.util.IOUtils;
 import org.mozilla.gecko.util.ProxySelector;
+import org.mozilla.gecko.util.ThreadUtils;
 
 import android.content.Context;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.os.Build;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.text.TextUtils;
 import android.util.Log;
@@ -94,33 +95,52 @@ public class SwitchBoard {
 
     /**
      * Loads a new config for a user. This method does network I/O, so it
      * should not be called on the main thread.
      *
      * @param c ApplicationContext
      * @param serverUrl Server URL endpoint.
      */
-    public static void loadConfig(Context c, @NonNull String serverUrl) {
+    public static void loadConfig(Context c, @NonNull String serverUrl,
+                                  @NonNull final ConfigStatusListener listener) {
         final URL url;
         try {
             url = new URL(serverUrl);
         } catch (MalformedURLException e) {
+            ThreadUtils.postToUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    listener.onExperimentsConfigLoadFailed();
+                }
+            });
             Log.e(TAG, "Exception creating server URL", e);
             return;
         }
 
         final String result = readFromUrlGET(url);
         if (DEBUG) Log.d(TAG, "Result: " + result);
         if (result == null) {
+            ThreadUtils.postToUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    listener.onExperimentsConfigLoadFailed();
+                }
+            });
             return;
         }
 
         // Cache result locally in shared preferences.
         Preferences.setDynamicConfigJson(c, result);
+        ThreadUtils.postToUiThread(new Runnable() {
+            @Override
+            public void run() {
+                listener.onExperimentsConfigLoaded();
+            }
+        });
     }
 
     public static boolean isInBucket(Context c, int low, int high) {
         final int userBucket = getUserBucket(c);
         return (userBucket >= low) && (userBucket < high);
     }
 
     /**
@@ -448,9 +468,15 @@ public class SwitchBoard {
         final DeviceUuidFactory df = new DeviceUuidFactory(c);
         final String uuid = df.getDeviceUuid().toString();
 
         CRC32 crc = new CRC32();
         crc.update(uuid.getBytes());
         long checksum = crc.getValue();
         return (int)(checksum % 100L);
     }
+
+    public interface ConfigStatusListener {
+        void onExperimentsConfigLoaded();
+
+        void onExperimentsConfigLoadFailed();
+    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/widget/SplashScreen.java
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/SplashScreen.java
@@ -34,16 +34,20 @@ public class SplashScreen extends Relati
         if (hasReachedThreshold) {
             vanish();
         } else {
             // if the threshold not reached, mark
             shouldHideAsap = true;
         }
     }
 
+    public void show(final int duration) {
+        atLeast(duration);
+    }
+
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
         // Splash Screen will at least wait for this long before disappear.
         atLeast(MIN_DISPLAY_TIME);
     }
 
     // the minimum time the splash screen will stay on the screen
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/EnvironmentUtils.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/EnvironmentUtils.java
@@ -8,21 +8,21 @@ import android.content.Context;
 import android.content.SharedPreferences;
 import android.support.annotation.Nullable;
 
 /**
  * Provides information about the current environment.
  */
 public class EnvironmentUtils {
     /**
-     * Must be kept in-sync with {@link org.mozilla.gecko.GeckoApp.PREFS_IS_FIRST_RUN}.
+     * Must be kept in-sync with {@link org.mozilla.gecko.GeckoApp#PREFS_IS_FIRST_RUN}.
      */
     private static final String GECKO_PREFS_IS_FIRST_RUN = "telemetry-isFirstRun";
     /**
-     * Must be kept in-sync with {@link org.mozilla.gecko.BrowserApp.FIRSTRUN_UUID}.
+     * Must be kept in-sync with {@link org.mozilla.gecko.firstrun.OnboardingHelper#FIRSTRUN_UUID}.
      */
     private static final String GECKO_PREFS_FIRSTRUN_UUID = "firstrun_uuid";
 
     public static boolean isFirstRun(final Context context) {
         return getDefaultGeckoSharedPreferences(context).getBoolean(GECKO_PREFS_IS_FIRST_RUN, true);
     }
 
     @Nullable
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseRobocopTest.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseRobocopTest.java
@@ -12,25 +12,24 @@ import android.test.ActivityInstrumentat
 import android.text.TextUtils;
 import android.util.Log;
 
 import com.robotium.solo.Solo;
 
 import org.mozilla.gecko.Actions;
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.Assert;
-import org.mozilla.gecko.BrowserApp;
 import org.mozilla.gecko.Driver;
 import org.mozilla.gecko.FennecInstrumentationTestRunner;
 import org.mozilla.gecko.FennecMochitestAssert;
 import org.mozilla.gecko.FennecNativeActions;
 import org.mozilla.gecko.FennecNativeDriver;
 import org.mozilla.gecko.FennecTalosAssert;
-import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.firstrun.OnboardingHelper;
 import org.mozilla.gecko.updater.UpdateServiceHelper;
 
 import java.net.HttpURLConnection;
 import java.net.URL;
 import java.util.Map;
 
 @SuppressWarnings("unchecked")
 public abstract class BaseRobocopTest extends ActivityInstrumentationTestCase2<Activity> {
@@ -121,17 +120,17 @@ public abstract class BaseRobocopTest ex
         return BaseRobocopTest.createActivityIntent(mConfig);
     }
 
     // Static function to allow re-use.
     public static Intent createActivityIntent(Map<String, String> config) {
         final Intent intent = new Intent(Intent.ACTION_MAIN);
         intent.putExtra("args", "-no-remote -profile " + config.get("profile"));
         // Don't show the first run experience.
-        intent.putExtra(BrowserApp.EXTRA_SKIP_STARTPANE, true);
+        intent.putExtra(OnboardingHelper.EXTRA_SKIP_STARTPANE, true);
 
         final String envString = config.get("envvars");
         if (!TextUtils.isEmpty(envString)) {
             final String[] envStrings = envString.split(",");
 
             for (int iter = 0; iter < envStrings.length; iter++) {
                 intent.putExtra("env" + iter, envStrings[iter]);
             }