Bug 1386192 - Test Leanplum Custom Message for Onboarding; r?sdaswani draft
authorPetru Lingurar <petru.lingurar@softvision.ro>
Fri, 11 May 2018 17:00:34 +0300
changeset 794187 1f7ccddc8a2f9ef1e69c5df109933f53dd18bf37
parent 794135 21f09d7e7214eaebf1e0980494159bd846e1bdd9
push id109601
push userplingurar@mozilla.com
push dateFri, 11 May 2018 14:06:36 +0000
reviewerssdaswani
bugs1386192
milestone62.0a1
Bug 1386192 - Test Leanplum Custom Message for Onboarding; r?sdaswani 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. Switched the OnBoarding process to use the values from LeanPlumVariables. If LeanPlum is not available or no internet connection the "default" values are returned, otherwise, after all variables have been downloaded and (possibly) new values overwrote the old, the updated values are returned. Because connecting to LeanPlum and downloading the Variables might take a few seconds we use a delay of 2 seconds until starting to show the Onboarding screens using the default values to ensure the best experience. MozReview-Commit-ID: 4EyHB6BWl1c ***
mobile/android/base/java/org/mozilla/gecko/BrowserApp.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/PanelConfig.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
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -66,16 +66,19 @@ import android.view.ViewGroup;
 import android.view.ViewStub;
 import android.view.ViewTreeObserver;
 import android.view.Window;
 import android.view.animation.Interpolator;
 import android.widget.Button;
 import android.widget.ListView;
 import android.widget.ViewFlipper;
 
+import com.leanplum.Leanplum;
+import com.leanplum.callbacks.VariablesChangedCallback;
+
 import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.DynamicToolbar.VisibilityTransition;
 import org.mozilla.gecko.Tabs.TabEvents;
 import org.mozilla.gecko.activitystream.ActivityStream;
 import org.mozilla.gecko.activitystream.ActivityStreamTelemetry;
 import org.mozilla.gecko.adjust.AdjustBrowserAppDelegate;
 import org.mozilla.gecko.animation.PropertyAnimator;
 import org.mozilla.gecko.annotation.RobocopTarget;
@@ -158,16 +161,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;
@@ -182,16 +186,18 @@ 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.Timer;
+import java.util.TimerTask;
 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,
@@ -204,16 +210,17 @@ public class BrowserApp extends GeckoApp
                                    OnUrlOpenListener,
                                    OnUrlOpenInBackgroundListener,
                                    PropertyAnimator.PropertyAnimationListener,
                                    TabsPanel.TabsLayoutChangeListener,
                                    View.OnKeyListener {
     private static final String LOGTAG = "GeckoBrowserApp";
 
     private static final int TABS_ANIMATION_DURATION = 450;
+    private static final int DELAY_SHOW_DEFAULT_ONBOARDING = 2 * 1000;
 
     // Intent String extras used to specify custom Switchboard configurations.
     private static final String INTENT_KEY_SWITCHBOARD_SERVER = "switchboard-server";
 
     // TODO: Replace with kinto endpoint.
     private static final String SWITCHBOARD_SERVER = "https://firefox.settings.services.mozilla.com/v1/buckets/fennec/collections/experiments/records";
 
     private static final String STATE_ABOUT_HOME_TOP_PADDING = "abouthome_top_padding";
@@ -1083,31 +1090,47 @@ public class BrowserApp extends GeckoApp
                                 .apply();
             }
         } finally {
             StrictMode.setThreadPolicy(savedPolicy);
         }
     }
 
     /**
-     * Code to actually show the first run pager, separated
-     * for distribution purposes.
+     * Code to actually show the first run pager, separated for distribution purposes.<br>
+     * If Mma is available will first check to make sure we have the most recent values from the server.
      */
     @UiThread
     private void checkFirstrunInternal() {
-        showFirstrunPager();
-
-        if (HardwareUtils.isTablet()) {
-            mTabStrip.setOnTabChangedListener(new TabStripInterface.OnTabAddedOrRemovedListener() {
+        if (SwitchBoard.isInExperiment(this, Experiments.LEANPLUM) && NetworkUtils.isConnected(this)) {
+            final Timer timer = new Timer();
+            final TimerTask task = new TimerTask() {
                 @Override
-                public void onTabChanged() {
-                    hideFirstrunPager(TelemetryContract.Method.BUTTON);
-                    mTabStrip.setOnTabChangedListener(null);
+                public void run() {
+                    showFirstrunPager();
+                }
+            };
+            final long showOnboardingScreensTaskStartTime = System.currentTimeMillis();
+            // If getting new values from LeanPlum servers takes too much time
+            // we will start showing the onboarding screens with the default values.
+            timer.schedule(task, DELAY_SHOW_DEFAULT_ONBOARDING);
+
+            Leanplum.addVariablesChangedAndNoDownloadsPendingHandler(new VariablesChangedCallback() {
+                @Override
+                public void variablesChanged() {
+                    final boolean isDelayToStartShowingOnBoardingReached =
+                            showOnboardingScreensTaskStartTime + DELAY_SHOW_DEFAULT_ONBOARDING > 0;
+                    if (!isDelayToStartShowingOnBoardingReached) {
+                        timer.cancel();
+                        showFirstrunPager();
+                    }
                 }
             });
+        } else {
+            showFirstrunPager();
         }
     }
 
     /**
      * 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
@@ -3025,16 +3048,26 @@ public class BrowserApp extends GeckoApp
                         !Tabs.hasHomepage(BrowserApp.this)) {
                         enterEditingMode();
                     }
                 }
             });
         }
 
         mHomeScreenContainer.setVisibility(View.VISIBLE);
+
+        if (HardwareUtils.isTablet()) {
+            mTabStrip.setOnTabChangedListener(new TabStripInterface.OnTabAddedOrRemovedListener() {
+                @Override
+                public void onTabChanged() {
+                    hideFirstrunPager(TelemetryContract.Method.BUTTON);
+                    mTabStrip.setOnTabChangedListener(null);
+                }
+            });
+        }
     }
 
     private void showHomePager(String panelId, Bundle panelRestoreData) {
         showHomePagerWithAnimator(panelId, panelRestoreData, null);
     }
 
     private void showHomePagerWithAnimator(String panelId, Bundle panelRestoreData, PropertyAnimator animator) {
         if (isHomePagerVisible()) {
--- a/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPager.java
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPager.java
@@ -61,19 +61,19 @@ public class FirstrunPager extends RtlVi
         }
 
         super.addView(child, index, params);
     }
 
     public void load(Context appContext, FragmentManager fm, final FirstrunAnimationContainer.OnFinishListener onFinishListener) {
         final List<FirstrunPagerConfig.FirstrunPanelConfig> panels;
 
-        if (Restrictions.isRestrictedProfile(context)) {
-            panels = FirstrunPagerConfig.getRestricted();
-        } else if (FirefoxAccounts.firefoxAccountsExist(context)) {
+        if (Restrictions.isRestrictedProfile(appContext)) {
+            panels = FirstrunPagerConfig.getRestricted(appContext);
+        } else if (FirefoxAccounts.firefoxAccountsExist(appContext)) {
             panels = FirstrunPagerConfig.forFxAUser(appContext);
         } else {
             panels = FirstrunPagerConfig.getDefault(appContext);
         }
 
         setAdapter(new ViewPagerAdapter(fm, panels));
         this.pagerNavigation = new FirstrunPanel.PagerNavigation() {
             @Override
@@ -139,17 +139,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 +167,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,92 @@
 /* -*- 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 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));
+        panels.add(FirstrunPanelConfig.getConfiguredPanel(context, PanelConfig.TYPE.PRIVACY));
+        panels.add(FirstrunPanelConfig.getConfiguredPanel(context, PanelConfig.TYPE.CUSTOMIZE));
+        panels.add(FirstrunPanelConfig.getConfiguredPanel(context, PanelConfig.TYPE.SYNC));
 
         return panels;
     }
 
-    public static List<FirstrunPanelConfig> forFxAUser(Context context) {
+    static List<FirstrunPanelConfig> forFxAUser(Context context) {
         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));
+        panels.add(FirstrunPanelConfig.getConfiguredPanel(context, PanelConfig.TYPE.PRIVACY));
+        panels.add(FirstrunPanelConfig.getConfiguredPanel(context, PanelConfig.TYPE.LAST_CUSTOMIZE));
 
         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 PanelConfig config = MmaDelegate.getPanelConfig(context, wantedPanelConfig);
+            return new FirstrunPanelConfig(config.getClassName(), config.getTitle(), config.getImage(),
+                    config.getMessage(), config.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,23 +18,24 @@ 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");
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/PanelConfig.java
@@ -0,0 +1,56 @@
+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 TYPE type;
+    private String title;
+    private String message;
+    private String text;
+    private Bitmap image;
+
+    public PanelConfig(TYPE type, String title, String message, String text, Bitmap image) {
+        this.type = type;
+        this.title = title;
+        this.message = message;
+        this.text = text;
+        this.image = image;
+    }
+
+    public String getClassName() {
+        switch (type) {
+            default:    // Unreachable. Just to make Lint happy.
+            case WELCOME:
+            case PRIVACY:
+            case CUSTOMIZE:
+                return FirstrunPanel.class.getName();
+            case LAST_CUSTOMIZE:
+                return LastPanel.class.getName();
+            case SYNC:
+                return SyncPanel.class.getName();
+        }
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public String getText() {
+        return text;
+    }
+
+    public Bitmap getImage() {
+        return image;
+    }
+}
--- 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,144 @@
+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.NonNull;
+
+import com.leanplum.Var;
+import com.leanplum.annotations.Variable;
+
+import org.mozilla.gecko.R;
+
+import java.io.IOException;
+import java.io.InputStream;
+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 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;
+    @Variable(group = FIRSTRUN_WELCOME_PANEL_GROUP_NAME)
+    private static Var<String> welcomeDrawable = Var.defineResource("welcomeDrawable", R.drawable.firstrun_welcome);
+
+    @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;
+    @Variable(group = FIRSTRUN_PRIVACY_PANEL_GROUP_NAME)
+    private static Var<String> privacyDrawable = Var.defineResource("privacyDrawable", R.drawable.firstrun_private);
+
+    @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;
+    @Variable(group = FIRSTRUN_CUSTOMIZE_PANEL_GROUP_NAME)
+    private static Var<String> customizingDrawable = Var.defineResource("customizingDrawable", R.drawable.firstrun_data);
+
+    @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;
+    @Variable(group = FIRSTRUN_SYNC_PANEL_GROUP_NAME)
+    private static Var<String> syncDrawable = Var.defineResource("syncDrawable", R.drawable.firstrun_sync);
+
+
+    /**
+     * Allows initializing all variables with Resource values.<br>
+     * Use {@link #getInstance(Context)} to get a reference to the Singleton.
+     * @param context
+     */
+    public static void init(Context context) {
+        // Simple Singleton as we don't expect concurrency problems.
+        if (INSTANCE == null) {
+            INSTANCE = new LeanplumVariables(context);
+        }
+    }
+
+    /**
+     * 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 context) {
+        init(context);
+        return INSTANCE;
+    }
+
+
+    /**
+     * Allows setting default values for instance variables.
+     * @param context used to access application resources
+     */
+    private LeanplumVariables(@NonNull Context context) {
+        final Resources resources = context.getResources();
+        welcomePanelTitle = resources.getString(R.string.firstrun_panel_title_welcome);
+        welcomePanelMessage = resources.getString(R.string.firstrun_urlbar_message);
+        welcomePanelSubtext = resources.getString(R.string.firstrun_urlbar_subtext);
+
+        privacyPanelTitle = resources.getString(R.string.firstrun_panel_title_privacy);
+        privacyPanelMessage = resources.getString(R.string.firstrun_privacy_message);
+        privacyPanelSubtext = resources.getString(R.string.firstrun_privacy_subtext);
+
+        customizePanelTitle = resources.getString(R.string.firstrun_panel_title_customize);
+        customizePanelMessage = resources.getString(R.string.firstrun_customize_message);
+        customizePanelSubtext = resources.getString(R.string.firstrun_customize_subtext);
+
+        syncPanelTitle = resources.getString(R.string.firstrun_sync_title);
+        syncPanelMessage = resources.getString(R.string.firstrun_sync_message);
+        syncPanelSubtext = resources.getString(R.string.firstrun_sync_subtext);
+    }
+
+
+    public static Bitmap getWelcomeImage() {
+        return getBitmapFromMmaVar(welcomeDrawable);
+    }
+
+    public static Bitmap getPrivacyImage() {
+        return getBitmapFromMmaVar(privacyDrawable);
+    }
+
+    public static Bitmap getCustomizingImage() {
+        return getBitmapFromMmaVar(customizingDrawable);
+    }
+
+    public static Bitmap getSyncImage() {
+        return getBitmapFromMmaVar(syncDrawable);
+    }
+
+    private static Bitmap getBitmapFromMmaVar(@NonNull Var mmaVar) {
+        InputStream varStream = null;
+        Bitmap bitmap;
+        try {
+            varStream = mmaVar.stream();
+            bitmap = BitmapFactory.decodeStream(varStream);
+        } finally {
+            try {
+                if (varStream != null) {
+                    varStream.close();
+                }
+            } catch (IOException e) {
+                // no-op
+            }
+        }
+
+        return bitmap;
+    }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/mma/MmaDelegate.java
+++ b/mobile/android/base/java/org/mozilla/gecko/mma/MmaDelegate.java
@@ -19,16 +19,17 @@ 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 java.util.HashMap;
 import java.util.Map;
 import java.util.UUID;
@@ -195,16 +196,20 @@ public class MmaDelegate {
         if (isMmaEnabled(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) {
+        return mmaHelper.getPanelConfig(context, panelConfigType);
+    }
+
     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) {
--- 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,11 @@ 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);
 }
--- a/mobile/android/base/java/org/mozilla/gecko/mma/MmaLeanplumImp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/mma/MmaLeanplumImp.java
@@ -4,47 +4,49 @@
  * 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.internal.Constants;
 
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.MmaConstants;
+import org.mozilla.gecko.firstrun.PanelConfig;
 
 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) {
             return;
         }
         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);
 
@@ -114,9 +116,31 @@ 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) {
+        // Ensure we have the resources ready
+        LeanplumVariables.init(context);
+
+        switch (type) {
+            default:    // Unreachable. Just to make Lint happy.
+            case WELCOME:
+                return new PanelConfig(type, LeanplumVariables.welcomePanelTitle, LeanplumVariables.welcomePanelMessage,
+                        LeanplumVariables.welcomePanelSubtext, LeanplumVariables.getWelcomeImage());
+            case PRIVACY:
+                return new PanelConfig(type, LeanplumVariables.privacyPanelTitle, LeanplumVariables.privacyPanelMessage,
+                        LeanplumVariables.privacyPanelSubtext, LeanplumVariables.getPrivacyImage());
+            case CUSTOMIZE:
+            case LAST_CUSTOMIZE:
+                return new PanelConfig(type, LeanplumVariables.customizePanelTitle, LeanplumVariables.customizePanelMessage,
+                        LeanplumVariables.customizePanelSubtext, LeanplumVariables.getCustomizingImage());
+            case SYNC:
+                return new PanelConfig(type, LeanplumVariables.syncPanelTitle, LeanplumVariables.syncPanelMessage,
+                        LeanplumVariables.syncPanelSubtext, LeanplumVariables.getSyncImage());
+        }
+    }
 }