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
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
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
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 extends StandOutWindow> toCls, int toId, int requestCode,
205 | Bundle data, Class extends StandOutWindow> 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 extends StandOutWindow> 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 extends StandOutWindow> 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 extends StandOutWindow> 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 extends StandOutWindow> 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 extends StandOutWindow> toCls, int toId, int requestCode,
302 | Bundle data, Class extends StandOutWindow> 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 extends StandOutWindow> fromCls = (Class extends StandOutWindow>) 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
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 extends StandOutWindow> 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