import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo

This commit is contained in:
Roy Tam 2018-01-19 03:59:58 +08:00
commit dcd9973243
150858 changed files with 23884658 additions and 0 deletions

View file

@ -0,0 +1,237 @@
/* -*- 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.app.Activity;
import android.content.Context;
import android.database.Cursor;
import android.os.Bundle;
import android.support.annotation.CallSuper;
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.BrowserApp;
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.delegates.TabsTrayVisibilityAwareDelegate;
import org.mozilla.gecko.Experiments;
import org.mozilla.gecko.util.ThreadUtils;
import java.lang.ref.WeakReference;
import ch.boye.httpclientandroidlib.util.TextUtils;
/**
* Promote "Add to home screen" if user visits website often.
*/
public class AddToHomeScreenPromotion extends TabsTrayVisibilityAwareDelegate 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 = "lastVisitMinimumAgeMs";
private static final String EXPERIMENT_LAST_VISIT_MAXIMUM_AGE = "lastVisitMaximumAgeMs";
private WeakReference<Activity> activityReference;
private boolean isEnabled;
private int minimumVisits;
private int lastVisitMinimumAgeMs;
private int lastVisitMaximumAgeMs;
@CallSuper
@Override
public void onCreate(BrowserApp browserApp, Bundle savedInstanceState) {
super.onCreate(browserApp, savedInstanceState);
activityReference = new WeakReference<Activity>(browserApp);
initializeExperiment(browserApp);
}
@Override
public void onResume(BrowserApp browserApp) {
Tabs.registerOnTabsChangedListener(this);
}
@Override
public void onPause(BrowserApp browserApp) {
Tabs.unregisterOnTabsChangedListener(this);
}
private void initializeExperiment(Context context) {
if (!SwitchBoard.isInExperiment(context, Experiments.PROMOTE_ADD_TO_HOMESCREEN)) {
Log.v(LOGTAG, "Experiment not enabled");
// Experiment is not enabled. No need to try to read values.
return;
}
JSONObject values = SwitchBoard.getExperimentValuesFromJson(context, 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.
return;
}
try {
initializeWithValues(
values.getInt(EXPERIMENT_MINIMUM_TOTAL_VISITS),
values.getInt(EXPERIMENT_LAST_VISIT_MINIMUM_AGE),
values.getInt(EXPERIMENT_LAST_VISIT_MAXIMUM_AGE));
} catch (JSONException e) {
Log.w(LOGTAG, "Could not read experiment values", e);
}
}
private void initializeWithValues(int minimumVisits, int lastVisitMinimumAgeMs, int lastVisitMaximumAgeMs) {
this.isEnabled = true;
this.minimumVisits = minimumVisits;
this.lastVisitMinimumAgeMs = lastVisitMinimumAgeMs;
this.lastVisitMaximumAgeMs = lastVisitMaximumAgeMs;
}
@Override
public void onTabChanged(final Tab tab, Tabs.TabEvents msg, String 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) {
return;
}
if (tab.isPrivate()) {
// Never show the prompt for private browsing tabs.
return;
}
if (isTabsTrayVisible()) {
// We only want to show this prompt if this tab is in the foreground and not on top
// of the tabs tray.
return;
}
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
maybeShowPromotionForUrl(tab.getURL(), tab.getTitle());
}
});
}
private void maybeShowPromotionForUrl(String url, String title) {
if (!isEnabled) {
return;
}
final Context context = activityReference.get();
if (context == null) {
return;
}
if (!shouldShowPromotion(context, url, title)) {
return;
}
HomeScreenPrompt.show(context, url, title);
}
private boolean shouldShowPromotion(Context context, 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 (!url.startsWith("https://")) {
// Only promote websites that are served over HTTPS.
return false;
}
URLHistory history = getHistoryForURL(context, url);
if (history == null) {
// There's no history for this URL yet or we can't read it right now. Just ignore.
return false;
}
if (history.visits < minimumVisits) {
// This URL has not been visited often enough.
return false;
}
if (history.lastVisit > System.currentTimeMillis() - lastVisitMinimumAgeMs) {
// 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() - lastVisitMaximumAgeMs) {
// The last visit is to old. Do not show promotion.
return false;
}
if (hasAcceptedOrDeclinedHomeScreenShortcut(context, url)) {
// 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;
}
protected boolean hasAcceptedOrDeclinedHomeScreenShortcut(Context context, String url) {
final UrlAnnotations urlAnnotations = BrowserDB.from(context).getUrlAnnotations();
return urlAnnotations.hasAcceptedOrDeclinedHomeScreenShortcut(context.getContentResolver(), url);
}
protected URLHistory getHistoryForURL(Context context, String url) {
final GeckoProfile profile = GeckoProfile.get(context);
final BrowserDB browserDB = BrowserDB.from(profile);
Cursor cursor = null;
try {
cursor = browserDB.getHistoryForURL(context.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;
}
}

View file

@ -0,0 +1,237 @@
/* -*- 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.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.BrowserDB;
import org.mozilla.gecko.db.UrlAnnotations;
import org.mozilla.gecko.icons.IconCallback;
import org.mozilla.gecko.icons.IconResponse;
import org.mozilla.gecko.icons.Icons;
import org.mozilla.gecko.Experiments;
import org.mozilla.gecko.util.ActivityUtils;
import org.mozilla.gecko.util.ThreadUtils;
/**
* Prompt to promote adding the current website to the home screen.
*/
public class HomeScreenPrompt extends Locales.LocaleAwareActivity implements IconCallback {
private static final String EXTRA_TITLE = "title";
private static final String EXTRA_URL = "url";
private static final String TELEMETRY_EXTRA = "home_screen_promotion";
private View containerView;
private ImageView iconView;
private String title;
private String url;
private boolean isAnimating;
private boolean hasAccepted;
private boolean hasDeclined;
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, TELEMETRY_EXTRA);
}
@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);
findViewById(R.id.add).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) {
onDecline();
}
});
}
private void addToHomeScreen() {
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
GeckoAppShell.createShortcut(title, url);
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, TELEMETRY_EXTRA);
ActivityUtils.goToHomeScreen(HomeScreenPrompt.this);
finish();
}
});
}
private void loadShortcutIcon() {
Icons.with(this)
.pageUrl(url)
.skipNetwork()
.skipMemory()
.forLauncherIcon()
.build()
.execute(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();
}
/**
* Remember that the user rejected creating a home screen shortcut for this URL.
*/
private void rememberRejection() {
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
final UrlAnnotations urlAnnotations = BrowserDB.from(HomeScreenPrompt.this).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() {
onDecline();
}
private void onDecline() {
if (hasDeclined || hasAccepted) {
return;
}
rememberRejection();
slideOut();
// Technically not always an action triggered by the "back" button but with the same effect: Finishing this
// activity and going back to the previous one.
Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, TelemetryContract.Method.BACK, TELEMETRY_EXTRA);
hasDeclined = true;
}
/**
* User clicked outside of the prompt.
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
onDecline();
return true;
}
@Override
public void onIconResponse(IconResponse response) {
iconView.setImageBitmap(response.getBitmap());
}
}

View file

@ -0,0 +1,103 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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.content.Intent;
import android.content.SharedPreferences;
import com.keepsafe.switchboard.SwitchBoard;
import org.mozilla.gecko.BrowserApp;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.R;
import org.mozilla.gecko.Tab;
import org.mozilla.gecko.Tabs;
import org.mozilla.gecko.delegates.BrowserAppDelegateWithReference;
import org.mozilla.gecko.reader.ReaderModeUtils;
import org.mozilla.gecko.Experiments;
public class ReaderViewBookmarkPromotion extends BrowserAppDelegateWithReference implements Tabs.OnTabsChangedListener {
private static final String PREF_FIRST_RV_HINT_SHOWN = "first_reader_view_hint_shown";
private static final String FIRST_READERVIEW_OPEN_TELEMETRYEXTRA = "first_readerview_open_prompt";
private boolean hasEnteredReaderMode = false;
@Override
public void onResume(BrowserApp browserApp) {
Tabs.registerOnTabsChangedListener(this);
}
@Override
public void onPause(BrowserApp browserApp) {
Tabs.unregisterOnTabsChangedListener(this);
}
@Override
public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
switch (msg) {
case LOCATION_CHANGE:
// old url: data
// new url: tab.getURL()
final boolean enteringReaderMode = ReaderModeUtils.isEnteringReaderMode(data, tab.getURL());
if (!hasEnteredReaderMode && enteringReaderMode) {
hasEnteredReaderMode = true;
promoteBookmarking();
}
break;
}
}
@Override
public void onActivityResult(BrowserApp browserApp, int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case BrowserApp.ACTIVITY_REQUEST_TRIPLE_READERVIEW:
if (resultCode == BrowserApp.ACTIVITY_RESULT_TRIPLE_READERVIEW_ADD_BOOKMARK) {
final Tab tab = Tabs.getInstance().getSelectedTab();
if (tab != null) {
tab.addBookmark();
}
} else if (resultCode == BrowserApp.ACTIVITY_RESULT_TRIPLE_READERVIEW_IGNORE) {
// Nothing to do: we won't show this promotion again either way.
}
break;
}
}
private void promoteBookmarking() {
final BrowserApp browserApp = getBrowserApp();
if (browserApp == null) {
return;
}
final SharedPreferences prefs = GeckoSharedPrefs.forProfile(browserApp);
final boolean isEnabled = SwitchBoard.isInExperiment(browserApp, Experiments.TRIPLE_READERVIEW_BOOKMARK_PROMPT);
// We reuse the same preference as for the first offline reader view bookmark
// as we only want to show one of the two UIs (they both explain the same
// functionality).
if (!isEnabled || prefs.getBoolean(PREF_FIRST_RV_HINT_SHOWN, false)) {
return;
}
SimpleHelperUI.show(browserApp,
FIRST_READERVIEW_OPEN_TELEMETRYEXTRA,
BrowserApp.ACTIVITY_REQUEST_TRIPLE_READERVIEW,
R.string.helper_triple_readerview_open_title,
R.string.helper_triple_readerview_open_message,
R.drawable.helper_readerview_bookmark, // We share the icon with the usual helper UI
R.string.helper_triple_readerview_open_button,
BrowserApp.ACTIVITY_RESULT_TRIPLE_READERVIEW_ADD_BOOKMARK,
BrowserApp.ACTIVITY_RESULT_TRIPLE_READERVIEW_IGNORE);
prefs
.edit()
.putBoolean(PREF_FIRST_RV_HINT_SHOWN, true)
.apply();
}
}

View file

@ -0,0 +1,194 @@
/* -*- 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.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.DrawableRes;
import android.support.annotation.StringRes;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import org.mozilla.gecko.Locales;
import org.mozilla.gecko.R;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
/**
* Generic HelperUI (prompt) that can be populated with an image, title, message and action button.
* See show() for usage. This is run as an Activity, results must be handled in the parent Activities
* onActivityResult().
*/
public class SimpleHelperUI extends Locales.LocaleAwareActivity {
public static final String PREF_FIRST_RVBP_SHOWN = "first_reader_view_bookmark_prompt_shown";
public static final String FIRST_RVBP_SHOWN_TELEMETRYEXTRA = "first_readerview_bookmark_prompt";
private View containerView;
private boolean isAnimating;
private String mTelemetryExtra;
private static final String EXTRA_TELEMETRYEXTRA = "telemetryextra";
private static final String EXTRA_TITLE = "title";
private static final String EXTRA_MESSAGE = "message";
private static final String EXTRA_IMAGE = "image";
private static final String EXTRA_BUTTON = "button";
private static final String EXTRA_RESULTCODE_POSITIVE = "positive";
private static final String EXTRA_RESULTCODE_NEGATIVE = "negative";
/**
* Show a generic helper UI/prompt.
*
* @param owner The owning Activity, the result of this prompt will be delivered to its
* onActivityResult().
* @param requestCode The request code for the Activity that will be created, this is passed to
* onActivityResult() to identify the prompt.
*
* @param positiveResultCode The result code passed to onActivityResult() when the button has
* been pressed.
* @param negativeResultCode The result code passed to onActivityResult() when the prompt was
* dismissed, either by pressing outside the prompt or by pressing the
* device back button.
*/
public static void show(Activity owner, String telemetryExtra,
int requestCode,
@StringRes int title, @StringRes int message,
@DrawableRes int image, @StringRes int buttonText,
int positiveResultCode, int negativeResultCode) {
Intent intent = new Intent(owner, SimpleHelperUI.class);
intent.putExtra(EXTRA_TELEMETRYEXTRA, telemetryExtra);
intent.putExtra(EXTRA_TITLE, title);
intent.putExtra(EXTRA_MESSAGE, message);
intent.putExtra(EXTRA_IMAGE, image);
intent.putExtra(EXTRA_BUTTON, buttonText);
intent.putExtra(EXTRA_RESULTCODE_POSITIVE, positiveResultCode);
intent.putExtra(EXTRA_RESULTCODE_NEGATIVE, negativeResultCode);
owner.startActivityForResult(intent, requestCode);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mTelemetryExtra = getIntent().getStringExtra(EXTRA_TELEMETRYEXTRA);
setupViews();
slideIn();
}
private void setupViews() {
final Intent i = getIntent();
setContentView(R.layout.simple_helper_ui);
containerView = findViewById(R.id.container);
((ImageView) findViewById(R.id.image)).setImageResource(i.getIntExtra(EXTRA_IMAGE, 0));
((TextView) findViewById(R.id.title)).setText(i.getIntExtra(EXTRA_TITLE, 0));
((TextView) findViewById(R.id.message)).setText(i.getIntExtra(EXTRA_MESSAGE, 0));
final Button button = ((Button) findViewById(R.id.button));
button.setText(i.getIntExtra(EXTRA_BUTTON, 0));
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
slideOut();
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, mTelemetryExtra);
setResult(i.getIntExtra(EXTRA_RESULTCODE_POSITIVE, -1));
}
});
}
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 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() {
slideOut();
Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, TelemetryContract.Method.BACK, mTelemetryExtra);
setResult(getIntent().getIntExtra(EXTRA_RESULTCODE_NEGATIVE, -1));
}
/**
* User clicked outside of the prompt.
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
slideOut();
// Not really an action triggered by the "back" button but with the same effect: Finishing this
// activity and going back to the previous one.
Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, TelemetryContract.Method.BACK, mTelemetryExtra);
setResult(getIntent().getIntExtra(EXTRA_RESULTCODE_NEGATIVE, -1));
return true;
}
}