package com.artfulbits.ui;
import android.content.Context;
import android.content.res.Configuration;
import android.hardware.SensorManager;
import android.os.Handler;
import android.os.Message;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.OrientationEventListener;
import android.view.View;
import android.view.WindowManager;
import android.widget.PopupWindow;
import android.widget.TextView;
import com.artfulbits.R;
import com.artfulbits.utils.LogEx;
import com.artfulbits.utils.Use;
import com.artfulbits.utils.ValidUtils;
import java.util.Locale;
import java.util.logging.Logger;
public class Progress
implements Handler.Callback, CountDownWithCallback.Callback, View.OnClickListener {
/* [ CONSTANTS ] ======================================================================================================================================= */
/** Unique message ID. Request timer/countdown update. */
private final static int MSG_UPDATE_TIMER = -1;
/** Request progress view to hide. */
private final static int MSG_HIDE = -2;
/** Request progress to display a message. */
private final static int MSG_UPDATE_TEXT = -3;
/** Rotation listening. 30 degrees is a threshold for rotation. */
private final static int ANGLE_THRESHOLD = 30;
/** 7.5 seconds is a default UX timeout for web requests. */
public final static long DEFAULT_UX_TIMEOUT = 7500;
/** User cancel action */
public final static int STATE_CANCEL = -100;
/** Action not processed in specified time. */
public final static int STATE_TIMEOUT = -101;
/** Our own class Logger instance. */
private final static Logger _log = LogEx.getLogger(Progress.class);
/* [ MEMBERS ] ========================================================================================================================================= */
public final PopupWindow popup;
public final View progress;
public final TextView countdown;
public final TextView messages;
public final View parent;
public final int x;
public final int y;
public final int width;
public final int height;
public final int frame;
public final boolean isPortrait;
/** Reference on application context. */
private final Context mContext;
/** messages handler. */
private final Handler mHandler = new Handler(this);
/** Count down timer. */
private final CountDownWithCallback mTimer;
/** Reference on callback interface. */
private final Callback mCallback;
/** Listen screen orientation changes and if needed recreate progress screen. */
private final OrientationCallback mOrientation;
/* [ CONSTRUCTORS ] ==================================================================================================================================== */
/**
* Copy constructor.
*
* @param v reference on new parent
* @param source progress window to inherit.
*/
protected Progress(final View v, final Progress source) {
final Context context = mContext = source.mContext;
parent = v;
// reuse elements of source - progress view
progress = source.progress;
countdown = source.countdown;
messages = source.messages;
source.mOrientation.disable();
// reuse elements of source - timers, callbacks, orientation listener
mTimer = new CountDownWithCallback(source.mTimer);
mOrientation = source.mOrientation.setParent(this);
mCallback = source.mCallback;
// update progress view
progress.setOnClickListener(this);
progress.setTag(R.id.tag_popup_holder, this);
// get orientation mode
isPortrait = (Configuration.ORIENTATION_PORTRAIT == context.getResources().getConfiguration().orientation);
// get screen metrics
final WindowManager wm = Use.service(context, Context.WINDOW_SERVICE);
final DisplayMetrics metrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(metrics);
// x,y positions on screen
final int[] location = new int[]{v.getLeft(), v.getTop()};
v.getLocationOnScreen(location);
// calculate bounds
frame = (int) Use.dp2pixel(context, R.dimen.frame_normal);
x = location[0] + frame;
y = location[1] + frame;
width = Math.min(v.getWidth() - frame * 2, metrics.widthPixels - x - frame);
height = Math.min(v.getHeight() - frame * 2, metrics.heightPixels - y - frame);
// create popup window
popup = new PopupWindow(progress, width, height);
// start listen orientation change
mOrientation.enable();
}
/**
* Construct progress window.
*
* @param context application context.
* @param v parent view reference.
* @param total timeout total in millis.
* @param callback user interaction listener.
*/
public Progress(final Context context, final View v, final long total, final Callback callback) {
final LayoutInflater inflater = LayoutInflater.from(context);
mContext = context;
mCallback = callback;
parent = v;
// create progress view and get references on it controls
progress = inflater.inflate(R.layout.elm_progress_inidicator, null);
countdown = Use.id(progress, R.id.txt_countdown);
messages = Use.id(progress, R.id.txt_message);
progress.setOnClickListener(this);
progress.setTag(R.id.tag_popup_holder, this);
// get orientation mode
isPortrait = (Configuration.ORIENTATION_PORTRAIT == context.getResources().getConfiguration().orientation);
// get screen metrics
final WindowManager wm = Use.service(context, Context.WINDOW_SERVICE);
final DisplayMetrics metrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(metrics);
// x,y positions on screen
final int[] location = new int[]{v.getLeft(), v.getTop()};
v.getLocationOnScreen(location);
// calculate bounds
frame = (int) Use.dp2pixel(context, R.dimen.frame_normal);
x = location[0] + frame;
y = location[1] + frame;
width = Math.min(v.getWidth() - frame * 2, metrics.widthPixels - x - frame);
height = Math.min(v.getHeight() - frame * 2, metrics.heightPixels - y - frame);
// create popup window
popup = new PopupWindow(progress, width, height);
mTimer = new CountDownWithCallback(total, 100, this);
// listen orientation change
mOrientation = new OrientationCallback(this);
mOrientation.enable();
}
/* [ STATIC METHODS ] ================================================================================================================================== */
/**
* Create and show progress popup window with 7.5 seconds timeout.
*
* @param context application context
* @param v parent view which location and size will be used for popup adjusting
* @param callback callback on user actions listening
* @return instance of the Progress window
*/
public static Progress show(final Context context, final View v, final Callback callback) {
final Progress holder = new Progress(context, v, DEFAULT_UX_TIMEOUT, callback);
return holder.show().start();
}
/**
* Recreate progress popup window with reuse of existing progress.
*
* @param v new parent instance.
* @param prev instance on resources to reuse.
* @return newly created progress window.
*/
public static Progress recreate(final View v, final Progress prev) {
ValidUtils.isNull(v, "Parent instance required.");
ValidUtils.isNull(prev, "Instance of prviously created progress instance needed.");
return new Progress(v, prev);
}
/**
* Detect are we in landscape mode.
*
* @param degree orientation event listener value.
* @return True - landscape, otherwise False.
*/
public static boolean isLandscape(int degree) {
return (degree >= (90 - ANGLE_THRESHOLD) && degree <= (90 + ANGLE_THRESHOLD));
}
/**
* Detect are we in portrait mode.
*
* @param degree orientation event listener value.
* @return True - portrait, otherwise False.
*/
public static boolean isPortrait(int degree) {
return ((degree >= (360 - ANGLE_THRESHOLD) && degree <= 360) || (degree >= 0 && degree <= ANGLE_THRESHOLD));
}
/* [ Interface Callback ] ============================================================================================================================== */
/** {@inheritDoc} */
@Override
public final boolean handleMessage(final Message msg) {
if (MSG_UPDATE_TIMER == msg.what) {
final float until = (float) msg.arg1 / 1000;
final String text = String.format(Locale.US, "%.1fs", until);
countdown.setText(text);
return true;
} else if (MSG_HIDE == msg.what) {
if (popup.isShowing()) {
popup.dismiss();
}
return true;
} else if (MSG_UPDATE_TEXT == msg.what) {
if (msg.arg1 != 0) {
messages.setText(msg.arg1);
} else if (msg.obj instanceof CharSequence) {
messages.setText((CharSequence) msg.obj);
}
return true;
}
return false;
}
/** {@inheritDoc} */
@Override
public final void onTick(long until) {
mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_TIMER, (int) until, -1));
}
/** {@inheritDoc} */
@Override
public final void onFinish() {
mHandler.sendMessage(mHandler.obtainMessage(MSG_HIDE));
if (null != mCallback) {
mCallback.onFinish(this, STATE_TIMEOUT);
}
}
/* [ Interface OnClickListener ] ======================================================================================================================= */
/** {@inheritDoc} */
@Override
public final void onClick(final View v) {
if (R.id.rl_progress_indicator == v.getId()) {
if (null != mCallback) {
mCallback.onFinish(this, STATE_CANCEL);
}
}
}
/* [ IMPLEMENTATION & HELPERS ] ======================================================================================================================== */
/**
* Dismiss progress dialog.
*
* @return this instance, for chained calls.
*/
public Progress dismiss() {
mTimer.cancel();
mHandler.sendMessage(mHandler.obtainMessage(MSG_HIDE));
return this;
}
/**
* Raised on detecting orientation change.
*
* @param portrait True - rotated to portrait, otherwise to landscape.
*/
protected void onOrientationChanged(boolean portrait) {
try {
popup.dismiss();
mTimer.cancel();
} catch (Throwable ignore) {
}
}
/**
* Show custom message on progress view.
*
* @param message the message to show
* @return this progress, for chained calls.
*/
public Progress report(final String message) {
mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_TEXT, -1, -1, message));
return this;
}
/**
* Show custom message by it ID on progress view.
*
* @param idMessage the id of message to show
* @return this progress, for cahined calls.
*/
public Progress report(final int idMessage) {
mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_TEXT, idMessage, -1));
return this;
}
/**
* Show progress view based on done configuration.
*
* @return this progress, for chained calls.
*/
public Progress show() {
popup.showAtLocation(this.parent, Gravity.NO_GRAVITY, width, height);
popup.update(x, y, width, height, true);
return this;
}
/**
* Start countdown timer and progress displaying.
*
* @return this progress, for chained calls.
*/
public Progress start() {
mTimer.start();
return this;
}
/* [ NESTED DECLARATIONS ] ============================================================================================================================= */
/** Orientation changed event listener. */
private static class OrientationCallback
extends OrientationEventListener {
/** Reference on listener. */
private Progress mParent;
/**
* Create listener for orientation change.
*
* @param parent listener instance.
*/
public OrientationCallback(final Progress parent) {
super(parent.mContext, SensorManager.SENSOR_DELAY_UI);
mParent = parent;
}
/**
* Assign new parent for callback.
*
* @param progress parent instance.
*/
public OrientationCallback setParent(final Progress progress) {
ValidUtils.isNull(progress, "Progress instance required");
synchronized (this) {
mParent = progress;
}
return this;
}
/** {@inheritDoc} */
@Override
public void onOrientationChanged(int orientation) {
// NOTE: 180 degree rotation is not detected
boolean portrait = isPortrait(orientation);
_log.finest("Orientation degree: " + orientation);
synchronized (this) {
if (mParent.isPortrait != portrait) {
mParent.onOrientationChanged(portrait);
}
}
}
}
/** Callback interface for Progress popup window. */
public interface Callback {
/**
* @param caller reference on caller.
* @param state possible states: {@link com.artfulbits.ui.Progress#STATE_TIMEOUT},
* {@link com.artfulbits.ui.Progress#STATE_CANCEL}
*/
public void onFinish(final Progress caller, int state);
}
}