Add: floating window, layout inspector
This commit is contained in:
442
app/src/main/java/com/stardust/view/Floaty.java
Normal file
442
app/src/main/java/com/stardust/view/Floaty.java
Normal file
@@ -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.
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
410
app/src/main/java/com/stardust/view/ResizableFloaty.java
Normal file
410
app/src/main/java/com/stardust/view/ResizableFloaty.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
54
app/src/main/java/com/stardust/view/ViewStack.java
Normal file
54
app/src/main/java/com/stardust/view/ViewStack.java
Normal file
@@ -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<View> 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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 <code>dumpFile</code>
|
||||
*
|
||||
* @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();
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package com.stardust.view.accessibility;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
|
||||
/**
|
||||
* Created by Stardust on 2017/3/6.
|
||||
*/
|
||||
|
||||
public class AccessibilityNodeInfoHelper {
|
||||
|
||||
/**
|
||||
* Returns the node's bounds clipped to the size of the display
|
||||
*
|
||||
* @param node
|
||||
* @param width pixel width of the display
|
||||
* @param height pixel height of the display
|
||||
* @return null if node is null, else a Rect containing visible bounds
|
||||
*/
|
||||
public static Rect getVisibleBoundsInScreen(AccessibilityNodeInfo node, int width, int height) {
|
||||
if (node == null) {
|
||||
return null;
|
||||
}
|
||||
// targeted node's bounds
|
||||
Rect nodeRect = new Rect();
|
||||
node.getBoundsInScreen(nodeRect);
|
||||
|
||||
Rect displayRect = new Rect();
|
||||
displayRect.top = 0;
|
||||
displayRect.left = 0;
|
||||
displayRect.right = width;
|
||||
displayRect.bottom = height;
|
||||
|
||||
boolean intersect = nodeRect.intersect(displayRect);
|
||||
return nodeRect;
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package com.stardust.view.accessibility;
|
||||
|
||||
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.tool.Shell;
|
||||
|
||||
/**
|
||||
* Created by Stardust on 2017/1/26.
|
||||
*/
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
public static boolean isAccessibilityServiceEnabled(Context context, Class<? extends AccessibilityService> accessibilityService) {
|
||||
ComponentName expectedComponentName = new ComponentName(context, accessibilityService);
|
||||
|
||||
String enabledServicesSetting = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
|
||||
if (enabledServicesSetting == null)
|
||||
return false;
|
||||
|
||||
TextUtils.SimpleStringSplitter colonSplitter = new TextUtils.SimpleStringSplitter(':');
|
||||
colonSplitter.setString(enabledServicesSetting);
|
||||
|
||||
while (colonSplitter.hasNext()) {
|
||||
String componentNameString = colonSplitter.next();
|
||||
ComponentName enabledService = ComponentName.unflattenFromString(componentNameString);
|
||||
|
||||
if (enabledService != null && enabledService.equals(expectedComponentName))
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user