package tk.wasdennnoch.androidn_ify.systemui.recents.navigate; import android.app.ActivityManager; import android.content.Context; import android.os.Build; import android.os.SystemClock; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.animation.Animation; import android.view.animation.LinearInterpolator; import android.view.animation.ScaleAnimation; import android.widget.FrameLayout; import java.util.List; import de.robv.android.xposed.XC_MethodHook; import de.robv.android.xposed.XC_MethodReplacement; import de.robv.android.xposed.XposedHelpers; import de.robv.android.xposed.callbacks.XC_InitPackageResources; import de.robv.android.xposed.callbacks.XC_LayoutInflated; import tk.wasdennnoch.androidn_ify.R; import tk.wasdennnoch.androidn_ify.XposedHook; import tk.wasdennnoch.androidn_ify.utils.ConfigUtils; import tk.wasdennnoch.androidn_ify.utils.ResourceUtils; @SuppressWarnings({"SameParameterValue", "WeakerAccess"}) public class RecentsNavigation { private static final String PACKAGE_SYSTEMUI = XposedHook.PACKAGE_SYSTEMUI; //private static final String CLASS_PHONE_STATUS_BAR = "com.android.systemui.statusbar.phone.PhoneStatusBar"; private static final String TAG = "RecentsNavigation"; private static long mStartRecentsActivityTime = 0; private static Object mRecentsActivity; private static ConfigUtils mConfig; private static ClassLoader mClassLoader; private static boolean mIsNavigating = false; private static int mCurrentIndex = 0; private static TaskProgress mCurrentProgress = null; private static boolean mBackPressed = false; private static boolean mSkipFirstApp = false; private static final XC_MethodHook startRecentsActivityHook = new XC_MethodHook() { @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { mStartRecentsActivityTime = SystemClock.elapsedRealtime(); mIsNavigating = false; if (mConfig.recents.double_tap) { XposedHelpers.setLongField(param.thisObject, "mLastToggleTime", 0); } } }; private static final XC_MethodHook recentsActivityOnStartHook = new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { mRecentsActivity = param.thisObject; } }; private static boolean mResetScroll; public static boolean isDoubleTap() { return (mConfig.recents.force_double_tap || (mConfig.recents.double_tap && ((SystemClock.elapsedRealtime() - mStartRecentsActivityTime) < mConfig.recents.double_tap_speed))); } private static final XC_MethodHook onBackPressedHook = new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { mIsNavigating = false; mBackPressed = true; } }; private static final XC_MethodHook resetNavigatingStatus = new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { mIsNavigating = false; } }; private static final XC_MethodHook dismissRecentsToFocusedTaskOrHomeHook = new XC_MethodReplacement() { @Override protected Object replaceHookedMethod(MethodHookParam param) throws Throwable { return dismissRecentsToFocusedTaskOrHome((boolean) param.args[0]); } }; public static boolean dismissRecentsToFocusedTaskOrHome(boolean checkFilteredStackState) { Object ssp = getSystemServicesProxy(); Object mRecentsView = XposedHelpers.getObjectField(mRecentsActivity, "mRecentsView"); if (isRecentsTopMost(ssp, getTopMostTask(ssp))) { // If we currently have filtered stacks, then unfilter those first if (checkFilteredStackState && (boolean) XposedHelpers.callMethod(mRecentsView, "unfilterFilteredStacks")) return true; // If we have a focused Task, launch that Task now if (launchFocusedTask()) return true; // If we launched from Home, then return to Home if (XposedHelpers.getBooleanField(XposedHelpers.getObjectField(mRecentsActivity, "mConfig"), "launchedFromHome")) { dismissRecentsToHomeRaw(true); return true; } // Otherwise, try and return to the Task that Recents was launched from if ((boolean) XposedHelpers.callMethod(mRecentsView, "launchPreviousTask")) return true; // If none of the other cases apply, then just go Home dismissRecentsToHomeRaw(true); return true; } return false; } @SuppressWarnings("unchecked") public static boolean launchFocusedTask() { XposedHook.logD(TAG, "launchFocusedTask"); boolean isDoubleTap = isDoubleTap(); boolean navigateRecents = mConfig.recents.navigate_recents; boolean launchedFromHome = XposedHelpers.getBooleanField(XposedHelpers.getObjectField(mRecentsActivity, "mConfig"), "launchedFromHome"); int doubleTapLaunchIndexBackward = (launchedFromHome) ? 1 : 2; Object mRecentsView = XposedHelpers.getObjectField(mRecentsActivity, "mRecentsView"); // Get the first stack view List<Object> stackViews = (List<Object>) XposedHelpers.callMethod(mRecentsView, "getTaskStackViews"); int stackCount = stackViews.size(); for (int i = 0; i < stackCount; i++) { Object stackView = stackViews.get(i); Object stack = XposedHelpers.callMethod(stackView, "getStack"); // Iterate the stack views and try and find the focused task List<Object> taskViews = (List<Object>) XposedHelpers.callMethod(stackView, "getTaskViews"); List<Object> tasks = (List<Object>) XposedHelpers.callMethod(stack, "getTasks"); int taskViewCount = taskViews.size(); int taskCount = tasks.size(); if (!mIsNavigating && !mBackPressed) { mCurrentIndex = taskCount; mIsNavigating = true; mResetScroll = true; mSkipFirstApp = !launchedFromHome; } if (!mBackPressed && isDoubleTap && taskViewCount > (doubleTapLaunchIndexBackward - 1)) { Object tv = taskViews.get(taskViewCount - doubleTapLaunchIndexBackward); Object task = XposedHelpers.callMethod(tv, "getTask"); XposedHelpers.callMethod(mRecentsView, "onTaskViewClicked", stackView, tv, stack, task, false); return true; } else { if (!mBackPressed && navigateRecents) { return navigateRecents(tasks, taskViews, stackView, stack); } else { mBackPressed = false; for (int j = 0; j < taskViewCount; j++) { Object tv = taskViews.get(j); Object task = XposedHelpers.callMethod(tv, "getTask"); if ((boolean) XposedHelpers.callMethod(tv, "isFocusedTask")) { XposedHelpers.callMethod(mRecentsView, "onTaskViewClicked", stackView, tv, stack, task, false); return true; } } } } } return false; } private static boolean navigateRecents(List<Object> tasks, List<Object> taskViews, Object stackView, Object stack) { int taskCount = tasks.size(); int taskViewCount = taskViews.size(); if (taskCount < 1) return false; if (mCurrentIndex == 0) { mCurrentIndex = tasks.size() - 1; mResetScroll = true; } else { mCurrentIndex--; } if (taskCount <= mCurrentIndex) mCurrentIndex = tasks.size() - 1; if (mCurrentProgress != null) { mCurrentProgress.stop(); mCurrentProgress = null; } Object anchorTask = null; for (int i = 0; i < taskViewCount; i++) { Object tv = taskViews.get(i); Object task = XposedHelpers.callMethod(tv, "getTask"); if (task == tasks.get(mCurrentIndex)) { XposedHook.logD(TAG, "task found"); Object mStackScroller = XposedHelpers.getObjectField(stackView, "mStackScroller"); if (mResetScroll) { XposedHelpers.callMethod(mStackScroller, "setStackScroll", 10f); mResetScroll = false; } if (anchorTask == null) anchorTask = XposedHelpers.callMethod(XposedHelpers.getObjectField(stackView, "mStack"), "getFrontMostTask"); Object mLayoutAlgorithm = XposedHelpers.getObjectField(stackView, "mLayoutAlgorithm"); float anchorTaskScroll = (float) XposedHelpers.callMethod(mLayoutAlgorithm, "getStackScrollForTask", anchorTask); float curScroll = (float) XposedHelpers.callMethod(mStackScroller, "getStackScroll"); float newScroll = (float) XposedHelpers.callMethod(mLayoutAlgorithm, "getStackScrollForTask", task); XposedHelpers.callMethod(mStackScroller, "setStackScroll", curScroll - Math.abs(newScroll - anchorTaskScroll)); XposedHelpers.callMethod(mStackScroller, "boundScroll"); XposedHelpers.callMethod(stackView, "requestSynchronizeStackViewsWithModel", 200); FrameLayout taskView = (FrameLayout) taskViews.get(i); mCurrentProgress = new TaskProgress(taskView, stackView, stack, task); mCurrentProgress.start(); if (mSkipFirstApp) { mSkipFirstApp = false; navigateRecents(tasks, taskViews, stackView, stack); } return true; } anchorTask = task; } return true; } private static class TaskProgress implements Animation.AnimationListener { private final FrameLayout mTaskView; private final FrameLayout mTaskViewHeader; private final View mProgressView; private ScaleAnimation mScaleAnim; private final Object mStackView; private final Object mStack; private final Object mTask; public TaskProgress(FrameLayout taskView, Object stackView, Object stack, Object task) { mTaskView = taskView; mTaskViewHeader = (FrameLayout) XposedHelpers.getObjectField(mTaskView, "mHeaderView"); mProgressView = mTaskViewHeader.findViewById(R.id.task_progress); mStackView = stackView; mStack = stack; mTask = task; } public void start() { mScaleAnim = new ScaleAnimation(1, 0, 1, 1); mScaleAnim.setDuration(mConfig.recents.navigation_delay); mScaleAnim.setInterpolator(new LinearInterpolator()); mScaleAnim.setRepeatCount(0); mScaleAnim.setAnimationListener(this); mProgressView.setVisibility(View.VISIBLE); mProgressView.startAnimation(mScaleAnim); } public void stop() { if (mScaleAnim != null) { mScaleAnim.setAnimationListener(null); } mProgressView.clearAnimation(); mProgressView.setVisibility(View.GONE); } @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { mProgressView.setVisibility(View.GONE); mScaleAnim.setAnimationListener(null); if (!mIsNavigating) return; mIsNavigating = false; Object mRecentsView = XposedHelpers.getObjectField(mRecentsActivity, "mRecentsView"); XposedHelpers.callMethod(mRecentsView, "onTaskViewClicked", mStackView, mTaskView, mStack, mTask, false); } @Override public void onAnimationRepeat(Animation animation) { } } public static boolean isRecentsTopMost(Object ssp, Object topMostTask) { return (boolean) XposedHelpers.callMethod(ssp, "isRecentsTopMost", topMostTask, null); } public static Object getTopMostTask(Object ssp) { return XposedHelpers.callMethod(ssp, "getTopMostTask"); } public static void dismissRecentsToHomeRaw(boolean animated) { XposedHelpers.callMethod(mRecentsActivity, "dismissRecentsToHomeRaw", animated); } public static void hookSystemUI(ClassLoader classLoader) { try { if (Build.VERSION.SDK_INT < 23 || ConfigUtils.recents().alternative_method) return; mConfig = ConfigUtils.getInstance(); mClassLoader = classLoader; if (mConfig.recents.double_tap || mConfig.recents.navigate_recents) { Class<?> classRecents = XposedHelpers.findClass("com.android.systemui.recents.Recents", classLoader); Class<?> classRecentsActivity = XposedHelpers.findClass("com.android.systemui.recents.RecentsActivity", classLoader); XposedHelpers.findAndHookMethod(classRecents, "startRecentsActivity", ActivityManager.RunningTaskInfo.class, boolean.class, startRecentsActivityHook); XposedHelpers.findAndHookMethod(classRecentsActivity, "onStart", recentsActivityOnStartHook); XposedHelpers.findAndHookMethod(classRecentsActivity, "onBackPressed", onBackPressedHook); XposedHelpers.findAndHookMethod(classRecentsActivity, "dismissRecentsToHomeRaw", boolean.class, resetNavigatingStatus); XposedHelpers.findAndHookMethod(classRecentsActivity, "dismissRecentsToFocusedTaskOrHome", boolean.class, dismissRecentsToFocusedTaskOrHomeHook); } /*try { XposedHelpers.findAndHookMethod(CLASS_PHONE_STATUS_BAR, classLoader, "prepareNavigationBarView", prepareNavigationBarViewHook); } catch (NoSuchMethodError e) { // CM takes a boolean parameter XposedHelpers.findAndHookMethod(CLASS_PHONE_STATUS_BAR, classLoader, "prepareNavigationBarView", boolean.class, prepareNavigationBarViewHook); }*/ } catch (Throwable t) { XposedHook.logE(TAG, "Error hooking SystemUI", t); } } private static final XC_LayoutInflated recents_task_view_header = new XC_LayoutInflated() { @Override public void handleLayoutInflated(LayoutInflatedParam liparam) throws Throwable { FrameLayout header = (FrameLayout) liparam.view; Context context = header.getContext(); ResourceUtils res = ResourceUtils.getInstance(context); int progressHeight = res.getDimensionPixelSize(R.dimen.task_progress_height); FrameLayout.LayoutParams progressLp = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, progressHeight); progressLp.gravity = Gravity.BOTTOM; View progress = new View(context); progress.setId(R.id.task_progress); progress.setLayoutParams(progressLp); progress.setBackgroundColor(0xA0FFFFFF); progress.setVisibility(View.GONE); header.addView(progress); } }; public static void hookResSystemui(XC_InitPackageResources.InitPackageResourcesParam resparam) { try { if (ConfigUtils.notifications().change_style) { resparam.res.hookLayout(PACKAGE_SYSTEMUI, "layout", "recents_task_view_header", recents_task_view_header); } } catch (Throwable t) { XposedHook.logE(TAG, "Error hooking SystemUI resources", t); } } public static Object getSystemServicesProxy() { Class<?> classRecentsTaskLoader = XposedHelpers.findClass("com.android.systemui.recents.model.RecentsTaskLoader", mClassLoader); return XposedHelpers.callMethod(XposedHelpers.callStaticMethod(classRecentsTaskLoader, "getInstance"), "getSystemServicesProxy"); } }