summaryrefslogtreecommitdiffstats
path: root/mobile/android/base/java/org/mozilla/gecko/promotion
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/base/java/org/mozilla/gecko/promotion')
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/promotion/AddToHomeScreenPromotion.java237
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/promotion/HomeScreenPrompt.java237
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/promotion/ReaderViewBookmarkPromotion.java103
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/promotion/SimpleHelperUI.java194
4 files changed, 771 insertions, 0 deletions
diff --git a/mobile/android/base/java/org/mozilla/gecko/promotion/AddToHomeScreenPromotion.java b/mobile/android/base/java/org/mozilla/gecko/promotion/AddToHomeScreenPromotion.java
new file mode 100644
index 000000000..c1eeb6bd5
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/promotion/AddToHomeScreenPromotion.java
@@ -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;
+ }
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/promotion/HomeScreenPrompt.java b/mobile/android/base/java/org/mozilla/gecko/promotion/HomeScreenPrompt.java
new file mode 100644
index 000000000..0f2df8a2c
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/promotion/HomeScreenPrompt.java
@@ -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());
+ }
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/promotion/ReaderViewBookmarkPromotion.java b/mobile/android/base/java/org/mozilla/gecko/promotion/ReaderViewBookmarkPromotion.java
new file mode 100644
index 000000000..db5a531c6
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/promotion/ReaderViewBookmarkPromotion.java
@@ -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();
+ }
+
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/promotion/SimpleHelperUI.java b/mobile/android/base/java/org/mozilla/gecko/promotion/SimpleHelperUI.java
new file mode 100644
index 000000000..b6b857fb9
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/promotion/SimpleHelperUI.java
@@ -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;
+ }
+}