Bug 1232706 - Promote "Add to home screen" for frequently visited websites. r?margaret draft
authorSebastian Kaspari <s.kaspari@gmail.com>
Wed, 06 Apr 2016 09:41:34 +0200
changeset 348541 1e7f6d1aa4c8e6de1880c45f1d5af2b36b84d1c1
parent 348123 64693d39a134df2274838df42029c58e9451ad94
child 517865 f1c117e581b610dbe2d816bcd62eb51b9f186778
push id14842
push users.kaspari@gmail.com
push dateThu, 07 Apr 2016 17:59:35 +0000
reviewersmargaret
bugs1232706
milestone48.0a1
Bug 1232706 - Promote "Add to home screen" for frequently visited websites. r?margaret MozReview-Commit-ID: 2OW7GKxuQmr
mobile/android/base/AndroidManifest.xml.in
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/GeckoAppShell.java
mobile/android/base/java/org/mozilla/gecko/db/BrowserContract.java
mobile/android/base/java/org/mozilla/gecko/db/BrowserDB.java
mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java
mobile/android/base/java/org/mozilla/gecko/db/LocalUrlAnnotations.java
mobile/android/base/java/org/mozilla/gecko/db/StubBrowserDB.java
mobile/android/base/java/org/mozilla/gecko/db/UrlAnnotations.java
mobile/android/base/java/org/mozilla/gecko/favicons/Favicons.java
mobile/android/base/java/org/mozilla/gecko/promotion/AddToHomeScreenPromotion.java
mobile/android/base/java/org/mozilla/gecko/promotion/HomeScreenPrompt.java
mobile/android/base/java/org/mozilla/gecko/util/Experiments.java
mobile/android/base/locales/en-US/android_strings.dtd
mobile/android/base/moz.build
mobile/android/base/strings.xml.in
mobile/android/services/src/main/res/layout/homescreen_prompt.xml
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -154,16 +154,20 @@
         </activity-alias>
 
         <service android:name="org.mozilla.gecko.GeckoService" />
 
         <activity android:name="org.mozilla.gecko.trackingprotection.TrackingProtectionPrompt"
                   android:launchMode="singleTop"
                   android:theme="@style/OverlayActivity" />
 
+        <activity android:name="org.mozilla.gecko.promotion.HomeScreenPrompt"
+                  android:launchMode="singleTop"
+                  android:theme="@style/OverlayActivity" />
+
         <!-- The main reason for the Tab Queue build flag is to not mess with the VIEW intent filter
              before the rest of the plumbing is in place -->
 
         <service android:name="org.mozilla.gecko.tabqueue.TabQueueService" />
 
         <activity android:name="org.mozilla.gecko.tabqueue.TabQueuePrompt"
                   android:launchMode="singleTop"
                   android:theme="@style/OverlayActivity" />
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -50,16 +50,17 @@ import org.mozilla.gecko.javaaddons.Java
 import org.mozilla.gecko.menu.GeckoMenu;
 import org.mozilla.gecko.menu.GeckoMenuItem;
 import org.mozilla.gecko.mozglue.ContextUtils;
 import org.mozilla.gecko.mozglue.ContextUtils.SafeIntent;
 import org.mozilla.gecko.overlays.ui.ShareDialog;
 import org.mozilla.gecko.permissions.Permissions;
 import org.mozilla.gecko.preferences.ClearOnShutdownPref;
 import org.mozilla.gecko.preferences.GeckoPreferences;
+import org.mozilla.gecko.promotion.AddToHomeScreenPromotion;
 import org.mozilla.gecko.prompts.Prompt;
 import org.mozilla.gecko.prompts.PromptListItem;
 import org.mozilla.gecko.reader.ReaderModeUtils;
 import org.mozilla.gecko.reader.ReadingListHelper;
 import org.mozilla.gecko.restrictions.Restrictable;
 import org.mozilla.gecko.restrictions.RestrictedProfileConfiguration;
 import org.mozilla.gecko.restrictions.Restrictions;
 import org.mozilla.gecko.search.SearchEngineManager;
@@ -225,16 +226,17 @@ public class BrowserApp extends GeckoApp
     private ToolbarProgressView mProgressView;
     private FirstrunAnimationContainer mFirstrunAnimationContainer;
     private HomePager mHomePager;
     private TabsPanel mTabsPanel;
     private ViewGroup mHomePagerContainer;
     private ActionModeCompat mActionMode;
     private TabHistoryController tabHistoryController;
     private ZoomedView mZoomedView;
+    private AddToHomeScreenPromotion mAddToHomeScreenPromotion;
 
     private static final int GECKO_TOOLS_MENU = -1;
     private static final int ADDON_MENU_OFFSET = 1000;
     public static final String TAB_HISTORY_FRAGMENT_TAG = "tabHistoryFragment";
 
     private static class MenuItemInfo {
         public int id;
         public String label;
@@ -812,16 +814,18 @@ public class BrowserApp extends GeckoApp
                        .andFallback(new Runnable() {
                            @Override
                            public void run() {
                                showUpdaterPermissionSnackbar();
                            }
                        })
                       .run();
         }
+
+        mAddToHomeScreenPromotion = new AddToHomeScreenPromotion(this);
     }
 
     /**
      * Initializes the default Switchboard URLs the first time.
      * @param intent
      */
     private void initSwitchboard(Intent intent) {
         if (Experiments.isDisabled(new SafeIntent(intent)) || !AppConstants.MOZ_SWITCHBOARD) {
@@ -1049,30 +1053,34 @@ public class BrowserApp extends GeckoApp
         }
 
         EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener) this,
             "Prompt:ShowTop");
 
         processTabQueue();
 
         mScreenshotObserver.start();
+
+        mAddToHomeScreenPromotion.resume();
     }
 
     @Override
     public void onPause() {
         super.onPause();
 
         // Needed for Adjust to get accurate session measurements
         AdjustConstants.getAdjustHelper().onPause();
 
         // Register for Prompt:ShowTop so we can foreground this activity even if it's hidden.
         EventDispatcher.getInstance().registerGeckoThreadListener((GeckoEventListener) this,
             "Prompt:ShowTop");
 
         mScreenshotObserver.stop();
+
+        mAddToHomeScreenPromotion.pause();
     }
 
     @Override
     public void onStart() {
         super.onStart();
 
         // Queue this work so that the first launch of the activity doesn't
         // trigger profile init too early.
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -4,16 +4,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
 import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.URLMetadataTable;
+import org.mozilla.gecko.db.UrlAnnotations;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.gfx.FullScreenState;
 import org.mozilla.gecko.gfx.Layer;
 import org.mozilla.gecko.gfx.LayerView;
 import org.mozilla.gecko.gfx.PluginLayer;
 import org.mozilla.gecko.health.HealthRecorder;
@@ -1802,48 +1803,23 @@ public abstract class GeckoApp
 
     @Override
     public String getDefaultUAString() {
         return HardwareUtils.isTablet() ? AppConstants.USER_AGENT_FENNEC_TABLET :
                                           AppConstants.USER_AGENT_FENNEC_MOBILE;
     }
 
     @Override
-    public void createShortcut(final String title, final String URI) {
-        ThreadUtils.assertOnBackgroundThread();
-        final BrowserDB db = GeckoProfile.get(getApplicationContext()).getDB();
-
-        final ContentResolver cr = getContext().getContentResolver();
-        final Map<String, Map<String, Object>> metadata = db.getURLMetadata().getForURLs(cr,
-                Collections.singletonList(URI),
-                Collections.singletonList(URLMetadataTable.TOUCH_ICON_COLUMN)
-        );
-
-        final Map<String, Object> row = metadata.get(URI);
-
-        String touchIconURL = null;
-
-        if (row != null) {
-            touchIconURL = (String) row.get(URLMetadataTable.TOUCH_ICON_COLUMN);
-        }
-
-        OnFaviconLoadedListener listener = new OnFaviconLoadedListener() {
+    public void createShortcut(final String title, final String url) {
+        Favicons.getPreferredIconForHomeScreenShortcut(this, url, new OnFaviconLoadedListener() {
             @Override
             public void onFaviconLoaded(String url, String faviconURL, Bitmap favicon) {
                 doCreateShortcut(title, url, favicon);
             }
-        };
-
-        // Retrieve the icon while bypassing the cache. Homescreen icon creation is a one-off event, hence it isn't
-        // useful to cache these icons. (Android takes care of storing homescreen icons after a shortcut
-        // has been created.)
-        // The cache is also (currently) limited to 32dp, hence we explicitly need to avoid accessing those icons.
-        // If touchIconURL is null, then Favicons falls back to finding the best possible favicon for
-        // the site URI, hence we can use this call even when there is no touchIcon defined.
-        Favicons.getPreferredSizeFaviconForPage(getApplicationContext(), URI, touchIconURL, listener);
+        });
     }
 
     private void doCreateShortcut(final String aTitle, final String aURI, final Bitmap aIcon) {
         // The intent to be launched by the shortcut.
         Intent shortcutIntent = new Intent();
         shortcutIntent.setAction(GeckoApp.ACTION_HOMESCREEN_SHORTCUT);
         shortcutIntent.setData(Uri.parse(aURI));
         shortcutIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME,
@@ -1859,16 +1835,19 @@ public abstract class GeckoApp
             intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, aURI);
         }
 
         // Do not allow duplicate items.
         intent.putExtra("duplicate", false);
 
         intent.setAction("com.android.launcher.action.INSTALL_SHORTCUT");
         getApplicationContext().sendBroadcast(intent);
+
+        final UrlAnnotations urlAnnotations = GeckoProfile.get(getApplicationContext()).getDB().getUrlAnnotations();
+        urlAnnotations.insertHomeScreenShortcut(getContentResolver(), aURI, true);
     }
 
     private void processAlertCallback(SafeIntent intent) {
         String alertName = "";
         String alertCookie = "";
         Uri data = intent.getData();
         if (data != null) {
             alertName = data.getQueryParameter("name");
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoAppShell.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoAppShell.java
@@ -757,16 +757,18 @@ public class GeckoAppShell
     // This is the entry point from nsIShellService.
     @WrapForJNI
     public static void createShortcut(final String aTitle, final String aURI) {
         final GeckoInterface geckoInterface = getGeckoInterface();
         if (geckoInterface == null) {
             return;
         }
         geckoInterface.createShortcut(aTitle, aURI);
+
+
     }
 
     @JNITarget
     static public int getPreferredIconSize() {
         if (Versions.feature11Plus) {
             ActivityManager am = (ActivityManager)
                     getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
             return am.getLauncherLargeIconSize();
@@ -1934,16 +1936,18 @@ public class GeckoAppShell
         if (geckoInterface == null) {
             return;
         }
         geckoInterface.checkUriVisited(uri);
     }
 
     @WrapForJNI(stubName = "MarkURIVisited")
     static void markUriVisited(final String uri) {
+        Log.w("SKDBG", "***** markUrlVisited()");
+
         final GeckoInterface geckoInterface = getGeckoInterface();
         if (geckoInterface == null) {
             return;
         }
         geckoInterface.markUriVisited(uri);
     }
 
     @WrapForJNI(stubName = "SetURITitle")
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserContract.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserContract.java
@@ -528,17 +528,26 @@ public class BrowserContract {
             FEED("feed"),
 
             /**
              * This key maps URLs of feeds to an object describing the feed.
              *
              * Key:   feed_subscription
              * Value: JSON object describing feed
              */
-            FEED_SUBSCRIPTION("feed_subscription");
+            FEED_SUBSCRIPTION("feed_subscription"),
+
+            /**
+             * Indicator that we user interacted with the URL in regards to home screen shortcuts.
+             *
+             * Key:   home_screen_shortcut
+             * Value: True: User created an home screen shortcut for this URL
+             *        False: User declined to create a shortcut for this URL
+             */
+            HOME_SCREEN_SHORTCUT("home_screen_shortcut");
 
             private final String dbValue;
 
             Key(final String dbValue) { this.dbValue = dbValue; }
             public String getDbValue() { return dbValue; }
         }
 
         public enum SyncStatus {
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserDB.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserDB.java
@@ -89,16 +89,18 @@ public interface BrowserDB {
      */
     public abstract Cursor getAllVisitedHistory(ContentResolver cr);
 
     /**
      * Can return <code>null</code>.
      */
     public abstract Cursor getRecentHistory(ContentResolver cr, int limit);
 
+    public abstract Cursor getHistoryForURL(ContentResolver cr, String uri);
+
     public abstract Cursor getRecentHistoryBetweenTime(ContentResolver cr, int historyLimit, long start, long end);
 
     public abstract long getPrePathLastVisitedTimeMilliseconds(ContentResolver cr, String prePath);
 
     public abstract void expireHistory(ContentResolver cr, ExpirePriority priority);
 
     public abstract void removeHistoryEntry(ContentResolver cr, String url);
 
--- a/mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java
@@ -533,17 +533,17 @@ public class LocalBrowserDB implements B
                                        .appendQueryParameter(BrowserContract.PARAM_LIMIT,
                                                              String.valueOf(limit))
                                        .build();
     }
 
     private Uri combinedUriWithLimit(int limit) {
         return mCombinedUriWithProfile.buildUpon()
                                       .appendQueryParameter(BrowserContract.PARAM_LIMIT,
-                                                            String.valueOf(limit))
+                                              String.valueOf(limit))
                                       .build();
     }
 
     private static Uri withDeleted(final Uri uri) {
         return uri.buildUpon()
                   .appendQueryParameter(BrowserContract.PARAM_SHOW_DELETED, "1")
                   .build();
     }
@@ -636,25 +636,25 @@ public class LocalBrowserDB implements B
             selection = Combined.URL + " NOT IN (SELECT " +
                                                  Bookmarks.URL + " FROM bookmarks WHERE " +
                                                  DBUtils.qualifyColumn("bookmarks", Bookmarks.PARENT) + " = ? AND " +
                                                  DBUtils.qualifyColumn("bookmarks", Bookmarks.IS_DELETED) + " == 0)";
             selectionArgs = new String[] { String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID) };
         }
 
         return filterAllSites(cr,
-                              new String[] { Combined._ID,
-                                             Combined.URL,
-                                             Combined.TITLE,
-                                             Combined.BOOKMARK_ID,
-                                             Combined.HISTORY_ID },
-                              constraint,
-                              limit,
-                              null,
-                              selection, selectionArgs);
+                new String[]{Combined._ID,
+                        Combined.URL,
+                        Combined.TITLE,
+                        Combined.BOOKMARK_ID,
+                        Combined.HISTORY_ID},
+                constraint,
+                limit,
+                null,
+                selection, selectionArgs);
     }
 
     @Override
     public void updateVisitedHistory(ContentResolver cr, String uri) {
         ContentValues values = new ContentValues();
 
         values.put(History.URL, uri);
         values.put(History.DATE_LAST_VISITED, System.currentTimeMillis());
@@ -687,26 +687,26 @@ public class LocalBrowserDB implements B
                         History.VISITS + " > 0",
                         null,
                         null);
     }
 
     @Override
     public Cursor getRecentHistory(ContentResolver cr, int limit) {
         return cr.query(combinedUriWithLimit(limit),
-                        new String[] { Combined._ID,
-                                       Combined.BOOKMARK_ID,
-                                       Combined.HISTORY_ID,
-                                       Combined.URL,
-                                       Combined.TITLE,
-                                       Combined.DATE_LAST_VISITED,
-                                       Combined.VISITS },
-                        History.DATE_LAST_VISITED + " > 0",
-                        null,
-                        History.DATE_LAST_VISITED + " DESC");
+                new String[]{Combined._ID,
+                        Combined.BOOKMARK_ID,
+                        Combined.HISTORY_ID,
+                        Combined.URL,
+                        Combined.TITLE,
+                        Combined.DATE_LAST_VISITED,
+                        Combined.VISITS},
+                History.DATE_LAST_VISITED + " > 0",
+                null,
+                History.DATE_LAST_VISITED + " DESC");
     }
 
     @Override
     public Cursor getRecentHistoryBetweenTime(ContentResolver cr, int limit, long start, long end) {
         return cr.query(combinedUriWithLimit(limit),
                 new String[] { Combined._ID,
                         Combined.BOOKMARK_ID,
                         Combined.HISTORY_ID,
@@ -714,16 +714,28 @@ public class LocalBrowserDB implements B
                         Combined.TITLE,
                         Combined.DATE_LAST_VISITED,
                         Combined.VISITS },
                 History.DATE_LAST_VISITED + " >= " + start + " AND " + History.DATE_LAST_VISITED + " < " + end,
                 null,
                 History.DATE_LAST_VISITED + " DESC");
     }
 
+    public Cursor getHistoryForURL(ContentResolver cr, String uri) {
+        return cr.query(mHistoryUriWithProfile,
+                new String[] {
+                    History.VISITS,
+                    History.DATE_LAST_VISITED
+                },
+                History.URL + "= ?",
+                new String[] { uri },
+                History.DATE_LAST_VISITED + " DESC"
+        );
+    }
+
     @Override
     public long getPrePathLastVisitedTimeMilliseconds(ContentResolver cr, String prePath) {
         if (prePath == null) {
             return 0;
         }
         // If we don't end with a trailing slash, then both https://foo.com and https://foo.company.biz will match.
         if (!prePath.endsWith("/")) {
             prePath = prePath + "/";
--- a/mobile/android/base/java/org/mozilla/gecko/db/LocalUrlAnnotations.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/LocalUrlAnnotations.java
@@ -41,16 +41,32 @@ public class LocalUrlAnnotations impleme
      * Insert mapping from website URL to URL of the feed.
      */
     @Override
     public void insertFeedUrl(ContentResolver cr, String originUrl, String feedUrl) {
         insertAnnotation(cr, originUrl, Key.FEED, feedUrl);
     }
 
     /**
+     * Did we ever create a home screen short cut for this URL? If this method returns true this
+     * doesn't mean that the shortcut still exists - only that we created one in the past.
+     */
+    @Override
+    public boolean hasAcceptedOrDeclinedHomeScreenShortcut(ContentResolver cr, String url) {
+        return hasResultsForSelection(cr,
+                BrowserContract.UrlAnnotations.URL + " = ?",
+                new String[]{url});
+    }
+
+    @Override
+    public void insertHomeScreenShortcut(ContentResolver cr, String url, boolean hasCreatedShortCut) {
+        insertAnnotation(cr, url, Key.HOME_SCREEN_SHORTCUT, String.valueOf(hasCreatedShortCut));
+    }
+
+    /**
      * Returns true if there's a mapping from the given website URL to a feed URL. False otherwise.
      */
     @Override
     public boolean hasFeedUrlForWebsite(ContentResolver cr, String websiteUrl) {
         return hasResultsForSelection(cr,
                 BrowserContract.UrlAnnotations.URL + " = ? AND " + BrowserContract.UrlAnnotations.KEY + " = ?",
                 new String[]{websiteUrl, Key.FEED.getDbValue()});
     }
--- a/mobile/android/base/java/org/mozilla/gecko/db/StubBrowserDB.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/StubBrowserDB.java
@@ -185,16 +185,22 @@ class StubUrlAnnotations implements UrlA
     @Override
     public void insertFeedSubscription(ContentResolver cr, FeedSubscription subscription) {}
 
     @Override
     public boolean hasFeedUrlForWebsite(ContentResolver cr, String websiteUrl) { return false; }
 
     @Override
     public void insertFeedUrl(ContentResolver cr, String originUrl, String feedUrl) {}
+
+    @Override
+    public boolean hasAcceptedOrDeclinedHomeScreenShortcut(ContentResolver cr, String url) { return false; }
+
+    @Override
+    public void insertHomeScreenShortcut(ContentResolver cr, String url, boolean hasCreatedShortCut) {}
 }
 
 /*
  * This base implementation just stubs all methods. For the
  * real implementations, see LocalBrowserDB.java.
  */
 public class StubBrowserDB implements BrowserDB {
     private final StubSearches searches = new StubSearches();
@@ -265,16 +271,21 @@ public class StubBrowserDB implements Br
         return null;
     }
 
     public Cursor getRecentHistory(ContentResolver cr, int limit) {
         return null;
     }
 
     @Override
+    public Cursor getHistoryForURL(ContentResolver cr, String uri) {
+        return null;
+    }
+
+    @Override
     public Cursor getRecentHistoryBetweenTime(ContentResolver cr, int limit, long time, long end) {
         return null;
     }
 
     @Override
     public long getPrePathLastVisitedTimeMilliseconds(ContentResolver cr, String prePath) { return 0; }
 
     public void expireHistory(ContentResolver cr, BrowserContract.ExpirePriority priority) {
--- a/mobile/android/base/java/org/mozilla/gecko/db/UrlAnnotations.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/UrlAnnotations.java
@@ -20,9 +20,12 @@ public interface UrlAnnotations {
     void deleteFeedUrl(ContentResolver cr, String websiteUrl);
     boolean hasWebsiteForFeedUrl(ContentResolver cr, String feedUrl);
     void deleteFeedSubscription(ContentResolver cr, FeedSubscription subscription);
     void updateFeedSubscription(ContentResolver cr, FeedSubscription subscription);
     boolean hasFeedSubscription(ContentResolver cr, String feedUrl);
     void insertFeedSubscription(ContentResolver cr, FeedSubscription subscription);
     boolean hasFeedUrlForWebsite(ContentResolver cr, String websiteUrl);
     void insertFeedUrl(ContentResolver cr, String originUrl, String feedUrl);
+
+    boolean hasAcceptedOrDeclinedHomeScreenShortcut(ContentResolver cr, String url);
+    void insertHomeScreenShortcut(ContentResolver cr, String url, boolean hasCreatedShortCut);
 }
--- a/mobile/android/base/java/org/mozilla/gecko/favicons/Favicons.java
+++ b/mobile/android/base/java/org/mozilla/gecko/favicons/Favicons.java
@@ -3,20 +3,22 @@
  * 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.favicons;
 
 import android.graphics.drawable.Drawable;
 import org.mozilla.gecko.AboutPages;
 import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.db.URLMetadataTable;
 import org.mozilla.gecko.favicons.cache.FaviconCache;
 import org.mozilla.gecko.util.GeckoJarReader;
 import org.mozilla.gecko.util.NonEvictingLruCache;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.res.Resources;
@@ -28,16 +30,17 @@ import android.text.TextUtils;
 import android.util.Log;
 import android.util.SparseArray;
 
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 public class Favicons {
     private static final String LOGTAG = "GeckoFavicons";
 
     // A magic URL representing the app's own favicon, used for about: pages.
@@ -591,9 +594,37 @@ public class Favicons {
      *
      * @param url page URL to get a large favicon image for.
      * @param onFaviconLoadedListener listener to call back with the result.
      */
     public static void getPreferredSizeFaviconForPage(Context context, String url, String iconURL, OnFaviconLoadedListener onFaviconLoadedListener) {
         int preferredSize = GeckoAppShell.getPreferredIconSize();
         loadUncachedFavicon(context, url, iconURL, LoadFaviconTask.FLAG_BYPASS_CACHE_WHEN_DOWNLOADING_ICONS, preferredSize, onFaviconLoadedListener);
     }
+
+    public static void getPreferredIconForHomeScreenShortcut(Context context, String url, OnFaviconLoadedListener onFaviconLoadedListener) {
+        ThreadUtils.assertOnBackgroundThread();
+
+        final BrowserDB db = GeckoProfile.get(context).getDB();
+
+        final ContentResolver cr = context.getContentResolver();
+        final Map<String, Map<String, Object>> metadata = db.getURLMetadata().getForURLs(cr,
+                Collections.singletonList(url),
+                Collections.singletonList(URLMetadataTable.TOUCH_ICON_COLUMN)
+        );
+
+        final Map<String, Object> row = metadata.get(url);
+
+        String touchIconURL = null;
+
+        if (row != null) {
+            touchIconURL = (String) row.get(URLMetadataTable.TOUCH_ICON_COLUMN);
+        }
+
+        // Retrieve the icon while bypassing the cache. Homescreen icon creation is a one-off event, hence it isn't
+        // useful to cache these icons. (Android takes care of storing homescreen icons after a shortcut
+        // has been created.)
+        // The cache is also (currently) limited to 32dp, hence we explicitly need to avoid accessing those icons.
+        // If touchIconURL is null, then Favicons falls back to finding the best possible favicon for
+        // the site URI, hence we can use this call even when there is no touchIcon defined.
+        getPreferredSizeFaviconForPage(context, url, touchIconURL, onFaviconLoadedListener);
+    }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/promotion/AddToHomeScreenPromotion.java
@@ -0,0 +1,196 @@
+package org.mozilla.gecko.promotion;
+
+import android.app.Activity;
+import android.database.Cursor;
+import android.util.Log;
+
+import com.keepsafe.switchboard.SwitchBoard;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.AboutPages;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.Tab;
+import org.mozilla.gecko.Tabs;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.db.UrlAnnotations;
+import org.mozilla.gecko.util.Experiments;
+
+import ch.boye.httpclientandroidlib.util.TextUtils;
+
+/**
+ * Promote "Add to home screen" if user visits website often.
+ */
+public class AddToHomeScreenPromotion implements Tabs.OnTabsChangedListener {
+    private static class URLHistory {
+        public final long visits;
+        public final long lastVisit;
+
+        private URLHistory(long visits, long lastVisit) {
+            this.visits = visits;
+            this.lastVisit = lastVisit;
+        }
+    }
+
+    private static final String LOGTAG = "GeckoPromoteShortcut";
+
+    private static final String EXPERIMENT_MINIMUM_TOTAL_VISITS = "minimumTotalVisits";
+    private static final String EXPERIMENT_LAST_VISIT_MINIMUM_AGE = "lastVisitMinimumAge";
+    private static final String EXPERIMENT_LAST_VISIT_MAXIMUM_AGE = "lastVisitMaximumAge";
+
+    private Activity activity;
+    private boolean isEnabled;
+    private int minimumVisits;
+    private int lastVisitMinimumAge;
+    private int lastVisitMaximumAge;
+
+    public AddToHomeScreenPromotion(Activity activity) {
+        this.activity = activity;
+
+        initializeExperiment();
+    }
+
+    public void resume() {
+        Tabs.registerOnTabsChangedListener(this);
+    }
+
+    public void pause() {
+        Tabs.unregisterOnTabsChangedListener(this);
+    }
+
+    private void initializeExperiment() {
+        isEnabled = SwitchBoard.isInExperiment(activity, Experiments.PROMOTE_ADD_TO_HOMESCREEN);
+        if (!isEnabled) {
+            Log.v(LOGTAG, "Experiment not enabled");
+            // Experiment is not enabled. No need to try to read values.
+            return;
+        }
+
+        JSONObject values = SwitchBoard.getExperimentValuesFromJson(activity, Experiments.PROMOTE_ADD_TO_HOMESCREEN);
+        if (values == null) {
+            // We didn't get any values for this experiment. Let's disable it instead of picking default
+            // values that might be bad.
+            isEnabled = false;
+            return;
+        }
+
+        try {
+            minimumVisits = values.getInt(EXPERIMENT_MINIMUM_TOTAL_VISITS);
+            lastVisitMinimumAge = values.getInt(EXPERIMENT_LAST_VISIT_MINIMUM_AGE);
+            lastVisitMaximumAge = values.getInt(EXPERIMENT_LAST_VISIT_MAXIMUM_AGE);
+        } catch (JSONException e) {
+            Log.w(LOGTAG, "Could not read experiment values", e);
+
+            // Disable experiment to avoid running the experiment with partial values.
+            isEnabled = false;
+        }
+    }
+
+    @Override
+    public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
+        if (tab == null) {
+            return;
+        }
+
+        if (!Tabs.getInstance().isSelectedTab(tab)) {
+            // We only ever want to show this promotion for the current tab.
+            return;
+        }
+
+        if (Tabs.TabEvents.LOADED == msg) {
+            maybeShowPromotionForUrl(tab.getURL(), tab.getTitle());
+        }
+    }
+
+    private void maybeShowPromotionForUrl(String url, String title) {
+        if (!isEnabled) {
+            return;
+        }
+
+        if (!shouldShowPromotion(url, title)) {
+            return;
+        }
+
+        HomeScreenPrompt.show(activity, url, title);
+    }
+
+    private boolean shouldShowPromotion(String url, String title) {
+        if (TextUtils.isEmpty(url) || TextUtils.isEmpty(title)) {
+            // We require an URL and a title for the shortcut.
+            return false;
+        }
+
+        if (AboutPages.isAboutPage(url)) {
+            // No promotion for our internal sites.
+            return false;
+        }
+
+        if (true) {
+            return true;
+        }
+
+        if (!url.startsWith("https://")) {
+            // Only promote websites that are served over HTTPS.
+            return false;
+        }
+
+        URLHistory history = getHistoryForURL(url);
+        if (history == null) {
+            Log.v(LOGTAG, "No previous visits");
+            // There's no history for this URL yet or we can't read it right now. Just ignore.
+            return false;
+        }
+
+        if (history.visits < minimumVisits) {
+            Log.v(LOGTAG, "Not enough visits (" + history.visits + ")");
+            return false;
+        }
+
+        if (history.lastVisit > System.currentTimeMillis() - lastVisitMinimumAge) {
+            Log.v(LOGTAG, "Last visit not old enough");
+            // The last visit is too new. Do not show promotion. This is mostly to avoid that the
+            // promotion shows up for a quick refreshs and in the worst case the last visit could
+            // be the current visit (race).
+            return false;
+        }
+
+        if (history.lastVisit < System.currentTimeMillis() - lastVisitMaximumAge) {
+            Log.v(LOGTAG, "Last visit too old");
+            // The last visit is to old. Do not show promotion.
+            return false;
+        }
+
+        final UrlAnnotations urlAnnotations = GeckoProfile.get(activity).getDB().getUrlAnnotations();
+        if (urlAnnotations.hasAcceptedOrDeclinedHomeScreenShortcut(activity.getContentResolver(), url)) {
+            Log.v(LOGTAG, "Shortcut already accepted or declined in the past");
+            // The user has already created a shortcut in the past or actively declined to create one.
+            // Let's not ask again for this url - We do not want to be annoying.
+            return false;
+        }
+
+        return true;
+    }
+
+    private URLHistory getHistoryForURL(String url) {
+        final GeckoProfile profile = GeckoProfile.get(activity);
+        final BrowserDB browserDB = profile.getDB();
+
+        Cursor cursor = null;
+        try {
+            cursor = browserDB.getHistoryForURL(activity.getContentResolver(), url);
+
+            if (cursor.moveToFirst()) {
+                return new URLHistory(
+                    cursor.getInt(cursor.getColumnIndex(BrowserContract.History.VISITS)),
+                    cursor.getLong(cursor.getColumnIndex(BrowserContract.History.DATE_LAST_VISITED)));
+            }
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+
+        return null;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/promotion/HomeScreenPrompt.java
@@ -0,0 +1,260 @@
+/* -*- 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.promotion;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.Locales;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.Telemetry;
+import org.mozilla.gecko.TelemetryContract;
+import org.mozilla.gecko.db.UrlAnnotations;
+import org.mozilla.gecko.favicons.Favicons;
+import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
+import org.mozilla.gecko.util.Experiments;
+import org.mozilla.gecko.util.ThreadUtils;
+
+/**
+ * Prompt to promote adding the current website to the home screen.
+ */
+public class HomeScreenPrompt extends Locales.LocaleAwareActivity implements OnFaviconLoadedListener {
+    private static final String EXTRA_TITLE = "title";
+    private static final String EXTRA_URL = "url";
+
+    private View containerView;
+    private ImageView iconView;
+    private View confirmationView;
+    private View addButton;
+    private String title;
+    private String url;
+    private boolean isAnimating;
+    private boolean hasAccepted;
+
+    public static void show(Context context, String url, String title) {
+        Intent intent = new Intent(context, HomeScreenPrompt.class);
+        intent.putExtra(EXTRA_TITLE, title);
+        intent.putExtra(EXTRA_URL, url);
+        context.startActivity(intent);
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        fetchDataFromIntent();
+        setupViews();
+        loadShortcutIcon();
+
+        slideIn();
+
+        Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, Experiments.PROMOTE_ADD_TO_HOMESCREEN);
+
+        // Technically this isn't triggered by a "service". But it's also triggered by a background task and without
+        // actual user interaction.
+        Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.SERVICE, "home_screen_promotion");
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+
+        Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, Experiments.PROMOTE_ADD_TO_HOMESCREEN);
+    }
+
+    private void fetchDataFromIntent() {
+        final Bundle extras = getIntent().getExtras();
+
+        title = extras.getString(EXTRA_TITLE);
+        url = extras.getString(EXTRA_URL);
+    }
+
+    private void setupViews() {
+        setContentView(R.layout.homescreen_prompt);
+
+        ((TextView) findViewById(R.id.title)).setText(title);
+
+        Uri uri = Uri.parse(url);
+        ((TextView) findViewById(R.id.host)).setText(uri.getHost());
+
+        containerView = findViewById(R.id.container);
+        iconView = (ImageView) findViewById(R.id.icon);
+        confirmationView = findViewById(R.id.confirmation);
+        addButton = findViewById(R.id.add);
+
+        addButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                hasAccepted = true;
+
+                addToHomeScreen();
+            }
+        });
+
+        findViewById(R.id.close).setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                rememberRejection();
+                slideOut();
+            }
+        });
+    }
+
+    private void addToHomeScreen() {
+        ThreadUtils.postToBackgroundThread(new Runnable() {
+            @Override
+            public void run() {
+                GeckoAppShell.createShortcut(title, url);
+            }
+        });
+
+        //animateConfirmation();
+    }
+
+    private void goToHomeScreen() {
+        Intent startMain = new Intent(Intent.ACTION_MAIN);
+        startMain.addCategory(Intent.CATEGORY_HOME);
+        startMain.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        startActivity(startMain);
+
+        finish();
+    }
+
+    private void animateConfirmation() {
+        confirmationView.setVisibility(View.VISIBLE);
+        confirmationView.setAlpha(0);
+
+        final Animator buttonAlphaAnimator = ObjectAnimator.ofFloat(addButton, "alpha", 0);
+        buttonAlphaAnimator.setDuration(300);
+
+        final Animator confirmationAlphaAnimator = ObjectAnimator.ofFloat(confirmationView, "alpha", 1);
+        confirmationAlphaAnimator.setDuration(300);
+        confirmationAlphaAnimator.setStartDelay(200);
+
+        final AnimatorSet set = new AnimatorSet();
+        set.playTogether(buttonAlphaAnimator, confirmationAlphaAnimator);
+        set.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                slideOutDelayed();
+            }
+        });
+        set.start();
+    }
+
+    private void loadShortcutIcon() {
+        ThreadUtils.postToBackgroundThread(new Runnable() {
+            @Override
+            public void run() {
+                Favicons.getPreferredIconForHomeScreenShortcut(HomeScreenPrompt.this, url, HomeScreenPrompt.this);
+            }
+        });
+    }
+
+    private void slideIn() {
+        containerView.setTranslationY(500);
+        containerView.setAlpha(0);
+
+        final Animator translateAnimator = ObjectAnimator.ofFloat(containerView, "translationY", 0);
+        translateAnimator.setDuration(400);
+
+        final Animator alphaAnimator = ObjectAnimator.ofFloat(containerView, "alpha", 1);
+        alphaAnimator.setStartDelay(200);
+        alphaAnimator.setDuration(600);
+
+        final AnimatorSet set = new AnimatorSet();
+        set.playTogether(alphaAnimator, translateAnimator);
+        set.setStartDelay(400);
+
+        set.start();
+    }
+
+    private void slideOutDelayed() {
+        ThreadUtils.postDelayedToUiThread(new Runnable() {
+            @Override
+            public void run() {
+                slideOut();
+            }
+        }, 1000);
+    }
+
+    private void rememberRejection() {
+        if (hasAccepted) {
+            // User has already accepted to create a shortcut.
+            return;
+        }
+
+        final UrlAnnotations urlAnnotations = GeckoProfile.get(this).getDB().getUrlAnnotations();
+        urlAnnotations.insertHomeScreenShortcut(getContentResolver(), url, false);
+    }
+
+    private void slideOut() {
+        if (isAnimating) {
+            return;
+        }
+
+        isAnimating = true;
+
+        ObjectAnimator animator = ObjectAnimator.ofFloat(containerView, "translationY", containerView.getHeight());
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                finish();
+            }
+
+        });
+        animator.start();
+    }
+
+    @Override
+    public void finish() {
+        super.finish();
+
+        // Don't perform an activity-dismiss animation.
+        overridePendingTransition(0, 0);
+    }
+
+    @Override
+    public void onBackPressed() {
+        rememberRejection();
+        slideOut();
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        rememberRejection();
+        slideOut();
+
+        return true;
+    }
+
+    @Override
+    public void onFaviconLoaded(String url, String faviconURL, final Bitmap favicon) {
+        if (favicon == null) {
+            return;
+        }
+
+        ThreadUtils.postToUiThread(new Runnable() {
+            @Override
+            public void run() {
+                iconView.setImageBitmap(favicon);
+            }
+        });
+    }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/util/Experiments.java
+++ b/mobile/android/base/java/org/mozilla/gecko/util/Experiments.java
@@ -40,16 +40,19 @@ public class Experiments {
     // on the client, they are not part of the server config.
     public static final String ONBOARDING2_A = "onboarding2-a"; // Control: Single (blue) welcome screen
     public static final String ONBOARDING2_B = "onboarding2-b"; // 4 static Feature slides
     public static final String ONBOARDING2_C = "onboarding2-c"; // 4 static + 1 clickable (Data saving) Feature slides
 
     // Synchronizing the catalog of downloadable content from Kinto
     public static final String DOWNLOAD_CONTENT_CATALOG_SYNC = "download-content-catalog-sync";
 
+    // Promotion for "Add to homescreen"
+    public static final String PROMOTE_ADD_TO_HOMESCREEN = "promote-add-to-homescreen";
+
     public static final String PREF_ONBOARDING_VERSION = "onboarding_version";
 
     private static volatile Boolean disabled = null;
 
     /**
      * Determines whether Switchboard is disabled by the MOZ_DISABLE_SWITCHBOARD
      * environment variable. We need to read this value from the intent string
      * extra because environment variables from our test harness aren't set
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -776,8 +776,10 @@ just addresses the organization to follo
 
 <!ENTITY eol_notification_title2 "&brandShortName; will no longer update">
 <!ENTITY eol_notification_summary "Tap to learn more">
 
 <!-- LOCALIZATION NOTE (whatsnew_notification_title, whatsnew_notification_summary): These strings
      are used for a system notification that's shown to users after the app updates. -->
 <!ENTITY whatsnew_notification_title "&brandShortName; is up to date">
 <!ENTITY whatsnew_notification_summary "Find out what\'s new in this version">
+
+<!ENTITY promotion_add_to_homescreen "Add to home screen">
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -507,16 +507,18 @@ gbjar.sources += ['java/org/mozilla/geck
     'preferences/PrivateDataPreference.java',
     'preferences/SearchEnginePreference.java',
     'preferences/SearchPreferenceCategory.java',
     'preferences/SetHomepagePreference.java',
     'preferences/SyncPreference.java',
     'PrefsHelper.java',
     'PrintHelper.java',
     'PrivateTab.java',
+    'promotion/AddToHomeScreenPromotion.java',
+    'promotion/HomeScreenPrompt.java',
     'prompts/ColorPickerInput.java',
     'prompts/IconGridInput.java',
     'prompts/IntentChooserPrompt.java',
     'prompts/IntentHandler.java',
     'prompts/Prompt.java',
     'prompts/PromptInput.java',
     'prompts/PromptListAdapter.java',
     'prompts/PromptListItem.java',
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -600,9 +600,11 @@
   <string name="eol_notification_summary">&eol_notification_summary;</string>
   <!-- https://support.mozilla.org/1/mobile/%VERSION%/%OS%/%LOCALE%/honeycomb -->
   <string name="eol_notification_url">https://support.mozilla.org/1/mobile/&formatS1;/&formatS2;/&formatS3;/unsupported-version</string>
 
   <string name="whatsnew_notification_title">&whatsnew_notification_title;</string>
   <string name="whatsnew_notification_summary">&whatsnew_notification_summary;</string>
   <!-- https://support.mozilla.org/1/mobile/%VERSION%/%OS%/%LOCALE%/new-android -->
   <string name="whatsnew_notification_url">https://support.mozilla.org/1/mobile/&formatS1;/&formatS2;/&formatS3;/new-android</string>
+
+  <string name="promotion_add_to_homescreen">&promotion_add_to_homescreen;</string>
 </resources>
new file mode 100644
--- /dev/null
+++ b/mobile/android/services/src/main/res/layout/homescreen_prompt.xml
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- 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/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:clipChildren="false"
+    android:clipToPadding="false">
+
+    <RelativeLayout
+        android:id="@+id/container"
+        android:layout_width="@dimen/overlay_prompt_container_width"
+        android:layout_height="wrap_content"
+        android:layout_gravity="bottom|center"
+        android:background="@android:color/white"
+        android:orientation="vertical">
+
+        <ImageView
+            android:id="@+id/close"
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:layout_marginTop="30dp"
+            android:layout_marginRight="30dp"
+            android:layout_marginLeft="10dp"
+            android:padding="6dp"
+            android:maxLines="2"
+            android:ellipsize="end"
+            android:layout_alignParentRight="true"
+            android:src="@drawable/tab_close_active"/>
+
+        <TextView
+            android:id="@+id/title"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="6dp"
+            android:layout_marginLeft="30dp"
+            android:layout_marginTop="30dp"
+            android:layout_toLeftOf="@id/close"
+            android:fontFamily="sans-serif-light"
+            android:textColor="@color/text_and_tabs_tray_grey"
+            android:textSize="20sp"
+            tools:text="The Pokedex" />
+
+        <TextView
+            android:id="@+id/host"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_below="@id/title"
+            android:layout_marginBottom="20dp"
+            android:layout_marginLeft="30dp"
+            android:layout_marginRight="30dp"
+            android:maxLines="1"
+            android:ellipsize="end"
+            android:textColor="@color/placeholder_grey"
+            android:textSize="16sp"
+            tools:text="pokedex.org" />
+
+        <ImageView
+            android:id="@+id/confirmation"
+            android:layout_width="30dp"
+            android:layout_height="30dp"
+            android:layout_below="@id/host"
+            android:layout_marginBottom="20dp"
+            android:layout_marginTop="10dp"
+            android:layout_marginLeft="20dp"
+            android:layout_centerHorizontal="true"
+            android:src="@drawable/img_check"
+            android:visibility="gone" />
+
+        <ImageView
+            android:id="@+id/icon"
+            android:layout_width="50dp"
+            android:layout_height="50dp"
+            android:layout_below="@id/host"
+            android:layout_marginBottom="20dp"
+            android:layout_marginLeft="30dp"
+            android:src="@drawable/icon" />
+
+        <Button
+            android:id="@+id/add"
+            style="@style/Widget.BaseButton"
+            android:layout_width="wrap_content"
+            android:layout_height="50dp"
+            android:layout_alignParentRight="true"
+            android:layout_below="@id/host"
+            android:layout_marginBottom="20dp"
+            android:layout_marginRight="30dp"
+            android:layout_marginLeft="20dp"
+            android:background="@drawable/button_background_action_orange_round"
+            android:paddingLeft="16dp"
+            android:paddingRight="16dp"
+            android:text="@string/promotion_add_to_homescreen"
+            android:textColor="@android:color/white"
+            android:textSize="16sp" />
+
+    </RelativeLayout>
+</merge>