Android-Android5.1屏幕固定功能(screen pinning)分析

本文详细分析了Android 5.1中的屏幕固定(Screen Pinning)功能。该功能允许用户将屏幕锁定在特定应用上,防止切换。开启此功能的设置在安全选项中,其系统设置涉及`Settings.java`和`ScreenPinningSettings.java`。在开启后,`Settings.db`数据库会新增`lock_to_app_enabled`项。屏幕固定功能与最近任务(Recents)紧密关联,点击Recents中的特定图标会触发固定请求。源码分析揭示了从点击事件到界面显示的回调流程,以及`ActivityManagerService`如何管理activity栈和任务以实现屏幕固定。此外,第三方应用也可调用`Activity.startLockTask()`和`Activity.stopLockTask()`方法来启用和关闭屏幕固定模式。
    一、设置中开启屏幕固定:  
    此功能在设置-安全中开启,不清楚以往的版本中是否支持就有已经有了此功能,但是Android4.4设置中到时没有发现此项。在Android 5.0发现了此项设置。刚一看到此项设置,就心想:“这是什么鬼!”。设置中的代码在SecuritySettings.java和ScreenPinningSettings.java中,代码量不多,Preference XML文件是security_settings_misc.xml:                 
if (Settings.System.getInt(getContentResolver(),
                Settings.System.LOCK_TO_APP_ENABLED, 0) != 0) {
            root.findPreference(KEY_SCREEN_PINNING).setSummary(
                    getResources().getString(R.string.switch_on_text));
        }
    看到上面代码后,到\android5.1\frameworks\base\core\java\android\provider\Settings.java找到了LOCK_TO_APP_ENABLED,然后就发现这货被hide了,意思就说,在独立应用中是不能去设置此项的值的:     
/**
         * Whether lock-to-app will be triggered by long-press on recents.
         * @hide
         */
        public static final String LOCK_TO_APP_ENABLED = "lock_to_app_enabled";
    之后,本想查看下这货是怎么写进数据库的,纵所周知,provider settings里面的东西一般都会写进数据库,而settings.db的文件是这里被创建的:            
\android5.1\frameworks\base\packages\SettingsProvider\src\com\android\providers\settings\DatabaseHelper.java
     按照介个意思,我想应该是会在这里写进数据库啊,然后就在DatabaseHelper.java搜索LOCK_TO_APP_ENABLED,但是没有找到,只能说,它不是在这里写进数据库的,无奈之下,再度查看ScreenPinningSettings.java中的相关代码:       
private void setLockToAppEnabled(boolean isEnabled) {
        Settings.System.putInt(getContentResolver(), Settings.System.LOCK_TO_APP_ENABLED,
                isEnabled ? 1 : 0);
      }
      Settings.System.putInt()方法有如下描述:       
/**
         * Convenience function for updating a single settings value as an
         * integer. This will either create a new entry in the table if the
         * given name does not exist, or modify the value of the existing row
         * with that name.  Note that internally setting values are always
         * stored as strings, so this function converts the given value to a
         * string before storing it.
         *
         * @param cr The ContentResolver to access.
         * @param name The name of the setting to modify.
         * @param value The new value for the setting.
         * @return true if the value was set, false on database errors
         */
        public static boolean putInt(ContentResolver cr, String name, int value) {
            return putIntForUser(cr, name, value, UserHandle.myUserId());
        }
     由此推断,settings.db数据库system table中本没有lock_to_app_enabled此项,而在开启screen pinning后,会向此表中写入lock_to_app_enabled的数据:    
     settings.db 在手机中的位置: /data/data/com.android.providers.settings/database/settings.db (需要root)。
    
    二、屏幕固定开启后视图的显示:
           在Android5.1 -Recents分析 中曾提到过screen pinning。从代码上看,screen pinning和 Recents绑定到了一块,效果图大致是这样的:
               (图1)
    意思就说,在显示Recents的时候,如果screen pinning在设置中已开启,那么在Recents 视图中最上面的app 缩略图的右下角会有个图标。点击图标以后会出现如下提示界面:
       (图2)
    此时点击“知道了”就会固定到Recents中显示的对应应用界面。通过 Android5.1 -Recents分析 可知图1中的提示图标是在TaskView,其ID为lock_to_app_fab。既然响应点击事件,就可以在TaskView.java中直接找到onClick()方法:   
  @Override
     public void onClick(final View v) {
        final TaskView tv = this;
        final boolean delayViewClick = (v != this) && (v != mActionButtonView);
        if (delayViewClick) {
            // We purposely post the handler delayed to allow for the touch feedback to draw
            postDelayed(new Runnable() {
                @Override
                public void run() {
                    if (Constants.DebugFlags.App.EnableTaskFiltering && v == mHeaderView.mApplicationIcon) {
                        if (mCb != null) {
                            mCb.onTaskViewAppIconClicked(tv);
                        }
                    } else if (v == mHeaderView.mDismissButton) {
                        dismissTask();
                    }
                }
            }, 125);
        } else {
            if (v == mActionButtonView) {
                // Reset the translation of the action button before we animate it out
                mActionButtonView.setTranslationZ(0f);
            }
            if (mCb != null) {
                mCb.onTaskViewClicked(tv, tv.getTask(), (v == mActionButtonView));
            }
        }
    }
         其中mActionButtonView就是响应点击事件的view。图2显示的view的布局为:screen_pinning_request_text_area.xml,其中Button ID:screen_pinning_ok_button就是图2中显示的“知道了”。这部分view 在ScreenPinningRequest.java中被inflate。   
 private void inflateView(boolean isLandscape) {
            // We only want this landscape orientation on <600dp, so rather than handle
            // resource overlay for -land and -sw600dp-land, just inflate this
            // other view for this single case.
            mLayout = (ViewGroup) View.inflate(getContext(), isLandscape
                    ? R.layout.screen_pinning_request_land_phone : R.layout.screen_pinning_request,
                    null);
            // Catch touches so they don't trigger cancel/activate, like outside does.
            mLayout.setClickable(true);
        ...
        ...
  }
  inflate视图,但是图2中中view是如何显示出来的呢?源码中是通过callback一层层的回调来实现的,前面提到过图1中的view是在TaskView中,TaskView有内部接口,在响应了view的onClick方法以后会调用TaskView类内部的callback:            
 if (mCb != null) {
             mCb.onTaskViewClicked(tv, tv.getTask(), (v == mActionButtonView));
        }
  而TaskStackView视图包含TaskView视图,并实现了TaskView内部的callback,并在此调用自己的callback:    
     @Override
    public void onTaskViewClicked(TaskView tv, Task task, boolean lockToTask) {
        // Cancel any doze triggers
        mUIDozeTrigger.stopDozing();
        if (mCb != null) {
            mCb.onTaskViewClicked(this, tv, mStack, task, lockToTask);
        }
    }
  而RecentsView视图又包含TaskStackView视图,并实现TaskStackView的接口,RecentsView在此调用自己callback(onScreenPinningRequest):   
   @Override
    public void onTaskViewClicked(final TaskStackView stackView, final TaskView tv,
                                  final TaskStack stack, final Task task, final boolean lockToTask) {
        // Notify any callbacks of the launching of a new task
        if (mCb != null) {
            mCb.onTaskViewClicked();
        }
        ...
         if (lockToTask) {
                animStartedListener = new ActivityOptions.OnAnimationStartedListener() {
                    boolean mTriggered = false;
                    @Override
                    public void onAnimationStarted() {
                        if (!mTriggered) {
                            postDelayed(new Runnable() {
                                @Override
                                public void run() {
                                    mCb.onScreenPinningRequest();
                                }
                            }, 350);
                            mTriggered = true;
                        }
                    }
                };
            }
        ...

   }
  到这里,callback回调还没有完,RecentsView的 RecentsViewCallbacks 接口被RecentsActivity实现:  
    @Override
    public void onScreenPinningRequest() {
        if (mStatusBar != null) {
            mStatusBar.showScreenPinningRequest(false);
        }
    }
    直到这里callback回调才算基本结束,mStatusBar是PhoneStatusBar类的实例对象,其showScreenPinningRequest方法:     
 public void showScreenPinningRequest(boolean allowCancel) {
        mScreenPinningRequest.showPrompt(allowCancel);
     }
    ScreenPinningRequest.java的showPrompt()方法:     
public void showPrompt(boolean allowCancel) {
        clearPrompt();
        mRequestWindow = new RequestWindowView(mContext, allowCancel);
        mRequestWindow.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
        // show the confirmation
        WindowManager.LayoutParams lp = getWindowLayoutParams();
        mWindowManager.addView(mRequestWindow, lp);
    }
    到这里,图2中的视图在响应了图1视图中的onClick事件以后就显示出来了。

三、屏幕固定实现的功能:
    经过上面的分析可知,最终响应Button-screen_pinning_ok_button来实现屏幕固定的功能,代码自然在ScreenPinningRequest.java中:    
  @Override
    public void onClick(View v) {
        if (v.getId() == R.id.screen_pinning_ok_button || mRequestWindow == v) {
            try {
                ActivityManagerNative.getDefault().startLockTaskModeOnCurrent();
            } catch (RemoteException e) {}
        }
        clearPrompt();
    }
    其中ActivityManagerNative.getDefault() 相当于 ActivityManagerService,所以直接在ActivityManagerService.java中查找startLockTaskModeOnCurrent()方法:    
  @Override
    public void startLockTaskModeOnCurrent() throws RemoteException {
        enforceCallingPermission(android.Manifest.permission.MANAGE_ACTIVITY_STACKS,
                "startLockTaskModeOnCurrent");
        long ident = Binder.clearCallingIdentity();
        try {
            ActivityRecord r = null;
            synchronized (this) {
                r = mStackSupervisor.topRunningActivityLocked();
            }
            startLockTaskMode(r.task);
        } finally {
            Binder.restoreCallingIdentity(ident);
        }
    }
    void startLockTaskMode(TaskRecord task) {
        final String pkg;
        synchronized (this) {
            pkg = task.intent.getComponent().getPackageName();
        }
        boolean isSystemInitiated = Binder.getCallingUid() == Process.SYSTEM_UID;
        if (!isSystemInitiated && !isLockTaskAuthorized(pkg)) {
            StatusBarManagerInternal statusBarManager = LocalServices.getService(
                    StatusBarManagerInternal.class);
            if (statusBarManager != null) {
                statusBarManager.showScreenPinningRequest();
            }
            return;
        }
        long ident = Binder.clearCallingIdentity();
        try {
            synchronized (this) {
                // Since we lost lock on task, make sure it is still there.
                task = mStackSupervisor.anyTaskForIdLocked(task.taskId);
                if (task != null) {
                    if (!isSystemInitiated
                            && ((mStackSupervisor.getFocusedStack() == null)
                                    || (task != mStackSupervisor.getFocusedStack().topTask()))) {
                        throw new IllegalArgumentException("Invalid task, not in foreground");
                    }
                    mStackSupervisor.setLockTaskModeLocked(task, !isSystemInitiated,
                            "startLockTask");
                }
            }
        } finally {
            Binder.restoreCallingIdentity(ident);
        }
    }
    从代码中可看出,此功能的实现都管理activity 的stack 和 task。锁住stack 和 task 不让新的进来,就达到屏幕固定的目的,因为在这种情况下,不能为其他的activity准备stack和task。而取消此模式,有其对应的方法:  
  @Override
    public void stopLockTaskModeOnCurrent() throws RemoteException {
        enforceCallingPermission(android.Manifest.permission.MANAGE_ACTIVITY_STACKS,
                "stopLockTaskModeOnCurrent");
        long ident = Binder.clearCallingIdentity();
        try {
            stopLockTaskMode();
        } finally {
            Binder.restoreCallingIdentity(ident);
        }
    }

四、在独立应用中屏幕固定模式又会怎样:
    首先,此功能是否支持在第三方应用里面实现呢?也许会考虑使用ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE)
    来看看是否有相关的方法,但是ActivityManager.java中的相关接口都是hide的,不能被第三方应用使用:     
/**
     * @hide
     */
    public void startLockTaskMode(int taskId) {
        try {
            ActivityManagerNative.getDefault().startLockTaskMode(taskId);
        } catch (RemoteException e) {
        }
    }
    /**
     * @hide
     */
    public void stopLockTaskMode() {
        try {
            ActivityManagerNative.getDefault().stopLockTaskMode();
        } catch (RemoteException e) {
        }
    }
    虽然ActivityManager类不让使用,但是Activity.java中却提供了相关方法(需要API>=21):   
  public void startLockTask() {
        try {
            ActivityManagerNative.getDefault().startLockTaskMode(mToken);
        } catch (RemoteException e) {
        }
    }
    
     public void stopLockTask() {
        try {
            ActivityManagerNative.getDefault().stopLockTaskMode();
        } catch (RemoteException e) {
        }
    }
    于是在模拟器上测试了一下:
    
    这么看来,此功能算是支持第三方应用开启,并且还提供了一个放来来判断系统是否处于此模式:      
/**
     * Return whether currently in lock task mode.  When in this mode
     * no new tasks can be created or switched to.
     *
     * @see Activity#startLockTask()
     */
    public boolean isInLockTaskMode() {
        try {
            return ActivityManagerNative.getDefault().isInLockTaskMode();
        } catch (RemoteException e) {
            return false;
        }
    }

    而这也带来了思考的问题,当在设置中开启屏幕固定功能,并在Recents上固定某个应用的界面,那么这个应用的界面在onResume的时候是否需要使用isInLockTaskMode来做判断,从而做相应的处理? 这个就要看情况而定吧,我用自己前面瞎写的手电筒应用做测试,如果开启此模式,手电筒会出问题,这个跟我实现手电筒的代码有关系。
    问题又来了,在第三方应用中开启屏幕固定功能,提示界面又是如何显示出来的呢?这个就要回到前面提到的PhoneStatusBar.java,前面在Recents界面固定某个应用的界面是RecentsActivity中实现RecentsView内部接口并调用了PhoneStatusBar类中的showScreenPinningRequest(boolean allowCancel)方法。但是PhoneStatusBar类中还重写了一个父类的方法showScreenPinningRequest()。应用独立开启屏幕固定功能就会调用此方法:   
    @Override
    public void showScreenPinningRequest() {
        if (mKeyguardMonitor.isShowing()) {
            // Don't allow apps to trigger this from keyguard.
            return;
        }
        // Show screen pinning request, since this comes from an app, show 'no thanks', button.
        showScreenPinningRequest(true);
    }


评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值