package org.onebusaway.android.util; import com.github.amlcurran.showcaseview.OnShowcaseEventListener; import com.github.amlcurran.showcaseview.ShowcaseView; import com.github.amlcurran.showcaseview.targets.Target; import com.github.amlcurran.showcaseview.targets.ViewTarget; import org.onebusaway.android.BuildConfig; import org.onebusaway.android.R; import org.onebusaway.android.app.Application; import org.onebusaway.android.io.request.ObaArrivalInfoResponse; import android.content.Context; import android.content.DialogInterface; import android.content.SharedPreferences; import android.content.res.Resources; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.os.Build; import android.support.annotation.DrawableRes; import android.support.v4.content.res.ResourcesCompat; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.text.Spannable; import android.text.SpannableString; import android.text.style.ImageSpan; import android.view.MotionEvent; import android.view.ViewGroup; import android.widget.RelativeLayout; /** * A class containing utility methods related to showing a tutorial to users for how to use various * OBA features, using the ShowcaseView library (https://github.com/amlcurran/ShowcaseView). */ public class ShowcaseViewUtils { public static final String TUTORIAL_WELCOME = ".tutorial_welcome"; public static final String TUTORIAL_OPT_OUT_DIALOG = ".tutorial_opt_out_dialog"; public static final String TUTORIAL_ARRIVAL_HEADER_ARRIVAL_INFO = ".tutorial_arrival_header_arrival_info"; public static final String TUTORIAL_ARRIVAL_HEADER_SLIDING_PANEL = ".tutorial_arrival_header_sliding_panel"; public static final String TUTORIAL_ARRIVAL_SORT = ".tutorial_arrival_sort"; public static final String TUTORIAL_ARRIVAL_HEADER_STAR_ROUTE = ".tutorial_arrival_header_star_route"; public static final String TUTORIAL_RECENT_STOPS_ROUTES = ".tutorial_recent_stops_routes"; public static final String TUTORIAL_STARRED_STOPS_SORT = ".tutorial_starred_stops_sort"; public static final String TUTORIAL_STARRED_STOPS_SHORTCUT = ".tutorial_starred stops_shortcut"; public static final String TUTORIAL_SEND_FEEDBACK_OPEN311_CATEGORIES = ".tutorial_send_feedback_open311_categories"; private static ShowcaseView mShowcaseView; /** * Returns true if this API level supports the ShowcaseView library tutorials, false if it does * not * * @return true if this API level supports the ShowcaseView library tutorials, false if it does * not */ public static boolean supportsShowcaseView() { // ShowcaseView only works on API Level >= 11 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB; } /** * Shows the tutorial for the specified tutorialType. This method handles checking to see if * other ShowcaseViews are already being shown, as well as if this tutorial has already been * shown - if either of these cases are true, this method is a no-op. * * @param tutorialType type of tutorial to show, defined by the TUTORIAL_* constants in * ShowcaseViewUtils * @param activity activity used to show the tutorial * @param response The response that contains arrival info, or null if this is not available. * Some tutorials require that arrival info is showing - these tutorials * will only be displayed if arrival info is provided in this parameter. */ public synchronized static void showTutorial(String tutorialType, final AppCompatActivity activity, final ObaArrivalInfoResponse response) { if (!supportsShowcaseView() || activity == null) { return; } if (isShowcaseViewShowing() && !tutorialType.equals(TUTORIAL_ARRIVAL_HEADER_SLIDING_PANEL)) { // Only tutorials that are chained (fired from listeners when another ShowcaseView // closes) should pass this point - otherwise, return return; } SharedPreferences settings = Application.getPrefs(); // If user has opted out of tutorials, do nothing boolean showTutorials = settings.getBoolean( activity.getString(R.string.preference_key_show_tutorial_screens), true); if (!showTutorials) { return; } // If we've already shown this tutorial to the user, do nothing boolean showedThisTutorial = settings.getBoolean(tutorialType, false); if (showedThisTutorial) { return; } // Make sure we're not spamming the user with tutorials if (giveUserTutorialBreak(activity, tutorialType)) { return; } Resources r = activity.getResources(); OnShowcaseEventListener listener = null; boolean moveButtonLeft = false; String title; SpannableString text; Target target = Target.NONE; switch (tutorialType) { case TUTORIAL_WELCOME: title = r.getString(R.string.tutorial_welcome_title); text = new SpannableString(r.getString(R.string.tutorial_welcome_text)); break; case TUTORIAL_ARRIVAL_HEADER_ARRIVAL_INFO: if (response == null) { throw new IllegalArgumentException( "ObaArrivalInfoResponse must be provided for the '" + tutorialType + "' tutorial type."); } if (response.getArrivalInfo().length < 1) { // We need at least one arrival time return; } title = r.getString(R.string.tutorial_arrival_header_arrival_info_title); text = new SpannableString( r.getString(R.string.tutorial_arrival_header_arrival_info_text)); target = new ViewTarget(R.id.eta_and_min, activity); moveButtonLeft = true; listener = new OnShowcaseEventListener() { @Override public void onShowcaseViewHide(ShowcaseView showcaseView) { showTutorial(TUTORIAL_ARRIVAL_HEADER_SLIDING_PANEL, activity, response); } @Override public void onShowcaseViewDidHide(ShowcaseView showcaseView) { } @Override public void onShowcaseViewShow(ShowcaseView showcaseView) { } @Override public void onShowcaseViewTouchBlocked(MotionEvent motionEvent) { } }; break; case TUTORIAL_ARRIVAL_HEADER_SLIDING_PANEL: title = r.getString(R.string.tutorial_arrival_header_sliding_panel_title); text = new SpannableString( r.getString(R.string.tutorial_arrival_header_sliding_panel_text)); target = new ViewTarget(R.id.expand_collapse, activity); moveButtonLeft = true; break; case TUTORIAL_ARRIVAL_SORT: title = r.getString(R.string.tutorial_arrival_sort_title); text = new SpannableString(r.getString(R.string.tutorial_arrival_sort_text)); addIcon(activity, text, R.drawable.ic_action_content_sort); break; case TUTORIAL_ARRIVAL_HEADER_STAR_ROUTE: if (response == null) { throw new IllegalArgumentException( "ObaArrivalInfoResponse must be provided for the '" + tutorialType + "' tutorial type."); } if (response.getArrivalInfo().length < 1) { // We need at least one arrival time return; } title = r.getString(R.string.tutorial_arrival_header_star_route_title); text = new SpannableString( r.getString(R.string.tutorial_arrival_header_star_route_text)); target = new ViewTarget(R.id.eta_route_favorite, activity); break; case TUTORIAL_RECENT_STOPS_ROUTES: title = r.getString(R.string.tutorial_recent_stops_routes_title); text = new SpannableString( r.getString(R.string.tutorial_recent_stops_routes_text)); addIcon(activity, text, R.drawable.ic_navigation_more_vert); break; case TUTORIAL_STARRED_STOPS_SORT: title = r.getString(R.string.tutorial_starred_stops_sort_title); text = new SpannableString(r.getString(R.string.tutorial_starred_stops_sort_text)); addIcon(activity, text, R.drawable.ic_action_content_sort); break; case TUTORIAL_STARRED_STOPS_SHORTCUT: if (BuildConfig.FLAVOR_platform .equalsIgnoreCase(BuildFlavorUtils.AMAZON_FLAVOR_PLATFORM)) { // Amazon doesn't support shortcuts - see #419 return; } title = r.getString(R.string.tutorial_starred_stops_shortcut_title); text = new SpannableString( r.getString(R.string.tutorial_starred_stops_shortcut_text)); break; case TUTORIAL_SEND_FEEDBACK_OPEN311_CATEGORIES: title = r.getString(R.string.tutorial_send_feedback_transit_service_title); text = new SpannableString( r.getString(R.string.tutorial_send_feedback_transit_service_text)); target = new ViewTarget(R.id.ri_spinnerServices, activity); break; default: throw new IllegalArgumentException( "tutorialType must be one of the TUTORIAL_* constants in ShowcaseViewUtils"); } mShowcaseView = new ShowcaseView.Builder(activity) .setTarget(target) .setStyle(R.style.CustomShowcaseTheme) .setContentTitle(title) .setContentText(text) .build(); // If button should be positioned to the left, then set the parameters if (moveButtonLeft) { moveButtonLeft(activity, mShowcaseView); } if (listener != null) { mShowcaseView.setOnShowcaseEventListener(listener); } // Set the preference for this tutorial type so it doesn't show again doNotShowTutorial(tutorialType); } /** * Returns true if the ShowcaseView is currently showing, false if it is not * * @return true if the ShowcaseView is currently showing, false if it is not */ public static boolean isShowcaseViewShowing() { return mShowcaseView != null && mShowcaseView.isShowing(); } /** * Hides a currently showing ShowcaseView */ public static void hideShowcaseView() { if (mShowcaseView != null) { mShowcaseView.hide(); } } /** * Give the user a break from tutorials - only show every 10th time, unless its the beginning * three important screens * * @param tutorialType type of tutorial to show, defined by the TUTORIAL_* constants in * ShowcaseViewUtils * @return true if we should give the user a break and not show a tutorial, false if it's ok * to show them one */ private static boolean giveUserTutorialBreak(Context context, String tutorialType) { final String TUTORIAL_COUNTER = context.getString(R.string.preference_key_tutorial_counter); if (!(tutorialType.equals(TUTORIAL_WELCOME) || tutorialType.equals(TUTORIAL_ARRIVAL_HEADER_ARRIVAL_INFO) || tutorialType.equals(TUTORIAL_ARRIVAL_HEADER_SLIDING_PANEL))) { int counter = Application.getPrefs().getInt(TUTORIAL_COUNTER, 0); counter++; PreferenceUtils.saveInt(TUTORIAL_COUNTER, counter); if (!(counter % 10 == 0)) { // Wait longer to show the next tutorial return true; } } return false; } /** * Creates a dialog that prompts the user if they want to see tutorial popups * * @return a dialog that prompts the user if they want to see tutorial popups */ public static void showOptOutDialog(final AppCompatActivity activity) { if (!UIUtils.canManageDialog(activity)) { return; } final String showTutorialsKey = activity .getString(R.string.preference_key_show_tutorial_screens); AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setTitle(R.string.tutorial_opt_out_dialog_title) .setMessage(R.string.tutorial_opt_out_dialog_text) .setPositiveButton(R.string.rt_yes, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // Make sure tutorials are enabled - they will show on their own PreferenceUtils.saveBoolean(showTutorialsKey, true); // Show the welcome tutorial showTutorial(ShowcaseViewUtils.TUTORIAL_WELCOME, activity, null); } }) .setNegativeButton(R.string.rt_no, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // Turn off all tutorials PreferenceUtils.saveBoolean(showTutorialsKey, false); } }); builder.create().show(); // Don't show this dialog again PreferenceUtils.saveBoolean(TUTORIAL_OPT_OUT_DIALOG, false); } /** * Adds the provided icon to the right side of the provided SpannableString * * @param text SpannableString to add sort icon to * @param drawableResource ID of the drawable resource to add to the right side of the * SpannableString */ private static void addIcon(Context context, SpannableString text, @DrawableRes int drawableResource) { Drawable d = ResourcesCompat.getDrawable(context.getResources(), drawableResource, context.getTheme()); d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight()); d.setColorFilter(context.getResources().getColor(R.color.header_text_color), PorterDuff.Mode.SRC_IN); ImageSpan imageSpan = new ImageSpan(d, ImageSpan.ALIGN_BOTTOM); text.setSpan(imageSpan, text.length() - 1, text.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); } /** * Moves the button to acknowledge the ShowcaseView to be left aligned * * @param v ShowcaseView for which the button should be left aligned */ private static void moveButtonLeft(Context context, ShowcaseView v) { RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); lp.addRule(RelativeLayout.ALIGN_PARENT_LEFT); lp.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM); int p = UIUtils.dpToPixels(context, 12); lp.setMargins(p, p, p, p); v.setButtonPosition(lp); } /** * Sets a particular tutorial to the "viewed" state, so a user won't ever see it again. * * This is also useful in the case where a user has already found a particular feature but we * haven't yet shown them the tutorial for that feature. In this case, they don't need to see * the tutorial for that feature, so we can avoid annoying them with another popup for something * they already know. * * @param tutorialType type of tutorial to not show, defined by the TUTORIAL_* constants in * ShowcaseViewUtils */ public static void doNotShowTutorial(String tutorialType) { PreferenceUtils.saveBoolean(tutorialType, true); } /** * Resets all tutorials so they are shown to the user again. If any ShowcaseView is showing, * it will be hidden. */ public static void resetAllTutorials(Context context) { if (mShowcaseView != null) { mShowcaseView.hide(); } PreferenceUtils .saveBoolean(context.getString(R.string.preference_key_show_tutorial_screens), true); PreferenceUtils.saveBoolean(TUTORIAL_WELCOME, false); PreferenceUtils.saveBoolean(TUTORIAL_ARRIVAL_HEADER_ARRIVAL_INFO, false); PreferenceUtils.saveBoolean(TUTORIAL_ARRIVAL_HEADER_SLIDING_PANEL, false); PreferenceUtils.saveBoolean(TUTORIAL_ARRIVAL_SORT, false); PreferenceUtils.saveBoolean(TUTORIAL_ARRIVAL_HEADER_STAR_ROUTE, false); PreferenceUtils.saveBoolean(TUTORIAL_RECENT_STOPS_ROUTES, false); PreferenceUtils.saveBoolean(TUTORIAL_STARRED_STOPS_SORT, false); PreferenceUtils.saveBoolean(TUTORIAL_STARRED_STOPS_SHORTCUT, false); PreferenceUtils.saveBoolean(TUTORIAL_SEND_FEEDBACK_OPEN311_CATEGORIES, false); } }