Android 11 无线充电动画、铃声及问题分析

Android 充电的方式有三种:电源充电,USB充电,无线充电,其中原生实现了无线充电的动画,以及相关提示音,无线充电动画的起始位置则是从PowerManagerService开始

流程分析

1. PowerManagerService

在PowerManagerService中有这样一个方法updateIsPoweredLocked,在充电状态发生改变时都会进入到该方法,而无线充电的处理逻辑也在此方法中

private void updateIsPoweredLocked(int dirty) {

    if ((dirty & DIRTY_BATTERY_STATE) != 0) {

        final boolean wasPowered = mIsPowered;

        final int oldPlugType = mPlugType;

        final boolean oldLevelLow = mBatteryLevelLow;

        //BatteryManagerInternal的实现类再BatteryService中,isPowered中查看当前state是否

        //为AC,USB,Wireless,如果是则返回true

        mIsPowered = mBatteryManagerInternal.isPowered(BatteryManager.BATTERY_PLUGGED_ANY);

        //当前插入的类型 AC,USB,Wireless

        mPlugType = mBatteryManagerInternal.getPlugType();

        //电池电量

        mBatteryLevel = mBatteryManagerInternal.getBatteryLevel();

        //电量小于等于15则为低电状态

        mBatteryLevelLow = mBatteryManagerInternal.getBatteryLevelLow();

        if (wasPowered != mIsPowered || oldPlugType != mPlugType) {

            mDirty |= DIRTY_IS_POWERED;



            //更新无线充电的状态,此值决定当前是否为无线充电

            final boolean dockedOnWirelessCharger = mWirelessChargerDetector.update(

                    mIsPowered, mPlugType);



            // 将插拔设备视为用户行为。当用户插上或拔出设备时,它会立即关闭,

            // 这让用户感到不安。有些设备在插入或拔出时也会唤醒设备,因为它们

            // 没有充电指示灯。

            final long now = mClock.uptimeMillis();

            //判断当前状态是否应该亮屏,插入USB充电或无线充电时会亮屏

            //google 在此方法中做了一些错误状态规避

            if (shouldWakeUpWhenPluggedOrUnpluggedLocked(wasPowered, oldPlugType,

                    dockedOnWirelessCharger)) {

                //去点亮屏幕

                wakeUpNoUpdateLocked(now, PowerManager.WAKE_REASON_PLUGGED_IN,

                        "android.server.power:PLUGGED:" + mIsPowered, Process.SYSTEM_UID,

                        mContext.getOpPackageName(), Process.SYSTEM_UID);

            }

            userActivityNoUpdateLocked(

                    now, PowerManager.USER_ACTIVITY_EVENT_OTHER, 0, Process.SYSTEM_UID);



            // 仅在启动完成后播放充电声音,因此充电声音不会与潜在的通知声音一起播放

            // mBootCompleted此值在启动完成后会置为true

            if (mBootCompleted) {

                if (mIsPowered && !BatteryManager.isPlugWired(oldPlugType)

                        && BatteryManager.isPlugWired(mPlugType)) {

                    mNotifier.onWiredChargingStarted(mUserId);

                } else if (dockedOnWirelessCharger) {

                    //通知无线充电状态

                    mNotifier.onWirelessChargingStarted(mBatteryLevel, mUserId);

                }

            }

        }

        mBatterySaverStateMachine.setBatteryStatus(mIsPowered, mBatteryLevel, mBatteryLevelLow);

    }

}

此方法会在多种情况下调用,接收电池改变广播,以及其他一些情况

最终如果dockedOnWirelessCharger值为true的话,会向下通知无线充电的状态

/**

 * 在无线充电开始时调用 - 提供用户反馈(声音和视觉)。

 */

public void onWirelessChargingStarted(int batteryLevel, @UserIdInt int userId) {

    mSuspendBlocker.acquire();

    Message msg = mHandler.obtainMessage(MSG_WIRELESS_CHARGING_STARTED);

    msg.setAsynchronous(true);

    msg.arg1 = batteryLevel;

    msg.arg2 = userId;

    mHandler.sendMessage(msg);

}

此方法中仅仅发送了一条消息,去查看消息的处理方式,Handler处理消息并最终调用如下方法

private void showWirelessChargingStarted(int batteryLevel, @UserIdInt int userId) {

    // 播放声音 + 触觉

    playChargingStartedFeedback(userId, true /* wireless */);



    // 显示动画

    if (mShowWirelessChargingAnimationConfig && mStatusBarManagerInternal != null) {

        mStatusBarManagerInternal.showChargingAnimation(batteryLevel);

    }

    mSuspendBlocker.release();

}

该方法开始兵分两路,播放声音,并且去显示充电动画

播放声音:

private void playChargingStartedFeedback(@UserIdInt int userId, boolean wireless) {

    //此值查看当前是否为请勿打扰模式并且是否可以使用

    if (!isChargingFeedbackEnabled(userId)) {

        return;

    }



    // 震动

    final boolean vibrate = Settings.Secure.getIntForUser(mContext.getContentResolver(),

            Settings.Secure.CHARGING_VIBRATION_ENABLED, 1, userId) != 0;

    if (vibrate) {

        mVibrator.vibrate(CHARGING_VIBRATION_EFFECT, VIBRATION_ATTRIBUTES);

    }



    // 播放声音

    //不管是无线充电还是线充电,铃声的URI已经存储在数据库中

    final String soundPath = Settings.Global.getString(mContext.getContentResolver(),

            wireless ? Settings.Global.WIRELESS_CHARGING_STARTED_SOUND

                    : Settings.Global.CHARGING_STARTED_SOUND);

    final Uri soundUri = Uri.parse("file://" + soundPath);

    if (soundUri != null) {

        final Ringtone sfx = RingtoneManager.getRingtone(mContext, soundUri);

        if (sfx != null) {

            sfx.setStreamType(AudioManager.STREAM_SYSTEM);

            //播放充电提示音

            sfx.play();

        }

    }

}

显示动画:

mShowWirelessChargingAnimationConfig  此值配置是否支持无线充电动画,可以进行客制化

mStatusBarManagerInternal.showChargingAnimation(batteryLevel); 及 StatusBarManagerService进行的实现

@Override

public void showChargingAnimation(int batteryLevel) {

    if (mBar != null) {

        try {

            mBar.showWirelessChargingAnimation(batteryLevel);

        } catch (RemoteException ex){

        }

    }

}

由StatusBarManagerService通知statusbar去显示无线充电动画

public void showWirelessChargingAnimation(int batteryLevel) {

    showChargingAnimation(batteryLevel, UNKNOWN_BATTERY_LEVEL, 0);

}



protected void showChargingAnimation(int batteryLevel, int transmittingBatteryLevel,

            long animationDelay) {

    //此时处于Doze模式或锁屏界面,此时通知面板在手机界面显示

    if (mDozing || mKeyguardManager.isKeyguardLocked()) {

        // 在Doze或锁屏下,在动画开始前先隐藏通知面板

        WirelessChargingAnimation.makeWirelessChargingAnimation(mContext, null,

                transmittingBatteryLevel, batteryLevel,

                new WirelessChargingAnimation.Callback() {

                    @Override

                    public void onAnimationStarting() {

                        // 通过CrossFadeHelper.fadeOut隐藏通知面板,内部实现则是调用View.setVisibility

                        mNotificationShadeWindowController.setRequestTopUi(true, TAG);

                        CrossFadeHelper.fadeOut(mNotificationPanelViewController.getView(), 1);

                    }



                    @Override

                    public void onAnimationEnded() {

                        // 通过CrossFadeHelper.fadeIn,将通知面板显示出来

                        CrossFadeHelper.fadeIn(mNotificationPanelViewController.getView());

                        mNotificationShadeWindowController.setRequestTopUi(false, TAG);

                    }

                }, mDozing).show(animationDelay);

    } else {

        // 如果在桌面状态下则直接显示充电动画

        WirelessChargingAnimation.makeWirelessChargingAnimation(mContext, null,

                transmittingBatteryLevel, batteryLevel,

                new WirelessChargingAnimation.Callback() {

                    @Override

                    public void onAnimationStarting() {

                        mNotificationShadeWindowController.setRequestTopUi(true, TAG);

                    }



                    @Override

                    public void onAnimationEnded() {

                        mNotificationShadeWindowController.setRequestTopUi(false, TAG);

                    }

                }, false).show(animationDelay);

    }

}

调用WirelessChargingAnimation.makeWirelessChargingAnimation显示充电动画, makeWirelessChargingAnimation()方法返回一个

WirelessChargingAnimation对象,并且调用该对象的 show 方法

public void show(long delay) {

    if (mCurrentWirelessChargingView == null ||

            mCurrentWirelessChargingView.mNextView == null) {

        throw new RuntimeException("setView must have been called");

    }



    // 先清除之前的动画状态

    if (mPreviousWirelessChargingView != null) {

        mPreviousWirelessChargingView.hide(0);

    }



    mPreviousWirelessChargingView = mCurrentWirelessChargingView;

    // 显示现在的动画

    mCurrentWirelessChargingView.show(delay);

    // 设置动画结束时间

    mCurrentWirelessChargingView.hide(delay + DURATION);

}



// 发送show消息

public void show(long delay) {

    if (DEBUG) Slog.d(TAG, "SHOW: " + this);

    mHandler.sendMessageDelayed(Message.obtain(mHandler, SHOW), delay);

}



// 发送hide消息

public void hide(long duration) {

    mHandler.removeMessages(HIDE);



    if (DEBUG) Slog.d(TAG, "HIDE: " + this);

    mHandler.sendMessageDelayed(Message.obtain(mHandler, HIDE), duration);

}

最终的处理方法在handleShow及handleHide

private void handleShow() {

    if (mView != mNextView) {

        // remove the old view if necessary

        handleHide();

        mView = mNextView;

        Context context = mView.getContext().getApplicationContext();

        String packageName = mView.getContext().getOpPackageName();

        if (context == null) {

            context = mView.getContext();

        }

        mWM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

        mParams.packageName = packageName;

        mParams.hideTimeoutMilliseconds = DURATION;



        if (mView.getParent() != null) {

            mWM.removeView(mView);

        }



        try {

            if (mCallback != null) {

                mCallback.onAnimationStarting();

            }

            mWM.addView(mView, mParams);

        } catch (WindowManager.BadTokenException e) {

            Slog.d(TAG, "Unable to add wireless charging view. " + e);

        }

    }

}



private void handleHide() {

    if (DEBUG) Slog.d(TAG, "HANDLE HIDE: " + this + " mView=" + mView);

    if (mView != null) {

        if (mView.getParent() != null) {

            if (DEBUG) Slog.d(TAG, "REMOVE! " + mView + " in " + this);

            if (mCallback != null) {

                mCallback.onAnimationEnded();

            }

            mWM.removeViewImmediate(mView);

        }



        mView = null;

    }

}

至此无线充电动画和铃声流程结束

问题分析

1. 在测试无线充电过程中,反复在无线充电底座靠近拿起手机,偶现无线充电已连接但手机没有亮屏

这个问题就牵扯到PowerManagerService中的流程,在我们上面分析的流程中,有一个shouldWakeUpWhenPluggedOrUnpluggedLocked方法,如果此方法返回true那么才会去WakeUp, 那么大概的方向已经确定,就是这个方法的返回值可能出现问题

查看此方法

private boolean shouldWakeUpWhenPluggedOrUnpluggedLocked(

            boolean wasPowered, int oldPlugType, boolean dockedOnWirelessCharger) {

    // 除非经过配置,否则在通电时不要唤醒。

    if (!mWakeUpWhenPluggedOrUnpluggedConfig) {

        return false;

    }



    // 与无线充电器断开连接时不要唤醒。

    if (wasPowered && !mIsPowered

            && oldPlugType == BatteryManager.BATTERY_PLUGGED_WIRELESS) {

        return false;

    }



    // 除非我们确定,否则不要在对接无线充电器时醒来。

    if (!wasPowered && mIsPowered

            && mPlugType == BatteryManager.BATTERY_PLUGGED_WIRELESS

            && !dockedOnWirelessCharger) {

        return false;

    }



    // 如果已经开始充电,并且获得了电量,那么就不要唤醒

    if (mIsPowered && getWakefulnessLocked() == WAKEFULNESS_DREAMING) {

        return false;

    }



    // 启用剧院模式时不要唤醒。

    if (mTheaterModeEnabled && !mWakeUpWhenPluggedOrUnpluggedInTheaterModeConfig) {

        return false;

    }



    // SystemUI 显示充电指示灯

    if (mAlwaysOnEnabled && getWakefulnessLocked() == WAKEFULNESS_DOZING) {

        return false;

    }



    // 否则唤醒!

    return true;

}

此方法中规避了很多异常操作,只有当这些异常操作都不成立的时候才会去唤醒屏幕,当然还有更完整的解释

实施启发式方法来检测无线充电器的对接或断开。有些设备的无线充电电路无法检测到设备何时放在无线充电器上,除非设备实际从充电器接收电力。如果电池已快充满或太热,设备可能会停止供电。因此,我们不能总是依靠电池服务无线插头信号来准确指示设备是否已与无线充电器对接或断开对接。

 这是一个问题,因为当设备插入无线充电器时,电源管理器通常会唤醒屏幕并播放提示音。对于系统来说,抑制虚假的对接和取消对接信号很重要,因为它们可能会干扰用户(尤其是当它们导致在深夜没有明显原因播放音调时)。

 为了避免杂散信号,我们对无线充电器应用了一些特殊策略。

 1. 设备与无线充电器断开连接时不要唤醒设备,因为设备可能仍在无线充电器上,但由于电池已满而不再通电。理想情况下,如果我们可以确定用户已从无线充电器中取出设备,我们就会唤醒设备,但由于硬件限制,我们必须更加保守。

 2. 如果电池已基本充满,请勿在连接无线充电器时唤醒设备。这种情况可能表明设备一直放在充电器上,只是因为电池已经充满而没有通电。我们无法判断该设备是刚刚放在充电器上,还是已经在那里放了半夜慢慢放电,直到达到需要再次开始充电的程度。因此,当电池电量高于给定阈值时,我们会抑制对接信号。

3. 如果设备自上次断开对接后似乎没有移动,请勿在接入无线充电器时唤醒该设备,因为之前的断开对接信号可能是虚假的。我们使用重力传感器来检测这种情况。

此段注释来自 WirelessChargerDetector.java

2. 在无线充电底座附近短距离抬起并放下设备,无充电动画显示,无充电提示音播放

如果要有无线充电动画显示,并且有提示音,无线充电的状态必须通知下去,通过复现并打印log发现,因为dockedOnWirelessCharger此值为false,所以不会去通知 无线充电的状态,所以需要分析此值为false的原因,此值的来源为mWirelessChargerDetector.update();

因此,对该方法进行添加log查看

/**
     * 如果检测到对接,则更新充电状态并返回 true。
     *
     * @param isPowered 如果设备已通电,则为真。
     * @param plugType 当前插头类型。
     * @return 如果在抑制虚假对接或取消对接信号后确定设备刚刚对接在无线充电器上,则为真。
     */
public boolean update(boolean isPowered, int plugType) {
    synchronized (mLock) {
        final boolean wasPoweredWirelessly = mPoweredWirelessly;

        if (isPowered && plugType == BatteryManager.BATTERY_PLUGGED_WIRELESS) {
            // 设备正在从无线充电器接收电源。 异步更新静止位置。
            mPoweredWirelessly = true;
            mMustUpdateRestPosition = true;
            startDetectionLocked();
        } else {
            // 设备可能在也可能不在无线充电器上,具体取决于我们收到的拔出信号是否是虚假的。
            mPoweredWirelessly = false;
            if (mAtRest) {
                if (plugType != 0 && plugType != BatteryManager.BATTERY_PLUGGED_WIRELESS) {
                    // 该设备已插入新的非无线电源。 可以安全地假设它不再在无线充电器上。
                    mMustUpdateRestPosition = false;
                    clearAtRestLocked();
                } else {
                    // 该设备可能仍在无线充电器上,但我们不知道。 检查设备是否在充电器上保持静止,
                    //以便我们知道在需要时忽略下一个无线插头事件。
                    startDetectionLocked();
                }
            }
        }

        // 仅当设备刚刚开始以无线方式接收电源并且不知道设备之前已经在无线充电器上静止时,才报告设备已对接。
        return mPoweredWirelessly && !wasPoweredWirelessly && !mAtRest;
    }
}

最终的返回值由三个值决定,添加log查看这三个值的状态

最终确定为wasPoweredWirelessly的值为true导致不会显示充电动画及铃声

因为短距离抬起手机此时手机的plugType还并没有刷新还是处于BatteryManager.BATTERY_PLUGGED_WIRELESS状态,因此wasPoweredWirelessly的值一直为true,所以这个状态下不会显示充电动画及铃声

你可能感兴趣的:(Android,android)