package me.barrasso.android.volume.popup;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
import android.content.Intent;
import android.util.Log;
import android.util.DisplayMetrics;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Property;
import android.view.Display;
import android.view.Surface;
import android.view.View;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.ViewConfiguration;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.view.accessibility.AccessibilityEvent;
import android.view.ViewTreeObserver;
import android.view.View.OnSystemUiVisibilityChangeListener;
import android.widget.FrameLayout;
import android.graphics.Rect;
import me.barrasso.android.volume.BuildConfig;
import me.barrasso.android.volume.ui.SystemInfo;
import java.lang.reflect.Field;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Base class for a popup window that is always attached to {@link WindowManager},
* but toggles it's visibility between {@link View#VISIBLE} and {@link View#GONE}.<br />
* Implementing classes need to create and manage their own {@link View}'s as well
* as user interaction, but this class takes care of adding, removing, showing, and
* hiding the window on the screen base on user interaction.<br />
* <em>Note:</em> All layouts are assigned to a decor {@link FrameLayout} as their
* parent. This is used to keep track of key events and prevent the broadcasting
* of accessibility events.<br />
* All subclasses must implement {@link #getWindowParams} and {@link #onCreate},
* returning {@link android.view.WindowManager.LayoutParams} and creating the
* {@link android.view.View}s to be presented, respectively.
*/
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public abstract class PopupWindow implements View.OnKeyListener,
View.OnTouchListener, OnSystemUiVisibilityChangeListener, View.OnLongClickListener {
public static final int MESSAGE_HIDE = 0x00000001;
public static final int MESSAGE_SHOW = 0x00000010;
// Property of PopupWindows: auto hide duration.
public static final Property<PopupWindow, Integer> TIMEOUT =
Property.of(PopupWindow.class, Integer.TYPE, "autoHideDuration");
public static final int POSITION_UNCHANGED = Integer.MIN_VALUE;
private static final AtomicInteger VIEW_ID = new AtomicInteger(0);
private static final AtomicInteger ATTACHED_WINDOWS = new AtomicInteger(0);
private static final AtomicInteger VISIBLE_WINDOWS = new AtomicInteger(0);
protected static int getInternalStyle(final String clazzName, final String resIdName) {
try {
Class<?> clazz = Class.forName(clazzName);
if (null != clazz) {
Field aDialog = clazz.getField(resIdName);
if (null != aDialog) {
aDialog.setAccessible(true);
return (Integer) aDialog.get(null);
}
}
} catch (Throwable e) { /* Shit */ }
return 0;
}
/** @returns The resource ID for "com.android.systemui.R$dimen#resIdName", or 0. */
protected static int getSystemUiDimen(final String resIdName) {
return getInternalStyle("com.android.systemui.R$dimen", resIdName);
}
/** @returns The resource ID for "com.android.internal.R$style#resIdName", or 0. */
protected static int getInternalStyle(final String resIdName) {
return getInternalStyle("com.android.internal.R$style", resIdName);
}
/** Set hidden {@link WindowManager.LayoutParams$hasListeners} */
public static WindowManager.LayoutParams setHasSystemUiListeners(
WindowManager.LayoutParams mParams, boolean hasListeners) {
if (mParams == null) return mParams;
try {
final Field mField = mParams.getClass()
.getDeclaredField("hasSystemUiListeners");
if (mField == null) return mParams;
mField.setAccessible(true);
mField.setBoolean(mParams, hasListeners);
} catch (Throwable e) { }
return mParams;
}
// ========== WINDOW MANAGEMENT ==========
/** Generate and return a new View ID, [1, {@link Integer#MAX_VALUE}]. */
protected static int generateId() {
return VIEW_ID.incrementAndGet();
}
/** @return The number of active popup windows. */
public static int getAttachedWindows() {
return ATTACHED_WINDOWS.get();
}
/** @return The number of visible popup windows. */
public static int getVisibleWindows() {
return VISIBLE_WINDOWS.get();
}
/*package*/ class HideHandler extends Handler {
public HideHandler(Looper loopah) {
super(loopah);
}
@Override
public void handleMessage(Message message) {
switch (message.what) {
case MESSAGE_HIDE:
hide();
break;
case MESSAGE_SHOW:
show();
break;
}
}
}
// Window barHeight and height (only to be used based on rotation!)
private final int widthPixels;
private final int heightPixels;
// 0 for never, otherwise time in milliseconds.
protected int autoHideDuration = 0;
protected int mStatusBarHeight;
protected boolean mShowing;
protected boolean attached;
/*package*/ boolean mAllowOffScreen = false;
/*package*/ boolean mCancelable = true;
/*package*/ boolean mCloseOnTouchOutside = true;
/*package*/ boolean mDelayAutoHideOnUserInteraction = true;
/*package*/ boolean mCloseOnLongClick = false;
protected boolean enabled = true;
protected final ViewConfiguration mViewConfiguration;
protected final Handler mUiHandler;
protected PopupWindowManager pWindowManager;
protected View mLayout;
private ViewGroup mDecor;
protected boolean created = false;
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public PopupWindow(PopupWindowManager windowManager) {
pWindowManager = windowManager;
// Obtain height & barHeight once, then determine which is which
// based on the orientation/ rotation of the device.
DisplayMetrics dm = new DisplayMetrics();
Display mDisplay = pWindowManager.getWindowManager().getDefaultDisplay();
mRotation = PopupWindowManager.getRotation(mDisplay);
mDisplay.getMetrics(dm);
widthPixels = ((isLandscape()) ? dm.heightPixels : dm.widthPixels);
heightPixels = ((isLandscape()) ? dm.widthPixels : dm.heightPixels);
Context context = pWindowManager.getContext();
mStatusBarHeight = SystemInfo.getStatusBarHeight(context);
mViewConfiguration = ViewConfiguration.get(context);
mUiHandler = new HideHandler(context.getMainLooper());
onCreate();
// If a layout was made, initialize the PopupWindow and handle
// system-wide events. A layout MUST be supplied at initialization
// or the PopupWindow cannot be displayed.
if (mLayout != null) {
SilentFrameLayout layout = new SilentFrameLayout(context);
layout.setId(generateId());
layout.setOnTouchListener(this);
layout.setOnDispatchKeyListener(this);
layout.setOnSystemUiVisibilityChangeListener(this);
layout.getViewTreeObserver().addOnScrollChangedListener(gScrollListener);
layout.setOnLongClickListener(this);
// Place the designated layout in a container.
layout.addView(mLayout);
mDecor = layout;
created = true;
attach();
}
// Become managed by PopupWindowManager. It's already been created, so we
// might as well become managed by it.
pWindowManager.add(this);
}
public Resources getResources() {
return getContext().getResources();
}
public Context getContext() {
return pWindowManager.getContext();
}
/** @return The height, in pixels, of the system status bar. */
public int getStatusBarHeight() {
return mStatusBarHeight;
}
/**
* If true, {@link #setAutoHideDuration} will be delayed by {@link @onUserInteraction}.
* Motion, keyboard, trackpad, etc. events will all delay the hiding of this window.<br />
* Default value is true.
*/
public void setTransient(boolean mTransient) {
mDelayAutoHideOnUserInteraction = mTransient;
}
/**
* If true (and this window parameters has {@link android.view.WindowManager.LayoutParams#FLAG_WATCH_OUTSIDE_TOUCH}
* this window will automatically close when the user touches outside of it.<br />
* Default value is true.
*/
public void setCanceledOnTouchOutside(boolean mCloseOnTouch) {
mCloseOnTouchOutside = mCloseOnTouch;
}
/**
* If true, a {@link View.OnLongClickListener#onLongClick(android.view.View)}
* will hide this popup window. Default value is false.
*/
public void setCloseOnLongClick(boolean mCloseOnLong) {
mCloseOnLongClick = mCloseOnLong;
}
/**
* The time, in milliseconds, to automatically hide this
* popup window after being shown. 0 for never (default).
*/
public void setAutoHideDuration(int duration) {
autoHideDuration = duration;
}
public int getAutoHideDuration() { return autoHideDuration; }
/**
* Allowing the PopupWindow to be shown, partly or entirely,
* off of the screen. This will bound all calls to {@link #move(int, int)}
* and {@link #position(int, int)} by {0, statusBarHeight} and {screenWidth, screenHeight}.
*/
public void setAllowOffScreen(boolean allowOffScreen) {
mAllowOffScreen = allowOffScreen;
}
/** Set the click listener for the popup window as a whole. */
public void setOnClickListener(OnClickListener mClickListener) {
mDecor.setOnClickListener(mClickListener);
}
/** @see {@link Intent#ACTION_CLOSE_SYSTEM_DIALOGS} */
public void closeSystemDialogs(final String reason) {
// Override to handle specific requests to close system dialogs.
if (mCancelable) {
hide();
}
}
/** @see {@link Intent#ACTION_SCREEN_OFF} */
public void screen(final boolean on) {
// Override to specially handle screen on/ off events.
if (mCancelable && !on) {
hide();
}
}
/** Set true to enable/ disable this PopupWindow. */
public void setEnabled(final boolean mEnabled) {
enabled = mEnabled;
if (enabled) {
attach();
} else {
hide();
detach();
}
}
/** @return True if this PopupWindow is enabled. */
public boolean isEnabled() {
return enabled;
}
/**
* Set whether this window can be cancelled by user interaction (i.e. back button).<br />
* Default value is true.
*/
public void setCancelable(boolean cancelable) {
mCancelable = cancelable;
}
/** @return True if this window is cancelable. Default value is true. */
public boolean isCancelable() {
return mCancelable;
}
/** @return The decor window {@link View}. */
public View peekDecor() {
return mDecor;
}
/**
* Move the PopupWindow by a delta x and y. Pass 0 to keep the
* current position. <em>Note</em>: not all windows observe this behavior.
*/
public void move(final int dx, final int dy) {
LayoutParams wParams = getWindowParams();
wParams.gravity = (Gravity.LEFT | Gravity.TOP);
wParams.x += dx;
wParams.y += dy;
if (!mAllowOffScreen) bound();
onWindowAttributesChanged();
}
/**
* Update the x and y positions of the window. Pass
* {@link PopupWindow#POSITION_UNCHANGED} to keep the
* current position. <em>Note</em>: not all windows
* observe this behavior.
*/
public void position(final int x, final int y) {
LayoutParams wParams = getWindowParams();
wParams.gravity = (Gravity.LEFT | Gravity.TOP);
if (x != POSITION_UNCHANGED) wParams.x = x;
if (y != POSITION_UNCHANGED) wParams.y = y;
if (!mAllowOffScreen) bound();
onWindowAttributesChanged();
}
/** Bind the {@link WindowManager.LayoutParams} within the screen. */
private void bound() {
LayoutParams wParams = getWindowParams();
wParams.x = Math.min(Math.max(0, wParams.x), getWindowWidth());
wParams.y = Math.min(Math.max(mStatusBarHeight, wParams.y), getWindowHeight());
}
/** @return A {@link android.graphics.Rect} of the root view's position. */
public Rect getBounds() {
LayoutParams wParams = getWindowParams();
return new Rect(wParams.x, wParams.y, wParams.x + getWidth(), wParams.y + getHeight());
}
/**
* Called when the status bar changes visibility because of a call to
* {@link View#setSystemUiVisibility(int)}.
*
* @param visibility Bitwise-or of flags {@link android.view.View#SYSTEM_UI_FLAG_LOW_PROFILE} or
* {@link android.view.View#SYSTEM_UI_FLAG_HIDE_NAVIGATION}. This tells you the
* <strong>global</strong> state of the UI visibility flags, not what your
* app is currently applying.
*/
@Override
public void onSystemUiVisibilityChange(int visibility) {
if (BuildConfig.DEBUG) {
Log.v("PopupWindow", "--onSystemUiVisibilityChange(" + String.valueOf(visibility) + ')');
}
}
// onKey is used for Views with focus. If they do not handle the event
// we want to be sure to handle the {@link KeyEvent#KEYCODE_BACK} event!
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (BuildConfig.DEBUG) {
Log.v("PopupWindow", "--onKey(" + String.valueOf(keyCode) + ')');
}
switch (keyCode) {
case KeyEvent.KEYCODE_BACK: {
switch (event.getAction()) {
case KeyEvent.ACTION_UP: {
if (!event.isCanceled()) {
onBackPressed();
}
break;
}
}
return true;
}
}
// Delay auto-hiding if set to do so (and we didn't already handle this event).
if (event.getAction() == KeyEvent.ACTION_DOWN) {
onUserInteraction();
}
return false;
}
// Handle scroll events globally, cause user interaction event.
/*package*/ ViewTreeObserver.OnScrollChangedListener gScrollListener = new ViewTreeObserver.OnScrollChangedListener() {
public void onScrollChanged() {
if (BuildConfig.DEBUG) Log.v("PopupWindow", "--OnScrollChangedListener()");
onUserInteraction();
}
};
@Override
public boolean onLongClick(View decor) {
if (BuildConfig.DEBUG) Log.v("PopupWindow", "--onLongClick()");
if (mCloseOnLongClick) {
hide();
return true;
}
return false;
}
/**
* Finds a view that was identified by the id attribute from the XML.
*
* @param id the identifier of the view to find
* @return The view if found or null otherwise.
*/
public View findViewById(int id) {
return mDecor.findViewById(id);
}
/**
* Called whenever a key, touch, or trackball event is dispatched to the
* popup window. Implement this method if you wish to know that the user has
* interacted with the device in some way while your window is running.
*/
protected void onUserInteraction() {
if (BuildConfig.DEBUG) Log.v("PopupWindow", "--onUserInteraction()");
if (mShowing && autoHideDuration > 0 && mDelayAutoHideOnUserInteraction) {
mUiHandler.removeMessages(MESSAGE_HIDE);
mUiHandler.sendEmptyMessageDelayed(MESSAGE_HIDE, autoHideDuration);
}
}
/**
* Show the popup window.<br /><em>Note:</em> not all inheriting
* classes can be shown without providing additional information!
*/
protected void show() {
if (!attached) attach(); // If we're not attached, so it!
if (!mShowing && null != mDecor) {
onVisibilityChanged(View.VISIBLE);
mDecor.setVisibility(View.VISIBLE);
mShowing = true;
VISIBLE_WINDOWS.incrementAndGet();
// Auto-hide after a certain time?
if (autoHideDuration > 0) {
mUiHandler.sendEmptyMessageDelayed(MESSAGE_HIDE, autoHideDuration);
}
}
onUserInteraction();
}
/** Hide the popup window. */
public void hide() {
if (mShowing && null != mDecor) {
onVisibilityChanged(View.GONE);
mDecor.setVisibility(View.GONE);
mShowing = false;
VISIBLE_WINDOWS.decrementAndGet();
// Remove unnecessary calls to hide().
mUiHandler.removeMessages(MESSAGE_HIDE);
}
}
/** @return The ID for the root view of this popup window. */
public int getId() {
return ((mDecor == null) ? View.NO_ID : mDecor.getId());
}
/** @return A name for this PopupWindow.
* Default implementation returns the class name. */
public String getName() { return getClass().getSimpleName(); }
/** Initialize this popup window and create a base layout to display. */
abstract void onCreate();
/** @return The window parameters for displaying this popup window. */
abstract WindowManager.LayoutParams getWindowParams();
/** Window visibility changed.
* @see {@link android.view.View} */
public void onVisibilityChanged(int visibility) {}
protected int mRotation;
/** Device rotation changed.
* @see {@link android.view.Surface} */
public void onRotationChanged(int rotation) {
mRotation = rotation;
onUserInteraction();
}
public int getRotation() { return mRotation; }
/** @return True if the device is currently in landscape. */
public boolean isLandscape() {
return (mRotation == Surface.ROTATION_90 ||
mRotation == Surface.ROTATION_270);
}
public void onAttachedToWindow() {}
public void onDetachedFromWindow() {}
/**
* Called when the dialog has detected the user's press of the back
* key. The default implementation simply cancels the window (only if
* it is cancelable), but you can override this to do whatever you want.
*/
public void onBackPressed() {
if (BuildConfig.DEBUG)
Log.v("PopupWindow", "--onBackPressed()");
if (mCancelable) {
hide();
}
}
/** Proxy for {@link WindowManager#updateViewLayout}. */
public void onWindowAttributesChanged() {
if (BuildConfig.DEBUG) {
Log.v("PopupWindow", "Updating " + getName() + " with the following window parameters:");
Log.v("PopupWindow", getWindowParams().toString());
}
WindowManager.LayoutParams params = setHasSystemUiListeners(getWindowParams(), true);
if (!attached) {
attach();
} else {
pWindowManager.updateViewLayout(mDecor, params);
}
if (null != mDecor) {
mDecor.requestLayout();
mDecor.invalidate();
}
}
/** @return True if the popup window is mShowing. */
public boolean isShowing() {
return mShowing;
}
/** @return True if the popup window is attached. */
public boolean isAttached() {
return attached;
}
/** Attach the popup window via {@link WindowManager}. */
protected void attach() {
if (!attached && null != mDecor) {
onAttachedToWindow();
mDecor.setVisibility(View.GONE);
pWindowManager.addView(mDecor,
setHasSystemUiListeners(getWindowParams(), true));
attached = true;
ATTACHED_WINDOWS.incrementAndGet();
if (BuildConfig.DEBUG) {
Log.v("PopupWindow", "Attaching " + getName() + " with the following window parameters:");
Log.v("PopupWindow", getWindowParams().toString());
}
}
}
/** Detach the popup window via {@link WindowManager}. */
protected void detach() {
if (attached && null != mDecor) {
onDetachedFromWindow();
pWindowManager.removeView(mDecor);
mDecor.setVisibility(View.VISIBLE);
attached = false;
ATTACHED_WINDOWS.decrementAndGet();
}
}
/** Destroy this popup window. If this method is overridden, be SURE to call super! */
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public void onDestroy() {
detach();
// We should remove ourself or we'll run into issues
// with "NullPointerException: Attempt to invoke virtual method"
if (null != pWindowManager)
pWindowManager.remove(this);
pWindowManager = null;
if (null != mDecor) {
mDecor.setOnKeyListener(null);
mDecor.setOnClickListener(null);
mDecor.setOnTouchListener(null);
mDecor.getViewTreeObserver().removeOnScrollChangedListener(gScrollListener);
mDecor.setOnSystemUiVisibilityChangeListener(null);
final int children = mDecor.getChildCount();
if (children > 0) mDecor.removeViewsInLayout(0, children);
if (mDecor instanceof SilentFrameLayout)
((SilentFrameLayout) mDecor).setOnDispatchKeyListener(null);
}
mLayout = null;
mDecor = null;
mShowing = false;
}
/** @return A {@link View.OnClickListener} that hides this popup window. */
public View.OnClickListener getHideOnClickListener() {
return new View.OnClickListener() {
@Override
public void onClick(View v) {
hide();
}
};
}
/** @return The popup window's height. */
public int getHeight() {
return mDecor.getHeight();
}
/** @return The popup window's barHeight. */
public int getWidth() {
return mDecor.getWidth();
}
/** @return The {@link android.view.Window} barHeight. */
public int getWindowWidth() {
return (getWindowDimensions())[0];
}
/** @return The {@link android.view.Window} height. */
public int getWindowHeight() {
return (getWindowDimensions())[1];
}
/** @return The {@link android.view.Window} barHeight and height (array index 0 and 1 respectively). */
public int[] getWindowDimensions() {
final int[] WINDOW_DIMS = new int[2];
final boolean isLandscape = pWindowManager.isLandscape();
WINDOW_DIMS[0] = ((isLandscape) ? heightPixels : widthPixels);
WINDOW_DIMS[1] = ((isLandscape) ? widthPixels : heightPixels);
return WINDOW_DIMS;
}
// Methods localized from android.view.Window (hidden from SDK).
/**
* Called when a touch screen event was not handled by any of the views
* under it. This is most useful to process touch events that happen outside
* of your window bounds, where there is no view to receive it.
*
* @param event The touch screen event being processed.
* @return Return true if you have consumed the event, false if you haven't.
* The default implementation will cancel the dialog when a touch
* happens outside of the window bounds.
*/
@Override
public boolean onTouch(View v, MotionEvent event) {
final boolean outOfBounds = isOutOfBounds(event);
if (mShowing && shouldCloseOnTouch(event)) {
hide();
return true;
} else if (mShowing && !outOfBounds &&
(event.getActionMasked() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
return false;
}
/** @hide */
public boolean shouldCloseOnTouch(MotionEvent event) {
if (mCloseOnTouchOutside && (event.getActionMasked() == MotionEvent.ACTION_OUTSIDE || isOutOfBounds(event)) && mDecor != null) {
return true;
}
return false;
}
public boolean isOutOfBounds(MotionEvent event) {
final int x = (int) event.getX();
final int y = (int) event.getY();
final int slop = mViewConfiguration.getScaledWindowTouchSlop();
return (x < -slop) || (y < -slop)
|| (x > (getWidth()+slop))
|| (y > (getHeight()+slop));
}
// Imported from eyes-free, SimpleOverlay.java
// Speech Enabled Eyes-Free Android Applications
// Copyright (C) 2010 Google Inc.
// https://code.google.com/p/eyes-free/source/browse/trunk/libraries/utils/src/com/googlecode/eyesfree/widget/SimpleOverlay.java?r=799
/**
* {@link FrameLayout} that does not send accessibility events and
* proxies all key events to a designated listener.
*/
private static final class SilentFrameLayout extends FrameLayout {
private View.OnKeyListener mOnDispatchKeyListener;
public SilentFrameLayout(Context context) {
super(context);
}
public void setOnDispatchKeyListener(View.OnKeyListener onDispatchKeyListener) {
mOnDispatchKeyListener = onDispatchKeyListener;
}
@Override
public boolean requestSendAccessibilityEvent(View view, AccessibilityEvent event) {
return false; // Never send accessibility events.
}
@Override
public void sendAccessibilityEventUnchecked(AccessibilityEvent event) {
return; // Never send accessibility events.
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (null != mOnDispatchKeyListener) {
if (mOnDispatchKeyListener.onKey(this, event.getKeyCode(), event)) {
return true;
}
}
return super.dispatchKeyEvent(event);
}
}
}