Bug 774479 - Create about:feedback page, and show it to users after the app has been launched 10 times. r=mfinkle

This commit is contained in:
Margaret Leibovic 2012-08-26 21:33:11 -07:00
parent a19ad5cd0d
commit c8694baa6d
13 changed files with 514 additions and 3 deletions

View File

@ -426,7 +426,8 @@ pref("plugins.use_placeholder", 0);
// The breakpad report server to link to in about:crashes
pref("breakpad.reportURL", "http://crash-stats.mozilla.com/report/index/");
pref("app.support.baseURL", "http://support.mozilla.org/1/mobile/%VERSION%/%OS%/%LOCALE%/");
pref("app.feedbackURL", "http://input.mozilla.com/feedback/");
// Used to submit data to input from about:feedback
pref("app.feedback.postURL", "http://m.input.mozilla.org/%LOCALE%/feedback");
pref("app.privacyURL", "http://www.mozilla.com/%LOCALE%/m/privacy.html");
pref("app.creditsURL", "http://www.mozilla.org/credits/");
pref("app.channelURL", "http://www.mozilla.org/%LOCALE%/firefox/channel/");

View File

@ -5,12 +5,19 @@
package org.mozilla.gecko;
import org.mozilla.gecko.db.BrowserContract.Combined;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.util.GeckoAsyncTask;
import org.mozilla.gecko.util.GeckoBackgroundThread;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.app.Activity;
import android.content.SharedPreferences;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
@ -49,6 +56,9 @@ abstract public class BrowserApp extends GeckoApp
private FindInPageBar mFindInPageBar;
// We'll ask for feedback after the user launches the app this many times.
private static final int FEEDBACK_LAUNCH_COUNT = 10;
@Override
public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
switch(msg) {
@ -188,6 +198,10 @@ abstract public class BrowserApp extends GeckoApp
if (savedInstanceState != null) {
mBrowserToolbar.setTitle(savedInstanceState.getString(SAVED_STATE_TITLE));
}
registerEventListener("Feedback:LastUrl");
registerEventListener("Feedback:OpenPlayStore");
registerEventListener("Feedback:MaybeLater");
}
@Override
@ -195,6 +209,10 @@ abstract public class BrowserApp extends GeckoApp
super.onDestroy();
if (mAboutHomeContent != null)
mAboutHomeContent.onDestroy();
unregisterEventListener("Feedback:LastUrl");
unregisterEventListener("Feedback:OpenPlayStore");
unregisterEventListener("Feedback:MaybeLater");
}
@Override
@ -338,6 +356,14 @@ abstract public class BrowserApp extends GeckoApp
removeAddonMenuItem(id);
}
});
} else if (event.equals("Feedback:OpenPlayStore")) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("market://details?id=" + getPackageName()));
startActivity(intent);
} else if (event.equals("Feedback:MaybeLater")) {
resetFeedbackLaunchCount();
} else if (event.equals("Feedback:LastUrl")) {
getLastUrl();
} else {
super.handleMessage(event, message);
}
@ -801,4 +827,74 @@ abstract public class BrowserApp extends GeckoApp
return super.onOptionsItemSelected(item);
}
}
/*
* If the app has been launched a certain number of times, and we haven't asked for feedback before,
* open a new tab with about:feedback when launching the app from the icon shortcut.
*/
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
if (!Intent.ACTION_MAIN.equals(intent.getAction()))
return;
(new GeckoAsyncTask<Void, Void, Boolean>(mAppContext, GeckoAppShell.getHandler()) {
@Override
public synchronized Boolean doInBackground(Void... params) {
// Check to see how many times the app has been launched.
SharedPreferences settings = getPreferences(Activity.MODE_PRIVATE);
String keyName = getPackageName() + ".feedback_launch_count";
int launchCount = settings.getInt(keyName, 0);
if (launchCount >= FEEDBACK_LAUNCH_COUNT)
return false;
// Increment the launch count and store the new value.
launchCount++;
settings.edit().putInt(keyName, launchCount).commit();
// If we've reached our magic number, show the feedback page.
return launchCount == FEEDBACK_LAUNCH_COUNT;
}
@Override
public void onPostExecute(Boolean shouldShowFeedbackPage) {
if (shouldShowFeedbackPage)
loadUrlInTab("about:feedback");
}
}).execute();
}
private void resetFeedbackLaunchCount() {
GeckoBackgroundThread.post(new Runnable() {
@Override
public synchronized void run() {
SharedPreferences settings = getPreferences(Activity.MODE_PRIVATE);
settings.edit().putInt(getPackageName() + ".feedback_launch_count", 0).commit();
}
});
}
private void getLastUrl() {
(new GeckoAsyncTask<Void, Void, String>(mAppContext, GeckoAppShell.getHandler()) {
@Override
public synchronized String doInBackground(Void... params) {
// Get the most recent URL stored in browser history.
String url = "";
Cursor c = BrowserDB.getRecentHistory(getContentResolver(), 1);
if (c.moveToFirst()) {
url = c.getString(c.getColumnIndexOrThrow(Combined.URL));
}
c.close();
return url;
}
@Override
public void onPostExecute(String url) {
// Don't bother sending a message if there is no URL.
if (url.length() > 0)
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Feedback:LastUrl", url));
}
}).execute();
}
}

View File

@ -2145,11 +2145,11 @@ abstract public class GeckoApp
((GeckoApplication) getApplication()).removeApplicationLifecycleCallbacks(this);
}
private void registerEventListener(String event) {
protected void registerEventListener(String event) {
GeckoAppShell.getEventDispatcher().registerEventListener(event, this);
}
private void unregisterEventListener(String event) {
protected void unregisterEventListener(String event) {
GeckoAppShell.getEventDispatcher().unregisterEventListener(event, this);
}

View File

@ -0,0 +1,227 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" [
<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
%brandDTD;
<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
%globalDTD;
<!ENTITY % aboutFeedbackDTD SYSTEM "chrome://browser/locale/aboutFeedback.dtd" >
%aboutFeedbackDTD;
]>
<!-- 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/. -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>&pageTitle;</title>
<meta name="viewport" content="width=device-width; user-scalable=0" />
<link rel="stylesheet" href="chrome://browser/skin/aboutFeedback.css" type="text/css"/>
<link rel="icon" type="image/png" href="chrome://branding/content/favicon32.png" />
</head>
<body dir="&locale.dir;" onload="init();" onunload="uninit();">
<section id="intro" active="true">
<h1 class="header">&intro.header;</h1>
<div class="message">&intro.message;</div>
<div class="link-box" onclick="switchSection('happy');">
<a>&intro.happyLink;</a>
</div>
<div class="link-box" onclick="switchSection('sad');">
<a>&intro.sadLink;</a>
</div>
<div class="link-box-bottom" onclick="switchSection('idea');">
<a>&intro.ideaLink;</a>
</div>
<div id="sumo-message" class="fine-print">&support.pre;<a id="sumo-link">&support.link;</a>&support.post;</div>
</section>
<section id="happy">
<h1 class="header">&happy.header;</h1>
<div class="message-box">
<div class="message">&happy.message;</div>
<div class="fine-print">&happy.finePrint;</div>
</div>
<div class="link-box-bottom" onclick="openPlayStore();">
<div class="stars"/>
<a>&happy.ratingLink;</a>
</div>
<div class="bottom-links">
<a onclick="maybeLater();">&happy.maybeLater;</a>
<a onclick="window.close();">&happy.noThanks;</a>
</div>
</section>
<section id="sad">
<form onsubmit="sendFeedback(event);">
<div class="message">&sad.message;</div>
<textarea class="description" placeholder="&sad.placeholder;" rows="8" required="true"/>
<div class="message">&sad.lastSite;</div>
<input id="last-url" type="url" placeholder="&sad.urlPlaceholder;"/>
<div class="fine-print">&feedback.privacy;</div>
<input class="send-feedback" type="submit" value="&feedback.send;"/>
</form>
</section>
<section id="idea">
<form onsubmit="sendFeedback(event);">
<div class="message">&idea.message;</div>
<textarea class="description" placeholder="&idea.placeholder;" rows="8" required="true"/>
<div class="fine-print">&feedback.privacy;</div>
<input class="send-feedback" type="submit" value="&feedback.send;"/>
</form>
</section>
<section id="thanks-sad">
<h1 class="header">&sad.thanksHeader;</h1>
<div class="message-box-bottom">
<div class="message">&sad.thanksMessageTop;</div>
<div class="message">&sad.thanksMessageBottom;</div>
</div>
</section>
<section id="thanks-idea">
<h1 class="header">&idea.thanksHeader;</h1>
<div class="message-box-bottom">
<div class="message">&idea.thanksMessageTop;</div>
<div class="message">&idea.thanksMessageBottom;</div>
</div>
</section>
<script type="application/javascript;version=1.8"><![CDATA[
let Cc = Components.classes;
let Ci = Components.interfaces;
let Cu = Components.utils;
Cu.import("resource://gre/modules/Services.jsm");
function dump(a) {
Services.console.logStringMessage(a);
}
function sendMessageToJava(aMessage) {
Cc["@mozilla.org/android/bridge;1"].getService(Ci.nsIAndroidBridge).
handleGeckoMessage(JSON.stringify(aMessage));
}
function init() {
let sumoLink = Services.urlFormatter.formatURLPref("app.support.baseURL");
document.getElementById("sumo-link").href = sumoLink;
window.addEventListener("popstate", function (aEvent) {
updateActiveSection(aEvent.state ? aEvent.state.section : "intro")
}, false);
// Fill "Last visited site" input with most recent history entry URL.
Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
document.getElementById("last-url").value = aData;
}, "Feedback:LastUrl", false);
sendMessageToJava({
gecko: {
type: "Feedback:LastUrl"
}
});
}
function uninit() {
Services.obs.removeObserver(this, "Feedback:LastUrl");
}
function switchSection(aSection) {
history.pushState({ section: aSection }, aSection);
updateActiveSection(aSection);
}
function updateActiveSection(aSection) {
document.querySelector("section[active]").removeAttribute("active");
document.getElementById(aSection).setAttribute("active", true);
}
function openPlayStore() {
sendMessageToJava({
gecko: {
type: "Feedback:OpenPlayStore"
}
});
window.close();
}
function maybeLater() {
window.close();
sendMessageToJava({
gecko: {
type: "Feedback:MaybeLater"
}
});
}
function sendFeedback(aEvent) {
// Prevent the page from reloading.
aEvent.preventDefault();
let section = history.state.section;
// Sanity check.
if (section != "sad" && section != "idea") {
Cu.reportError("Trying to send feedback from an invalid section: " + section);
return;
}
let sectionElement = document.getElementById(section);
let descriptionElement = sectionElement.querySelector(".description");
// Bail if the description value isn't valid. HTML5 form validation will take care
// of showing an error message for us.
if (!descriptionElement.validity.valid)
return;
let data = new FormData();
data.append("description", descriptionElement.value);
if (section == "sad") {
data.append("_type", 2);
let urlElement = document.getElementById("last-url");
// Bail if the URL value isn't valid. HTML5 form validation will take care
// of showing an error message for us.
if (!urlElement.validity.valid)
return;
// Only send a URL string if the user provided one.
if (urlElement.value) {
data.append("add_url", true);
data.append("url", urlElement.value);
}
} else {
// Otherwise we're in the "idea" section.
data.append("_type", 3);
}
let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
data.append("device", sysInfo.get("device"));
data.append("manufacturer", sysInfo.get("manufacturer"));
let req = new XMLHttpRequest();
req.addEventListener("error", function() {
Cu.reportError("Error sending feedback to input.mozilla.org: " + req.statusText);
}, false);
req.addEventListener("abort", function() {
Cu.reportError("Aborted sending feedback to input.mozilla.org: " + req.statusText);
}, false);
let postURL = Services.urlFormatter.formatURLPref("app.feedback.postURL");
req.open("POST", postURL, true);
req.send(data);
switchSection("thanks-" + section);
}
]]></script>
</body>
</html>

View File

@ -14,6 +14,7 @@ chrome.jar:
content/aboutCertError.xhtml (content/aboutCertError.xhtml)
content/aboutDownloads.xhtml (content/aboutDownloads.xhtml)
content/aboutDownloads.js (content/aboutDownloads.js)
content/aboutFeedback.xhtml (content/aboutFeedback.xhtml)
content/aboutReader.html (content/aboutReader.html)
content/aboutReaderContent.html (content/aboutReaderContent.html)
content/aboutReader.js (content/aboutReader.js)

View File

@ -68,6 +68,10 @@ let modules = {
uri: "chrome://browser/content/aboutReaderContent.html",
privileged: false,
hide: true
},
feedback: {
uri: "chrome://browser/content/aboutFeedback.xhtml",
privileged: true
}
}

View File

@ -11,6 +11,7 @@ contract @mozilla.org/network/protocol/about;1?what=apps {322ba47e-7047-4f71-aeb
contract @mozilla.org/network/protocol/about;1?what=downloads {322ba47e-7047-4f71-aebf-cb7d69325cd9}
contract @mozilla.org/network/protocol/about;1?what=reader {322ba47e-7047-4f71-aebf-cb7d69325cd9}
contract @mozilla.org/network/protocol/about;1?what=readercontent {322ba47e-7047-4f71-aebf-cb7d69325cd9}
contract @mozilla.org/network/protocol/about;1?what=feedback {322ba47e-7047-4f71-aebf-cb7d69325cd9}
#ifdef MOZ_SAFE_BROWSING
contract @mozilla.org/network/protocol/about;1?what=blocked {322ba47e-7047-4f71-aebf-cb7d69325cd9}
#endif

View File

@ -0,0 +1,49 @@
<!-- 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/. -->
<!ENTITY pageTitle "&brandShortName; Feedback">
<!ENTITY intro.header "Have a minute?">
<!ENTITY intro.message "Tell us what you think about &brandShortName; for Android so far.">
<!ENTITY intro.happyLink "I love it">
<!ENTITY intro.sadLink "I ran into some problems">
<!ENTITY intro.ideaLink "I have an idea">
<!-- LOCALIZATION NOTE (support.pre): Include a trailing space as needed. -->
<!-- LOCALIZATION NOTE (support.link): Avoid leading/trailing spaces, this text is a link. -->
<!-- LOCALIZATION NOTE (support.post): Include a starting space as needed. -->
<!ENTITY support.pre "If you need help or have a problem with &brandShortName;, please visit ">
<!ENTITY support.link "&brandShortName; Support">
<!ENTITY support.post ".">
<!ENTITY happy.header "That's great to hear!">
<!ENTITY happy.message "Want to share the love by giving us a 5 star rating on Google Play?">
<!ENTITY happy.finePrint "It takes less than a minute and feels great.">
<!ENTITY happy.ratingLink "Yes, go to Google Play">
<!ENTITY happy.maybeLater "Maybe Later">
<!ENTITY happy.noThanks "No thanks">
<!ENTITY sad.message "Were sorry that you had some problems with &brandShortName;. Please tell us what happened so that we can fix it.">
<!ENTITY sad.placeholder "Enter your feedback here">
<!ENTITY sad.lastSite "Last visited site (optional)">
<!-- LOCALIZATION NOTE (sad.urlPlaceholder): Placeholder text that appears in "Last visited site" input box when there is no text. -->
<!ENTITY sad.urlPlaceholder "http://">
<!ENTITY sad.thanksHeader "Thanks for letting us know.">
<!-- LOCALIZATION NOTE (sad.thanksMessageTop, sad.thanksMessageBottom): These two
strings will appear as separate paragraphs but make up one message. -->
<!ENTITY sad.thanksMessageTop "Were always working to make &brandShortName; better. Rest assured that real people will look at your feedback and do their very best to resolve your issue.">
<!ENTITY sad.thanksMessageBottom "Or else.">
<!ENTITY idea.message "We love hearing your ideas! Please share your thoughts below. (Just the ones about &brandShortName;, please.)">
<!ENTITY idea.placeholder "Enter your idea here">
<!ENTITY idea.thanksHeader "Thanks!">
<!-- LOCALIZATION NOTE (idea.thanksMessageTop, idea.thanksMessageBottom): These two
strings will appear as separate paragraphs but make up one message. -->
<!ENTITY idea.thanksMessageTop "We appreciate you taking the time to share your thoughts. Were always working to make &brandShortName; better and contributions like yours can lead to great things.">
<!ENTITY idea.thanksMessageBottom "You can't see it, but we're giving you a high five right now.">
<!ENTITY feedback.privacy "For your privacy, please don't include any personally indentifiable information in your feedback.">
<!ENTITY feedback.send "Send Feedback">

View File

@ -14,6 +14,7 @@
locale/@AB_CD@/browser/aboutCertError.dtd (%chrome/aboutCertError.dtd)
locale/@AB_CD@/browser/aboutDownloads.dtd (%chrome/aboutDownloads.dtd)
locale/@AB_CD@/browser/aboutDownloads.properties (%chrome/aboutDownloads.properties)
locale/@AB_CD@/browser/aboutFeedback.dtd (%chrome/aboutFeedback.dtd)
locale/@AB_CD@/browser/aboutReader.properties (%chrome/aboutReader.properties)
locale/@AB_CD@/browser/browser.properties (%chrome/browser.properties)
locale/@AB_CD@/browser/config.dtd (%chrome/config.dtd)

View File

@ -0,0 +1,129 @@
/* 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/. */
body {
-moz-text-size-adjust: none;
font-family: Roboto,"Droid Sans",helvetica,arial,clean,sans-serif;
font-size: 14px;
color: #222;
background-image: url("chrome://browser/skin/images/about-bg-lightblue.png");
padding: 40px 10px 10px 10px;
}
a {
color: #004b98;
}
section {
max-width: 500px;
margin-left: auto;
margin-right: auto;
}
section:not([active]) {
display: none;
}
.header {
font-size: 24px;
text-align: center;
margin-top: 20px;
}
.message-box,
.message-box-bottom,
.link-box,
.link-box-bottom {
background-color: #e4e9ee;
padding: 15px;
}
.message-box,
.link-box {
border-bottom: 1px solid #c8cdd4;
}
.message-box-bottom,
.link-box-bottom {
border-bottom: 2px solid #c8cdd4;
margin-bottom: 10px;
}
.link-box,
.link-box-bottom {
text-align: center;
}
.link-box:active,
.link-box-bottom:active {
background-color: #a7b0b8;
}
.message {
margin-bottom: 10px;
}
.fine-print {
font-size: 12px;
color: #666;
}
.stars {
width: 80px;
height: 10px;
margin: -20px auto 10px auto;
background-image: url("chrome://browser/skin/images/5stars.png");
background-size: 64px 10px;
background-position: center;
background-repeat: no-repeat;
background-color: #e4e9ee;
}
.link-box-bottom:active > .stars {
background-color: transparent;
}
.bottom-links {
text-align: center;
position: absolute;
left: 0;
bottom: 40px;
width: 100%;
}
.bottom-links > a {
margin: 0 25px;
text-decoration: underline;
}
#sumo-message {
position: absolute;
bottom: 20px;
color: #444;
-moz-padding-end: 30px;
}
.description,
#last-url {
font-family: Roboto,"Droid Sans",helvetica,arial,clean,sans-serif;
font-size: 14px;
margin-bottom: 10px;
padding: 5px;
width: -moz-calc(100% - 10px);
}
.send-feedback {
margin-top: 10px;
padding: 10px;
font-size: 16px;
width: 100%;
background-image: -moz-linear-gradient(rgb(87,94,102), rgb(71,77,83) 90%, rgb(45,49,53));
border-radius: 4px;
border-width: 0;
color: #fff;
}
.send-feedback:active {
background-image: -moz-linear-gradient(rgb(138,143,148), rgb(127,130,135) 90%, rgb(108,111,114));
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -11,6 +11,7 @@ chrome.jar:
skin/aboutAddons.css (aboutAddons.css)
skin/aboutApps.css (aboutApps.css)
* skin/aboutDownloads.css (aboutDownloads.css)
skin/aboutFeedback.css (aboutFeedback.css)
skin/aboutReader.css (aboutReader.css)
skin/aboutReaderContent.css (aboutReaderContent.css)
* skin/browser.css (browser.css)
@ -24,6 +25,7 @@ chrome.jar:
skin/fonts/opensans-regular.ttf (fonts/opensans-regular.ttf)
skin/fonts/opensans-light.ttf (fonts/opensans-light.ttf)
skin/images/5stars.png (images/5stars.png)
skin/images/addons-32.png (images/addons-32.png)
skin/images/arrowleft-16.png (images/arrowleft-16.png)
skin/images/arrowright-16.png (images/arrowright-16.png)