Commit 4a144829 authored by benwells's avatar benwells Committed by Commit bot

Use heuristic to work out when to prompt for app install banners.

The heuristic ensures that the user is not prompted too much, and is only
prompted after showing sustained engagement with a site.

BUG=452825

Review URL: https://codereview.chromium.org/886643003

Cr-Commit-Position: refs/heads/master@{#315055}
parent f14f8f0e
...@@ -69,7 +69,9 @@ void AppBannerManager::BlockBanner(JNIEnv* env, ...@@ -69,7 +69,9 @@ void AppBannerManager::BlockBanner(JNIEnv* env,
GURL url(ConvertJavaStringToUTF8(env, jurl)); GURL url(ConvertJavaStringToUTF8(env, jurl));
std::string package_name = ConvertJavaStringToUTF8(env, jpackage); std::string package_name = ConvertJavaStringToUTF8(env, jpackage);
AppBannerSettingsHelper::Block(web_contents(), url, package_name); AppBannerSettingsHelper::RecordBannerEvent(
web_contents(), url, package_name,
AppBannerSettingsHelper::APP_BANNER_EVENT_DID_BLOCK, base::Time::Now());
} }
void AppBannerManager::Block() const { void AppBannerManager::Block() const {
...@@ -77,9 +79,10 @@ void AppBannerManager::Block() const { ...@@ -77,9 +79,10 @@ void AppBannerManager::Block() const {
return; return;
if (!web_app_data_.IsEmpty()) { if (!web_app_data_.IsEmpty()) {
AppBannerSettingsHelper::Block(web_contents(), AppBannerSettingsHelper::RecordBannerEvent(
web_contents()->GetURL(), web_contents(), web_contents()->GetURL(),
web_app_data_.start_url.spec()); web_app_data_.start_url.spec(),
AppBannerSettingsHelper::APP_BANNER_EVENT_DID_BLOCK, base::Time::Now());
} }
} }
...@@ -92,6 +95,12 @@ bool AppBannerManager::OnButtonClicked() const { ...@@ -92,6 +95,12 @@ bool AppBannerManager::OnButtonClicked() const {
return true; return true;
if (!web_app_data_.IsEmpty()) { if (!web_app_data_.IsEmpty()) {
AppBannerSettingsHelper::RecordBannerEvent(
web_contents(), web_contents()->GetURL(),
web_app_data_.start_url.spec(),
AppBannerSettingsHelper::APP_BANNER_EVENT_DID_ADD_TO_HOMESCREEN,
base::Time::Now());
InstallManifestApp(web_app_data_, *app_icon_.get()); InstallManifestApp(web_app_data_, *app_icon_.get());
return true; return true;
} }
...@@ -183,10 +192,35 @@ void AppBannerManager::OnDidCheckHasServiceWorker(bool has_service_worker) { ...@@ -183,10 +192,35 @@ void AppBannerManager::OnDidCheckHasServiceWorker(bool has_service_worker) {
if (icon_url.is_empty()) if (icon_url.is_empty())
return; return;
RecordCouldShowBanner(web_app_data_.start_url.spec());
if (!CheckIfShouldShow(web_app_data_.start_url.spec()))
return;
FetchIcon(icon_url); FetchIcon(icon_url);
} }
} }
void AppBannerManager::RecordCouldShowBanner(
const std::string& package_or_start_url) {
AppBannerSettingsHelper::RecordBannerEvent(
web_contents(), validated_url_, package_or_start_url,
AppBannerSettingsHelper::APP_BANNER_EVENT_COULD_SHOW, base::Time::Now());
}
bool AppBannerManager::CheckIfShouldShow(
const std::string& package_or_start_url) {
if (!AppBannerSettingsHelper::ShouldShowBanner(web_contents(), validated_url_,
package_or_start_url,
base::Time::Now())) {
return false;
}
AppBannerSettingsHelper::RecordBannerEvent(
web_contents(), validated_url_, package_or_start_url,
AppBannerSettingsHelper::APP_BANNER_EVENT_DID_SHOW, base::Time::Now());
return true;
}
bool AppBannerManager::OnMessageReceived(const IPC::Message& message) { bool AppBannerManager::OnMessageReceived(const IPC::Message& message) {
bool handled = true; bool handled = true;
IPC_BEGIN_MESSAGE_MAP(AppBannerManager, message) IPC_BEGIN_MESSAGE_MAP(AppBannerManager, message)
...@@ -237,11 +271,9 @@ void AppBannerManager::OnDidRetrieveMetaTagContent( ...@@ -237,11 +271,9 @@ void AppBannerManager::OnDidRetrieveMetaTagContent(
banners::TrackDisplayEvent(DISPLAY_BANNER_REQUESTED); banners::TrackDisplayEvent(DISPLAY_BANNER_REQUESTED);
if (!AppBannerSettingsHelper::IsAllowed(web_contents(), RecordCouldShowBanner(tag_content);
expected_url, if (!CheckIfShouldShow(tag_content))
tag_content)) {
return; return;
}
// Send the info to the Java side to get info about the app. // Send the info to the Java side to get info about the app.
JNIEnv* env = base::android::AttachCurrentThread(); JNIEnv* env = base::android::AttachCurrentThread();
......
...@@ -138,6 +138,13 @@ class AppBannerManager : public chrome::BitmapFetcherDelegate, ...@@ -138,6 +138,13 @@ class AppBannerManager : public chrome::BitmapFetcherDelegate,
// Called when the result of the CheckHasServiceWorker query has completed. // Called when the result of the CheckHasServiceWorker query has completed.
void OnDidCheckHasServiceWorker(bool has_service_worker); void OnDidCheckHasServiceWorker(bool has_service_worker);
// Record that the banner could be shown at this point, if the triggering
// heuristic allowed.
void RecordCouldShowBanner(const std::string& package_or_start_url);
// Check if the banner should be shown.
bool CheckIfShouldShow(const std::string& package_or_start_url);
// Fetches the icon for an app. // Fetches the icon for an app.
scoped_ptr<chrome::BitmapFetcher> fetcher_; scoped_ptr<chrome::BitmapFetcher> fetcher_;
GURL validated_url_; GURL validated_url_;
......
...@@ -23,21 +23,28 @@ const size_t kMaxAppsPerSite = 3; ...@@ -23,21 +23,28 @@ const size_t kMaxAppsPerSite = 3;
// Oldest could show banner event we care about, in days. // Oldest could show banner event we care about, in days.
const unsigned int kOldestCouldShowBannerEventInDays = 14; const unsigned int kOldestCouldShowBannerEventInDays = 14;
// Dictionary key to use for the 'could show banner' events. // Number of times that the banner could have been shown before the banner will
const char kCouldShowBannerEventsKey[] = "couldShowBannerEvents"; // actually be triggered.
const unsigned int kCouldShowEventsToTrigger = 2;
// Number of days that showing the banner will prevent it being seen again for.
const unsigned int kMinimumDaysBetweenBannerShows = 60;
// Number of days that the banner being blocked will prevent it being seen again
// for.
const unsigned int kMinimumBannerBlockedToBannerShown = 90;
// Dictionary keys to use for the events.
const char* kBannerEventKeys[] = {
"couldShowBannerEvents",
"didShowBannerEvent",
"didBlockBannerEvent",
"didAddToHomescreenEvent",
};
// Dictionary key to use whether the banner has been blocked. // Dictionary key to use whether the banner has been blocked.
const char kHasBlockedKey[] = "hasBlocked"; const char kHasBlockedKey[] = "hasBlocked";
base::Time DateFromTime(base::Time time) {
base::Time::Exploded exploded;
time.LocalExplode(&exploded);
exploded.hour = 0;
exploded.minute = 0;
exploded.second = 0;
return base::Time::FromLocalExploded(exploded);
}
scoped_ptr<base::DictionaryValue> GetOriginDict( scoped_ptr<base::DictionaryValue> GetOriginDict(
HostContentSettingsMap* settings, HostContentSettingsMap* settings,
const GURL& origin_url) { const GURL& origin_url) {
...@@ -72,10 +79,11 @@ base::DictionaryValue* GetAppDict(base::DictionaryValue* origin_dict, ...@@ -72,10 +79,11 @@ base::DictionaryValue* GetAppDict(base::DictionaryValue* origin_dict,
} // namespace } // namespace
void AppBannerSettingsHelper::RecordCouldShowBannerEvent( void AppBannerSettingsHelper::RecordBannerEvent(
content::WebContents* web_contents, content::WebContents* web_contents,
const GURL& origin_url, const GURL& origin_url,
const std::string& package_name_or_start_url, const std::string& package_name_or_start_url,
AppBannerEvent event,
base::Time time) { base::Time time) {
Profile* profile = Profile* profile =
Profile::FromBrowserContext(web_contents->GetBrowserContext()); Profile::FromBrowserContext(web_contents->GetBrowserContext());
...@@ -99,50 +107,92 @@ void AppBannerSettingsHelper::RecordCouldShowBannerEvent( ...@@ -99,50 +107,92 @@ void AppBannerSettingsHelper::RecordCouldShowBannerEvent(
if (!app_dict) if (!app_dict)
return; return;
base::ListValue* could_show_list = nullptr; std::string event_key(kBannerEventKeys[event]);
if (!app_dict->GetList(kCouldShowBannerEventsKey, &could_show_list)) {
could_show_list = new base::ListValue();
app_dict->Set(kCouldShowBannerEventsKey, make_scoped_ptr(could_show_list));
}
// Trim any items that are older than we should care about. For comparisons if (event == APP_BANNER_EVENT_COULD_SHOW) {
// the times are converted to local dates. base::ListValue* could_show_list = nullptr;
base::Time date = DateFromTime(time); if (!app_dict->GetList(event_key, &could_show_list)) {
base::ValueVector::iterator it = could_show_list->begin(); could_show_list = new base::ListValue();
while (it != could_show_list->end()) { app_dict->Set(event_key, make_scoped_ptr(could_show_list));
if ((*it)->IsType(base::Value::TYPE_DOUBLE)) { }
double internal_date;
(*it)->GetAsDouble(&internal_date); // Trim any items that are older than we should care about. For comparisons
base::Time other_date = // the times are converted to local dates.
DateFromTime(base::Time::FromInternalValue(internal_date)); base::Time date = time.LocalMidnight();
// This date has already been added. Don't add the date again, and don't base::ValueVector::iterator it = could_show_list->begin();
// bother trimming values as it will have been done the first time the while (it != could_show_list->end()) {
// date was added (unless the local date has changed, which we can live if ((*it)->IsType(base::Value::TYPE_DOUBLE)) {
// with). double internal_date;
if (other_date == date) (*it)->GetAsDouble(&internal_date);
return; base::Time other_date =
base::Time::FromInternalValue(internal_date).LocalMidnight();
base::TimeDelta delta = date - other_date; // This date has already been added. Don't add the date again, and don't
if (delta < // bother trimming values as it will have been done the first time the
base::TimeDelta::FromDays(kOldestCouldShowBannerEventInDays)) { // date was added (unless the local date has changed, which we can live
++it; // with).
continue; if (other_date == date)
return;
base::TimeDelta delta = date - other_date;
if (delta <
base::TimeDelta::FromDays(kOldestCouldShowBannerEventInDays)) {
++it;
continue;
}
} }
// Either this date is older than we care about, or it isn't a date, so
// remove it;
it = could_show_list->Erase(it, nullptr);
} }
// Either this date is older than we care about, or it isn't a date, so // Dates are stored in their raw form (i.e. not local dates) to be resilient
// remove it; // to time zone changes.
it = could_show_list->Erase(it, nullptr); could_show_list->AppendDouble(time.ToInternalValue());
} else {
app_dict->SetDouble(event_key, time.ToInternalValue());
} }
// Dates are stored in their raw form (i.e. not local dates) to be resilient
// to time zone changes.
could_show_list->AppendDouble(time.ToInternalValue());
settings->SetWebsiteSetting(pattern, ContentSettingsPattern::Wildcard(), settings->SetWebsiteSetting(pattern, ContentSettingsPattern::Wildcard(),
CONTENT_SETTINGS_TYPE_APP_BANNER, std::string(), CONTENT_SETTINGS_TYPE_APP_BANNER, std::string(),
origin_dict.release()); origin_dict.release());
} }
bool AppBannerSettingsHelper::ShouldShowBanner(
content::WebContents* web_contents,
const GURL& origin_url,
const std::string& package_name_or_start_url,
base::Time time) {
// Don't show if it has been added to the homescreen.
base::Time added_time =
GetSingleBannerEvent(web_contents, origin_url, package_name_or_start_url,
APP_BANNER_EVENT_DID_ADD_TO_HOMESCREEN);
if (!added_time.is_null())
return false;
base::Time blocked_time =
GetSingleBannerEvent(web_contents, origin_url, package_name_or_start_url,
APP_BANNER_EVENT_DID_BLOCK);
// Null times are in the distant past, so the delta between real times and
// null events will always be greater than the limits.
if (time - blocked_time <
base::TimeDelta::FromDays(kMinimumBannerBlockedToBannerShown)) {
return false;
}
base::Time shown_time =
GetSingleBannerEvent(web_contents, origin_url, package_name_or_start_url,
APP_BANNER_EVENT_DID_SHOW);
if (time - shown_time <
base::TimeDelta::FromDays(kMinimumDaysBetweenBannerShows)) {
return false;
}
std::vector<base::Time> could_show_events = GetCouldShowBannerEvents(
web_contents, origin_url, package_name_or_start_url);
return could_show_events.size() >= kCouldShowEventsToTrigger;
}
std::vector<base::Time> AppBannerSettingsHelper::GetCouldShowBannerEvents( std::vector<base::Time> AppBannerSettingsHelper::GetCouldShowBannerEvents(
content::WebContents* web_contents, content::WebContents* web_contents,
const GURL& origin_url, const GURL& origin_url,
...@@ -165,8 +215,9 @@ std::vector<base::Time> AppBannerSettingsHelper::GetCouldShowBannerEvents( ...@@ -165,8 +215,9 @@ std::vector<base::Time> AppBannerSettingsHelper::GetCouldShowBannerEvents(
if (!app_dict) if (!app_dict)
return result; return result;
std::string event_key(kBannerEventKeys[APP_BANNER_EVENT_COULD_SHOW]);
base::ListValue* could_show_list = nullptr; base::ListValue* could_show_list = nullptr;
if (!app_dict->GetList(kCouldShowBannerEventsKey, &could_show_list)) if (!app_dict->GetList(event_key, &could_show_list))
return result; return result;
for (auto value : *could_show_list) { for (auto value : *could_show_list) {
...@@ -181,6 +232,39 @@ std::vector<base::Time> AppBannerSettingsHelper::GetCouldShowBannerEvents( ...@@ -181,6 +232,39 @@ std::vector<base::Time> AppBannerSettingsHelper::GetCouldShowBannerEvents(
return result; return result;
} }
base::Time AppBannerSettingsHelper::GetSingleBannerEvent(
content::WebContents* web_contents,
const GURL& origin_url,
const std::string& package_name_or_start_url,
AppBannerEvent event) {
DCHECK(event != APP_BANNER_EVENT_COULD_SHOW);
DCHECK(event < APP_BANNER_EVENT_NUM_EVENTS);
if (package_name_or_start_url.empty())
return base::Time();
Profile* profile =
Profile::FromBrowserContext(web_contents->GetBrowserContext());
HostContentSettingsMap* settings = profile->GetHostContentSettingsMap();
scoped_ptr<base::DictionaryValue> origin_dict =
GetOriginDict(settings, origin_url);
if (!origin_dict)
return base::Time();
base::DictionaryValue* app_dict =
GetAppDict(origin_dict.get(), package_name_or_start_url);
if (!app_dict)
return base::Time();
std::string event_key(kBannerEventKeys[event]);
double internal_time;
if (!app_dict->GetDouble(event_key, &internal_time))
return base::Time();
return base::Time::FromInternalValue(internal_time);
}
bool AppBannerSettingsHelper::IsAllowed( bool AppBannerSettingsHelper::IsAllowed(
content::WebContents* web_contents, content::WebContents* web_contents,
const GURL& origin_url, const GURL& origin_url,
......
...@@ -17,37 +17,62 @@ class WebContents; ...@@ -17,37 +17,62 @@ class WebContents;
class GURL; class GURL;
// Utility class for reading and updating ContentSettings for app banners. // Utility class to record banner events for the given package or start url.
//
// These events are used to decide when banners should be shown, using a
// heuristic based on how many different days in a recent period of time (for
// example the past two weeks) the banner could have been shown, when it was
// last shown, when it was last blocked, and when it was last installed (for
// ServiceWorker style apps - native apps can query whether the app was
// installed directly).
//
// The desired effect is to have banners appear once a user has demonstrated
// an ongoing relationship with the app, and not to pester the user too much.
//
// For most events only the last event is recorded. The exception are the
// could show events. For these a list of the events is maintained. At most
// one event is stored per day, and events outside the window the heuristic
// uses are discarded. Local times are used to enforce these rules, to ensure
// what we count as a day matches what the user perceives to be days.
class AppBannerSettingsHelper { class AppBannerSettingsHelper {
public: public:
// TODO(benwells): Use this method to implement smarter triggering logic. enum AppBannerEvent {
// See http://crbug.com/452825. APP_BANNER_EVENT_COULD_SHOW,
// Records that a banner could have been shown for the given package or start APP_BANNER_EVENT_DID_SHOW,
// url. APP_BANNER_EVENT_DID_BLOCK,
// APP_BANNER_EVENT_DID_ADD_TO_HOMESCREEN,
// These events are used to decide when banners should be shown, using a APP_BANNER_EVENT_NUM_EVENTS,
// heuristic based on how many different days in a recent period of time (for };
// example the past two weeks) the banner could have been shown. The desired
// effect is to have banners appear once a user has demonstrated an ongoing static void RecordBannerEvent(content::WebContents* web_contents,
// relationship with the app. const GURL& origin_url,
// const std::string& package_name_or_start_url,
// At most one event is stored per day, and events outside the window the AppBannerEvent event,
// heuristic uses are discarded. Local times are used to enforce these rules, base::Time time);
// to ensure what we count as a day matches what the user perceives to be
// days. // Determine if the banner should be shown, given the recorded events for the
static void RecordCouldShowBannerEvent( // supplied app.
content::WebContents* web_contents, static bool ShouldShowBanner(content::WebContents* web_contents,
const GURL& origin_url, const GURL& origin_url,
const std::string& package_name_or_start_url, const std::string& package_name_or_start_url,
base::Time time); base::Time time);
// Gets the could have been shown events that are stored for the given package // Gets the could have been shown events that are stored for the given package
// or start url. This is only used for testing. // or start url. This is only exposed for testing.
static std::vector<base::Time> GetCouldShowBannerEvents( static std::vector<base::Time> GetCouldShowBannerEvents(
content::WebContents* web_contents, content::WebContents* web_contents,
const GURL& origin_url, const GURL& origin_url,
const std::string& package_name_or_start_url); const std::string& package_name_or_start_url);
// Get the recorded event for an event type that only records the last event.
// Should not be used with APP_BANNER_EVENT_COULD_SHOW. This is only exposed
// for testing.
static base::Time GetSingleBannerEvent(
content::WebContents* web_contents,
const GURL& origin_url,
const std::string& package_name_or_start_url,
AppBannerEvent event);
// Checks if a URL is allowed to show a banner for the given package or start // Checks if a URL is allowed to show a banner for the given package or start
// url. // url.
static bool IsAllowed(content::WebContents* web_contents, static bool IsAllowed(content::WebContents* web_contents,
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment