package fr.nicolaspomepuy.discreetapprate; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.util.Log; import android.view.Display; import android.view.Gravity; import android.view.Surface; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; import java.util.Date; /** * Created by nicolas on 06/03/14. */ public class AppRate { protected static final String PREFS_NAME = "app_rate_prefs"; private static final String KEY_ELAPSED_TIME = "elapsed_time"; private static final String KEY_COUNT = "count"; private static final String KEY_CLICKED = "clicked"; protected static final String KEY_LAST_CRASH = "last_crash"; private static final String KEY_MONITOR_START = "monitor_start"; private static final String KEY_MONITOR_TOTAL = "monitor_total"; private static final String KEY_LAST_COUNT_UPDATE = "last_count_update"; private Activity activity; private String text; private int initialLaunchCount = 5; private RetryPolicy policy = RetryPolicy.EXPONENTIAL; private OnShowListener onShowListener; private SharedPreferences settings; private SharedPreferences.Editor editor; private int delay = 0; private int autoHide = 0; private boolean allowPlayLink = true; private long installedSince; private boolean debug; private AppRateTheme theme = AppRateTheme.DARK; private long pauseAfterCrash; private boolean fromTop = false; private long minimumMonitoringTime; private long minimumInterval; private static final Typeface kitkatFont = Typeface.create("sans-serif-condensed", Typeface.NORMAL); private AppRate(Activity activity) { this.activity = activity; } public static AppRate with(Activity activity) { if (activity == null) { throw new IllegalStateException("Activity cannot be null"); } AppRate instance = new AppRate(activity); instance.text = activity.getString(R.string.dra_rate_app); instance.settings = activity.getSharedPreferences(PREFS_NAME, 0); instance.editor = instance.settings.edit(); return instance; } /* * * ******************** SETTINGS ******************** * */ /** * Enable debug mode which will send state when actions are triggered * * @param debug has to be debuggable * @return the {@link AppRate} instance */ public AppRate debug(boolean debug) { this.debug = debug; return this; } /** * Text to be displayed in the view * * @param text text to be displayed * @return the {@link AppRate} instance */ public AppRate text(String text) { this.text = text; return this; } /** * Text to be displayed in the view * * @param textRes text ressource to be displayed * @return the {@link AppRate} instance */ public AppRate text(int textRes) { this.text = activity.getString(textRes); return this; } /** * Initial times {@link AppRate} has to be called before the view is shown * * @param initialLaunchCount times count * @return the {@link AppRate} instance */ public AppRate initialLaunchCount(int initialLaunchCount) { this.initialLaunchCount = initialLaunchCount; return this; } /** * Policy to use to show the {@link AppRate} again * * @param policy the {@link RetryPolicy} to be used * @return the {@link AppRate} instance */ public AppRate retryPolicy(RetryPolicy policy) { this.policy = policy; return this; } /** * Listener used to get {@link AppRate} lifecycle * * @param onShowListener the listener * @return the {@link AppRate} instance */ public AppRate listener(OnShowListener onShowListener) { this.onShowListener = onShowListener; return this; } /** * Add a constraint to show the view only if the app is installed for more than * /!\ This is only available for 2.2+ devices * * @param installedSince the time in seconds * @return the {@link AppRate} instance */ public AppRate atLeastInstalledSince(long installedSince) { this.installedSince = installedSince; return this; } /** * Delay the {@link AppRate} showing time * * @param delay the delay in ms * @return the {@link AppRate} instance */ public AppRate delay(int delay) { this.delay = delay; return this; } /** * Auto hide the {@link AppRate} after delay * * @param autoHide the auto-delay in ms * @return the {@link AppRate} instance */ public AppRate autoHide(int autoHide) { this.autoHide = autoHide; return this; } public AppRate allowPlayLink(boolean allowPlayLink) { this.allowPlayLink = allowPlayLink; return this; } /** * Set the theme (LIGHT or DARK) * * @param theme the {@link fr.nicolaspomepuy.discreetapprate.AppRateTheme} to be used * @return the {@link AppRate} instance */ public AppRate theme(AppRateTheme theme) { this.theme = theme; return this; } /** * Pause duration after a crash (in sec.) * /!\ Calling {@link #initExceptionHandler(android.content.Context)} is mandatory to make it work. * You should do it in your {@link android.app.Application} class * * @param pauseAfterCrash the time to pause * @return the {@link AppRate} instance */ public AppRate pauseTimeAfterCrash(long pauseAfterCrash) { this.pauseAfterCrash = pauseAfterCrash; return this; } /** * Set the minimum monitoring time needed before showing the view * * @param minimumMonitoringTime the minimum time in seconds * @return the {@link AppRate} instance */ public AppRate minimumMonitoringTime(long minimumMonitoringTime) { this.minimumMonitoringTime = minimumMonitoringTime; return this; } /** * Set the minimum interval to increment the count * * @param minimumInterval the minimum interval in seconds * @return the {@link AppRate} instance */ public AppRate minimumInterval(long minimumInterval) { this.minimumInterval = minimumInterval; return this; } /* * * ******************** ACTIONS ******************** * */ /** * Check and show if showing the view is needed */ public void checkAndShow() { // if (!Utils.isGooglePlayInstalled(activity)) { // if (debug) LogD("Play Store is not installed. Won't do anything"); // return; // } if (debug) LogD("Last crash: " + ((System.currentTimeMillis() - settings.getLong(KEY_LAST_CRASH, 0L)) / 1000) + " seconds ago"); if ((System.currentTimeMillis() - settings.getLong(KEY_LAST_CRASH, 0L)) / 1000 < pauseAfterCrash) { if (debug) LogD("A recent crash avoids anything to be done."); return; } if (settings.getLong(KEY_MONITOR_TOTAL, 0L) / 1000 < minimumMonitoringTime) { if (debug) LogD("Monitor time not reached. Nothing will be done"); return; } if (!Utils.isOnline(activity)) { if (debug) LogD("Device is not online. AppRate try to show up next time."); return; } if (!incrementViews()) { return; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) { Date installDate = Utils.installTimeFromPackageManager(activity.getPackageManager(), activity.getPackageName()); Date now = new Date(); if (now.getTime() - installDate.getTime() < installedSince * 1000) { if (debug) LogD("Date not reached. Time elapsed since installation (in sec.): " + ((now.getTime() - installDate.getTime()) / 1000)); return; } } if (!settings.getBoolean(KEY_ELAPSED_TIME, false)) { // It's the first time the time is elapsed editor.putBoolean(KEY_ELAPSED_TIME, true); if (debug) LogD("First time after the time is elapsed"); if (settings.getInt(KEY_COUNT, 5) > initialLaunchCount) { if (debug) LogD("Initial count passed. Resetting to initialLaunchCount"); // Initial count passed. Resetting to initialLaunchCount editor.putInt(KEY_COUNT, initialLaunchCount); } editor.commit(); } boolean clicked = settings.getBoolean(KEY_CLICKED, false); if (clicked) return; int count = settings.getInt(KEY_COUNT, 0); if (count == initialLaunchCount) { if (debug) LogD("initialLaunchCount reached"); showAppRate(); } else if (policy == RetryPolicy.INCREMENTAL && count % initialLaunchCount == 0) { if (debug) LogD("initialLaunchCount incremental reached"); showAppRate(); } else if (policy == RetryPolicy.EXPONENTIAL && count % initialLaunchCount == 0 && Utils.isPowerOfTwo(count / initialLaunchCount)) { if (debug) LogD("initialLaunchCount exponential reached"); showAppRate(); } else { if (debug) LogD("Nothing to show. initialLaunchCount: " + initialLaunchCount + " - Current count: " + count); } } /** * Reset the count to start over */ public void reset() { if (debug) LogD("Count reset"); editor.putInt(KEY_COUNT, 0); editor.putBoolean(KEY_CLICKED, false); editor.putLong(KEY_LAST_CRASH, 0L); editor.commit(); } /** * Will force the {@link AppRate} to show */ public void forceShow() { if (!Utils.isGooglePlayInstalled(activity)) { if (debug) LogD("Play Store is not installed. Won't do anything"); } if (debug) LogD("Force Show"); showAppRate(); } /** * Avoid showing the view again. Can only be undone by {@link #reset()}. */ public void neverShowAgain() { editor.putBoolean(KEY_CLICKED, true); editor.commit(); } /** * Start monitoring */ public void startMonitoring() { if (debug) LogD("Start monitoring"); long start = settings.getLong(KEY_MONITOR_START, 0); if (start != 0) { if (debug) LogD("Monitor error. Start monitoring called before end. Adding the result"); endMonitoring(); } editor.putLong(KEY_MONITOR_START, System.currentTimeMillis()); editor.commit(); } /** * End montoring */ public void endMonitoring() { if (debug) LogD("End monitoring"); editor.commit(); long start = settings.getLong(KEY_MONITOR_START, 0); if (start == 0) { if (debug) LogD("Monitor error. End monitoring called before start."); return; } editor.putLong(KEY_MONITOR_TOTAL, settings.getLong(KEY_MONITOR_TOTAL, 0) + (System.currentTimeMillis() - start)); editor.putLong(KEY_MONITOR_START, 0); editor.commit(); } /** * Initialize the {@link ExceptionHandler}. */ public static void initExceptionHandler(Context context) { Log.d("AppRate", "Init AppRate ExceptionHandler"); Thread.UncaughtExceptionHandler currentHandler = Thread.getDefaultUncaughtExceptionHandler(); // Don't register again if already registered. if (!(currentHandler instanceof ExceptionHandler)) { // Register default exceptions handler. Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler(currentHandler, context)); } } /* * * ******************** PRIVATE ******************** * */ private boolean incrementViews() { if (System.currentTimeMillis() - settings.getLong(KEY_LAST_COUNT_UPDATE, 0L) < minimumInterval*1000) { if (debug) LogD("Count not incremented due to minimum interval not reached"); return false; } editor.putInt(KEY_COUNT, settings.getInt(KEY_COUNT, 0) + 1); editor.putLong(KEY_LAST_COUNT_UPDATE, System.currentTimeMillis()); editor.commit(); return true; } public AppRate fromTop(boolean fromTop) { this.fromTop = fromTop; return this; } @SuppressLint("NewApi") private void showAppRate() { final ViewGroup mainView = (ViewGroup) activity.getLayoutInflater().inflate(R.layout.app_rate, null); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) return; } ImageView close = (ImageView) mainView.findViewById(R.id.close); TextView textView = (TextView) mainView.findViewById(R.id.text); RelativeLayout container = (RelativeLayout) mainView.findViewById(R.id.container); if (fromTop) { FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) container.getLayoutParams(); lp.gravity = Gravity.TOP; container.setLayoutParams(lp); } else { FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) container.getLayoutParams(); lp.gravity = Gravity.BOTTOM; container.setLayoutParams(lp); } textView.setTypeface(kitkatFont); textView.setText(text); close.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { hideAllViews(mainView); if (onShowListener != null) onShowListener.onRateAppDismissed(); } }); if (allowPlayLink) { textView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + activity.getPackageName()))); hideAllViews(mainView); editor.putBoolean(KEY_CLICKED, true); editor.commit(); if (onShowListener != null) onShowListener.onRateAppClicked(); } }); } if (theme == AppRateTheme.LIGHT) { PorterDuff.Mode mMode = PorterDuff.Mode.SRC_ATOP; Drawable d = activity.getResources().getDrawable(R.drawable.ic_action_remove); d.setColorFilter(Color.BLACK, mMode); close.setImageDrawable(d); textView.setTextColor(Color.BLACK); container.setBackgroundColor(0X88ffffff); } else { Drawable d = activity.getResources().getDrawable(R.drawable.ic_action_remove); d.clearColorFilter(); close.setImageDrawable(d); container.setBackgroundColor(0Xaa000000); } // Manage translucent themes if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { Window win = activity.getWindow(); WindowManager.LayoutParams winParams = win.getAttributes(); if (fromTop) { boolean isTranslucent = Utils.hasFlag(winParams.flags, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); if (isTranslucent) { if (debug) LogD("Activity is translucent"); FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) container.getLayoutParams(); int actionBarHeight = 0; //ActionBar size if (activity.getActionBar() != null) { actionBarHeight = activity.getActionBar().getHeight(); } lp.topMargin = Utils.getStatusBarHeight(activity)+actionBarHeight; container.setLayoutParams(lp); } } else { boolean isTranslucent = Utils.hasFlag(winParams.flags, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); if (isTranslucent) { if (debug) LogD("Activity is translucent"); Display display = ((WindowManager) activity.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); int orientation = display.getRotation(); FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) container.getLayoutParams(); switch (orientation) { case Surface.ROTATION_0: case Surface.ROTATION_180: lp.bottomMargin = Utils.getSoftbuttonsbarHeight(activity); container.setLayoutParams(lp); break; case Surface.ROTATION_90: case Surface.ROTATION_270: lp.rightMargin = Utils.getSoftbuttonsbarWidth(activity); container.setLayoutParams(lp); break; } } } } if (autoHide > 0) { activity.getWindow().getDecorView().postDelayed(new Runnable() { @Override public void run() { hideAllViews(mainView); } }, autoHide); } else { // Don't hide if not correctly set } if (delay > 0) { activity.getWindow().getDecorView().postDelayed(new Runnable() { @Override public void run() { displayViews(mainView); } }, delay); } else { displayViews(mainView); } } private void hideAllViews(final ViewGroup mainView) { Animation hideAnimation; if (fromTop) { hideAnimation = AnimationUtils.loadAnimation(activity, R.anim.fade_out_from_top); } else { hideAnimation = AnimationUtils.loadAnimation(activity, R.anim.fade_out); } hideAnimation.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { mainView.removeAllViews(); } @Override public void onAnimationRepeat(Animation animation) { } }); mainView.startAnimation(hideAnimation); } private void displayViews(ViewGroup mainView) { activity.addContentView(mainView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); Animation fadeInAnimation; if (fromTop) { fadeInAnimation = AnimationUtils.loadAnimation(activity, R.anim.fade_in_from_top); } else { fadeInAnimation = AnimationUtils.loadAnimation(activity, R.anim.fade_in); } mainView.startAnimation(fadeInAnimation); if (onShowListener != null) onShowListener.onRateAppShowing(); } public interface OnShowListener { void onRateAppShowing(); void onRateAppDismissed(); void onRateAppClicked(); } private void LogD(String s) { Log.d("DicreetAppRate", s); } }