package me.barrasso.android.volume.popup; import android.annotation.TargetApi; import android.content.Context; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Intent; import android.content.IntentFilter; import android.util.Log; import android.os.IInterface; import android.os.RemoteException; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.view.Display; import android.view.View; import android.view.Surface; import android.text.TextUtils; import android.provider.Settings; import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodInfo; import android.database.ContentObserver; import me.barrasso.android.volume.BuildConfig; import me.barrasso.android.volume.utils.ReflectionUtils; // Hidden APIs import android.view.IRotationWatcher; import java.util.Collection; import java.util.List; import java.util.WeakHashMap; import java.lang.reflect.Method; /** * Manager of {@link PopupWindow}s. Useful to monitoring system-wide events such as * the home button being pressed, the screen being turned off, and the device rotating. * This class is the centralized location linking PopupWindows to their owner.<br /> * Classes using this tool may hold on to references of {@link PopupWindow}s and show/ * hide them as necessary. Although it is not required, hard references to these windows * should be held and {@link PopupWindowManager#remove} should be called to discard unused * windows and better facilitate clean up. */ @TargetApi(Build.VERSION_CODES.FROYO) public class PopupWindowManager extends IRotationWatcher.Stub { public static final String TAG = PopupWindowManager.class.getSimpleName(); /** @return True if the current {@link Thread} is the main, UI thread. */ public static boolean isUiThread() { return (Looper.myLooper() != null && Looper.myLooper().equals(Looper.getMainLooper())); // return (Looper.getMainLooper().getThread().equals(Thread.currentThread())); } protected static void postOnUiThread(Runnable run) { (new Handler(Looper.getMainLooper())).post(run); } // ========== CANCEL RECEIVER ========== // Localized from PhoneWindowManager... these are helpful! static public final String SYSTEM_DIALOG_REASON_KEY = "reason"; static public final String SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS = "globalactions"; static public final String SYSTEM_DIALOG_REASON_RECENT_APPS = "recentapps"; static public final String SYSTEM_DIALOG_REASON_HOME_KEY = "homekey"; static public final String SYSTEM_DIALOG_REASON_KEYGUARD = "lock"; // Filter for a BroadcastReceiver to cancel this Window, as necessary. private static final IntentFilter CANCEL_FILTER = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); static { CANCEL_FILTER.addAction(Intent.ACTION_SCREEN_ON); CANCEL_FILTER.addAction(Intent.ACTION_SCREEN_OFF); CANCEL_FILTER.addAction(Intent.ACTION_USER_PRESENT); CANCEL_FILTER.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); } private static IInterface iWindowManager; /** @return An instance of android.view.IWindowManager. */ public synchronized static IInterface getIWindowManager() { if (null == iWindowManager) { iWindowManager = ReflectionUtils.getIInterface( Context.WINDOW_SERVICE, "android.view.IWindowManager$Stub"); } return iWindowManager; } @SuppressWarnings("depecation") protected static int getRotation(Display mDisplay) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) return mDisplay.getRotation(); else return mDisplay.getOrientation(); } /** Register an {@link IRotationWatcher} with IWindowManager. */ protected static boolean watchRotations(IRotationWatcher watcher, final boolean watch) { // NOTE: removeRotationWatcher is only available on Android 4.3 (API 18) and 4.4 (API 19 & 20). final String methodName = ((watch) ? "watchRotation" : "removeRotationWatcher" ); final IInterface mWM = getIWindowManager(); if (null == mWM) return false; try { Method mMethod = mWM.getClass().getDeclaredMethod( methodName, new Class[]{ IRotationWatcher.class }); if (mMethod == null) return false; mMethod.setAccessible(true); mMethod.invoke(mWM, watcher); return true; } catch (Throwable t) { Log.e(TAG, "Cannot register " + IRotationWatcher.class.getSimpleName() , t); return false; } } /** * {@link BroadcastReceiver} to handle events like turning the screen off, * pressing the home button, etc. These might result in canceling the * popup window depending on the settings. */ /*package*/ final class CancelReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (null == intent || TextUtils.isEmpty(intent.getAction())) return; final String mAction = intent.getAction(); // Handle global actions (often used to notify dialogs). if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equalsIgnoreCase(mAction)) { final String mReason = intent.getStringExtra(SYSTEM_DIALOG_REASON_KEY); closeSystemDialogs(mReason); } else if (Intent.ACTION_SCREEN_OFF.equalsIgnoreCase(mAction) || Intent.ACTION_SCREEN_ON.equalsIgnoreCase(mAction)) { final boolean sOn = Intent.ACTION_SCREEN_ON.equalsIgnoreCase(mAction); screen(sOn); // The screen state has changed (on/ off). } } } /** * {@link ContentObserver} to handle changes in the active Input Method. */ /*package*/ final class InputMethodObserver extends ContentObserver { public InputMethodObserver(Handler h) { super(h); } @Override public boolean deliverSelfNotifications() { return true; } @Override public void onChange(boolean selfChange) { super.onChange(selfChange); mActiveInputMethod = retrieveActiveInputMethod(); } } // Map of managed PopupWindows identified by their unique IDs. protected final WeakHashMap<Integer, PopupWindow> mPopupWindows = new WeakHashMap<Integer, PopupWindow>(); protected final Handler mUiHandler = new Handler(Looper.getMainLooper()); protected CancelReceiver mCancelReceiver; protected WindowManager mWindowManager; protected InputMethodObserver mInputMethodObserver; protected InputMethodManager mInputMethodManager; protected ComponentName mActiveInputMethod; protected Context mContext; protected int mRotation; protected boolean isScreenOn = true; public PopupWindowManager(Context context) { mContext = context; mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); mInputMethodManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); mActiveInputMethod = retrieveActiveInputMethod(); mRotation = getRotation(mWindowManager.getDefaultDisplay()); registerCancelReceiver(); watchRotations(this, true); registerInputMethodObserver(); } /** @return True if the device is currently in landscape. */ public boolean isLandscape() { return (mRotation == Surface.ROTATION_90 || mRotation == Surface.ROTATION_270); } /** @see {@link InputMethodManager#isActive} */ public boolean isInputViewShown() { return mInputMethodManager.isActive(); } /** * @return The {@link ComponentName} for the active Input Method, * or null if one could not be obtained. */ public ComponentName getActiveInputMethod() { return mActiveInputMethod; } protected ComponentName retrieveActiveInputMethod() { if (null == mContext) return null; final String id = Settings.Secure.getString( mContext.getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD ); if (TextUtils.isEmpty(id)) return null; List<InputMethodInfo> mInputMethodProperties = mInputMethodManager.getEnabledInputMethodList(); for (InputMethodInfo mInputMethod : mInputMethodProperties) { if (id.equals(mInputMethod.getId())) { return mInputMethod.getComponent(); } } return null; } public int getRotation() { return mRotation; } public final Context getContext() { return mContext; } /** Add a {@link PopupWindow} to be managed. */ public void add(PopupWindow window) { synchronized (mPopupWindows) { mPopupWindows.put(window.getId(), window); } } /** * Removes and destroys a {@link PopupWindow}. * @return True if a PopupWindow was removed this way. */ public boolean remove(PopupWindow window) { synchronized (mPopupWindows) { PopupWindow removed = mPopupWindows.remove(window.getId()); if (null != removed) removed.onDestroy(); return (null != removed); } } /** * Hide all cancelable {@link PopupWindow}s. * @param force True if even non-cancelable windows should be hidden. */ protected void hide(final boolean force) { if (BuildConfig.DEBUG) { Log.v(TAG, ((force) ? "Hiding ALL PopupWindows." : "Hiding cancelable PopupWindows.")); } // Hide all popup windows as necessary. postOnUiThread(new Runnable() { @Override public void run() { synchronized (mPopupWindows) { for (PopupWindow pw : mPopupWindows.values()) { if (force || pw.isCancelable()) { pw.hide(); } } } } }); } /** * @see {@link Intent#ACTION_CLOSE_SYSTEM_DIALOGS} */ protected void closeSystemDialogs(final String reason) { if (BuildConfig.DEBUG) { Log.v(TAG, Intent.ACTION_CLOSE_SYSTEM_DIALOGS + ':' + reason); } // Hide all popup windows as necessary. postOnUiThread(new Runnable() { @Override public void run() { synchronized (mPopupWindows) { for (PopupWindow pw : mPopupWindows.values()) { pw.closeSystemDialogs(reason); } } } }); } /** @return True if the screen is on. */ public boolean isScreenOn() { return isScreenOn; } /** * @see {@link Intent#ACTION_SCREEN_OFF} */ protected void screen(final boolean on) { if (BuildConfig.DEBUG) { Log.v(TAG, ((on) ? Intent.ACTION_SCREEN_ON : Intent.ACTION_SCREEN_OFF)); } isScreenOn = on; // Hide all popup windows as necessary. postOnUiThread(new Runnable() { @Override public void run() { synchronized (mPopupWindows) { for (PopupWindow pw : mPopupWindows.values()) { if (null != pw) { pw.screen(on); } } } } }); } @Override public void onRotationChanged(final int rotation) throws RemoteException { if (BuildConfig.DEBUG) Log.v(TAG, "--onRotationChanged(" + String.valueOf(rotation) + ')'); mRotation = rotation; // Propagate the rotation change event to all PopupWindows. postOnUiThread(new Runnable() { @Override public void run() { synchronized (mPopupWindows) { for (PopupWindow pw : mPopupWindows.values()) { if (null != pw) { pw.onRotationChanged(rotation); } } } } }); } // ========== WINDOW MANAGER ========== public final WindowManager getWindowManager() { return mWindowManager; } /** Convenience method for {@link WindowManager#updateViewLayout}. */ public void updateViewLayout(View layout, WindowManager.LayoutParams params) { mWindowManager.updateViewLayout(layout, params); } /** Convenience method for {@link WindowManager#addView}. */ public void addView(View layout, WindowManager.LayoutParams params) { mWindowManager.addView(layout, params); } /** Convenience method for {@link WindowManager#removeView}. */ public void removeView(View layout) { mWindowManager.removeView(layout); } protected void registerCancelReceiver() { if (null == mCancelReceiver) mCancelReceiver = new CancelReceiver(); mContext.registerReceiver(mCancelReceiver, CANCEL_FILTER); } protected void registerInputMethodObserver() { if (null == mInputMethodObserver) mInputMethodObserver = new InputMethodObserver(mUiHandler); mContext.getContentResolver().registerContentObserver( Settings.Secure.CONTENT_URI, true, mInputMethodObserver); } protected void unregisterCancelReceiver() { if (null != mCancelReceiver) mContext.unregisterReceiver(mCancelReceiver); mCancelReceiver = null; } protected void unregisterInputMethodObserver() { if (null != mInputMethodObserver) mContext.getContentResolver().unregisterContentObserver(mInputMethodObserver); mInputMethodObserver = null; } /** Destroy all {@link PopupWindow}s and all receivers/ listeners. */ public void destroy() { unregisterCancelReceiver(); unregisterInputMethodObserver(); watchRotations(this, false); synchronized (mPopupWindows) { Collection<PopupWindow> windows = mPopupWindows.values(); synchronized (windows) { for (PopupWindow pw : windows) { pw.onDestroy(); } } mPopupWindows.clear(); } mWindowManager = null; mInputMethodManager = null; mContext = null; } }