├── lint.xml ├── src └── main │ ├── res │ ├── drawable-hdpi │ │ ├── close.png │ │ ├── hide.png │ │ ├── border.9.png │ │ ├── corner.png │ │ ├── maximize.png │ │ └── border_focused.9.png │ ├── values │ │ └── strings.xml │ └── layout │ │ ├── drop_down_list_item.xml │ │ └── system_window_decorators.xml │ ├── java │ └── wei │ │ └── mark │ │ └── standout │ │ ├── Utils.java │ │ ├── ui │ │ ├── TouchInfo.java │ │ └── Window.java │ │ ├── WindowCache.java │ │ ├── constants │ │ └── StandOutFlags.java │ │ └── StandOutWindow.java │ └── AndroidManifest.xml └── .gitignore /lint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/main/res/drawable-hdpi/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdtianyu/StandOut/master/src/main/res/drawable-hdpi/close.png -------------------------------------------------------------------------------- /src/main/res/drawable-hdpi/hide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdtianyu/StandOut/master/src/main/res/drawable-hdpi/hide.png -------------------------------------------------------------------------------- /src/main/res/drawable-hdpi/border.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdtianyu/StandOut/master/src/main/res/drawable-hdpi/border.9.png -------------------------------------------------------------------------------- /src/main/res/drawable-hdpi/corner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdtianyu/StandOut/master/src/main/res/drawable-hdpi/corner.png -------------------------------------------------------------------------------- /src/main/res/drawable-hdpi/maximize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdtianyu/StandOut/master/src/main/res/drawable-hdpi/maximize.png -------------------------------------------------------------------------------- /src/main/res/drawable-hdpi/border_focused.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdtianyu/StandOut/master/src/main/res/drawable-hdpi/border_focused.9.png -------------------------------------------------------------------------------- /src/main/java/wei/mark/standout/Utils.java: -------------------------------------------------------------------------------- 1 | package wei.mark.standout; 2 | 3 | public class Utils { 4 | public static boolean isSet(int flags, int flag) { 5 | return (flags & flag) == flag; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Window icon 4 | Hide 5 | Close 6 | Corner 7 | Maximize 8 | disable_notify_key 9 | 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # files for the dex VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # generated files 12 | bin/ 13 | gen/ 14 | 15 | # Local configuration file (sdk path, etc) 16 | local.properties 17 | 18 | # Windows thumbnail db 19 | Thumbs.db 20 | 21 | # OSX files 22 | .DS_Store 23 | 24 | # Eclipse project files 25 | .classpath 26 | .project 27 | 28 | # Android Studio 29 | *.iml 30 | .idea 31 | #.idea/workspace.xml - remove # and delete .idea if it better suit your needs. 32 | .gradle 33 | build/ 34 | 35 | #NDK 36 | obj/ 37 | 38 | /*/out 39 | /*/*/build 40 | /*/*/production 41 | *.iws 42 | *.ipr 43 | *~ 44 | *.swp 45 | -------------------------------------------------------------------------------- /src/main/java/wei/mark/standout/ui/TouchInfo.java: -------------------------------------------------------------------------------- 1 | package wei.mark.standout.ui; 2 | 3 | import java.util.Locale; 4 | 5 | /** 6 | * This class holds temporal touch and gesture information. Mainly used to hold 7 | * temporary data for onTouchEvent(MotionEvent). 8 | * 9 | * @author Mark Wei 10 | * 11 | */ 12 | public class TouchInfo { 13 | /** 14 | * The state of the window. 15 | */ 16 | public int firstX, firstY, lastX, lastY; 17 | public double dist, scale, firstWidth, firstHeight; 18 | public float ratio; 19 | 20 | /** 21 | * Whether we're past the move threshold already. 22 | */ 23 | public boolean moving; 24 | 25 | @Override 26 | public String toString() { 27 | return String 28 | .format(Locale.US, 29 | "WindowTouchInfo { firstX=%d, firstY=%d,lastX=%d, lastY=%d, firstWidth=%d, firstHeight=%d }", 30 | firstX, firstY, lastX, lastY, firstWidth, firstHeight); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/res/layout/drop_down_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 17 | 18 | 26 | 27 | -------------------------------------------------------------------------------- /src/main/res/layout/system_window_decorators.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 | 24 | 25 | 36 | 37 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 64 | 65 | 66 | 75 | 76 | -------------------------------------------------------------------------------- /src/main/java/wei/mark/standout/WindowCache.java: -------------------------------------------------------------------------------- 1 | package wei.mark.standout; 2 | 3 | import java.util.HashMap; 4 | import java.util.HashSet; 5 | import java.util.Map; 6 | import java.util.Set; 7 | 8 | import wei.mark.standout.ui.Window; 9 | 10 | import android.util.SparseArray; 11 | 12 | public class WindowCache { 13 | public Map, SparseArray> sWindows; 14 | 15 | public WindowCache() { 16 | sWindows = new HashMap, SparseArray>(); 17 | } 18 | 19 | /** 20 | * Returns whether the window corresponding to the class and id exists in 21 | * the {@link #sWindows} cache. 22 | * 23 | * @param id 24 | * The id representing the window. 25 | * @param cls 26 | * Class corresponding to the window. 27 | * @return True if the window corresponding to the class and id exists in 28 | * the cache, or false if it does not exist. 29 | */ 30 | public boolean isCached(int id, Class cls) { 31 | return getCache(id, cls) != null; 32 | } 33 | 34 | /** 35 | * Returns the window corresponding to the id from the {@link #sWindows} 36 | * cache. 37 | * 38 | * @param id 39 | * The id representing the window. 40 | * @param cls 41 | * The class of the implementation of the window. 42 | * @return The window corresponding to the id if it exists in the cache, or 43 | * null if it does not. 44 | */ 45 | public Window getCache(int id, Class cls) { 46 | SparseArray l2 = sWindows.get(cls); 47 | if (l2 == null) { 48 | return null; 49 | } 50 | 51 | return l2.get(id); 52 | } 53 | 54 | /** 55 | * Add the window corresponding to the id in the {@link #sWindows} cache. 56 | * 57 | * @param id 58 | * The id representing the window. 59 | * @param cls 60 | * The class of the implementation of the window. 61 | * @param window 62 | * The window to be put in the cache. 63 | */ 64 | public void putCache(int id, Class cls, Window window) { 65 | SparseArray l2 = sWindows.get(cls); 66 | if (l2 == null) { 67 | l2 = new SparseArray(); 68 | sWindows.put(cls, l2); 69 | } 70 | 71 | l2.put(id, window); 72 | } 73 | 74 | /** 75 | * Remove the window corresponding to the id from the {@link #sWindows} 76 | * cache. 77 | * 78 | * @param id 79 | * The id representing the window. 80 | * @param cls 81 | * The class of the implementation of the window. 82 | */ 83 | public void removeCache(int id, Class cls) { 84 | SparseArray l2 = sWindows.get(cls); 85 | if (l2 != null) { 86 | l2.remove(id); 87 | if (l2.size() == 0) { 88 | sWindows.remove(cls); 89 | } 90 | } 91 | } 92 | 93 | /** 94 | * Returns the size of the {@link #sWindows} cache. 95 | * 96 | * @return True if the cache corresponding to this class is empty, false if 97 | * it is not empty. 98 | * @param cls 99 | * The class of the implementation of the window. 100 | */ 101 | public int getCacheSize(Class cls) { 102 | SparseArray l2 = sWindows.get(cls); 103 | if (l2 == null) { 104 | return 0; 105 | } 106 | 107 | return l2.size(); 108 | } 109 | 110 | /** 111 | * Returns the ids in the {@link #sWindows} cache. 112 | * 113 | * @param cls 114 | * The class of the implementation of the window. 115 | * @return The ids representing the cached windows. 116 | */ 117 | public Set getCacheIds(Class cls) { 118 | SparseArray l2 = sWindows.get(cls); 119 | if (l2 == null) { 120 | return new HashSet(); 121 | } 122 | 123 | Set keys = new HashSet(); 124 | for (int i = 0; i < l2.size(); i++) { 125 | keys.add(l2.keyAt(i)); 126 | } 127 | return keys; 128 | } 129 | 130 | public int size() { 131 | return sWindows.size(); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/wei/mark/standout/constants/StandOutFlags.java: -------------------------------------------------------------------------------- 1 | package wei.mark.standout.constants; 2 | 3 | import wei.mark.standout.StandOutWindow; 4 | import wei.mark.standout.ui.Window; 5 | import android.view.Gravity; 6 | import android.view.MotionEvent; 7 | import android.view.View; 8 | 9 | /** 10 | * Flags to be returned from {@link StandOutWindow#getFlags(int)}. 11 | * 12 | * @author Mark Wei 13 | * 14 | */ 15 | public class StandOutFlags { 16 | // This counter keeps track of which primary bit to set for each flag 17 | private static int flag_bit = 0; 18 | 19 | /** 20 | * Setting this flag indicates that the window wants the system provided 21 | * window decorations (titlebar, hide/close buttons, resize handle, etc). 22 | */ 23 | public static final int FLAG_DECORATION_SYSTEM = 1 << flag_bit++; 24 | 25 | /** 26 | * Setting this flag indicates that the window decorator should NOT provide 27 | * a close button. 28 | * 29 | *

30 | * This flag also sets {@link #FLAG_DECORATION_SYSTEM}. 31 | */ 32 | public static final int FLAG_DECORATION_CLOSE_DISABLE = FLAG_DECORATION_SYSTEM 33 | | 1 << flag_bit++; 34 | 35 | /** 36 | * Setting this flag indicates that the window decorator should NOT provide 37 | * a resize handle. 38 | * 39 | *

40 | * This flag also sets {@link #FLAG_DECORATION_SYSTEM}. 41 | */ 42 | public static final int FLAG_DECORATION_RESIZE_DISABLE = FLAG_DECORATION_SYSTEM 43 | | 1 << flag_bit++; 44 | 45 | /** 46 | * Setting this flag indicates that the window decorator should NOT provide 47 | * a resize handle. 48 | * 49 | *

50 | * This flag also sets {@link #FLAG_DECORATION_SYSTEM}. 51 | */ 52 | public static final int FLAG_DECORATION_MAXIMIZE_DISABLE = FLAG_DECORATION_SYSTEM 53 | | 1 << flag_bit++; 54 | 55 | /** 56 | * Setting this flag indicates that the window decorator should NOT provide 57 | * a resize handle. 58 | * 59 | *

60 | * This flag also sets {@link #FLAG_DECORATION_SYSTEM}. 61 | */ 62 | public static final int FLAG_DECORATION_MOVE_DISABLE = FLAG_DECORATION_SYSTEM 63 | | 1 << flag_bit++; 64 | 65 | /** 66 | * Setting this flag indicates that the window can be moved by dragging the 67 | * body. 68 | * 69 | *

70 | * Note that if {@link #FLAG_DECORATION_SYSTEM} is set, the window can 71 | * always be moved by dragging the titlebar regardless of this flag. 72 | */ 73 | public static final int FLAG_BODY_MOVE_ENABLE = 1 << flag_bit++; 74 | 75 | /** 76 | * Setting this flag indicates that windows are able to be hidden, that 77 | * {@link StandOutWindow#getHiddenIcon(int)}, 78 | * {@link StandOutWindow#getHiddenTitle(int)}, and 79 | * {@link StandOutWindow#getHiddenMessage(int)} are implemented, and that 80 | * the system window decorator should provide a hide button if 81 | * {@link #FLAG_DECORATION_SYSTEM} is set. 82 | */ 83 | public static final int FLAG_WINDOW_HIDE_ENABLE = 1 << flag_bit++; 84 | 85 | /** 86 | * Setting this flag indicates that the window should be brought to the 87 | * front upon user interaction. 88 | * 89 | *

90 | * Note that if you set this flag, there is a noticeable flashing of the 91 | * window during {@link MotionEvent#ACTION_UP}. This the hack that allows 92 | * the system to bring the window to the front. 93 | */ 94 | public static final int FLAG_WINDOW_BRING_TO_FRONT_ON_TOUCH = 1 << flag_bit++; 95 | 96 | /** 97 | * Setting this flag indicates that the window should be brought to the 98 | * front upon user tap. 99 | * 100 | *

101 | * Note that if you set this flag, there is a noticeable flashing of the 102 | * window during {@link MotionEvent#ACTION_UP}. This the hack that allows 103 | * the system to bring the window to the front. 104 | */ 105 | public static final int FLAG_WINDOW_BRING_TO_FRONT_ON_TAP = 1 << flag_bit++; 106 | 107 | /** 108 | * Setting this flag indicates that the system should keep the window's 109 | * position within the edges of the screen. If this flag is not set, the 110 | * window will be able to be dragged off of the screen. 111 | * 112 | *

113 | * If this flag is set, the window's {@link Gravity} is recommended to be 114 | * {@link Gravity#TOP} | {@link Gravity#LEFT}. If the gravity is anything 115 | * other than TOP|LEFT, then even though the window will be displayed within 116 | * the edges, it will behave as if the user can drag it off the screen. 117 | * 118 | */ 119 | public static final int FLAG_WINDOW_EDGE_LIMITS_ENABLE = 1 << flag_bit++; 120 | 121 | /** 122 | * Setting this flag indicates that the system should keep the window's 123 | * aspect ratio constant when resizing. 124 | * 125 | *

126 | * The aspect ratio will only be enforced in 127 | * {@link StandOutWindow#onTouchHandleResize(int, Window, View, MotionEvent)} 128 | * . The aspect ratio will not be enforced if you set the width or height of 129 | * the window's LayoutParams manually. 130 | * 131 | * @see StandOutWindow#onTouchHandleResize(int, Window, View, MotionEvent) 132 | */ 133 | public static final int FLAG_WINDOW_ASPECT_RATIO_ENABLE = 1 << flag_bit++; 134 | 135 | /** 136 | * Setting this flag indicates that the system should resize the window when 137 | * it detects a pinch-to-zoom gesture. 138 | * 139 | * @see Window#onInterceptTouchEvent(MotionEvent) 140 | */ 141 | public static final int FLAG_WINDOW_PINCH_RESIZE_ENABLE = 1 << flag_bit++; 142 | 143 | /** 144 | * Setting this flag indicates that the window does not need focus. If this 145 | * flag is set, the system will not take care of setting and unsetting the 146 | * focus of windows based on user touch and key events. 147 | * 148 | *

149 | * You will most likely need focus if your window contains any of the 150 | * following: Button, ListView, EditText. 151 | * 152 | *

153 | * The benefit of disabling focus is that your window will not consume any 154 | * key events. Normally, focused windows will consume the Back and Menu 155 | * keys. 156 | * 157 | * @see {@link StandOutWindow#focus(int)} 158 | * @see {@link StandOutWindow#unfocus(int)} 159 | * 160 | */ 161 | public static final int FLAG_WINDOW_FOCUSABLE_DISABLE = 1 << flag_bit++; 162 | 163 | /** 164 | * Setting this flag indicates that the system should not change the 165 | * window's visual state when focus is changed. If this flag is set, the 166 | * implementation can choose to change the visual state in 167 | * {@link StandOutWindow#onFocusChange(int, Window, boolean)}. 168 | * 169 | * @see {@link Window#onFocus(boolean)} 170 | * 171 | */ 172 | public static final int FLAG_WINDOW_FOCUS_INDICATOR_DISABLE = 1 << flag_bit++; 173 | 174 | /** 175 | * Setting this flag indicates that the system should disable all 176 | * compatibility workarounds. The default behavior is to run 177 | * {@link Window#fixCompatibility(View, int)} on the view returned by the 178 | * implementation. 179 | * 180 | * @see {@link Window#fixCompatibility(View, int)} 181 | */ 182 | public static final int FLAG_FIX_COMPATIBILITY_ALL_DISABLE = 1 << flag_bit++; 183 | 184 | /** 185 | * Setting this flag indicates that the system should disable all additional 186 | * functionality. The default behavior is to run 187 | * {@link Window#addFunctionality(View, int)} on the view returned by the 188 | * implementation. 189 | * 190 | * @see {@link StandOutWindow#addFunctionality(View, int)} 191 | */ 192 | public static final int FLAG_ADD_FUNCTIONALITY_ALL_DISABLE = 1 << flag_bit++; 193 | 194 | /** 195 | * Setting this flag indicates that the system should disable adding the 196 | * resize handle additional functionality to a custom View R.id.corner. 197 | * 198 | *

199 | * If {@link #FLAG_DECORATION_SYSTEM} is set, the user will always be able 200 | * to resize the window with the default corner. 201 | * 202 | * @see {@link Window#addFunctionality(View, int)} 203 | */ 204 | public static final int FLAG_ADD_FUNCTIONALITY_RESIZE_DISABLE = 1 << flag_bit++; 205 | 206 | /** 207 | * Setting this flag indicates that the system should disable adding the 208 | * drop down menu additional functionality to a custom View 209 | * R.id.window_icon. 210 | * 211 | *

212 | * If {@link #FLAG_DECORATION_SYSTEM} is set, the user will always be able 213 | * to show the drop down menu with the default window icon. 214 | * 215 | * @see {@link Window#addFunctionality(View, int)} 216 | */ 217 | public static final int FLAG_ADD_FUNCTIONALITY_DROP_DOWN_DISABLE = 1 << flag_bit++; 218 | } -------------------------------------------------------------------------------- /src/main/java/wei/mark/standout/ui/Window.java: -------------------------------------------------------------------------------- 1 | package wei.mark.standout.ui; 2 | 3 | import java.util.LinkedList; 4 | import java.util.Queue; 5 | 6 | import wei.mark.standout.R; 7 | import wei.mark.standout.StandOutWindow; 8 | import wei.mark.standout.StandOutWindow.StandOutLayoutParams; 9 | import wei.mark.standout.Utils; 10 | import wei.mark.standout.constants.StandOutFlags; 11 | import android.content.Context; 12 | import android.content.res.Configuration; 13 | import android.os.Bundle; 14 | import android.util.DisplayMetrics; 15 | import android.util.Log; 16 | import android.view.Gravity; 17 | import android.view.KeyEvent; 18 | import android.view.LayoutInflater; 19 | import android.view.MotionEvent; 20 | import android.view.View; 21 | import android.view.ViewGroup; 22 | import android.widget.FrameLayout; 23 | import android.widget.ImageView; 24 | import android.widget.PopupWindow; 25 | import android.widget.TextView; 26 | 27 | /** 28 | * Special view that represents a floating window. 29 | * 30 | * @author Mark Wei 31 | * 32 | */ 33 | public class Window extends FrameLayout { 34 | public static final int VISIBILITY_GONE = 0; 35 | public static final int VISIBILITY_VISIBLE = 1; 36 | public static final int VISIBILITY_TRANSITION = 2; 37 | 38 | static final String TAG = "Window"; 39 | 40 | /** 41 | * Class of the window, indicating which application the window belongs to. 42 | */ 43 | public Class cls; 44 | /** 45 | * Id of the window. 46 | */ 47 | public int id; 48 | 49 | /** 50 | * Whether the window is shown, hidden/closed, or in transition. 51 | */ 52 | public int visibility; 53 | 54 | /** 55 | * Whether the window is focused. 56 | */ 57 | public boolean focused; 58 | 59 | /** 60 | * Original params from {@link StandOutWindow#getParams(int, Window)}. 61 | */ 62 | public StandOutLayoutParams originalParams; 63 | /** 64 | * Original flags from {@link StandOutWindow#getFlags(int)}. 65 | */ 66 | public int flags; 67 | 68 | /** 69 | * Touch information of the window. 70 | */ 71 | public TouchInfo touchInfo; 72 | 73 | /** 74 | * Data attached to the window. 75 | */ 76 | public Bundle data; 77 | 78 | /** 79 | * Width and height of the screen. 80 | */ 81 | int displayWidth, displayHeight; 82 | 83 | /** 84 | * Context of the window. 85 | */ 86 | private final StandOutWindow mContext; 87 | private LayoutInflater mLayoutInflater; 88 | 89 | public Window(Context context) { 90 | super(context); 91 | mContext = null; 92 | } 93 | 94 | public Window(final StandOutWindow context, final int id) { 95 | super(context); 96 | context.setTheme(context.getThemeStyle()); 97 | 98 | mContext = context; 99 | mLayoutInflater = LayoutInflater.from(context); 100 | 101 | this.cls = context.getClass(); 102 | this.id = id; 103 | this.originalParams = context.getParams(id, this); 104 | this.flags = context.getFlags(id); 105 | this.touchInfo = new TouchInfo(); 106 | touchInfo.ratio = (float) originalParams.width / originalParams.height; 107 | this.data = new Bundle(); 108 | DisplayMetrics metrics = mContext.getResources() 109 | .getDisplayMetrics(); 110 | displayWidth = metrics.widthPixels; 111 | displayHeight = (int) (metrics.heightPixels - 25 * metrics.density); 112 | 113 | // create the window contents 114 | View content; 115 | FrameLayout body; 116 | 117 | if (Utils.isSet(flags, StandOutFlags.FLAG_DECORATION_SYSTEM)) { 118 | // requested system window decorations 119 | content = getSystemDecorations(); 120 | body = (FrameLayout) content.findViewById(R.id.body); 121 | } else { 122 | // did not request decorations. will provide own implementation 123 | content = new FrameLayout(context); 124 | content.setId(R.id.content); 125 | body = (FrameLayout) content; 126 | } 127 | 128 | addView(content); 129 | 130 | body.setOnTouchListener(new OnTouchListener() { 131 | 132 | @Override 133 | public boolean onTouch(View v, MotionEvent event) { 134 | // pass all touch events to the implementation 135 | boolean consumed = false; 136 | 137 | // handle move and bring to front 138 | consumed = context.onTouchHandleMove(id, Window.this, v, event) 139 | || consumed; 140 | 141 | // alert implementation 142 | consumed = context.onTouchBody(id, Window.this, v, event) 143 | || consumed; 144 | 145 | return consumed; 146 | } 147 | }); 148 | 149 | // attach the view corresponding to the id from the 150 | // implementation 151 | context.createAndAttachView(id, body); 152 | 153 | // make sure the implementation attached the view 154 | if (body.getChildCount() == 0) { 155 | throw new RuntimeException( 156 | "You must attach your view to the given frame in createAndAttachView()"); 157 | } 158 | 159 | // implement StandOut specific workarounds 160 | if (!Utils.isSet(flags, 161 | StandOutFlags.FLAG_FIX_COMPATIBILITY_ALL_DISABLE)) { 162 | fixCompatibility(body); 163 | } 164 | // implement StandOut specific additional functionality 165 | if (!Utils.isSet(flags, 166 | StandOutFlags.FLAG_ADD_FUNCTIONALITY_ALL_DISABLE)) { 167 | addFunctionality(body); 168 | } 169 | 170 | // attach the existing tag from the frame to the window 171 | setTag(body.getTag()); 172 | } 173 | 174 | @Override 175 | public boolean onInterceptTouchEvent(MotionEvent event) { 176 | StandOutLayoutParams params = getLayoutParams(); 177 | 178 | // focus window 179 | if (event.getAction() == MotionEvent.ACTION_DOWN) { 180 | if (mContext.getFocusedWindow() != this) { 181 | mContext.focus(id); 182 | } 183 | } 184 | 185 | // multitouch 186 | if (event.getPointerCount() >= 2 187 | && Utils.isSet(flags, 188 | StandOutFlags.FLAG_WINDOW_PINCH_RESIZE_ENABLE) 189 | && (event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_DOWN) { 190 | touchInfo.scale = 1; 191 | touchInfo.dist = -1; 192 | touchInfo.firstWidth = params.width; 193 | touchInfo.firstHeight = params.height; 194 | return true; 195 | } 196 | 197 | return false; 198 | } 199 | 200 | @Override 201 | public boolean onTouchEvent(MotionEvent event) { 202 | // handle touching outside 203 | switch (event.getAction()) { 204 | case MotionEvent.ACTION_OUTSIDE: 205 | // unfocus window 206 | if (mContext.getFocusedWindow() == this) { 207 | mContext.unfocus(this); 208 | } 209 | 210 | // notify implementation that ACTION_OUTSIDE occurred 211 | mContext.onTouchBody(id, this, this, event); 212 | break; 213 | } 214 | 215 | // handle multitouch 216 | if (event.getPointerCount() >= 2 217 | && Utils.isSet(flags, 218 | StandOutFlags.FLAG_WINDOW_PINCH_RESIZE_ENABLE)) { 219 | // 2 fingers or more 220 | 221 | float x0 = event.getX(0); 222 | float y0 = event.getY(0); 223 | float x1 = event.getX(1); 224 | float y1 = event.getY(1); 225 | 226 | double dist = Math 227 | .sqrt(Math.pow(x0 - x1, 2) + Math.pow(y0 - y1, 2)); 228 | 229 | switch (event.getAction() & MotionEvent.ACTION_MASK) { 230 | case MotionEvent.ACTION_MOVE: 231 | if (touchInfo.dist == -1) { 232 | touchInfo.dist = dist; 233 | } 234 | touchInfo.scale *= dist / touchInfo.dist; 235 | touchInfo.dist = dist; 236 | 237 | // scale the window with anchor point set to middle 238 | edit().setAnchorPoint(.5f, .5f) 239 | .setSize( 240 | (int) (touchInfo.firstWidth * touchInfo.scale), 241 | (int) (touchInfo.firstHeight * touchInfo.scale)) 242 | .commit(); 243 | break; 244 | } 245 | mContext.onResize(id, this, this, event); 246 | } 247 | 248 | return true; 249 | } 250 | 251 | @Override 252 | public boolean dispatchKeyEvent(KeyEvent event) { 253 | if (mContext.onKeyEvent(id, this, event)) { 254 | Log.d(TAG, "Window " + id + " key event " + event 255 | + " cancelled by implementation."); 256 | return false; 257 | } 258 | 259 | if (event.getAction() == KeyEvent.ACTION_UP) { 260 | switch (event.getKeyCode()) { 261 | case KeyEvent.KEYCODE_BACK: 262 | mContext.unfocus(this); 263 | return true; 264 | } 265 | } 266 | 267 | return super.dispatchKeyEvent(event); 268 | } 269 | 270 | @Override 271 | protected void onConfigurationChanged(Configuration newConfig) { 272 | super.onConfigurationChanged(newConfig); 273 | DisplayMetrics metrics = mContext.getResources() 274 | .getDisplayMetrics(); 275 | displayWidth = metrics.widthPixels; 276 | displayHeight = (int) (metrics.heightPixels - 25 * metrics.density); 277 | int width = (int) (getWidth() * (metrics.widthPixels * 1.0 / metrics.heightPixels)); 278 | int height = (int) (getHeight() * (metrics.heightPixels * 1.0 / metrics.widthPixels)); 279 | edit().setSize(width, height, false).commit(); 280 | } 281 | 282 | /** 283 | * Request or remove the focus from this window. 284 | * 285 | * @param focus 286 | * Whether we want to gain or lose focus. 287 | * @return True if focus changed successfully, false if it failed. 288 | */ 289 | public boolean onFocus(boolean focus) { 290 | if (!Utils.isSet(flags, StandOutFlags.FLAG_WINDOW_FOCUSABLE_DISABLE)) { 291 | // window is focusable 292 | 293 | if (focus == focused) { 294 | // window already focused/unfocused 295 | return false; 296 | } 297 | 298 | focused = focus; 299 | 300 | // alert callbacks and cancel if instructed 301 | if (mContext.onFocusChange(id, this, focus)) { 302 | Log.d(TAG, "Window " + id + " focus change " 303 | + (focus ? "(true)" : "(false)") 304 | + " cancelled by implementation."); 305 | focused = !focus; 306 | return false; 307 | } 308 | 309 | if (!Utils.isSet(flags, 310 | StandOutFlags.FLAG_WINDOW_FOCUS_INDICATOR_DISABLE)) { 311 | // change visual state 312 | View content = findViewById(R.id.content); 313 | if (focus) { 314 | // gaining focus 315 | content.setBackgroundResource(R.drawable.border_focused); 316 | } else { 317 | // losing focus 318 | if (Utils 319 | .isSet(flags, StandOutFlags.FLAG_DECORATION_SYSTEM)) { 320 | // system decorations 321 | content.setBackgroundResource(R.drawable.border); 322 | } else { 323 | // no decorations 324 | content.setBackgroundResource(0); 325 | } 326 | } 327 | } 328 | 329 | // set window manager params 330 | StandOutLayoutParams params = getLayoutParams(); 331 | params.setFocusFlag(focus); 332 | mContext.updateViewLayout(id, params); 333 | 334 | if (focus) { 335 | mContext.setFocusedWindow(this); 336 | } else { 337 | if (mContext.getFocusedWindow() == this) { 338 | mContext.setFocusedWindow(null); 339 | } 340 | } 341 | 342 | return true; 343 | } 344 | return false; 345 | } 346 | 347 | @Override 348 | public void setLayoutParams(ViewGroup.LayoutParams params) { 349 | if (params instanceof StandOutLayoutParams) { 350 | super.setLayoutParams(params); 351 | } else { 352 | throw new IllegalArgumentException( 353 | "Window" 354 | + id 355 | + ": LayoutParams must be an instance of StandOutLayoutParams."); 356 | } 357 | } 358 | 359 | /** 360 | * Convenience method to start editting the size and position of this 361 | * window. Make sure you call {@link Editor#commit()} when you are done to 362 | * update the window. 363 | * 364 | * @return The Editor associated with this window. 365 | */ 366 | public Editor edit() { 367 | return new Editor(); 368 | } 369 | 370 | @Override 371 | public StandOutLayoutParams getLayoutParams() { 372 | StandOutLayoutParams params = (StandOutLayoutParams) super 373 | .getLayoutParams(); 374 | if (params == null) { 375 | params = originalParams; 376 | } 377 | return params; 378 | } 379 | 380 | /** 381 | * Returns the system window decorations if the implementation sets 382 | * {@link #FLAG_DECORATION_SYSTEM}. 383 | * 384 | *

385 | * The system window decorations support hiding, closing, moving, and 386 | * resizing. 387 | * 388 | * @return The frame view containing the system window decorations. 389 | */ 390 | private View getSystemDecorations() { 391 | final View decorations = mLayoutInflater.inflate( 392 | R.layout.system_window_decorators, this, false); 393 | 394 | // icon 395 | final ImageView icon = (ImageView) decorations 396 | .findViewById(R.id.window_icon); 397 | icon.setImageResource(mContext.getAppIcon()); 398 | icon.setOnClickListener(new OnClickListener() { 399 | 400 | @Override 401 | public void onClick(View v) { 402 | PopupWindow dropDown = mContext.getDropDown(id); 403 | if (dropDown != null) { 404 | dropDown.showAsDropDown(icon); 405 | } 406 | } 407 | }); 408 | 409 | // title 410 | TextView title = (TextView) decorations.findViewById(R.id.title); 411 | title.setText(mContext.getTitle(id)); 412 | 413 | // hide 414 | View hide = decorations.findViewById(R.id.hide); 415 | hide.setOnClickListener(new OnClickListener() { 416 | 417 | @Override 418 | public void onClick(View v) { 419 | mContext.hide(id); 420 | } 421 | }); 422 | hide.setVisibility(View.GONE); 423 | 424 | // maximize 425 | View maximize = decorations.findViewById(R.id.maximize); 426 | maximize.setOnClickListener(new OnClickListener() { 427 | 428 | @Override 429 | public void onClick(View v) { 430 | StandOutLayoutParams params = getLayoutParams(); 431 | boolean isMaximized = data 432 | .getBoolean(WindowDataKeys.IS_MAXIMIZED); 433 | if (isMaximized && params.width == displayWidth 434 | && params.height == displayHeight && params.x == 0 435 | && params.y == 0) { 436 | data.putBoolean(WindowDataKeys.IS_MAXIMIZED, false); 437 | int oldWidth = data.getInt( 438 | WindowDataKeys.WIDTH_BEFORE_MAXIMIZE, -1); 439 | int oldHeight = data.getInt( 440 | WindowDataKeys.HEIGHT_BEFORE_MAXIMIZE, -1); 441 | int oldX = data 442 | .getInt(WindowDataKeys.X_BEFORE_MAXIMIZE, -1); 443 | int oldY = data 444 | .getInt(WindowDataKeys.Y_BEFORE_MAXIMIZE, -1); 445 | edit().setSize(oldWidth, oldHeight).setPosition(oldX, oldY) 446 | .commit(); 447 | } else { 448 | data.putBoolean(WindowDataKeys.IS_MAXIMIZED, true); 449 | data.putInt(WindowDataKeys.WIDTH_BEFORE_MAXIMIZE, 450 | params.width); 451 | data.putInt(WindowDataKeys.HEIGHT_BEFORE_MAXIMIZE, 452 | params.height); 453 | data.putInt(WindowDataKeys.X_BEFORE_MAXIMIZE, params.x); 454 | data.putInt(WindowDataKeys.Y_BEFORE_MAXIMIZE, params.y); 455 | edit().setSize(1f, 1f).setPosition(0, 0).commit(); 456 | } 457 | } 458 | }); 459 | 460 | // close 461 | View close = decorations.findViewById(R.id.close); 462 | close.setOnClickListener(new OnClickListener() { 463 | 464 | @Override 465 | public void onClick(View v) { 466 | mContext.close(id); 467 | } 468 | }); 469 | 470 | // move 471 | View titlebar = decorations.findViewById(R.id.titlebar); 472 | titlebar.setOnTouchListener(new OnTouchListener() { 473 | 474 | @Override 475 | public boolean onTouch(View v, MotionEvent event) { 476 | // handle dragging to move 477 | boolean consumed = mContext.onTouchHandleMove(id, Window.this, 478 | v, event); 479 | return consumed; 480 | } 481 | }); 482 | 483 | // resize 484 | View corner = decorations.findViewById(R.id.corner); 485 | corner.setOnTouchListener(new OnTouchListener() { 486 | 487 | @Override 488 | public boolean onTouch(View v, MotionEvent event) { 489 | // handle dragging to move 490 | boolean consumed = mContext.onTouchHandleResize(id, 491 | Window.this, v, event); 492 | 493 | return consumed; 494 | } 495 | }); 496 | 497 | // set window appearance and behavior based on flags 498 | if (Utils.isSet(flags, StandOutFlags.FLAG_WINDOW_HIDE_ENABLE)) { 499 | hide.setVisibility(View.VISIBLE); 500 | } 501 | if (Utils.isSet(flags, StandOutFlags.FLAG_DECORATION_MAXIMIZE_DISABLE)) { 502 | maximize.setVisibility(View.GONE); 503 | } 504 | if (Utils.isSet(flags, StandOutFlags.FLAG_DECORATION_CLOSE_DISABLE)) { 505 | close.setVisibility(View.GONE); 506 | } 507 | if (Utils.isSet(flags, StandOutFlags.FLAG_DECORATION_MOVE_DISABLE)) { 508 | titlebar.setOnTouchListener(null); 509 | } 510 | if (Utils.isSet(flags, StandOutFlags.FLAG_DECORATION_RESIZE_DISABLE)) { 511 | corner.setVisibility(View.GONE); 512 | } 513 | 514 | return decorations; 515 | } 516 | 517 | /** 518 | * Implement StandOut specific additional functionalities. 519 | * 520 | *

521 | * Currently, this method does the following: 522 | * 523 | *

524 | * Attach resize handles: For every View found to have id R.id.corner, 525 | * attach an OnTouchListener that implements resizing the window. 526 | * 527 | * @param root 528 | * The view hierarchy that is part of the window. 529 | */ 530 | void addFunctionality(View root) { 531 | // corner for resize 532 | if (!Utils.isSet(flags, 533 | StandOutFlags.FLAG_ADD_FUNCTIONALITY_RESIZE_DISABLE)) { 534 | View corner = root.findViewById(R.id.corner); 535 | if (corner != null) { 536 | corner.setOnTouchListener(new OnTouchListener() { 537 | 538 | @Override 539 | public boolean onTouch(View v, MotionEvent event) { 540 | // handle dragging to move 541 | boolean consumed = mContext.onTouchHandleResize(id, 542 | Window.this, v, event); 543 | 544 | return consumed; 545 | } 546 | }); 547 | } 548 | } 549 | 550 | // window_icon for drop down 551 | if (!Utils.isSet(flags, 552 | StandOutFlags.FLAG_ADD_FUNCTIONALITY_DROP_DOWN_DISABLE)) { 553 | final View icon = root.findViewById(R.id.window_icon); 554 | if (icon != null) { 555 | icon.setOnClickListener(new OnClickListener() { 556 | 557 | @Override 558 | public void onClick(View v) { 559 | PopupWindow dropDown = mContext.getDropDown(id); 560 | if (dropDown != null) { 561 | dropDown.showAsDropDown(icon); 562 | } 563 | } 564 | }); 565 | } 566 | } 567 | } 568 | 569 | /** 570 | * Iterate through each View in the view hiearchy and implement StandOut 571 | * specific compatibility workarounds. 572 | * 573 | *

574 | * Currently, this method does the following: 575 | * 576 | *

577 | * Nothing yet. 578 | * 579 | * @param root 580 | * The root view hierarchy to iterate through and check. 581 | */ 582 | void fixCompatibility(View root) { 583 | Queue queue = new LinkedList(); 584 | queue.add(root); 585 | 586 | View view = null; 587 | while ((view = queue.poll()) != null) { 588 | // do nothing yet 589 | 590 | // iterate through children 591 | if (view instanceof ViewGroup) { 592 | ViewGroup group = (ViewGroup) view; 593 | for (int i = 0; i < group.getChildCount(); i++) { 594 | queue.add(group.getChildAt(i)); 595 | } 596 | } 597 | } 598 | } 599 | 600 | /** 601 | * Convenient way to resize or reposition a Window. The Editor allows you to 602 | * easily resize and reposition the window around anchor points. 603 | * 604 | * @author Mark Wei 605 | * 606 | */ 607 | public class Editor { 608 | /** 609 | * Special value for width, height, x, or y positions that represents 610 | * that the value should not be changed. 611 | */ 612 | public static final int UNCHANGED = Integer.MIN_VALUE; 613 | 614 | /** 615 | * Layout params of the window associated with this Editor. 616 | */ 617 | StandOutLayoutParams mParams; 618 | 619 | /** 620 | * The position of the anchor point as a percentage of the window's 621 | * width/height. The anchor point is only used by the {@link Editor}. 622 | * 623 | *

624 | * The anchor point effects the following methods: 625 | * 626 | *

627 | * {@link #setSize(float, float)}, {@link #setSize(int, int)}, 628 | * {@link #setPosition(int, int)}, {@link #setPosition(int, int)}. 629 | * 630 | * The window will move, expand, or shrink around the anchor point. 631 | * 632 | *

633 | * Values must be between 0 and 1, inclusive. 0 means the left/top, 0.5 634 | * is the center, 1 is the right/bottom. 635 | */ 636 | float anchorX, anchorY; 637 | 638 | public Editor() { 639 | mParams = getLayoutParams(); 640 | anchorX = anchorY = 0; 641 | } 642 | 643 | public Editor setAnchorPoint(float x, float y) { 644 | if (x < 0 || x > 1 || y < 0 || y > 1) { 645 | throw new IllegalArgumentException( 646 | "Anchor point must be between 0 and 1, inclusive."); 647 | } 648 | 649 | anchorX = x; 650 | anchorY = y; 651 | 652 | return this; 653 | } 654 | 655 | /** 656 | * Set the size of this window as percentages of max screen size. The 657 | * window will expand and shrink around the top-left corner, unless 658 | * you've set a different anchor point with 659 | * {@link #setAnchorPoint(float, float)}. 660 | * 661 | * Changes will not applied until you {@link #commit()}. 662 | * 663 | * @param percentWidth 664 | * @param percentHeight 665 | * @return The same Editor, useful for method chaining. 666 | */ 667 | public Editor setSize(float percentWidth, float percentHeight) { 668 | return setSize((int) (displayWidth * percentWidth), 669 | (int) (displayHeight * percentHeight)); 670 | } 671 | 672 | /** 673 | * Set the size of this window in absolute pixels. The window will 674 | * expand and shrink around the top-left corner, unless you've set a 675 | * different anchor point with {@link #setAnchorPoint(float, float)}. 676 | * 677 | * Changes will not applied until you {@link #commit()}. 678 | * 679 | * @param width 680 | * @param height 681 | * @return The same Editor, useful for method chaining. 682 | */ 683 | public Editor setSize(int width, int height) { 684 | return setSize(width, height, false); 685 | } 686 | 687 | /** 688 | * Set the size of this window in absolute pixels. The window will 689 | * expand and shrink around the top-left corner, unless you've set a 690 | * different anchor point with {@link #setAnchorPoint(float, float)}. 691 | * 692 | * Changes will not applied until you {@link #commit()}. 693 | * 694 | * @param width 695 | * @param height 696 | * @param skip 697 | * Don't call {@link #setPosition(int, int)} to avoid stack 698 | * overflow. 699 | * @return The same Editor, useful for method chaining. 700 | */ 701 | private Editor setSize(int width, int height, boolean skip) { 702 | if (mParams != null) { 703 | if (anchorX < 0 || anchorX > 1 || anchorY < 0 || anchorY > 1) { 704 | throw new IllegalStateException( 705 | "Anchor point must be between 0 and 1, inclusive."); 706 | } 707 | 708 | int lastWidth = mParams.width; 709 | int lastHeight = mParams.height; 710 | 711 | if (width != UNCHANGED) { 712 | mParams.width = width; 713 | } 714 | if (height != UNCHANGED) { 715 | mParams.height = height; 716 | } 717 | 718 | // set max width/height 719 | int maxWidth = mParams.maxWidth; 720 | int maxHeight = mParams.maxHeight; 721 | 722 | if (Utils.isSet(flags, 723 | StandOutFlags.FLAG_WINDOW_EDGE_LIMITS_ENABLE)) { 724 | maxWidth = (int) Math.min(maxWidth, displayWidth); 725 | maxHeight = (int) Math.min(maxHeight, displayHeight); 726 | } 727 | 728 | // keep window between min and max 729 | mParams.width = Math.min( 730 | Math.max(mParams.width, mParams.minWidth), maxWidth); 731 | mParams.height = Math.min( 732 | Math.max(mParams.height, mParams.minHeight), maxHeight); 733 | 734 | // keep window in aspect ratio 735 | if (Utils.isSet(flags, 736 | StandOutFlags.FLAG_WINDOW_ASPECT_RATIO_ENABLE)) { 737 | int ratioWidth = (int) (mParams.height * touchInfo.ratio); 738 | int ratioHeight = (int) (mParams.width / touchInfo.ratio); 739 | if (ratioHeight >= mParams.minHeight 740 | && ratioHeight <= mParams.maxHeight) { 741 | // width good adjust height 742 | mParams.height = ratioHeight; 743 | } else { 744 | // height good adjust width 745 | mParams.width = ratioWidth; 746 | } 747 | } 748 | 749 | if (!skip) { 750 | // set position based on anchor point 751 | setPosition((int) (mParams.x + lastWidth * anchorX), 752 | (int) (mParams.y + lastHeight * anchorY)); 753 | } 754 | } 755 | 756 | return this; 757 | } 758 | 759 | /** 760 | * Set the position of this window as percentages of max screen size. 761 | * The window's top-left corner will be positioned at the given x and y, 762 | * unless you've set a different anchor point with 763 | * {@link #setAnchorPoint(float, float)}. 764 | * 765 | * Changes will not applied until you {@link #commit()}. 766 | * 767 | * @param percentWidth 768 | * @param percentHeight 769 | * @return The same Editor, useful for method chaining. 770 | */ 771 | public Editor setPosition(float percentWidth, float percentHeight) { 772 | return setPosition((int) (displayWidth * percentWidth), 773 | (int) (displayHeight * percentHeight)); 774 | } 775 | 776 | /** 777 | * Set the position of this window in absolute pixels. The window's 778 | * top-left corner will be positioned at the given x and y, unless 779 | * you've set a different anchor point with 780 | * {@link #setAnchorPoint(float, float)}. 781 | * 782 | * Changes will not applied until you {@link #commit()}. 783 | * 784 | * @param x 785 | * @param y 786 | * @return The same Editor, useful for method chaining. 787 | */ 788 | public Editor setPosition(int x, int y) { 789 | return setPosition(x, y, false); 790 | } 791 | 792 | /** 793 | * Set the position of this window in absolute pixels. The window's 794 | * top-left corner will be positioned at the given x and y, unless 795 | * you've set a different anchor point with 796 | * {@link #setAnchorPoint(float, float)}. 797 | * 798 | * Changes will not applied until you {@link #commit()}. 799 | * 800 | * @param x 801 | * @param y 802 | * @param skip 803 | * Don't call {@link #setPosition(int, int)} and 804 | * {@link #setSize(int, int)} to avoid stack overflow. 805 | * @return The same Editor, useful for method chaining. 806 | */ 807 | private Editor setPosition(int x, int y, boolean skip) { 808 | if (mParams != null) { 809 | if (anchorX < 0 || anchorX > 1 || anchorY < 0 || anchorY > 1) { 810 | throw new IllegalStateException( 811 | "Anchor point must be between 0 and 1, inclusive."); 812 | } 813 | 814 | // sets the x and y correctly according to anchorX and 815 | // anchorY 816 | if (x != UNCHANGED) { 817 | mParams.x = (int) (x - mParams.width * anchorX); 818 | } 819 | if (y != UNCHANGED) { 820 | mParams.y = (int) (y - mParams.height * anchorY); 821 | } 822 | 823 | if (Utils.isSet(flags, 824 | StandOutFlags.FLAG_WINDOW_EDGE_LIMITS_ENABLE)) { 825 | // if gravity is not TOP|LEFT throw exception 826 | if (mParams.gravity != (Gravity.TOP | Gravity.START)) { 827 | throw new IllegalStateException( 828 | "The window " 829 | + id 830 | + " gravity must be TOP|LEFT if FLAG_WINDOW_EDGE_LIMITS_ENABLE or FLAG_WINDOW_EDGE_TILE_ENABLE is set."); 831 | } 832 | 833 | // keep window inside edges 834 | mParams.x = Math.min(Math.max(mParams.x, 0), displayWidth 835 | - mParams.width); 836 | mParams.y = Math.min(Math.max(mParams.y, 0), displayHeight 837 | - mParams.height); 838 | } 839 | } 840 | 841 | return this; 842 | } 843 | 844 | /** 845 | * Commit the changes to this window. Updates the layout. This Editor 846 | * cannot be used after you commit. 847 | */ 848 | public void commit() { 849 | if (mParams != null) { 850 | mContext.updateViewLayout(id, mParams); 851 | mParams = null; 852 | } 853 | } 854 | } 855 | 856 | public static class WindowDataKeys { 857 | public static final String IS_MAXIMIZED = "isMaximized"; 858 | public static final String WIDTH_BEFORE_MAXIMIZE = "widthBeforeMaximize"; 859 | public static final String HEIGHT_BEFORE_MAXIMIZE = "heightBeforeMaximize"; 860 | public static final String X_BEFORE_MAXIMIZE = "xBeforeMaximize"; 861 | public static final String Y_BEFORE_MAXIMIZE = "yBeforeMaximize"; 862 | } 863 | } -------------------------------------------------------------------------------- /src/main/java/wei/mark/standout/StandOutWindow.java: -------------------------------------------------------------------------------- 1 | package wei.mark.standout; 2 | 3 | import android.app.ActivityManager; 4 | import android.app.Notification; 5 | import android.app.NotificationChannel; 6 | import android.app.NotificationManager; 7 | import android.app.PendingIntent; 8 | import android.app.Service; 9 | import android.content.Context; 10 | import android.content.Intent; 11 | import android.content.SharedPreferences; 12 | import android.graphics.PixelFormat; 13 | import android.graphics.drawable.Drawable; 14 | import android.net.Uri; 15 | import android.os.Build; 16 | import android.os.Bundle; 17 | import android.os.IBinder; 18 | import android.preference.PreferenceManager; 19 | import android.util.DisplayMetrics; 20 | import android.util.Log; 21 | import android.view.Gravity; 22 | import android.view.KeyEvent; 23 | import android.view.LayoutInflater; 24 | import android.view.MotionEvent; 25 | import android.view.View; 26 | import android.view.View.OnClickListener; 27 | import android.view.ViewGroup; 28 | import android.view.WindowManager; 29 | import android.view.animation.Animation; 30 | import android.view.animation.Animation.AnimationListener; 31 | import android.view.animation.AnimationUtils; 32 | import android.widget.FrameLayout; 33 | import android.widget.ImageView; 34 | import android.widget.LinearLayout; 35 | import android.widget.PopupWindow; 36 | import android.widget.TextView; 37 | 38 | import java.util.ArrayList; 39 | import java.util.LinkedList; 40 | import java.util.List; 41 | import java.util.Set; 42 | 43 | import wei.mark.standout.constants.StandOutFlags; 44 | import wei.mark.standout.ui.Window; 45 | 46 | /** 47 | * Extend this class to easily create and manage floating StandOut windows. 48 | * 49 | * @author Mark Wei 50 | * 51 | * Contributors: Jason 52 | */ 53 | public abstract class StandOutWindow extends Service { 54 | static final String TAG = "StandOutWindow"; 55 | 56 | /** 57 | * StandOut window id: You may use this sample id for your first window. 58 | */ 59 | public static final int DEFAULT_ID = 0; 60 | 61 | /** 62 | * Special StandOut window id: You may NOT use this id for any windows. 63 | */ 64 | public static final int ONGOING_NOTIFICATION_ID = -1; 65 | 66 | /** 67 | * StandOut window id: You may use this id when you want it to be 68 | * disregarded. The system makes no distinction for this id; it is only used 69 | * to improve code readability. 70 | */ 71 | public static final int DISREGARD_ID = -2; 72 | 73 | /** 74 | * Intent action: Show a new window corresponding to the id. 75 | */ 76 | public static final String ACTION_SHOW = "SHOW"; 77 | 78 | /** 79 | * Intent action: Restore a previously hidden window corresponding to the 80 | * id. The window should be previously hidden with {@link #ACTION_HIDE}. 81 | */ 82 | public static final String ACTION_RESTORE = "RESTORE"; 83 | 84 | /** 85 | * Intent action: Close an existing window with an existing id. 86 | */ 87 | public static final String ACTION_CLOSE = "CLOSE"; 88 | 89 | /** 90 | * Intent action: Close all existing windows. 91 | */ 92 | public static final String ACTION_CLOSE_ALL = "CLOSE_ALL"; 93 | 94 | /** 95 | * Intent action: Send data to a new or existing window. 96 | */ 97 | public static final String ACTION_SEND_DATA = "SEND_DATA"; 98 | 99 | /** 100 | * Intent action: Hide an existing window with an existing id. To enable the 101 | * ability to restore this window, make sure you implement 102 | * {@link #getHiddenNotification(int)}. 103 | */ 104 | public static final String ACTION_HIDE = "HIDE"; 105 | 106 | /** 107 | * Show a new window corresponding to the id, or restore a previously hidden 108 | * window. 109 | * 110 | * @param context A Context of the application package implementing this class. 111 | * @param cls The Service extending {@link StandOutWindow} that will be used 112 | * to create and manage the window. 113 | * @param id The id representing this window. If the id exists, and the 114 | * corresponding window was previously hidden, then that window 115 | * will be restored. 116 | * @see #show(int) 117 | */ 118 | public static void show(Context context, 119 | Class cls, int id) { 120 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 121 | context.startForegroundService(getShowIntent(context, cls, id)); 122 | } else { 123 | context.startService(getShowIntent(context, cls, id)); 124 | } 125 | } 126 | 127 | /** 128 | * Hide the existing window corresponding to the id. To enable the ability 129 | * to restore this window, make sure you implement 130 | * {@link #getHiddenNotification(int)}. 131 | * 132 | * @param context A Context of the application package implementing this class. 133 | * @param cls The Service extending {@link StandOutWindow} that is managing 134 | * the window. 135 | * @param id The id representing this window. The window must previously be 136 | * shown. 137 | * @see #hide(int) 138 | */ 139 | public static void hide(Context context, 140 | Class cls, int id) { 141 | context.startService(getHideIntent(context, cls, id)); 142 | } 143 | 144 | /** 145 | * Close an existing window with an existing id. 146 | * 147 | * @param context A Context of the application package implementing this class. 148 | * @param cls The Service extending {@link StandOutWindow} that is managing 149 | * the window. 150 | * @param id The id representing this window. The window must previously be 151 | * shown. 152 | * @see #close(int) 153 | */ 154 | public static void close(Context context, 155 | Class cls, int id) { 156 | context.startService(getCloseIntent(context, cls, id)); 157 | } 158 | 159 | /** 160 | * Close all existing windows. 161 | * 162 | * @param context A Context of the application package implementing this class. 163 | * @param cls The Service extending {@link StandOutWindow} that is managing 164 | * the window. 165 | * @see #closeAll() 166 | */ 167 | public static void closeAll(Context context, 168 | Class cls) { 169 | ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); 170 | for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { 171 | if (cls.getName().equals(service.service.getClassName())) { 172 | context.startService(getCloseAllIntent(context, cls)); 173 | Log.d(TAG, "closeAll: service is running"); 174 | return; 175 | } 176 | } 177 | Log.d(TAG, "closeAll: service is not running"); 178 | } 179 | 180 | /** 181 | * This allows windows of different applications to communicate with each 182 | * other. 183 | * 184 | *

185 | * Send {@link Parceleable} data in a {@link Bundle} to a new or existing 186 | * windows. The implementation of the recipient window can handle what to do 187 | * with the data. To receive a result, provide the class and id of the 188 | * sender. 189 | * 190 | * @param context A Context of the application package implementing the class of 191 | * the sending window. 192 | * @param toCls The Service's class extending {@link StandOutWindow} that is 193 | * managing the receiving window. 194 | * @param toId The id of the receiving window, or DISREGARD_ID. 195 | * @param requestCode Provide a request code to declare what kind of data is being 196 | * sent. 197 | * @param data A bundle of parceleable data to be sent to the receiving 198 | * window. 199 | * @param fromCls Provide the class of the sending window if you want a result. 200 | * @param fromId Provide the id of the sending window if you want a result. 201 | * @see #sendData(int, Class, int, int, Bundle) 202 | */ 203 | public static void sendData(Context context, 204 | Class toCls, int toId, int requestCode, 205 | Bundle data, Class fromCls, int fromId) { 206 | context.startService(getSendDataIntent(context, toCls, toId, 207 | requestCode, data, fromCls, fromId)); 208 | } 209 | 210 | /** 211 | * See {@link #show(Context, Class, int)}. 212 | * 213 | * @param context A Context of the application package implementing this class. 214 | * @param cls The Service extending {@link StandOutWindow} that will be used 215 | * to create and manage the window. 216 | * @param id The id representing this window. If the id exists, and the 217 | * corresponding window was previously hidden, then that window 218 | * will be restored. 219 | * @return An {@link Intent} to use with 220 | * {@link Context#startService(Intent)}. 221 | */ 222 | public static Intent getShowIntent(Context context, 223 | Class cls, int id) { 224 | boolean cached = sWindowCache.isCached(id, cls); 225 | String action = cached ? ACTION_RESTORE : ACTION_SHOW; 226 | Uri uri = cached ? Uri.parse("standout://" + cls + '/' + id) : null; 227 | return new Intent(context, cls).putExtra("id", id).setAction(action) 228 | .setData(uri); 229 | } 230 | 231 | /** 232 | * See {@link #hide(Context, Class, int)}. 233 | * 234 | * @param context A Context of the application package implementing this class. 235 | * @param cls The Service extending {@link StandOutWindow} that is managing 236 | * the window. 237 | * @param id The id representing this window. If the id exists, and the 238 | * corresponding window was previously hidden, then that window 239 | * will be restored. 240 | * @return An {@link Intent} to use with 241 | * {@link Context#startService(Intent)}. 242 | */ 243 | public static Intent getHideIntent(Context context, 244 | Class cls, int id) { 245 | return new Intent(context, cls).putExtra("id", id).setAction( 246 | ACTION_HIDE); 247 | } 248 | 249 | /** 250 | * See {@link #close(Context, Class, int)}. 251 | * 252 | * @param context A Context of the application package implementing this class. 253 | * @param cls The Service extending {@link StandOutWindow} that is managing 254 | * the window. 255 | * @param id The id representing this window. If the id exists, and the 256 | * corresponding window was previously hidden, then that window 257 | * will be restored. 258 | * @return An {@link Intent} to use with 259 | * {@link Context#startService(Intent)}. 260 | */ 261 | public static Intent getCloseIntent(Context context, 262 | Class cls, int id) { 263 | return new Intent(context, cls).putExtra("id", id).setAction( 264 | ACTION_CLOSE); 265 | } 266 | 267 | /** 268 | * See {@link #closeAll(Context, Class, int)}. 269 | * 270 | * @param context A Context of the application package implementing this class. 271 | * @param cls The Service extending {@link StandOutWindow} that is managing 272 | * the window. 273 | * @return An {@link Intent} to use with 274 | * {@link Context#startService(Intent)}. 275 | */ 276 | public static Intent getCloseAllIntent(Context context, 277 | Class cls) { 278 | return new Intent(context, cls).setAction(ACTION_CLOSE_ALL); 279 | } 280 | 281 | /** 282 | * See {@link #sendData(Context, Class, int, int, Bundle, Class, int)}. 283 | * 284 | * @param context A Context of the application package implementing the class of 285 | * the sending window. 286 | * @param toCls The Service's class extending {@link StandOutWindow} that is 287 | * managing the receiving window. 288 | * @param toId The id of the receiving window. 289 | * @param requestCode Provide a request code to declare what kind of data is being 290 | * sent. 291 | * @param data A bundle of parceleable data to be sent to the receiving 292 | * window. 293 | * @param fromCls If the sending window wants a result, provide the class of the 294 | * sending window. 295 | * @param fromId If the sending window wants a result, provide the id of the 296 | * sending window. 297 | * @return An {@link Intnet} to use with 298 | * {@link Context#startService(Intent)}. 299 | */ 300 | public static Intent getSendDataIntent(Context context, 301 | Class toCls, int toId, int requestCode, 302 | Bundle data, Class fromCls, int fromId) { 303 | return new Intent(context, toCls).putExtra("id", toId) 304 | .putExtra("requestCode", requestCode) 305 | .putExtra("wei.mark.standout.data", data) 306 | .putExtra("wei.mark.standout.fromCls", fromCls) 307 | .putExtra("fromId", fromId).setAction(ACTION_SEND_DATA); 308 | } 309 | 310 | // internal map of ids to shown/hidden views 311 | static WindowCache sWindowCache; 312 | static Window sFocusedWindow; 313 | 314 | // static constructors 315 | static { 316 | sWindowCache = new WindowCache(); 317 | sFocusedWindow = null; 318 | } 319 | 320 | // internal system services 321 | WindowManager mWindowManager; 322 | private NotificationManager mNotificationManager; 323 | LayoutInflater mLayoutInflater; 324 | 325 | // internal state variables 326 | private boolean startedForeground; 327 | 328 | private SharedPreferences pref; 329 | private String disableNotifyKey; 330 | private boolean disableNotify = false; 331 | 332 | @Override 333 | public IBinder onBind(Intent intent) { 334 | return null; 335 | } 336 | 337 | @Override 338 | public void onCreate() { 339 | super.onCreate(); 340 | Log.d(TAG, "onCreate"); 341 | 342 | mWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE); 343 | mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); 344 | mLayoutInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); 345 | 346 | startedForeground = false; 347 | pref = PreferenceManager.getDefaultSharedPreferences(this); 348 | disableNotifyKey = getString(R.string.disable_notify_key); 349 | } 350 | 351 | @Override 352 | public int onStartCommand(Intent intent, int flags, int startId) { 353 | super.onStartCommand(intent, flags, startId); 354 | Log.d(TAG, "onStartCommand: " + intent); 355 | 356 | disableNotify = pref.getBoolean(disableNotifyKey, false) && 357 | (Build.VERSION.SDK_INT < Build.VERSION_CODES.O); 358 | 359 | // intent should be created with 360 | // getShowIntent(), getHideIntent(), getCloseIntent() 361 | if (intent != null) { 362 | String action = intent.getAction(); 363 | int id = intent.getIntExtra("id", DEFAULT_ID); 364 | 365 | // this will interfere with getPersistentNotification() 366 | if (id == ONGOING_NOTIFICATION_ID) { 367 | throw new RuntimeException( 368 | "ID cannot equals StandOutWindow.ONGOING_NOTIFICATION_ID"); 369 | } 370 | 371 | if (ACTION_SHOW.equals(action) || ACTION_RESTORE.equals(action)) { 372 | show(id); 373 | } else if (ACTION_HIDE.equals(action)) { 374 | hide(id); 375 | } else if (ACTION_CLOSE.equals(action)) { 376 | close(id); 377 | } else if (ACTION_CLOSE_ALL.equals(action)) { 378 | closeAll(); 379 | } else if (ACTION_SEND_DATA.equals(action)) { 380 | if (!isExistingId(id) && id != DISREGARD_ID) { 381 | Log.w(TAG, 382 | "Sending data to non-existant window. If this is not intended, make sure toId is either an existing window's id or DISREGARD_ID."); 383 | } 384 | Bundle data = intent.getBundleExtra("wei.mark.standout.data"); 385 | int requestCode = intent.getIntExtra("requestCode", 0); 386 | @SuppressWarnings("unchecked") 387 | Class fromCls = (Class) intent 388 | .getSerializableExtra("wei.mark.standout.fromCls"); 389 | int fromId = intent.getIntExtra("fromId", DEFAULT_ID); 390 | onReceiveData(id, requestCode, data, fromCls, fromId); 391 | } 392 | } else { 393 | Log.w(TAG, "Tried to onStartCommand() with a null intent."); 394 | } 395 | 396 | // the service is started in foreground in show() 397 | // so we don't expect Android to kill this service 398 | return START_NOT_STICKY; 399 | } 400 | 401 | @Override 402 | public void onDestroy() { 403 | super.onDestroy(); 404 | 405 | Log.d(TAG, "onDestroy"); 406 | // closes all windows 407 | closeAll(); 408 | } 409 | 410 | /** 411 | * Return the name of every window in this implementation. The name will 412 | * appear in the default implementations of the system window decoration 413 | * title and notification titles. 414 | * 415 | * @return The name. 416 | */ 417 | public abstract String getAppName(); 418 | 419 | /** 420 | * Return the icon resource for every window in this implementation. The 421 | * icon will appear in the default implementations of the system window 422 | * decoration and notifications. 423 | * 424 | * @return The icon. 425 | */ 426 | public abstract int getAppIcon(); 427 | 428 | /** 429 | * Create a new {@link View} corresponding to the id, and add it as a child 430 | * to the frame. The view will become the contents of this StandOut window. 431 | * The view MUST be newly created, and you MUST attach it to the frame. 432 | * 433 | *

434 | * If you are inflating your view from XML, make sure you use 435 | * {@link LayoutInflater#inflate(int, ViewGroup, boolean)} to attach your 436 | * view to frame. Set the ViewGroup to be frame, and the boolean to true. 437 | * 438 | *

439 | * If you are creating your view programmatically, make sure you use 440 | * {@link FrameLayout#addView(View)} to add your view to the frame. 441 | * 442 | * @param id The id representing the window. 443 | * @param frame The {@link FrameLayout} to attach your view as a child to. 444 | */ 445 | public abstract void createAndAttachView(int id, FrameLayout frame); 446 | 447 | /** 448 | * Return the {@link StandOutWindow#LayoutParams} for the corresponding id. 449 | * The system will set the layout params on the view for this StandOut 450 | * window. The layout params may be reused. 451 | * 452 | * @param id The id of the window. 453 | * @param window The window corresponding to the id. Given as courtesy, so you 454 | * may get the existing layout params. 455 | * @return The {@link StandOutWindow#LayoutParams} corresponding to the id. 456 | * The layout params will be set on the window. The layout params 457 | * returned will be reused whenever possible, minimizing the number 458 | * of times getParams() will be called. 459 | */ 460 | public abstract StandOutLayoutParams getParams(int id, Window window); 461 | 462 | /** 463 | * Implement this method to change modify the behavior and appearance of the 464 | * window corresponding to the id. 465 | * 466 | *

467 | * You may use any of the flags defined in {@link StandOutFlags}. This 468 | * method will be called many times, so keep it fast. 469 | * 470 | *

471 | * Use bitwise OR (|) to set flags, and bitwise XOR (^) to unset flags. To 472 | * test if a flag is set, use {@link Utils#isSet(int, int)}. 473 | * 474 | * @param id The id of the window. 475 | * @return A combination of flags. 476 | */ 477 | public int getFlags(int id) { 478 | return 0; 479 | } 480 | 481 | /** 482 | * Implement this method to set a custom title for the window corresponding 483 | * to the id. 484 | * 485 | * @param id The id of the window. 486 | * @return The title of the window. 487 | */ 488 | public String getTitle(int id) { 489 | return getAppName(); 490 | } 491 | 492 | /** 493 | * Implement this method to set a custom icon for the window corresponding 494 | * to the id. 495 | * 496 | * @param id The id of the window. 497 | * @return The icon of the window. 498 | */ 499 | public int getIcon(int id) { 500 | return getAppIcon(); 501 | } 502 | 503 | /** 504 | * Return the title for the persistent notification. This is called every 505 | * time {@link #show(int)} is called. 506 | * 507 | * @param id The id of the window shown. 508 | * @return The title for the persistent notification. 509 | */ 510 | public String getPersistentNotificationTitle(int id) { 511 | return getAppName() + " Running"; 512 | } 513 | 514 | /** 515 | * Return the message for the persistent notification. This is called every 516 | * time {@link #show(int)} is called. 517 | * 518 | * @param id The id of the window shown. 519 | * @return The message for the persistent notification. 520 | */ 521 | public String getPersistentNotificationMessage(int id) { 522 | return ""; 523 | } 524 | 525 | /** 526 | * Return the intent for the persistent notification. This is called every 527 | * time {@link #show(int)} is called. 528 | * 529 | *

530 | * The returned intent will be packaged into a {@link PendingIntent} to be 531 | * invoked when the user clicks the notification. 532 | * 533 | * @param id The id of the window shown. 534 | * @return The intent for the persistent notification. 535 | */ 536 | public Intent getPersistentNotificationIntent(int id) { 537 | return null; 538 | } 539 | 540 | /** 541 | * Return the icon resource for every hidden window in this implementation. 542 | * The icon will appear in the default implementations of the hidden 543 | * notifications. 544 | * 545 | * @return The icon. 546 | */ 547 | public int getHiddenIcon() { 548 | return getAppIcon(); 549 | } 550 | 551 | /** 552 | * Return the title for the hidden notification corresponding to the window 553 | * being hidden. 554 | * 555 | * @param id The id of the hidden window. 556 | * @return The title for the hidden notification. 557 | */ 558 | public String getHiddenNotificationTitle(int id) { 559 | return getAppName() + " Hidden"; 560 | } 561 | 562 | /** 563 | * Return the message for the hidden notification corresponding to the 564 | * window being hidden. 565 | * 566 | * @param id The id of the hidden window. 567 | * @return The message for the hidden notification. 568 | */ 569 | public String getHiddenNotificationMessage(int id) { 570 | return ""; 571 | } 572 | 573 | /** 574 | * Return the intent for the hidden notification corresponding to the window 575 | * being hidden. 576 | * 577 | *

578 | * The returned intent will be packaged into a {@link PendingIntent} to be 579 | * invoked when the user clicks the notification. 580 | * 581 | * @param id The id of the hidden window. 582 | * @return The intent for the hidden notification. 583 | */ 584 | public Intent getHiddenNotificationIntent(int id) { 585 | return null; 586 | } 587 | 588 | /** 589 | * Return a persistent {@link Notification} for the corresponding id. You 590 | * must return a notification for AT LEAST the first id to be requested. 591 | * Once the persistent notification is shown, further calls to 592 | * {@link #getPersistentNotification(int)} may return null. This way Android 593 | * can start the StandOut window service in the foreground and will not kill 594 | * the service on low memory. 595 | * 596 | *

597 | * As a courtesy, the system will request a notification for every new id 598 | * shown. Your implementation is encouraged to include the 599 | * {@link PendingIntent#FLAG_UPDATE_CURRENT} flag in the notification so 600 | * that there is only one system-wide persistent notification. 601 | * 602 | *

603 | * See the StandOutExample project for an implementation of 604 | * {@link #getPersistentNotification(int)} that keeps one system-wide 605 | * persistent notification that creates a new window on every click. 606 | * 607 | * @param id The id of the window. 608 | * @return The {@link Notification} corresponding to the id, or null if 609 | * you've previously returned a notification. 610 | */ 611 | public Notification getPersistentNotification(int id) { 612 | // basic notification stuff 613 | // http://developer.android.com/guide/topics/ui/notifiers/notifications.html 614 | int icon = getAppIcon(); 615 | long when = System.currentTimeMillis(); 616 | Context c = getApplicationContext(); 617 | String contentTitle = getPersistentNotificationTitle(id); 618 | String contentText = getPersistentNotificationMessage(id); 619 | String tickerText = String.format("%s: %s", contentTitle, contentText); 620 | 621 | // getPersistentNotification() is called for every new window 622 | // so we replace the old notification with a new one that has 623 | // a bigger id 624 | Intent notificationIntent = getPersistentNotificationIntent(id); 625 | 626 | PendingIntent contentIntent = null; 627 | 628 | if (notificationIntent != null) { 629 | contentIntent = PendingIntent.getService(this, 0, 630 | notificationIntent, 631 | // flag updates existing persistent notification 632 | PendingIntent.FLAG_UPDATE_CURRENT); 633 | } 634 | 635 | Notification.Builder builder = new Notification.Builder(c) 636 | .setSmallIcon(icon) 637 | .setTicker(tickerText) 638 | .setWhen(when) 639 | .setContentTitle(contentTitle) 640 | .setContentText(contentText) 641 | .setSmallIcon(getAppIcon()) 642 | .setContentIntent(contentIntent); 643 | 644 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 645 | 646 | NotificationManager nm = ((NotificationManager) getSystemService( 647 | Context.NOTIFICATION_SERVICE)); 648 | if (nm != null) { 649 | String CHANNEL_ID = getPackageName() + "." + getClass().getSimpleName(); 650 | NotificationChannel channel = new NotificationChannel(CHANNEL_ID, getAppName(), 651 | NotificationManager.IMPORTANCE_LOW); 652 | nm.createNotificationChannel(channel); 653 | builder.setChannelId(CHANNEL_ID); 654 | } 655 | } 656 | 657 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 658 | return builder.build(); 659 | } else { 660 | //noinspection deprecation 661 | return builder.getNotification(); 662 | } 663 | } 664 | 665 | /** 666 | * Return a hidden {@link Notification} for the corresponding id. The system 667 | * will request a notification for every id that is hidden. 668 | * 669 | *

670 | * If null is returned, StandOut will assume you do not wish to support 671 | * hiding this window, and will {@link #close(int)} it for you. 672 | * 673 | *

674 | * See the StandOutExample project for an implementation of 675 | * {@link #getHiddenNotification(int)} that for every hidden window keeps a 676 | * notification which restores that window upon user's click. 677 | * 678 | * @param id The id of the window. 679 | * @return The {@link Notification} corresponding to the id or null. 680 | */ 681 | public Notification getHiddenNotification(int id) { 682 | // same basics as getPersistentNotification() 683 | int icon = getHiddenIcon(); 684 | long when = System.currentTimeMillis(); 685 | Context c = getApplicationContext(); 686 | String contentTitle = getHiddenNotificationTitle(id); 687 | String contentText = getHiddenNotificationMessage(id); 688 | String tickerText = String.format("%s: %s", contentTitle, contentText); 689 | 690 | // the difference here is we are providing the same id 691 | Intent notificationIntent = getHiddenNotificationIntent(id); 692 | 693 | PendingIntent contentIntent = null; 694 | 695 | if (notificationIntent != null) { 696 | contentIntent = PendingIntent.getService(this, 0, 697 | notificationIntent, 698 | // flag updates existing persistent notification 699 | PendingIntent.FLAG_UPDATE_CURRENT); 700 | } 701 | 702 | Notification.Builder builder = new Notification.Builder(c) 703 | .setSmallIcon(icon) 704 | .setTicker(tickerText) 705 | .setWhen(when) 706 | .setContentTitle(contentTitle) 707 | .setContentText(contentText) 708 | .setContentIntent(contentIntent); 709 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 710 | return builder.build(); 711 | } else { 712 | //noinspection deprecation 713 | return builder.getNotification(); 714 | } 715 | } 716 | 717 | /** 718 | * Return the animation to play when the window corresponding to the id is 719 | * shown. 720 | * 721 | * @param id The id of the window. 722 | * @return The animation to play or null. 723 | */ 724 | public Animation getShowAnimation(int id) { 725 | return AnimationUtils.loadAnimation(this, android.R.anim.fade_in); 726 | } 727 | 728 | /** 729 | * Return the animation to play when the window corresponding to the id is 730 | * hidden. 731 | * 732 | * @param id The id of the window. 733 | * @return The animation to play or null. 734 | */ 735 | public Animation getHideAnimation(int id) { 736 | return AnimationUtils.loadAnimation(this, android.R.anim.fade_out); 737 | } 738 | 739 | /** 740 | * Return the animation to play when the window corresponding to the id is 741 | * closed. 742 | * 743 | * @param id The id of the window. 744 | * @return The animation to play or null. 745 | */ 746 | public Animation getCloseAnimation(int id) { 747 | return AnimationUtils.loadAnimation(this, android.R.anim.fade_out); 748 | } 749 | 750 | /** 751 | * Implement this method to set a custom theme for all windows in this 752 | * implementation. 753 | * 754 | * @return The theme to set on the window, or 0 for device default. 755 | */ 756 | public int getThemeStyle() { 757 | return 0; 758 | } 759 | 760 | /** 761 | * You probably want to leave this method alone and implement 762 | * {@link #getDropDownItems(int)} instead. Only implement this method if you 763 | * want more control over the drop down menu. 764 | * 765 | *

766 | * Implement this method to set a custom drop down menu when the user clicks 767 | * on the icon of the window corresponding to the id. The icon is only shown 768 | * when {@link StandOutFlags#FLAG_DECORATION_SYSTEM} is set. 769 | * 770 | * @param id The id of the window. 771 | * @return The drop down menu to be anchored to the icon, or null to have no 772 | * dropdown menu. 773 | */ 774 | public PopupWindow getDropDown(final int id) { 775 | final List items; 776 | 777 | List dropDownListItems = getDropDownItems(id); 778 | if (dropDownListItems != null) { 779 | items = dropDownListItems; 780 | } else { 781 | items = new ArrayList(); 782 | } 783 | 784 | // add default drop down items 785 | items.add(new DropDownListItem( 786 | android.R.drawable.ic_menu_close_clear_cancel, "Quit " 787 | + getAppName(), new Runnable() { 788 | 789 | @Override 790 | public void run() { 791 | closeAll(); 792 | } 793 | })); 794 | 795 | // turn item list into views in PopupWindow 796 | LinearLayout list = new LinearLayout(this); 797 | list.setOrientation(LinearLayout.VERTICAL); 798 | 799 | final PopupWindow dropDown = new PopupWindow(list, 800 | StandOutLayoutParams.WRAP_CONTENT, 801 | StandOutLayoutParams.WRAP_CONTENT, true); 802 | 803 | for (final DropDownListItem item : items) { 804 | ViewGroup listItem = (ViewGroup) mLayoutInflater.inflate( 805 | R.layout.drop_down_list_item, list, false); 806 | list.addView(listItem); 807 | 808 | ImageView icon = (ImageView) listItem.findViewById(R.id.icon); 809 | icon.setImageResource(item.icon); 810 | 811 | TextView description = (TextView) listItem 812 | .findViewById(R.id.description); 813 | description.setText(item.description); 814 | 815 | listItem.setOnClickListener(new OnClickListener() { 816 | 817 | @Override 818 | public void onClick(View v) { 819 | item.action.run(); 820 | dropDown.dismiss(); 821 | } 822 | }); 823 | } 824 | 825 | Drawable background; 826 | 827 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 828 | background = getResources().getDrawable( 829 | android.R.drawable.editbox_dropdown_dark_frame, getTheme()); 830 | } else { 831 | //noinspection deprecation 832 | background = getResources().getDrawable( 833 | android.R.drawable.editbox_dropdown_dark_frame); 834 | } 835 | 836 | dropDown.setBackgroundDrawable(background); 837 | return dropDown; 838 | } 839 | 840 | /** 841 | * Implement this method to populate the drop down menu when the user clicks 842 | * on the icon of the window corresponding to the id. The icon is only shown 843 | * when {@link StandOutFlags#FLAG_DECORATION_SYSTEM} is set. 844 | * 845 | * @param id The id of the window. 846 | * @return The list of items to show in the drop down menu, or null or empty 847 | * to have no dropdown menu. 848 | */ 849 | public List getDropDownItems(int id) { 850 | return null; 851 | } 852 | 853 | /** 854 | * Implement this method to be alerted to touch events in the body of the 855 | * window corresponding to the id. 856 | * 857 | *

858 | * Note that even if you set {@link #FLAG_DECORATION_SYSTEM}, you will not 859 | * receive touch events from the system window decorations. 860 | * 861 | * @param id The id of the view, provided as a courtesy. 862 | * @param window The window corresponding to the id, provided as a courtesy. 863 | * @param view The view where the event originated from. 864 | * @param event See linked method. 865 | * @see {@link View.OnTouchListener#onTouch(View, MotionEvent)} 866 | */ 867 | public boolean onTouchBody(int id, Window window, View view, 868 | MotionEvent event) { 869 | return false; 870 | } 871 | 872 | /** 873 | * Implement this method to be alerted to when the window corresponding to 874 | * the id is moved. 875 | * 876 | * @param id The id of the view, provided as a courtesy. 877 | * @param window The window corresponding to the id, provided as a courtesy. 878 | * @param view The view where the event originated from. 879 | * @param event See linked method. 880 | * @see {@link #onTouchHandleMove(int, Window, View, MotionEvent)} 881 | */ 882 | public void onMove(int id, Window window, View view, MotionEvent event) { 883 | } 884 | 885 | /** 886 | * Implement this method to be alerted to when the window corresponding to 887 | * the id is resized. 888 | * 889 | * @param id The id of the view, provided as a courtesy. 890 | * @param window The window corresponding to the id, provided as a courtesy. 891 | * @param view The view where the event originated from. 892 | * @param event See linked method. 893 | * @see {@link #onTouchHandleResize(int, Window, View, MotionEvent)} 894 | */ 895 | public void onResize(int id, Window window, View view, MotionEvent event) { 896 | } 897 | 898 | /** 899 | * Implement this callback to be alerted when a window corresponding to the 900 | * id is about to be shown. This callback will occur before the view is 901 | * added to the window manager. 902 | * 903 | * @param id The id of the view, provided as a courtesy. 904 | * @param view The view about to be shown. 905 | * @return Return true to cancel the view from being shown, or false to 906 | * continue. 907 | * @see #show(int) 908 | */ 909 | public boolean onShow(int id, Window window) { 910 | return false; 911 | } 912 | 913 | /** 914 | * Implement this callback to be alerted when a window corresponding to the 915 | * id is about to be hidden. This callback will occur before the view is 916 | * removed from the window manager and {@link #getHiddenNotification(int)} 917 | * is called. 918 | * 919 | * @param id The id of the view, provided as a courtesy. 920 | * @param view The view about to be hidden. 921 | * @return Return true to cancel the view from being hidden, or false to 922 | * continue. 923 | * @see #hide(int) 924 | */ 925 | public boolean onHide(int id, Window window) { 926 | return false; 927 | } 928 | 929 | /** 930 | * Implement this callback to be alerted when a window corresponding to the 931 | * id is about to be closed. This callback will occur before the view is 932 | * removed from the window manager. 933 | * 934 | * @param id The id of the view, provided as a courtesy. 935 | * @param view The view about to be closed. 936 | * @return Return true to cancel the view from being closed, or false to 937 | * continue. 938 | * @see #close(int) 939 | */ 940 | public boolean onClose(int id, Window window) { 941 | return false; 942 | } 943 | 944 | /** 945 | * Implement this callback to be alerted when all windows are about to be 946 | * closed. This callback will occur before any views are removed from the 947 | * window manager. 948 | * 949 | * @return Return true to cancel the views from being closed, or false to 950 | * continue. 951 | * @see #closeAll() 952 | */ 953 | public boolean onCloseAll() { 954 | return false; 955 | } 956 | 957 | /** 958 | * Implement this callback to be alerted when a window corresponding to the 959 | * id has received some data. The sender is described by fromCls and fromId 960 | * if the sender wants a result. To send a result, use 961 | * {@link #sendData(int, Class, int, int, Bundle)}. 962 | * 963 | * @param id The id of your receiving window. 964 | * @param requestCode The sending window provided this request code to declare what 965 | * kind of data is being sent. 966 | * @param data A bundle of parceleable data that was sent to your receiving 967 | * window. 968 | * @param fromCls The sending window's class. Provided if the sender wants a 969 | * result. 970 | * @param fromId The sending window's id. Provided if the sender wants a 971 | * result. 972 | */ 973 | public void onReceiveData(int id, int requestCode, Bundle data, 974 | Class fromCls, int fromId) { 975 | } 976 | 977 | /** 978 | * Implement this callback to be alerted when a window corresponding to the 979 | * id is about to be updated in the layout. This callback will occur before 980 | * the view is updated by the window manager. 981 | * 982 | * @param id The id of the window, provided as a courtesy. 983 | * @param view The window about to be updated. 984 | * @param params The updated layout params. 985 | * @return Return true to cancel the window from being updated, or false to 986 | * continue. 987 | * @see #updateViewLayout(int, Window, StandOutLayoutParams) 988 | */ 989 | public boolean onUpdate(int id, Window window, StandOutLayoutParams params) { 990 | return false; 991 | } 992 | 993 | /** 994 | * Implement this callback to be alerted when a window corresponding to the 995 | * id is about to be bought to the front. This callback will occur before 996 | * the window is brought to the front by the window manager. 997 | * 998 | * @param id The id of the window, provided as a courtesy. 999 | * @param view The window about to be brought to the front. 1000 | * @return Return true to cancel the window from being brought to the front, 1001 | * or false to continue. 1002 | * @see #bringToFront(int) 1003 | */ 1004 | public boolean onBringToFront(int id, Window window) { 1005 | return false; 1006 | } 1007 | 1008 | /** 1009 | * Implement this callback to be alerted when a window corresponding to the 1010 | * id is about to have its focus changed. This callback will occur before 1011 | * the window's focus is changed. 1012 | * 1013 | * @param id The id of the window, provided as a courtesy. 1014 | * @param view The window about to be brought to the front. 1015 | * @param focus Whether the window is gaining or losing focus. 1016 | * @return Return true to cancel the window's focus from being changed, or 1017 | * false to continue. 1018 | * @see #focus(int) 1019 | */ 1020 | public boolean onFocusChange(int id, Window window, boolean focus) { 1021 | return false; 1022 | } 1023 | 1024 | /** 1025 | * Implement this callback to be alerted when a window corresponding to the 1026 | * id receives a key event. This callback will occur before the window 1027 | * handles the event with {@link Window#dispatchKeyEvent(KeyEvent)}. 1028 | * 1029 | * @param id The id of the window, provided as a courtesy. 1030 | * @param view The window about to receive the key event. 1031 | * @param event The key event. 1032 | * @return Return true to cancel the window from handling the key event, or 1033 | * false to let the window handle the key event. 1034 | * @see {@link Window#dispatchKeyEvent(KeyEvent)} 1035 | */ 1036 | public boolean onKeyEvent(int id, Window window, KeyEvent event) { 1037 | return false; 1038 | } 1039 | 1040 | private void tryRemoveView(View view) { 1041 | try { 1042 | mWindowManager.removeView(view); 1043 | } catch (Exception e) { 1044 | e.printStackTrace(); 1045 | } 1046 | } 1047 | 1048 | /** 1049 | * Show or restore a window corresponding to the id. Return the window that 1050 | * was shown/restored. 1051 | * 1052 | * @param id The id of the window. 1053 | * @return The window shown. 1054 | */ 1055 | public final synchronized Window show(int id) { 1056 | // get the window corresponding to the id 1057 | Window cachedWindow = getWindow(id); 1058 | final Window window; 1059 | 1060 | // check cache first 1061 | if (cachedWindow != null) { 1062 | window = cachedWindow; 1063 | } else { 1064 | window = new Window(this, id); 1065 | } 1066 | 1067 | // alert callbacks and cancel if instructed 1068 | if (onShow(id, window)) { 1069 | Log.d(TAG, "Window " + id + " show cancelled by implementation."); 1070 | return null; 1071 | } 1072 | 1073 | // focus an already shown window 1074 | if (window.visibility == Window.VISIBILITY_VISIBLE) { 1075 | Log.d(TAG, "Window " + id + " is already shown."); 1076 | focus(id); 1077 | return window; 1078 | } 1079 | 1080 | window.visibility = Window.VISIBILITY_VISIBLE; 1081 | 1082 | // get animation 1083 | Animation animation = getShowAnimation(id); 1084 | 1085 | // get the params corresponding to the id 1086 | StandOutLayoutParams params = window.getLayoutParams(); 1087 | 1088 | params.flags |= WindowManager.LayoutParams.FLAG_FULLSCREEN | 1089 | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD | 1090 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED | 1091 | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON; 1092 | 1093 | try { 1094 | tryRemoveView(window); 1095 | // add the view to the window manager 1096 | mWindowManager.addView(window, params); 1097 | 1098 | // animate 1099 | if (animation != null) { 1100 | window.getChildAt(0).startAnimation(animation); 1101 | } 1102 | 1103 | // add view to internal map 1104 | sWindowCache.putCache(id, getClass(), window); 1105 | } catch (Exception ex) { 1106 | ex.printStackTrace(); 1107 | } 1108 | 1109 | if (!disableNotify) { 1110 | // get the persistent notification 1111 | Notification notification = getPersistentNotification(id); 1112 | 1113 | // show the notification 1114 | if (notification != null) { 1115 | notification.flags = notification.flags 1116 | | Notification.FLAG_NO_CLEAR; 1117 | 1118 | // only show notification if not shown before 1119 | if (!startedForeground || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 1120 | // tell Android system to show notification 1121 | startForeground( 1122 | getClass().hashCode() + ONGOING_NOTIFICATION_ID, 1123 | notification); 1124 | startedForeground = true; 1125 | } else { 1126 | // update notification if shown before 1127 | mNotificationManager.notify(getClass().hashCode() 1128 | + ONGOING_NOTIFICATION_ID, notification); 1129 | } 1130 | } else { 1131 | // notification can only be null if it was provided before 1132 | if (!startedForeground) { 1133 | throw new RuntimeException("Your StandOutWindow service must" 1134 | + "provide a persistent notification." 1135 | + "The notification prevents Android" 1136 | + "from killing your service in low" 1137 | + "memory situations."); 1138 | } 1139 | } 1140 | } 1141 | 1142 | focus(id); 1143 | 1144 | return window; 1145 | } 1146 | 1147 | /** 1148 | * Hide a window corresponding to the id. Show a notification for the hidden 1149 | * window. 1150 | * 1151 | * @param id The id of the window. 1152 | */ 1153 | public final synchronized void hide(int id) { 1154 | // get the view corresponding to the id 1155 | final Window window = getWindow(id); 1156 | 1157 | if (window == null) { 1158 | Log.e(TAG, "Tried to hide(" + id 1159 | + ") a null window."); 1160 | return; 1161 | } 1162 | 1163 | // alert callbacks and cancel if instructed 1164 | if (onHide(id, window)) { 1165 | Log.d(TAG, "Window " + id + " hide cancelled by implementation."); 1166 | return; 1167 | } 1168 | 1169 | // ignore if window is already hidden 1170 | if (window.visibility == Window.VISIBILITY_GONE) { 1171 | Log.d(TAG, "Window " + id + " is already hidden."); 1172 | } 1173 | 1174 | // check if hide enabled 1175 | if (Utils.isSet(window.flags, StandOutFlags.FLAG_WINDOW_HIDE_ENABLE)) { 1176 | window.visibility = Window.VISIBILITY_TRANSITION; 1177 | 1178 | // get the hidden notification for this view 1179 | Notification notification = getHiddenNotification(id); 1180 | 1181 | // get animation 1182 | Animation animation = getHideAnimation(id); 1183 | 1184 | try { 1185 | // animate 1186 | if (animation != null) { 1187 | animation.setAnimationListener(new AnimationListener() { 1188 | 1189 | @Override 1190 | public void onAnimationStart(Animation animation) { 1191 | } 1192 | 1193 | @Override 1194 | public void onAnimationRepeat(Animation animation) { 1195 | } 1196 | 1197 | @Override 1198 | public void onAnimationEnd(Animation animation) { 1199 | // remove the window from the window manager 1200 | mWindowManager.removeView(window); 1201 | window.visibility = Window.VISIBILITY_GONE; 1202 | } 1203 | }); 1204 | window.getChildAt(0).startAnimation(animation); 1205 | } else { 1206 | // remove the window from the window manager 1207 | mWindowManager.removeView(window); 1208 | } 1209 | } catch (Exception ex) { 1210 | ex.printStackTrace(); 1211 | } 1212 | 1213 | if (!disableNotify) { 1214 | // display the notification 1215 | notification.flags = notification.flags 1216 | | Notification.FLAG_NO_CLEAR 1217 | | Notification.FLAG_AUTO_CANCEL; 1218 | 1219 | mNotificationManager.notify(getClass().hashCode() + id, 1220 | notification); 1221 | } 1222 | 1223 | } else { 1224 | // if hide not enabled, close window 1225 | close(id); 1226 | Log.d(TAG, "hide not enabled, close window"); 1227 | } 1228 | } 1229 | 1230 | /** 1231 | * Close a window corresponding to the id. 1232 | * 1233 | * @param id The id of the window. 1234 | */ 1235 | public final synchronized void close(final int id) { 1236 | // get the view corresponding to the id 1237 | final Window window = getWindow(id); 1238 | 1239 | if (window == null) { 1240 | Log.e(TAG, "Tried to close(" + id 1241 | + ") a null window."); 1242 | return; 1243 | } 1244 | 1245 | if (window.visibility == Window.VISIBILITY_TRANSITION) { 1246 | return; 1247 | } 1248 | 1249 | // alert callbacks and cancel if instructed 1250 | if (onClose(id, window)) { 1251 | Log.w(TAG, "Window " + id + " close cancelled by implementation."); 1252 | return; 1253 | } 1254 | 1255 | if (!disableNotify) { 1256 | // remove hidden notification 1257 | mNotificationManager.cancel(getClass().hashCode() + id); 1258 | } 1259 | 1260 | unfocus(window); 1261 | 1262 | window.visibility = Window.VISIBILITY_TRANSITION; 1263 | 1264 | // get animation 1265 | Animation animation = getCloseAnimation(id); 1266 | 1267 | // remove window 1268 | try { 1269 | // animate 1270 | if (animation != null) { 1271 | animation.setAnimationListener(new AnimationListener() { 1272 | 1273 | @Override 1274 | public void onAnimationStart(Animation animation) { 1275 | } 1276 | 1277 | @Override 1278 | public void onAnimationRepeat(Animation animation) { 1279 | } 1280 | 1281 | @Override 1282 | public void onAnimationEnd(Animation animation) { 1283 | // remove the window from the window manager 1284 | mWindowManager.removeView(window); 1285 | window.visibility = Window.VISIBILITY_GONE; 1286 | 1287 | // remove view from internal map 1288 | sWindowCache.removeCache(id, 1289 | StandOutWindow.this.getClass()); 1290 | 1291 | // if we just released the last window, quit 1292 | if (getExistingIds().size() == 0) { 1293 | // tell Android to remove the persistent 1294 | // notification 1295 | // the Service will be shutdown by the system on low 1296 | // memory 1297 | startedForeground = false; 1298 | stopForeground(true); 1299 | } 1300 | } 1301 | }); 1302 | window.getChildAt(0).startAnimation(animation); 1303 | } else { 1304 | // remove the window from the window manager 1305 | mWindowManager.removeView(window); 1306 | 1307 | // remove view from internal map 1308 | sWindowCache.removeCache(id, getClass()); 1309 | 1310 | // if we just released the last window, quit 1311 | if (sWindowCache.getCacheSize(getClass()) == 0) { 1312 | // tell Android to remove the persistent notification 1313 | // the Service will be shutdown by the system on low memory 1314 | startedForeground = false; 1315 | stopForeground(true); 1316 | } 1317 | } 1318 | } catch (Exception ex) { 1319 | ex.printStackTrace(); 1320 | } 1321 | } 1322 | 1323 | /** 1324 | * Close all existing windows. 1325 | */ 1326 | public final synchronized void closeAll() { 1327 | // alert callbacks and cancel if instructed 1328 | if (onCloseAll()) { 1329 | Log.w(TAG, "Windows close all cancelled by implementation."); 1330 | return; 1331 | } 1332 | 1333 | // add ids to temporary set to avoid concurrent modification 1334 | LinkedList ids = new LinkedList(); 1335 | for (int id : getExistingIds()) { 1336 | ids.add(id); 1337 | } 1338 | 1339 | // close each window 1340 | for (int id : ids) { 1341 | close(id); 1342 | } 1343 | stopForeground(true); 1344 | } 1345 | 1346 | /** 1347 | * Send {@link Parceleable} data in a {@link Bundle} to a new or existing 1348 | * windows. The implementation of the recipient window can handle what to do 1349 | * with the data. To receive a result, provide the id of the sender. 1350 | * 1351 | * @param fromId Provide the id of the sending window if you want a result. 1352 | * @param toCls The Service's class extending {@link StandOutWindow} that is 1353 | * managing the receiving window. 1354 | * @param toId The id of the receiving window. 1355 | * @param requestCode Provide a request code to declare what kind of data is being 1356 | * sent. 1357 | * @param data A bundle of parceleable data to be sent to the receiving 1358 | * window. 1359 | */ 1360 | public final void sendData(int fromId, 1361 | Class toCls, int toId, int requestCode, 1362 | Bundle data) { 1363 | StandOutWindow.sendData(this, toCls, toId, requestCode, data, 1364 | getClass(), fromId); 1365 | } 1366 | 1367 | /** 1368 | * Bring the window corresponding to this id in front of all other windows. 1369 | * The window may flicker as it is removed and restored by the system. 1370 | * 1371 | * @param id The id of the window to bring to the front. 1372 | */ 1373 | public final synchronized void bringToFront(int id) { 1374 | Window window = getWindow(id); 1375 | if (window == null) { 1376 | Log.e(TAG, "Tried to bringToFront(" + id 1377 | + ") a null window."); 1378 | return; 1379 | } 1380 | 1381 | if (window.visibility == Window.VISIBILITY_GONE) { 1382 | Log.e(TAG, "Tried to bringToFront(" + id 1383 | + ") a window that is not shown."); 1384 | return; 1385 | } 1386 | 1387 | if (window.visibility == Window.VISIBILITY_TRANSITION) { 1388 | return; 1389 | } 1390 | 1391 | // alert callbacks and cancel if instructed 1392 | if (onBringToFront(id, window)) { 1393 | Log.w(TAG, "Window " + id 1394 | + " bring to front cancelled by implementation."); 1395 | return; 1396 | } 1397 | 1398 | StandOutLayoutParams params = window.getLayoutParams(); 1399 | 1400 | // remove from window manager then add back 1401 | try { 1402 | mWindowManager.removeView(window); 1403 | } catch (Exception ex) { 1404 | ex.printStackTrace(); 1405 | } 1406 | try { 1407 | mWindowManager.addView(window, params); 1408 | } catch (Exception ex) { 1409 | ex.printStackTrace(); 1410 | } 1411 | } 1412 | 1413 | /** 1414 | * Request focus for the window corresponding to this id. A maximum of one 1415 | * window can have focus, and that window will receive all key events, 1416 | * including Back and Menu. 1417 | * 1418 | * @param id The id of the window. 1419 | * @return True if focus changed successfully, false if it failed. 1420 | */ 1421 | public final synchronized boolean focus(int id) { 1422 | // check if that window is focusable 1423 | final Window window = getWindow(id); 1424 | if (window == null) { 1425 | Log.e(TAG, "Tried to focus(" + id 1426 | + ") a null window."); 1427 | return false; 1428 | } 1429 | 1430 | if (!Utils.isSet(window.flags, 1431 | StandOutFlags.FLAG_WINDOW_FOCUSABLE_DISABLE)) { 1432 | // remove focus from previously focused window 1433 | if (sFocusedWindow != null) { 1434 | unfocus(sFocusedWindow); 1435 | } 1436 | 1437 | return window.onFocus(true); 1438 | } 1439 | 1440 | return false; 1441 | } 1442 | 1443 | /** 1444 | * Remove focus for the window corresponding to this id. Once a window is 1445 | * unfocused, it will stop receiving key events. 1446 | * 1447 | * @param id The id of the window. 1448 | * @return True if focus changed successfully, false if it failed. 1449 | */ 1450 | public final synchronized boolean unfocus(int id) { 1451 | Window window = getWindow(id); 1452 | return unfocus(window); 1453 | } 1454 | 1455 | /** 1456 | * Courtesy method for your implementation to use if you want to. Gets a 1457 | * unique id to assign to a new window. 1458 | * 1459 | * @return The unique id. 1460 | */ 1461 | public final int getUniqueId() { 1462 | int unique = DEFAULT_ID; 1463 | for (int id : getExistingIds()) { 1464 | unique = Math.max(unique, id + 1); 1465 | } 1466 | return unique; 1467 | } 1468 | 1469 | /** 1470 | * Return whether the window corresponding to the id exists. This is useful 1471 | * for testing if the id is being restored (return true) or shown for the 1472 | * first time (return false). 1473 | * 1474 | * @param id The id of the window. 1475 | * @return True if the window corresponding to the id is either shown or 1476 | * hidden, or false if it has never been shown or was previously 1477 | * closed. 1478 | */ 1479 | public final boolean isExistingId(int id) { 1480 | return sWindowCache.isCached(id, getClass()); 1481 | } 1482 | 1483 | /** 1484 | * Return the ids of all shown or hidden windows. 1485 | * 1486 | * @return A set of ids, or an empty set. 1487 | */ 1488 | public final Set getExistingIds() { 1489 | return sWindowCache.getCacheIds(getClass()); 1490 | } 1491 | 1492 | /** 1493 | * Return the window corresponding to the id, if it exists in cache. The 1494 | * window will not be created with 1495 | * {@link #createAndAttachView(int, ViewGroup)}. This means the returned 1496 | * value will be null if the window is not shown or hidden. 1497 | * 1498 | * @param id The id of the window. 1499 | * @return The window if it is shown/hidden, or null if it is closed. 1500 | */ 1501 | public final Window getWindow(int id) { 1502 | return sWindowCache.getCache(id, getClass()); 1503 | } 1504 | 1505 | /** 1506 | * Return the window that currently has focus. 1507 | * 1508 | * @return The window that has focus. 1509 | */ 1510 | public final Window getFocusedWindow() { 1511 | return sFocusedWindow; 1512 | } 1513 | 1514 | /** 1515 | * Sets the window that currently has focus. 1516 | */ 1517 | public final void setFocusedWindow(Window window) { 1518 | sFocusedWindow = window; 1519 | } 1520 | 1521 | /** 1522 | * Change the title of the window, if such a title exists. A title exists if 1523 | * {@link StandOutFlags#FLAG_DECORATION_SYSTEM} is set, or if your own view 1524 | * contains a TextView with id R.id.title. 1525 | * 1526 | * @param id The id of the window. 1527 | * @param text The new title. 1528 | */ 1529 | public final void setTitle(int id, String text) { 1530 | Window window = getWindow(id); 1531 | if (window != null) { 1532 | View title = window.findViewById(R.id.title); 1533 | if (title instanceof TextView) { 1534 | ((TextView) title).setText(text); 1535 | } 1536 | } 1537 | } 1538 | 1539 | /** 1540 | * Change the icon of the window, if such a icon exists. A icon exists if 1541 | * {@link StandOutFlags#FLAG_DECORATION_SYSTEM} is set, or if your own view 1542 | * contains a TextView with id R.id.window_icon. 1543 | * 1544 | * @param id The id of the window. 1545 | * @param drawableRes The new icon. 1546 | */ 1547 | public final void setIcon(int id, int drawableRes) { 1548 | Window window = getWindow(id); 1549 | if (window != null) { 1550 | View icon = window.findViewById(R.id.window_icon); 1551 | if (icon instanceof ImageView) { 1552 | ((ImageView) icon).setImageResource(drawableRes); 1553 | } 1554 | } 1555 | } 1556 | 1557 | public abstract boolean isDisableMove(int id); 1558 | 1559 | /** 1560 | * Internal touch handler for handling moving the window. 1561 | * 1562 | * @see {@link View#onTouchEvent(MotionEvent)} 1563 | */ 1564 | public boolean onTouchHandleMove(int id, Window window, View view, 1565 | MotionEvent event) { 1566 | if (isDisableMove(id)) { 1567 | return true; 1568 | } 1569 | StandOutLayoutParams params = window.getLayoutParams(); 1570 | 1571 | // how much you have to move in either direction in order for the 1572 | // gesture to be a move and not tap 1573 | 1574 | int totalDeltaX = window.touchInfo.lastX - window.touchInfo.firstX; 1575 | int totalDeltaY = window.touchInfo.lastY - window.touchInfo.firstY; 1576 | 1577 | switch (event.getAction()) { 1578 | case MotionEvent.ACTION_DOWN: 1579 | window.touchInfo.lastX = (int) event.getRawX(); 1580 | window.touchInfo.lastY = (int) event.getRawY(); 1581 | 1582 | window.touchInfo.firstX = window.touchInfo.lastX; 1583 | window.touchInfo.firstY = window.touchInfo.lastY; 1584 | break; 1585 | case MotionEvent.ACTION_MOVE: 1586 | int deltaX = (int) event.getRawX() - window.touchInfo.lastX; 1587 | int deltaY = (int) event.getRawY() - window.touchInfo.lastY; 1588 | 1589 | window.touchInfo.lastX = (int) event.getRawX(); 1590 | window.touchInfo.lastY = (int) event.getRawY(); 1591 | 1592 | if (window.touchInfo.moving 1593 | || Math.abs(totalDeltaX) >= params.threshold 1594 | || Math.abs(totalDeltaY) >= params.threshold) { 1595 | window.touchInfo.moving = true; 1596 | 1597 | // if window is moveable 1598 | if (Utils.isSet(window.flags, 1599 | StandOutFlags.FLAG_BODY_MOVE_ENABLE)) { 1600 | 1601 | // update the position of the window 1602 | if (event.getPointerCount() == 1) { 1603 | params.x += deltaX; 1604 | params.y += deltaY; 1605 | } 1606 | 1607 | window.edit().setPosition(params.x, params.y).commit(); 1608 | } 1609 | } 1610 | break; 1611 | case MotionEvent.ACTION_UP: 1612 | window.touchInfo.moving = false; 1613 | 1614 | if (event.getPointerCount() == 1) { 1615 | 1616 | // bring to front on tap 1617 | boolean tap = Math.abs(totalDeltaX) < params.threshold 1618 | && Math.abs(totalDeltaY) < params.threshold; 1619 | if (tap 1620 | && Utils.isSet( 1621 | window.flags, 1622 | StandOutFlags.FLAG_WINDOW_BRING_TO_FRONT_ON_TAP)) { 1623 | StandOutWindow.this.bringToFront(id); 1624 | } 1625 | } 1626 | 1627 | // bring to front on touch 1628 | else if (Utils.isSet(window.flags, 1629 | StandOutFlags.FLAG_WINDOW_BRING_TO_FRONT_ON_TOUCH)) { 1630 | StandOutWindow.this.bringToFront(id); 1631 | } 1632 | 1633 | break; 1634 | } 1635 | 1636 | onMove(id, window, view, event); 1637 | 1638 | return true; 1639 | } 1640 | 1641 | /** 1642 | * Internal touch handler for handling resizing the window. 1643 | * 1644 | * @see {@link View#onTouchEvent(MotionEvent)} 1645 | */ 1646 | public boolean onTouchHandleResize(int id, Window window, View view, 1647 | MotionEvent event) { 1648 | StandOutLayoutParams params = (StandOutLayoutParams) window 1649 | .getLayoutParams(); 1650 | 1651 | switch (event.getAction()) { 1652 | case MotionEvent.ACTION_DOWN: 1653 | window.touchInfo.lastX = (int) event.getRawX(); 1654 | window.touchInfo.lastY = (int) event.getRawY(); 1655 | 1656 | window.touchInfo.firstX = window.touchInfo.lastX; 1657 | window.touchInfo.firstY = window.touchInfo.lastY; 1658 | break; 1659 | case MotionEvent.ACTION_MOVE: 1660 | int deltaX = (int) event.getRawX() - window.touchInfo.lastX; 1661 | int deltaY = (int) event.getRawY() - window.touchInfo.lastY; 1662 | 1663 | // update the size of the window 1664 | params.width += deltaX; 1665 | params.height += deltaY; 1666 | 1667 | // keep window between min/max width/height 1668 | if (params.width >= params.minWidth 1669 | && params.width <= params.maxWidth) { 1670 | window.touchInfo.lastX = (int) event.getRawX(); 1671 | } 1672 | 1673 | if (params.height >= params.minHeight 1674 | && params.height <= params.maxHeight) { 1675 | window.touchInfo.lastY = (int) event.getRawY(); 1676 | } 1677 | 1678 | window.edit().setSize(params.width, params.height).commit(); 1679 | break; 1680 | case MotionEvent.ACTION_UP: 1681 | break; 1682 | } 1683 | 1684 | onResize(id, window, view, event); 1685 | 1686 | return true; 1687 | } 1688 | 1689 | /** 1690 | * Remove focus for the window, which could belong to another application. 1691 | * Since we don't allow windows from different applications to directly 1692 | * interact with each other, except for 1693 | * {@link #sendData(Context, Class, int, int, Bundle, Class, int)}, this 1694 | * method is private. 1695 | * 1696 | * @param window The window to unfocus. 1697 | * @return True if focus changed successfully, false if it failed. 1698 | */ 1699 | public synchronized boolean unfocus(Window window) { 1700 | if (window == null) { 1701 | Log.e(TAG, 1702 | "Tried to unfocus a null window."); 1703 | return false; 1704 | } 1705 | return window.onFocus(false); 1706 | } 1707 | 1708 | /** 1709 | * Update the window corresponding to this id with the given params. 1710 | * 1711 | * @param id The id of the window. 1712 | * @param params The updated layout params to apply. 1713 | */ 1714 | public void updateViewLayout(int id, StandOutLayoutParams params) { 1715 | Window window = getWindow(id); 1716 | 1717 | if (window == null) { 1718 | Log.e(TAG, "Tried to updateViewLayout(" + id + ") a null window."); 1719 | return; 1720 | } 1721 | 1722 | if (window.visibility == Window.VISIBILITY_GONE) { 1723 | return; 1724 | } 1725 | 1726 | if (window.visibility == Window.VISIBILITY_TRANSITION) { 1727 | return; 1728 | } 1729 | 1730 | // alert callbacks and cancel if instructed 1731 | if (onUpdate(id, window, params)) { 1732 | Log.w(TAG, "Window " + id + " update cancelled by implementation."); 1733 | return; 1734 | } 1735 | 1736 | try { 1737 | window.setLayoutParams(params); 1738 | mWindowManager.updateViewLayout(window, params); 1739 | } catch (Exception ex) { 1740 | ex.printStackTrace(); 1741 | } 1742 | } 1743 | 1744 | /** 1745 | * LayoutParams specific to floating StandOut windows. 1746 | * 1747 | * @author Mark Wei 1748 | */ 1749 | public class StandOutLayoutParams extends WindowManager.LayoutParams { 1750 | /** 1751 | * Special value for x position that represents the left of the screen. 1752 | */ 1753 | public static final int LEFT = 0; 1754 | /** 1755 | * Special value for y position that represents the top of the screen. 1756 | */ 1757 | public static final int TOP = 0; 1758 | /** 1759 | * Special value for x position that represents the right of the screen. 1760 | */ 1761 | public static final int RIGHT = Integer.MAX_VALUE; 1762 | /** 1763 | * Special value for y position that represents the bottom of the 1764 | * screen. 1765 | */ 1766 | public static final int BOTTOM = Integer.MAX_VALUE; 1767 | /** 1768 | * Special value for x or y position that represents the center of the 1769 | * screen. 1770 | */ 1771 | public static final int CENTER = Integer.MIN_VALUE; 1772 | /** 1773 | * Special value for x or y position which requests that the system 1774 | * determine the position. 1775 | */ 1776 | public static final int AUTO_POSITION = Integer.MIN_VALUE + 1; 1777 | 1778 | /** 1779 | * The distance that distinguishes a tap from a drag. 1780 | */ 1781 | public int threshold; 1782 | 1783 | /** 1784 | * Optional constraints of the window. 1785 | */ 1786 | public int minWidth, minHeight, maxWidth, maxHeight; 1787 | 1788 | /** 1789 | * @param id The id of the window. 1790 | */ 1791 | public StandOutLayoutParams(int id) { 1792 | super(200, 200, getWindowType(), 1793 | StandOutLayoutParams.FLAG_NOT_TOUCH_MODAL 1794 | | StandOutLayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, 1795 | PixelFormat.TRANSLUCENT); 1796 | 1797 | int windowFlags = getFlags(id); 1798 | 1799 | setFocusFlag(false); 1800 | 1801 | if (!Utils.isSet(windowFlags, 1802 | StandOutFlags.FLAG_WINDOW_EDGE_LIMITS_ENABLE)) { 1803 | // windows may be moved beyond edges 1804 | flags |= FLAG_LAYOUT_NO_LIMITS; 1805 | } 1806 | 1807 | x = getX(id, width); 1808 | y = getY(id, height); 1809 | 1810 | gravity = Gravity.TOP | Gravity.START; 1811 | 1812 | threshold = 10; 1813 | minWidth = minHeight = 0; 1814 | maxWidth = maxHeight = Integer.MAX_VALUE; 1815 | } 1816 | 1817 | /** 1818 | * @param id The id of the window. 1819 | * @param w The width of the window. 1820 | * @param h The height of the window. 1821 | */ 1822 | public StandOutLayoutParams(int id, int w, int h) { 1823 | this(id); 1824 | width = w; 1825 | height = h; 1826 | } 1827 | 1828 | /** 1829 | * @param id The id of the window. 1830 | * @param w The width of the window. 1831 | * @param h The height of the window. 1832 | * @param xpos The x position of the window. 1833 | * @param ypos The y position of the window. 1834 | */ 1835 | public StandOutLayoutParams(int id, int w, int h, int xpos, int ypos) { 1836 | this(id, w, h); 1837 | 1838 | if (xpos != AUTO_POSITION) { 1839 | x = xpos; 1840 | } 1841 | if (ypos != AUTO_POSITION) { 1842 | y = ypos; 1843 | } 1844 | 1845 | DisplayMetrics displaymetrics = new DisplayMetrics(); 1846 | mWindowManager.getDefaultDisplay().getMetrics(displaymetrics); 1847 | int width = displaymetrics.widthPixels; 1848 | int height = displaymetrics.heightPixels; 1849 | 1850 | if (x == RIGHT) { 1851 | x = width - w; 1852 | } else if (x == CENTER) { 1853 | x = (width - w) / 2; 1854 | } 1855 | 1856 | if (y == BOTTOM) { 1857 | y = height - h; 1858 | } else if (y == CENTER) { 1859 | y = (height - h) / 2; 1860 | } 1861 | } 1862 | 1863 | /** 1864 | * @param id The id of the window. 1865 | * @param w The width of the window. 1866 | * @param h The height of the window. 1867 | * @param xpos The x position of the window. 1868 | * @param ypos The y position of the window. 1869 | * @param minWidth The minimum width of the window. 1870 | * @param minHeight The mininum height of the window. 1871 | */ 1872 | public StandOutLayoutParams(int id, int w, int h, int xpos, int ypos, 1873 | int minWidth, int minHeight) { 1874 | this(id, w, h, xpos, ypos); 1875 | 1876 | this.minWidth = minWidth; 1877 | this.minHeight = minHeight; 1878 | } 1879 | 1880 | /** 1881 | * @param id The id of the window. 1882 | * @param w The width of the window. 1883 | * @param h The height of the window. 1884 | * @param xpos The x position of the window. 1885 | * @param ypos The y position of the window. 1886 | * @param minWidth The minimum width of the window. 1887 | * @param minHeight The mininum height of the window. 1888 | * @param threshold The touch distance threshold that distinguishes a tap from 1889 | * a drag. 1890 | */ 1891 | public StandOutLayoutParams(int id, int w, int h, int xpos, int ypos, 1892 | int minWidth, int minHeight, int threshold) { 1893 | this(id, w, h, xpos, ypos, minWidth, minHeight); 1894 | 1895 | this.threshold = threshold; 1896 | } 1897 | 1898 | // helper to create cascading windows 1899 | private int getX(int id, int width) { 1900 | DisplayMetrics displaymetrics = new DisplayMetrics(); 1901 | mWindowManager.getDefaultDisplay().getMetrics(displaymetrics); 1902 | int displayWidth = displaymetrics.widthPixels; 1903 | 1904 | int types = sWindowCache.size(); 1905 | 1906 | int initialX = 100 * types; 1907 | int variableX = 100 * id; 1908 | int rawX = initialX + variableX; 1909 | 1910 | return rawX % (displayWidth - width); 1911 | } 1912 | 1913 | // helper to create cascading windows 1914 | private int getY(int id, int height) { 1915 | DisplayMetrics displaymetrics = new DisplayMetrics(); 1916 | mWindowManager.getDefaultDisplay().getMetrics(displaymetrics); 1917 | int displayWidth = displaymetrics.widthPixels; 1918 | int displayHeight = displaymetrics.heightPixels; 1919 | 1920 | int types = sWindowCache.size(); 1921 | 1922 | int initialY = 100 * types; 1923 | int variableY = x + 200 * (100 * id) / (displayWidth - width); 1924 | 1925 | int rawY = initialY + variableY; 1926 | 1927 | return rawY % (displayHeight - height); 1928 | } 1929 | 1930 | public void setFocusFlag(boolean focused) { 1931 | if (focused) { 1932 | flags = flags ^ StandOutLayoutParams.FLAG_NOT_FOCUSABLE; 1933 | } else { 1934 | flags = flags | StandOutLayoutParams.FLAG_NOT_FOCUSABLE; 1935 | } 1936 | } 1937 | } 1938 | 1939 | private int getWindowType() { 1940 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 1941 | return WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; 1942 | } else { 1943 | return WindowManager.LayoutParams.TYPE_PHONE; 1944 | } 1945 | } 1946 | 1947 | protected class DropDownListItem { 1948 | public int icon; 1949 | public String description; 1950 | public Runnable action; 1951 | 1952 | public DropDownListItem(int icon, String description, Runnable action) { 1953 | super(); 1954 | this.icon = icon; 1955 | this.description = description; 1956 | this.action = action; 1957 | } 1958 | 1959 | @Override 1960 | public String toString() { 1961 | return description; 1962 | } 1963 | } 1964 | } 1965 | --------------------------------------------------------------------------------