MultitapAlertDialog.java

package net.zer0bandwidth.android.lib.ui;

import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Build;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import net.zer0bandwidth.android.lib.R;
import net.zer0bandwidth.android.lib.app.AppUtils;

/**
 * Allows specification of a number of multiple taps before the positive button
 * takes action.
 *
 * Unlike the standard {@link AlertDialog}, this class is its own builder.
 *
 * Special thanks to <code><b>@dapayne1</b></code> for the original inspiration.
 *
 * When using the Android Compatibility Library, see also
 * {@link MultitapAlertCompatDialog}.
 *
 * @since zer0bandwidth-net/android 0.1.1 (#21)
 */
@SuppressWarnings( "unused" )                              // This is a library.
public class MultitapAlertDialog
extends AlertDialog
implements DialogInterface.OnClickListener
{
	public static final String LOG_TAG =
			MultitapAlertDialog.class.getSimpleName() ;

	/**
	 * Specifies the default number of taps required on the positive button
	 * before its action is executed.
	 */
	public static final int DEFAULT_TAPS_REQUIRED = 5 ;

	/** The number of times that the positive button has been tapped so far. */
	protected int m_nTapsCurrent = 0 ;

	/** The number of taps required to execute the positive button's action. */
	protected int m_nTapsRequired = DEFAULT_TAPS_REQUIRED ;

	/**
	 * The resource ID of the string which formats the positive button's label.
	 * This defaults to a left-to-right format, but may be changed to a
	 * right-to-left format at runtime if the context demands it.
	 * @see R.string#label_btnMultitapPositive_LTR
	 * @see R.string#label_btnMultitapPositive_RTL
	 */
	protected int m_resPositiveLabelFormat =
			R.string.label_btnMultitapPositive_LTR ;

	/**
	 * The positive button label. This will be used as the first part of the
	 * string format.
	 * @see #m_resPositiveLabelFormat
	 * @see R.string#label_btnMultitapPositive_LTR
	 * @see R.string#label_btnMultitapPositive_RTL
	 */
	protected String m_sPositiveLabel = null ;

	/** The task to be executed if the positive button is tapped sufficiently. */
	protected Runnable m_taskPositive = null ;

	/** The task to be executed if the negative button is tapped. */
	protected Runnable m_taskNegative = null ;

	/**
	 * Once the dialog is constructed, this will hold a persistent reference to
	 * the positive button itself, so that we can dynamically update its text.
	 */
	protected Button m_btnPositive = null ;

	/**
	 * Additional constructor allowing initial setup of title and message.
	 * @param ctx the context that will spawn the dialog
	 * @param resTitle the resource ID of the title string
	 * @param resMessage the resource ID of the message string
	 */
	public MultitapAlertDialog( Context ctx, int resTitle, int resMessage )
	{
		super(ctx) ;
		this.setTitle( resTitle ) ;
		this.setMessage( ctx.getString( resMessage ) ) ;
		if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 )
		{
			m_resPositiveLabelFormat = ( AppUtils.isTextRTL(ctx) ?
					R.string.label_btnMultitapPositive_RTL :
					R.string.label_btnMultitapPositive_LTR )
				;
		}
		else m_resPositiveLabelFormat = R.string.label_btnMultitapPositive_LTR ;
	}

	/**
	 * Sets the number of taps required to execute the positive action.
	 * @param nTaps the number of taps required
	 * @return (fluid)
	 */
	public MultitapAlertDialog setPositiveTapsRequired( int nTaps )
	{ this.m_nTapsRequired = nTaps ; return this ; }

	/**
	 * Add a positive button with the specified label, such that the specified
	 * task is executed when tapped the requisite number of times.
	 * @param resLabel the label for the button
	 * @param task the task to be executed
	 * @return (fluid)
	 */
	public MultitapAlertDialog setPositiveButton( int resLabel, Runnable task )
	{
		final Context ctx = this.getContext() ;
		m_taskPositive = task ;
		m_sPositiveLabel = ctx.getString( resLabel ) ;
		this.setButton( AlertDialog.BUTTON_POSITIVE, m_sPositiveLabel, this ) ;
		return this ;
	}

	/**
	 * Add a positive button which will execute the specified task when tapped
	 * the requisite number of times.
	 *
	 * The Android standard "OK" text ({@link android.R.string#ok}) will be used
	 * as the button label.
	 *
	 * @param task the task to be executed
	 * @return (fluid)
	 */
	public MultitapAlertDialog setPositiveButton( Runnable task )
	{ return this.setPositiveButton( android.R.string.ok, task ) ; }

	/**
	 * Add a negative button with the specified label, such that the specified
	 * task is executed when tapped <i>once</i>.
	 * @param resLabel the label for the button
	 * @param task the task to be executed
	 * @return (fluid)
	 */
	public MultitapAlertDialog setNegativeButton( int resLabel, Runnable task )
	{
		final Context ctx = this.getContext() ;
		m_taskNegative = task ;
		final String sNegativeLabel = ctx.getString( resLabel ) ;
		this.setButton( AlertDialog.BUTTON_NEGATIVE, sNegativeLabel, this ) ;
		return this ;
	}

	/**
	 * Add a negative button which will execute the specified task when tapped
	 * <i>once</i>.
	 *
	 * The Android standard "Cancel" ({@link android.R.string#cancel}) will be
	 * used as the button label.
	 *
	 * @param task the task to be executed
	 * @return (fluid)
	 */
	public MultitapAlertDialog setNegativeButton( Runnable task )
	{ return this.setNegativeButton( android.R.string.cancel, task ) ; }

	/**
	 * Sets up a standard "OK" positive button and a standard "Cancel" negative
	 * button, using the specified tasks.
	 * @param taskPositive the task to be executed if the positive button is
	 *                     tapped the requisite number of times
	 * @param taskNegative the task to be executed if the negative button is
	 *                     tapped <i>once</i>
	 * @return (fluid)
	 */
	public MultitapAlertDialog setStandardButtons( Runnable taskPositive, Runnable taskNegative )
	{
		return this.setPositiveButton( taskPositive )
		           .setNegativeButton( taskNegative )
				;
	}

	/**
	 * Regenerates the label of the positive button, showing the difference
	 * between the number of taps required and the number of taps already
	 * intercepted.
	 * @return the positive button label
	 */
	protected MultitapAlertDialog regeneratePositiveLabel()
	{
		final int nTapsRemaining = m_nTapsRequired - m_nTapsCurrent ;
		final String sLabel = this.getContext().getString(
				m_resPositiveLabelFormat,
				m_sPositiveLabel, Integer.toString( nTapsRemaining )
			);
		m_btnPositive.setText( sLabel ) ;

		return this ;
	}

	@Override
	public void onClick( DialogInterface dia, int zButtonID )
	{
		switch( zButtonID )
		{
			case AlertDialog.BUTTON_POSITIVE:
			{
				Log.d( LOG_TAG, "Dismissing after positive press." ) ;
				this.dismiss() ;
				if( m_taskPositive != null ) m_taskPositive.run() ;
			} break ;
			case AlertDialog.BUTTON_NEGATIVE:
			default:
			{
				Log.d( LOG_TAG, (new StringBuilder())
						.append( "Clicked the negative button with " )
						.append( m_nTapsCurrent )
						.append( " prior taps." )
						.toString()
					);
				this.dismiss() ;
				if( m_taskNegative != null ) m_taskNegative.run() ;
			}
		}
	}

	@Override
	public void show()
	{
		super.show() ;
		m_nTapsCurrent = 0 ; // Just in case a value weirdly persisted.
		m_btnPositive = this.getButton( AlertDialog.BUTTON_POSITIVE ) ;
		if( m_btnPositive != null )
		{ // Override the dialog's click listener with our own.
			this.regeneratePositiveLabel() ;
			m_btnPositive.setOnClickListener( new Button.OnClickListener()
			{
				protected final MultitapAlertDialog m_dia =
						MultitapAlertDialog.this ;

				@Override
				public void onClick( View w )
				{
					if( ++(m_dia.m_nTapsCurrent) >= m_dia.m_nTapsRequired )
						m_dia.onClick( m_dia, AlertDialog.BUTTON_POSITIVE ) ;
					else
						m_dia.regeneratePositiveLabel() ;
				}
			});
		}
	}

}