1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 2 * This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 package org.mozilla.gecko.promotion; 7 8 import android.app.Activity; 9 import android.content.Context; 10 import android.database.Cursor; 11 import android.os.Bundle; 12 import android.support.annotation.CallSuper; 13 import android.support.annotation.Nullable; 14 import android.util.Log; 15 16 import org.mozilla.gecko.switchboard.SwitchBoard; 17 18 import org.json.JSONException; 19 import org.json.JSONObject; 20 import org.mozilla.gecko.AboutPages; 21 import org.mozilla.gecko.BrowserApp; 22 import org.mozilla.gecko.GeckoProfile; 23 import org.mozilla.gecko.Tab; 24 import org.mozilla.gecko.Tabs; 25 import org.mozilla.gecko.db.BrowserContract; 26 import org.mozilla.gecko.db.BrowserDB; 27 import org.mozilla.gecko.db.UrlAnnotations; 28 import org.mozilla.gecko.delegates.TabsTrayVisibilityAwareDelegate; 29 import org.mozilla.gecko.Experiments; 30 import org.mozilla.gecko.util.ThreadUtils; 31 32 import java.lang.ref.WeakReference; 33 34 import ch.boye.httpclientandroidlib.util.TextUtils; 35 36 /** 37 * Promote "Add to home screen" if user visits website often. 38 */ 39 public class AddToHomeScreenPromotion extends TabsTrayVisibilityAwareDelegate implements Tabs.OnTabsChangedListener { 40 public static class URLHistory { 41 public final long visits; 42 public final long lastVisit; 43 URLHistory(long visits, long lastVisit)44 private URLHistory(long visits, long lastVisit) { 45 this.visits = visits; 46 this.lastVisit = lastVisit; 47 } 48 } 49 50 private static final String LOGTAG = "GeckoPromoteShortcut"; 51 52 private static final String EXPERIMENT_MINIMUM_TOTAL_VISITS = "minimumTotalVisits"; 53 private static final String EXPERIMENT_LAST_VISIT_MINIMUM_AGE = "lastVisitMinimumAgeMs"; 54 private static final String EXPERIMENT_LAST_VISIT_MAXIMUM_AGE = "lastVisitMaximumAgeMs"; 55 56 private WeakReference<Activity> activityReference; 57 private boolean isEnabled; 58 private int minimumVisits; 59 private int lastVisitMinimumAgeMs; 60 private int lastVisitMaximumAgeMs; 61 62 @CallSuper 63 @Override onCreate(BrowserApp browserApp, Bundle savedInstanceState)64 public void onCreate(BrowserApp browserApp, Bundle savedInstanceState) { 65 super.onCreate(browserApp, savedInstanceState); 66 activityReference = new WeakReference<Activity>(browserApp); 67 68 initializeExperiment(browserApp); 69 } 70 71 @Override onResume(BrowserApp browserApp)72 public void onResume(BrowserApp browserApp) { 73 Tabs.registerOnTabsChangedListener(this); 74 } 75 76 @Override onPause(BrowserApp browserApp)77 public void onPause(BrowserApp browserApp) { 78 Tabs.unregisterOnTabsChangedListener(this); 79 } 80 initializeExperiment(Context context)81 private void initializeExperiment(Context context) { 82 if (!SwitchBoard.isInExperiment(context, Experiments.PROMOTE_ADD_TO_HOMESCREEN)) { 83 Log.v(LOGTAG, "Experiment not enabled"); 84 // Experiment is not enabled. No need to try to read values. 85 return; 86 } 87 88 JSONObject values = SwitchBoard.getExperimentValuesFromJson(context, Experiments.PROMOTE_ADD_TO_HOMESCREEN); 89 if (values == null) { 90 // We didn't get any values for this experiment. Let's disable it instead of picking default 91 // values that might be bad. 92 return; 93 } 94 95 try { 96 initializeWithValues( 97 values.getInt(EXPERIMENT_MINIMUM_TOTAL_VISITS), 98 values.getInt(EXPERIMENT_LAST_VISIT_MINIMUM_AGE), 99 values.getInt(EXPERIMENT_LAST_VISIT_MAXIMUM_AGE)); 100 } catch (JSONException e) { 101 Log.w(LOGTAG, "Could not read experiment values", e); 102 } 103 } 104 initializeWithValues(int minimumVisits, int lastVisitMinimumAgeMs, int lastVisitMaximumAgeMs)105 private void initializeWithValues(int minimumVisits, int lastVisitMinimumAgeMs, int lastVisitMaximumAgeMs) { 106 this.isEnabled = true; 107 108 this.minimumVisits = minimumVisits; 109 this.lastVisitMinimumAgeMs = lastVisitMinimumAgeMs; 110 this.lastVisitMaximumAgeMs = lastVisitMaximumAgeMs; 111 } 112 113 @Override onTabChanged(final Tab tab, Tabs.TabEvents msg, String data)114 public void onTabChanged(final Tab tab, Tabs.TabEvents msg, String data) { 115 if (tab == null) { 116 return; 117 } 118 119 if (!Tabs.getInstance().isSelectedTab(tab)) { 120 // We only ever want to show this promotion for the current tab. 121 return; 122 } 123 124 if (Tabs.TabEvents.LOADED != msg) { 125 return; 126 } 127 128 if (tab.isPrivate()) { 129 // Never show the prompt for private browsing tabs. 130 return; 131 } 132 133 if (isTabsTrayVisible()) { 134 // We only want to show this prompt if this tab is in the foreground and not on top 135 // of the tabs tray. 136 return; 137 } 138 139 ThreadUtils.postToBackgroundThread(new Runnable() { 140 @Override 141 public void run() { 142 maybeShowPromotionForUrl(tab.getURL(), tab.getTitle()); 143 } 144 }); 145 } 146 maybeShowPromotionForUrl(String url, String title)147 private void maybeShowPromotionForUrl(String url, String title) { 148 if (!isEnabled) { 149 return; 150 } 151 152 final Context context = activityReference.get(); 153 if (context == null) { 154 return; 155 } 156 157 if (!shouldShowPromotion(context, url, title)) { 158 return; 159 } 160 161 HomeScreenPrompt.show(context, url, title); 162 } 163 shouldShowPromotion(Context context, String url, String title)164 private boolean shouldShowPromotion(Context context, String url, String title) { 165 if (TextUtils.isEmpty(url) || TextUtils.isEmpty(title)) { 166 // We require an URL and a title for the shortcut. 167 return false; 168 } 169 170 if (AboutPages.isAboutPage(url)) { 171 // No promotion for our internal sites. 172 return false; 173 } 174 175 if (!url.startsWith("https://")) { 176 // Only promote websites that are served over HTTPS. 177 return false; 178 } 179 180 URLHistory history = getHistoryForURL(context, url); 181 if (history == null) { 182 // There's no history for this URL yet or we can't read it right now. Just ignore. 183 return false; 184 } 185 186 if (history.visits < minimumVisits) { 187 // This URL has not been visited often enough. 188 return false; 189 } 190 191 if (history.lastVisit > System.currentTimeMillis() - lastVisitMinimumAgeMs) { 192 // The last visit is too new. Do not show promotion. This is mostly to avoid that the 193 // promotion shows up for a quick refreshs and in the worst case the last visit could 194 // be the current visit (race). 195 return false; 196 } 197 198 if (history.lastVisit < System.currentTimeMillis() - lastVisitMaximumAgeMs) { 199 // The last visit is to old. Do not show promotion. 200 return false; 201 } 202 203 if (hasAcceptedOrDeclinedHomeScreenShortcut(context, url)) { 204 // The user has already created a shortcut in the past or actively declined to create one. 205 // Let's not ask again for this url - We do not want to be annoying. 206 return false; 207 } 208 209 return true; 210 } 211 hasAcceptedOrDeclinedHomeScreenShortcut(Context context, String url)212 protected boolean hasAcceptedOrDeclinedHomeScreenShortcut(Context context, String url) { 213 final UrlAnnotations urlAnnotations = BrowserDB.from(context).getUrlAnnotations(); 214 return urlAnnotations.hasAcceptedOrDeclinedHomeScreenShortcut(context.getContentResolver(), url); 215 } 216 217 @Nullable getHistoryForURL(Context context, String url)218 public static URLHistory getHistoryForURL(Context context, String url) { 219 final GeckoProfile profile = GeckoProfile.get(context); 220 final BrowserDB browserDB = BrowserDB.from(profile); 221 222 Cursor cursor = null; 223 try { 224 cursor = browserDB.getHistoryForURL(context.getContentResolver(), url); 225 226 if (cursor == null) { 227 return null; 228 } 229 230 if (cursor.moveToFirst()) { 231 return new URLHistory( 232 cursor.getInt(cursor.getColumnIndex(BrowserContract.History.VISITS)), 233 cursor.getLong(cursor.getColumnIndex(BrowserContract.History.DATE_LAST_VISITED))); 234 } 235 } finally { 236 if (cursor != null) { 237 cursor.close(); 238 } 239 } 240 241 return null; 242 } 243 } 244