监听Home键之后启动一个后台Activity延迟5s

2019-08-27  本文已影响0人  福later

问题引入

最近做项目时遇到这样一个问题,原本需求是这样的:
在一个播放界面,播放时退出当前界面,或者点击home键时,窗口上会显示一个小的悬浮窗,点击这个悬浮窗,就会跳转至播放界面。
很显然这个悬浮窗是全局的,即时程序退至后台,依然坚挺的显示在界面只上。
然后测试发现这样一个问题:

当点击home之后,立即点击悬浮窗跳转,会有一段时间的延迟才会跳转。如果放置一段时间再进行点击,则能立即跳转,没有问题。
当点击返回,finish掉当前播放页,不管是立即还是放置一会儿再点击,都是ok的。
Intent intent = new Intent(applicationContext, PlayActivity.class);
intent.putExtra(InteractionFmMainActivity.INFO_ID_KEY, PlayActivity.sParamsIdKey);
intent.putExtra(InteractionFmMainActivity.INFO_TYPE_KEY,PlayActivity.sParamsInfoTypeKey);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
applicationContext.startActivity(intent);

WHY

通过搜索引擎,得知这原来是Google官方就是这么设置的!

That is, don't call startActivity() from BroadcastReceivers or Services running in the background. Doing so will interrupt whatever application is currently running, and result in an annoyed user. Perhaps even worse, your Activity may become a "keystroke bandit" and receive some of the input the user was in the middle of providing to the previous Activity. Depending on what your application does, this could be bad news.

解决方案

显然上述的解决方案是行不通的,光是要求root手机,就有点过分了,试问哪个用户会为了你一个app大费周章去root手机,简直是得不偿失。那么真的就无计可施了吗?
答案当然是NO,不然鄙人也不会在这做这个问题记录

Intent intent = new Intent(context, A.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent pendingIntent =
PendingIntent.getActivity(context, 0, intent, 0);
try {
pendingIntent.send();
} catch (PendingIntent.CanceledException e) {
e.printStackTrace();
}

他将intent用PendingIntent包裹后,进行启动,于是我也按照这方法修改了自己的代码:

Intent intent = new Intent(applicationContext, PlayActivity.class);
intent.putExtra(InteractionFmMainActivity.INFO_ID_KEY, PlayActivity.sParamsIdKey);
intent.putExtra(InteractionFmMainActivity.INFO_TYPE_KEY,PlayActivity.sParamsInfoTypeKey);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
try {
      PendingIntent pendingIntent = PendingIntent.getActivity(ApplicationGlobal.getGlobalContext(), 0, intent, 0);
                        pendingIntent.send();
     } catch (Exception e) {
                        e.printStackTrace();
     }

经测试后确实是完美解决了问题,感谢这位大神的解答!

刨根问底

那么究竟为什么会有这个问题呢,下面我们从源码的角度进行剖析。

1.事件分发前的拦截过程
Home事件在分发前的关键拦截过程:

......
if (keyCode == KeyEvent.KEYCODE_HOME) {
            if (!down) {
                .........
                launchHomeFromHotKey();
                return -1;

            }

        ........

        }

    }

void launchHomeFromHotKey() {
        .....
         try {
                ActivityManagerNative.getDefault().stopAppSwitches();
                } catch (RemoteException e) {
                }
         .....

 }

最后会走到ActivityManagerService的stopAppSwitches()方法

public void stopAppSwitches() {
        if (checkCallingPermission(android.Manifest.permission.STOP_APP_SWITCHES)
                != PackageManager.PERMISSION_GRANTED) {
            throw new SecurityException("Requires permission "
                    + android.Manifest.permission.STOP_APP_SWITCHES);
        }

        synchronized(this) {
            mAppSwitchesAllowedTime = SystemClock.uptimeMillis()
                    + APP_SWITCH_DELAY_TIME;
            mDidAppSwitch = false;
            mHandler.removeMessages(DO_PENDING_ACTIVITY_LAUNCHES_MSG);
            Message msg = mHandler.obtainMessage(DO_PENDING_ACTIVITY_LAUNCHES_MSG);
            mHandler.sendMessageDelayed(msg, APP_SWITCH_DELAY_TIME);
        }

    }

2.startActivity的启动流程
关于启动流程,网上已经有很多的相关资料,在这里我们只分析ActivityStackSupervisor类的startActivityLocked的方法,在此方法内我们可以发现在执行下个流程的startActivityUncheckedLocked方法前,会有个条件判断,如下:

final ActivityStack stack = getFocusedStack();
        if (stack.mResumedActivity == null
                || stack.mResumedActivity.info.applicationInfo.uid != callingUid) {
            if (!mService.checkAppSwitchAllowedLocked(callingPid, callingUid, "Activity start")) {
                PendingActivityLaunch pal =
                        new PendingActivityLaunch(r, sourceRecord, startFlags, stack);
                mService.mPendingActivityLaunches.add(pal);
                setDismissKeyguard(false);
                ActivityOptions.abort(options);
                return ActivityManager.START_SWITCHES_CANCELED;
            }

        }

由于是后台服务启动的Activity,所以stack.mResumedActivity.info.applicationInfo.uid != callingUid的值肯定为true,其中callingUid为后台服务的UID,stack.mResumedActivity.info.applicationInfo.uid为当前前台显示Activity的UID。

继续分析ActivityManagerService类的checkAppSwitchAllowedLocked的方法:

int checkComponentPermission(String permission, int pid, int uid,
            int owningUid, boolean exported) {
        ...
        return ActivityManager.checkComponentPermission(permission, uid,
                owningUid, exported);
    }

最后分析ActivityManager类的checkComponentPermission的方法。

public static int checkComponentPermission(String permission, int uid,
            int owningUid, boolean exported) {
        if (uid == 0 || uid == Process.SYSTEM_UID) {
            return PackageManager.PERMISSION_GRANTED;
        }

        ....

        try {
            return AppGlobals.getPackageManager()
                    .checkUidPermission(permission, uid);
        } catch (RemoteException e) {
            Slog.e(TAG, "PackageManager is dead?!?", e);
        }
        return PackageManager.PERMISSION_DENIED;
    }

由上可以发现后台服务的UID如果为Process.SYSTEM_UID,或者启动的Activity具有android.Manifest.permission.STOP_APP_SWITCHES权限,就不会进入延时5s启动Activity流程,而是进入startActivityUncheckedLocked方法正常启动Activity。

3.原因分析
经过一、二的分析,再在关键地方加入日志,把callingUid的值打印出来,最后发现在应用中点击悬浮窗进行跳转操作时(或者直接home后,点击应用图标),callingUid的值为1000,与Process.SYSTEM_UID相等,这种情况是完全ok的,Activity会立即启动。而从桌面点击悬浮窗按钮进行跳转时,callingUid的值为50122,进入到延时5s启动Activity的流程。

上一篇下一篇

猜你喜欢

热点阅读