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