+ * If you want only a portion of the screens in the Hover menu to look different, then consider
+ * using composition of {@link NavigatorContent} to achieve the desired effect. For example,
+ * if you want a {@code Toolbar} to appear on one or more screens, consider placing your content
+ * within a {@link ToolbarNavigatorContent}, and then add the {@code ToolbarNavigatorContent}
+ * as the content of the default {@code Navigator}.
+ *
+ * @return Custom Navigator to use on every screen in the Hover menu
+ */
+ protected Navigator createNavigator() {
+ return null; // Subclasses can override this to provide a custom Navigator.
+ }
+
+ abstract protected HoverMenuAdapter createHoverMenuAdapter();
+
+ /**
+ * Hook method for subclasses to take action when the user exits the HoverMenu. This method runs
+ * just before this {@code HoverMenuService} calls {@code stopSelf()}.
+ */
+ protected void onHoverMenuExitingByUserRequest() {
+ // Hook for subclasses.
+ }
+
+ private void savePreferredLocation() {
+ String memento = mHoverMenu.getVisualState();
+ mPrefs.edit().putString(PREF_HOVER_MENU_VISUAL_STATE, memento).apply();
+ }
+
+ private String loadPreferredLocation() {
+ return mPrefs.getString(PREF_HOVER_MENU_VISUAL_STATE, null);
+ }
+}
diff --git a/app/src/main/java/com/stardust/hover/HoverMenuView.java b/app/src/main/java/com/stardust/hover/HoverMenuView.java
new file mode 100644
index 000000000..165465fcf
--- /dev/null
+++ b/app/src/main/java/com/stardust/hover/HoverMenuView.java
@@ -0,0 +1,1115 @@
+package com.stardust.hover;
+
+/**
+ * Created by Stardust on 2017/3/11.
+ */
+
+
+import android.animation.Animator;
+import android.animation.LayoutTransition;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.TimeInterpolator;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.os.Build;
+import android.os.Vibrator;
+import android.support.annotation.ColorInt;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AnticipateInterpolator;
+import android.view.animation.BounceInterpolator;
+import android.view.animation.OvershootInterpolator;
+import android.widget.RelativeLayout;
+
+import io.mattcarroll.hover.Navigator;
+import io.mattcarroll.hover.R;
+import io.mattcarroll.hover.HoverMenuAdapter;
+import io.mattcarroll.hover.defaulthovermenu.CollapsedMenuAnchor;
+import io.mattcarroll.hover.defaulthovermenu.DefaultNavigator;
+import io.mattcarroll.hover.defaulthovermenu.Dragger;
+import io.mattcarroll.hover.defaulthovermenu.HoverMenuContentView;
+import io.mattcarroll.hover.defaulthovermenu.MagnetPositioner;
+import io.mattcarroll.hover.defaulthovermenu.Positionable;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Stack;
+
+/**
+ * {@code HoverMenuView} is a floating menu implementation. This implementation displays tabs along
+ * the top of its display, from right to left. Below the tabs, filling the remainder of the display
+ * is a content region that displays the content for a selected tab. The content region includes
+ * a visual indicator showing which tab is currently selected. Each tab's content includes a title
+ * and a visual area. The visual area can display any {@code View}.
+ *
+ * {@code HoverMenuView} cannot be used in XML because it requires additional parameters in its
+ * constructor.
+ */
+@SuppressLint("ViewConstructor")
+public class HoverMenuView extends RelativeLayout {
+
+ private static final String TAG = "HoverMenuView";
+
+ private static final float EXIT_RADIUS_IN_DP = 75;
+
+ private static final int EXPANDED = 1;
+ private static final int COLLAPSED = 2;
+ private static final int TRANSITIONING = 3;
+
+ //------ Views From Layout XML -------
+ private View mTabAnchorView;
+ private HoverMenuContentView mContentView; // Content view to display a menu
+ private Navigator mNavigator;
+ private View mShadeView; // Dark backdrop that fills screen behind menu
+ private View mExitGradientBackground; // Dark gradient that appears behind exit view on lower part of screen
+ private View mExitView; // An "x" that appears near bottom of screen to signify "exit" from floating menu
+
+ private View mLastTabInStrip;
+ private TabSelectionListener mTabSelectionListener;
+
+ private List mMotionAnimations = new ArrayList<>();
+ private Point mDraggingPoint = new Point();
+ private float mExitRadiusInPx; // Radius around mCenterOfExitView where the "primary" bubble can be dropped to signify an exit
+ private boolean mIsExitRegionActivated = false;
+
+ private HoverMenuAdapter mAdapter;
+ private int mTabSizeInPx;
+ private List mTabIds = new ArrayList<>();
+ private String mActiveTabId;
+ private View mActiveTab; // The tab whose menu is currently displayed, and the tab that will represent the collapsed menu.
+
+ private int mVisualState;
+ private CollapsedMenuAnchor mMenuAnchor;
+ private Dragger mWindowDragWatcher;
+ private HoverMenuTransitionListener mTransitionListener;
+ private HoverMenuExitRequestListener mExitRequestListener;
+
+ private HoverMenuContentView.HoverMenuContentResizer mHoverMenuContentResizer = new HoverMenuContentView.HoverMenuContentResizer() {
+ @Override
+ public void makeHoverMenuContentFullscreen() {
+ RelativeLayout.LayoutParams contentContainerLayoutParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0);
+ contentContainerLayoutParams.height = 0;
+ contentContainerLayoutParams.addRule(RelativeLayout.BELOW, R.id.view_tab_strip_anchor);
+ contentContainerLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
+ contentContainerLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
+ contentContainerLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
+ mContentView.setLayoutParams(contentContainerLayoutParams);
+ }
+
+ @Override
+ public void makeHoverMenuContentAsTallAsItsContent() {
+ RelativeLayout.LayoutParams contentContainerLayoutParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ contentContainerLayoutParams.addRule(RelativeLayout.BELOW, R.id.view_tab_strip_anchor);
+ contentContainerLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
+ contentContainerLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
+ mContentView.setLayoutParams(contentContainerLayoutParams);
+ }
+ };
+
+ private OnTouchListener mTabOnTouchListener = new OnTouchListener() {
+ @Override
+ public boolean onTouch(View view, MotionEvent motionEvent) {
+ if (MotionEvent.ACTION_DOWN == motionEvent.getAction()) {
+ showTabAsPressed(view);
+ } else if (MotionEvent.ACTION_UP == motionEvent.getAction()) {
+ showTabAsNotPressed(view);
+ }
+
+ return false;
+ }
+ };
+
+ private OnClickListener mTabOnClickListener = new OnClickListener() {
+ @Override
+ public void onClick(View tab) {
+ String id = (String) tab.getTag(R.string.floatingmenuview_tabview_id);
+
+ // If this is a selection of the tab that is already active, then collapse the menu.
+ if (id.equals(mActiveTabId)) {
+ collapse();
+ }
+
+ // Select the clicked tab.
+ selectTab(id);
+ }
+ };
+
+ private Positionable mSidePullerPositioner = new Positionable() {
+ @Override
+ public void setPosition(@NonNull Point position) {
+ setCollapsedPosition(position.x, position.y);
+ }
+ };
+
+ private CollapsedMenuViewController mCollapsedMenuViewController = new CollapsedMenuViewController() {
+
+ @Override
+ public void onPress() {
+ onPressOfCollapsedMenu();
+ startDragMode();
+ }
+
+ @Override
+ public void onDragTo(@NonNull Point dragCenterPosition) {
+ int left = dragCenterPosition.x - (getActiveTab().getWidth() / 2);
+ int top = dragCenterPosition.y - (getActiveTab().getHeight() / 2);
+
+ setCollapsedPosition(left, top);
+
+ checkForExitRegionActivation();
+ }
+
+ @Override
+ public void onRelease() {
+ stopDragMode();
+ onReleaseOfCollapsedMenu();
+ }
+
+ @Override
+ public void pullToAnchor() {
+ Log.d(TAG, "pullToAnchor()");
+ MagnetPositioner magnetPositioner = new MagnetPositioner(getResources().getDisplayMetrics(), mSidePullerPositioner, new MagnetPositioner.OnCompletionListener() {
+ @Override
+ public void onPullToSideCompleted() {
+ // TODO: this collapsed logic is duplicated
+ mVisualState = COLLAPSED;
+ enableDragging();
+ mTransitionListener.onCollapsed();
+ }
+ });
+
+ View activeTab = getActiveTab();
+ Log.d(TAG, "Active tab location - left: " + activeTab.getX() + ", top: " + activeTab.getY());
+ Rect tabViewBounds = new Rect(
+ (int) activeTab.getX(),
+ (int) activeTab.getY(),
+ (int) activeTab.getX() + activeTab.getWidth(),
+ (int) activeTab.getY() + activeTab.getHeight());
+
+ // Update the anchor location.
+ mMenuAnchor.setAnchorByInterpolation(tabViewBounds);
+ Log.d(TAG, "New anchor normalized Y: " + mMenuAnchor.getAnchorNormalizedY());
+
+ // Pull the tab bounds to the anchor position.
+ magnetPositioner.pullToAnchor(mMenuAnchor, tabViewBounds, new BounceInterpolator());
+ }
+ };
+
+ private Dragger.DragListener mDragListener = new Dragger.DragListener() {
+ @Override
+ public void onPress(float x, float y) {
+ getCollapsedMenuViewController().onPress();
+ }
+
+ @Override
+ public void onDragStart(float x, float y) {
+ }
+
+ @Override
+ public void onDragTo(float x, float y) {
+ getCollapsedMenuViewController().onDragTo(new Point(
+ (int) x + (getActiveTab().getWidth() / 2),
+ (int) y + (getActiveTab().getHeight() / 2))
+ );
+ }
+
+ @Override
+ public void onReleasedAt(float x, float y) {
+ Log.d(TAG, "Menu released at - x: " + x + ", y: " + y);
+ // Update the visual pressed state of the hover menu.
+ getCollapsedMenuViewController().onRelease();
+
+ if (isActiveTabInExitRegion()) {
+ // Notify our listener that an exit has been requested.
+ Log.d(TAG, "Hover menu dropped in exit region. Requesting exit.");
+ mExitRequestListener.onExitRequested();
+ } else {
+ // The user did not choose to exit the menu so pull the menu to the side of the screen.
+ getCollapsedMenuViewController().pullToAnchor();
+ }
+ }
+
+ @Override
+ public void onTap() {
+ // Update the visual pressed state of the hover menu.
+ getCollapsedMenuViewController().onRelease();
+
+ expand();
+ }
+ };
+
+ private final HoverMenuAdapter.ContentChangeListener mAdapterListener = new HoverMenuAdapter.ContentChangeListener() {
+ @Override
+ public void onContentChange(@NonNull HoverMenuAdapter adapter) {
+ // Update all the tab views.
+ removeAllTabs();
+ addAllTabs();
+
+ // TODO: need null check in case selected tab is gone.
+ Log.d(TAG, "Content change. Active id: " + mActiveTabId);
+ Log.d(TAG, "Active tab view: " + findViewById(mActiveTabId.hashCode()));
+ mActiveTab = findViewById(mActiveTabId.hashCode());
+ mContentView.setActiveTab(mActiveTab);
+ }
+ };
+
+ public HoverMenuView(Context context, @Nullable Navigator navigator, @NonNull Dragger windowDragWatcher,
+ @NonNull PointF savedAnchor) {
+ super(context);
+ mWindowDragWatcher = windowDragWatcher;
+ mMenuAnchor = new CollapsedMenuAnchor(getResources().getDisplayMetrics(), 10);
+ mMenuAnchor.setAnchorAt((int) savedAnchor.x, savedAnchor.y); // TODO: savedAnchor isn't really a position, its a tuple of values (side, y-pos)
+ mNavigator = null == navigator ? new DefaultNavigator(context) : navigator;
+ init();
+ }
+
+ private void init() {
+ Log.d(TAG, "init()");
+ LayoutInflater.from(getContext()).inflate(R.layout.view_hover_menu, this, true);
+
+ mTabSizeInPx = getResources().getDimensionPixelSize(R.dimen.floating_icon_size);
+
+ mTabAnchorView = findViewById(R.id.view_tab_strip_anchor);
+ mContentView = (HoverMenuContentView) findViewById(R.id.view_content);
+ mShadeView = findViewById(R.id.view_shade);
+ mExitGradientBackground = findViewById(R.id.view_exit_gradient);
+ mExitView = findViewById(R.id.view_exit);
+ mExitRadiusInPx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, EXIT_RADIUS_IN_DP, getResources().getDisplayMetrics());
+
+ mContentView.setContentResizer(mHoverMenuContentResizer);
+ mContentView.setNavigator(mNavigator);
+
+ initLayoutTransitionAnimations();
+
+ // Initially we're not really in the EXPANDED or COLLAPSED states.
+ mVisualState = TRANSITIONING;
+
+ setTabSelectionListener(null);
+ setHoverMenuTransitionListener(null);
+ setHoverMenuExitRequestListener(null);
+ }
+
+ public void release() {
+ mWindowDragWatcher.deactivate();
+ }
+
+ public void setContentBackgroundColor(@ColorInt int color) {
+ mContentView.setBackgroundColor(color);
+ }
+
+ public CollapsedMenuViewController getCollapsedMenuViewController() {
+ return mCollapsedMenuViewController;
+ }
+
+ public void collapse() {
+ if (EXPANDED == mVisualState) {
+ Log.d(TAG, "Collapsed Hover menu.");
+ doCollapse(true);
+ }
+ }
+
+ public void expand() {
+ if (COLLAPSED == mVisualState) {
+ Log.d(TAG, "Expanding Hover menu.");
+ doExpansion(true);
+ }
+ }
+
+ public void setHoverMenuTransitionListener(@Nullable HoverMenuTransitionListener transitionListener) {
+ mTransitionListener = null == transitionListener ? new NoOpHoverMenuTransitionListener() : transitionListener;
+ }
+
+ public void setHoverMenuExitRequestListener(@Nullable HoverMenuExitRequestListener exitRequestListener) {
+ mExitRequestListener = null == exitRequestListener ? new NoOpHoverMenuExitRequestListener() : exitRequestListener;
+ }
+
+ // TODO: create custom object to hold anchor state
+ public PointF getAnchorState() {
+ return new PointF(mMenuAnchor.getAnchorSide(), mMenuAnchor.getAnchorNormalizedY());
+ }
+
+ public void setAnchorState(@NonNull PointF anchorState) {
+ mMenuAnchor.setAnchorAt((int) anchorState.x, anchorState.y);
+
+ // If we're in the collapsed state, update the collapsed position to the new anchor position.
+ if (COLLAPSED == mVisualState) {
+ moveActiveTabToAnchor();
+ }
+ }
+
+ private void doCollapse(boolean animate) {
+ mVisualState = TRANSITIONING;
+ mTransitionListener.onCollapsing();
+
+ if (animate) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ LayoutTransition transition = getLayoutTransition();
+ transition.enableTransitionType(LayoutTransition.DISAPPEARING);
+ }
+
+ Iterator tabIterator = getTailToHeadTabIterator();
+ int timeUntilVisiblityChange = 0;
+ while (tabIterator.hasNext()) {
+ final View tabView = tabIterator.next();
+
+ // We don't want to hide the active tab, so only hide the other tabs.
+ if (getActiveTab() != tabView) {
+ postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ tabView.setVisibility(View.GONE);
+ }
+ }, timeUntilVisiblityChange);
+
+ timeUntilVisiblityChange += 50;
+ }
+ }
+
+ // Disable transition animations after tabs are done animating in.
+ postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ LayoutTransition transition = getLayoutTransition();
+ transition.disableTransitionType(LayoutTransition.DISAPPEARING);
+ }
+ }
+ }, timeUntilVisiblityChange);
+
+ postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ getShadeView().setVisibility(View.GONE);
+
+ // Move the active tab to the anchor position.
+ animateActiveTabToAnchorPosition();
+ }
+ }, timeUntilVisiblityChange);
+ } else {
+ // Change visibilities immediately.
+ Iterator tailToHeadTabIterator = getTailToHeadTabIterator();
+ while (tailToHeadTabIterator.hasNext()) {
+ View tabView = tailToHeadTabIterator.next();
+
+ // We don't want to hide the very first tab, so only hide tabs that have yet another tab leading them.
+ if (getActiveTab() != tabView) {
+ Log.d(TAG, "Hiding tab: " + tabView.getTag(R.string.floatingmenuview_tabview_id));
+ tabView.setVisibility(View.GONE);
+ }
+ }
+
+ getShadeView().setVisibility(View.GONE);
+
+ moveActiveTabToAnchor();
+ mTransitionListener.onCollapsed();
+ }
+
+ // Close the content area.
+ getContentView().setVisibility(View.GONE);
+ }
+
+ private void doExpansion(boolean animate) {
+ mVisualState = TRANSITIONING;
+ mTransitionListener.onExpanding();
+
+ disableDragging();
+
+ getShadeView().setVisibility(View.VISIBLE);
+
+ if (animate) {
+ animateActiveTabToExpandedPosition(new OvershootInterpolator());
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ LayoutTransition transition = getLayoutTransition();
+ transition.enableTransitionType(LayoutTransition.APPEARING);
+ }
+
+ Iterator headToTailTabIterator = getHeadToTailTabIterator();
+ int timeUntilVisiblityChange = 0;
+ while (headToTailTabIterator.hasNext()) {
+ View tabView = headToTailTabIterator.next();
+ Log.d(TAG, "Showing tab: " + tabView.getTag(R.string.floatingmenuview_tabview_id));
+ final View tabRef = tabView;
+ postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ tabRef.setVisibility(View.VISIBLE);
+ }
+ }, timeUntilVisiblityChange);
+
+ timeUntilVisiblityChange += 50;
+ }
+
+ // Disable transition animations after tabs are done animating in.
+ postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ LayoutTransition transition = getLayoutTransition();
+ transition.disableTransitionType(LayoutTransition.APPEARING);
+ }
+
+ // Set state to expanded.
+ mVisualState = EXPANDED;
+ mTransitionListener.onExpanded();
+ mContentView.setActiveTab(mActiveTab);
+ }
+ }, timeUntilVisiblityChange);
+ } else {
+ // Change visibilities immediately.
+ moveActiveTabTo(0, 0);
+
+ Iterator headToTailTabIterator = getHeadToTailTabIterator();
+ while (headToTailTabIterator.hasNext()) {
+ View tabView = headToTailTabIterator.next();
+ Log.d(TAG, "Showing tab: " + tabView.getTag(R.string.floatingmenuview_tabview_id));
+ tabView.setVisibility(View.VISIBLE);
+ }
+
+ // Set state to expanded.
+ mVisualState = EXPANDED;
+ mTransitionListener.onExpanded();
+ }
+
+ // Open the content.
+ getContentView().setVisibility(View.VISIBLE);
+ }
+
+ private void enableDragging() {
+ Point anchorPosition = mDraggingPoint;
+
+ Log.d(TAG, "Enabling dragging - x: " + anchorPosition.x + ", y: " + anchorPosition.y);
+ Log.d(TAG, "Drag width: " + mTabSizeInPx + ", height: " + mTabSizeInPx);
+
+ // TODO: should create a method specifically for placing the drag bounds.
+ mWindowDragWatcher.deactivate();
+ mWindowDragWatcher.activate(mDragListener, new Rect(
+ anchorPosition.x,
+ anchorPosition.y,
+ anchorPosition.x + getActiveTab().getWidth(),
+ anchorPosition.y + getActiveTab().getHeight()
+ ));
+ }
+
+ private void disableDragging() {
+ mWindowDragWatcher.deactivate();
+ }
+
+ private void moveActiveTabToAnchor() {
+ Rect anchoredBounds = mMenuAnchor.anchor(new Rect(
+ (int) mActiveTab.getX(),
+ (int) mActiveTab.getY(),
+ (int) mActiveTab.getX() + mActiveTab.getWidth(),
+ (int) mActiveTab.getY() + mActiveTab.getHeight()
+ ));
+ moveActiveTabTo(anchoredBounds.left, anchoredBounds.top);
+ }
+
+ private void moveActiveTabTo(int x, int y) {
+ View activeTab = getActiveTab();
+ if (null != activeTab) {
+ activeTab.setTranslationX(x);
+ activeTab.setTranslationY(y);
+ }
+ }
+
+ public void setAdapter(@Nullable HoverMenuAdapter adapter) {
+ Log.d(TAG, "setAdapter()");
+
+ // Remove all existing tabs from any previous adapter.
+ mActiveTabId = null;
+ removeAllTabs();
+ if (null != mAdapter) {
+ mAdapter.removeContentChangeListener(mAdapterListener);
+ }
+
+ // Update our adapter reference.
+ mAdapter = adapter;
+
+ if (null != adapter) {
+ // Create all the tabs that the new adapter wants.
+ addAllTabs();
+
+ // Start listening for adapter changes.
+ mAdapter.addContentChangeListener(mAdapterListener);
+
+ // Select the first tab.
+ setActiveTab(mTabIds.get(0));
+
+ // TODO: we might set an adapter while expanded - track down the need for this and change it.
+ // We force a collapse because at this point we're in a weird initial state.
+ doCollapse(true);
+ }
+ }
+
+ @Nullable
+ private View getActiveTab() {
+ return mActiveTab;
+ }
+
+ @NonNull
+ private View getShadeView() {
+ return mShadeView;
+ }
+
+ @NonNull
+ private HoverMenuContentView getContentView() {
+ return mContentView;
+ }
+
+ @NonNull
+ private Iterator getHeadToTailTabIterator() {
+ return new HeadToTailTabIterator(mTabAnchorView);
+ }
+
+ @NonNull
+ private Iterator getTailToHeadTabIterator() {
+ return new TailToHeadTabIterator(mTabAnchorView);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ Log.d(TAG, "onLayoutChange()");
+ Log.d(TAG, "New - left: " + left + ", top: " + top + ", right: " + right + ", bottom: " + bottom);
+ Rect newBounds = new Rect(left, top, right, bottom);
+
+ // Adjust anchor position.
+ mMenuAnchor.setDisplayBounds(newBounds);
+
+ if (null != mActiveTab) {
+ Log.d(TAG, "Active tab - (" + mActiveTab.getX() + ", " + mActiveTab.getY() + "), width: " + mActiveTab.getWidth() + ", " + mActiveTab.getHeight());
+ Log.d(TAG, "Active tab visibility: " + (mActiveTab.getVisibility() == VISIBLE ? "VISIBLE" : "NOT VISIBLE"));
+ } else {
+ Log.d(TAG, "Active tab is null.");
+ return;
+ }
+
+ Rect anchoredBounds = mMenuAnchor.anchor(new Rect(0, 0, getActiveTab().getWidth(), getActiveTab().getHeight()));
+ Log.d(TAG, "Adjusted anchor bounds at (" + anchoredBounds.left + ", " + anchoredBounds.top + ")");
+
+ if (!changed) {
+ return;
+ }
+
+ // TODO: how can we avoid this null check?
+ if (null != mActiveTab) {
+ Log.d(TAG, "Adjusting tab position due to layout change.");
+ getCollapsedMenuViewController().onDragTo(new Point(
+ anchoredBounds.left + (mActiveTab.getWidth() / 2),
+ anchoredBounds.top + (mActiveTab.getHeight() / 2)
+ ));
+ } else {
+ Log.d(TAG, "There is no active tab, no need to adjust positioning during layout change.");
+ }
+
+ if (mVisualState == COLLAPSED) {
+ Log.d(TAG, "Restarting dragging so that the drag area is adjusted due to layout change.");
+ enableDragging();
+ }
+ }
+
+ public boolean isExpanded() {
+ return mVisualState == EXPANDED;
+ }
+
+ private void addTab(@NonNull String id, @NonNull View tabView) {
+ mTabIds.add(id);
+
+ Log.d(TAG, "Setting tab ID to: " + id.hashCode());
+ tabView.setId(id.hashCode());
+ tabView.setTag(R.string.floatingmenuview_tabview_id, id);
+ tabView.setOnTouchListener(mTabOnTouchListener);
+ tabView.setOnClickListener(mTabOnClickListener);
+
+ if (null == mLastTabInStrip) {
+ Log.d(TAG, "Adding first tab: " + tabView.getTag(R.string.floatingmenuview_tabview_id));
+
+ // This is the first tab in the strip.
+ addTabAfter(tabView, mTabAnchorView);
+ } else {
+ Log.d(TAG, "Adding additional tab: " + tabView.getTag(R.string.floatingmenuview_tabview_id) + " after " + mLastTabInStrip.getTag(R.string.floatingmenuview_tabview_id));
+ // There are already tabs in the strip, append this one to the end.
+ addTabAfter(tabView, mLastTabInStrip);
+
+ if (!isExpanded()) {
+ tabView.setVisibility(GONE);
+ }
+ }
+
+ mLastTabInStrip = tabView;
+ }
+
+ private void addTabAfter(@NonNull View tabView, @NonNull View leadingView) {
+ RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(mTabSizeInPx, mTabSizeInPx);
+ layoutParams.addRule(LEFT_OF, leadingView.getId());
+ layoutParams.addRule(RelativeLayout.ALIGN_TOP, leadingView.getId());
+
+ // Tell the leading view who is anchored to it.
+ leadingView.setTag(tabView);
+
+ addView(tabView, layoutParams);
+ }
+
+ private void removeTab(@NonNull String id) {
+ View tabView = findViewById(id.hashCode());
+ if (null != tabView) {
+ RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) tabView.getLayoutParams();
+ int anchorViewId;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ anchorViewId = layoutParams.getRule(RelativeLayout.LEFT_OF);
+ } else {
+ anchorViewId = layoutParams.getRules()[RelativeLayout.LEFT_OF];
+ }
+ View leadingTabView = findViewById(anchorViewId);
+
+ View trailingTabView = (View) tabView.getTag();
+ if (null != trailingTabView) {
+ addTabAfter(trailingTabView, leadingTabView);
+ } else if (tabView == mLastTabInStrip) {
+ mLastTabInStrip = leadingTabView;
+ }
+
+ removeView(tabView);
+ }
+ }
+
+ private void addAllTabs() {
+ for (int i = 0; i < mAdapter.getTabCount(); ++i) {
+ addTab(i + "", mAdapter.getTabView(i));
+ mTabIds.add(i + "");
+ }
+ }
+
+ private void removeAllTabs() {
+ for (String tabId : mTabIds) {
+ removeView(findViewById(tabId.hashCode()));
+ }
+
+ mTabIds.clear();
+ mLastTabInStrip = null;
+ }
+
+// private void refreshTabs() {
+// int tabCount = mAdapter.getTabCount();
+// for (int i = 0; i < tabCount; ++i) {
+// replaceTabView()
+// }
+// }
+
+ private void selectTab(@NonNull String id) {
+ Log.d(TAG, "Selecting tab: " + id.hashCode());
+ setActiveTab(id);
+ mTabSelectionListener.onTabSelected(id);
+ }
+
+ private void setActiveTab(String id) {
+ if (id.equals(mActiveTabId)) {
+ // This tab is already selected.
+ return;
+ }
+
+ mActiveTabId = id;
+ mActiveTab = findViewById(id.hashCode());
+ mContentView.setActiveTab(mActiveTab);
+
+ // This is a top-level menu item so clear all content from the menu to start fresh.
+// mContentView.clearContent();
+ mNavigator.clearContent();
+
+ // Activate the chosen tab.
+// mAdapter.getNavigatorContent(Integer.parseInt(id)).execute(getContext(), mContentView);
+// mContentView.pushContent(mAdapter.getNavigatorContent(Integer.parseInt(id)));
+ mNavigator.pushContent(mAdapter.getNavigatorContent(Integer.parseInt(id)));
+ }
+
+ public void setTabSelectionListener(@Nullable TabSelectionListener tabSelectionListener) {
+ if (null != tabSelectionListener) {
+ mTabSelectionListener = tabSelectionListener;
+ } else {
+ mTabSelectionListener = new NoOpTabSelectionListener();
+ }
+ }
+
+ public boolean onBackPressed() {
+ if (isExpanded() && !mContentView.onBackPressed()) {
+ collapse();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private void onPressOfCollapsedMenu() {
+ // Scale down the active tab to give "pressed" effect.
+ if (null != mActiveTab) {
+ showTabAsPressed(mActiveTab);
+ }
+ }
+
+ private void onReleaseOfCollapsedMenu() {
+ // Return scale of the active tab to normal state.
+ if (null != mActiveTab) {
+ showTabAsNotPressed(mActiveTab);
+ }
+ }
+
+ private void onMenuCollapsed() {
+ mVisualState = COLLAPSED;
+ enableDragging();
+ mTransitionListener.onCollapsed();
+ }
+
+ private void showTabAsPressed(@NonNull View tabView) {
+ tabView.setScaleX(0.95f);
+ tabView.setScaleY(0.95f);
+ }
+
+ private void showTabAsNotPressed(@NonNull View tabView) {
+ tabView.setScaleX(1.0f);
+ tabView.setScaleY(1.0f);
+ }
+
+ private void startDragMode() {
+ Log.d(TAG, "startDragMode()");
+ mExitGradientBackground.setAlpha(0.0f);
+ ObjectAnimator.ofFloat(mExitGradientBackground, "alpha", 0.0f, 1.0f).setDuration(300).start();
+ mExitGradientBackground.setVisibility(VISIBLE);
+
+ LayoutTransition transition = getLayoutTransition();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ transition.enableTransitionType(LayoutTransition.APPEARING);
+ }
+
+ mExitView.setVisibility(VISIBLE);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ transition.disableTransitionType(LayoutTransition.APPEARING);
+ }
+ }
+
+ private void stopDragMode() {
+ Log.d(TAG, "stopDragMode()");
+ ObjectAnimator animator = ObjectAnimator.ofFloat(mExitGradientBackground, "alpha", 1.0f, 0.0f).setDuration(300);
+ animator.addListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ mExitGradientBackground.setVisibility(INVISIBLE);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animator) {
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animator) {
+ }
+ });
+ animator.start();
+
+ LayoutTransition transition = getLayoutTransition();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ transition.enableTransitionType(LayoutTransition.DISAPPEARING);
+ }
+
+ mExitView.setVisibility(INVISIBLE);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ transition.disableTransitionType(LayoutTransition.DISAPPEARING);
+ }
+ }
+
+ private void checkForExitRegionActivation() {
+ if (!mIsExitRegionActivated && isActiveTabInExitRegion()) {
+ activateExitRegion();
+ } else if (mIsExitRegionActivated && !isActiveTabInExitRegion()) {
+ deactivateExitRegion();
+ }
+ }
+
+ private void activateExitRegion() {
+ mIsExitRegionActivated = true;
+
+ Vibrator vibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE);
+ vibrator.vibrate(50);
+
+ mExitView.setScaleX(1.25f);
+ mExitView.setScaleY(1.25f);
+ }
+
+ private void deactivateExitRegion() {
+ mIsExitRegionActivated = false;
+
+ mExitView.setScaleX(1.0f);
+ mExitView.setScaleY(1.0f);
+ }
+
+ private void setCollapsedPosition(int x, int y) {
+ Log.d(TAG, "Setting collapsed position - x: " + x + ", y: " + y);
+ mDraggingPoint.set(x, y);
+
+ if (!isExpanded() && null != mActiveTab) {
+// Log.d(TAG, "Setting collapsed menu position (dragging) - x: " + mDraggingPoint.x + ", y: " + mDraggingPoint.y);
+ mActiveTab.setX(mDraggingPoint.x);
+ mActiveTab.setY(mDraggingPoint.y);
+ }
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(@NonNull KeyEvent event) {
+ if (KeyEvent.ACTION_UP == event.getAction() && KeyEvent.KEYCODE_BACK == event.getKeyCode()) {
+ return onBackPressed();
+ } else {
+ return super.dispatchKeyEvent(event);
+ }
+ }
+
+ private void initLayoutTransitionAnimations() {
+ setLayoutTransition(new LayoutTransition());
+ final LayoutTransition transition = getLayoutTransition();
+
+ transition.setAnimator(LayoutTransition.APPEARING, createEnterObjectAnimator());
+
+ transition.setAnimator(LayoutTransition.DISAPPEARING, createExitObjectAnimator());
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ transition.disableTransitionType(LayoutTransition.APPEARING);
+ transition.disableTransitionType(LayoutTransition.DISAPPEARING);
+ transition.disableTransitionType(LayoutTransition.CHANGE_APPEARING);
+ transition.disableTransitionType(LayoutTransition.CHANGE_DISAPPEARING);
+ transition.disableTransitionType(LayoutTransition.CHANGING);
+ }
+ }
+
+ private ObjectAnimator createEnterObjectAnimator() {
+ PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 0.0f, 1.0f);
+ PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 0.0f, 1.0f);
+
+ // Target object doesn't matter because it is overriden by layout system.
+ ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(new Object(), scaleX, scaleY);
+ animator.setDuration(500);
+ animator.setInterpolator(new OvershootInterpolator());
+ return animator;
+ }
+
+ private ObjectAnimator createExitObjectAnimator() {
+ PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1.0f, 0.1f);
+ PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 1.0f, 0.1f);
+
+ // Target object doesn't matter because it is overriden by layout system.
+ ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(new Object(), scaleX, scaleY);
+ animator.setDuration(500);
+ animator.setInterpolator(new AnticipateInterpolator());
+ return animator;
+ }
+
+ private void animateActiveTabToAnchorPosition() {
+ Log.d(TAG, "animateActiveTabToAnchorPosition()");
+ MagnetPositioner magnetPositioner = new MagnetPositioner(getResources().getDisplayMetrics(), mSidePullerPositioner, new MagnetPositioner.OnCompletionListener() {
+ @Override
+ public void onPullToSideCompleted() {
+ onMenuCollapsed();
+ }
+ });
+
+ View activeTab = getActiveTab();
+ Log.d(TAG, "Active tab location - left: " + activeTab.getX() + ", top: " + activeTab.getY());
+ Rect tabViewBounds = new Rect(
+ (int) activeTab.getX(),
+ (int) activeTab.getY(),
+ (int) activeTab.getX() + activeTab.getWidth(),
+ (int) activeTab.getY() + activeTab.getHeight());
+
+ // Pull the tab bounds to the anchor position.
+ magnetPositioner.pullToAnchor(mMenuAnchor, tabViewBounds, new OvershootInterpolator());
+ }
+
+ private void animateActiveTabToExpandedPosition(TimeInterpolator interpolator) {
+ int x = 0;
+ int y = 0;
+
+ if (null == mActiveTab) {
+ return;
+ }
+
+ for (Animator animator : mMotionAnimations) {
+ if (animator.isRunning()) {
+ animator.cancel();
+ }
+ }
+ mMotionAnimations.clear();
+
+ double distanceToAnimate = getDistanceBetweenTwoPoints(x, y, mActiveTab.getTranslationX(), mActiveTab.getTranslationY());
+ int animationTimeInMillis = getTimeForAnimationDistance(distanceToAnimate);
+
+ PropertyValuesHolder xChange = PropertyValuesHolder.ofFloat("translationX", mActiveTab.getTranslationX(), x);
+ PropertyValuesHolder yChange = PropertyValuesHolder.ofFloat("translationY", mActiveTab.getTranslationY(), y);
+
+ ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(mActiveTab, xChange, yChange);
+ animator.setDuration(animationTimeInMillis);
+ animator.setInterpolator(interpolator);
+ animator.start();
+
+ mMotionAnimations.add(animator);
+ }
+
+ public boolean isActiveTabInExitRegion() {
+ PointF centerOfExitView = new PointF(mExitView.getX() + (mExitView.getWidth() / 2), mExitView.getY() + (mExitView.getHeight() / 2));
+ PointF iconPosition = new PointF(mActiveTab.getX() + (mActiveTab.getWidth() / 2), mActiveTab.getY() + (mActiveTab.getHeight() / 2));
+ double distance = getDistanceBetweenTwoPoints(iconPosition.x, iconPosition.y, centerOfExitView.x, centerOfExitView.y);
+ return distance <= mExitRadiusInPx;
+ }
+
+ private double getDistanceBetweenTwoPoints(float x1, float y1, float x2, float y2) {
+ return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
+ }
+
+ private int getTimeForAnimationDistance(double distanceInPx) {
+ double speedInDpPerSecond = 1000;
+ double distanceInDp = distanceInPx / getResources().getDisplayMetrics().density;
+ final int timingOffset = 200; // To give some time at front and back of animation regardless of how close we are to destination.
+
+ return (int) ((distanceInDp / speedInDpPerSecond) * 1000) + timingOffset;
+ }
+
+ public interface TabSelectionListener {
+
+ void onTabSelected(String id);
+
+ }
+
+ public static class NoOpTabSelectionListener implements TabSelectionListener {
+ @Override
+ public void onTabSelected(String id) {
+ // no-op
+ }
+ }
+
+ private static class HeadToTailTabIterator implements Iterator {
+
+ private View mCurrTabView;
+
+ public HeadToTailTabIterator(@NonNull View anchorView) {
+ mCurrTabView = anchorView;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return null != mCurrTabView.getTag();
+ }
+
+ @Override
+ public View next() {
+ return mCurrTabView = (View) mCurrTabView.getTag();
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException("You cannot remove tabs through this iterator.");
+ }
+
+ }
+
+ private static class TailToHeadTabIterator implements Iterator {
+
+ private Stack mTabStack = new Stack<>();
+
+ public TailToHeadTabIterator(@NonNull View anchorView) {
+ // Tabs are connected as a singly-linked list so to reverse order we put them on a stack.
+ View tabView = (View) anchorView.getTag();
+ while (null != tabView) {
+ mTabStack.push(tabView);
+ tabView = (View) tabView.getTag();
+ }
+ }
+
+ @Override
+ public boolean hasNext() {
+ return !mTabStack.isEmpty();
+ }
+
+ @Override
+ public View next() {
+ return mTabStack.pop();
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException("You cannot remove tabs through this iterator.");
+ }
+ }
+
+ public interface CollapsedMenuViewController {
+ void onPress();
+
+ void onDragTo(@NonNull Point dragCenterPosition);
+
+ void onRelease();
+
+ void pullToAnchor();
+ }
+
+ public interface HoverMenuTransitionListener {
+ void onCollapsing();
+
+ void onCollapsed();
+
+ void onExpanding();
+
+ void onExpanded();
+ }
+
+ public static class NoOpHoverMenuTransitionListener implements HoverMenuTransitionListener {
+
+ @Override
+ public void onCollapsing() {
+ // No-Op
+ }
+
+ @Override
+ public void onCollapsed() {
+ // No-Op
+ }
+
+ @Override
+ public void onExpanding() {
+ // No-Op
+ }
+
+ @Override
+ public void onExpanded() {
+ // No-Op
+ }
+
+ }
+
+ public interface HoverMenuExitRequestListener {
+ void onExitRequested();
+ }
+
+ public static class NoOpHoverMenuExitRequestListener implements HoverMenuExitRequestListener {
+
+ @Override
+ public void onExitRequested() {
+ // No-Op
+ }
+
+ }
+}
diff --git a/app/src/main/java/com/stardust/hover/SimpleHoverMenuTransitionListener.java b/app/src/main/java/com/stardust/hover/SimpleHoverMenuTransitionListener.java
new file mode 100644
index 000000000..085bc41dc
--- /dev/null
+++ b/app/src/main/java/com/stardust/hover/SimpleHoverMenuTransitionListener.java
@@ -0,0 +1,30 @@
+package com.stardust.hover;
+
+import io.mattcarroll.hover.defaulthovermenu.*;
+import io.mattcarroll.hover.defaulthovermenu.HoverMenuView;
+
+/**
+ * Created by Stardust on 2017/3/12.
+ */
+
+public class SimpleHoverMenuTransitionListener implements HoverMenuView.HoverMenuTransitionListener {
+ @Override
+ public void onCollapsing() {
+
+ }
+
+ @Override
+ public void onCollapsed() {
+
+ }
+
+ @Override
+ public void onExpanding() {
+
+ }
+
+ @Override
+ public void onExpanded() {
+
+ }
+}
diff --git a/app/src/main/java/com/stardust/hover/WindowHoverMenu.java b/app/src/main/java/com/stardust/hover/WindowHoverMenu.java
new file mode 100644
index 000000000..181190dbe
--- /dev/null
+++ b/app/src/main/java/com/stardust/hover/WindowHoverMenu.java
@@ -0,0 +1,254 @@
+package com.stardust.hover;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.view.ViewConfiguration;
+import android.view.WindowManager;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import io.mattcarroll.hover.HoverMenu;
+import io.mattcarroll.hover.HoverMenuAdapter;
+import io.mattcarroll.hover.Navigator;
+import io.mattcarroll.hover.defaulthovermenu.HoverMenuView;
+import io.mattcarroll.hover.defaulthovermenu.window.InWindowDragger;
+import io.mattcarroll.hover.defaulthovermenu.window.WindowViewController;
+
+/**
+ * Created by Stardust on 2017/3/11.
+ */
+
+public class WindowHoverMenu implements HoverMenu {
+
+ private HoverMenuView.HoverMenuTransitionListener mHoverMenuTransitionListener;
+
+ private static final String TAG = "WindowHoverMenu";
+
+ public WindowViewController getWindowViewController() {
+ return mWindowViewController;
+ }
+
+ private WindowViewController mWindowViewController; // Shows/hides/positions Views in a Window.
+
+
+ private HoverMenuView mHoverMenuView; // The visual presentation of the Hover menu.
+ private boolean mIsShowingHoverMenu; // Are we currently display mHoverMenuView?
+ private boolean mIsInDragMode; // If we're not in drag mode then we're in menu mode.
+ private Set mOnExitListeners = new HashSet<>();
+
+ private HoverMenuView.HoverMenuTransitionListener mHoverMenuTransitionListenerProxy = new HoverMenuView.HoverMenuTransitionListener() {
+ @Override
+ public void onCollapsing() {
+ if (mHoverMenuTransitionListener != null)
+ mHoverMenuTransitionListener.onCollapsing();
+ }
+
+ @Override
+ public void onCollapsed() {
+ mIsInDragMode = true;
+ // When collapsed, we make mHoverMenuView untouchable so that the WindowDragWatcher can
+ // take over. We do this so that touch events outside the drag area can propagate to
+ // applications on screen.
+ mWindowViewController.makeUntouchable(mHoverMenuView);
+ if (mHoverMenuTransitionListener != null)
+ mHoverMenuTransitionListener.onCollapsed();
+ }
+
+ @Override
+ public void onExpanding() {
+ mIsInDragMode = false;
+ if (mHoverMenuTransitionListener != null)
+ mHoverMenuTransitionListener.onExpanding();
+ }
+
+ @Override
+ public void onExpanded() {
+ mWindowViewController.makeTouchable(mHoverMenuView);
+ if (mHoverMenuTransitionListener != null)
+ mHoverMenuTransitionListener.onExpanded();
+ }
+ };
+
+ private HoverMenuView.HoverMenuExitRequestListener mMenuExitRequestListener = new HoverMenuView.HoverMenuExitRequestListener() {
+ @Override
+ public void onExitRequested() {
+ hide();
+ }
+ };
+
+ public WindowHoverMenu(@NonNull Context context, @NonNull WindowManager windowManager, @Nullable Navigator navigator, @Nullable String savedVisualState) {
+ mWindowViewController = new WindowViewController(windowManager);
+
+ InWindowDragger inWindowDragger = new InWindowDragger(
+ context,
+ mWindowViewController,
+ ViewConfiguration.get(context).getScaledTouchSlop()
+ );
+
+ PointF anchorState = new PointF(2, 0.5f); // Default to right side, half way down. See CollapsedMenuAnchor.
+ if (null != savedVisualState) {
+ try {
+ VisualStateMemento visualStateMemento = VisualStateMemento.fromJsonString(savedVisualState);
+ anchorState.set(visualStateMemento.getAnchorSide(), visualStateMemento.getNormalizedPositionY());
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ mHoverMenuView = new HoverMenuView(context, navigator, inWindowDragger, anchorState);
+ mHoverMenuView.setHoverMenuExitRequestListener(mMenuExitRequestListener);
+ mHoverMenuView.setHoverMenuTransitionListener(mHoverMenuTransitionListenerProxy);
+ }
+
+
+ public void setHoverMenuTransitionListener(HoverMenuView.HoverMenuTransitionListener hoverMenuTransitionListener) {
+ mHoverMenuTransitionListener = hoverMenuTransitionListener;
+ }
+
+ public HoverMenuView getHoverMenuView() {
+ return mHoverMenuView;
+ }
+
+ @Override
+ public String getVisualState() {
+ PointF anchor = mHoverMenuView.getAnchorState();
+ return new VisualStateMemento((int) anchor.x, anchor.y).toJsonString();
+ }
+
+ @Override
+ public void restoreVisualState(@NonNull String savedVisualState) {
+ try {
+ VisualStateMemento memento = VisualStateMemento.fromJsonString(savedVisualState);
+ mHoverMenuView.setAnchorState(new PointF(memento.getAnchorSide(), memento.getNormalizedPositionY()));
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void setAdapter(@Nullable HoverMenuAdapter adapter) {
+ mHoverMenuView.setAdapter(adapter);
+ }
+
+ /**
+ * Initializes and displays the Hover menu. To destroy and remove the Hover menu, use {@link #hide()}.
+ */
+ @Override
+ public void show() {
+ if (!mIsShowingHoverMenu) {
+ mWindowViewController.addView(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT, false, mHoverMenuView);
+
+ // Sync our control state with the HoverMenuView state.
+ if (mHoverMenuView.isExpanded()) {
+ mWindowViewController.makeTouchable(mHoverMenuView);
+ } else {
+ collapseMenu();
+ }
+
+ mIsShowingHoverMenu = true;
+ }
+ }
+
+ /**
+ * Exits the Hover menu system. This method is the inverse of {@link #show()}.
+ */
+ @Override
+ public void hide() {
+ if (mIsShowingHoverMenu) {
+ mIsShowingHoverMenu = false;
+
+ // Notify our exit listeners that we're exiting.
+ notifyOnExitListeners();
+
+ // Cleanup the control structures and Views.
+ mWindowViewController.removeView(mHoverMenuView);
+ mHoverMenuView.release();
+ }
+ }
+
+ /**
+ * Expands the Hover menu to show all of its tabs and a content area for the selected tab. To
+ * collapse the menu down a single active tab, use {@link #collapseMenu()}.
+ */
+ @Override
+ public void expandMenu() {
+ if (mIsInDragMode) {
+ mHoverMenuView.expand();
+ }
+ }
+
+ /**
+ * Collapses the Hover menu down to its single active tab and allows the tab to be dragged
+ * around the display. This method is the inverse of {@link #expandMenu()}.
+ */
+ @Override
+ public void collapseMenu() {
+ if (!mIsInDragMode) {
+ mHoverMenuView.collapse();
+ }
+ }
+
+
+ @Override
+ public void addOnExitListener(@NonNull OnExitListener onExitListener) {
+ mOnExitListeners.add(onExitListener);
+ }
+
+ @Override
+ public void removeOnExitListener(@NonNull OnExitListener onExitListener) {
+ mOnExitListeners.remove(onExitListener);
+ }
+
+ private void notifyOnExitListeners() {
+ for (OnExitListener listener : mOnExitListeners) {
+ listener.onExitByUserRequest();
+ }
+ }
+
+ private static class VisualStateMemento {
+
+ private static final String JSON_KEY_ANCHOR_SIDE = "anchor_side";
+ private static final String JSON_KEY_NORMALIZED_POSITION_Y = "normalized_position_y";
+
+ public static VisualStateMemento fromJsonString(@NonNull String jsonString) throws JSONException {
+ JSONObject jsonObject = new JSONObject(jsonString);
+ int anchorSide = jsonObject.getInt(JSON_KEY_ANCHOR_SIDE);
+ float normalizedPositionY = (float) jsonObject.getDouble(JSON_KEY_NORMALIZED_POSITION_Y);
+ return new VisualStateMemento(anchorSide, normalizedPositionY);
+ }
+
+ private int mAnchorSide;
+ private float mNormalizedPositionY;
+
+ public VisualStateMemento(int anchorSide, float normalizedPositionY) {
+ mAnchorSide = anchorSide;
+ mNormalizedPositionY = normalizedPositionY;
+ }
+
+ public int getAnchorSide() {
+ return mAnchorSide;
+ }
+
+ public float getNormalizedPositionY() {
+ return mNormalizedPositionY;
+ }
+
+ public String toJsonString() {
+ try {
+ JSONObject jsonObject = new JSONObject();
+ jsonObject.put(JSON_KEY_ANCHOR_SIDE, mAnchorSide);
+ jsonObject.put(JSON_KEY_NORMALIZED_POSITION_Y, mNormalizedPositionY);
+ return jsonObject.toString();
+ } catch (JSONException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/App.java b/app/src/main/java/com/stardust/scriptdroid/App.java
index 9eec3d4b6..83b710f0b 100644
--- a/app/src/main/java/com/stardust/scriptdroid/App.java
+++ b/app/src/main/java/com/stardust/scriptdroid/App.java
@@ -2,20 +2,28 @@
import android.app.Activity;
import android.app.Application;
+import android.graphics.Paint;
import android.os.Bundle;
import android.preference.PreferenceManager;
+import android.text.TextPaint;
+import android.util.Log;
-import com.squareup.leakcanary.LeakCanary;
-import com.stardust.scriptdroid.bounds_assist.BoundsAssistant;
-import com.stardust.scriptdroid.droid.runtime.action.ActionPerformAccessibilityDelegate;
+import com.stardust.automator.AccessibilityEventCommandHost;
+import com.stardust.scriptdroid.accessibility.AccessibilityInfoProvider;
+import com.stardust.scriptdroid.layout_inspector.LayoutInspector;
import com.stardust.scriptdroid.record.AccessibilityRecorderDelegate;
import com.stardust.scriptdroid.service.AccessibilityWatchDogService;
+import com.squareup.leakcanary.LeakCanary;
+import com.stardust.scriptdroid.droid.runtime.action.ActionPerformAccessibilityDelegate;
+import com.stardust.scriptdroid.tool.ViewTool;
import com.stardust.scriptdroid.ui.error.ErrorReportActivity;
import com.stardust.theme.ThemeColorManager;
import com.stardust.util.CrashHandler;
import com.stardust.util.StateObserver;
import java.lang.ref.WeakReference;
+import java.lang.reflect.Method;
+import java.util.Arrays;
/**
* Created by Stardust on 2017/1/27.
@@ -23,6 +31,8 @@
public class App extends Application {
+ private static final String TAG = "App";
+
private static WeakReference instance;
private static StateObserver stateObserver;
private static WeakReference currentActivity;
@@ -49,14 +59,15 @@ public void onCreate() {
stateObserver = new StateObserver(PreferenceManager.getDefaultSharedPreferences(this));
registerActivityLifecycleCallback();
initAccessibilityServiceDelegates();
-
}
private void initAccessibilityServiceDelegates() {
AccessibilityWatchDogService.addDelegateIfNeeded(100, ActionPerformAccessibilityDelegate.class);
AccessibilityWatchDogService.addDelegateIfNeeded(200, AccessibilityRecorderDelegate.getInstance());
- AccessibilityWatchDogService.addDelegateIfNeeded(300, BoundsAssistant.class);
+ AccessibilityWatchDogService.addDelegateIfNeeded(300, AccessibilityEventCommandHost.instance);
+ AccessibilityWatchDogService.addDelegateIfNeeded(400, AccessibilityInfoProvider.instance);
+ AccessibilityWatchDogService.addDelegateIfNeeded(500, LayoutInspector.getInstance());
}
@@ -85,6 +96,10 @@ public static Activity currentActivity() {
return currentActivity.get();
}
+ public static String getResString(int id) {
+ return getApp().getString(id);
+ }
+
private static class SimpleActivityLifecycleCallbacks implements ActivityLifecycleCallbacks {
diff --git a/app/src/main/java/com/stardust/scriptdroid/accessibility/AccessibilityInfoProvider.java b/app/src/main/java/com/stardust/scriptdroid/accessibility/AccessibilityInfoProvider.java
new file mode 100644
index 000000000..44477dba0
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/accessibility/AccessibilityInfoProvider.java
@@ -0,0 +1,51 @@
+package com.stardust.scriptdroid.accessibility;
+
+import android.accessibilityservice.AccessibilityService;
+import android.content.ComponentName;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.stardust.scriptdroid.App;
+import com.stardust.view.accessibility.AccessibilityDelegate;
+
+/**
+ * Created by Stardust on 2017/3/9.
+ */
+
+public class AccessibilityInfoProvider implements AccessibilityDelegate {
+
+ public static AccessibilityInfoProvider instance = new AccessibilityInfoProvider();
+
+ private String mLatestPackage = "";
+ private String mLatestActivity = "";
+
+ public synchronized String getLatestPackage() {
+ return mLatestPackage;
+ }
+
+ public synchronized String getLatestActivity() {
+ return mLatestActivity;
+ }
+
+ @Override
+ public boolean onAccessibilityEvent(AccessibilityService service, AccessibilityEvent event) {
+ setLatestComponent(event.getPackageName(), event.getClassName());
+ return false;
+ }
+
+ private synchronized void setLatestComponent(CharSequence latestPackage, CharSequence latestClass) {
+ if (latestPackage == null)
+ return;
+ mLatestPackage = latestPackage.toString();
+ if (latestClass == null)
+ return;
+ try {
+ ComponentName componentName = new ComponentName(latestPackage.toString(), latestClass.toString());
+ ActivityInfo activityInfo = App.getApp().getPackageManager().getActivityInfo(componentName, 0);
+ mLatestActivity = activityInfo.name;
+ } catch (PackageManager.NameNotFoundException ignored) {
+
+ }
+ }
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/bounds_assist/BoundsAssistClipList.java b/app/src/main/java/com/stardust/scriptdroid/bounds_assist/BoundsAssistClipList.java
deleted file mode 100644
index b4ebd98f8..000000000
--- a/app/src/main/java/com/stardust/scriptdroid/bounds_assist/BoundsAssistClipList.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.stardust.scriptdroid.bounds_assist;
-
-/**
- * Created by Stardust on 2017/2/4.
- */
-
-public interface BoundsAssistClipList {
-
- interface OnClipChangedListener {
- void onClipRemove(int position);
-
- void onClipInsert(int position);
-
- void onChange();
- }
-
- int size();
-
- String get(int i);
-
- void add(String clip);
-
- void setOnClipChangedListener(OnClipChangedListener listener);
-
-}
diff --git a/app/src/main/java/com/stardust/scriptdroid/bounds_assist/BoundsAssistant.java b/app/src/main/java/com/stardust/scriptdroid/bounds_assist/BoundsAssistant.java
deleted file mode 100644
index b23dbc84a..000000000
--- a/app/src/main/java/com/stardust/scriptdroid/bounds_assist/BoundsAssistant.java
+++ /dev/null
@@ -1,93 +0,0 @@
-package com.stardust.scriptdroid.bounds_assist;
-
-import android.accessibilityservice.AccessibilityService;
-import android.graphics.Rect;
-import android.preference.PreferenceManager;
-import android.view.accessibility.AccessibilityEvent;
-import android.view.accessibility.AccessibilityNodeInfo;
-import android.widget.Toast;
-
-import com.afollestad.materialdialogs.MaterialDialog;
-import com.stardust.scriptdroid.App;
-import com.stardust.scriptdroid.Pref;
-import com.stardust.scriptdroid.R;
-import com.stardust.scriptdroid.service.AccessibilityDelegate;
-import com.stardust.theme.dialog.ThemeColorMaterialDialogBuilder;
-
-
-/**
- * Created by Stardust on 2017/2/4.
- */
-
-public class BoundsAssistant implements AccessibilityDelegate {
-
- public static final String KEY_BOUNDS_ASSIST_ENABLE = "ASSIST_MODE_ENABLE";
-
- private static boolean assistModeEnable;
- private static BoundsAssistClipList boundsAssistClipList = SharedPrefBoundsAssistClipList.getInstance();
-
- public static boolean isAssistModeEnable() {
- return assistModeEnable;
- }
-
- public static void setAssistModeEnable(boolean assistModeEnable) {
- if (assistModeEnable && Pref.isFirstEnableAssistMode()) {
- showAssistModeInfoDialog();
- }
- BoundsAssistant.assistModeEnable = assistModeEnable;
- App.getStateObserver().setState(KEY_BOUNDS_ASSIST_ENABLE, assistModeEnable);
- }
-
- private static void showAssistModeInfoDialog() {
- new ThemeColorMaterialDialogBuilder(App.currentActivity())
- .title(R.string.text_alert)
- .content(R.string.assist_mode_notice)
- .positiveText(R.string.ok)
- .show();
- }
-
- private static void performAssistance(AccessibilityEvent event) {
- if (!assistModeEnable) {
- return;
- }
- if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_CLICKED || event.getEventType() == AccessibilityEvent.TYPE_VIEW_LONG_CLICKED) {
- AccessibilityNodeInfo nodeInfo = event.getSource();
- if (nodeInfo == null) {
- return;
- }
- nodeInfo.refresh();
- Rect rect = getBoundsInScreen(nodeInfo);
- saveAndAlertBounds(rect);
- nodeInfo.recycle();
- }
- }
-
- @Override
- public boolean onAccessibilityEvent(AccessibilityService service, AccessibilityEvent event) {
- performAssistance(event);
- return false;
- }
-
- public static Rect getBoundsInScreen(AccessibilityNodeInfo nodeInfo) {
- Rect rect = new Rect();
- nodeInfo.getBoundsInScreen(rect);
- return rect;
- }
-
- private static void saveAndAlertBounds(Rect rect) {
- String str = boundsToString(rect);
- boundsAssistClipList.add(str);
- Toast.makeText(App.getApp(), str, Toast.LENGTH_SHORT).show();
- }
-
- public static String boundsToString(Rect rect) {
- return rect.toString().replace('-', ',').replace(" ", "").substring(4);
- }
-
-
- static {
- assistModeEnable = PreferenceManager.getDefaultSharedPreferences(App.getApp()).getBoolean(KEY_BOUNDS_ASSIST_ENABLE, false);
- }
-
-
-}
diff --git a/app/src/main/java/com/stardust/scriptdroid/bounds_assist/SharedPrefBoundsAssistClipList.java b/app/src/main/java/com/stardust/scriptdroid/bounds_assist/SharedPrefBoundsAssistClipList.java
deleted file mode 100644
index 3a4bc1a89..000000000
--- a/app/src/main/java/com/stardust/scriptdroid/bounds_assist/SharedPrefBoundsAssistClipList.java
+++ /dev/null
@@ -1,89 +0,0 @@
-package com.stardust.scriptdroid.bounds_assist;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-
-import com.google.gson.Gson;
-import com.google.gson.reflect.TypeToken;
-import com.stardust.scriptdroid.App;
-
-import java.lang.reflect.Type;
-import java.util.LinkedList;
-import java.util.List;
-
-/**
- * Created by Stardust on 2017/2/4.
- */
-
-public class SharedPrefBoundsAssistClipList implements BoundsAssistClipList {
-
- private static final Gson GSON = new Gson();
-
-
- private static SharedPrefBoundsAssistClipList instance = new SharedPrefBoundsAssistClipList(App.getApp());
- private OnClipChangedListener mOnClipChangedListener;
-
- public static SharedPrefBoundsAssistClipList getInstance() {
- return instance;
- }
-
- private static final int MAX_SIZE = 10;
- private static final String SHARED_PREF_NAME = "SharedPrefBoundsAssistClipList";
- private SharedPreferences mSharedPreferences;
- private List mAssistClipList = new LinkedList<>();
-
- public SharedPrefBoundsAssistClipList(Context context) {
- mSharedPreferences = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE);
- readFromSharedPref();
- }
-
- private void readFromSharedPref() {
- Type type = new TypeToken>() {
- }.getType();
- List l = GSON.fromJson(mSharedPreferences.getString(SHARED_PREF_NAME, ""), type);
- if (l != null)
- mAssistClipList.addAll(l);
- }
-
- @Override
- public synchronized int size() {
- return mAssistClipList.size();
- }
-
- @Override
- public synchronized String get(int i) {
- return mAssistClipList.get(i);
- }
-
- @Override
- public synchronized void add(String clip) {
- mAssistClipList.add(0, clip);
- notifyInsert();
- if (mAssistClipList.size() > MAX_SIZE) {
- mAssistClipList.remove(mAssistClipList.size() - 1);
- notifyRemove();
- }
- syncWithSharedPref();
- }
-
- private void notifyRemove() {
- if (mOnClipChangedListener != null)
- mOnClipChangedListener.onClipRemove(mAssistClipList.size());
- }
-
- private void notifyInsert() {
- if (mOnClipChangedListener != null) {
- mOnClipChangedListener.onClipInsert(0);
- }
- }
-
- @Override
- public void setOnClipChangedListener(OnClipChangedListener listener) {
- mOnClipChangedListener = listener;
- }
-
- private void syncWithSharedPref() {
- mSharedPreferences.edit().putString(SHARED_PREF_NAME, GSON.toJson(mAssistClipList)).apply();
- }
-
-}
diff --git a/app/src/main/java/com/stardust/scriptdroid/droid/runtime/DroidRuntime.java b/app/src/main/java/com/stardust/scriptdroid/droid/runtime/DroidRuntime.java
index aa1e15e3e..fceaa9bda 100644
--- a/app/src/main/java/com/stardust/scriptdroid/droid/runtime/DroidRuntime.java
+++ b/app/src/main/java/com/stardust/scriptdroid/droid/runtime/DroidRuntime.java
@@ -16,21 +16,21 @@
import android.widget.Toast;
import com.afollestad.materialdialogs.MaterialDialog;
+import com.stardust.automator.AccessibilityEventCommandHost;
+import com.stardust.scriptdroid.Pref;
+import com.stardust.scriptdroid.droid.runtime.action.ActionTarget;
+import com.stardust.scriptdroid.tool.AccessibilityServiceTool;
+import com.stardust.scriptdroid.service.AccessibilityWatchDogService;
+import com.stardust.theme.dialog.ThemeColorMaterialDialogBuilder;
import com.jraska.console.Console;
import com.stardust.scriptdroid.App;
-import com.stardust.scriptdroid.Pref;
import com.stardust.scriptdroid.R;
import com.stardust.scriptdroid.droid.runtime.action.Action;
import com.stardust.scriptdroid.droid.runtime.action.ActionFactory;
import com.stardust.scriptdroid.droid.runtime.action.ActionPerformAccessibilityDelegate;
-import com.stardust.scriptdroid.droid.runtime.action.ActionTarget;
-import com.stardust.scriptdroid.droid.runtime.action.GetTextAction;
import com.stardust.scriptdroid.file.FileUtils;
-import com.stardust.scriptdroid.service.AccessibilityDelegate;
-import com.stardust.scriptdroid.service.AccessibilityWatchDogService;
import com.stardust.scriptdroid.tool.Shell;
import com.stardust.scriptdroid.ui.console.ConsoleActivity;
-import com.stardust.theme.dialog.ThemeColorMaterialDialogBuilder;
import com.stardust.view.accessibility.AccessibilityServiceUtils;
import java.io.File;
@@ -194,7 +194,7 @@ private void ensureAccessibilityServiceEnable() {
errorMessage = App.getApp().getString(R.string.text_auto_operate_service_enabled_but_not_running);
} else {
if (Pref.def().getBoolean(App.getApp().getString(R.string.key_enable_accessibility_service_by_root), false)) {
- if (!AccessibilityServiceUtils.enableAccessibilityServiceByRootAndWaitFor(App.getApp(), AccessibilityWatchDogService.class, 3000)) {
+ if (!AccessibilityServiceTool.enableAccessibilityServiceByRootAndWaitFor(AccessibilityWatchDogService.class, 3000)) {
errorMessage = App.getApp().getString(R.string.text_enable_accessibility_service_by_root_timeout);
}
} else {
@@ -208,10 +208,6 @@ private void ensureAccessibilityServiceEnable() {
}
}
- public List getTexts() {
- return performAction(new GetTextAction());
- }
-
public void toast(final String text) {
mUIHandler.post(new Runnable() {
@Override
@@ -229,16 +225,10 @@ public void sleep(long millis) {
}
}
- public void addAccessibilityDelegate(final Object delegate) {
+ public void addAccessibilityEventListener(final Object delegate) {
if (delegate == null)
return;
- AccessibilityWatchDogService.addDelegate(new AccessibilityDelegate() {
- @Override
- public boolean onAccessibilityEvent(AccessibilityService service, AccessibilityEvent event) {
- return false;
- }
- }, 500);
}
public Shell.CommandResult shell(String cmd, int root) {
@@ -255,6 +245,10 @@ public String getActivityName() {
return ActionPerformAccessibilityDelegate.getLatestActivity();
}
+ public UiSelector selector() {
+ return new UiSelector();
+ }
+
public boolean isStopped() {
return Thread.currentThread().isInterrupted();
}
diff --git a/app/src/main/java/com/stardust/scriptdroid/droid/runtime/UiSelector.java b/app/src/main/java/com/stardust/scriptdroid/droid/runtime/UiSelector.java
new file mode 100644
index 000000000..3da9d8a8a
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/droid/runtime/UiSelector.java
@@ -0,0 +1,228 @@
+package com.stardust.scriptdroid.droid.runtime;
+
+import android.accessibilityservice.AccessibilityService;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.util.Log;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.bezyapps.floatieslibrary.Floaty;
+import com.stardust.automator.AccessibilityEventCommandHost;
+import com.stardust.automator.ActionArgument;
+import com.stardust.automator.UiGlobalSelector;
+import com.stardust.automator.UiObject;
+import com.stardust.automator.UiObjectCollection;
+import com.stardust.automator.filter.DfsFilter;
+import com.stardust.scriptdroid.accessibility.AccessibilityInfoProvider;
+
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ARGUMENT_COLUMN_INT;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ARGUMENT_PROGRESS_VALUE;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ARGUMENT_ROW_INT;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ARGUMENT_SELECTION_END_INT;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ARGUMENT_SELECTION_START_INT;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.ACTION_CLEAR_FOCUS;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.ACTION_CLICK;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.ACTION_COLLAPSE;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.ACTION_COPY;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.ACTION_CUT;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.ACTION_DISMISS;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.ACTION_EXPAND;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.ACTION_FOCUS;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.ACTION_LONG_CLICK;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.ACTION_PASTE;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.ACTION_SELECT;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.ACTION_SET_SELECTION;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.ACTION_SET_TEXT;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CONTEXT_CLICK;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_DOWN;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_LEFT;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_RIGHT;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_UP;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SET_PROGRESS;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SHOW_ON_SCREEN;
+
+/**
+ * Created by Stardust on 2017/3/9.
+ */
+
+public class UiSelector extends UiGlobalSelector {
+
+ private class FindCommand implements AccessibilityEventCommandHost.Command {
+
+ UiObjectCollection result;
+
+ @Override
+ public void execute(AccessibilityService service, AccessibilityEvent event) {
+ AccessibilityNodeInfo root = service.getRootInActiveWindow();
+ if (root != null) {
+ result = findOf(root);
+ }
+ }
+ }
+
+ private static final String TAG = "UiSelector";
+
+ public UiObjectCollection find() {
+ FindCommand command = new FindCommand();
+ AccessibilityEventCommandHost.instance.executeAndWaitForEvent(command);
+ return command.result;
+ }
+
+
+ @NonNull
+ public UiObjectCollection untilFind() {
+ UiObjectCollection uiObjectCollection;
+ do {
+ uiObjectCollection = find();
+ } while (uiObjectCollection == null || uiObjectCollection.size() == 0);
+ return uiObjectCollection;
+ }
+
+ public UiObject findOne() {
+ UiObjectCollection collection = find();
+ if (collection == null || collection.size() == 0) {
+ return null;
+ }
+ return new UiObject(collection.get(0).getInfo());
+ }
+
+ @NonNull
+ public UiObject untilFindOne() {
+ UiObjectCollection collection = untilFind();
+ return new UiObject(collection.get(0).getInfo());
+ }
+
+ public UiSelector id(final String id) {
+ if (!id.contains(":")) {
+ addFilter(new DfsFilter() {
+ @Override
+ protected boolean isIncluded(AccessibilityNodeInfo nodeInfo) {
+ String fullId = AccessibilityInfoProvider.instance.getLatestPackage() + ":id/" + id;
+ return fullId.equals(nodeInfo.getViewIdResourceName());
+ }
+ });
+ } else {
+ super.id(id);
+ }
+ return this;
+ }
+
+
+ public boolean performAction(int action, ActionArgument... arguments) {
+ return untilFind().performAction(action, arguments);
+ }
+
+ public boolean click() {
+ return performAction(ACTION_CLICK);
+ }
+
+ public boolean longClick() {
+ return performAction(ACTION_LONG_CLICK);
+ }
+
+ public boolean accessibilityFocus() {
+ return performAction(ACTION_ACCESSIBILITY_FOCUS);
+ }
+
+ public boolean clearAccessibilityFocus() {
+ return performAction(ACTION_CLEAR_ACCESSIBILITY_FOCUS);
+ }
+
+ public boolean focus() {
+ return performAction(ACTION_FOCUS);
+ }
+
+ public boolean clearFocus() {
+ return performAction(ACTION_CLEAR_FOCUS);
+ }
+
+ public boolean copy() {
+ return performAction(ACTION_COPY);
+ }
+
+ public boolean paste() {
+ return performAction(ACTION_PASTE);
+ }
+
+ public boolean select() {
+ return performAction(ACTION_SELECT);
+ }
+
+ public boolean cut() {
+ return performAction(ACTION_CUT);
+ }
+
+ public boolean collapse() {
+ return performAction(ACTION_COLLAPSE);
+ }
+
+ public boolean expand() {
+ return performAction(ACTION_EXPAND);
+ }
+
+ public boolean dismiss() {
+ return performAction(ACTION_DISMISS);
+ }
+
+ public boolean show() {
+ return performAction(ACTION_SHOW_ON_SCREEN.getId());
+ }
+
+ public boolean scrollForward() {
+ return performAction(ACTION_SCROLL_FORWARD);
+ }
+
+ public boolean scrollBackward() {
+ return performAction(ACTION_SCROLL_BACKWARD);
+ }
+
+ public boolean scrollUp() {
+ return performAction(ACTION_SCROLL_UP.getId());
+ }
+
+ public boolean scrollDown() {
+ return performAction(ACTION_SCROLL_DOWN.getId());
+ }
+
+ public boolean scrollLeft() {
+ return performAction(ACTION_SCROLL_LEFT.getId());
+ }
+
+ public boolean scrollRight() {
+ return performAction(ACTION_SCROLL_RIGHT.getId());
+ }
+
+ public boolean contextClick() {
+ return performAction(ACTION_CONTEXT_CLICK.getId());
+ }
+
+ public boolean setSelection(int s, int e) {
+ return performAction(ACTION_SET_SELECTION,
+ new ActionArgument.IntActionArgument(ACTION_ARGUMENT_SELECTION_START_INT, s),
+ new ActionArgument.IntActionArgument(ACTION_ARGUMENT_SELECTION_END_INT, e));
+ }
+
+ public boolean setText(String text) {
+ return performAction(ACTION_SET_TEXT,
+ new ActionArgument.CharSequenceActionArgument(ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text));
+ }
+
+ public boolean setProgress(float value) {
+ return performAction(ACTION_SET_PROGRESS.getId(),
+ new ActionArgument.FloatActionArgument(ACTION_ARGUMENT_PROGRESS_VALUE, value));
+ }
+
+ public boolean scrollTo(int row, int column) {
+ return performAction(ACTION_SCROLL_TO_POSITION.getId(),
+ new ActionArgument.IntActionArgument(ACTION_ARGUMENT_ROW_INT, row),
+ new ActionArgument.IntActionArgument(ACTION_ARGUMENT_COLUMN_INT, column));
+ }
+
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/droid/runtime/action/Able.java b/app/src/main/java/com/stardust/scriptdroid/droid/runtime/action/Able.java
index ced0f07ca..4042385ea 100644
--- a/app/src/main/java/com/stardust/scriptdroid/droid/runtime/action/Able.java
+++ b/app/src/main/java/com/stardust/scriptdroid/droid/runtime/action/Able.java
@@ -9,7 +9,6 @@
* Created by Stardust on 2017/1/27.
*/
-@FunctionalInterface
public interface Able {
SparseArray ABLE_MAP = new SparseArrayEntries()
diff --git a/app/src/main/java/com/stardust/scriptdroid/droid/runtime/action/ActionPerformAccessibilityDelegate.java b/app/src/main/java/com/stardust/scriptdroid/droid/runtime/action/ActionPerformAccessibilityDelegate.java
index be4979c72..0189d3793 100644
--- a/app/src/main/java/com/stardust/scriptdroid/droid/runtime/action/ActionPerformAccessibilityDelegate.java
+++ b/app/src/main/java/com/stardust/scriptdroid/droid/runtime/action/ActionPerformAccessibilityDelegate.java
@@ -10,7 +10,7 @@
import com.stardust.scriptdroid.App;
import com.stardust.scriptdroid.droid.runtime.DroidRuntime;
-import com.stardust.scriptdroid.service.AccessibilityDelegate;
+import com.stardust.view.accessibility.AccessibilityDelegate;
/**
@@ -50,6 +50,7 @@ public boolean onAccessibilityEvent(AccessibilityService service, AccessibilityE
AccessibilityNodeInfo root = service.getRootInActiveWindow();
if (root == null) {
Log.v(TAG, "root = null");
+ return false;
}
Log.i(TAG, "perform action:" + action);
if (action.perform(root)) {
diff --git a/app/src/main/java/com/stardust/scriptdroid/droid/script/JavaScriptEngine.java b/app/src/main/java/com/stardust/scriptdroid/droid/script/JavaScriptEngine.java
index 242ac11d8..dbbeb28b8 100644
--- a/app/src/main/java/com/stardust/scriptdroid/droid/script/JavaScriptEngine.java
+++ b/app/src/main/java/com/stardust/scriptdroid/droid/script/JavaScriptEngine.java
@@ -33,8 +33,7 @@ private static String readInitScript() {
try {
return FileUtils.readString(App.getApp().getAssets().open("javasccript_engine_init.js"));
} catch (IOException e) {
- e.printStackTrace();
- return "Error: Unable to read init script.";
+ throw new RuntimeException(e);
}
}
diff --git a/app/src/main/java/com/stardust/scriptdroid/droid/script/RhinoJavaScriptEngine.java b/app/src/main/java/com/stardust/scriptdroid/droid/script/RhinoJavaScriptEngine.java
index a3e198873..3249d9f5a 100644
--- a/app/src/main/java/com/stardust/scriptdroid/droid/script/RhinoJavaScriptEngine.java
+++ b/app/src/main/java/com/stardust/scriptdroid/droid/script/RhinoJavaScriptEngine.java
@@ -1,7 +1,7 @@
package com.stardust.scriptdroid.droid.script;
-import com.stardust.scriptdroid.App;
import com.stardust.scriptdroid.droid.Droid;
+import com.stardust.scriptdroid.App;
import com.stardust.scriptdroid.droid.runtime.DroidRuntime;
import com.stardust.scriptdroid.droid.runtime.ScriptStopException;
@@ -58,10 +58,10 @@ private Context createContext() {
private void init(Context context, Scriptable scope) {
ScriptableObject.putProperty(scope, "context", App.getApp());
ScriptableObject.putProperty(scope, "__engine__", "rhino");
- context.evaluateString(scope, Init.getInitScript(), "", 1, null);
for (Map.Entry variable : mVariableMap.entrySet()) {
ScriptableObject.putProperty(scope, variable.getKey(), variable.getValue());
}
+ context.evaluateString(scope, Init.getInitScript(), "", 1, null);
mThreads.add(Thread.currentThread());
}
diff --git a/app/src/main/java/com/stardust/scriptdroid/droid/script/ScriptExecuteActivity.java b/app/src/main/java/com/stardust/scriptdroid/droid/script/ScriptExecuteActivity.java
index d4bd0b1f2..166dceda0 100644
--- a/app/src/main/java/com/stardust/scriptdroid/droid/script/ScriptExecuteActivity.java
+++ b/app/src/main/java/com/stardust/scriptdroid/droid/script/ScriptExecuteActivity.java
@@ -8,8 +8,6 @@
import com.stardust.scriptdroid.droid.Droid;
import com.stardust.scriptdroid.droid.RunningConfig;
-import static com.stardust.scriptdroid.droid.Droid.JAVA_SCRIPT_ENGINE;
-
/**
* Created by Stardust on 2017/2/5.
*/
@@ -43,9 +41,9 @@ private void handleIntent(Intent intent) {
}
private void runScript() {
- JAVA_SCRIPT_ENGINE.set("activity", this);
+ Droid.JAVA_SCRIPT_ENGINE.set("activity", this);
try {
- mResult = JAVA_SCRIPT_ENGINE.execute(mScript);
+ mResult = Droid.JAVA_SCRIPT_ENGINE.execute(mScript);
} catch (Exception e) {
mOnRunFinishedListener.onException(e);
finish();
@@ -61,7 +59,7 @@ public void finish() {
@Override
protected void onDestroy() {
super.onDestroy();
- JAVA_SCRIPT_ENGINE.set("activity", null);
- JAVA_SCRIPT_ENGINE.removeAndDestroy();
+ Droid.JAVA_SCRIPT_ENGINE.set("activity", null);
+ Droid.JAVA_SCRIPT_ENGINE.removeAndDestroy();
}
}
diff --git a/app/src/main/java/com/stardust/scriptdroid/droid/script/file/ScriptFile.java b/app/src/main/java/com/stardust/scriptdroid/droid/script/file/ScriptFile.java
index 8630f25f1..d7b93bef0 100644
--- a/app/src/main/java/com/stardust/scriptdroid/droid/script/file/ScriptFile.java
+++ b/app/src/main/java/com/stardust/scriptdroid/droid/script/file/ScriptFile.java
@@ -2,9 +2,9 @@
import android.os.Environment;
+import com.stardust.scriptdroid.droid.Droid;
import com.stardust.scriptdroid.App;
import com.stardust.scriptdroid.R;
-import com.stardust.scriptdroid.droid.Droid;
import java.io.File;
diff --git a/app/src/main/java/com/stardust/scriptdroid/external/floating_window/FloatingWindowManger.java b/app/src/main/java/com/stardust/scriptdroid/external/floating_window/FloatingWindowManger.java
new file mode 100644
index 000000000..6b3a1c235
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/external/floating_window/FloatingWindowManger.java
@@ -0,0 +1,32 @@
+package com.stardust.scriptdroid.external.floating_window;
+
+import android.content.Intent;
+import android.view.View;
+
+import com.stardust.scriptdroid.App;
+import com.stardust.scriptdroid.R;
+import com.stardust.scriptdroid.layout_inspector.view.LayoutInspectView;
+import com.stardust.view.Floaty;
+import com.stardust.view.ResizableFloaty;
+
+/**
+ * Created by Stardust on 2017/3/10.
+ */
+
+public class FloatingWindowManger {
+
+ public static void showFloatingWindow() {
+ if (!HoverMenuService.isServiceRunning())
+ App.getApp().startService(new Intent(App.getApp(), HoverMenuService.class));
+ }
+
+ public static boolean isFloatingWindowShowing() {
+ return HoverMenuService.isServiceRunning();
+ }
+
+
+ public static void hideFloatingWindow() {
+ if (HoverMenuService.isServiceRunning())
+ HoverMenuService.stopService();
+ }
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/external/floating_window/HoverMenuAdapter.java b/app/src/main/java/com/stardust/scriptdroid/external/floating_window/HoverMenuAdapter.java
new file mode 100644
index 000000000..988b29a05
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/external/floating_window/HoverMenuAdapter.java
@@ -0,0 +1,214 @@
+package com.stardust.scriptdroid.external.floating_window;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.support.annotation.ColorInt;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.TypedValue;
+import android.view.View;
+
+import com.stardust.scriptdroid.R;
+import com.stardust.scriptdroid.external.floating_window.content.MainMenuNavigatorContent;
+import com.stardust.scriptdroid.external.floating_window.content.RecordNavigatorContent;
+import com.stardust.scriptdroid.external.floating_window.content.ScritpListNavigatorContent;
+import com.stardust.theme.ThemeColorManagerCompat;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import io.mattcarroll.hover.NavigatorContent;
+
+/**
+ * Created by Stardust on 2017/3/11.
+ */
+
+public class HoverMenuAdapter implements io.mattcarroll.hover.HoverMenuAdapter {
+
+
+ public static final String ID_MAIN = "intro";
+ public static final String ID_RECORD = "record";
+ public static final String ID_SCRIPT_LIST = "script_list";
+ public static final String MENU_ID = "menu";
+ public static final String PLACEHOLDER_ID = "placeholder";
+
+ private final Context mContext;
+ private final List mTabIds;
+ private final Map mData = new LinkedHashMap<>();
+ private final Set mContentChangeListeners = new HashSet<>();
+ private View[] mViews;
+
+ public HoverMenuAdapter(@NonNull Context context) {
+ mContext = context;
+
+ mData.put(HoverMenuAdapter.ID_MAIN, new MainMenuNavigatorContent(context));
+ mData.put(HoverMenuAdapter.ID_RECORD, new RecordNavigatorContent(context));
+ mData.put(HoverMenuAdapter.ID_SCRIPT_LIST, new ScritpListNavigatorContent(context));
+
+ mTabIds = new ArrayList<>();
+ for (String tabId : mData.keySet()) {
+ mTabIds.add(tabId);
+ }
+ mViews = new View[mTabIds.size()];
+ }
+
+ @Override
+ public int getTabCount() {
+ return mTabIds.size();
+ }
+
+ @Override
+ public View getTabView(int index) {
+ mViews[index] = getTabViewInner(index);
+ return mViews[index];
+ }
+
+ private View getTabViewInner(int index) {
+ String menuItemId = mTabIds.get(index);
+ if (ID_MAIN.equals(menuItemId)) {
+ return createTabView(R.drawable.ic_android_eat_js);
+ } else if (ID_RECORD.equals(menuItemId)) {
+ return createTabView(R.drawable.ic_video_record);
+ } else if (ID_SCRIPT_LIST.equals(menuItemId)) {
+ return createTabView(R.drawable.ic_menu);
+ } else {
+ throw new RuntimeException("Unknown tab selected: " + index);
+ }
+ }
+
+ @Override
+ public long getTabId(int position) {
+ return position;
+ }
+
+ @Override
+ public NavigatorContent getNavigatorContent(int index) {
+ String tabId = mTabIds.get(index);
+ return mData.get(tabId);
+ }
+
+ @Override
+ public void addContentChangeListener(@NonNull ContentChangeListener listener) {
+ mContentChangeListeners.add(listener);
+ }
+
+ @Override
+ public void removeContentChangeListener(@NonNull ContentChangeListener listener) {
+ mContentChangeListeners.remove(listener);
+ }
+
+ public void selectTab(String id) {
+ int i = mTabIds.indexOf(id);
+ mViews[i].performClick();
+ }
+
+ protected void notifyDataSetChanged() {
+ for (ContentChangeListener listener : mContentChangeListeners) {
+ listener.onContentChange(this);
+ }
+ }
+
+ private View createTabView(@DrawableRes int tabBitmapRes) {
+ return createTabView(tabBitmapRes, ThemeColorManagerCompat.getColorPrimary(), Color.WHITE);
+ }
+
+ private View createTabView(@DrawableRes int tabBitmapRes, @ColorInt int backgroundColor, @ColorInt Integer iconColor) {
+ Resources resources = mContext.getResources();
+ int padding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, resources.getDisplayMetrics());
+
+ DemoTabView view = new DemoTabView(mContext, resources.getDrawable(R.drawable.tab_background), resources.getDrawable(tabBitmapRes));
+ view.setTabBackgroundColor(backgroundColor);
+ view.setTabForegroundColor(iconColor);
+ view.setPadding(padding, padding, padding, padding);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ view.setElevation(padding);
+ }
+ return view;
+ }
+
+ public class DemoTabView extends View {
+
+ private int mBackgroundColor;
+ private Integer mForegroundColor;
+
+ private Drawable mCircleDrawable;
+ private Drawable mIconDrawable;
+ private int mIconInsetLeft, mIconInsetTop, mIconInsetRight, mIconInsetBottom;
+
+ public DemoTabView(Context context, Drawable backgroundDrawable, Drawable iconDrawable) {
+ super(context);
+ mCircleDrawable = backgroundDrawable;
+ mIconDrawable = iconDrawable;
+ init();
+ }
+
+ private void init() {
+ int insetsDp = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, getContext().getResources().getDisplayMetrics());
+ mIconInsetLeft = mIconInsetTop = mIconInsetRight = mIconInsetBottom = insetsDp;
+ }
+
+ public void setTabBackgroundColor(@ColorInt int backgroundColor) {
+ mBackgroundColor = backgroundColor;
+ mCircleDrawable.setColorFilter(mBackgroundColor, PorterDuff.Mode.SRC_ATOP);
+ }
+
+ public void setTabForegroundColor(@ColorInt Integer foregroundColor) {
+ mForegroundColor = foregroundColor;
+ if (null != mForegroundColor) {
+ mIconDrawable.setColorFilter(mForegroundColor, PorterDuff.Mode.SRC_ATOP);
+ } else {
+ mIconDrawable.setColorFilter(null);
+ }
+ }
+
+ public void setIcon(@Nullable Drawable icon) {
+ mIconDrawable = icon;
+ if (null != mForegroundColor && null != mIconDrawable) {
+ mIconDrawable.setColorFilter(mForegroundColor, PorterDuff.Mode.SRC_ATOP);
+ }
+ updateIconBounds();
+
+ invalidate();
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ // Make circle as large as View minus padding.
+ mCircleDrawable.setBounds(getPaddingLeft(), getPaddingTop(), w - getPaddingRight(), h - getPaddingBottom());
+
+ // Re-size the icon as necessary.
+ updateIconBounds();
+
+ invalidate();
+ }
+
+ private void updateIconBounds() {
+ if (null != mIconDrawable) {
+ Rect bounds = new Rect(mCircleDrawable.getBounds());
+ bounds.set(bounds.left + mIconInsetLeft, bounds.top + mIconInsetTop, bounds.right - mIconInsetRight, bounds.bottom - mIconInsetBottom);
+ mIconDrawable.setBounds(bounds);
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ mCircleDrawable.draw(canvas);
+ if (null != mIconDrawable) {
+ mIconDrawable.draw(canvas);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/external/floating_window/HoverMenuService.java b/app/src/main/java/com/stardust/scriptdroid/external/floating_window/HoverMenuService.java
new file mode 100644
index 000000000..5b2b0a08a
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/external/floating_window/HoverMenuService.java
@@ -0,0 +1,191 @@
+package com.stardust.scriptdroid.external.floating_window;
+
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.IBinder;
+import android.support.annotation.Nullable;
+import android.util.Log;
+import android.view.View;
+import android.view.WindowManager;
+
+import com.stardust.hover.HoverMenuBuilder;
+import com.stardust.hover.SimpleHoverMenuTransitionListener;
+import com.stardust.hover.WindowHoverMenu;
+import com.stardust.scriptdroid.external.floating_window.view.FloatingLayoutBoundsView;
+import com.stardust.scriptdroid.external.floating_window.view.FloatingLayoutHierarchyView;
+import com.stardust.scriptdroid.layout_inspector.LayoutInspector;
+import com.stardust.scriptdroid.layout_inspector.NodeInfo;
+import com.stardust.theme.ThemeColorManagerCompat;
+import com.stardust.util.MessageEvent;
+
+import org.greenrobot.eventbus.EventBus;
+import org.greenrobot.eventbus.Subscribe;
+
+import java.lang.ref.WeakReference;
+
+import io.mattcarroll.hover.HoverMenu;
+import io.mattcarroll.hover.defaulthovermenu.window.WindowViewController;
+
+/**
+ * Created by Stardust on 2017/3/11.
+ */
+
+public class HoverMenuService extends Service {
+
+ public static final String MESSAGE_SHOW_AND_EXPAND_MENU = "MESSAGE_SHOW_AND_EXPAND_MENU";
+ public static final String MESSAGE_SHOW_LAYOUT_HIERARCHY = "MESSAGE_SHOW_LAYOUT_HIERARCHY";
+ public static final String MESSAGE_SHOW_LAYOUT_BOUNDS = "MESSAGE_SHOW_LAYOUT_BOUNDS";
+ public static final String MESSAGE_COLLAPSE_MENU = "MESSAGE_COLLAPSE_MENU";
+ public static final String MESSAGE_MENU_COLLAPSING = "MESSAGE_MENU_COLLAPSING";
+
+ private static WeakReference service;
+
+ public static boolean isServiceRunning() {
+ return service != null && service.get() != null;
+ }
+
+ public static void stopService() {
+ if (isServiceRunning()) {
+ service.get().stopSelf();
+ }
+ }
+
+
+ private static final String TAG = "HoverMenuService";
+
+ private static final String PREF_FILE = "hover_menu";
+ private static final String PREF_HOVER_MENU_VISUAL_STATE = "hover_menu_visual_state";
+
+ private boolean mIsRunning;
+ private SharedPreferences mPrefs;
+
+ private WindowViewController mWindowViewController;
+
+ private FloatingLayoutHierarchyView mFloatingLayoutHierarchyView;
+ private FloatingLayoutBoundsView mFloatingLayoutBoundsView;
+
+ private WindowHoverMenu mWindowHoverMenu;
+ private HoverMenu.OnExitListener mWindowHoverMenuMenuExitListener = new HoverMenu.OnExitListener() {
+ @Override
+ public void onExitByUserRequest() {
+ savePreferredLocation();
+ mWindowHoverMenu.hide();
+ stopSelf();
+ }
+ };
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ if (isServiceRunning()) {
+ stopSelf();
+ } else {
+ service = new WeakReference<>(this);
+ }
+ EventBus.getDefault().register(this);
+ mPrefs = getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE);
+ initViews();
+ }
+
+ private void initViews() {
+ mFloatingLayoutHierarchyView = new FloatingLayoutHierarchyView(this);
+ mFloatingLayoutBoundsView = new FloatingLayoutBoundsView(this);
+ initWindowMenu();
+ mWindowViewController.addView(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT, true, mFloatingLayoutHierarchyView);
+ mWindowViewController.addView(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT, true, mFloatingLayoutBoundsView);
+ }
+
+ private void initWindowMenu() {
+ mWindowHoverMenu = (WindowHoverMenu) new HoverMenuBuilder(this)
+ .displayWithinWindow()
+ .useAdapter(new HoverMenuAdapter(this))
+ .restoreVisualState(loadPreferredLocation())
+ .build();
+ mWindowHoverMenu.getHoverMenuView().setContentBackgroundColor(ThemeColorManagerCompat.getColorPrimary());
+ mWindowHoverMenu.addOnExitListener(mWindowHoverMenuMenuExitListener);
+ mWindowViewController = mWindowHoverMenu.getWindowViewController();
+ mWindowHoverMenu.setHoverMenuTransitionListener(new SimpleHoverMenuTransitionListener() {
+ @Override
+ public void onExpanding() {
+ captureCurrentWindow();
+ }
+
+ @Override
+ public void onCollapsing() {
+ LayoutInspector.getInstance().clearCapture();
+ EventBus.getDefault().post(new MessageEvent(MESSAGE_MENU_COLLAPSING));
+ }
+ });
+ }
+
+ private void captureCurrentWindow() {
+ NodeInfo nodeInfo = LayoutInspector.getInstance().captureCurrentWindow();
+ if (nodeInfo == null)
+ return;
+ mFloatingLayoutHierarchyView.setRootNode(nodeInfo);
+ mFloatingLayoutBoundsView.setRootNode(nodeInfo);
+ }
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (!mIsRunning) {
+ mIsRunning = true;
+ mWindowHoverMenu.show();
+ }
+ return START_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ mWindowHoverMenu.hide();
+ mIsRunning = false;
+ if (isServiceRunning() && service.get() == this)
+ service.clear();
+ }
+
+
+ private void savePreferredLocation() {
+ String memento = mWindowHoverMenu.getVisualState();
+ mPrefs.edit().putString(PREF_HOVER_MENU_VISUAL_STATE, memento).apply();
+ }
+
+ private String loadPreferredLocation() {
+ return mPrefs.getString(PREF_HOVER_MENU_VISUAL_STATE, null);
+ }
+
+ @Subscribe
+ public void onMessageEvent(MessageEvent event) {
+ switch (event.message) {
+ case MESSAGE_SHOW_AND_EXPAND_MENU:
+ mWindowHoverMenu.show();
+ break;
+ case MESSAGE_SHOW_LAYOUT_HIERARCHY:
+ mWindowHoverMenu.hide();
+ showView(mFloatingLayoutHierarchyView);
+ break;
+ case MESSAGE_SHOW_LAYOUT_BOUNDS:
+ mWindowHoverMenu.hide();
+ showView(mFloatingLayoutBoundsView);
+ break;
+ case MESSAGE_COLLAPSE_MENU:
+ mWindowHoverMenu.collapseMenu();
+ break;
+ }
+ }
+
+ private void showView(View view) {
+ view.setVisibility(View.VISIBLE);
+ mWindowViewController.makeTouchable(view);
+ }
+
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/external/floating_window/content/MainMenuNavigatorContent.java b/app/src/main/java/com/stardust/scriptdroid/external/floating_window/content/MainMenuNavigatorContent.java
new file mode 100644
index 000000000..d40f2b01c
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/external/floating_window/content/MainMenuNavigatorContent.java
@@ -0,0 +1,82 @@
+package com.stardust.scriptdroid.external.floating_window.content;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.view.View;
+import android.widget.Toast;
+
+import com.stardust.scriptdroid.App;
+import com.stardust.scriptdroid.R;
+import com.stardust.scriptdroid.external.floating_window.HoverMenuService;
+import com.stardust.scriptdroid.layout_inspector.LayoutInspector;
+import com.stardust.util.MessageEvent;
+import com.stardust.view.ViewBinder;
+import com.stardust.view.ViewBinding;
+
+import org.greenrobot.eventbus.EventBus;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import io.mattcarroll.hover.Navigator;
+import io.mattcarroll.hover.NavigatorContent;
+import io.mattcarroll.hover.defaulthovermenu.menus.Menu;
+import io.mattcarroll.hover.defaulthovermenu.menus.MenuAction;
+import io.mattcarroll.hover.defaulthovermenu.menus.MenuItem;
+import io.mattcarroll.hover.defaulthovermenu.menus.MenuListNavigatorContent;
+
+/**
+ * Created by Stardust on 2017/3/12.
+ */
+
+public class MainMenuNavigatorContent implements NavigatorContent {
+
+
+ private View mView;
+
+
+ public MainMenuNavigatorContent(Context context) {
+ mView = View.inflate(context, R.layout.floating_window_main_menu, null);
+ ViewBinder.bind(this);
+ }
+
+ @ViewBinding.Click(R.id.layout_hierarchy)
+ private void showLayoutHierarchy() {
+ if (LayoutInspector.getInstance().getCapture() == null) {
+ Toast.makeText(mView.getContext(), R.string.text_no_accessibility_permission_to_capture, Toast.LENGTH_SHORT).show();
+ } else {
+ EventBus.getDefault().post(new MessageEvent(HoverMenuService.MESSAGE_SHOW_LAYOUT_HIERARCHY));
+ }
+ }
+
+ @ViewBinding.Click(R.id.layout_bounds)
+ private void showLayoutBounds() {
+ if (LayoutInspector.getInstance().getCapture() == null) {
+ Toast.makeText(mView.getContext(), R.string.text_no_accessibility_permission_to_capture, Toast.LENGTH_SHORT).show();
+ } else {
+ EventBus.getDefault().post(new MessageEvent(HoverMenuService.MESSAGE_SHOW_LAYOUT_BOUNDS));
+ }
+ }
+
+ @NonNull
+ @Override
+ public View getView() {
+ return mView;
+ }
+
+ @Override
+ public void onShown(@NonNull Navigator navigator) {
+
+ }
+
+ @Override
+ public void onHidden() {
+
+ }
+
+ public View findViewById(int id) {
+ return mView.findViewById(id);
+ }
+
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/external/floating_window/content/RecordNavigatorContent.java b/app/src/main/java/com/stardust/scriptdroid/external/floating_window/content/RecordNavigatorContent.java
new file mode 100644
index 000000000..a082f5ea6
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/external/floating_window/content/RecordNavigatorContent.java
@@ -0,0 +1,40 @@
+package com.stardust.scriptdroid.external.floating_window.content;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.view.View;
+import android.widget.ImageView;
+
+import com.stardust.scriptdroid.R;
+
+import io.mattcarroll.hover.Navigator;
+import io.mattcarroll.hover.NavigatorContent;
+
+/**
+ * Created by Stardust on 2017/3/12.
+ */
+
+public class RecordNavigatorContent implements NavigatorContent {
+
+ private View mView;
+
+ public RecordNavigatorContent(Context context) {
+ mView = new View(context);
+ }
+
+ @NonNull
+ @Override
+ public View getView() {
+ return mView;
+ }
+
+ @Override
+ public void onShown(@NonNull Navigator navigator) {
+
+ }
+
+ @Override
+ public void onHidden() {
+
+ }
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/external/floating_window/content/ScritpListNavigatorContent.java b/app/src/main/java/com/stardust/scriptdroid/external/floating_window/content/ScritpListNavigatorContent.java
new file mode 100644
index 000000000..2695f5ec5
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/external/floating_window/content/ScritpListNavigatorContent.java
@@ -0,0 +1,43 @@
+package com.stardust.scriptdroid.external.floating_window.content;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.view.ContextThemeWrapper;
+import android.view.View;
+
+import com.stardust.scriptdroid.R;
+import com.stardust.scriptdroid.droid.script.file.SharedPrefScriptFileList;
+import com.stardust.scriptdroid.external.floating_window.view.FloatingScriptFileListView;
+
+import io.mattcarroll.hover.Navigator;
+import io.mattcarroll.hover.NavigatorContent;
+
+/**
+ * Created by Stardust on 2017/3/12.
+ */
+
+public class ScritpListNavigatorContent implements NavigatorContent {
+
+ private FloatingScriptFileListView mFloatingScriptFileListView;
+
+ public ScritpListNavigatorContent(Context context) {
+ mFloatingScriptFileListView = new FloatingScriptFileListView(new ContextThemeWrapper(context, R.style.AppTheme));
+ mFloatingScriptFileListView.setScriptFileList(SharedPrefScriptFileList.getInstance());
+ }
+
+ @NonNull
+ @Override
+ public View getView() {
+ return mFloatingScriptFileListView;
+ }
+
+ @Override
+ public void onShown(@NonNull Navigator navigator) {
+
+ }
+
+ @Override
+ public void onHidden() {
+
+ }
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/external/floating_window/view/FloatingLayoutBoundsView.java b/app/src/main/java/com/stardust/scriptdroid/external/floating_window/view/FloatingLayoutBoundsView.java
new file mode 100644
index 000000000..a2795dce9
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/external/floating_window/view/FloatingLayoutBoundsView.java
@@ -0,0 +1,75 @@
+package com.stardust.scriptdroid.external.floating_window.view;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.view.KeyEvent;
+import android.view.WindowManager;
+
+import com.afollestad.materialdialogs.MaterialDialog;
+import com.afollestad.materialdialogs.Theme;
+import com.stardust.scriptdroid.external.floating_window.HoverMenuService;
+import com.stardust.scriptdroid.layout_inspector.NodeInfo;
+import com.stardust.scriptdroid.layout_inspector.view.LayoutBoundsView;
+import com.stardust.scriptdroid.layout_inspector.view.NodeInfoView;
+import com.stardust.scriptdroid.layout_inspector.view.OnNodeInfoSelectListener;
+import com.stardust.util.MessageEvent;
+
+import org.greenrobot.eventbus.EventBus;
+
+/**
+ * Created by Stardust on 2017/3/12.
+ */
+
+public class FloatingLayoutBoundsView extends LayoutBoundsView {
+
+ private MaterialDialog mNodeInfoDialog;
+ private NodeInfoView mNodeInfoView;
+
+ public FloatingLayoutBoundsView(Context context) {
+ super(context);
+ init();
+ }
+
+ private void init() {
+ setOnNodeInfoSelectListener(new OnNodeInfoSelectListener() {
+ @Override
+ public void onNodeSelect(NodeInfo info) {
+ showNodeInfo(info);
+ }
+ });
+ setBackgroundColor(0x66000000);
+ setVisibility(GONE);
+ getPaint().setColor(0xFF222222);
+ getPaint().setStrokeWidth(2f);
+ }
+
+
+ private void showNodeInfo(NodeInfo info) {
+ ensureDialog();
+ mNodeInfoView.setNodeInfo(info);
+ mNodeInfoDialog.show();
+ }
+
+ private void ensureDialog() {
+ if (mNodeInfoDialog == null) {
+ mNodeInfoView = new NodeInfoView(getContext());
+ mNodeInfoDialog = new MaterialDialog.Builder(getContext())
+ .customView(mNodeInfoView, false)
+ .theme(Theme.LIGHT)
+ .build();
+ mNodeInfoDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
+ }
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ if (event.getKeyCode() == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
+ EventBus.getDefault().post(new MessageEvent(HoverMenuService.MESSAGE_SHOW_AND_EXPAND_MENU));
+ setVisibility(GONE);
+ return true;
+ }
+ return super.dispatchKeyEvent(event);
+ }
+
+
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/external/floating_window/view/FloatingLayoutHierarchyView.java b/app/src/main/java/com/stardust/scriptdroid/external/floating_window/view/FloatingLayoutHierarchyView.java
new file mode 100644
index 000000000..9b6fe66d2
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/external/floating_window/view/FloatingLayoutHierarchyView.java
@@ -0,0 +1,79 @@
+package com.stardust.scriptdroid.external.floating_window.view;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.WindowManager;
+
+import com.afollestad.materialdialogs.MaterialDialog;
+import com.afollestad.materialdialogs.Theme;
+import com.stardust.scriptdroid.R;
+import com.stardust.scriptdroid.external.floating_window.HoverMenuService;
+import com.stardust.scriptdroid.layout_inspector.NodeInfo;
+import com.stardust.scriptdroid.layout_inspector.view.LayoutHierarchyView;
+import com.stardust.scriptdroid.layout_inspector.view.NodeInfoView;
+import com.stardust.scriptdroid.layout_inspector.view.OnNodeInfoSelectListener;
+import com.stardust.util.MessageEvent;
+
+import org.greenrobot.eventbus.EventBus;
+
+/**
+ * Created by Stardust on 2017/3/12.
+ */
+
+public class FloatingLayoutHierarchyView extends LayoutHierarchyView {
+
+ private static final String TAG = "FloatingHierarchyView";
+
+ private MaterialDialog mNodeInfoDialog;
+ private NodeInfoView mNodeInfoView;
+
+ public FloatingLayoutHierarchyView(Context context) {
+ super(context);
+ init();
+ }
+
+ private void init() {
+ setBackgroundColor(0x99ffffff);
+ setVisibility(GONE);
+ setShowClickedNodeBounds(true);
+ getBoundsPaint().setStrokeWidth(3);
+ getBoundsPaint().setColor(0xFFD32F2F);
+ setOnNodeInfoLongClickListener(new OnNodeInfoSelectListener() {
+ @Override
+ public void onNodeSelect(NodeInfo info) {
+ showNodeInfo(info);
+ }
+ });
+ }
+
+ private void showNodeInfo(NodeInfo info) {
+ ensureDialog();
+ mNodeInfoView.setNodeInfo(info);
+ mNodeInfoDialog.show();
+ }
+
+ private void ensureDialog() {
+ if (mNodeInfoDialog == null) {
+ mNodeInfoView = new NodeInfoView(getContext());
+ mNodeInfoDialog = new MaterialDialog.Builder(getContext())
+ .customView(mNodeInfoView, false)
+ .theme(Theme.LIGHT)
+ .build();
+ mNodeInfoDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
+ }
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ if (event.getKeyCode() == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
+ EventBus.getDefault().post(new MessageEvent(HoverMenuService.MESSAGE_SHOW_AND_EXPAND_MENU));
+ setVisibility(GONE);
+ return true;
+ }
+ return super.dispatchKeyEvent(event);
+ }
+
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/external/floating_window/view/FloatingScriptFileListView.java b/app/src/main/java/com/stardust/scriptdroid/external/floating_window/view/FloatingScriptFileListView.java
new file mode 100644
index 000000000..14e7eb882
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/external/floating_window/view/FloatingScriptFileListView.java
@@ -0,0 +1,215 @@
+package com.stardust.scriptdroid.external.floating_window.view;
+
+import android.content.Context;
+import android.os.Environment;
+import android.support.annotation.NonNull;
+import android.support.v7.widget.DividerItemDecoration;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.afollestad.materialdialogs.MaterialDialog;
+import com.stardust.scriptdroid.App;
+import com.stardust.scriptdroid.R;
+import com.stardust.scriptdroid.droid.script.file.ScriptFile;
+import com.stardust.scriptdroid.droid.script.file.ScriptFileList;
+import com.stardust.scriptdroid.external.floating_window.HoverMenuService;
+import com.stardust.scriptdroid.tool.ViewTool;
+import com.stardust.scriptdroid.ui.main.operation.ScriptFileOperation;
+import com.stardust.scriptdroid.ui.main.operation.ScriptFileOperationPopupMenu;
+import com.stardust.theme.dialog.ThemeColorMaterialDialogBuilder;
+import com.stardust.util.MessageEvent;
+
+import org.greenrobot.eventbus.EventBus;
+import org.greenrobot.eventbus.Subscribe;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by Stardust on 2017/3/12.
+ */
+
+public class FloatingScriptFileListView extends RecyclerView {
+
+
+ private ScriptFileList mScriptFileList;
+
+ private final OnClickListener mOnItemClickListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ int position = getChildViewHolder(v).getAdapterPosition();
+ onItemClicked(v, position);
+ }
+ };
+
+ private final OnClickListener mOnEditIconClickListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ int position = getChildViewHolder((View) v.getParent()).getAdapterPosition();
+ onEditIconClick(v, position);
+ }
+ };
+
+ private final OnClickListener mOnMoreIconClickListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mOperateFileIndex = getChildViewHolder((View) v.getParent()).getAdapterPosition();
+ showOrDismissOperationPopupMenu(v);
+ }
+
+ };
+
+ private ScriptFileOperationPopupMenu mScriptFileOperationPopupMenu;
+ private int mOperateFileIndex;
+
+ public FloatingScriptFileListView(Context context) {
+ super(context);
+ init();
+ }
+
+ private void init() {
+ setAdapter(new Adapter());
+ setLayoutManager(new LinearLayoutManager(getContext()));
+ addItemDecoration(new DividerItemDecoration(getContext(), DividerItemDecoration.VERTICAL));
+ initScriptFileOperationPopupMenu();
+ }
+
+ @Subscribe
+ public void showMessage(ScriptFileOperation.ShowMessageEvent event) {
+ Toast.makeText(getContext(), event.messageResId, Toast.LENGTH_SHORT).show();
+ }
+
+ private void initScriptFileOperationPopupMenu() {
+ mScriptFileOperationPopupMenu = new ScriptFileOperationPopupMenu(getContext(), getScriptFileOperations());
+ mScriptFileOperationPopupMenu.setOnItemClickListener(new ScriptFileOperationPopupMenu.OnItemClickListener() {
+ @Override
+ public void onClick(View view, int position, ScriptFileOperation operation) {
+ operation.operate(FloatingScriptFileListView.this, mScriptFileList, mOperateFileIndex);
+ mScriptFileOperationPopupMenu.dismiss();
+ if (!(operation instanceof ScriptFileOperation.Rename))
+ EventBus.getDefault().post(new MessageEvent(HoverMenuService.MESSAGE_COLLAPSE_MENU));
+ }
+ });
+ }
+
+ protected List getScriptFileOperations() {
+ List scriptFileOperations = new ArrayList<>();
+ scriptFileOperations.add(new ScriptFileOperation.Run());
+ scriptFileOperations.add(new ScriptFileOperation.Rename() {
+ @Override
+ public void operate(final RecyclerView recyclerView, final ScriptFileList scriptFileList, final int position) {
+ String oldName = scriptFileList.get(position).name;
+ MaterialDialog dialog = new ThemeColorMaterialDialogBuilder(recyclerView.getContext())
+ .title(R.string.text_rename)
+ .checkBoxPrompt(App.getApp().getString(R.string.text_rename_file_meanwhile), false, null)
+ .input(App.getApp().getString(R.string.text_please_input_new_name), oldName, new MaterialDialog.InputCallback() {
+ @Override
+ public void onInput(@NonNull MaterialDialog dialog, CharSequence input) {
+ scriptFileList.rename(position, input.toString(), dialog.isPromptCheckBoxChecked());
+ recyclerView.getAdapter().notifyItemChanged(position);
+ }
+ })
+ .build();
+ dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
+ dialog.show();
+ }
+ });
+ scriptFileOperations.add(new ScriptFileOperation.OpenByOtherApp());
+ scriptFileOperations.add(new ScriptFileOperation.CreateShortcut());
+ scriptFileOperations.add(new ScriptFileOperation.Remove());
+ scriptFileOperations.add(new ScriptFileOperation.Delete());
+ return scriptFileOperations;
+ }
+
+ protected void onItemClicked(View v, int position) {
+ new ScriptFileOperation.Run().operate(this, mScriptFileList, position);
+ EventBus.getDefault().post(new MessageEvent(HoverMenuService.MESSAGE_COLLAPSE_MENU));
+ }
+
+ protected void onEditIconClick(View v, int position) {
+ new ScriptFileOperation.Edit().operate(this, mScriptFileList, position);
+ EventBus.getDefault().post(new MessageEvent(HoverMenuService.MESSAGE_COLLAPSE_MENU));
+ }
+
+ public void setScriptFileList(ScriptFileList scriptFileList) {
+ mScriptFileList = scriptFileList;
+ getAdapter().notifyDataSetChanged();
+ }
+
+ private void showOrDismissOperationPopupMenu(View v) {
+ if (mScriptFileOperationPopupMenu.isShowing()) {
+ mScriptFileOperationPopupMenu.dismiss();
+ } else {
+ mScriptFileOperationPopupMenu.show(v);
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ EventBus.getDefault().unregister(this);
+ }
+
+ @Subscribe
+ public void onMenuCollapsing(MessageEvent event) {
+ if (event.message.equals(HoverMenuService.MESSAGE_MENU_COLLAPSING) && mScriptFileOperationPopupMenu.isShowing()) {
+ mScriptFileOperationPopupMenu.dismiss();
+ }
+ }
+
+ private class Adapter extends RecyclerView.Adapter {
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View itemView = LayoutInflater.from(getContext()).inflate(R.layout.floating_script_list_recycler_view_item, parent, false);
+ return new ViewHolder(itemView);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ ScriptFile scriptFile = mScriptFileList.get(position);
+ holder.name.setText(scriptFile.name);
+ holder.path.setText(trimFilePath(scriptFile.path));
+ }
+
+ private final String SD_CARD_PATH = Environment.getExternalStorageDirectory().toString();
+
+ private String trimFilePath(String path) {
+ if (path.startsWith(SD_CARD_PATH)) {
+ path = path.substring(SD_CARD_PATH.length());
+ }
+ return path;
+ }
+
+ @Override
+ public int getItemCount() {
+ return mScriptFileList.size();
+ }
+ }
+
+ private class ViewHolder extends RecyclerView.ViewHolder {
+
+ TextView name, path;
+
+ ViewHolder(View itemView) {
+ super(itemView);
+ name = (TextView) itemView.findViewById(R.id.name);
+ path = (TextView) itemView.findViewById(R.id.path);
+ ViewTool.$(itemView, R.id.edit).setOnClickListener(mOnEditIconClickListener);
+ ViewTool.$(itemView, R.id.more).setOnClickListener(mOnMoreIconClickListener);
+ itemView.setOnClickListener(mOnItemClickListener);
+ }
+ }
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/external/notification/bounds_assist/BoundsAssistSwitchNotification.java b/app/src/main/java/com/stardust/scriptdroid/external/notification/bounds_assist/BoundsAssistSwitchNotification.java
deleted file mode 100644
index 1248d0004..000000000
--- a/app/src/main/java/com/stardust/scriptdroid/external/notification/bounds_assist/BoundsAssistSwitchNotification.java
+++ /dev/null
@@ -1,97 +0,0 @@
-package com.stardust.scriptdroid.external.notification.bounds_assist;
-
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.content.Context;
-import android.preference.PreferenceManager;
-import android.support.v4.app.NotificationCompat;
-
-import com.stardust.scriptdroid.App;
-import com.stardust.scriptdroid.R;
-import com.stardust.scriptdroid.bounds_assist.BoundsAssistant;
-import com.stardust.util.StateObserver;
-
-
-/**
- * Created by Stardust on 2017/2/2.
- */
-
-public class BoundsAssistSwitchNotification {
-
- private static final int NOTIFY_ID = 11126;
- public static final String KEY_BOUNDS_ASSIST_SWITCH_NOTIFICATION_ENABLE = "KEY_BOUNDS_ASSIST_SWITCH_NOTIFICATION_ENABLE";
-
- private static boolean enable = false;
-
- public static boolean isEnable() {
- return enable;
- }
-
- static {
- App.getStateObserver().register(KEY_BOUNDS_ASSIST_SWITCH_NOTIFICATION_ENABLE, new StateObserver.OnStateChangedListener() {
- @Override
- public void onStateChanged(boolean newState) {
- setEnable(newState);
- }
-
- @Override
- public void initState(boolean state) {
- enable = state;
- if (enable) {
- showNotification();
- }
- }
- });
- App.getStateObserver().register(BoundsAssistant.KEY_BOUNDS_ASSIST_ENABLE, new StateObserver.OnStateChangedListener() {
- @Override
- public void onStateChanged(boolean newState) {
- if (enable) {
- setEnable(false);
- setEnable(true);
- }
- }
-
- @Override
- public void initState(boolean state) {
-
- }
- });
- }
-
- public static void setEnable(boolean enable) {
- if (BoundsAssistSwitchNotification.enable == enable)
- return;
- BoundsAssistSwitchNotification.enable = enable;
- PreferenceManager.getDefaultSharedPreferences(App.getApp()).edit().putBoolean(KEY_BOUNDS_ASSIST_SWITCH_NOTIFICATION_ENABLE, enable).apply();
- if (enable) {
- showNotification();
- } else {
- hideNotification();
- }
- }
-
-
- private static void showNotification() {
- Notification notification = new NotificationCompat.Builder(App.getApp())
- .setAutoCancel(false)
- .setSmallIcon(R.drawable.ic_robot_head)
- .setDeleteIntent(BoundsAssistSwitchNotificationHandleService.getDeletePendingIntent())
- .setContentText(BoundsAssistant.isAssistModeEnable() ?
- App.getApp().getString(R.string.text_assist_mode_enabled) :
- App.getApp().getString(R.string.text_assist_mode_disabled))
- .setContentIntent(BoundsAssistSwitchNotificationHandleService.getStartIntent())
- .build();
- showNotification(notification);
- }
-
- private static void showNotification(Notification notification) {
- NotificationManager notificationManager = (NotificationManager) App.getApp().getSystemService(Context.NOTIFICATION_SERVICE);
- notificationManager.notify(NOTIFY_ID, notification);
- }
-
- private static void hideNotification() {
- NotificationManager notificationManager = (NotificationManager) App.getApp().getSystemService(Context.NOTIFICATION_SERVICE);
- notificationManager.cancel(NOTIFY_ID);
- }
-
-}
diff --git a/app/src/main/java/com/stardust/scriptdroid/external/notification/bounds_assist/BoundsAssistSwitchNotificationHandleService.java b/app/src/main/java/com/stardust/scriptdroid/external/notification/bounds_assist/BoundsAssistSwitchNotificationHandleService.java
deleted file mode 100644
index d3c081c39..000000000
--- a/app/src/main/java/com/stardust/scriptdroid/external/notification/bounds_assist/BoundsAssistSwitchNotificationHandleService.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package com.stardust.scriptdroid.external.notification.bounds_assist;
-
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.Intent;
-import android.os.IBinder;
-import android.support.annotation.Nullable;
-
-import com.stardust.scriptdroid.App;
-import com.stardust.scriptdroid.bounds_assist.BoundsAssistant;
-
-import static com.stardust.scriptdroid.external.notification.bounds_assist.BoundsAssistSwitchNotification.KEY_BOUNDS_ASSIST_SWITCH_NOTIFICATION_ENABLE;
-
-/**
- * Created by Stardust on 2017/2/2.
- */
-public class BoundsAssistSwitchNotificationHandleService extends Service {
-
-
- private static final String EXTRA_INTENT_VALID = "BoundsAssistSwitchNotificationHandleService.intentValid";
- private static final String EXTRA_ACTION = "action";
-
- private static final int ACTION_TOGGLE_ASSIST_MODE = 1;
- private static final int ACTION_CANCEL_ASSIST_MODE_NOTIFICATION = 2;
-
-
- public static PendingIntent getStartIntent() {
- Intent intent = new Intent(App.getApp(), BoundsAssistSwitchNotificationHandleService.class)
- .putExtra(EXTRA_INTENT_VALID, true)
- .putExtra(EXTRA_ACTION, ACTION_TOGGLE_ASSIST_MODE);
- return PendingIntent.getService(App.getApp(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- boolean intentValid = intent.getBooleanExtra(EXTRA_INTENT_VALID, false);
- if (intentValid) {
- performAction(intent.getIntExtra(EXTRA_ACTION, 0));
- }
- stopSelf();
- return super.onStartCommand(intent, flags, startId);
- }
-
- private void performAction(int action) {
- switch (action) {
- case ACTION_TOGGLE_ASSIST_MODE:
- BoundsAssistant.setAssistModeEnable(!BoundsAssistant.isAssistModeEnable());
- break;
- case ACTION_CANCEL_ASSIST_MODE_NOTIFICATION:
- App.getStateObserver().setState(KEY_BOUNDS_ASSIST_SWITCH_NOTIFICATION_ENABLE, false);
- break;
- }
- }
-
- @Nullable
- @Override
- public IBinder onBind(Intent intent) {
- return null;
- }
-
- public static PendingIntent getDeletePendingIntent() {
- Intent deleteIntent = new Intent(App.getApp(), BoundsAssistSwitchNotificationHandleService.class)
- .putExtra(EXTRA_INTENT_VALID, true)
- .putExtra(EXTRA_ACTION, ACTION_CANCEL_ASSIST_MODE_NOTIFICATION);
- return PendingIntent.getService(App.getApp(), 0, deleteIntent, PendingIntent.FLAG_ONE_SHOT);
- }
-}
diff --git a/app/src/main/java/com/stardust/scriptdroid/external/notification/record/ActionRecordSwitchHandleService.java b/app/src/main/java/com/stardust/scriptdroid/external/notification/record/ActionRecordSwitchHandleService.java
index cf27e8e32..9749bb334 100644
--- a/app/src/main/java/com/stardust/scriptdroid/external/notification/record/ActionRecordSwitchHandleService.java
+++ b/app/src/main/java/com/stardust/scriptdroid/external/notification/record/ActionRecordSwitchHandleService.java
@@ -8,9 +8,9 @@
import android.support.annotation.Nullable;
import android.widget.Toast;
+import com.stardust.scriptdroid.record.AccessibilityRecorderDelegate;
import com.stardust.scriptdroid.App;
import com.stardust.scriptdroid.R;
-import com.stardust.scriptdroid.record.AccessibilityRecorderDelegate;
import com.stardust.scriptdroid.service.AccessibilityWatchDogService;
import com.stardust.scriptdroid.service.VolumeChangeListenService;
import com.stardust.scriptdroid.ui.main.MainActivity;
diff --git a/app/src/main/java/com/stardust/scriptdroid/external/open/EditIntentActivity.java b/app/src/main/java/com/stardust/scriptdroid/external/open/EditIntentActivity.java
index 84f874a73..1a50f35d6 100644
--- a/app/src/main/java/com/stardust/scriptdroid/external/open/EditIntentActivity.java
+++ b/app/src/main/java/com/stardust/scriptdroid/external/open/EditIntentActivity.java
@@ -7,8 +7,8 @@
import android.widget.Toast;
import com.stardust.scriptdroid.ui.BaseActivity;
-import com.stardust.scriptdroid.R;
import com.stardust.scriptdroid.ui.edit.EditActivity;
+import com.stardust.scriptdroid.R;
/**
* Created by Stardust on 2017/2/2.
diff --git a/app/src/main/java/com/stardust/scriptdroid/external/open/ImportIntentActivity.java b/app/src/main/java/com/stardust/scriptdroid/external/open/ImportIntentActivity.java
index efe33c5d3..90252c74f 100644
--- a/app/src/main/java/com/stardust/scriptdroid/external/open/ImportIntentActivity.java
+++ b/app/src/main/java/com/stardust/scriptdroid/external/open/ImportIntentActivity.java
@@ -8,12 +8,12 @@
import android.widget.Toast;
import com.afollestad.materialdialogs.MaterialDialog;
-import com.stardust.scriptdroid.ui.BaseActivity;
-import com.stardust.scriptdroid.R;
import com.stardust.scriptdroid.droid.script.file.ScriptFile;
import com.stardust.scriptdroid.droid.script.file.SharedPrefScriptFileList;
+import com.stardust.scriptdroid.ui.BaseActivity;
import com.stardust.scriptdroid.ui.main.MainActivity;
import com.stardust.theme.dialog.ThemeColorMaterialDialogBuilder;
+import com.stardust.scriptdroid.R;
import java.io.File;
diff --git a/app/src/main/java/com/stardust/scriptdroid/external/open/RunIntentActivity.java b/app/src/main/java/com/stardust/scriptdroid/external/open/RunIntentActivity.java
index 62e6ec0ac..63b7fc115 100644
--- a/app/src/main/java/com/stardust/scriptdroid/external/open/RunIntentActivity.java
+++ b/app/src/main/java/com/stardust/scriptdroid/external/open/RunIntentActivity.java
@@ -7,8 +7,8 @@
import android.text.TextUtils;
import android.widget.Toast;
-import com.stardust.scriptdroid.R;
import com.stardust.scriptdroid.external.shortcut.ShortcutActivity;
+import com.stardust.scriptdroid.R;
/**
* Created by Stardust on 2017/2/22.
diff --git a/app/src/main/java/com/stardust/scriptdroid/external/shortcut/ShortcutActivity.java b/app/src/main/java/com/stardust/scriptdroid/external/shortcut/ShortcutActivity.java
index e303d7ba7..15e66f871 100644
--- a/app/src/main/java/com/stardust/scriptdroid/external/shortcut/ShortcutActivity.java
+++ b/app/src/main/java/com/stardust/scriptdroid/external/shortcut/ShortcutActivity.java
@@ -6,8 +6,8 @@
import android.text.TextUtils;
import android.widget.Toast;
-import com.stardust.scriptdroid.R;
import com.stardust.scriptdroid.droid.Droid;
+import com.stardust.scriptdroid.R;
import java.io.File;
import java.util.LinkedList;
diff --git a/app/src/main/java/com/stardust/scriptdroid/external/tile/BoundsAssistEnableTileService.java b/app/src/main/java/com/stardust/scriptdroid/external/tile/BoundsAssistEnableTileService.java
deleted file mode 100644
index 6c1f21042..000000000
--- a/app/src/main/java/com/stardust/scriptdroid/external/tile/BoundsAssistEnableTileService.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.stardust.scriptdroid.external.tile;
-
-import android.os.Build;
-import android.service.quicksettings.Tile;
-import android.service.quicksettings.TileService;
-import android.support.annotation.RequiresApi;
-
-import com.stardust.scriptdroid.bounds_assist.BoundsAssistant;
-
-/**
- * Created by Stardust on 2017/1/26.
- */
-
-@RequiresApi(api = Build.VERSION_CODES.N)
-public class BoundsAssistEnableTileService extends TileService {
-
- public void onClick() {
- BoundsAssistant.setAssistModeEnable(!BoundsAssistant.isAssistModeEnable());
- updateTile();
- }
-
- private void updateTile() {
- getQsTile().setState(BoundsAssistant.isAssistModeEnable() ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE);
- getQsTile().updateTile();
- }
-
-
- @Override
- public void onStartListening() {
- updateTile();
- }
-}
diff --git a/app/src/main/java/com/stardust/scriptdroid/file/SampleFileManager.java b/app/src/main/java/com/stardust/scriptdroid/file/SampleFileManager.java
index de7b97ba9..0d75c82df 100644
--- a/app/src/main/java/com/stardust/scriptdroid/file/SampleFileManager.java
+++ b/app/src/main/java/com/stardust/scriptdroid/file/SampleFileManager.java
@@ -2,13 +2,13 @@
import android.content.Context;
-import com.stardust.scriptdroid.App;
import com.stardust.scriptdroid.Pref;
-import com.stardust.scriptdroid.R;
import com.stardust.scriptdroid.droid.script.file.ScriptFile;
import com.stardust.scriptdroid.droid.script.file.ScriptFileList;
import com.stardust.scriptdroid.droid.script.file.SharedPrefScriptFileList;
import com.stardust.util.MapEntries;
+import com.stardust.scriptdroid.App;
+import com.stardust.scriptdroid.R;
import java.util.LinkedHashMap;
import java.util.Map;
diff --git a/app/src/main/java/com/stardust/scriptdroid/layout_inspector/LayoutInspector.java b/app/src/main/java/com/stardust/scriptdroid/layout_inspector/LayoutInspector.java
new file mode 100644
index 000000000..e966ad8b6
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/layout_inspector/LayoutInspector.java
@@ -0,0 +1,53 @@
+package com.stardust.scriptdroid.layout_inspector;
+
+import android.accessibilityservice.AccessibilityService;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.stardust.view.accessibility.AccessibilityDelegate;
+
+/**
+ * Created by Stardust on 2017/3/10.
+ */
+
+public class LayoutInspector implements AccessibilityDelegate {
+
+
+ private static LayoutInspector instance = new LayoutInspector();
+
+ public static LayoutInspector getInstance() {
+ return instance;
+ }
+
+ private AccessibilityNodeInfo mRootInActiveWindow;
+ private NodeInfo mCapture;
+
+ @Override
+ public boolean onAccessibilityEvent(AccessibilityService service, AccessibilityEvent event) {
+ mRootInActiveWindow = service.getRootInActiveWindow();
+ return false;
+ }
+
+ public AccessibilityNodeInfo getRootInActiveWindow() {
+ return mRootInActiveWindow;
+ }
+
+ public NodeInfo captureCurrentWindow() {
+ if (mRootInActiveWindow == null) {
+ mCapture = null;
+ } else {
+ mCapture = NodeInfo.capture(mRootInActiveWindow);
+ }
+ return mCapture;
+ }
+
+
+ public void clearCapture() {
+ mRootInActiveWindow = null;
+ mCapture = null;
+ }
+
+ public NodeInfo getCapture() {
+ return mCapture;
+ }
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/layout_inspector/NodeInfo.java b/app/src/main/java/com/stardust/scriptdroid/layout_inspector/NodeInfo.java
new file mode 100644
index 000000000..8a6057177
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/layout_inspector/NodeInfo.java
@@ -0,0 +1,88 @@
+package com.stardust.scriptdroid.layout_inspector;
+
+import android.graphics.Rect;
+import android.support.annotation.NonNull;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by Stardust on 2017/3/10.
+ */
+
+public class NodeInfo {
+
+ private List children = new ArrayList<>();
+
+ public String id;
+ public CharSequence contentDesc, className, packageName, text;
+ public int drawingOrder;
+ public boolean accessibilityFocused, checked, clickable, contextClickable, dismissable, editable, enabled,
+ focusable, longClickable, selected, scrollable, visibleToUser;
+ public String bounds;
+
+ private Rect mBoundsInScreen;
+
+ public NodeInfo(AccessibilityNodeInfoCompat node) {
+ id = simplifyId(node.getViewIdResourceName());
+ contentDesc = node.getContentDescription();
+ className = node.getClassName();
+ packageName = node.getPackageName();
+ text = node.getText();
+
+ drawingOrder = node.getDrawingOrder();
+
+ accessibilityFocused = node.isAccessibilityFocused();
+ checked = node.isChecked();
+ clickable = node.isClickable();
+ contextClickable = node.isContextClickable();
+ dismissable = node.isDismissable();
+ enabled = node.isEnabled();
+ editable = node.isEditable();
+ focusable = node.isFocusable();
+ longClickable = node.isLongClickable();
+ selected = node.isSelected();
+ scrollable = node.isScrollable();
+ visibleToUser = node.isVisibleToUser();
+
+ mBoundsInScreen = new Rect();
+ node.getBoundsInScreen(mBoundsInScreen);
+ bounds = boundsToString(mBoundsInScreen);
+ }
+
+ private String simplifyId(String idResourceName) {
+ if(idResourceName == null)
+ return null;
+ int i = idResourceName.indexOf('/');
+ return idResourceName.substring(i + 1);
+ }
+
+ public Rect getBoundsInScreen() {
+ return mBoundsInScreen;
+ }
+
+ public static String boundsToString(Rect rect) {
+ return rect.toString().replace('-', ',').replace(" ", "").substring(4);
+ }
+
+ public NodeInfo(AccessibilityNodeInfo node) {
+ this(new AccessibilityNodeInfoCompat(node));
+ }
+
+ public static NodeInfo capture(@NonNull AccessibilityNodeInfo root) {
+ NodeInfo nodeInfo = new NodeInfo(root);
+ for (int i = 0; i < root.getChildCount(); i++) {
+ AccessibilityNodeInfo child = root.getChild(i);
+ if (child != null) {
+ nodeInfo.children.add(capture(child));
+ }
+ }
+ return nodeInfo;
+ }
+
+ public List getChildren() {
+ return children;
+ }
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/layout_inspector/view/LayoutBoundsView.java b/app/src/main/java/com/stardust/scriptdroid/layout_inspector/view/LayoutBoundsView.java
new file mode 100644
index 000000000..303f7df45
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/layout_inspector/view/LayoutBoundsView.java
@@ -0,0 +1,121 @@
+package com.stardust.scriptdroid.layout_inspector.view;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.os.Build;
+import android.support.annotation.Nullable;
+import android.support.annotation.RequiresApi;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.stardust.scriptdroid.layout_inspector.NodeInfo;
+import com.stardust.scriptdroid.tool.ViewTool;
+
+/**
+ * Created by Stardust on 2017/3/10.
+ */
+
+public class LayoutBoundsView extends View {
+
+ private NodeInfo mRootNode;
+
+
+ private Paint mPaint;
+ private int mStatusBarHeight;
+ private OnNodeInfoSelectListener mOnNodeInfoSelectListener;
+
+ public LayoutBoundsView(Context context) {
+ super(context);
+ init();
+ }
+
+ public LayoutBoundsView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public LayoutBoundsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init();
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+ public LayoutBoundsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ init();
+ }
+
+
+ public void setOnNodeInfoSelectListener(OnNodeInfoSelectListener onNodeInfoSelectListener) {
+ mOnNodeInfoSelectListener = onNodeInfoSelectListener;
+ }
+
+ public void setRootNode(NodeInfo rootNode) {
+ mRootNode = rootNode;
+ }
+
+
+ private void init() {
+ mPaint = new Paint();
+ mPaint.setColor(Color.GREEN);
+ mPaint.setStyle(Paint.Style.STROKE);
+ mStatusBarHeight = ViewTool.getStatusBarHeight(getContext());
+ setWillNotDraw(false);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ draw(canvas, mRootNode);
+ }
+
+ public Paint getPaint() {
+ return mPaint;
+ }
+
+
+ private void draw(Canvas canvas, NodeInfo node) {
+ if (node == null)
+ return;
+ drawRect(canvas, node.getBoundsInScreen(), mStatusBarHeight, mPaint);
+ for (NodeInfo child : node.getChildren()) {
+ draw(canvas, child);
+ }
+ }
+
+ static void drawRect(Canvas canvas, Rect rect, int statusBarHeight, Paint paint) {
+ Rect offsetRect = new Rect(rect);
+ offsetRect.offset(0, -statusBarHeight);
+ canvas.drawRect(offsetRect, paint);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN && mRootNode != null) {
+ NodeInfo nodeInfo = findNodeAt(mRootNode, (int) event.getRawX(), (int) event.getRawY());
+ onNodeInfoClick(nodeInfo);
+ }
+ return super.onTouchEvent(event);
+
+ }
+
+ private void onNodeInfoClick(NodeInfo nodeInfo) {
+ if (mOnNodeInfoSelectListener != null) {
+ mOnNodeInfoSelectListener.onNodeSelect(nodeInfo);
+ }
+ }
+
+ private NodeInfo findNodeAt(NodeInfo node, int x, int y) {
+ for (NodeInfo child : node.getChildren()) {
+ if (child != null && child.getBoundsInScreen().contains(x, y)) {
+ return findNodeAt(child, x, y);
+ }
+ }
+ return node;
+ }
+
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/layout_inspector/view/LayoutHierarchyView.java b/app/src/main/java/com/stardust/scriptdroid/layout_inspector/view/LayoutHierarchyView.java
new file mode 100644
index 000000000..191445ca6
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/layout_inspector/view/LayoutHierarchyView.java
@@ -0,0 +1,210 @@
+package com.stardust.scriptdroid.layout_inspector.view;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.stardust.scriptdroid.R;
+import com.stardust.scriptdroid.layout_inspector.NodeInfo;
+import com.stardust.scriptdroid.tool.ViewTool;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+import pl.openrnd.multilevellistview.ItemInfo;
+import pl.openrnd.multilevellistview.MultiLevelListAdapter;
+import pl.openrnd.multilevellistview.MultiLevelListView;
+import pl.openrnd.multilevellistview.NestType;
+import pl.openrnd.multilevellistview.OnItemClickListener;
+
+/**
+ * Created by Stardust on 2017/3/10.
+ */
+
+public class LayoutHierarchyView extends MultiLevelListView {
+
+ private Adapter mAdapter;
+ private OnNodeInfoSelectListener mOnNodeInfoSelectListener;
+ private AdapterView.OnItemLongClickListener mOnItemLongClickListenerProxy = new AdapterView.OnItemLongClickListener() {
+ @Override
+ public boolean onItemLongClick(AdapterView> parent, View view, int position, long id) {
+ if (mOnNodeInfoSelectListener != null) {
+ mOnNodeInfoSelectListener.onNodeSelect(((ViewHolder) view.getTag()).nodeInfo);
+ return true;
+ }
+ return false;
+ }
+
+ };
+
+ private Paint mPaint;
+ private int mStatusBarHeight;
+ private NodeInfo mClickedNodeInfo;
+
+ private boolean mShowClickedNodeBounds;
+
+ public LayoutHierarchyView(Context context) {
+ super(context);
+ init();
+ }
+
+ public LayoutHierarchyView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public LayoutHierarchyView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init();
+ }
+
+ public void setShowClickedNodeBounds(boolean showClickedNodeBounds) {
+ mShowClickedNodeBounds = showClickedNodeBounds;
+ }
+
+
+ private void init() {
+ mAdapter = new Adapter();
+ setAdapter(mAdapter);
+ setNestType(NestType.MULTIPLE);
+ ((ListView) getChildAt(0)).setOnItemLongClickListener(mOnItemLongClickListenerProxy);
+ setWillNotDraw(false);
+ initPaint();
+ setOnItemClickListener(new OnItemClickListener() {
+ @Override
+ public void onItemClicked(MultiLevelListView parent, View view, Object item, ItemInfo itemInfo) {
+ mClickedNodeInfo = (NodeInfo) item;
+ invalidate();
+ }
+
+ @Override
+ public void onGroupItemClicked(MultiLevelListView parent, View view, Object item, ItemInfo itemInfo) {
+ mClickedNodeInfo = (NodeInfo) item;
+ invalidate();
+ }
+ });
+ }
+
+ private void initPaint() {
+ mPaint = new Paint();
+ mPaint.setColor(Color.DKGRAY);
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setAntiAlias(true);
+ mPaint.setStrokeWidth(3);
+ mStatusBarHeight = ViewTool.getStatusBarHeight(getContext());
+ }
+
+ public Paint getBoundsPaint() {
+ return mPaint;
+ }
+
+ public void setRootNode(NodeInfo rootNodeInfo) {
+ mAdapter.setDataItems(Collections.singletonList(rootNodeInfo));
+ mClickedNodeInfo = null;
+ }
+
+ public void setOnNodeInfoLongClickListener(final OnNodeInfoSelectListener onNodeInfoSelectListener) {
+ mOnNodeInfoSelectListener = onNodeInfoSelectListener;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (mShowClickedNodeBounds && mClickedNodeInfo != null) {
+ LayoutBoundsView.drawRect(canvas, mClickedNodeInfo.getBoundsInScreen(), mStatusBarHeight, mPaint);
+ }
+ }
+
+ private class ViewHolder {
+ TextView nameView;
+ TextView infoView;
+ ImageView arrowView;
+ LevelBeamView levelBeamView;
+ NodeInfo nodeInfo;
+
+ ViewHolder(View view) {
+ infoView = (TextView) view.findViewById(R.id.dataItemInfo);
+ nameView = (TextView) view.findViewById(R.id.dataItemName);
+ arrowView = (ImageView) view.findViewById(R.id.dataItemArrow);
+ levelBeamView = (LevelBeamView) view.findViewById(R.id.dataItemLevelBeam);
+ }
+ }
+
+
+ private class Adapter extends MultiLevelListAdapter {
+
+
+ @Override
+ public List> getSubObjects(Object object) {
+ return ((NodeInfo) object).getChildren();
+ }
+
+ @Override
+ public boolean isExpandable(Object object) {
+ return !((NodeInfo) object).getChildren().isEmpty();
+ }
+
+ @Override
+ public View getViewForObject(Object object, View convertView, ItemInfo itemInfo) {
+ NodeInfo nodeInfo = (NodeInfo) object;
+ ViewHolder viewHolder;
+ if (convertView == null) {
+ convertView = LayoutInflater.from(getContext()).inflate(R.layout.layout_hierarchy_view_item, null);
+ viewHolder = new ViewHolder(convertView);
+ convertView.setTag(viewHolder);
+ } else {
+ viewHolder = (ViewHolder) convertView.getTag();
+ }
+
+ viewHolder.nameView.setText(simplifyClassName(nodeInfo.className));
+ viewHolder.nodeInfo = nodeInfo;
+ if (viewHolder.infoView.getVisibility() == VISIBLE)
+ viewHolder.infoView.setText(getItemInfoDsc(itemInfo));
+
+ if (itemInfo.isExpandable() && !isAlwaysExpanded()) {
+ viewHolder.arrowView.setVisibility(View.VISIBLE);
+ viewHolder.arrowView.setImageResource(itemInfo.isExpanded() ?
+ R.drawable.arrow_up : R.drawable.arrow_down);
+ } else {
+ viewHolder.arrowView.setVisibility(View.GONE);
+ }
+
+ viewHolder.levelBeamView.setLevel(itemInfo.getLevel());
+
+ return convertView;
+ }
+
+ private String simplifyClassName(CharSequence className) {
+ String s = className.toString();
+ if (s.startsWith("android.widget.")) {
+ s = s.substring(15);
+ }
+ return s;
+ }
+
+
+ private String getItemInfoDsc(ItemInfo itemInfo) {
+ StringBuilder builder = new StringBuilder();
+
+ builder.append(String.format(Locale.getDefault(), "level[%d], idx in level[%d/%d]",
+ itemInfo.getLevel() + 1, /*Indexing starts from 0*/
+ itemInfo.getIdxInLevel() + 1 /*Indexing starts from 0*/,
+ itemInfo.getLevelSize()));
+
+ if (itemInfo.isExpandable()) {
+ builder.append(String.format(", expanded[%b]", itemInfo.isExpanded()));
+ }
+ return builder.toString();
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/layout_inspector/view/LayoutInspectView.java b/app/src/main/java/com/stardust/scriptdroid/layout_inspector/view/LayoutInspectView.java
new file mode 100644
index 000000000..21c7e5185
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/layout_inspector/view/LayoutInspectView.java
@@ -0,0 +1,92 @@
+package com.stardust.scriptdroid.layout_inspector.view;
+
+import android.content.Context;
+import android.os.Build;
+import android.support.annotation.AttrRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.RequiresApi;
+import android.support.annotation.StyleRes;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.ViewSwitcher;
+
+import com.stardust.scriptdroid.R;
+import com.stardust.scriptdroid.layout_inspector.LayoutInspector;
+import com.stardust.scriptdroid.layout_inspector.NodeInfo;
+
+/**
+ * Created by Stardust on 2017/3/10.
+ */
+
+public class LayoutInspectView extends FrameLayout {
+
+ private ViewSwitcher mViewSwitcher;
+ private NodeInfoView mNodeInfoView;
+ private LayoutHierarchyView mLayoutBoundsView;
+ private View mCurrentView;
+
+ public LayoutInspectView(@NonNull Context context) {
+ super(context);
+ init();
+ }
+
+ public LayoutInspectView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public LayoutInspectView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init();
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+ public LayoutInspectView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ init();
+ }
+
+ private void init() {
+ inflate(getContext(), R.layout.floating_window_expand, this);
+ mNodeInfoView = (NodeInfoView) findViewById(R.id.node_info);
+ mLayoutBoundsView = (LayoutHierarchyView) findViewById(R.id.bounds_view);
+ mViewSwitcher = (ViewSwitcher) findViewById(R.id.view_switcher);
+ mCurrentView = mNodeInfoView;
+ mLayoutBoundsView.setOnNodeInfoLongClickListener(new OnNodeInfoSelectListener() {
+ @Override
+ public void onNodeSelect(NodeInfo info) {
+ mNodeInfoView.setNodeInfo(info);
+ showNodeInfoView();
+ }
+ });
+ }
+
+ public void showNodeInfoView() {
+ if (mCurrentView != mNodeInfoView) {
+ mViewSwitcher.showPrevious();
+ mCurrentView = mNodeInfoView;
+ }
+ }
+
+ public void showLayoutBoundsView() {
+ mLayoutBoundsView.setRootNode(LayoutInspector.getInstance().captureCurrentWindow());
+ backToLayoutBoundsView();
+ }
+
+ public boolean isLayoutBoundsViewShowing() {
+ return mCurrentView == mLayoutBoundsView;
+ }
+
+ public boolean isNodeInfoViewShowing() {
+ return mCurrentView == mNodeInfoView;
+ }
+
+ public void backToLayoutBoundsView() {
+ if (mCurrentView != mLayoutBoundsView) {
+ mViewSwitcher.showNext();
+ mCurrentView = mLayoutBoundsView;
+ }
+ }
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/layout_inspector/view/LevelBeamView.java b/app/src/main/java/com/stardust/scriptdroid/layout_inspector/view/LevelBeamView.java
new file mode 100644
index 000000000..0981e833d
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/layout_inspector/view/LevelBeamView.java
@@ -0,0 +1,97 @@
+package com.stardust.scriptdroid.layout_inspector.view;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+
+import com.stardust.scriptdroid.R;
+
+/**
+ * Created by Stardust on 2017/3/10.
+ */
+
+public class LevelBeamView extends View {
+
+ private static final String TAG = "LevelBeamView";
+
+ private static final int[] colors = {
+ 0xff1abc9c,
+ 0xff3498db,
+ 0xffe67e22,
+ 0xff8e44ad,
+ 0xfff1c40f,
+ 0xff2ecc71,
+ };
+
+ private int mLevel;
+
+ private int mPaddingLeft, mPaddingRight;
+ private int mLinesWidth;
+ private int mLinesOffset;
+ private Paint mLinePaint;
+
+ public LevelBeamView(Context context) {
+ super(context);
+ init();
+ }
+
+ public LevelBeamView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public LevelBeamView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init();
+ }
+
+ public void setLevel(int level) {
+ mLevel = level;
+ requestLayout();
+ }
+
+ private void init() {
+ setWillNotDraw(false);
+ mPaddingLeft = (int) getResources().getDimension(R.dimen.level_beam_view_padding_left);
+ mPaddingRight = (int) getResources().getDimension(R.dimen.level_beam_view_padding_right);
+
+ mLinesWidth = (int) getResources().getDimension(R.dimen.level_beam_view_line_width);
+ mLinesOffset = (int) getResources().getDimension(R.dimen.level_beam_view_line_offset);
+
+ mLinePaint = new Paint();
+ mLinePaint.setAntiAlias(true);
+ mLinePaint.setColor(Color.RED);
+ mLinePaint.setStyle(Paint.Style.FILL);
+ mLinePaint.setStrokeWidth(mLinesWidth);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int width = mPaddingLeft + mPaddingRight + (mLevel + 1) * (mLinesWidth + mLinesOffset);
+ int height = View.MeasureSpec.getSize(heightMeasureSpec);
+ setMeasuredDimension(width, height);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ Log.i(TAG, "onDraw");
+ super.onDraw(canvas);
+ for (int lvl = 0; lvl <= mLevel; lvl++) {
+ float LINE_X = mPaddingLeft + lvl * mLinesWidth;
+ if (lvl >= 1) {
+ LINE_X += lvl * mLinesOffset;
+ }
+ mLinePaint.setColor(getColorForLevel(lvl));
+ canvas.drawLine(LINE_X, 0, LINE_X, canvas.getHeight(), mLinePaint);
+ }
+ }
+
+ private int getColorForLevel(int level) {
+ return colors[level % colors.length];
+ }
+
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/layout_inspector/view/NodeInfoView.java b/app/src/main/java/com/stardust/scriptdroid/layout_inspector/view/NodeInfoView.java
new file mode 100644
index 000000000..6fc378034
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/layout_inspector/view/NodeInfoView.java
@@ -0,0 +1,115 @@
+package com.stardust.scriptdroid.layout_inspector.view;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.Nullable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.stardust.scriptdroid.R;
+import com.stardust.scriptdroid.layout_inspector.NodeInfo;
+import com.stardust.scriptdroid.tool.ClipboardTool;
+
+import java.lang.reflect.Field;
+
+import de.codecrafters.tableview.TableDataAdapter;
+import de.codecrafters.tableview.TableView;
+import de.codecrafters.tableview.model.TableColumnWeightModel;
+import de.codecrafters.tableview.toolkit.SimpleTableDataAdapter;
+import de.codecrafters.tableview.toolkit.SimpleTableHeaderAdapter;
+import de.codecrafters.tableview.toolkit.TableDataRowBackgroundProviders;
+
+/**
+ * Created by Stardust on 2017/3/10.
+ */
+
+public class NodeInfoView extends TableView {
+
+ private static final Field[] fields = NodeInfo.class.getFields();
+ private String[][] mData = new String[fields.length][2];
+
+ public NodeInfoView(Context context) {
+ super(context);
+ init();
+ }
+
+ public NodeInfoView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public NodeInfoView(Context context, @Nullable AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init();
+ }
+
+ public void setNodeInfo(NodeInfo nodeInfo) {
+ for (int i = 0; i < fields.length; i++) {
+ try {
+ Object value = fields[i].get(nodeInfo);
+ mData[i][1] = value == null ? "null" : value.toString();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ getDataAdapter().notifyDataSetChanged();
+ }
+
+ private void init() {
+ initData();
+ setUpTableConfig();
+ setAdapter();
+ setUpStyle();
+ }
+
+ private void setAdapter() {
+ setDataAdapter(new TableDataAdapter(getContext(), mData) {
+ SimpleTableDataAdapter mSimpleTableDataAdapter = new SimpleTableDataAdapter(getContext(), mData);
+
+ @Override
+ public View getCellView(int rowIndex, int columnIndex, ViewGroup parentView) {
+ final TextView textView = (TextView) mSimpleTableDataAdapter.getCellView(rowIndex, columnIndex, parentView);
+ textView.setSingleLine(false);
+ textView.setMaxLines(3);
+ textView.setTextColor(0xcc000000);
+ textView.setTextIsSelectable(true);
+ textView.setOnLongClickListener(new OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View v) {
+ ClipboardTool.setClip(textView.getText());
+ Toast.makeText(getContext(), R.string.text_already_copy_to_clip, Toast.LENGTH_SHORT).show();
+ return true;
+ }
+
+ });
+ return textView;
+ }
+ });
+ }
+
+ private void setUpTableConfig() {
+ setColumnCount(2);
+ setColumnModel(new TableColumnWeightModel(2));
+ }
+
+ private void initData() {
+ for (int i = 0; i < mData.length; i++) {
+ mData[i][0] = fields[i].getName();
+ mData[i][1] = "";
+ }
+ }
+
+ private void setUpStyle() {
+ int colorEvenRows = Color.WHITE;
+ int colorOddRows = 0xffe7e7e7;
+ setDataRowBackgroundProvider(TableDataRowBackgroundProviders.alternatingRowColors(colorEvenRows, colorOddRows));
+ getChildAt(0).setVisibility(GONE);
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/stardust/scriptdroid/layout_inspector/view/OnNodeInfoSelectListener.java b/app/src/main/java/com/stardust/scriptdroid/layout_inspector/view/OnNodeInfoSelectListener.java
new file mode 100644
index 000000000..98564164d
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/layout_inspector/view/OnNodeInfoSelectListener.java
@@ -0,0 +1,12 @@
+package com.stardust.scriptdroid.layout_inspector.view;
+
+import com.stardust.scriptdroid.layout_inspector.NodeInfo;
+
+/**
+ * Created by Stardust on 2017/3/10.
+ */
+
+public interface OnNodeInfoSelectListener {
+ void onNodeSelect(NodeInfo info);
+
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/record/AccessibilityRecorderDelegate.java b/app/src/main/java/com/stardust/scriptdroid/record/AccessibilityRecorderDelegate.java
index 7f0ee3899..a1c717715 100644
--- a/app/src/main/java/com/stardust/scriptdroid/record/AccessibilityRecorderDelegate.java
+++ b/app/src/main/java/com/stardust/scriptdroid/record/AccessibilityRecorderDelegate.java
@@ -3,7 +3,7 @@
import android.accessibilityservice.AccessibilityService;
import android.view.accessibility.AccessibilityEvent;
-import com.stardust.scriptdroid.service.AccessibilityDelegate;
+import com.stardust.view.accessibility.AccessibilityDelegate;
import static com.stardust.scriptdroid.external.notification.record.ActionRecordSwitchView.PAUSED;
import static com.stardust.scriptdroid.external.notification.record.ActionRecordSwitchView.RECORDING;
diff --git a/app/src/main/java/com/stardust/scriptdroid/record/ActionRecorder.java b/app/src/main/java/com/stardust/scriptdroid/record/ActionRecorder.java
index 4daa2d612..8cb7fe8c5 100644
--- a/app/src/main/java/com/stardust/scriptdroid/record/ActionRecorder.java
+++ b/app/src/main/java/com/stardust/scriptdroid/record/ActionRecorder.java
@@ -7,13 +7,12 @@
import android.view.accessibility.AccessibilityNodeInfo;
import com.stardust.scriptdroid.droid.runtime.action.FilterAction;
+import com.stardust.scriptdroid.layout_inspector.NodeInfo;
import com.stardust.util.SparseArrayEntries;
+import com.stardust.view.accessibility.AccessibilityNodeInfoHelper;
import java.util.List;
-import static com.stardust.scriptdroid.bounds_assist.BoundsAssistant.boundsToString;
-import static com.stardust.scriptdroid.bounds_assist.BoundsAssistant.getBoundsInScreen;
-
/**
* Created by Stardust on 2017/2/14.
@@ -69,7 +68,7 @@ public void onAccessibilityEvent(AccessibilityService service, AccessibilityEven
AccessibilityNodeInfo source = event.getSource();
if (source == null)
return;
- String bounds = boundsToString(getBoundsInScreen(source));
+ String bounds = NodeInfo.boundsToString(AccessibilityNodeInfoHelper.getBoundsInScreen(source));
source.recycle();
onAccessibilityEvent(event, bounds, sb);
}
@@ -130,7 +129,7 @@ private void recycle(List list) {
private static int findInEditableList(List editableList, AccessibilityNodeInfo editable) {
int i = 0;
for (AccessibilityNodeInfo nodeInfo : editableList) {
- if (getBoundsInScreen(nodeInfo).equals(getBoundsInScreen(editable))) {
+ if (AccessibilityNodeInfoHelper.getBoundsInScreen(nodeInfo).equals(AccessibilityNodeInfoHelper.getBoundsInScreen(editable))) {
return i;
}
i++;
diff --git a/app/src/main/java/com/stardust/scriptdroid/record/inputevent/InputEventRecorder.java b/app/src/main/java/com/stardust/scriptdroid/record/inputevent/InputEventRecorder.java
index 4b265b8ee..ea92d84cb 100644
--- a/app/src/main/java/com/stardust/scriptdroid/record/inputevent/InputEventRecorder.java
+++ b/app/src/main/java/com/stardust/scriptdroid/record/inputevent/InputEventRecorder.java
@@ -1,20 +1,12 @@
package com.stardust.scriptdroid.record.inputevent;
import android.preference.PreferenceManager;
-import android.text.TextUtils;
-import android.util.Pair;
import com.stardust.scriptdroid.App;
-import com.stardust.scriptdroid.tool.Shell;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
-import java.io.StringReader;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
import jackpal.androidterm.ShellTermSession;
import jackpal.androidterm.emulatorview.TermSession;
diff --git a/app/src/main/java/com/stardust/scriptdroid/record/inputevent/InputEventToJsConverter.java b/app/src/main/java/com/stardust/scriptdroid/record/inputevent/InputEventToJsConverter.java
index 48ac9f3e5..0c173054d 100644
--- a/app/src/main/java/com/stardust/scriptdroid/record/inputevent/InputEventToJsConverter.java
+++ b/app/src/main/java/com/stardust/scriptdroid/record/inputevent/InputEventToJsConverter.java
@@ -2,7 +2,6 @@
import android.support.annotation.NonNull;
-import com.stardust.scriptdroid.droid.runtime.DroidRuntime;
import com.stardust.util.MapEntries;
import java.util.HashMap;
diff --git a/app/src/main/java/com/stardust/scriptdroid/record/inputevent/InputEventToSendEventConverter.java b/app/src/main/java/com/stardust/scriptdroid/record/inputevent/InputEventToSendEventConverter.java
index 39fec2bdc..74a95e085 100644
--- a/app/src/main/java/com/stardust/scriptdroid/record/inputevent/InputEventToSendEventConverter.java
+++ b/app/src/main/java/com/stardust/scriptdroid/record/inputevent/InputEventToSendEventConverter.java
@@ -3,8 +3,6 @@
import android.support.annotation.NonNull;
import java.text.DecimalFormat;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
/**
diff --git a/app/src/main/java/com/stardust/scriptdroid/service/AccessibilityWatchDogService.java b/app/src/main/java/com/stardust/scriptdroid/service/AccessibilityWatchDogService.java
index f7091438e..8acdfc77f 100644
--- a/app/src/main/java/com/stardust/scriptdroid/service/AccessibilityWatchDogService.java
+++ b/app/src/main/java/com/stardust/scriptdroid/service/AccessibilityWatchDogService.java
@@ -5,7 +5,9 @@
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;
+import com.stardust.view.accessibility.AccessibilityDelegate;
import com.stardust.scriptdroid.App;
+import com.stardust.scriptdroid.tool.AccessibilityServiceTool;
import com.stardust.view.accessibility.AccessibilityServiceUtils;
import java.lang.ref.WeakReference;
@@ -79,6 +81,7 @@ public void onAccessibilityEvent(AccessibilityEvent event) {
Log.v(TAG, "onAccessibilityEvent: " + event);
synchronized (mDelegates) {
for (Map.Entry entry : mDelegates.entrySet()) {
+ Log.v(TAG, "delegate: " + entry.getValue().getClass().getName());
if (entry.getValue().onAccessibilityEvent(this, event))
break;
}
@@ -101,7 +104,7 @@ public static void disable() {
if (instance != null && instance.get() != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
instance.get().disableSelf();
} else {
- AccessibilityServiceUtils.goToAccessibilitySetting(App.getApp());
+ AccessibilityServiceTool.goToAccessibilitySetting();
}
}
diff --git a/app/src/main/java/com/stardust/scriptdroid/service/VolumeChangeListenService.java b/app/src/main/java/com/stardust/scriptdroid/service/VolumeChangeListenService.java
index e024cc519..c667d7a95 100644
--- a/app/src/main/java/com/stardust/scriptdroid/service/VolumeChangeListenService.java
+++ b/app/src/main/java/com/stardust/scriptdroid/service/VolumeChangeListenService.java
@@ -8,9 +8,9 @@
import android.os.IBinder;
import android.support.annotation.Nullable;
-import com.stardust.scriptdroid.Pref;
import com.stardust.scriptdroid.external.notification.record.ActionRecordSwitchHandleService;
import com.stardust.scriptdroid.record.AccessibilityRecorderDelegate;
+import com.stardust.scriptdroid.Pref;
import java.lang.ref.WeakReference;
diff --git a/app/src/main/java/com/stardust/scriptdroid/tool/AccessibilityServiceTool.java b/app/src/main/java/com/stardust/scriptdroid/tool/AccessibilityServiceTool.java
index e2f3fb910..83a0f8b5e 100644
--- a/app/src/main/java/com/stardust/scriptdroid/tool/AccessibilityServiceTool.java
+++ b/app/src/main/java/com/stardust/scriptdroid/tool/AccessibilityServiceTool.java
@@ -1,19 +1,16 @@
package com.stardust.scriptdroid.tool;
-import android.accessibilityservice.AccessibilityService;
-import android.content.ComponentName;
import android.content.Context;
-import android.content.Intent;
-import android.provider.Settings;
-import android.text.TextUtils;
import android.widget.Toast;
-import com.stardust.scriptdroid.App;
import com.stardust.scriptdroid.Pref;
-import com.stardust.scriptdroid.R;
import com.stardust.scriptdroid.service.AccessibilityWatchDogService;
+import com.stardust.scriptdroid.App;
+import com.stardust.scriptdroid.R;
import com.stardust.view.accessibility.AccessibilityServiceUtils;
+import static com.stardust.view.accessibility.AccessibilityServiceUtils.isAccessibilityServiceEnabled;
+
/**
* Created by Stardust on 2017/1/26.
*/
@@ -22,12 +19,37 @@ public class AccessibilityServiceTool {
public static void enableAccessibilityService() {
if (Pref.def().getBoolean(App.getApp().getString(R.string.key_enable_accessibility_service_by_root), false)) {
- if (!AccessibilityServiceUtils.enableAccessibilityServiceByRootAndWaitFor(App.getApp(), AccessibilityWatchDogService.class, 3000)) {
- AccessibilityServiceUtils.goToAccessibilitySetting(App.getApp());
+ if (!enableAccessibilityServiceByRootAndWaitFor(AccessibilityWatchDogService.class, 3000)) {
+ goToAccessibilitySetting();
}
} else {
- AccessibilityServiceUtils.goToAccessibilitySetting(App.getApp());
+ goToAccessibilitySetting();
+ }
+ }
+
+ public static void goToAccessibilitySetting() {
+ Context context = App.getApp();
+ if (Pref.isFirstGoToAccessibilitySetting()) {
+ Toast.makeText(context, context.getString(R.string.text_please_choose) + context.getString(R.string._app_name), Toast.LENGTH_LONG).show();
}
+ AccessibilityServiceUtils.goToAccessibilitySetting(context);
}
+ public static boolean enableAccessibilityServiceByRootAndWaitFor(Class accessibilityService, int timeout) {
+ Shell.execCommand("settings put secure enabled_accessibility_services %accessibility:"
+ + App.getApp().getPackageName() + "/" + accessibilityService.getName(), true);
+ long millis = System.currentTimeMillis();
+ while (true) {
+ if (isAccessibilityServiceEnabled(App.getApp(), accessibilityService))
+ return true;
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ if (System.currentTimeMillis() - millis >= timeout) {
+ return false;
+ }
+ }
+ }
}
diff --git a/app/src/main/java/com/stardust/scriptdroid/tool/ClipboardTool.java b/app/src/main/java/com/stardust/scriptdroid/tool/ClipboardTool.java
new file mode 100644
index 000000000..afa28a9ef
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/tool/ClipboardTool.java
@@ -0,0 +1,19 @@
+package com.stardust.scriptdroid.tool;
+
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+
+import com.stardust.scriptdroid.App;
+
+/**
+ * Created by Stardust on 2017/3/10.
+ */
+
+public class ClipboardTool {
+
+
+ public static void setClip(CharSequence text) {
+ ((ClipboardManager) App.getApp().getSystemService(Context.CLIPBOARD_SERVICE)).setPrimaryClip(ClipData.newPlainText("", text));
+ }
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/tool/ImageSelector.java b/app/src/main/java/com/stardust/scriptdroid/tool/ImageSelector.java
index 91c81fc91..ad6db814c 100644
--- a/app/src/main/java/com/stardust/scriptdroid/tool/ImageSelector.java
+++ b/app/src/main/java/com/stardust/scriptdroid/tool/ImageSelector.java
@@ -3,14 +3,9 @@
import android.app.Activity;
import android.content.Intent;
import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
import android.net.Uri;
import android.provider.MediaStore;
-import android.support.annotation.NonNull;
-import android.support.v7.app.AppCompatActivity;
-import com.afollestad.materialdialogs.folderselector.FileChooserDialog;
import com.stardust.app.OnActivityResultDelegate;
import com.stardust.scriptdroid.R;
diff --git a/app/src/main/java/com/stardust/scriptdroid/tool/Shell.java b/app/src/main/java/com/stardust/scriptdroid/tool/Shell.java
index 0e0efddbc..c218f38b1 100644
--- a/app/src/main/java/com/stardust/scriptdroid/tool/Shell.java
+++ b/app/src/main/java/com/stardust/scriptdroid/tool/Shell.java
@@ -1,27 +1,17 @@
package com.stardust.scriptdroid.tool;
import android.app.Activity;
-import android.content.res.AssetFileDescriptor;
-import android.os.ParcelFileDescriptor;
import android.preference.PreferenceManager;
import android.util.Log;
import com.stardust.scriptdroid.App;
-import com.stericson.RootShell.RootShell;
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
import java.io.BufferedReader;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
-import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import jackpal.androidterm.ShellTermSession;
-import jackpal.androidterm.Term;
-import jackpal.androidterm.emulatorview.TermSession;
import jackpal.androidterm.util.TermSettings;
/**
diff --git a/app/src/main/java/com/stardust/scriptdroid/tool/ViewTool.java b/app/src/main/java/com/stardust/scriptdroid/tool/ViewTool.java
index 972c5a528..06c0bff43 100644
--- a/app/src/main/java/com/stardust/scriptdroid/tool/ViewTool.java
+++ b/app/src/main/java/com/stardust/scriptdroid/tool/ViewTool.java
@@ -1,7 +1,11 @@
package com.stardust.scriptdroid.tool;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Rect;
import android.support.annotation.IdRes;
import android.view.View;
+import android.view.Window;
/**
* Created by Stardust on 2017/1/24.
@@ -13,4 +17,13 @@ public class ViewTool {
public static V $(View view, @IdRes int resId) {
return (V) view.findViewById(resId);
}
+
+ public static int getStatusBarHeight(Context context) {
+ int result = 0;
+ int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
+ if (resourceId > 0) {
+ result = context.getResources().getDimensionPixelSize(resourceId);
+ }
+ return result;
+ }
}
diff --git a/app/src/main/java/com/stardust/scriptdroid/ui/BaseActivity.java b/app/src/main/java/com/stardust/scriptdroid/ui/BaseActivity.java
index a5ff74426..a8d9c01e6 100644
--- a/app/src/main/java/com/stardust/scriptdroid/ui/BaseActivity.java
+++ b/app/src/main/java/com/stardust/scriptdroid/ui/BaseActivity.java
@@ -4,7 +4,6 @@
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
-import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
diff --git a/app/src/main/java/com/stardust/scriptdroid/ui/console/ConsoleActivity.java b/app/src/main/java/com/stardust/scriptdroid/ui/console/ConsoleActivity.java
index 37be7626f..5909bec20 100644
--- a/app/src/main/java/com/stardust/scriptdroid/ui/console/ConsoleActivity.java
+++ b/app/src/main/java/com/stardust/scriptdroid/ui/console/ConsoleActivity.java
@@ -9,8 +9,8 @@
import android.view.MenuItem;
import android.widget.TextView;
-import com.jraska.console.Console;
import com.stardust.scriptdroid.ui.BaseActivity;
+import com.jraska.console.Console;
import com.stardust.scriptdroid.R;
/**
diff --git a/app/src/main/java/com/stardust/scriptdroid/ui/edit/EditActivity.java b/app/src/main/java/com/stardust/scriptdroid/ui/edit/EditActivity.java
index e95210a53..8aa6900fc 100644
--- a/app/src/main/java/com/stardust/scriptdroid/ui/edit/EditActivity.java
+++ b/app/src/main/java/com/stardust/scriptdroid/ui/edit/EditActivity.java
@@ -15,6 +15,15 @@
import com.afollestad.materialdialogs.DialogAction;
import com.afollestad.materialdialogs.MaterialDialog;
+import com.stardust.scriptdroid.Pref;
+import com.stardust.scriptdroid.droid.Droid;
+import com.stardust.scriptdroid.ui.edit.editor920.Editor920Activity;
+import com.stardust.scriptdroid.ui.edit.sidemenu.EditSideMenuFragment;
+import com.stardust.scriptdroid.ui.edit.sidemenu.FunctionListRecyclerView;
+import com.stardust.theme.dialog.ThemeColorMaterialDialogBuilder;
+import com.stardust.util.SparseArrayEntries;
+import com.stardust.view.ViewBinding;
+import com.stardust.widget.ToolbarMenuItem;
import com.jecelyin.editor.v2.common.Command;
import com.jecelyin.editor.v2.common.SaveListener;
import com.jecelyin.editor.v2.core.widget.TextView;
@@ -22,20 +31,10 @@
import com.jecelyin.editor.v2.view.EditorView;
import com.jecelyin.editor.v2.view.menu.MenuDef;
import com.stardust.scriptdroid.App;
-import com.stardust.scriptdroid.Pref;
import com.stardust.scriptdroid.R;
-import com.stardust.scriptdroid.droid.Droid;
import com.stardust.scriptdroid.ui.edit.completion.InputMethodEnhanceBar;
-import com.stardust.scriptdroid.ui.edit.editor920.Editor920Activity;
-import com.stardust.scriptdroid.ui.edit.sidemenu.AssistClipListRecyclerView;
-import com.stardust.scriptdroid.ui.edit.sidemenu.EditSideMenuFragment;
-import com.stardust.scriptdroid.ui.edit.sidemenu.FunctionListRecyclerView;
import com.stardust.theme.ThemeColorManager;
-import com.stardust.theme.dialog.ThemeColorMaterialDialogBuilder;
-import com.stardust.util.SparseArrayEntries;
import com.stardust.view.ViewBinder;
-import com.stardust.view.ViewBinding;
-import com.stardust.widget.ToolbarMenuItem;
import java.io.File;
@@ -154,15 +153,7 @@ public void onClick(FunctionListRecyclerView.Function function, int position) {
insertText(function.name);
mDrawerLayout.closeDrawer(GravityCompat.END);
}
- })
- .setOnClipClickListener(new AssistClipListRecyclerView.OnClipClickListener() {
- @Override
- public void onClick(String clip, int position) {
- insertText(clip);
- mDrawerLayout.closeDrawer(GravityCompat.END);
- }
});
-
}
private void setUpEditor() {
diff --git a/app/src/main/java/com/stardust/scriptdroid/ui/edit/completion/CodeCompletion.java b/app/src/main/java/com/stardust/scriptdroid/ui/edit/completion/CodeCompletion.java
index 95df5c71f..41304f59e 100644
--- a/app/src/main/java/com/stardust/scriptdroid/ui/edit/completion/CodeCompletion.java
+++ b/app/src/main/java/com/stardust/scriptdroid/ui/edit/completion/CodeCompletion.java
@@ -4,8 +4,8 @@
import android.text.TextUtils;
import android.text.TextWatcher;
-import com.jecelyin.editor.v2.core.widget.TextView;
import com.stardust.scriptdroid.Pref;
+import com.jecelyin.editor.v2.core.widget.TextView;
import java.util.ArrayList;
import java.util.Collection;
diff --git a/app/src/main/java/com/stardust/scriptdroid/ui/edit/sidemenu/AssistClipListRecyclerView.java b/app/src/main/java/com/stardust/scriptdroid/ui/edit/sidemenu/AssistClipListRecyclerView.java
deleted file mode 100644
index 25a60c2c6..000000000
--- a/app/src/main/java/com/stardust/scriptdroid/ui/edit/sidemenu/AssistClipListRecyclerView.java
+++ /dev/null
@@ -1,130 +0,0 @@
-package com.stardust.scriptdroid.ui.edit.sidemenu;
-
-import android.bug.WrapContentLinearLayoutManager;
-import android.content.Context;
-import android.support.annotation.Nullable;
-import android.support.v7.widget.RecyclerView;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import com.stardust.scriptdroid.R;
-import com.stardust.scriptdroid.bounds_assist.BoundsAssistClipList;
-import com.stardust.scriptdroid.bounds_assist.SharedPrefBoundsAssistClipList;
-import com.stardust.widget.ExpandableRecyclerView;
-
-/**
- * Created by Stardust on 2017/2/4.
- */
-
-public class AssistClipListRecyclerView extends ExpandableRecyclerView {
-
- public interface OnClipClickListener {
- void onClick(String clip, int position);
- }
-
- private BoundsAssistClipList mBoundsAssistClipList = SharedPrefBoundsAssistClipList.getInstance();
- private OnClipClickListener mOnClipClickListener;
-
- public AssistClipListRecyclerView(Context context) {
- super(context);
- init();
- }
-
- public AssistClipListRecyclerView(Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
- init();
- }
-
- public AssistClipListRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- init();
- }
-
- private void init() {
- setLayoutManager(new WrapContentLinearLayoutManager(getContext()));
- setAdapter(new Adapter());
- setOnChildClickListener(new OnChildClickListener() {
- @Override
- public void onClick(View view, int position) {
- if (mOnClipClickListener != null) {
- String clip = (String) ((ChildViewHolder) getChildViewHolder(view)).mTextView.getText();
- mOnClipClickListener.onClick(clip, position);
- }
- }
- });
- mBoundsAssistClipList.setOnClipChangedListener(new BoundsAssistClipList.OnClipChangedListener() {
- @Override
- public void onClipRemove(int position) {
- if (isExpanded())
- getAdapter().notifyItemRemoved(position);
- }
-
- @Override
- public void onClipInsert(int position) {
- if (isExpanded())
- getAdapter().notifyItemInserted(position);
- }
-
- @Override
- public void onChange() {
- getAdapter().notifyDataSetChanged();
- }
- });
- }
-
- public void setOnClipClickListener(OnClipClickListener onClipClickListener) {
- mOnClipClickListener = onClipClickListener;
- }
-
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- //防止因mAssistClipList持有OnClipChangedListener引用从而导致这个RecyclerView及其Context被引用,内存泄漏
- mBoundsAssistClipList.setOnClipChangedListener(null);
- }
-
- private class Adapter extends ExpandableRecyclerView.DefaultTitleAdapter {
-
- Adapter() {
- setIcon(R.drawable.ic_robot_head_green);
- setTitle(R.string.text_assist_clip);
- }
-
- @Override
- protected RecyclerView.ViewHolder onCreateChildViewHolder(ViewGroup parent, int viewType) {
- return new ChildViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.assist_clip_list_recycler_view_item, parent, false));
- }
-
- @Override
- protected void onBindChildViewHolder(RecyclerView.ViewHolder holder, int position) {
- ChildViewHolder viewHolder = (ChildViewHolder) holder;
- viewHolder.mTextView.setText(mBoundsAssistClipList.get(position));
- }
-
- @Override
- protected int getChildItemCount() {
- return mBoundsAssistClipList.size();
- }
-
- @Override
- protected int getChildItemViewType(int position) {
- return 0;
- }
-
- }
-
- private class ChildViewHolder extends RecyclerView.ViewHolder {
-
- TextView mTextView;
-
- ChildViewHolder(View itemView) {
- super(itemView);
- mTextView = (TextView) itemView.findViewById(R.id.clip);
- }
-
- }
-}
diff --git a/app/src/main/java/com/stardust/scriptdroid/ui/edit/sidemenu/EditSideMenuFragment.java b/app/src/main/java/com/stardust/scriptdroid/ui/edit/sidemenu/EditSideMenuFragment.java
index 0392d3e09..dba0cbb34 100644
--- a/app/src/main/java/com/stardust/scriptdroid/ui/edit/sidemenu/EditSideMenuFragment.java
+++ b/app/src/main/java/com/stardust/scriptdroid/ui/edit/sidemenu/EditSideMenuFragment.java
@@ -9,27 +9,23 @@
import android.view.View;
import android.view.ViewGroup;
+import com.stardust.app.Fragment;
+import com.stardust.scriptdroid.external.floating_window.FloatingWindowManger;
+import com.stardust.view.ViewBinding;
import com.stardust.scriptdroid.App;
-import com.stardust.scriptdroid.ui.help.DocumentationActivity;
import com.stardust.scriptdroid.R;
import com.stardust.scriptdroid.ui.console.ConsoleActivity;
-import com.stardust.scriptdroid.bounds_assist.BoundsAssistant;
-import com.stardust.scriptdroid.external.notification.bounds_assist.BoundsAssistSwitchNotification;
import com.stardust.scriptdroid.ui.help.HelpCatalogueActivity;
import com.stardust.view.ViewBinder;
-import com.stardust.view.ViewBinding;
-
-import static com.stardust.scriptdroid.external.notification.bounds_assist.BoundsAssistSwitchNotification.KEY_BOUNDS_ASSIST_SWITCH_NOTIFICATION_ENABLE;
/**
* Created by Stardust on 2017/2/4.
*/
-public class EditSideMenuFragment extends com.stardust.app.Fragment {
+public class EditSideMenuFragment extends Fragment {
private FunctionListRecyclerView.OnFunctionClickListener mOnFunctionClickListener;
- private AssistClipListRecyclerView.OnClipClickListener mOnClipClickListener;
public static EditSideMenuFragment setFragment(AppCompatActivity activity, int viewId) {
EditSideMenuFragment fragment = new EditSideMenuFragment();
@@ -37,7 +33,7 @@ public static EditSideMenuFragment setFragment(AppCompatActivity activity, int v
return fragment;
}
- private SwitchCompat mAssistServiceSwitch, mAssistServiceNotificationSwitch;
+ private SwitchCompat mFloatingWindowSwitch;
@Nullable
@Override
@@ -48,7 +44,7 @@ public View createView(LayoutInflater inflater, @Nullable ViewGroup container, @
@Override
public void onStart() {
super.onStart();
- if (mAssistServiceSwitch == null) {
+ if (mFloatingWindowSwitch == null) {
setUpUI();
}
syncSwitchState();
@@ -57,14 +53,9 @@ public void onStart() {
private void setUpUI() {
setUpSwitchCompat();
setUpFunctionList();
- setUpClipList();
ViewBinder.bind(this);
}
- private void setUpClipList() {
- AssistClipListRecyclerView assistClipListRecyclerView = $(R.id.assist_clip_list);
- assistClipListRecyclerView.setOnClipClickListener(mOnClipClickListener);
- }
private void setUpFunctionList() {
FunctionListRecyclerView functionListRecyclerView = $(R.id.function_list);
@@ -73,15 +64,11 @@ private void setUpFunctionList() {
}
private void syncSwitchState() {
- mAssistServiceSwitch.setChecked(BoundsAssistant.isAssistModeEnable());
- mAssistServiceNotificationSwitch.setChecked(BoundsAssistSwitchNotification.isEnable());
+ mFloatingWindowSwitch.setChecked(FloatingWindowManger.isFloatingWindowShowing());
}
private void setUpSwitchCompat() {
- mAssistServiceSwitch = $(R.id.sw_assist_service);
- mAssistServiceNotificationSwitch = $(R.id.sw_assist_service_notification);
- App.getStateObserver().register(BoundsAssistant.KEY_BOUNDS_ASSIST_ENABLE, mAssistServiceSwitch);
- App.getStateObserver().register(KEY_BOUNDS_ASSIST_SWITCH_NOTIFICATION_ENABLE, mAssistServiceNotificationSwitch);
+ mFloatingWindowSwitch = $(R.id.sw_floating_window);
}
@ViewBinding.Click(R.id.syntax_and_api)
@@ -94,24 +81,18 @@ private void startConsoleActivity() {
startActivity(new Intent(getContext(), ConsoleActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
}
- @ViewBinding.Check(R.id.sw_assist_service)
- private void setAssistServiceEnable(boolean enable) {
- BoundsAssistant.setAssistModeEnable(enable);
+ @ViewBinding.Check(R.id.sw_floating_window)
+ private void setFloatingWindowEnable(boolean enable) {
+ if (enable && !FloatingWindowManger.isFloatingWindowShowing()) {
+ FloatingWindowManger.showFloatingWindow();
+ } else if (!enable && FloatingWindowManger.isFloatingWindowShowing()) {
+ FloatingWindowManger.hideFloatingWindow();
+ }
}
- @ViewBinding.Click(R.id.assist_service)
+ @ViewBinding.Click(R.id.floating_window)
private void toggleAssistServiceSwitch() {
- mAssistServiceSwitch.toggle();
- }
-
- @ViewBinding.Check(R.id.sw_assist_service_notification)
- private void setAssistServiceNotificationEnable(boolean enable) {
- BoundsAssistSwitchNotification.setEnable(enable);
- }
-
- @ViewBinding.Click(R.id.assist_service_notification)
- private void toggleAssistServiceNotificationSwitch() {
- mAssistServiceNotificationSwitch.toggle();
+ mFloatingWindowSwitch.toggle();
}
public EditSideMenuFragment setOnFunctionClickListener(FunctionListRecyclerView.OnFunctionClickListener onFunctionClickListener) {
@@ -119,8 +100,4 @@ public EditSideMenuFragment setOnFunctionClickListener(FunctionListRecyclerView.
return this;
}
- public EditSideMenuFragment setOnClipClickListener(AssistClipListRecyclerView.OnClipClickListener onClipClickListener) {
- mOnClipClickListener = onClipClickListener;
- return this;
- }
}
diff --git a/app/src/main/java/com/stardust/scriptdroid/ui/error/ErrorReportActivity.java b/app/src/main/java/com/stardust/scriptdroid/ui/error/ErrorReportActivity.java
index 75ad8538f..4818f1965 100644
--- a/app/src/main/java/com/stardust/scriptdroid/ui/error/ErrorReportActivity.java
+++ b/app/src/main/java/com/stardust/scriptdroid/ui/error/ErrorReportActivity.java
@@ -14,8 +14,8 @@
import com.afollestad.materialdialogs.DialogAction;
import com.afollestad.materialdialogs.MaterialDialog;
import com.stardust.scriptdroid.ui.BaseActivity;
-import com.stardust.scriptdroid.R;
import com.stardust.theme.dialog.ThemeColorMaterialDialogBuilder;
+import com.stardust.scriptdroid.R;
import java.util.Timer;
import java.util.TimerTask;
diff --git a/app/src/main/java/com/stardust/scriptdroid/ui/error/IssueReportActivity.java b/app/src/main/java/com/stardust/scriptdroid/ui/error/IssueReportActivity.java
index 4d37905f3..83128fdd9 100644
--- a/app/src/main/java/com/stardust/scriptdroid/ui/error/IssueReportActivity.java
+++ b/app/src/main/java/com/stardust/scriptdroid/ui/error/IssueReportActivity.java
@@ -11,7 +11,6 @@
import com.heinrichreimersoftware.androidissuereporter.IssueReporterActivity;
import com.heinrichreimersoftware.androidissuereporter.model.github.GithubTarget;
import com.stardust.scriptdroid.R;
-import com.stardust.theme.ThemeColorManager;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
diff --git a/app/src/main/java/com/stardust/scriptdroid/ui/help/DocumentationActivity.java b/app/src/main/java/com/stardust/scriptdroid/ui/help/DocumentationActivity.java
index a29de0038..b25a330a5 100644
--- a/app/src/main/java/com/stardust/scriptdroid/ui/help/DocumentationActivity.java
+++ b/app/src/main/java/com/stardust/scriptdroid/ui/help/DocumentationActivity.java
@@ -3,13 +3,11 @@
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
-import android.support.v7.widget.Toolbar;
-import android.view.View;
import com.stardust.scriptdroid.ui.BaseActivity;
+import com.stardust.widget.CommonMarkdownView;
import com.stardust.scriptdroid.R;
import com.stardust.scriptdroid.file.FileUtils;
-import com.stardust.widget.CommonMarkdownView;
import java.io.IOException;
diff --git a/app/src/main/java/com/stardust/scriptdroid/ui/help/HelpCatalogueActivity.java b/app/src/main/java/com/stardust/scriptdroid/ui/help/HelpCatalogueActivity.java
index 540b208d8..3b6024fc0 100644
--- a/app/src/main/java/com/stardust/scriptdroid/ui/help/HelpCatalogueActivity.java
+++ b/app/src/main/java/com/stardust/scriptdroid/ui/help/HelpCatalogueActivity.java
@@ -12,12 +12,12 @@
import android.view.ViewGroup;
import android.widget.TextView;
+import com.stardust.scriptdroid.App;
+import com.stardust.scriptdroid.ui.BaseActivity;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
-import com.stardust.scriptdroid.App;
import com.stardust.scriptdroid.R;
import com.stardust.scriptdroid.file.FileUtils;
-import com.stardust.scriptdroid.ui.BaseActivity;
import org.json.JSONObject;
diff --git a/app/src/main/java/com/stardust/scriptdroid/ui/main/MainActivity.java b/app/src/main/java/com/stardust/scriptdroid/ui/main/MainActivity.java
index f97d455e9..ddcb84088 100644
--- a/app/src/main/java/com/stardust/scriptdroid/ui/main/MainActivity.java
+++ b/app/src/main/java/com/stardust/scriptdroid/ui/main/MainActivity.java
@@ -12,7 +12,6 @@
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
-import android.os.Environment;
import android.support.annotation.NonNull;
import android.support.design.widget.Snackbar;
import android.support.v4.widget.DrawerLayout;
@@ -29,37 +28,31 @@
import com.afollestad.materialdialogs.folderselector.FileChooserDialog;
import com.stardust.app.NotRemindAgainDialog;
import com.stardust.app.OnActivityResultDelegate;
-import com.stardust.scriptdroid.App;
-import com.stardust.scriptdroid.BuildConfig;
import com.stardust.scriptdroid.Pref;
-import com.stardust.scriptdroid.R;
-import com.stardust.scriptdroid.droid.runtime.DroidRuntime;
import com.stardust.scriptdroid.droid.script.file.ScriptFile;
import com.stardust.scriptdroid.droid.script.file.ScriptFileList;
import com.stardust.scriptdroid.droid.script.file.SharedPrefScriptFileList;
import com.stardust.scriptdroid.external.notification.record.ActionRecordSwitchNotification;
-import com.stardust.scriptdroid.file.FileUtils;
import com.stardust.scriptdroid.file.SampleFileManager;
import com.stardust.scriptdroid.record.inputevent.InputEventRecorder;
-import com.stardust.scriptdroid.record.inputevent.InputEventToJsConverter;
import com.stardust.scriptdroid.record.inputevent.InputEventToJsRecorder;
import com.stardust.scriptdroid.service.AccessibilityWatchDogService;
import com.stardust.scriptdroid.tool.AccessibilityServiceTool;
-import com.stardust.scriptdroid.tool.BackPressedHandler;
import com.stardust.scriptdroid.tool.ImageSelector;
-import com.stardust.scriptdroid.tool.Shell;
import com.stardust.scriptdroid.ui.BaseActivity;
-import com.stardust.scriptdroid.ui.main.operation.ScriptFileOperation;
import com.stardust.scriptdroid.ui.settings.SettingsActivity;
import com.stardust.theme.dialog.ThemeColorMaterialDialogBuilder;
-import com.stardust.view.ViewBinder;
import com.stardust.view.ViewBinding;
-import com.stardust.view.accessibility.AccessibilityServiceUtils;
import com.stardust.widget.SlidingUpPanel;
+import com.stardust.scriptdroid.BuildConfig;
+import com.stardust.scriptdroid.R;
+import com.stardust.scriptdroid.file.FileUtils;
+import com.stardust.scriptdroid.tool.BackPressedHandler;
+import com.stardust.scriptdroid.ui.main.operation.ScriptFileOperation;
+import com.stardust.view.ViewBinder;
+import com.stardust.view.accessibility.AccessibilityServiceUtils;
import java.io.File;
-import java.util.Timer;
-import java.util.TimerTask;
public class MainActivity extends BaseActivity implements FileChooserDialog.FileCallback {
@@ -85,7 +78,6 @@ protected void onCreate(Bundle savedInstanceState) {
checkPermissions();
registerReceivers();
handleIntent(getIntent());
- //Shell.test(this);
}
private void registerReceivers() {
diff --git a/app/src/main/java/com/stardust/scriptdroid/ui/main/ScriptListRecyclerView.java b/app/src/main/java/com/stardust/scriptdroid/ui/main/ScriptListRecyclerView.java
index c257f44e9..72d14671c 100644
--- a/app/src/main/java/com/stardust/scriptdroid/ui/main/ScriptListRecyclerView.java
+++ b/app/src/main/java/com/stardust/scriptdroid/ui/main/ScriptListRecyclerView.java
@@ -3,6 +3,7 @@
import android.content.Context;
import android.os.Environment;
import android.support.annotation.Nullable;
+import android.support.design.widget.Snackbar;
import android.support.v7.widget.DividerItemDecoration;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
@@ -14,17 +15,20 @@
import android.widget.TextView;
import com.afollestad.materialdialogs.MaterialDialog;
-import com.stardust.scriptdroid.R;
import com.stardust.scriptdroid.droid.script.file.ScriptFile;
import com.stardust.scriptdroid.droid.script.file.ScriptFileList;
+import com.stardust.scriptdroid.tool.ViewTool;
+import com.stardust.theme.dialog.ThemeColorMaterialDialogBuilder;
+import com.stardust.scriptdroid.R;
import com.stardust.scriptdroid.ui.main.operation.ScriptFileOperation;
import com.stardust.scriptdroid.ui.main.operation.ScriptFileOperationPopupMenu;
import com.stardust.scriptdroid.tool.ClassTool;
-import com.stardust.theme.dialog.ThemeColorMaterialDialogBuilder;
-import static com.stardust.scriptdroid.tool.ViewTool.$;
+import org.greenrobot.eventbus.EventBus;
+import org.greenrobot.eventbus.Subscribe;
-import static com.stardust.scriptdroid.ui.main.operation.ScriptFileOperation.*;
+import java.util.ArrayList;
+import java.util.List;
/**
* Created by Stardust on 2017/1/23.
@@ -38,16 +42,15 @@ public class ScriptListRecyclerView extends ThemeColorRecyclerView {
@Override
public void onClick(View v) {
int position = getChildViewHolder(v).getAdapterPosition();
- new ScriptFileOperation.Run().operate(ScriptListRecyclerView.this, mScriptFileList, position);
+ onItemClicked(v, position);
}
};
-
private final OnClickListener mOnEditIconClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
int position = getChildViewHolder((View) v.getParent()).getAdapterPosition();
- new ScriptFileOperation.Edit().operate(ScriptListRecyclerView.this, mScriptFileList, position);
+ onEditIconClick(v, position);
}
};
@@ -55,7 +58,6 @@ public void onClick(View v) {
@Override
public void onClick(View v) {
mOperateFileIndex = getChildViewHolder((View) v.getParent()).getAdapterPosition();
- //showOperationDialog(position);
showOrDismissOperationPopupMenu(v);
}
@@ -93,31 +95,40 @@ private void init() {
}
private void initScriptFileOperationPopupMenu() {
- mScriptFileOperationPopupMenu = new ScriptFileOperationPopupMenu(getContext());
+ mScriptFileOperationPopupMenu = new ScriptFileOperationPopupMenu(getContext(), getScriptFileOperations());
mScriptFileOperationPopupMenu.setOnItemClickListener(new ScriptFileOperationPopupMenu.OnItemClickListener() {
@Override
- public void onClick(View view, int position) {
- ScriptFileOperation.getOperation(position).operate(ScriptListRecyclerView.this, mScriptFileList, mOperateFileIndex);
+ public void onClick(View view, int position, ScriptFileOperation operation) {
+ operation.operate(ScriptListRecyclerView.this, mScriptFileList, mOperateFileIndex);
mScriptFileOperationPopupMenu.dismiss();
}
});
}
+ protected List getScriptFileOperations() {
+ List scriptFileOperations = new ArrayList<>();
+ scriptFileOperations.add(new ScriptFileOperation.Run());
+ scriptFileOperations.add(new ScriptFileOperation.Rename());
+ scriptFileOperations.add(new ScriptFileOperation.OpenByOtherApp());
+ scriptFileOperations.add(new ScriptFileOperation.CreateShortcut());
+ scriptFileOperations.add(new ScriptFileOperation.Remove());
+ scriptFileOperations.add(new ScriptFileOperation.Delete());
+ return scriptFileOperations;
+ }
+
+ protected void onItemClicked(View v, int position) {
+ new ScriptFileOperation.Run().operate(ScriptListRecyclerView.this, mScriptFileList, position);
+ }
+
+ protected void onEditIconClick(View v, int position) {
+ new ScriptFileOperation.Edit().operate(ScriptListRecyclerView.this, mScriptFileList, position);
+ }
+
public void setScriptFileList(ScriptFileList scriptFileList) {
mScriptFileList = scriptFileList;
getAdapter().notifyDataSetChanged();
}
- private void showOperationDialog(final int position) {
- new ThemeColorMaterialDialogBuilder(getContext()).items(ScriptFileOperation.getOperationNames())
- .itemsCallback(new MaterialDialog.ListCallback() {
- @Override
- public void onSelection(MaterialDialog dialog, View itemView, int operation, CharSequence text) {
- ScriptFileOperation.getOperation(operation).operate(ScriptListRecyclerView.this, mScriptFileList, position);
- }
- }).show();
- }
-
private void showOrDismissOperationPopupMenu(View v) {
if (mScriptFileOperationPopupMenu.isShowing()) {
mScriptFileOperationPopupMenu.dismiss();
@@ -126,6 +137,23 @@ private void showOrDismissOperationPopupMenu(View v) {
}
}
+ @Subscribe
+ public void showMessage(ScriptFileOperation.ShowMessageEvent event) {
+ Snackbar.make(this, event.messageResId, Snackbar.LENGTH_SHORT).show();
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ EventBus.getDefault().unregister(this);
+ }
+
private class Adapter extends RecyclerView.Adapter {
@Override
@@ -164,18 +192,10 @@ private class ViewHolder extends RecyclerView.ViewHolder {
super(itemView);
name = (TextView) itemView.findViewById(R.id.name);
path = (TextView) itemView.findViewById(R.id.path);
- $(itemView, R.id.edit).setOnClickListener(mOnEditIconClickListener);
- $(itemView, R.id.more).setOnClickListener(mOnMoreIconClickListener);
+ ViewTool.$(itemView, R.id.edit).setOnClickListener(mOnEditIconClickListener);
+ ViewTool.$(itemView, R.id.more).setOnClickListener(mOnMoreIconClickListener);
itemView.setOnClickListener(mOnItemClickListener);
}
}
- static {
- loadScriptFileOperations();
- }
-
- private static void loadScriptFileOperations() {
- ClassTool.loadClasses(Run.class, Rename.class, OpenByOtherApp.class, CreateShortcut.class, Remove.class, Delete.class);
- }
-
}
diff --git a/app/src/main/java/com/stardust/scriptdroid/ui/main/SlideMenuFragment.java b/app/src/main/java/com/stardust/scriptdroid/ui/main/SlideMenuFragment.java
index 2d880349d..fc9c742b7 100644
--- a/app/src/main/java/com/stardust/scriptdroid/ui/main/SlideMenuFragment.java
+++ b/app/src/main/java/com/stardust/scriptdroid/ui/main/SlideMenuFragment.java
@@ -13,18 +13,15 @@
import com.stardust.app.Fragment;
import com.stardust.scriptdroid.App;
import com.stardust.scriptdroid.R;
-import com.stardust.scriptdroid.bounds_assist.BoundsAssistant;
import com.stardust.scriptdroid.droid.Droid;
-import com.stardust.scriptdroid.external.notification.bounds_assist.BoundsAssistSwitchNotification;
+import com.stardust.scriptdroid.external.floating_window.FloatingWindowManger;
import com.stardust.scriptdroid.service.AccessibilityWatchDogService;
import com.stardust.scriptdroid.tool.AccessibilityServiceTool;
import com.stardust.scriptdroid.ui.console.ConsoleActivity;
import com.stardust.scriptdroid.ui.help.HelpCatalogueActivity;
import com.stardust.view.ViewBinder;
import com.stardust.view.ViewBinding;
-import com.stardust.view.accessibility.AccessibilityServiceUtils;
-import static com.stardust.scriptdroid.external.notification.bounds_assist.BoundsAssistSwitchNotification.KEY_BOUNDS_ASSIST_SWITCH_NOTIFICATION_ENABLE;
/**
* Created by Stardust on 2017/1/30.
@@ -38,7 +35,7 @@ public static void setFragment(AppCompatActivity activity, int viewId) {
activity.getSupportFragmentManager().beginTransaction().replace(viewId, fragment).commit();
}
- private SwitchCompat mAutoOperateServiceSwitch, mAssistServiceSwitch, mAssistServiceNotificationSwitch;
+ private SwitchCompat mAutoOperateServiceSwitch, mAssistServiceSwitch;
@Nullable
@Override
@@ -64,16 +61,12 @@ public void run() {
mAutoOperateServiceSwitch.setChecked(AccessibilityWatchDogService.isEnable());
}
}, 450);
- mAssistServiceSwitch.setChecked(BoundsAssistant.isAssistModeEnable());
- mAssistServiceNotificationSwitch.setChecked(BoundsAssistSwitchNotification.isEnable());
+ mAssistServiceSwitch.setChecked(FloatingWindowManger.isFloatingWindowShowing());
}
private void setUpSwitchCompat() {
mAutoOperateServiceSwitch = $(R.id.sw_auto_operate_service);
- mAssistServiceSwitch = $(R.id.sw_assist_service);
- mAssistServiceNotificationSwitch = $(R.id.sw_assist_service_notification);
- App.getStateObserver().register(BoundsAssistant.KEY_BOUNDS_ASSIST_ENABLE, mAssistServiceSwitch);
- App.getStateObserver().register(KEY_BOUNDS_ASSIST_SWITCH_NOTIFICATION_ENABLE, mAssistServiceNotificationSwitch);
+ mAssistServiceSwitch = $(R.id.sw_floating_window);
}
@@ -101,25 +94,20 @@ private void setAutoOperateServiceEnable(boolean enable) {
}
}
- @ViewBinding.Check(R.id.sw_assist_service)
- private void setAssistServiceEnable(boolean enable) {
- BoundsAssistant.setAssistModeEnable(enable);
+ @ViewBinding.Check(R.id.sw_floating_window)
+ private void setFloatingWindowEnable(boolean enable) {
+ if (enable && !FloatingWindowManger.isFloatingWindowShowing()) {
+ FloatingWindowManger.showFloatingWindow();
+ } else if (!enable && FloatingWindowManger.isFloatingWindowShowing()) {
+ FloatingWindowManger.hideFloatingWindow();
+ }
}
- @ViewBinding.Click(R.id.assist_service)
+ @ViewBinding.Click(R.id.floating_window)
private void toggleAssistServiceSwitch() {
mAssistServiceSwitch.toggle();
}
- @ViewBinding.Check(R.id.sw_assist_service_notification)
- private void setAssistServiceNotificationEnable(boolean enable) {
- BoundsAssistSwitchNotification.setEnable(enable);
- }
-
- @ViewBinding.Click(R.id.assist_service_notification)
- private void toggleAssistServiceNotificationSwitch() {
- mAssistServiceNotificationSwitch.toggle();
- }
@ViewBinding.Click(R.id.stop_all_running_scripts)
private void stopAllRunningScripts() {
diff --git a/app/src/main/java/com/stardust/scriptdroid/ui/main/operation/ScriptFileOperation.java b/app/src/main/java/com/stardust/scriptdroid/ui/main/operation/ScriptFileOperation.java
index 7ece6e0e1..a03a5fd83 100644
--- a/app/src/main/java/com/stardust/scriptdroid/ui/main/operation/ScriptFileOperation.java
+++ b/app/src/main/java/com/stardust/scriptdroid/ui/main/operation/ScriptFileOperation.java
@@ -5,17 +5,20 @@
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.design.widget.Snackbar;
+import android.support.v7.widget.RecyclerView;
import com.afollestad.materialdialogs.MaterialDialog;
-import com.stardust.scriptdroid.App;
-import com.stardust.scriptdroid.ui.edit.EditActivity;
-import com.stardust.scriptdroid.R;
-import com.stardust.scriptdroid.external.shortcut.ShortcutActivity;
import com.stardust.scriptdroid.droid.script.file.ScriptFile;
import com.stardust.scriptdroid.droid.script.file.ScriptFileList;
import com.stardust.scriptdroid.external.shortcut.Shortcut;
+import com.stardust.scriptdroid.external.shortcut.ShortcutActivity;
+import com.stardust.scriptdroid.ui.edit.EditActivity;
import com.stardust.scriptdroid.ui.main.ScriptListRecyclerView;
import com.stardust.theme.dialog.ThemeColorMaterialDialogBuilder;
+import com.stardust.scriptdroid.App;
+import com.stardust.scriptdroid.R;
+
+import org.greenrobot.eventbus.EventBus;
import java.util.ArrayList;
import java.util.List;
@@ -26,27 +29,23 @@
public abstract class ScriptFileOperation {
- private static List operationNames = new ArrayList<>();
- private static List operations = new ArrayList<>();
+ public static class ShowMessageEvent {
+ public int messageResId;
- public static ScriptFileOperation getOperation(int index) {
- return operations.get(index);
+ public ShowMessageEvent(int message) {
+ this.messageResId = message;
+ }
}
- public static List getOperationNames() {
- return operationNames;
- }
+ public abstract void operate(RecyclerView recyclerView, ScriptFileList scriptFileList, int position);
- public abstract void operate(ScriptListRecyclerView recyclerView, ScriptFileList scriptFileList, int position);
+ private String mName;
- private static void addOperation(String name, int iconResId, ScriptFileOperation operation) {
- operation.mName = name;
- operation.mIconResId = iconResId;
- operationNames.add(name);
- operations.add(operation);
+ public ScriptFileOperation(String name, int iconResId) {
+ mName = name;
+ mIconResId = iconResId;
}
- private String mName;
private int mIconResId;
public String getName() {
@@ -59,13 +58,13 @@ public int getIconResId() {
public static class Run extends ScriptFileOperation {
- static {
- addOperation(App.getApp().getString(R.string.text_run), R.drawable.ic_play_green, new Run());
+ public Run() {
+ super(App.getApp().getString(R.string.text_run), R.drawable.ic_play_green);
}
@Override
- public void operate(ScriptListRecyclerView recyclerView, ScriptFileList scriptFileList, int position) {
- Snackbar.make(recyclerView, R.string.text_start_running, Snackbar.LENGTH_SHORT).show();
+ public void operate(RecyclerView recyclerView, ScriptFileList scriptFileList, int position) {
+ EventBus.getDefault().post(new ShowMessageEvent(R.string.text_start_running));
ScriptFile scriptFile = scriptFileList.get(position);
scriptFile.run();
}
@@ -73,12 +72,12 @@ public void operate(ScriptListRecyclerView recyclerView, ScriptFileList scriptFi
public static class Edit extends ScriptFileOperation {
- static {
- //addOperation(App.getApp().getString(R.string.text_edit)", R.drawable.ic_edit_green_48dp, new Edit());
+ public Edit() {
+ super(App.getApp().getString(R.string.text_edit), R.drawable.ic_edit_green_48dp);
}
@Override
- public void operate(ScriptListRecyclerView recyclerView, ScriptFileList scriptFileList, int position) {
+ public void operate(RecyclerView recyclerView, ScriptFileList scriptFileList, int position) {
Context context = recyclerView.getContext();
ScriptFile scriptFile = scriptFileList.get(position);
EditActivity.editFile(context, scriptFile.name, scriptFile.path);
@@ -87,12 +86,12 @@ public void operate(ScriptListRecyclerView recyclerView, ScriptFileList scriptFi
public static class OpenByOtherApp extends ScriptFileOperation {
- static {
- addOperation(App.getApp().getString(R.string.text_open_by_other_apps), R.drawable.ic_open_in_new_green_48dp, new OpenByOtherApp());
+ public OpenByOtherApp() {
+ super(App.getApp().getString(R.string.text_open_by_other_apps), R.drawable.ic_open_in_new_green_48dp);
}
@Override
- public void operate(ScriptListRecyclerView recyclerView, ScriptFileList scriptFileList, int position) {
+ public void operate(RecyclerView recyclerView, ScriptFileList scriptFileList, int position) {
Context context = recyclerView.getContext();
ScriptFile scriptFile = scriptFileList.get(position);
Uri uri = Uri.parse("file://" + scriptFile.path);
@@ -102,12 +101,12 @@ public void operate(ScriptListRecyclerView recyclerView, ScriptFileList scriptFi
public static class Rename extends ScriptFileOperation {
- static {
- addOperation(App.getApp().getString(R.string.text_rename), R.drawable.ic_rename_green, new Rename());
+ public Rename() {
+ super(App.getApp().getString(R.string.text_rename), R.drawable.ic_rename_green);
}
@Override
- public void operate(final ScriptListRecyclerView recyclerView, final ScriptFileList scriptFileList, final int position) {
+ public void operate(final RecyclerView recyclerView, final ScriptFileList scriptFileList, final int position) {
String oldName = scriptFileList.get(position).name;
new ThemeColorMaterialDialogBuilder(recyclerView.getContext())
.title(R.string.text_rename)
@@ -125,13 +124,12 @@ public void onInput(@NonNull MaterialDialog dialog, CharSequence input) {
public static class CreateShortcut extends ScriptFileOperation {
- static {
- addOperation(App.getApp().getString(R.string.text_send_shortcut), R.drawable.ic_shortcut_green, new CreateShortcut());
+ public CreateShortcut() {
+ super(App.getApp().getString(R.string.text_send_shortcut), R.drawable.ic_shortcut_green);
}
-
@Override
- public void operate(ScriptListRecyclerView recyclerView, ScriptFileList scriptFileList, int position) {
+ public void operate(RecyclerView recyclerView, ScriptFileList scriptFileList, int position) {
Context context = recyclerView.getContext();
ScriptFile scriptFile = scriptFileList.get(position);
new Shortcut(context).name(scriptFile.name)
@@ -139,18 +137,18 @@ public void operate(ScriptListRecyclerView recyclerView, ScriptFileList scriptFi
.icon(R.drawable.ic_robot_green)
.extras(new Intent().putExtra("path", scriptFile.path))
.send();
- Snackbar.make(recyclerView, R.string.text_already_create, Snackbar.LENGTH_SHORT).show();
+ EventBus.getDefault().post(R.string.text_already_create);
}
}
public static class Remove extends ScriptFileOperation {
- static {
- addOperation(App.getApp().getString(R.string.text_delete), R.drawable.ic_delete_green_48dp, new Remove());
+ public Remove() {
+ super(App.getApp().getString(R.string.text_delete), R.drawable.ic_delete_green_48dp);
}
@Override
- public void operate(ScriptListRecyclerView recyclerView, ScriptFileList scriptFileList, int position) {
+ public void operate(RecyclerView recyclerView, ScriptFileList scriptFileList, int position) {
scriptFileList.remove(position);
recyclerView.getAdapter().notifyItemRemoved(position);
}
@@ -158,14 +156,14 @@ public void operate(ScriptListRecyclerView recyclerView, ScriptFileList scriptFi
public static class Delete extends ScriptFileOperation {
- static {
- addOperation(App.getApp().getString(R.string.text_delete_absolutly), R.drawable.ic_delete_forever_green_48dp, new Delete());
+ public Delete() {
+ super(App.getApp().getString(R.string.text_delete_absolutly), R.drawable.ic_delete_forever_green_48dp);
}
@Override
- public void operate(ScriptListRecyclerView recyclerView, ScriptFileList scriptFileList, int position) {
+ public void operate(RecyclerView recyclerView, ScriptFileList scriptFileList, int position) {
boolean succeed = scriptFileList.deleteFromFileSystem(position);
- Snackbar.make(recyclerView, succeed ? R.string.text_already_delete : R.string.text_delete_failed, Snackbar.LENGTH_SHORT).show();
+ EventBus.getDefault().post(new ShowMessageEvent(succeed ? R.string.text_already_delete : R.string.text_delete_failed));
recyclerView.getAdapter().notifyItemRemoved(position);
}
}
diff --git a/app/src/main/java/com/stardust/scriptdroid/ui/main/operation/ScriptFileOperationPopupMenu.java b/app/src/main/java/com/stardust/scriptdroid/ui/main/operation/ScriptFileOperationPopupMenu.java
index ee6110100..f7eddde5d 100644
--- a/app/src/main/java/com/stardust/scriptdroid/ui/main/operation/ScriptFileOperationPopupMenu.java
+++ b/app/src/main/java/com/stardust/scriptdroid/ui/main/operation/ScriptFileOperationPopupMenu.java
@@ -18,9 +18,10 @@
import android.widget.PopupWindow;
import android.widget.TextView;
+import com.stardust.scriptdroid.tool.ViewTool;
import com.stardust.scriptdroid.R;
-import static com.stardust.scriptdroid.tool.ViewTool.$;
+import java.util.List;
/**
* Created by Stardust on 2017/1/24.
@@ -30,26 +31,31 @@ public class ScriptFileOperationPopupMenu extends PopupWindow {
public interface OnItemClickListener {
- void onClick(View view, int position);
+ void onClick(View view, int position, ScriptFileOperation operation);
}
private Context mContext;
private ScriptFileOperationListRecyclerView mOperationListRecyclerView;
private OnItemClickListener mOnItemClickListener;
+
+ private List mScriptFileOperationList;
+
private final View.OnClickListener mOnItemClickRealListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mOnItemClickListener != null) {
int position = mOperationListRecyclerView.getChildViewHolder(v).getAdapterPosition();
- mOnItemClickListener.onClick(v, position);
+ mOnItemClickListener.onClick(v, position, mScriptFileOperationList.get(position));
}
}
};
- public ScriptFileOperationPopupMenu(Context context) {
- super();
+
+ public ScriptFileOperationPopupMenu(Context context, List scriptFileOperationList) {
+ super(context);
mContext = context;
+ mScriptFileOperationList = scriptFileOperationList;
init();
}
@@ -77,7 +83,7 @@ private int getYInScreen(View anchor) {
private float getScreenHeight() {
DisplayMetrics displaymetrics = new DisplayMetrics();
- ((Activity) mContext).getWindowManager().getDefaultDisplay().getMetrics(displaymetrics);
+ ((WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getMetrics(displaymetrics);
return displaymetrics.heightPixels;
}
@@ -93,8 +99,9 @@ private void init() {
private void initContentView() {
View contentView = View.inflate(mContext, R.layout.script_file_operation_popup_menu_content, null);
setContentView(contentView);
- mOperationListRecyclerView = $(contentView, R.id.operation_list);
+ mOperationListRecyclerView = ViewTool.$(contentView, R.id.operation_list);
mOperationListRecyclerView.setOnItemClickListener(mOnItemClickRealListener);
+ mOperationListRecyclerView.setScriptFileOperationList(mScriptFileOperationList);
}
@@ -102,6 +109,8 @@ public static class ScriptFileOperationListRecyclerView extends RecyclerView {
private OnClickListener mOnItemClickListener;
+ private List mScriptFileOperationList;
+
public ScriptFileOperationListRecyclerView(Context context) {
super(context);
init();
@@ -117,6 +126,10 @@ public ScriptFileOperationListRecyclerView(Context context, @Nullable AttributeS
init();
}
+ public void setScriptFileOperationList(List scriptFileOperationList) {
+ mScriptFileOperationList = scriptFileOperationList;
+ }
+
private void init() {
setLayoutManager(new LinearLayoutManager(getContext()));
setAdapter(new Adapter() {
@@ -128,14 +141,14 @@ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
- ScriptFileOperation operation = ScriptFileOperation.getOperation(position);
+ ScriptFileOperation operation = mScriptFileOperationList.get(position);
holder.operationName.setText(operation.getName());
holder.icon.setImageResource(operation.getIconResId());
}
@Override
public int getItemCount() {
- return ScriptFileOperation.getOperationNames().size();
+ return mScriptFileOperationList.size();
}
});
}
@@ -152,8 +165,8 @@ private class ViewHolder extends RecyclerView.ViewHolder {
ViewHolder(View itemView) {
super(itemView);
itemView.setOnClickListener(mOnItemClickListener);
- operationName = $(itemView, R.id.name);
- icon = $(itemView, R.id.icon);
+ operationName = ViewTool.$(itemView, R.id.name);
+ icon = ViewTool.$(itemView, R.id.icon);
}
}
}
diff --git a/app/src/main/java/com/stardust/scriptdroid/ui/settings/AboutActivity.java b/app/src/main/java/com/stardust/scriptdroid/ui/settings/AboutActivity.java
index b1aac38e2..3eed313a3 100644
--- a/app/src/main/java/com/stardust/scriptdroid/ui/settings/AboutActivity.java
+++ b/app/src/main/java/com/stardust/scriptdroid/ui/settings/AboutActivity.java
@@ -6,20 +6,19 @@
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
-import android.support.v7.widget.Toolbar;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import com.afollestad.materialdialogs.DialogAction;
import com.afollestad.materialdialogs.MaterialDialog;
+import com.stardust.scriptdroid.tool.IntentTool;
import com.stardust.scriptdroid.ui.BaseActivity;
+import com.stardust.theme.dialog.ThemeColorMaterialDialogBuilder;
+import com.stardust.view.ViewBinding;
import com.stardust.scriptdroid.BuildConfig;
import com.stardust.scriptdroid.R;
-import com.stardust.scriptdroid.tool.IntentTool;
-import com.stardust.theme.dialog.ThemeColorMaterialDialogBuilder;
import com.stardust.view.ViewBinder;
-import com.stardust.view.ViewBinding;
import moe.feng.alipay.zerosdk.AlipayZeroSdk;
diff --git a/app/src/main/java/com/stardust/scriptdroid/ui/settings/SettingsActivity.java b/app/src/main/java/com/stardust/scriptdroid/ui/settings/SettingsActivity.java
index b16245403..7c062d428 100644
--- a/app/src/main/java/com/stardust/scriptdroid/ui/settings/SettingsActivity.java
+++ b/app/src/main/java/com/stardust/scriptdroid/ui/settings/SettingsActivity.java
@@ -10,15 +10,15 @@
import android.view.View;
import android.widget.Toast;
-import com.stardust.scriptdroid.R;
import com.stardust.scriptdroid.file.SampleFileManager;
import com.stardust.scriptdroid.tool.IntentTool;
import com.stardust.scriptdroid.ui.BaseActivity;
import com.stardust.scriptdroid.ui.error.IssueReportActivity;
+import com.stardust.util.MapEntries;
+import com.stardust.scriptdroid.R;
import com.stardust.scriptdroid.ui.main.MainActivity;
import com.stardust.theme.app.ColorSelectActivity;
import com.stardust.theme.util.ListBuilder;
-import com.stardust.util.MapEntries;
import java.util.List;
import java.util.Map;
@@ -26,7 +26,6 @@
import de.psdev.licensesdialog.LicenseResolver;
import de.psdev.licensesdialog.LicensesDialog;
import de.psdev.licensesdialog.licenses.License;
-import de.psdev.licensesdialog.licenses.MozillaPublicLicense11;
/**
* Created by Stardust on 2017/2/2.
diff --git a/app/src/main/java/com/stardust/theme/ThemeColorManagerCompat.java b/app/src/main/java/com/stardust/theme/ThemeColorManagerCompat.java
new file mode 100644
index 000000000..0c23a47e2
--- /dev/null
+++ b/app/src/main/java/com/stardust/theme/ThemeColorManagerCompat.java
@@ -0,0 +1,20 @@
+package com.stardust.theme;
+
+import com.stardust.scriptdroid.*;
+import com.stardust.scriptdroid.R;
+
+/**
+ * Created by Stardust on 2017/3/12.
+ */
+
+public class ThemeColorManagerCompat {
+
+ public static int getColorPrimary() {
+ int color = ThemeColorManager.getColorPrimary();
+ if (color == 0) {
+ return App.getApp().getResources().getColor(R.color.colorPrimary);
+ } else {
+ return color;
+ }
+ }
+}
diff --git a/app/src/main/java/com/stardust/util/FloatingWindowUtils.java b/app/src/main/java/com/stardust/util/FloatingWindowUtils.java
new file mode 100644
index 000000000..36765d64f
--- /dev/null
+++ b/app/src/main/java/com/stardust/util/FloatingWindowUtils.java
@@ -0,0 +1,43 @@
+package com.stardust.util;
+
+import android.Manifest;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.widget.Toast;
+
+/**
+ * Created by Stardust on 2017/3/10.
+ */
+
+public class FloatingWindowUtils {
+
+ public static boolean checkFloatingWindowPermission(Context context, int messageResId) {
+ if (!isFloatingWindowPermitted(context)) {
+ Toast.makeText(context, messageResId, Toast.LENGTH_SHORT).show();
+ goToAppSetting(context);
+ return false;
+ }
+ return true;
+ }
+
+ private static void goToAppSetting(Context context) {
+ try {
+ Intent i = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+ i.addCategory(Intent.CATEGORY_DEFAULT);
+ i.setData(Uri.parse("package:" + context.getPackageName()));
+ context.startActivity(i);
+ } catch (ActivityNotFoundException ignored) {
+
+ }
+ }
+
+ public static boolean isFloatingWindowPermitted(Context context) {
+ PackageManager pm = context.getPackageManager();
+ return pm.checkPermission(Manifest.permission.SYSTEM_ALERT_WINDOW, context.getPackageName())
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+}
diff --git a/app/src/main/java/com/stardust/util/MessageEvent.java b/app/src/main/java/com/stardust/util/MessageEvent.java
new file mode 100644
index 000000000..a88a00cbe
--- /dev/null
+++ b/app/src/main/java/com/stardust/util/MessageEvent.java
@@ -0,0 +1,24 @@
+package com.stardust.util;
+
+import java.lang.reflect.Method;
+
+/**
+ * Created by Stardust on 2017/3/12.
+ */
+
+public class MessageEvent {
+
+ public String message;
+ public Object param;
+
+ public MessageEvent(String message, Object param) {
+ this.message = message;
+ this.param = param;
+ }
+
+
+ public MessageEvent(String message) {
+ this.message = message;
+ }
+
+}
diff --git a/app/src/main/java/com/stardust/view/Floaty.java b/app/src/main/java/com/stardust/view/Floaty.java
new file mode 100644
index 000000000..5c16b4229
--- /dev/null
+++ b/app/src/main/java/com/stardust/view/Floaty.java
@@ -0,0 +1,442 @@
+package com.stardust.view;
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.graphics.Color;
+import android.graphics.PixelFormat;
+import android.os.IBinder;
+import android.support.annotation.Nullable;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.view.GestureDetectorCompat;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.LinearLayout;
+
+/**
+ * Created by ericbhatti on 11/24/15.
+ *
+ * Modified by Stardust on 2017/3/10
+ *
+ * @author Eric Bhatti
+ * @since 24 November, 2015
+ */
+public class Floaty {
+
+ public interface FloatyOrientationListener {
+
+
+ /**
+ * This method is called before the orientation change happens, you can use this to save the data of your views so you can later populate the data back in {@link #afterOrientationChange}
+ *
+ * @param floaty The floating window
+ */
+ public void beforeOrientationChange(Floaty floaty);
+
+ /**
+ * This method is called after the orientation change happens, you can use this to restore the data of your views that you saved in {@link #beforeOrientationChange}
+ *
+ * @param floaty The floating window
+ */
+ public void afterOrientationChange(Floaty floaty);
+
+ }
+
+
+ public interface OnBackPressedListener {
+ boolean onBackPressed();
+ }
+
+ private View.OnClickListener mOnHeadClickListener;
+
+
+ private OnBackPressedListener mOnBackPressedListener;
+
+ private final View head;
+ private final View body;
+ private final Context context;
+ private final Notification notification;
+ private final int notificationId;
+ private static Floaty floaty;
+ private final FloatyOrientationListener floatyOrientationListener;
+ private float ratioY = 0;
+ private float oldWidth = 0;
+ private float oldX = 0;
+ private boolean confChange = false;
+
+ private static final String LOG_TAG = "Floaty";
+
+
+ /**
+ * @return The body of the floaty which is assigned through the {@link #createInstance} method.
+ */
+
+ public View getBody() {
+ return floaty.body;
+ }
+
+
+ /**
+ * @return The head of the floaty which is assigned through the {@link #createInstance} method.
+ */
+
+ public View getHead() {
+ return floaty.head;
+ }
+
+ /**
+ * Creates a Singleton of the Floating Window
+ *
+ * @param context The application context
+ * @param head The head View, upon clicking it the body is to be opened
+ * @param body The body View
+ * @param notificationId The notificationId for your notification
+ * @param notification The notification which is displayed for the foreground service
+ * @param floatyOrientationListener The {@link FloatyOrientationListener} interface with callbacks which are called when orientation changes.
+ * @return A Floating Window
+ */
+
+ public static synchronized Floaty createInstance(Context context, View head, View body, int notificationId, Notification notification, FloatyOrientationListener
+ floatyOrientationListener) {
+ if (floaty == null) {
+ floaty = new Floaty(context, head, body, notificationId, notification, floatyOrientationListener);
+ }
+ return floaty;
+ }
+
+ /**
+ * Creates a Singleton of the Floating Window
+ *
+ * @param context The application context
+ * @param head The head View, upon clicking it the body is to be opened
+ * @param body The body View
+ * @param notificationId The notificationId for your notification
+ * @param notification The notification which is displayed for the foreground service
+ * @return A Floating Window
+ */
+ public static synchronized Floaty createInstance(Context context, View head, View body, int notificationId, Notification notification) {
+ if (floaty == null) {
+ floaty = new Floaty(context, head, body, notificationId, notification, new FloatyOrientationListener() {
+ @Override
+ public void beforeOrientationChange(Floaty floaty) {
+ Log.d(LOG_TAG, "beforeOrientationChange");
+ }
+
+ @Override
+ public void afterOrientationChange(Floaty floaty) {
+ Log.d(LOG_TAG, "afterOrientationChange");
+ }
+ });
+ }
+ return floaty;
+ }
+
+ public static synchronized Floaty createInstance(Context context, View head, View body) {
+ return createInstance(context, head, body, -1, null);
+ }
+
+ /**
+ * @return The same instance of Floating Window, which has been created through {@link #createInstance}. Don't call this method before createInstance
+ */
+ public static synchronized Floaty getInstance() {
+ if (floaty == null) {
+ throw new NullPointerException("Floaty not initialized! First call createInstance method, then to access Floaty in any other class call getInstance()");
+ }
+ return floaty;
+ }
+
+ private Floaty(Context context, View head, View body, int notificationId, Notification notification, FloatyOrientationListener floatyOrientationListener) {
+ this.head = head;
+ this.body = body;
+ this.context = context;
+ this.notification = notification;
+ this.notificationId = notificationId;
+ this.floatyOrientationListener = floatyOrientationListener;
+ }
+
+
+ /**
+ * Starts the service and adds it to the screen
+ */
+ public void startService() {
+ Intent intent = new Intent(context, Floaty.FloatHeadService.class);
+ context.startService(intent);
+ }
+
+ /**
+ * Stops the service and removes it from the screen
+ */
+ public void stopService() {
+ Intent intent = new Intent(context, Floaty.FloatHeadService.class);
+ context.stopService(intent);
+ }
+
+
+ public void setOnHeadClickListener(View.OnClickListener onHeadClickListener) {
+ mOnHeadClickListener = onHeadClickListener;
+ }
+
+ public void setOnBackPressedListener(OnBackPressedListener onBackPressedListener) {
+ mOnBackPressedListener = onBackPressedListener;
+ }
+
+ /**
+ * Helper method for notification creation.
+ *
+ * @param context
+ * @param contentTitle
+ * @param contentText
+ * @param notificationIcon
+ * @param contentIntent
+ * @return Notification for the Service
+ */
+ public static Notification createNotification(Context context, String contentTitle, String contentText, int notificationIcon, PendingIntent contentIntent) {
+ return new NotificationCompat.Builder(context)
+ .setContentTitle(contentTitle)
+ .setContentText(contentText)
+ .setSmallIcon(notificationIcon)
+ .setContentIntent(contentIntent).build();
+
+ }
+
+ public static class FloatHeadService extends Service {
+
+ private WindowManager windowManager;
+ private WindowManager.LayoutParams params;
+ private LinearLayout mLinearLayout;
+ GestureDetectorCompat gestureDetectorCompat;
+ DisplayMetrics metrics;
+ private boolean didFling;
+ private int[] clickLocation = new int[2];
+
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE || newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
+
+ int[] location = new int[2];
+ mLinearLayout.getLocationOnScreen(location);
+ floaty.oldWidth = metrics.widthPixels;
+ floaty.confChange = true;
+ if (floaty.getBody().getVisibility() == View.VISIBLE) {
+ floaty.oldX = clickLocation[0];
+ floaty.ratioY = (float) (clickLocation[1]) / (float) metrics.heightPixels;
+ } else {
+ floaty.oldX = location[0];
+ floaty.ratioY = (float) (location[1]) / (float) metrics.heightPixels;
+ }
+ floaty.floatyOrientationListener.beforeOrientationChange(floaty);
+ floaty.stopService();
+ floaty.startService();
+ floaty.floatyOrientationListener.afterOrientationChange(floaty);
+ }
+ }
+
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ Log.d(LOG_TAG, "onStartCommand");
+ metrics = new DisplayMetrics();
+ windowManager.getDefaultDisplay().getMetrics(metrics);
+ if (floaty.notification != null)
+ startForeground(floaty.notificationId, floaty.notification);
+ return START_STICKY;
+ }
+
+ private void showHead() {
+ floaty.head.setVisibility(View.VISIBLE);
+ floaty.body.setVisibility(View.GONE);
+ params.x = clickLocation[0];
+ params.y = clickLocation[1] - 36;
+ params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+ params.width = WindowManager.LayoutParams.WRAP_CONTENT;
+ params.height = WindowManager.LayoutParams.WRAP_CONTENT;
+ mLinearLayout.setBackgroundColor(Color.argb(0, 0, 0, 0));
+ windowManager.updateViewLayout(mLinearLayout, params);
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ Log.d(LOG_TAG, "onCreate");
+ mLinearLayout = new LinearLayout(getApplicationContext()) {
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ if (event.getKeyCode() == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
+ if (floaty.mOnBackPressedListener != null && !floaty.mOnBackPressedListener.onBackPressed()) {
+ showHead();
+ return true;
+ }
+ }
+ if (event.getKeyCode() == KeyEvent.KEYCODE_HOME) {
+ showHead();
+ return true;
+ }
+ return super.dispatchKeyEvent(event);
+ }
+ };
+
+ gestureDetectorCompat = new GestureDetectorCompat(floaty.context, new GestureDetector.SimpleOnGestureListener() {
+ private int initialX;
+ private int initialY;
+ private float initialTouchX;
+ private float initialTouchY;
+
+ @Override
+ public boolean onDown(MotionEvent event) {
+ Log.d(LOG_TAG, "onDown");
+ initialX = params.x;
+ initialY = params.y;
+ initialTouchX = event.getRawX();
+ initialTouchY = event.getRawY();
+ didFling = false;
+ return false;
+ }
+
+ @Override
+ public void onShowPress(MotionEvent e) {
+ Log.d(LOG_TAG, "onShowPress");
+ floaty.head.setAlpha(0.8f);
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ if (floaty.body.getVisibility() == View.VISIBLE) {
+ floaty.body.setVisibility(View.GONE);
+ params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+ params.width = WindowManager.LayoutParams.WRAP_CONTENT;
+ params.height = WindowManager.LayoutParams.WRAP_CONTENT;
+ mLinearLayout.setBackgroundColor(Color.argb(0, 0, 0, 0));
+ }
+ params.x = (initialX + (int) ((e2.getRawX() - initialTouchX)));
+ params.y = (initialY + (int) ((e2.getRawY() - initialTouchY)));
+ windowManager.updateViewLayout(mLinearLayout, params);
+ return false;
+ }
+
+ @Override
+ public boolean onSingleTapConfirmed(MotionEvent e) {
+ Log.d(LOG_TAG, "onSingleTapConfirmed");
+ if (floaty.body.getVisibility() == View.GONE) {
+ params.x = metrics.widthPixels;
+ params.y = 0;
+ params.flags = params.flags & ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+ params.width = WindowManager.LayoutParams.MATCH_PARENT;
+ params.height = WindowManager.LayoutParams.MATCH_PARENT;
+ //floaty.head.getLocationOnScreen(clickLocation);
+ floaty.head.setVisibility(View.GONE);
+ floaty.body.setVisibility(View.VISIBLE);
+ mLinearLayout.setBackgroundColor(Color.argb(200, 50, 50, 50));
+ } else {
+ floaty.body.setVisibility(View.GONE);
+ floaty.head.setVisibility(View.VISIBLE);
+ params.x = clickLocation[0];
+ params.y = clickLocation[1] - 36;
+ params.width = WindowManager.LayoutParams.WRAP_CONTENT;
+ params.height = WindowManager.LayoutParams.WRAP_CONTENT;
+ params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+ mLinearLayout.setBackgroundColor(Color.argb(0, 0, 0, 0));
+ }
+ windowManager.updateViewLayout(mLinearLayout, params);
+ if (floaty.mOnHeadClickListener != null) {
+ floaty.mOnHeadClickListener.onClick(floaty.head);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ Log.d(LOG_TAG, "onFling");
+ didFling = true;
+ int newX = params.x;
+ if (newX > (metrics.widthPixels / 2))
+ params.x = metrics.widthPixels;
+ else
+ params.x = 0;
+ windowManager.updateViewLayout(mLinearLayout, params);
+ return false;
+ }
+ });
+
+ mLinearLayout.setOrientation(LinearLayout.VERTICAL);
+ windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
+ metrics = new DisplayMetrics();
+ windowManager.getDefaultDisplay().getMetrics(metrics);
+ params = new WindowManager.LayoutParams(
+ WindowManager.LayoutParams.WRAP_CONTENT,
+ WindowManager.LayoutParams.WRAP_CONTENT,
+ WindowManager.LayoutParams.TYPE_PHONE,
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
+ PixelFormat.TRANSLUCENT);
+ params.gravity = Gravity.TOP | Gravity.LEFT;
+
+
+ if (floaty.confChange) {
+ floaty.confChange = false;
+ if (floaty.oldX < (floaty.oldWidth / 2)) {
+ params.x = 0;
+ } else {
+ params.x = metrics.widthPixels;
+ }
+ params.y = (int) (metrics.heightPixels * floaty.ratioY);
+ } else {
+ params.x = metrics.widthPixels;
+ params.y = 0;
+ }
+ floaty.body.setVisibility(View.GONE);
+ floaty.head.setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ gestureDetectorCompat.onTouchEvent(event);
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ floaty.head.setAlpha(1.0f);
+ if (!didFling) {
+ Log.d(LOG_TAG, "ACTION_UP");
+ int newX = params.x;
+ if (newX > (metrics.widthPixels / 2))
+ params.x = metrics.widthPixels;
+ else
+ params.x = 0;
+ windowManager.updateViewLayout(mLinearLayout, params);
+ }
+ }
+ return true;
+ }
+ });
+ windowManager.addView(mLinearLayout, params);
+ if (floaty.body.getParent() != null) {
+ ((ViewGroup) floaty.body.getParent()).removeView(floaty.body);
+ }
+ mLinearLayout.setFocusable(true);
+ LinearLayout.LayoutParams headParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
+ LinearLayout.LayoutParams bodyParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
+ headParams.gravity = Gravity.TOP | Gravity.RIGHT;
+ bodyParams.gravity = Gravity.TOP;
+ mLinearLayout.addView(floaty.head, headParams);
+ mLinearLayout.addView(floaty.body, bodyParams);
+ }
+
+ public void onDestroy() {
+ super.onDestroy();
+ if (mLinearLayout != null) {
+ mLinearLayout.removeAllViews();
+ windowManager.removeView(mLinearLayout);
+ }
+ stopForeground(true);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/stardust/view/ResizableFloaty.java b/app/src/main/java/com/stardust/view/ResizableFloaty.java
new file mode 100644
index 000000000..67f863de2
--- /dev/null
+++ b/app/src/main/java/com/stardust/view/ResizableFloaty.java
@@ -0,0 +1,410 @@
+package com.stardust.view;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.PixelFormat;
+import android.os.IBinder;
+import android.support.annotation.Nullable;
+import android.support.v4.view.GestureDetectorCompat;
+import android.util.DisplayMetrics;
+import android.view.GestureDetector;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.RelativeLayout;
+
+import com.stardust.scriptdroid.R;
+import com.stardust.scriptdroid.tool.ViewTool;
+import com.stardust.widget.ViewSwitcher;
+
+/**
+ * Created by Stardust on 2017/3/11.
+ */
+
+public class ResizableFloaty {
+
+ private static final String TAG = "ResizableFloaty";
+
+ private View mCollapsedView, mExpandedView;
+ private static ResizableFloaty floaty;
+
+ public ResizableFloaty(View collapsedView, View expandedView) {
+ mExpandedView = expandedView;
+ mCollapsedView = collapsedView;
+ }
+
+ public static void startService(Context context, View collapsedView, View expandedView) {
+ floaty = new ResizableFloaty(collapsedView, expandedView);
+ context.startService(new Intent(context, FloatingWindowService.class));
+ }
+
+ interface WindowBridge {
+ int getX();
+
+ int getY();
+
+ void updatePosition(int x, int y);
+
+ int getWidth();
+
+ int getHeight();
+
+ void updateMeasure(int width, int height);
+
+ int getScreenWidth();
+
+ int getScreenHeight();
+ }
+
+ public static class FloatingWindowService extends Service {
+
+ private WindowManager mWindowManager;
+ private WindowManager.LayoutParams mWindowLayoutParams;
+ private RelativeLayout mWindowView;
+ private ViewSwitcher mCollapseExpandViewSwitcher;
+ private View mResizer;
+ private ResizableFloaty mFloaty = floaty;
+ private ResizeGesture mResizeGesture;
+ private DragGesture mDragGesture;
+ private ViewStack mViewStack = new ViewStack(new ViewStack.CurrentViewSetter() {
+ @Override
+ public void setCurrentView(View v) {
+ mCollapseExpandViewSwitcher.setSecondView(v);
+ }
+ });
+
+ private WindowBridge mWindowBridge = new WindowBridge() {
+ DisplayMetrics mDisplayMetrics;
+
+ @Override
+ public int getX() {
+ return mWindowLayoutParams.x;
+ }
+
+ @Override
+ public int getY() {
+ return mWindowLayoutParams.y;
+ }
+
+ @Override
+ public void updatePosition(int x, int y) {
+ mWindowLayoutParams.x = x;
+ mWindowLayoutParams.y = y;
+ mWindowManager.updateViewLayout(mWindowView, mWindowLayoutParams);
+ }
+
+ @Override
+ public int getWidth() {
+ return mWindowView.getWidth();
+ }
+
+ @Override
+ public int getHeight() {
+ return mWindowView.getHeight();
+ }
+
+ @Override
+ public void updateMeasure(int width, int height) {
+ mWindowLayoutParams.width = width;
+ mWindowLayoutParams.height = height;
+ mWindowManager.updateViewLayout(mWindowView, mWindowLayoutParams);
+ }
+
+ @Override
+ public int getScreenWidth() {
+ ensureDisplayMetrics();
+ return mDisplayMetrics.widthPixels;
+ }
+
+ @Override
+ public int getScreenHeight() {
+ ensureDisplayMetrics();
+ return mDisplayMetrics.heightPixels;
+ }
+
+ private void ensureDisplayMetrics() {
+ if (mDisplayMetrics == null) {
+ mDisplayMetrics = new DisplayMetrics();
+ mWindowManager.getDefaultDisplay().getMetrics(mDisplayMetrics);
+ }
+ }
+ };
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
+ mWindowLayoutParams = createWindowLayoutParams();
+ initWindowView();
+ initGesture();
+ setUpListeners();
+ }
+
+ private void setUpListeners() {
+ mDragGesture.setOnDraggedViewClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ enableWindowFocus();
+ expand();
+ }
+ });
+ mWindowView.setOnKeyListener(new View.OnKeyListener() {
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
+ onBackPressed();
+ return true;
+ }
+ if (keyCode == KeyEvent.KEYCODE_HOME) {
+ onHomePressed();
+ return true;
+ }
+ return false;
+ }
+
+
+ });
+ }
+
+ private void expand() {
+ mCollapseExpandViewSwitcher.showSecond();
+ mResizeGesture.setResizeEnabled(true);
+ mDragGesture.setKeepToSide(false);
+ }
+
+ private void onBackPressed() {
+ if (mViewStack.canGoBack()) {
+ mViewStack.goBack();
+ } else {
+ collapse();
+ }
+ }
+
+ private void onHomePressed() {
+ mViewStack.goBackToFirst();
+ collapse();
+ }
+
+ private void collapse() {
+ mCollapseExpandViewSwitcher.showFirst();
+ disableWindowFocus();
+ mResizeGesture.setResizeEnabled(false);
+ mDragGesture.setKeepToSide(true);
+ }
+
+ private void disableWindowFocus() {
+ mWindowLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+ mWindowManager.updateViewLayout(mWindowView, mWindowLayoutParams);
+ }
+
+ private void initGesture() {
+ mResizeGesture = ResizeGesture.enableResize(mResizer, mWindowBridge);
+ mDragGesture = DragGesture.enableDrag(mWindowView, mWindowBridge);
+ mResizeGesture.setResizeEnabled(false);
+ mDragGesture.setKeepToSide(true);
+ }
+
+ private void enableWindowFocus() {
+ mWindowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+ mWindowManager.updateViewLayout(mWindowView, mWindowLayoutParams);
+ mWindowView.requestFocus();
+ }
+
+ private void initWindowView() {
+ mWindowView = (RelativeLayout) View.inflate(getApplicationContext(), R.layout.resizable_floaty_container, null);
+ mWindowView.setFocusableInTouchMode(true);
+ mCollapseExpandViewSwitcher = (ViewSwitcher) mWindowView.findViewById(R.id.container);
+ mResizer = mWindowView.findViewById(R.id.resizer);
+ ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ mCollapseExpandViewSwitcher.addView(floaty.mCollapsedView, params);
+ mCollapseExpandViewSwitcher.addView(floaty.mExpandedView, params);
+ mViewStack.setRootView(floaty.mExpandedView);
+ mWindowManager.addView(mWindowView, mWindowLayoutParams);
+ }
+
+ private WindowManager.LayoutParams createWindowLayoutParams() {
+ WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
+ WindowManager.LayoutParams.WRAP_CONTENT,
+ WindowManager.LayoutParams.WRAP_CONTENT,
+ WindowManager.LayoutParams.TYPE_PHONE,
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
+ PixelFormat.TRANSLUCENT);
+ layoutParams.gravity = Gravity.TOP | Gravity.START;
+ return layoutParams;
+ }
+ }
+
+ public static class DragGesture extends GestureDetector.SimpleOnGestureListener {
+
+ public static DragGesture enableDrag(final View view, WindowBridge bridge) {
+ final DragGesture gestureListener = new DragGesture(bridge, view) {
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ view.setAlpha(0.8f);
+ return super.onScroll(e1, e2, distanceX, distanceY);
+ }
+
+ };
+ final GestureDetectorCompat gestureDetector = new GestureDetectorCompat(view.getContext(), gestureListener);
+ view.setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ gestureDetector.onTouchEvent(event);
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ view.setAlpha(1.0f);
+ if (!gestureListener.mFlung && gestureListener.isKeepToSide()) {
+ gestureListener.keepToSide();
+ }
+ }
+ return true;
+ }
+ });
+ return gestureListener;
+ }
+
+ private WindowBridge mWindowBridge;
+ private boolean mKeepToSide;
+ private View.OnClickListener mOnClickListener;
+ private View mView;
+
+ private int initialX;
+ private int initialY;
+ private float initialTouchX;
+ private float initialTouchY;
+
+ private boolean mFlung = false;
+
+ public DragGesture(WindowBridge windowBridge, View view) {
+ mWindowBridge = windowBridge;
+ mView = view;
+ }
+
+ public void setKeepToSide(boolean keepToSide) {
+ mKeepToSide = keepToSide;
+ }
+
+ public boolean isKeepToSide() {
+ return mKeepToSide;
+ }
+
+ @Override
+ public boolean onDown(MotionEvent event) {
+ initialX = mWindowBridge.getX();
+ initialY = mWindowBridge.getY();
+ initialTouchX = event.getRawX();
+ initialTouchY = event.getRawY();
+ mFlung = false;
+ return false;
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ mWindowBridge.updatePosition(initialX + (int) ((e2.getRawX() - initialTouchX)),
+ initialY + (int) ((e2.getRawY() - initialTouchY)));
+ return false;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ mFlung = true;
+ if (mKeepToSide)
+ keepToSide();
+ return false;
+ }
+
+ public void keepToSide() {
+ int newX = mWindowBridge.getX();
+ if (newX > mWindowBridge.getScreenWidth() / 2)
+ mWindowBridge.updatePosition(mWindowBridge.getScreenWidth(), mWindowBridge.getY());
+ else
+ mWindowBridge.updatePosition(0, mWindowBridge.getY());
+ }
+
+ @Override
+ public boolean onSingleTapConfirmed(MotionEvent e) {
+ if (mOnClickListener != null)
+ mOnClickListener.onClick(mView);
+ return super.onSingleTapConfirmed(e);
+ }
+
+ public void setOnDraggedViewClickListener(View.OnClickListener onClickListener) {
+ mOnClickListener = onClickListener;
+ }
+ }
+
+ public static class ResizeGesture extends GestureDetector.SimpleOnGestureListener {
+
+ public static ResizeGesture enableResize(View resizer, WindowBridge windowBridge) {
+ ResizeGesture resizeGesture = new ResizeGesture(windowBridge, resizer);
+ final GestureDetector detector = new GestureDetector(resizer.getContext(), resizeGesture);
+ resizer.setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ detector.onTouchEvent(event);
+ return true;
+ }
+ });
+ return resizeGesture;
+ }
+
+ private WindowBridge mWindowBridge;
+ private float initialTouchX;
+ private float initialTouchY;
+ private int mInitialWidth, mInitialHeight;
+ private View mResizerView;
+ private int mMinHeight = 200, mMinWidth = 200;
+ private final int mStatusBarHeight;
+
+
+ public ResizeGesture(WindowBridge windowBridge, View resizerView) {
+ mWindowBridge = windowBridge;
+ mResizerView = resizerView;
+ mStatusBarHeight = ViewTool.getStatusBarHeight(resizerView.getContext());
+ }
+
+ public void setMinHeight(int minHeight) {
+ mMinHeight = minHeight;
+ }
+
+ public void setMinWidth(int minWidth) {
+ mMinWidth = minWidth;
+ }
+
+ public void setResizeEnabled(boolean enabled) {
+ mResizerView.setVisibility(enabled ? View.VISIBLE : View.GONE);
+ }
+
+ @Override
+ public boolean onDown(MotionEvent event) {
+ initialTouchX = event.getRawX();
+ initialTouchY = event.getRawY();
+ mInitialWidth = mWindowBridge.getWidth();
+ mInitialHeight = mWindowBridge.getHeight();
+ return false;
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, final MotionEvent e2, float distanceX, float distanceY) {
+ int newWidth = mInitialWidth + (int) ((e2.getRawX() - initialTouchX));
+ int newHeight = mInitialHeight + (int) ((e2.getRawY() - initialTouchY));
+ newWidth = Math.max(mMinWidth, newWidth);
+ newHeight = Math.max(mMinHeight, newHeight);
+ newWidth = Math.min(mWindowBridge.getScreenWidth() - mWindowBridge.getX() - mResizerView.getWidth(), newWidth);
+ newHeight = Math.min(mWindowBridge.getScreenHeight() - mWindowBridge.getY() - mResizerView.getHeight() - mStatusBarHeight, newHeight);
+ mWindowBridge.updateMeasure(newWidth, newHeight);
+ return true;
+ }
+ }
+
+
+}
diff --git a/app/src/main/java/com/stardust/view/ViewStack.java b/app/src/main/java/com/stardust/view/ViewStack.java
new file mode 100644
index 000000000..6bd885296
--- /dev/null
+++ b/app/src/main/java/com/stardust/view/ViewStack.java
@@ -0,0 +1,54 @@
+package com.stardust.view;
+
+import android.view.View;
+
+import java.util.Stack;
+
+/**
+ * Created by Stardust on 2017/3/11.
+ */
+
+public class ViewStack {
+
+ public interface CurrentViewSetter {
+ void setCurrentView(View v);
+ }
+
+ public interface NavigableView {
+ void goBack();
+ }
+
+ private Stack mStack = new Stack<>();
+ private CurrentViewSetter mCurrentViewSetter;
+
+ public ViewStack(CurrentViewSetter currentViewSetter) {
+ mCurrentViewSetter = currentViewSetter;
+ }
+
+ public void navigateTo(View v) {
+ mStack.push(v);
+ mCurrentViewSetter.setCurrentView(v);
+ }
+
+ public boolean canGoBack() {
+ return mStack.size() > 1;
+ }
+
+ public void goBack() {
+ mCurrentViewSetter.setCurrentView(mStack.pop());
+ }
+
+ public void goBackToFirst() {
+ while (mStack.size() > 1) {
+ mStack.pop();
+ }
+ mCurrentViewSetter.setCurrentView(mStack.peek());
+ }
+
+ public void setRootView(View view) {
+ mStack.clear();
+ mStack.push(view);
+ }
+
+
+}
diff --git a/app/src/main/java/com/stardust/view/accessibility/AccessibilityNodeInfoDumper.java b/app/src/main/java/com/stardust/view/accessibility/AccessibilityNodeInfoDumper.java
deleted file mode 100644
index e2a30cde8..000000000
--- a/app/src/main/java/com/stardust/view/accessibility/AccessibilityNodeInfoDumper.java
+++ /dev/null
@@ -1,237 +0,0 @@
-package com.stardust.view.accessibility;
-
-/**
- * Created by Stardust on 2017/3/6.
- */
-
-
-import android.os.Environment;
-import android.os.SystemClock;
-import android.util.Log;
-import android.util.Xml;
-import android.view.accessibility.AccessibilityNodeInfo;
-
-import org.xmlpull.v1.XmlSerializer;
-
-import java.io.File;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.StringWriter;
-
-public class AccessibilityNodeInfoDumper {
-
- private static final String LOGTAG = AccessibilityNodeInfoDumper.class.getSimpleName();
- private static final String[] NAF_EXCLUDED_CLASSES = new String[]{
- android.widget.GridView.class.getName(), android.widget.GridLayout.class.getName(),
- android.widget.ListView.class.getName(), android.widget.TableLayout.class.getName()
- };
-
- /**
- * Using {@link AccessibilityNodeInfo} this method will walk the layout hierarchy
- * and generates an xml dump into the /data/local/window_dump.xml
- *
- * @param root The root accessibility node.
- * @param rotation The rotaion of current display
- * @param width The pixel width of current display
- * @param height The pixel height of current display
- */
- public static void dumpWindowToFile(AccessibilityNodeInfo root, int rotation,
- int width, int height) {
- File baseDir = new File(Environment.getDataDirectory(), "local");
- if (!baseDir.exists()) {
- baseDir.mkdir();
- baseDir.setExecutable(true, false);
- baseDir.setWritable(true, false);
- baseDir.setReadable(true, false);
- }
- dumpWindowToFile(root,
- new File(new File(Environment.getDataDirectory(), "local"), "window_dump.xml"),
- rotation, width, height);
- }
-
- /**
- * Using {@link AccessibilityNodeInfo} this method will walk the layout hierarchy
- * and generates an xml dump to the location specified by dumpFile
- *
- * @param root The root accessibility node.
- * @param dumpFile The file to dump to.
- * @param rotation The rotaion of current display
- * @param width The pixel width of current display
- * @param height The pixel height of current display
- */
- public static void dumpWindowToFile(AccessibilityNodeInfo root, File dumpFile, int rotation,
- int width, int height) {
- if (root == null) {
- return;
- }
- final long startTime = SystemClock.uptimeMillis();
- try {
- FileWriter writer = new FileWriter(dumpFile);
- XmlSerializer serializer = Xml.newSerializer();
- StringWriter stringWriter = new StringWriter();
- serializer.setOutput(stringWriter);
- serializer.startDocument("UTF-8", true);
- serializer.startTag("", "hierarchy");
- serializer.attribute("", "rotation", Integer.toString(rotation));
- dumpNodeRec(root, serializer, 0, width, height);
- serializer.endTag("", "hierarchy");
- serializer.endDocument();
- writer.write(stringWriter.toString());
- writer.close();
- } catch (IOException e) {
- Log.e(LOGTAG, "failed to dump window to file", e);
- }
- final long endTime = SystemClock.uptimeMillis();
- Log.w(LOGTAG, "Fetch time: " + (endTime - startTime) + "ms");
- }
-
- private static void dumpNodeRec(AccessibilityNodeInfo node, XmlSerializer serializer, int index,
- int width, int height) throws IOException {
- serializer.startTag("", "node");
- if (!nafExcludedClass(node) && !nafCheck(node))
- serializer.attribute("", "NAF", Boolean.toString(true));
- serializer.attribute("", "index", Integer.toString(index));
- serializer.attribute("", "text", safeCharSeqToString(node.getText()));
- serializer.attribute("", "resource-id", safeCharSeqToString(node.getViewIdResourceName()));
- serializer.attribute("", "class", safeCharSeqToString(node.getClassName()));
- serializer.attribute("", "package", safeCharSeqToString(node.getPackageName()));
- serializer.attribute("", "content-desc", safeCharSeqToString(node.getContentDescription()));
- serializer.attribute("", "checkable", Boolean.toString(node.isCheckable()));
- serializer.attribute("", "checked", Boolean.toString(node.isChecked()));
- serializer.attribute("", "clickable", Boolean.toString(node.isClickable()));
- serializer.attribute("", "enabled", Boolean.toString(node.isEnabled()));
- serializer.attribute("", "focusable", Boolean.toString(node.isFocusable()));
- serializer.attribute("", "focused", Boolean.toString(node.isFocused()));
- serializer.attribute("", "scrollable", Boolean.toString(node.isScrollable()));
- serializer.attribute("", "long-clickable", Boolean.toString(node.isLongClickable()));
- serializer.attribute("", "password", Boolean.toString(node.isPassword()));
- serializer.attribute("", "selected", Boolean.toString(node.isSelected()));
- serializer.attribute("", "bounds", AccessibilityNodeInfoHelper.getVisibleBoundsInScreen(
- node, width, height).toShortString());
- int count = node.getChildCount();
- for (int i = 0; i < count; i++) {
- AccessibilityNodeInfo child = node.getChild(i);
- if (child != null) {
- if (child.isVisibleToUser()) {
- dumpNodeRec(child, serializer, i, width, height);
- child.recycle();
- } else {
- Log.i(LOGTAG, String.format("Skipping invisible child: %s", child.toString()));
- }
- } else {
- Log.i(LOGTAG, String.format("Null child %d/%d, parent: %s",
- i, count, node.toString()));
- }
- }
- serializer.endTag("", "node");
- }
-
- /**
- * The list of classes to exclude my not be complete. We're attempting to
- * only reduce noise from standard layout classes that may be falsely
- * configured to accept clicks and are also enabled.
- *
- * @param node
- * @return true if node is excluded.
- */
- private static boolean nafExcludedClass(AccessibilityNodeInfo node) {
- String className = safeCharSeqToString(node.getClassName());
- for (String excludedClassName : NAF_EXCLUDED_CLASSES) {
- if (className.endsWith(excludedClassName))
- return true;
- }
- return false;
- }
-
- /**
- * We're looking for UI controls that are enabled, clickable but have no
- * text nor content-description. Such controls configuration indicate an
- * interactive control is present in the UI and is most likely not
- * accessibility friendly. We refer to such controls here as NAF controls
- * (Not Accessibility Friendly)
- *
- * @param node
- * @return false if a node fails the check, true if all is OK
- */
- private static boolean nafCheck(AccessibilityNodeInfo node) {
- boolean isNaf = node.isClickable() && node.isEnabled()
- && safeCharSeqToString(node.getContentDescription()).isEmpty()
- && safeCharSeqToString(node.getText()).isEmpty();
-
- if (!isNaf)
- return true;
-
- // check children since sometimes the containing element is clickable
- // and NAF but a child's text or description is available. Will assume
- // such layout as fine.
- return childNafCheck(node);
- }
-
- /**
- * This should be used when it's already determined that the node is NAF and
- * a further check of its children is in order. A node maybe a container
- * such as LinerLayout and may be set to be clickable but have no text or
- * content description but it is counting on one of its children to fulfill
- * the requirement for being accessibility friendly by having one or more of
- * its children fill the text or content-description. Such a combination is
- * considered by this dumper as acceptable for accessibility.
- *
- * @param node
- * @return false if node fails the check.
- */
- private static boolean childNafCheck(AccessibilityNodeInfo node) {
- int childCount = node.getChildCount();
- for (int x = 0; x < childCount; x++) {
- AccessibilityNodeInfo childNode = node.getChild(x);
-
- if (!safeCharSeqToString(childNode.getContentDescription()).isEmpty()
- || !safeCharSeqToString(childNode.getText()).isEmpty())
- return true;
-
- if (childNafCheck(childNode))
- return true;
- }
- return false;
- }
-
- private static String safeCharSeqToString(CharSequence cs) {
- if (cs == null)
- return "";
- else {
- return stripInvalidXMLChars(cs);
- }
- }
-
- private static String stripInvalidXMLChars(CharSequence cs) {
- StringBuffer ret = new StringBuffer();
- char ch;
- /* http://www.w3.org/TR/xml11/#charsets
- [#x1-#x8], [#xB-#xC], [#xE-#x1F], [#x7F-#x84], [#x86-#x9F], [#xFDD0-#xFDDF],
- [#x1FFFE-#x1FFFF], [#x2FFFE-#x2FFFF], [#x3FFFE-#x3FFFF],
- [#x4FFFE-#x4FFFF], [#x5FFFE-#x5FFFF], [#x6FFFE-#x6FFFF],
- [#x7FFFE-#x7FFFF], [#x8FFFE-#x8FFFF], [#x9FFFE-#x9FFFF],
- [#xAFFFE-#xAFFFF], [#xBFFFE-#xBFFFF], [#xCFFFE-#xCFFFF],
- [#xDFFFE-#xDFFFF], [#xEFFFE-#xEFFFF], [#xFFFFE-#xFFFFF],
- [#x10FFFE-#x10FFFF].
- */
- for (int i = 0; i < cs.length(); i++) {
- ch = cs.charAt(i);
-
- if ((ch >= 0x1 && ch <= 0x8) || (ch >= 0xB && ch <= 0xC) || (ch >= 0xE && ch <= 0x1F) ||
- (ch >= 0x7F && ch <= 0x84) || (ch >= 0x86 && ch <= 0x9f) ||
- (ch >= 0xFDD0 && ch <= 0xFDDF) || (ch >= 0x1FFFE && ch <= 0x1FFFF) ||
- (ch >= 0x2FFFE && ch <= 0x2FFFF) || (ch >= 0x3FFFE && ch <= 0x3FFFF) ||
- (ch >= 0x4FFFE && ch <= 0x4FFFF) || (ch >= 0x5FFFE && ch <= 0x5FFFF) ||
- (ch >= 0x6FFFE && ch <= 0x6FFFF) || (ch >= 0x7FFFE && ch <= 0x7FFFF) ||
- (ch >= 0x8FFFE && ch <= 0x8FFFF) || (ch >= 0x9FFFE && ch <= 0x9FFFF) ||
- (ch >= 0xAFFFE && ch <= 0xAFFFF) || (ch >= 0xBFFFE && ch <= 0xBFFFF) ||
- (ch >= 0xCFFFE && ch <= 0xCFFFF) || (ch >= 0xDFFFE && ch <= 0xDFFFF) ||
- (ch >= 0xEFFFE && ch <= 0xEFFFF) || (ch >= 0xFFFFE && ch <= 0xFFFFF) ||
- (ch >= 0x10FFFE && ch <= 0x10FFFF))
- ret.append(".");
- else
- ret.append(ch);
- }
- return ret.toString();
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/stardust/widget/ViewSwitcher.java b/app/src/main/java/com/stardust/widget/ViewSwitcher.java
new file mode 100644
index 000000000..aa99e07de
--- /dev/null
+++ b/app/src/main/java/com/stardust/widget/ViewSwitcher.java
@@ -0,0 +1,63 @@
+package com.stardust.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+
+/**
+ * Created by Stardust on 2017/3/11.
+ */
+
+public class ViewSwitcher extends android.widget.ViewSwitcher {
+
+ private View mCurrentView;
+
+ public ViewSwitcher(Context context) {
+ super(context);
+ }
+
+ public ViewSwitcher(Context context, View first, View second) {
+ super(context);
+ addView(first);
+ addView(second);
+ }
+
+ public ViewSwitcher(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public View getCurrentView() {
+ return mCurrentView;
+ }
+
+ public int getCurrentViewIndex() {
+ return mCurrentView == getChildAt(0) ? 0 : 1;
+ }
+
+ public void showFirst() {
+ ensureCurrentView();
+ if (mCurrentView != getChildAt(0)) {
+ showPrevious();
+ mCurrentView = getChildAt(0);
+ }
+ }
+
+ private void ensureCurrentView() {
+ if (mCurrentView == null) {
+ mCurrentView = getChildAt(0);
+ }
+ }
+
+ public void showSecond() {
+ ensureCurrentView();
+ if (mCurrentView != getChildAt(1)) {
+ showNext();
+ mCurrentView = getChildAt(1);
+ }
+ }
+
+ public void setSecondView(View v) {
+ removeViewAt(1);
+ addView(v);
+ }
+}
diff --git a/app/src/main/res/drawable/arrow_down.png b/app/src/main/res/drawable/arrow_down.png
new file mode 100644
index 000000000..8eaf0f7f1
Binary files /dev/null and b/app/src/main/res/drawable/arrow_down.png differ
diff --git a/app/src/main/res/drawable/arrow_up.png b/app/src/main/res/drawable/arrow_up.png
new file mode 100644
index 000000000..3ffc57924
Binary files /dev/null and b/app/src/main/res/drawable/arrow_up.png differ
diff --git a/app/src/main/res/drawable/circle.xml b/app/src/main/res/drawable/circle.xml
new file mode 100644
index 000000000..1a1a6bc27
--- /dev/null
+++ b/app/src/main/res/drawable/circle.xml
@@ -0,0 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/circle_with_border.xml b/app/src/main/res/drawable/circle_with_border.xml
new file mode 100644
index 000000000..74698bae8
--- /dev/null
+++ b/app/src/main/res/drawable/circle_with_border.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_edit_white.png b/app/src/main/res/drawable/ic_edit_white.png
new file mode 100644
index 000000000..760e1e338
Binary files /dev/null and b/app/src/main/res/drawable/ic_edit_white.png differ
diff --git a/app/src/main/res/drawable/ic_menu.png b/app/src/main/res/drawable/ic_menu.png
new file mode 100644
index 000000000..9b437059e
Binary files /dev/null and b/app/src/main/res/drawable/ic_menu.png differ
diff --git a/app/src/main/res/drawable/ic_orange_circle.png b/app/src/main/res/drawable/ic_orange_circle.png
new file mode 100644
index 000000000..addea5e1b
Binary files /dev/null and b/app/src/main/res/drawable/ic_orange_circle.png differ
diff --git a/app/src/main/res/drawable/ic_paintbrush.png b/app/src/main/res/drawable/ic_paintbrush.png
new file mode 100644
index 000000000..45246d34c
Binary files /dev/null and b/app/src/main/res/drawable/ic_paintbrush.png differ
diff --git a/app/src/main/res/drawable/ic_pen.png b/app/src/main/res/drawable/ic_pen.png
new file mode 100644
index 000000000..2db9b8344
Binary files /dev/null and b/app/src/main/res/drawable/ic_pen.png differ
diff --git a/app/src/main/res/drawable/ic_resizer.png b/app/src/main/res/drawable/ic_resizer.png
new file mode 100644
index 000000000..a2ed94d22
Binary files /dev/null and b/app/src/main/res/drawable/ic_resizer.png differ
diff --git a/app/src/main/res/drawable/ic_stack.png b/app/src/main/res/drawable/ic_stack.png
new file mode 100644
index 000000000..a1251e539
Binary files /dev/null and b/app/src/main/res/drawable/ic_stack.png differ
diff --git a/app/src/main/res/drawable/more_three_dots_gray.png b/app/src/main/res/drawable/more_three_dots_gray.png
new file mode 100644
index 000000000..6a6346c03
Binary files /dev/null and b/app/src/main/res/drawable/more_three_dots_gray.png differ
diff --git a/app/src/main/res/drawable/more_three_dots_ios_7_white.png b/app/src/main/res/drawable/more_three_dots_ios_7_white.png
new file mode 100644
index 000000000..6b0d03f74
Binary files /dev/null and b/app/src/main/res/drawable/more_three_dots_ios_7_white.png differ
diff --git a/app/src/main/res/drawable/tab_background.png b/app/src/main/res/drawable/tab_background.png
new file mode 100644
index 000000000..ba00401b9
Binary files /dev/null and b/app/src/main/res/drawable/tab_background.png differ
diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml
index 6aa61a598..010fed279 100644
--- a/app/src/main/res/layout/activity_about.xml
+++ b/app/src/main/res/layout/activity_about.xml
@@ -16,7 +16,7 @@
+ app:theme="@style/AppTheme.AppBarOverlay">
+ tools:context="com.stardust.scriptdroid.ui.edit.EditActivity">
+ tools:context="com.stardust.scriptdroid.ui.main.MainActivity">
diff --git a/app/src/main/res/layout/floating_script_list_recycler_view_item.xml b/app/src/main/res/layout/floating_script_list_recycler_view_item.xml
new file mode 100644
index 000000000..4860318a3
--- /dev/null
+++ b/app/src/main/res/layout/floating_script_list_recycler_view_item.xml
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/floating_window_collapse.xml b/app/src/main/res/layout/floating_window_collapse.xml
new file mode 100644
index 000000000..e384dc50d
--- /dev/null
+++ b/app/src/main/res/layout/floating_window_collapse.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/floating_window_expand.xml b/app/src/main/res/layout/floating_window_expand.xml
new file mode 100644
index 000000000..1c1c1fbfc
--- /dev/null
+++ b/app/src/main/res/layout/floating_window_expand.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/floating_window_main_menu.xml b/app/src/main/res/layout/floating_window_main_menu.xml
new file mode 100644
index 000000000..2c8cdb6ff
--- /dev/null
+++ b/app/src/main/res/layout/floating_window_main_menu.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_edit_side_menu.xml b/app/src/main/res/layout/fragment_edit_side_menu.xml
index 8e5c2a12a..6df16b682 100644
--- a/app/src/main/res/layout/fragment_edit_side_menu.xml
+++ b/app/src/main/res/layout/fragment_edit_side_menu.xml
@@ -51,7 +51,7 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_slide_menu.xml b/app/src/main/res/layout/fragment_slide_menu.xml
index 39050b95d..bd94466b6 100644
--- a/app/src/main/res/layout/fragment_slide_menu.xml
+++ b/app/src/main/res/layout/fragment_slide_menu.xml
@@ -84,7 +84,7 @@
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/node_info_view_item.xml b/app/src/main/res/layout/node_info_view_item.xml
new file mode 100644
index 000000000..7a83f5d71
--- /dev/null
+++ b/app/src/main/res/layout/node_info_view_item.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/resizable_floaty_container.xml b/app/src/main/res/layout/resizable_floaty_container.xml
new file mode 100644
index 000000000..27aed6201
--- /dev/null
+++ b/app/src/main/res/layout/resizable_floaty_container.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml
index ae9a1e4a2..1175d1593 100644
--- a/app/src/main/res/menu/menu_main.xml
+++ b/app/src/main/res/menu/menu_main.xml
@@ -1,6 +1,6 @@
diff --git a/app/src/main/res/raw/licenses.xml b/app/src/main/res/raw/licenses.xml
index c99b533e0..d86a1ae95 100644
--- a/app/src/main/res/raw/licenses.xml
+++ b/app/src/main/res/raw/licenses.xml
@@ -1,5 +1,11 @@
+
+ LeakCanary
+ Copyright 2015 Square, Inc.
+ https://github.com/square/leakcanary
+ Apache Software License 2.0
+
Material Dialogs
https://github.com/afollestad/material-dialogs
@@ -49,9 +55,27 @@
Apache Software License 2.0
- LeakCanary
- Copyright 2015 Square, Inc.
- https://github.com/square/leakcanary
+ Floaties
+
+ https://github.com/ericbhatti/floaties
+ Apache Software License 2.0
+
+
+ SortableTableView
+ Copyright 2015 Ingo Schwarz
+ https://github.com/ISchwarz23/SortableTableView
+ Apache Software License 2.0
+
+
+ Android Terminal Emulator
+
+ https://github.com/jackpal/Android-Terminal-Emulator
+ Apache Software License 2.0
+
+
+ MultiLevelListView
+ 2016 (C) Copyright Open-RnD Sp. z o.o.
+ https://github.com/open-rnd/android-multi-level-listview
Apache Software License 2.0
\ No newline at end of file
diff --git a/app/src/main/res/raw/mpl_20_full.txt b/app/src/main/res/raw/mpl_20_full.txt
index 972fbad8d..bd14187ce 100644
--- a/app/src/main/res/raw/mpl_20_full.txt
+++ b/app/src/main/res/raw/mpl_20_full.txt
@@ -39,7 +39,7 @@ means any of the following:
any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or
-any new file in Source Code Form that contains any Covered Software.
+any new file in Source Code Form that regex any Covered Software.
1.11. “Patent Claims” of a Contributor
means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version.
@@ -161,7 +161,7 @@ You may distribute the Covered Software under the terms of the version of the Li
10.3. Modified Versions
-If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License).
+If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the key of the license steward (except to note that such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses
diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml
index 989696d83..32f194784 100644
--- a/app/src/main/res/values-zh/strings.xml
+++ b/app/src/main/res/values-zh/strings.xml
@@ -29,7 +29,7 @@
Issues不可用,请联系软件开发者.
未知错误
OK
-
+
- 描述至少要 %d 个字.
- 描述至少要 %d 个字.
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 812cb7be0..f8efef399 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -3,4 +3,8 @@
16dp
16dp
16dp
+ 3dp
+ 6dp
+ 2dp
+ -4dp
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 0d178ac80..11b0a0000 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -50,7 +50,7 @@
邮箱
https://github.com/hyb1996/NoRootScriptDroid
[免Root脚本精灵]下载地址:http://www.coolapk.com/apk/com.stardust.scriptdroid
- 点击区域辅助服务
+ 悬浮窗
通知栏点击区域辅助开关
其他
点击区域辅助服务已开启(点击关闭)
@@ -137,4 +137,9 @@
帮助
录制脚本(root)
使用音量上键,音量下键结束录制
+ 没有悬浮窗权限
+ 节点信息
+ 布局层次分析
+ 布局范围查看
+ 无障碍服务未启动
diff --git a/app/src/test/java/com/stardust/scriptdroid/ExampleUnitTest.java b/app/src/test/java/com/stardust/scriptdroid/ExampleUnitTest.java
index baa43816c..f70fbcd41 100644
--- a/app/src/test/java/com/stardust/scriptdroid/ExampleUnitTest.java
+++ b/app/src/test/java/com/stardust/scriptdroid/ExampleUnitTest.java
@@ -1,7 +1,6 @@
package com.stardust.scriptdroid;
import com.stardust.scriptdroid.record.inputevent.InputEventToJsConverter;
-import com.stardust.scriptdroid.record.inputevent.InputEventToSendEventConverter;
import org.junit.Test;
diff --git a/automator/.gitignore b/automator/.gitignore
new file mode 100644
index 000000000..796b96d1c
--- /dev/null
+++ b/automator/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/automator/build.gradle b/automator/build.gradle
new file mode 100644
index 000000000..32b0ae9b5
--- /dev/null
+++ b/automator/build.gradle
@@ -0,0 +1,31 @@
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion 25
+ buildToolsVersion "25.0.2"
+
+ defaultConfig {
+ minSdkVersion 19
+ targetSdkVersion 25
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ compile fileTree(dir: 'libs', include: ['*.jar'])
+ androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
+ exclude group: 'com.android.support', module: 'support-annotations'
+ })
+ compile 'com.android.support:appcompat-v7:25.2.0'
+ testCompile 'junit:junit:4.12'
+}
diff --git a/automator/proguard-rules.pro b/automator/proguard-rules.pro
new file mode 100644
index 000000000..fc015b80d
--- /dev/null
+++ b/automator/proguard-rules.pro
@@ -0,0 +1,25 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in E:\YiBin\eclipse\Android_SDK_windows/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class key to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file key.
+#-renamesourcefileattribute SourceFile
diff --git a/automator/src/androidTest/java/com/stardust/automator/ExampleInstrumentedTest.java b/automator/src/androidTest/java/com/stardust/automator/ExampleInstrumentedTest.java
new file mode 100644
index 000000000..bf26d3b76
--- /dev/null
+++ b/automator/src/androidTest/java/com/stardust/automator/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package com.stardust.automator;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumentation test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() throws Exception {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getTargetContext();
+
+ assertEquals("com.stardust.automator.test", appContext.getPackageName());
+ }
+}
diff --git a/automator/src/main/AndroidManifest.xml b/automator/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..05cba8708
--- /dev/null
+++ b/automator/src/main/AndroidManifest.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
diff --git a/automator/src/main/java/com/stardust/automator/AccessibilityEventCommandHost.java b/automator/src/main/java/com/stardust/automator/AccessibilityEventCommandHost.java
new file mode 100644
index 000000000..5c0f584ed
--- /dev/null
+++ b/automator/src/main/java/com/stardust/automator/AccessibilityEventCommandHost.java
@@ -0,0 +1,60 @@
+package com.stardust.automator;
+
+import android.accessibilityservice.AccessibilityService;
+import android.util.Log;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.stardust.view.accessibility.AccessibilityDelegate;
+
+import java.util.LinkedList;
+import java.util.Queue;
+
+/**
+ * Created by Stardust on 2017/3/9.
+ */
+
+public class AccessibilityEventCommandHost implements AccessibilityDelegate {
+
+ public interface Command {
+
+ void execute(AccessibilityService service, AccessibilityEvent event);
+ }
+
+ public static final AccessibilityEventCommandHost instance = new AccessibilityEventCommandHost();
+
+ private static final String TAG = "CommandHostDelegate";
+
+ private final Queue mCommands = new LinkedList<>();
+
+ @Override
+ public boolean onAccessibilityEvent(AccessibilityService service, AccessibilityEvent event) {
+ synchronized (mCommands) {
+ if (!mCommands.isEmpty()) {
+ Log.v(TAG, "execute " + mCommands.size() + " commands");
+ }
+ while (!mCommands.isEmpty()) {
+ Command command = mCommands.poll();
+ command.execute(service, event);
+ synchronized (command) {
+ command.notify();
+ }
+ }
+ }
+ return false;
+ }
+
+
+ public void executeAndWaitForEvent(Command command) {
+ synchronized (mCommands) {
+ mCommands.offer(command);
+ }
+ synchronized (command) {
+ try {
+ command.wait();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+}
diff --git a/automator/src/main/java/com/stardust/automator/ActionArgument.java b/automator/src/main/java/com/stardust/automator/ActionArgument.java
new file mode 100644
index 000000000..25fad5e5f
--- /dev/null
+++ b/automator/src/main/java/com/stardust/automator/ActionArgument.java
@@ -0,0 +1,63 @@
+package com.stardust.automator;
+
+import android.os.Bundle;
+
+/**
+ * Created by Stardust on 2017/3/9.
+ */
+
+public abstract class ActionArgument {
+
+ protected final String mKey;
+
+ private ActionArgument(String key) {
+ mKey = key;
+ }
+
+ public abstract void putIn(Bundle bundle);
+
+ public static class IntActionArgument extends ActionArgument {
+
+ private final int mInt;
+
+ public IntActionArgument(String name, int i) {
+ super(name);
+ mInt = i;
+ }
+
+ @Override
+ public void putIn(Bundle bundle) {
+ bundle.putInt(mKey, mInt);
+ }
+ }
+
+ public static class CharSequenceActionArgument extends ActionArgument {
+
+ private final CharSequence mCharSequence;
+
+ public CharSequenceActionArgument(String name, CharSequence charSequence) {
+ super(name);
+ mCharSequence = charSequence;
+ }
+
+ @Override
+ public void putIn(Bundle bundle) {
+ bundle.putCharSequence(mKey, mCharSequence);
+ }
+ }
+
+ public static class FloatActionArgument extends ActionArgument {
+ private final float mFloat;
+
+ public FloatActionArgument(String name, float value) {
+ super(name);
+ mFloat = value;
+ }
+
+ @Override
+ public void putIn(Bundle bundle) {
+ bundle.putFloat(mKey, mFloat);
+ }
+ }
+
+}
diff --git a/automator/src/main/java/com/stardust/automator/GC.java b/automator/src/main/java/com/stardust/automator/GC.java
new file mode 100644
index 000000000..94f622ff4
--- /dev/null
+++ b/automator/src/main/java/com/stardust/automator/GC.java
@@ -0,0 +1,12 @@
+package com.stardust.automator;
+
+import android.view.accessibility.AccessibilityNodeInfo;
+
+/**
+ * Created by Stardust on 2017/3/9.
+ */
+
+public class GC {
+
+
+}
diff --git a/automator/src/main/java/com/stardust/automator/UiBridge.java b/automator/src/main/java/com/stardust/automator/UiBridge.java
new file mode 100644
index 000000000..8ddb01c6d
--- /dev/null
+++ b/automator/src/main/java/com/stardust/automator/UiBridge.java
@@ -0,0 +1,15 @@
+package com.stardust.automator;
+
+/**
+ * Created by Stardust on 2017/3/9.
+ */
+
+public class UiBridge {
+
+
+ public UiBridge() {
+ }
+
+
+
+}
diff --git a/automator/src/main/java/com/stardust/automator/UiGlobalSelector.java b/automator/src/main/java/com/stardust/automator/UiGlobalSelector.java
new file mode 100644
index 000000000..37f88870b
--- /dev/null
+++ b/automator/src/main/java/com/stardust/automator/UiGlobalSelector.java
@@ -0,0 +1,289 @@
+package com.stardust.automator;
+
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.stardust.automator.filter.BooleanFilter;
+import com.stardust.automator.filter.BoundsFilter;
+import com.stardust.automator.filter.IdFilter;
+import com.stardust.automator.filter.PackageNameFilter;
+import com.stardust.automator.filter.TextFilter;
+import com.stardust.automator.filter.ClassNameFilter;
+import com.stardust.automator.filter.DescFilter;
+import com.stardust.automator.filter.ListFilter;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.*;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CONTEXT_CLICK;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_DOWN;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_LEFT;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_RIGHT;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_UP;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SET_PROGRESS;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SHOW_ON_SCREEN;
+
+/**
+ * Created by Stardust on 2017/3/8.
+ */
+
+public class UiGlobalSelector {
+
+ private Queue mFilters = new LinkedList<>();
+
+ //// 第一类筛选条件
+
+ public UiGlobalSelector id(String id) {
+ mFilters.add(IdFilter.equals(id));
+ return this;
+ }
+
+ public UiGlobalSelector idContains(String str) {
+ mFilters.add(IdFilter.contains(str));
+ return this;
+ }
+
+ public UiGlobalSelector idStartsWith(String prefix) {
+ mFilters.add(IdFilter.startsWith(prefix));
+ return this;
+ }
+
+ public UiGlobalSelector idEndsWith(String suffix) {
+ mFilters.add(IdFilter.endsWith(suffix));
+ return this;
+ }
+
+ public UiGlobalSelector idMatches(String regex) {
+ mFilters.add(IdFilter.matches(regex));
+ return this;
+ }
+
+ public UiGlobalSelector text(String text) {
+ mFilters.add(TextFilter.equals(text));
+ return this;
+ }
+
+ public UiGlobalSelector textContains(String str) {
+ mFilters.add(TextFilter.contains(str));
+ return this;
+ }
+
+ public UiGlobalSelector textStartsWith(String prefix) {
+ mFilters.add(TextFilter.startsWith(prefix));
+ return this;
+ }
+
+ public UiGlobalSelector textEndsWith(String suffix) {
+ mFilters.add(TextFilter.endsWith(suffix));
+ return this;
+ }
+
+ public UiGlobalSelector textMatches(String regex) {
+ mFilters.add(TextFilter.matches(regex));
+ return this;
+ }
+
+ public UiGlobalSelector desc(String desc) {
+ mFilters.add(DescFilter.equals(desc));
+ return this;
+ }
+
+ public UiGlobalSelector descContains(String str) {
+ mFilters.add(DescFilter.contains(str));
+ return this;
+ }
+
+ public UiGlobalSelector descStartsWith(String prefix) {
+ mFilters.add(DescFilter.startsWith(prefix));
+ return this;
+ }
+
+ public UiGlobalSelector descEndsWith(String suffix) {
+ mFilters.add(DescFilter.endsWith(suffix));
+ return this;
+ }
+
+ public UiGlobalSelector descMatches(String regex) {
+ mFilters.add(DescFilter.matches(regex));
+ return this;
+ }
+
+ public UiGlobalSelector className(String className) {
+ mFilters.add(ClassNameFilter.equals(className));
+ return this;
+ }
+
+ public UiGlobalSelector classNameContains(String str) {
+ mFilters.add(ClassNameFilter.contains(str));
+ return this;
+ }
+
+ public UiGlobalSelector classNameStartsWith(String prefix) {
+ mFilters.add(ClassNameFilter.startsWith(prefix));
+ return this;
+ }
+
+ public UiGlobalSelector classNameEndsWith(String suffix) {
+ mFilters.add(ClassNameFilter.endsWith(suffix));
+ return this;
+ }
+
+ public UiGlobalSelector classNameMatches(String regex) {
+ mFilters.add(ClassNameFilter.matches(regex));
+ return this;
+ }
+
+ public UiGlobalSelector packageName(String packageName) {
+ mFilters.add(PackageNameFilter.equals(packageName));
+ return this;
+ }
+
+ public UiGlobalSelector packageNameContains(String str) {
+ mFilters.add(PackageNameFilter.contains(str));
+ return this;
+ }
+
+ public UiGlobalSelector packageNameStartsWith(String prefix) {
+ mFilters.add(PackageNameFilter.startsWith(prefix));
+ return this;
+ }
+
+ public UiGlobalSelector packageNameEndsWith(String suffix) {
+ mFilters.add(PackageNameFilter.endsWith(suffix));
+ return this;
+ }
+
+ public UiGlobalSelector packageNameMatches(String regex) {
+ mFilters.add(PackageNameFilter.matches(regex));
+ return this;
+ }
+
+ public UiGlobalSelector bounds(int l, int t, int r, int b) {
+ mFilters.add(new BoundsFilter(new Rect(l, t, r, b), BoundsFilter.TYPE_EQUALS));
+ return this;
+ }
+
+ public UiGlobalSelector boundsInside(int l, int t, int r, int b) {
+ mFilters.add(new BoundsFilter(new Rect(l, t, r, b), BoundsFilter.TYPE_INSIDE));
+ return this;
+ }
+
+ public UiGlobalSelector boundsInParent(int l, int t, int r, int b) {
+ mFilters.add(new BoundsFilter(new Rect(l, t, r, b), BoundsFilter.TYPE_PARENT));
+ return this;
+ }
+
+ //// 第二类筛选条件 -able
+
+ public UiGlobalSelector checkable(boolean b) {
+ mFilters.add(BooleanFilter.get(BooleanFilter.CHECKABLE, b));
+ return this;
+ }
+
+ public UiGlobalSelector checked(boolean b) {
+ mFilters.add(BooleanFilter.get(BooleanFilter.CHECKED, b));
+ return this;
+ }
+
+ public UiGlobalSelector focusable(boolean b) {
+ mFilters.add(BooleanFilter.get(BooleanFilter.FOCUSABLE, b));
+ return this;
+ }
+
+ public UiGlobalSelector focused(boolean b) {
+ mFilters.add(BooleanFilter.get(BooleanFilter.FOCUSED, b));
+ return this;
+ }
+
+ public UiGlobalSelector visibleToUser(boolean b) {
+ mFilters.add(BooleanFilter.get(BooleanFilter.VISIBLE_TO_USER, b));
+ return this;
+ }
+
+ public UiGlobalSelector accessibilityFocused(boolean b) {
+ mFilters.add(BooleanFilter.get(BooleanFilter.ACCESSIBILITY_FOCUSED, b));
+ return this;
+ }
+
+ public UiGlobalSelector selected(boolean b) {
+ mFilters.add(BooleanFilter.get(BooleanFilter.SELECTED, b));
+ return this;
+ }
+
+ public UiGlobalSelector clickable(boolean b) {
+ mFilters.add(BooleanFilter.get(BooleanFilter.CLICKABLE, b));
+ return this;
+ }
+
+ public UiGlobalSelector longClickable(boolean b) {
+ mFilters.add(BooleanFilter.get(BooleanFilter.LONG_CLICKABLE, b));
+ return this;
+ }
+
+ public UiGlobalSelector enabled(boolean b) {
+ mFilters.add(BooleanFilter.get(BooleanFilter.ENABLED, b));
+ return this;
+ }
+
+ public UiGlobalSelector password(boolean b) {
+ mFilters.add(BooleanFilter.get(BooleanFilter.PASSWORD, b));
+ return this;
+ }
+
+ public UiGlobalSelector scrollable(boolean b) {
+ mFilters.add(BooleanFilter.get(BooleanFilter.SCROLLABLE, b));
+ return this;
+ }
+
+ public UiGlobalSelector editable(boolean b) {
+ mFilters.add(BooleanFilter.get(BooleanFilter.EDITABLE, b));
+ return this;
+ }
+
+ public UiGlobalSelector contentInvalid(boolean b) {
+ mFilters.add(BooleanFilter.get(BooleanFilter.CONTENT_INVALID, b));
+ return this;
+ }
+
+ public UiGlobalSelector contextClickable(boolean b) {
+ mFilters.add(BooleanFilter.get(BooleanFilter.CONTEXT_CLICKABLE, b));
+ return this;
+ }
+
+ public UiGlobalSelector multiLine(boolean b) {
+ mFilters.add(BooleanFilter.get(BooleanFilter.MULTI_LINE, b));
+ return this;
+ }
+
+ public UiGlobalSelector dismissable(boolean b) {
+ mFilters.add(BooleanFilter.get(BooleanFilter.DISMISSABLE, b));
+ return this;
+ }
+
+ public UiObjectCollection findOf(AccessibilityNodeInfo node) {
+ List list = new ArrayList<>();
+ list.add(node);
+ for (ListFilter filter : mFilters) {
+ list = filter.filter(list);
+ }
+ return UiObjectCollection.of(list);
+ }
+
+ public UiObject findOneOf(AccessibilityNodeInfo node) {
+ // TODO: 2017/3/9 优化
+ return new UiObject(findOf(node).get(0).getInfo());
+ }
+
+ public UiGlobalSelector addFilter(ListFilter filter) {
+ mFilters.add(filter);
+ return this;
+ }
+
+
+
+}
diff --git a/automator/src/main/java/com/stardust/automator/UiObject.java b/automator/src/main/java/com/stardust/automator/UiObject.java
new file mode 100644
index 000000000..2ffeb3b5a
--- /dev/null
+++ b/automator/src/main/java/com/stardust/automator/UiObject.java
@@ -0,0 +1,184 @@
+package com.stardust.automator;
+
+import android.os.Bundle;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import java.util.ArrayList;
+
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CONTEXT_CLICK;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_DOWN;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_LEFT;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_RIGHT;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_UP;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SET_PROGRESS;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SHOW_ON_SCREEN;
+
+/**
+ * Created by Stardust on 2017/3/9.
+ */
+
+public class UiObject extends AccessibilityNodeInfoCompat {
+
+ public UiObject(Object info) {
+ super(info);
+ }
+
+ public UiObject parent() {
+ return new UiObject(getParent().getInfo());
+ }
+
+ public UiObject child(int i) {
+ return new UiObject(getChild(i).getInfo());
+ }
+
+ public UiObjectCollection children() {
+ ArrayList list = new ArrayList<>(getChildCount());
+ for (int i = 0; i < getChildCount(); i++) {
+ list.add(getChild(i));
+ }
+ return UiObjectCollection.ofCompat(list);
+ }
+
+ public String id() {
+ return getViewIdResourceName();
+ }
+
+ public CharSequence text() {
+ return getText();
+ }
+
+ public CharSequence desc() {
+ return getContentDescription();
+ }
+
+ public CharSequence className() {
+ return getClassName();
+ }
+
+ public CharSequence packageName() {
+
+ return getPackageName();
+ }
+
+ public boolean performAction(int action, ActionArgument... arguments) {
+ Bundle bundle = argumentsToBundle(arguments);
+ return performAction(action, bundle);
+ }
+
+ private static Bundle argumentsToBundle(ActionArgument[] arguments) {
+ Bundle bundle = new Bundle();
+ for (ActionArgument arg : arguments) {
+ arg.putIn(bundle);
+ }
+ return bundle;
+ }
+
+ public boolean click() {
+ return performAction(ACTION_CLICK);
+ }
+
+ public boolean longClick() {
+ return performAction(ACTION_LONG_CLICK);
+ }
+
+ public boolean accessibilityFocus() {
+ return performAction(ACTION_ACCESSIBILITY_FOCUS);
+ }
+
+ public boolean clearAccessibilityFocus() {
+ return performAction(ACTION_CLEAR_ACCESSIBILITY_FOCUS);
+ }
+
+ public boolean focus() {
+ return performAction(ACTION_FOCUS);
+ }
+
+ public boolean clearFocus() {
+ return performAction(ACTION_CLEAR_FOCUS);
+ }
+
+ public boolean copy() {
+ return performAction(ACTION_COPY);
+ }
+
+ public boolean paste() {
+ return performAction(ACTION_PASTE);
+ }
+
+ public boolean select() {
+ return performAction(ACTION_SELECT);
+ }
+
+ public boolean cut() {
+ return performAction(ACTION_CUT);
+ }
+
+ public boolean collapse() {
+ return performAction(ACTION_COLLAPSE);
+ }
+
+ public boolean expand() {
+ return performAction(ACTION_EXPAND);
+ }
+
+ public boolean dismiss() {
+ return performAction(ACTION_DISMISS);
+ }
+
+ public boolean show() {
+ return performAction(ACTION_SHOW_ON_SCREEN.getId());
+ }
+
+ public boolean scrollForward() {
+ return performAction(ACTION_SCROLL_FORWARD);
+ }
+
+ public boolean scrollBackward() {
+ return performAction(ACTION_SCROLL_BACKWARD);
+ }
+
+ public boolean scrollUp() {
+ return performAction(ACTION_SCROLL_UP.getId());
+ }
+
+ public boolean scrollDown() {
+ return performAction(ACTION_SCROLL_DOWN.getId());
+ }
+
+ public boolean scrollLeft() {
+ return performAction(ACTION_SCROLL_LEFT.getId());
+ }
+
+ public boolean scrollRight() {
+ return performAction(ACTION_SCROLL_RIGHT.getId());
+ }
+
+ public boolean contextClick() {
+ return performAction(ACTION_CONTEXT_CLICK.getId());
+ }
+
+ public boolean setSelection(int s, int e) {
+ return performAction(ACTION_SET_SELECTION,
+ new ActionArgument.IntActionArgument(ACTION_ARGUMENT_SELECTION_START_INT, s),
+ new ActionArgument.IntActionArgument(ACTION_ARGUMENT_SELECTION_END_INT, e));
+ }
+
+ public boolean setText(String text) {
+ return performAction(ACTION_SET_TEXT,
+ new ActionArgument.CharSequenceActionArgument(ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text));
+ }
+
+ public boolean setProgress(float value) {
+ return performAction(ACTION_SET_PROGRESS.getId(),
+ new ActionArgument.FloatActionArgument(ACTION_ARGUMENT_PROGRESS_VALUE, value));
+ }
+
+ public boolean scrollTo(int row, int column) {
+ return performAction(ACTION_SCROLL_TO_POSITION.getId(),
+ new ActionArgument.IntActionArgument(ACTION_ARGUMENT_ROW_INT, row),
+ new ActionArgument.IntActionArgument(ACTION_ARGUMENT_COLUMN_INT, column));
+ }
+
+}
diff --git a/automator/src/main/java/com/stardust/automator/UiObjectCollection.java b/automator/src/main/java/com/stardust/automator/UiObjectCollection.java
new file mode 100644
index 000000000..5c64d6e90
--- /dev/null
+++ b/automator/src/main/java/com/stardust/automator/UiObjectCollection.java
@@ -0,0 +1,211 @@
+package com.stardust.automator;
+
+import android.os.Bundle;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.stardust.util.Consumer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.*;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CONTEXT_CLICK;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_DOWN;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_LEFT;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_RIGHT;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_UP;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SET_PROGRESS;
+import static android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SHOW_ON_SCREEN;
+
+/**
+ * Created by Stardust on 2017/3/9.
+ */
+
+public class UiObjectCollection {
+
+
+ public static UiObjectCollection ofCompat(List list) {
+ return new UiObjectCollection(list);
+ }
+
+ public static UiObjectCollection of(List list) {
+ List compatList = new ArrayList<>(list.size());
+ for (AccessibilityNodeInfo nodeInfo : list) {
+ compatList.add(new AccessibilityNodeInfoCompat(nodeInfo));
+ }
+ return new UiObjectCollection(compatList);
+ }
+
+ private List mNodes;
+ private UiObject[] mUiObjects;
+
+
+ private UiObjectCollection(List list) {
+ mNodes = list;
+ }
+
+ public boolean performAction(int action) {
+ boolean fail = false;
+ for (AccessibilityNodeInfoCompat node : mNodes) {
+ if (!node.performAction(action)) {
+ fail = true;
+ }
+ }
+ return !fail;
+ }
+
+ public boolean performAction(int action, ActionArgument... arguments) {
+ boolean fail = false;
+ Bundle bundle = argumentsToBundle(arguments);
+ for (AccessibilityNodeInfoCompat node : mNodes) {
+ if (!node.performAction(action, bundle)) {
+ fail = true;
+ }
+ }
+ return !fail;
+ }
+
+ private Bundle argumentsToBundle(ActionArgument[] arguments) {
+ Bundle bundle = new Bundle();
+ for (ActionArgument arg : arguments) {
+ arg.putIn(bundle);
+ }
+ return bundle;
+ }
+
+ public boolean click() {
+ return performAction(ACTION_CLICK);
+ }
+
+ public boolean longClick() {
+ return performAction(ACTION_LONG_CLICK);
+ }
+
+ public boolean accessibilityFocus() {
+ return performAction(ACTION_ACCESSIBILITY_FOCUS);
+ }
+
+ public boolean clearAccessibilityFocus() {
+ return performAction(ACTION_CLEAR_ACCESSIBILITY_FOCUS);
+ }
+
+ public boolean focus() {
+ return performAction(ACTION_FOCUS);
+ }
+
+ public boolean clearFocus() {
+ return performAction(ACTION_CLEAR_FOCUS);
+ }
+
+ public boolean copy() {
+ return performAction(ACTION_COPY);
+ }
+
+ public boolean paste() {
+ return performAction(ACTION_PASTE);
+ }
+
+ public boolean select() {
+ return performAction(ACTION_SELECT);
+ }
+
+ public boolean cut() {
+ return performAction(ACTION_CUT);
+ }
+
+ public boolean collapse() {
+ return performAction(ACTION_COLLAPSE);
+ }
+
+ public boolean expand() {
+ return performAction(ACTION_EXPAND);
+ }
+
+ public boolean dismiss() {
+ return performAction(ACTION_DISMISS);
+ }
+
+ public boolean show() {
+ return performAction(ACTION_SHOW_ON_SCREEN.getId());
+ }
+
+ public boolean scrollForward() {
+ return performAction(ACTION_SCROLL_FORWARD);
+ }
+
+ public boolean scrollBackward() {
+ return performAction(ACTION_SCROLL_BACKWARD);
+ }
+
+ public boolean scrollUp() {
+ return performAction(ACTION_SCROLL_UP.getId());
+ }
+
+ public boolean scrollDown() {
+ return performAction(ACTION_SCROLL_DOWN.getId());
+ }
+
+ public boolean scrollLeft() {
+ return performAction(ACTION_SCROLL_LEFT.getId());
+ }
+
+ public boolean scrollRight() {
+ return performAction(ACTION_SCROLL_RIGHT.getId());
+ }
+
+ public boolean contextClick() {
+ return performAction(ACTION_CONTEXT_CLICK.getId());
+ }
+
+ public boolean setSelection(int s, int e) {
+ return performAction(ACTION_SET_SELECTION,
+ new ActionArgument.IntActionArgument(ACTION_ARGUMENT_SELECTION_START_INT, s),
+ new ActionArgument.IntActionArgument(ACTION_ARGUMENT_SELECTION_END_INT, e));
+ }
+
+ public boolean setText(CharSequence text) {
+ return performAction(ACTION_SET_TEXT,
+ new ActionArgument.CharSequenceActionArgument(ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text));
+ }
+
+ public boolean setProgress(float value) {
+ return performAction(ACTION_SET_PROGRESS.getId(),
+ new ActionArgument.FloatActionArgument(ACTION_ARGUMENT_PROGRESS_VALUE, value));
+
+ }
+
+ public boolean scrollTo(int row, int column) {
+ return performAction(ACTION_SCROLL_TO_POSITION.getId(),
+ new ActionArgument.IntActionArgument(ACTION_ARGUMENT_ROW_INT, row),
+ new ActionArgument.IntActionArgument(ACTION_ARGUMENT_COLUMN_INT, column));
+ }
+
+ public UiObject get(int i) {
+ ensureUiObjectAt(i);
+ return mUiObjects[i];
+ }
+
+ private void ensureUiObjectAt(int i) {
+ if (mUiObjects == null) {
+ mUiObjects = new UiObject[mNodes.size()];
+ }
+ if (mUiObjects[i] == null) {
+ mUiObjects[i] = new UiObject(mNodes.get(i).getInfo());
+ }
+ }
+
+ public int size() {
+ return mNodes.size();
+ }
+
+ public UiObjectCollection each(Consumer consumer) {
+ for (int i = 0; i < mNodes.size(); i++) {
+ ensureUiObjectAt(i);
+ consumer.accept(mUiObjects[i]);
+ }
+ return this;
+ }
+
+}
diff --git a/automator/src/main/java/com/stardust/automator/filter/BooleanFilter.java b/automator/src/main/java/com/stardust/automator/filter/BooleanFilter.java
new file mode 100644
index 000000000..5063d0a7d
--- /dev/null
+++ b/automator/src/main/java/com/stardust/automator/filter/BooleanFilter.java
@@ -0,0 +1,193 @@
+package com.stardust.automator.filter;
+
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Created by Stardust on 2017/3/9.
+ */
+
+public class BooleanFilter extends DfsFilter {
+
+ private static Map cache = new HashMap<>();
+
+
+ public static BooleanFilter get(BooleanSupplier supplier, boolean b) {
+ BooleanFilter[] booleanFilters = cache.get(supplier);
+ if (booleanFilters == null) {
+ booleanFilters = new BooleanFilter[2];
+ cache.put(supplier, booleanFilters);
+ }
+ int i = b ? 1 : 0;
+ if (booleanFilters[i] == null) {
+ booleanFilters[i] = new BooleanFilter(supplier, b);
+ }
+ return booleanFilters[i];
+ }
+
+ public interface BooleanSupplier {
+
+ boolean get(AccessibilityNodeInfo node);
+
+ }
+
+ public static final BooleanSupplier CHECKABLE = new BooleanSupplier() {
+
+ @Override
+ public boolean get(AccessibilityNodeInfo node) {
+ return node.isCheckable();
+ }
+ };
+
+ public static final BooleanSupplier CHECKED = new BooleanSupplier() {
+
+ @Override
+ public boolean get(AccessibilityNodeInfo node) {
+ return node.isChecked();
+ }
+ };
+
+ public static final BooleanSupplier FOCUSABLE = new BooleanSupplier() {
+
+ @Override
+ public boolean get(AccessibilityNodeInfo node) {
+ return node.isFocusable();
+ }
+ };
+
+ public static final BooleanSupplier FOCUSED = new BooleanSupplier() {
+
+ @Override
+ public boolean get(AccessibilityNodeInfo node) {
+ return node.isFocused();
+ }
+ };
+
+ public static final BooleanSupplier VISIBLE_TO_USER = new BooleanSupplier() {
+
+ @Override
+ public boolean get(AccessibilityNodeInfo node) {
+ return node.isVisibleToUser();
+ }
+ };
+
+ public static final BooleanSupplier ACCESSIBILITY_FOCUSED = new BooleanSupplier() {
+
+ @Override
+ public boolean get(AccessibilityNodeInfo node) {
+ return node.isAccessibilityFocused();
+ }
+ };
+
+ public static final BooleanSupplier SELECTED = new BooleanSupplier() {
+
+ @Override
+ public boolean get(AccessibilityNodeInfo node) {
+ return node.isSelected();
+ }
+ };
+
+ public static final BooleanSupplier CLICKABLE = new BooleanSupplier() {
+
+ @Override
+ public boolean get(AccessibilityNodeInfo node) {
+ return node.isClickable();
+ }
+ };
+
+ public static final BooleanSupplier LONG_CLICKABLE = new BooleanSupplier() {
+
+ @Override
+ public boolean get(AccessibilityNodeInfo node) {
+ return node.isLongClickable();
+ }
+ };
+
+ public static final BooleanSupplier ENABLED = new BooleanSupplier() {
+
+ @Override
+ public boolean get(AccessibilityNodeInfo node) {
+ return node.isEnabled();
+ }
+ };
+
+
+ public static final BooleanSupplier PASSWORD = new BooleanSupplier() {
+
+ @Override
+ public boolean get(AccessibilityNodeInfo node) {
+ return node.isPassword();
+ }
+ };
+
+ public static final BooleanSupplier SCROLLABLE = new BooleanSupplier() {
+
+ @Override
+ public boolean get(AccessibilityNodeInfo node) {
+ return node.isScrollable();
+ }
+ };
+
+ public static final BooleanSupplier EDITABLE = new BooleanSupplier() {
+
+ @Override
+ public boolean get(AccessibilityNodeInfo node) {
+ return node.isEditable();
+ }
+ };
+ public static final BooleanSupplier CONTENT_INVALID = new BooleanSupplier() {
+
+ @Override
+ public boolean get(AccessibilityNodeInfo node) {
+ return node.isContentInvalid();
+ }
+ };
+
+ public static final BooleanSupplier CONTEXT_CLICKABLE = new BooleanSupplier() {
+
+ @Override
+ public boolean get(AccessibilityNodeInfo node) {
+ return node.isContextClickable();
+ }
+ };
+
+ public static final BooleanSupplier MULTI_LINE = new BooleanSupplier() {
+
+ @Override
+ public boolean get(AccessibilityNodeInfo node) {
+ return node.isMultiLine();
+ }
+ };
+
+ public static final BooleanSupplier DISMISSABLE = new BooleanSupplier() {
+
+ @Override
+ public boolean get(AccessibilityNodeInfo node) {
+ return node.isDismissable();
+ }
+ };
+
+ public static final BooleanSupplier importantForAccessibility = new BooleanSupplier() {
+
+ @Override
+ public boolean get(AccessibilityNodeInfo node) {
+ return node.isImportantForAccessibility();
+ }
+ };
+
+ private BooleanSupplier mBooleanSupplier;
+ private boolean mExceptedValue;
+
+ public BooleanFilter(BooleanSupplier booleanSupplier, boolean exceptedValue) {
+ mBooleanSupplier = booleanSupplier;
+ mExceptedValue = exceptedValue;
+ }
+
+ @Override
+ protected boolean isIncluded(AccessibilityNodeInfo nodeInfo) {
+ return nodeInfo != null && mBooleanSupplier.get(nodeInfo) == mExceptedValue;
+ }
+
+}
diff --git a/automator/src/main/java/com/stardust/automator/filter/BoundsFilter.java b/automator/src/main/java/com/stardust/automator/filter/BoundsFilter.java
new file mode 100644
index 000000000..aac8d8476
--- /dev/null
+++ b/automator/src/main/java/com/stardust/automator/filter/BoundsFilter.java
@@ -0,0 +1,36 @@
+package com.stardust.automator.filter;
+
+import android.graphics.Rect;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.stardust.view.accessibility.AccessibilityNodeInfoHelper;
+
+/**
+ * Created by Stardust on 2017/3/9.
+ */
+
+public class BoundsFilter extends DfsFilter {
+
+ public static final int TYPE_EQUALS = 0;
+ public static final int TYPE_INSIDE = 1;
+ public static final int TYPE_PARENT = 2;
+
+ private Rect mBounds;
+ private int mType;
+
+ public BoundsFilter(Rect bounds, int type) {
+ mBounds = bounds;
+ mType = type;
+ }
+
+ @Override
+ protected boolean isIncluded(AccessibilityNodeInfo nodeInfo) {
+ if (mType == TYPE_PARENT) {
+ return AccessibilityNodeInfoHelper.getBoundsInParent(nodeInfo).equals(mBounds);
+ }
+ Rect boundsInScreen = AccessibilityNodeInfoHelper.getBoundsInScreen(nodeInfo);
+ if (mType == TYPE_EQUALS)
+ return boundsInScreen.equals(mBounds);
+ return mBounds.contains(boundsInScreen);
+ }
+}
diff --git a/automator/src/main/java/com/stardust/automator/filter/ClassNameFilter.java b/automator/src/main/java/com/stardust/automator/filter/ClassNameFilter.java
new file mode 100644
index 000000000..a87902385
--- /dev/null
+++ b/automator/src/main/java/com/stardust/automator/filter/ClassNameFilter.java
@@ -0,0 +1,45 @@
+package com.stardust.automator.filter;
+
+import android.view.accessibility.AccessibilityNodeInfo;
+
+/**
+ * Created by Stardust on 2017/3/9.
+ */
+
+public class ClassNameFilter {
+
+ private static final KeyGetter CLASS_NAME_GETTER = new KeyGetter() {
+ @Override
+ public String getKey(AccessibilityNodeInfo nodeInfo) {
+ CharSequence charSequence = nodeInfo.getClassName();
+ return charSequence == null ? null : charSequence.toString();
+ }
+ };
+
+ public static ListFilter equals(String text) {
+ if (!text.contains(".")) {
+ text = "android.widget." + text;
+ }
+ return new EqualsFilter(text, CLASS_NAME_GETTER);
+ }
+
+ public static ListFilter contains(String str) {
+ return new ContainsFilter(str, CLASS_NAME_GETTER);
+ }
+
+ public static ListFilter startsWith(String prefix) {
+ return new StartsWithFilter(prefix, CLASS_NAME_GETTER);
+ }
+
+ public static ListFilter endsWith(String suffix) {
+ return new EndsWithFilter(suffix, CLASS_NAME_GETTER);
+ }
+
+ public static ListFilter matches(String regex) {
+ return new MatchesFilter(regex, CLASS_NAME_GETTER);
+ }
+
+ private ClassNameFilter() {
+
+ }
+}
diff --git a/automator/src/main/java/com/stardust/automator/filter/ContainsFilter.java b/automator/src/main/java/com/stardust/automator/filter/ContainsFilter.java
new file mode 100644
index 000000000..e0869bc8e
--- /dev/null
+++ b/automator/src/main/java/com/stardust/automator/filter/ContainsFilter.java
@@ -0,0 +1,24 @@
+package com.stardust.automator.filter;
+
+import android.view.accessibility.AccessibilityNodeInfo;
+
+/**
+ * Created by Stardust on 2017/3/9.
+ */
+
+public class ContainsFilter extends DfsFilter {
+ private final KeyGetter mKeyGetter;
+ private final String mContains;
+
+ ContainsFilter(String contains, KeyGetter keyGetter) {
+ mContains = contains;
+ mKeyGetter = keyGetter;
+ }
+
+ @Override
+ protected boolean isIncluded(AccessibilityNodeInfo nodeInfo) {
+ String key = mKeyGetter.getKey(nodeInfo);
+ return key != null && key.contains(mContains);
+ }
+
+}
diff --git a/automator/src/main/java/com/stardust/automator/filter/DescFilter.java b/automator/src/main/java/com/stardust/automator/filter/DescFilter.java
new file mode 100644
index 000000000..8bf75637d
--- /dev/null
+++ b/automator/src/main/java/com/stardust/automator/filter/DescFilter.java
@@ -0,0 +1,41 @@
+package com.stardust.automator.filter;
+
+import android.view.accessibility.AccessibilityNodeInfo;
+
+/**
+ * Created by Stardust on 2017/3/9.
+ */
+
+public class DescFilter {
+
+ private static final KeyGetter DESC_GETTER = new KeyGetter() {
+ @Override
+ public String getKey(AccessibilityNodeInfo nodeInfo) {
+ CharSequence charSequence = nodeInfo.getContentDescription();
+ return charSequence == null ? null : charSequence.toString();
+ }
+ };
+
+ public static ListFilter equals(String text) {
+ return new EqualsFilter(text, DESC_GETTER);
+ }
+
+ public static ListFilter contains(String str) {
+ return new ContainsFilter(str, DESC_GETTER);
+ }
+
+ public static ListFilter startsWith(String prefix) {
+ return new StartsWithFilter(prefix, DESC_GETTER);
+ }
+
+ public static ListFilter endsWith(String suffix) {
+ return new EndsWithFilter(suffix, DESC_GETTER);
+ }
+ public static ListFilter matches(String regex) {
+ return new MatchesFilter(regex, DESC_GETTER);
+ }
+
+ private DescFilter(){
+
+ }
+}
diff --git a/automator/src/main/java/com/stardust/automator/filter/DfsFilter.java b/automator/src/main/java/com/stardust/automator/filter/DfsFilter.java
new file mode 100644
index 000000000..2061f1214
--- /dev/null
+++ b/automator/src/main/java/com/stardust/automator/filter/DfsFilter.java
@@ -0,0 +1,52 @@
+package com.stardust.automator.filter;
+
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by Stardust on 2017/3/9.
+ */
+
+public abstract class DfsFilter implements ListFilter, Filter {
+
+ @Override
+ public List filter(List nodes) {
+ ArrayList list = new ArrayList<>();
+ for (AccessibilityNodeInfo node : nodes) {
+ if (isIncluded(node)) {
+ list.add(node);
+ }
+ filterChildren(node, list);
+ }
+ return list;
+ }
+
+ public List filter(AccessibilityNodeInfo node) {
+ ArrayList list = new ArrayList<>();
+ if (isIncluded(node)) {
+ list.add(node);
+ }
+ filterChildren(node, list);
+ return list;
+ }
+
+ private void filterChildren(AccessibilityNodeInfo parent, List list) {
+ for (int i = 0; i < parent.getChildCount(); i++) {
+ AccessibilityNodeInfo child = parent.getChild(i);
+ if (child == null)
+ continue;
+ boolean included = isIncluded(child);
+ if (included) {
+ list.add(child);
+ }
+ filterChildren(child, list);
+ if (!included) {
+ child.recycle();
+ }
+ }
+ }
+
+ protected abstract boolean isIncluded(AccessibilityNodeInfo nodeInfo);
+}
diff --git a/automator/src/main/java/com/stardust/automator/filter/EndsWithFilter.java b/automator/src/main/java/com/stardust/automator/filter/EndsWithFilter.java
new file mode 100644
index 000000000..a92e3e2a9
--- /dev/null
+++ b/automator/src/main/java/com/stardust/automator/filter/EndsWithFilter.java
@@ -0,0 +1,25 @@
+package com.stardust.automator.filter;
+
+import android.view.accessibility.AccessibilityNodeInfo;
+
+/**
+ * Created by Stardust on 2017/3/9.
+ */
+
+public class EndsWithFilter extends DfsFilter {
+
+ private final String mSuffix;
+ private final KeyGetter mKeyGetter;
+
+ public EndsWithFilter(String suffix, KeyGetter keyGetter) {
+ mSuffix = suffix;
+ mKeyGetter = keyGetter;
+ }
+
+ @Override
+ protected boolean isIncluded(AccessibilityNodeInfo nodeInfo) {
+ String key = mKeyGetter.getKey(nodeInfo);
+ return key != null && key.endsWith(mSuffix);
+ }
+
+}
diff --git a/automator/src/main/java/com/stardust/automator/filter/EqualsFilter.java b/automator/src/main/java/com/stardust/automator/filter/EqualsFilter.java
new file mode 100644
index 000000000..1814063a3
--- /dev/null
+++ b/automator/src/main/java/com/stardust/automator/filter/EqualsFilter.java
@@ -0,0 +1,27 @@
+package com.stardust.automator.filter;
+
+import android.view.accessibility.AccessibilityNodeInfo;
+
+/**
+ * Created by Stardust on 2017/3/9.
+ */
+
+public class EqualsFilter extends DfsFilter {
+
+ private String mText;
+ private KeyGetter mKeyGetter;
+
+ public EqualsFilter(String text, KeyGetter getter) {
+ mText = text;
+ mKeyGetter = getter;
+ }
+
+ @Override
+ protected boolean isIncluded(AccessibilityNodeInfo nodeInfo) {
+ String key = mKeyGetter.getKey(nodeInfo);
+ if(key != null){
+ return key.equals(mText);
+ }
+ return false;
+ }
+}
diff --git a/automator/src/main/java/com/stardust/automator/filter/Filter.java b/automator/src/main/java/com/stardust/automator/filter/Filter.java
new file mode 100644
index 000000000..d9b1b780d
--- /dev/null
+++ b/automator/src/main/java/com/stardust/automator/filter/Filter.java
@@ -0,0 +1,15 @@
+package com.stardust.automator.filter;
+
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import java.util.List;
+
+/**
+ * Created by Stardust on 2017/3/9.
+ */
+
+public interface Filter {
+
+ List filter(AccessibilityNodeInfo node);
+
+}
diff --git a/automator/src/main/java/com/stardust/automator/filter/IdFilter.java b/automator/src/main/java/com/stardust/automator/filter/IdFilter.java
new file mode 100644
index 000000000..48340af45
--- /dev/null
+++ b/automator/src/main/java/com/stardust/automator/filter/IdFilter.java
@@ -0,0 +1,53 @@
+package com.stardust.automator.filter;
+
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import java.util.List;
+
+/**
+ * Created by Stardust on 2017/3/9.
+ */
+
+public class IdFilter extends ListFilter.Default {
+
+ private static final KeyGetter ID_GETTER = new KeyGetter() {
+
+ @Override
+ public String getKey(AccessibilityNodeInfo nodeInfo) {
+ return nodeInfo.getViewIdResourceName();
+ }
+ };
+
+ public static IdFilter equals(String id) {
+ return new IdFilter(id);
+ }
+
+ public static StartsWithFilter startsWith(String prefix) {
+ return new StartsWithFilter(prefix, ID_GETTER);
+ }
+
+ public static ListFilter endsWith(String suffix) {
+ return new EndsWithFilter(suffix, ID_GETTER);
+ }
+
+ public static ContainsFilter contains(String contains) {
+ return new ContainsFilter(contains, ID_GETTER);
+ }
+
+ public static MatchesFilter matches(String regex) {
+ return new MatchesFilter(regex, ID_GETTER);
+ }
+
+ private String mId;
+
+ private IdFilter(String id) {
+ mId = id;
+ }
+
+ @Override
+ public List filter(AccessibilityNodeInfo node) {
+ return node.findAccessibilityNodeInfosByViewId(mId);
+ }
+
+
+}
diff --git a/automator/src/main/java/com/stardust/automator/filter/KeyGetter.java b/automator/src/main/java/com/stardust/automator/filter/KeyGetter.java
new file mode 100644
index 000000000..ba31c6f26
--- /dev/null
+++ b/automator/src/main/java/com/stardust/automator/filter/KeyGetter.java
@@ -0,0 +1,12 @@
+package com.stardust.automator.filter;
+
+import android.view.accessibility.AccessibilityNodeInfo;
+
+/**
+ * Created by Stardust on 2017/3/9.
+ */
+
+public interface KeyGetter {
+
+ String getKey(AccessibilityNodeInfo nodeInfo);
+}
diff --git a/automator/src/main/java/com/stardust/automator/filter/ListFilter.java b/automator/src/main/java/com/stardust/automator/filter/ListFilter.java
new file mode 100644
index 000000000..00e6f7741
--- /dev/null
+++ b/automator/src/main/java/com/stardust/automator/filter/ListFilter.java
@@ -0,0 +1,28 @@
+package com.stardust.automator.filter;
+
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by Stardust on 2017/3/9.
+ */
+
+public interface ListFilter {
+
+ List filter(List nodes);
+
+ abstract class Default implements Filter, ListFilter {
+
+ @Override
+ public List filter(List nodes) {
+ List list = new ArrayList<>();
+ for (AccessibilityNodeInfo node : nodes) {
+ list.addAll(filter(node));
+ }
+ return list;
+ }
+ }
+
+}
diff --git a/automator/src/main/java/com/stardust/automator/filter/MatchesFilter.java b/automator/src/main/java/com/stardust/automator/filter/MatchesFilter.java
new file mode 100644
index 000000000..1bf775019
--- /dev/null
+++ b/automator/src/main/java/com/stardust/automator/filter/MatchesFilter.java
@@ -0,0 +1,25 @@
+package com.stardust.automator.filter;
+
+import android.view.accessibility.AccessibilityNodeInfo;
+
+/**
+ * Created by Stardust on 2017/3/9.
+ */
+
+public class MatchesFilter extends DfsFilter {
+
+ private final String mRegex;
+ private final KeyGetter mKeyGetter;
+
+ MatchesFilter(String regex, KeyGetter keyGetter) {
+ mRegex = regex;
+ mKeyGetter = keyGetter;
+ }
+
+ @Override
+ protected boolean isIncluded(AccessibilityNodeInfo nodeInfo) {
+ String key = mKeyGetter.getKey(nodeInfo);
+ return key != null && key.matches(mRegex);
+ }
+
+}
diff --git a/automator/src/main/java/com/stardust/automator/filter/PackageNameFilter.java b/automator/src/main/java/com/stardust/automator/filter/PackageNameFilter.java
new file mode 100644
index 000000000..304642322
--- /dev/null
+++ b/automator/src/main/java/com/stardust/automator/filter/PackageNameFilter.java
@@ -0,0 +1,42 @@
+package com.stardust.automator.filter;
+
+import android.view.accessibility.AccessibilityNodeInfo;
+
+/**
+ * Created by Stardust on 2017/3/9.
+ */
+
+public class PackageNameFilter {
+
+ private static final KeyGetter PACKAGE_NAME_GETTER = new KeyGetter() {
+ @Override
+ public String getKey(AccessibilityNodeInfo nodeInfo) {
+ CharSequence charSequence = nodeInfo.getPackageName();
+ return charSequence == null ? null : charSequence.toString();
+ }
+ };
+
+ public static ListFilter equals(String text) {
+ return new EqualsFilter(text, PACKAGE_NAME_GETTER);
+ }
+
+ public static ListFilter contains(String str) {
+ return new ContainsFilter(str, PACKAGE_NAME_GETTER);
+ }
+
+ public static ListFilter startsWith(String prefix) {
+ return new StartsWithFilter(prefix, PACKAGE_NAME_GETTER);
+ }
+
+ public static ListFilter endsWith(String suffix) {
+ return new EndsWithFilter(suffix, PACKAGE_NAME_GETTER);
+ }
+
+ public static ListFilter matches(String regex) {
+ return new MatchesFilter(regex, PACKAGE_NAME_GETTER);
+ }
+
+ private PackageNameFilter() {
+
+ }
+}
diff --git a/automator/src/main/java/com/stardust/automator/filter/StartsWithFilter.java b/automator/src/main/java/com/stardust/automator/filter/StartsWithFilter.java
new file mode 100644
index 000000000..565ad0108
--- /dev/null
+++ b/automator/src/main/java/com/stardust/automator/filter/StartsWithFilter.java
@@ -0,0 +1,25 @@
+package com.stardust.automator.filter;
+
+import android.view.accessibility.AccessibilityNodeInfo;
+
+/**
+ * Created by Stardust on 2017/3/9.
+ */
+
+public class StartsWithFilter extends DfsFilter {
+
+ private final String mPrefix;
+ private final KeyGetter mKeyGetter;
+
+ public StartsWithFilter(String prefix, KeyGetter keyGetter) {
+ mPrefix = prefix;
+ mKeyGetter = keyGetter;
+ }
+
+ @Override
+ protected boolean isIncluded(AccessibilityNodeInfo nodeInfo) {
+ String key = mKeyGetter.getKey(nodeInfo);
+ return key != null && key.startsWith(mPrefix);
+ }
+
+}
diff --git a/automator/src/main/java/com/stardust/automator/filter/TextFilter.java b/automator/src/main/java/com/stardust/automator/filter/TextFilter.java
new file mode 100644
index 000000000..10b691fbc
--- /dev/null
+++ b/automator/src/main/java/com/stardust/automator/filter/TextFilter.java
@@ -0,0 +1,52 @@
+package com.stardust.automator.filter;
+
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import java.util.List;
+
+/**
+ * Created by Stardust on 2017/3/9.
+ */
+
+public class TextFilter extends ListFilter.Default {
+
+ private static final KeyGetter TEXT_GETTER = new KeyGetter() {
+ @Override
+ public String getKey(AccessibilityNodeInfo nodeInfo) {
+ CharSequence charSequence = nodeInfo.getText();
+ return charSequence == null ? null : charSequence.toString();
+ }
+ };
+
+ public static ListFilter equals(String text) {
+ return new EqualsFilter(text, TEXT_GETTER);
+ }
+
+ public static ListFilter contains(String str) {
+ return new TextFilter(str);
+ }
+
+ public static ListFilter startsWith(String prefix) {
+ return new StartsWithFilter(prefix, TEXT_GETTER);
+ }
+
+ public static ListFilter endsWith(String suffix) {
+ return new EndsWithFilter(suffix, TEXT_GETTER);
+ }
+
+ public static ListFilter matches(String regex) {
+ return new MatchesFilter(regex, TEXT_GETTER);
+ }
+
+ private String mText;
+
+ private TextFilter(String text) {
+ mText = text;
+ }
+
+
+ @Override
+ public List filter(AccessibilityNodeInfo node) {
+ return node.findAccessibilityNodeInfosByText(mText);
+ }
+}
diff --git a/automator/src/main/java/com/stardust/util/Consumer.java b/automator/src/main/java/com/stardust/util/Consumer.java
new file mode 100644
index 000000000..4ab22f404
--- /dev/null
+++ b/automator/src/main/java/com/stardust/util/Consumer.java
@@ -0,0 +1,11 @@
+package com.stardust.util;
+
+/**
+ * Created by Stardust on 2017/3/10.
+ */
+
+public interface Consumer {
+
+ void accept(T t);
+
+}
diff --git a/app/src/main/java/com/stardust/util/SparseArrayEntries.java b/automator/src/main/java/com/stardust/util/SparseArrayEntries.java
similarity index 100%
rename from app/src/main/java/com/stardust/util/SparseArrayEntries.java
rename to automator/src/main/java/com/stardust/util/SparseArrayEntries.java
diff --git a/app/src/main/java/com/stardust/scriptdroid/service/AccessibilityDelegate.java b/automator/src/main/java/com/stardust/view/accessibility/AccessibilityDelegate.java
similarity index 87%
rename from app/src/main/java/com/stardust/scriptdroid/service/AccessibilityDelegate.java
rename to automator/src/main/java/com/stardust/view/accessibility/AccessibilityDelegate.java
index cc26462f0..d3bb7f231 100644
--- a/app/src/main/java/com/stardust/scriptdroid/service/AccessibilityDelegate.java
+++ b/automator/src/main/java/com/stardust/view/accessibility/AccessibilityDelegate.java
@@ -1,4 +1,4 @@
-package com.stardust.scriptdroid.service;
+package com.stardust.view.accessibility;
import android.accessibilityservice.AccessibilityService;
import android.view.accessibility.AccessibilityEvent;
diff --git a/app/src/main/java/com/stardust/view/accessibility/AccessibilityNodeInfoHelper.java b/automator/src/main/java/com/stardust/view/accessibility/AccessibilityNodeInfoHelper.java
similarity index 74%
rename from app/src/main/java/com/stardust/view/accessibility/AccessibilityNodeInfoHelper.java
rename to automator/src/main/java/com/stardust/view/accessibility/AccessibilityNodeInfoHelper.java
index 98e4af41c..970da5e39 100644
--- a/app/src/main/java/com/stardust/view/accessibility/AccessibilityNodeInfoHelper.java
+++ b/automator/src/main/java/com/stardust/view/accessibility/AccessibilityNodeInfoHelper.java
@@ -30,8 +30,19 @@ public static Rect getVisibleBoundsInScreen(AccessibilityNodeInfo node, int widt
displayRect.left = 0;
displayRect.right = width;
displayRect.bottom = height;
-
boolean intersect = nodeRect.intersect(displayRect);
return nodeRect;
}
+
+ public static Rect getBoundsInParent(AccessibilityNodeInfo nodeInfo) {
+ Rect rect = new Rect();
+ nodeInfo.getBoundsInParent(rect);
+ return rect;
+ }
+
+ public static Rect getBoundsInScreen(AccessibilityNodeInfo nodeInfo) {
+ Rect rect = new Rect();
+ nodeInfo.getBoundsInScreen(rect);
+ return rect;
+ }
}
diff --git a/app/src/main/java/com/stardust/view/accessibility/AccessibilityServiceUtils.java b/automator/src/main/java/com/stardust/view/accessibility/AccessibilityServiceUtils.java
similarity index 54%
rename from app/src/main/java/com/stardust/view/accessibility/AccessibilityServiceUtils.java
rename to automator/src/main/java/com/stardust/view/accessibility/AccessibilityServiceUtils.java
index a1c9cbaac..ac5422817 100644
--- a/app/src/main/java/com/stardust/view/accessibility/AccessibilityServiceUtils.java
+++ b/automator/src/main/java/com/stardust/view/accessibility/AccessibilityServiceUtils.java
@@ -6,13 +6,7 @@
import android.content.Intent;
import android.provider.Settings;
import android.text.TextUtils;
-import android.widget.Toast;
-import com.stardust.scriptdroid.App;
-import com.stardust.scriptdroid.Pref;
-import com.stardust.scriptdroid.R;
-import com.stardust.scriptdroid.service.AccessibilityWatchDogService;
-import com.stardust.scriptdroid.tool.Shell;
/**
* Created by Stardust on 2017/1/26.
@@ -21,9 +15,6 @@
public class AccessibilityServiceUtils {
public static void goToAccessibilitySetting(Context context) {
- if (Pref.isFirstGoToAccessibilitySetting()) {
- Toast.makeText(context, context.getString(R.string.text_please_choose) + context.getString(R.string._app_name), Toast.LENGTH_LONG).show();
- }
context.startActivity(new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
}
@@ -48,22 +39,5 @@ public static boolean isAccessibilityServiceEnabled(Context context, Class ext
return false;
}
- public static boolean enableAccessibilityServiceByRootAndWaitFor(Context context, Class extends AccessibilityService> accessibilityService, long timeout) {
- Shell.execCommand("settings put secure enabled_accessibility_services %accessibility:"
- + context.getPackageName() + "/" + accessibilityService.getName(), true);
- long millis = System.currentTimeMillis();
- while (true) {
- if (isAccessibilityServiceEnabled(context, accessibilityService))
- return true;
- try {
- Thread.sleep(300);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- if (System.currentTimeMillis() - millis >= timeout) {
- return false;
- }
- }
- }
}
diff --git a/automator/src/main/res/values/strings.xml b/automator/src/main/res/values/strings.xml
new file mode 100644
index 000000000..854200555
--- /dev/null
+++ b/automator/src/main/res/values/strings.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/automator/src/test/java/com/stardust/automator/ExampleUnitTest.java b/automator/src/test/java/com/stardust/automator/ExampleUnitTest.java
new file mode 100644
index 000000000..9b67e597f
--- /dev/null
+++ b/automator/src/test/java/com/stardust/automator/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package com.stardust.automator;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() throws Exception {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/gradlew b/gradlew
index 9d82f7891..824fce23b 100644
--- a/gradlew
+++ b/gradlew
@@ -102,7 +102,7 @@ fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
- GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:key=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
diff --git a/settings.gradle b/settings.gradle
index e7b4def49..2e4e77f9e 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1 @@
-include ':app'
+include ':app', ':automator'