Android4.4 无Proximity Sensor的设备拨号中实现自动灭屏

     现在的电子产品越来越人性化,用户友好化在给用户带来全新体验的同时,也在改变着人们的日常生活,所以说科技是伟大的,创新是伟大的。

    随着移动设备的多元化发展,各种微型芯片的嵌入,使得它的功能越来越强大。比如各种各样的Sensor,最常见的一种是Proximity Sensor,现在的品牌机几乎都具备,也就是在打电话的时候,为了避免误操作,在电话接近耳朵的时候让手机处于灭屏状态,要实现这一功能使用Proximity Sensor是再好不过的了。

    但是也有一些设备不具备Proximity Sensor(比如我们的平板设备-_-,因为其主要功能并非打电话,所以没有添加接近传感器),为了做到用户友好化,就必须得在没有传感器的状况下添加自动灭屏功能。

    首先找到拨打电话的界面,4.4和之前的系统代码架构有了很大的改变,之前的拨号程序就是Phone,现在Phone基本上废掉了,而且之前提供了一个叫void setPokeLock(int pokey, IBinder lock, String tag)的方法,可以实现几秒后灭屏,还比较好用,之后的系统这个方法给删掉了。但是加了个Telephony的程序,代码路径packages/service/Telephony,按下拨打电话的按钮后,经过一系列的流程转换,最终会进入到PhoneGlobals.java中,代码路径:packages/services/Telephony/src/com/android/phone,其中有这么个方法:

/* package */ void updateWakeState() {
        PhoneConstants.State state = mCM.getState();

        // True if the speakerphone is in use.  (If so, we *always* use
        // the default timeout.  Since the user is obviously not holding
        // the phone up to his/her face, we don't need to worry about
        // false touches, and thus don't need to turn the screen off so
        // aggressively.)
        // Note that we need to make a fresh call to this method any
        // time the speaker state changes.  (That happens in
        // PhoneUtils.turnOnSpeaker().)
        boolean isSpeakerInUse = (state == PhoneConstants.State.OFFHOOK) && PhoneUtils.isSpeakerOn(this);

        // TODO (bug 1440854): The screen timeout *might* also need to
        // depend on the bluetooth state, but this isn't as clear-cut as
        // the speaker state (since while using BT it's common for the
        // user to put the phone straight into a pocket, in which case the
        // timeout should probably still be short.)

        // Decide whether to force the screen on or not.
        //
        // Force the screen to be on if the phone is ringing or dialing,
        // or if we're displaying the "Call ended" UI for a connection in
        // the "disconnected" state.
        // However, if the phone is disconnected while the user is in the
        // middle of selecting a quick response message, we should not force
        // the screen to be on.
        //
        boolean isRinging = (state == PhoneConstants.State.RINGING);
        boolean isDialing = (phone.getForegroundCall().getState() == Call.State.DIALING);
        boolean isVideoCallActive = PhoneUtils.isImsVideoCallActive(mCM.getActiveFgCall());
        boolean keepScreenOn = isRinging || isDialing || isVideoCallActive;
        // keepScreenOn == true means we'll hold a full wake lock:
        requestWakeState(keepScreenOn ? WakeState.FULL : WakeState.SLEEP);
    }
/* package */ void requestWakeState(WakeState ws) {
        if (VDBG) Log.d(LOG_TAG, "requestWakeState(" + ws + ")...");
        synchronized (this) {
            if (mWakeState != ws) {
                switch (ws) {
                    case PARTIAL:
                        // acquire the processor wake lock, and release the FULL
                        // lock if it is being held.
                        mPartialWakeLock.acquire();
                        if (mWakeLock.isHeld()) {
                            mWakeLock.release();
                        }
                        break;
                    case FULL:
                        // acquire the full wake lock, and release the PARTIAL
                        // lock if it is being held.
                        mWakeLock.acquire();
                        if (mPartialWakeLock.isHeld()) {
                            mPartialWakeLock.release();
                        }
                        break;
                    case SLEEP:
                    default:
                        // release both the PARTIAL and FULL locks.
                        if (mWakeLock.isHeld()) {
                            mWakeLock.release();
                        }
                        if (mPartialWakeLock.isHeld()) {
                            mPartialWakeLock.release();
                        }
                        break;
                }
                mWakeState = ws;
            }
        }
    }
这个updateWakestate就是更新通话状态中屏幕的状态的,如果有Proximity Sensor,会使用WakeLock锁去更新屏幕状态,WakeLock定义在PowerManager中,是一个内部类,这个类主要是通过申请和释放一个锁来控制屏幕的变化,最终还是调用到PowerManagerService中,PowerManagerService就是我们通常所说的Android电源管理类。

没有Proximity Sensor的状态下,keepScreenOn的状态最终会被置为false,也就进入到case SLEEP这个分支,什么也不干,这个时候,PowerManagerService就会调用Settings中用户设置的休眠时间去使屏幕休眠,如果设置中设置的是30分钟,那你拨打一个电话后30分钟后才会休眠。

最简单的使屏幕休眠的方法是goToSleep(long time),PowerManager中提供了接口以供调用,我们只需要获得PowerManager服务,就可以调用,当然在AndroidManifest中需要添加相应的权限。但是在拨号中这个方法是不可行的,会造成很不好的用户体验,原因就不多说了,直接说我改后测试成功的方法。


在PowerManagerService中,定义了一个标志位mDirty,其中有十二种状态的变化,通过各种逻辑处理和复杂的判断,最终达到管理电源的目的,由于这个电源管理框架是google直接维护的,代码写的精简而富于活力,其中的繁多的状态变化实在是跟的眼花缭乱,我实在无心去慢慢的打Log一步步的跟进流程,但是也有牛人把这套东西一步步跟的清清楚楚并分享出来了,参考:http://wenku.baidu.com/link?url=Ph3fYPtSmbOFpNAvgNIvLJkbo7SW7XWMuRsgLQ0640wPTvXo0DdfIHcXqHpRDN5JHrQb7saiKjAgFvS1Q4kHYqosnze97mIi3iFJjTefS3W

有兴趣的可以去深入了解。

继续讲我的,在我看到了一个叫private long mUserActivityTimeoutOverrideFromWindowManager = -1;的变量时,我觉得离解决这个问题不远了。这个官方还有注释,大概意思是,可通过应用程序设置这个值,临时的调整屏幕的亮度,-1为禁用。继续看这个变量在什么地方用了:

private void setUserActivityTimeoutOverrideFromWindowManagerInternal(long timeoutMillis) {
        synchronized (mLock) {
            if (mUserActivityTimeoutOverrideFromWindowManager != timeoutMillis) {
                mUserActivityTimeoutOverrideFromWindowManager = timeoutMillis;
                mDirty |= DIRTY_SETTINGS;
                updatePowerStateLocked();
            }
        }
    }

设置一个值后调用updatePowerStateLocked方法,这个方法是PowerManagerService的关键所在。

private void updatePowerStateLocked() {
        if (!mSystemReady || mDirty == 0) {
            return;
        }

        // Phase 0: Basic state updates.
        updateIsPoweredLocked(mDirty);
        updateStayOnLocked(mDirty);

        // Phase 1: Update wakefulness.
        // Loop because the wake lock and user activity computations are influenced
        // by changes in wakefulness.
        final long now = SystemClock.uptimeMillis();
        int dirtyPhase2 = 0;
        for (;;) {
            int dirtyPhase1 = mDirty;
            dirtyPhase2 |= dirtyPhase1;
            mDirty = 0;

            updateWakeLockSummaryLocked(dirtyPhase1);
            updateUserActivitySummaryLocked(now, dirtyPhase1);
            if (!updateWakefulnessLocked(dirtyPhase1)) {
                break;
            }
        }

        // Phase 2: Update dreams and display power state.
        updateDreamLocked(dirtyPhase2);
        updateDisplayPowerStateLocked(dirtyPhase2);

        // Phase 3: Send notifications, if needed.
        if (mDisplayReady) {
            sendPendingNotificationsLocked();
        }

        // Phase 4: Update suspend blocker.
        // Because we might release the last suspend blocker here, we need to make sure
        // we finished everything else first!
        updateSuspendBlockerLocked();
    }

关于这个方法所作的工作,前面给的连接里面解释的很清楚,我也就不多说了,我们需要知道的是属于用户的操作而使电源状态发生更改一定会调用updateUserActivitySummaryLocked(now, dirtyPhase1)方法,比如你触摸了屏幕,点击了一个Button等等,都会调用此方法,用以改变电源的状态,使你的屏幕亮起来,不进入灭屏状态(已经处于灭屏状态只能通过按power键唤醒)。

那么,看看updateUserActivitySummaryLocked干了什么:

private void updateUserActivitySummaryLocked(long now, int dirty) {
        // Update the status of the user activity timeout timer.
        if ((dirty & (DIRTY_USER_ACTIVITY | DIRTY_WAKEFULNESS | DIRTY_SETTINGS)) != 0) {
            mHandler.removeMessages(MSG_USER_ACTIVITY_TIMEOUT);

            long nextTimeout = 0;
            if (mWakefulness != WAKEFULNESS_ASLEEP) {
                final int screenOffTimeout = getScreenOffTimeoutLocked();
                final int screenDimDuration = getScreenDimDurationLocked(screenOffTimeout);

                mUserActivitySummary = 0;
                if (mLastUserActivityTime >= mLastWakeTime) {
                    nextTimeout = mLastUserActivityTime
                            + screenOffTimeout - screenDimDuration;
                    if (now < nextTimeout) {
                        mUserActivitySummary |= USER_ACTIVITY_SCREEN_BRIGHT;
                    } else {
                        nextTimeout = mLastUserActivityTime + screenOffTimeout;
                        if (now < nextTimeout) {
                            mUserActivitySummary |= USER_ACTIVITY_SCREEN_DIM;
                        }
                    }
                }
                if (mUserActivitySummary == 0
                        && mLastUserActivityTimeNoChangeLights >= mLastWakeTime) {
                    nextTimeout = mLastUserActivityTimeNoChangeLights + screenOffTimeout;
                    if (now < nextTimeout
                            && mDisplayPowerRequest.screenState
                                    != DisplayPowerRequest.SCREEN_STATE_OFF) {
                        mUserActivitySummary = mDisplayPowerRequest.screenState
                                == DisplayPowerRequest.SCREEN_STATE_BRIGHT ?
                                USER_ACTIVITY_SCREEN_BRIGHT : USER_ACTIVITY_SCREEN_DIM;
                    }
                }
                if (mUserActivitySummary != 0) {
                    Message msg = mHandler.obtainMessage(MSG_USER_ACTIVITY_TIMEOUT);
                    msg.setAsynchronous(true);
                    mHandler.sendMessageAtTime(msg, nextTimeout);
                }
            } else {
                mUserActivitySummary = 0;
            }

            if (DEBUG_SPEW) {
                Slog.d(TAG, "updateUserActivitySummaryLocked: mWakefulness="
                        + wakefulnessToString(mWakefulness)
                        + ", mUserActivitySummary=0x" + Integer.toHexString(mUserActivitySummary)
                        + ", nextTimeout=" + TimeUtils.formatUptime(nextTimeout));
            }
        }
    }

其中有final int screenOffTimeout = getScreenOffTimeoutLocked();  final int screenDimDuration = getScreenDimDurationLocked(screenOffTimeout);还有一个变量nextTimeout,这个变量就是控制下次灭屏时间的,大致是等于screenOffTimeout 减去screenDimDuration的值,getScreenOffTimeoutLocked() 和 getScreenDimDurationLocked(screenOffTimeout)是什么呢?看代码:

private int getScreenOffTimeoutLocked() {
        int timeout = mScreenOffTimeoutSetting;
        if (isMaximumScreenOffTimeoutFromDeviceAdminEnforcedLocked()) {
            timeout = Math.min(timeout, mMaximumScreenOffTimeoutFromDeviceAdmin);
        }
        if (mUserActivityTimeoutOverrideFromWindowManager >= 0) {
            timeout = (int)Math.min(timeout, mUserActivityTimeoutOverrideFromWindowManager);
        }
        return Math.max(timeout, MINIMUM_SCREEN_OFF_TIMEOUT);
    }

    private int getScreenDimDurationLocked(int screenOffTimeout) {
        return Math.min(SCREEN_DIM_DURATION,
                (int)(screenOffTimeout * MAXIMUM_SCREEN_DIM_RATIO));
    }


其中的mScreenOffTimeoutSetting就是取的Settings中用户设置的休眠时间,MINIMUM_SCREEN_OFF_TIMEOUT是一个常量值10 * 1000,也就是十秒,通常情况下,

getScreenOffTimeoutLocked()的返回值是settings中用户设置的休眠值,而此时getScreenDimDurationLocked返回的是SCREEN_DIM_DURATION,也是个常量值,7 * 1000,7秒。每次当用户触摸屏幕(或者其它的操作),都会重新设置此值,比如你设置的屏幕休眠时间为5分钟,你没有对你的设备进行任何操作,在4分59秒的时候触摸了一下屏幕,那么你的设备休眠时间又会重新计时,无操作五分钟后灭屏,如此循环下去,就是在这个地方处理的。

只有一种情况例外(这里指的是自然状态下,没有外来力量参与的情况,比如有了Proximity Sensor,那就是另外一回事了),那就是设备重启后进入锁屏状态时,会调用setUserActivityTimeoutOverrideFromWindowManager方法,将mUserActivityTimeoutOverrideFromWindowManager的值设为10000,也就是10秒,这个时候getScreenOffTimeoutLocked的返回值就不再是Settings中设置的休眠时间,而是10000,getScreenDimDurationLocked的返回值也不是SCREEN_DIM_DURATION,而是screenOffTimeout * MAXIMUM_SCREEN_DIM_RATIO,也就是2000,那么进入updateUserActivitySummaryLocked方法中计算出来的结果就是8秒灭屏,这也是为什么在锁屏状态下我们的设备会很快的进入休眠的原因。

在锁屏状态解除的时候,会在WindowManagerService中调用mPowerManager.setUserActivityTimeoutOverrideFromWindowManager(
                    mInnerFields.mUserActivityTimeout);

在这里将mUserActivityTimeoutOverrideFromWindowManager值设回-1,此时,设备的休眠时间又会回复到Settings中设置的时间。


看到这里我们就可以仿照这套流程添加一个变量来控制拨打电话的时候的电源状态。 在PowerManagerService中加入:

private long mUserActivityTimeoutOverrideFromCall = -1;

在getScreenOffTimeoutLocked()方法中加入:

if (mUserActivityTimeoutOverrideFromCall >= 0) {
        timeout = (int)Math.min(timeout, mUserActivityTimeoutOverrideFromCall);
}

还需要加入一个接口以供外部调用:

// Add liao --2015-01-23
    private void setUserActivityTimeoutOverrideFromCallInternal(long timeoutMillis) {
        synchronized (mLock) {
            if (mUserActivityTimeoutOverrideFromCall != timeoutMillis) {
                mUserActivityTimeoutOverrideFromCall = timeoutMillis;
                mDirty |= DIRTY_SETTINGS;
                updatePowerStateLocked();
            }
        }
    }
    // Add liao --2015-01-23
    public void setTimeout(long timeoutMillis) {
        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, null);

        final long ident = Binder.clearCallingIdentity();
        try {
            setUserActivityTimeoutOverrideFromCallInternal(timeoutMillis);
        } finally {
            Binder.restoreCallingIdentity(ident);
        }
    }

好,PowerManagerService中的东东加好了,再来PowerManager中加入setTimeout接口,让外部类可以调用:

public static final long POKE_LOCK_SHORT_TIMEOUT = 1000L;
public static final long POKE_LOCK_TIMEOUT_STOP = -1L;

public void setTimeout(long timeout) {
        try {
            mService.setTimeout(timeout);
        } catch (RemoteException e) {
        }
    }

当然,别忘记aidl,还需要在IPowerManager.aidl中加入 void setTimeout(long timeout);

对于aidl怎么在android系统中使用可以参考我前面写的文章。


对外的接口都添加完了,接下来就是拨号的地方去更改mUserActivityTimeoutOverrideFromCall的值了,在PhoneGlobals的updateWakeState()方法中加入:

        mPowerManager.setTimeout(PowerManager.POKE_LOCK_SHORT_TIMEOUT);
        if (state == PhoneConstants.State.IDLE) {
            mPowerManager.setTimeout(PowerManager.POKE_LOCK_TIMEOUT_STOP);
        }

这个方法一开始就执行了这一句:PhoneConstants.State state = mCM.getState();这是获取电话的状态,是在拨号中,还是拨通了,还是挂断了没有活动,在PhoneConstants中分别定义了这三种状态:

public enum State {
        IDLE, RINGING, OFFHOOK;
    };

当电话拨打的时候,就会调用setTimeout进入到PowerManagerService中的setUserActivityTimeoutOverrideFromCallInternal方法,更新mUserActivityTimeoutOverrideFromCall的值,并且调用updatePowerStateLocked(),从而进入到updateUserActivitySummaryLocked方法中,通过一系列的算法,最终得出的值和锁屏状态的灭屏时间是一样的,大概8秒,如果嫌8秒太长,可以将MINIMUM_SCREEN_OFF_TIMEOUT这个常量改小一点,比如改成7 * 1000,那么最终得出的灭屏时间就是5秒,拨号后3秒屏幕变暗,5秒灭屏。

其中,IDLE就表示没有拨号,电话挂断的时候必然会将状态改为IDLE,所以在电话挂断的时候在将mUserActivityTimeoutOverrideFromCall的值设置为-1,让灭屏时间回复到Settings中设置的时间。


至此,让没有Proximity Sensor的设备在通话过程中自动灭屏的功能告一段落。


郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。