From 767c0db68a1bfe4283d7fe380eadbadeb044e2c9 Mon Sep 17 00:00:00 2001 From: Bill Lin Date: Thu, 15 Aug 2019 20:42:06 +0800 Subject: Add timeout for displaying 5G icon in SystemUI SystemUI refer CarrierConfig timeout(Second) to show 5G icon gracefully System UI will show the 5G icon and post message delay by config value after the device connect to a 5G cell to make the UX better System UI stop displaying 5G icon when below conditions matched 1. device disconnect from 5G cell 2. the timer is expired. Fixes: 136636141 Bug: 136107473 Test: atest SystemUITests Test: enable SystemUI Demo Mode, simulate mServiceState callback 5G Test: adb shell am broadcast -a com.android.systemui.demo -e command network -e mobile show -e datatype 5g -e level 4 Test: adb shell am broadcast -a com.android.systemui.demo -e command network -e mobile show -e datatype 5g+ -e level 4 Design: go/sysui_5g_signal_timer Change-Id: I5e723c3edf28d31725fa9d847ed3569a1c322739 --- .../statusbar/policy/MobileSignalController.java | 63 +++++++ .../statusbar/policy/NetworkControllerImpl.java | 27 +++ .../policy/NetworkControllerBaseTest.java | 15 ++ .../policy/NetworkControllerDataTest.java | 182 +++++++++++++++++++++ 4 files changed, 287 insertions(+) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java index 7acf4fd27ced..dbfb09f7fc41 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java @@ -21,6 +21,7 @@ import android.database.ContentObserver; import android.net.NetworkCapabilities; import android.os.Handler; import android.os.Looper; +import android.os.Message; import android.provider.Settings.Global; import android.telephony.NetworkRegistrationInfo; import android.telephony.PhoneStateListener; @@ -54,6 +55,10 @@ import java.util.regex.Pattern; public class MobileSignalController extends SignalController< MobileSignalController.MobileState, MobileSignalController.MobileIconGroup> { + + // The message to display Nr5G icon gracfully by CarrierConfig timeout + private static final int MSG_DISPLAY_GRACE = 1; + private final TelephonyManager mPhone; private final SubscriptionDefaults mDefaults; private final String mNetworkNameDefault; @@ -76,8 +81,11 @@ public class MobileSignalController extends SignalController< private SignalStrength mSignalStrength; private MobileIconGroup mDefaultIcons; private Config mConfig; + private final Handler mDisplayGraceHandler; @VisibleForTesting boolean mInflateSignalStrengths = false; + @VisibleForTesting + boolean mIsShowingIconGracefully = false; // Some specific carriers have 5GE network which is special LTE CA network. private static final int NETWORK_TYPE_LTE_CA_5GE = TelephonyManager.MAX_NETWORK_TYPE + 1; @@ -116,6 +124,16 @@ public class MobileSignalController extends SignalController< updateTelephony(); } }; + + mDisplayGraceHandler = new Handler(receiverLooper) { + @Override + public void handleMessage(Message msg) { + if (msg.what == MSG_DISPLAY_GRACE) { + mIsShowingIconGracefully = false; + updateTelephony(); + } + } + }; } public void setConfiguration(Config config) { @@ -479,6 +497,10 @@ public class MobileSignalController extends SignalController< // When the device is camped on a 5G Non-Standalone network, the data network type is still // LTE. In this case, we first check which 5G icon should be shown. MobileIconGroup nr5GIconGroup = getNr5GIconGroup(); + if (mConfig.nrIconDisplayGracePeriodMs > 0) { + nr5GIconGroup = adjustNr5GIconGroupByDisplayGraceTime(nr5GIconGroup); + } + if (nr5GIconGroup != null) { mCurrentState.iconGroup = nr5GIconGroup; } else if (mNetworkToIconLookup.indexOfKey(mDataNetType) >= 0) { @@ -555,6 +577,46 @@ public class MobileSignalController extends SignalController< return null; } + /** + * The function to adjust MobileIconGroup depend on CarrierConfig's time + * nextIconGroup == null imply next state could be 2G/3G/4G/4G+ + * nextIconGroup != null imply next state will be 5G/5G+ + * Flag : mIsShowingIconGracefully + * --------------------------------------------------------------------------------- + * | Last state | Current state | Flag | Action | + * --------------------------------------------------------------------------------- + * | 5G/5G+ | 2G/3G/4G/4G+ | true | return previous IconGroup | + * | 5G/5G+ | 5G/5G+ | true | Bypass | + * | 2G/3G/4G/4G+ | 5G/5G+ | true | Bypass | + * | 2G/3G/4G/4G+ | 2G/3G/4G/4G+ | true | Bypass | + * | SS.connected | SS.disconnect | T|F | Reset timer | + * |NETWORK_TYPE_LTE|!NETWORK_TYPE_LTE| T|F | Reset timer | + * | 5G/5G+ | 2G/3G/4G/4G+ | false| Bypass | + * | 5G/5G+ | 5G/5G+ | false| Bypass | + * | 2G/3G/4G/4G+ | 5G/5G+ | false| SendMessageDelay(time), flag->true | + * | 2G/3G/4G/4G+ | 2G/3G/4G/4G+ | false| Bypass | + * --------------------------------------------------------------------------------- + */ + private MobileIconGroup adjustNr5GIconGroupByDisplayGraceTime( + MobileIconGroup candidateIconGroup) { + if (mIsShowingIconGracefully && candidateIconGroup == null) { + candidateIconGroup = (MobileIconGroup) mCurrentState.iconGroup; + } else if (!mIsShowingIconGracefully && candidateIconGroup != null + && mLastState.iconGroup != candidateIconGroup) { + mDisplayGraceHandler.sendMessageDelayed( + mDisplayGraceHandler.obtainMessage(MSG_DISPLAY_GRACE), + mConfig.nrIconDisplayGracePeriodMs); + mIsShowingIconGracefully = true; + } else if (!mCurrentState.connected || mDataState == TelephonyManager.DATA_DISCONNECTED + || candidateIconGroup == null) { + mDisplayGraceHandler.removeMessages(MSG_DISPLAY_GRACE); + mIsShowingIconGracefully = false; + candidateIconGroup = null; + } + + return candidateIconGroup; + } + private boolean isDataDisabled() { return !mPhone.isDataCapable(); } @@ -580,6 +642,7 @@ public class MobileSignalController extends SignalController< pw.println(" mDataNetType=" + mDataNetType + ","); pw.println(" mInflateSignalStrengths=" + mInflateSignalStrengths + ","); pw.println(" isDataDisabled=" + isDataDisabled() + ","); + pw.println(" mIsShowingIconGracefully=" + mIsShowingIconGracefully + ","); } class MobilePhoneStateListener extends PhoneStateListener { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java index 621b16ab9f41..bb3742198117 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java @@ -51,6 +51,7 @@ import android.telephony.SubscriptionManager; import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener; import android.telephony.TelephonyManager; import android.text.TextUtils; +import android.text.format.DateUtils; import android.util.Log; import android.util.MathUtils; import android.util.SparseArray; @@ -829,6 +830,13 @@ public class NetworkControllerImpl extends BroadcastReceiver pw.print(" mEmergencySource="); pw.println(emergencyToString(mEmergencySource)); + pw.println(" - config ------"); + pw.print(" patternOfCarrierSpecificDataIcon="); + pw.println(mConfig.patternOfCarrierSpecificDataIcon); + pw.print(" nr5GIconMap="); + pw.println(mConfig.nr5GIconMap.toString()); + pw.print(" nrIconDisplayGracePeriodMs="); + pw.println(mConfig.nrIconDisplayGracePeriodMs); for (int i = 0; i < mMobileSignalControllers.size(); i++) { MobileSignalController mobileSignalController = mMobileSignalControllers.valueAt(i); mobileSignalController.dump(pw); @@ -992,6 +1000,8 @@ public class NetworkControllerImpl extends BroadcastReceiver datatype.equals("3g") ? TelephonyIcons.THREE_G : datatype.equals("4g") ? TelephonyIcons.FOUR_G : datatype.equals("4g+") ? TelephonyIcons.FOUR_G_PLUS : + datatype.equals("5g") ? TelephonyIcons.NR_5G : + datatype.equals("5g+") ? TelephonyIcons.NR_5G_PLUS : datatype.equals("e") ? TelephonyIcons.E : datatype.equals("g") ? TelephonyIcons.G : datatype.equals("h") ? TelephonyIcons.H : @@ -1123,6 +1133,7 @@ public class NetworkControllerImpl extends BroadcastReceiver boolean inflateSignalStrengths = false; boolean alwaysShowDataRatIcon = false; public String patternOfCarrierSpecificDataIcon = ""; + public long nrIconDisplayGracePeriodMs; /** * Mapping from NR 5G status string to an integer. The NR 5G status string should match @@ -1175,6 +1186,9 @@ public class NetworkControllerImpl extends BroadcastReceiver add5GIconMapping(pair, config); } } + setDisplayGraceTime( + b.getInt(CarrierConfigManager.KEY_5G_ICON_DISPLAY_GRACE_PERIOD_SEC_INT), + config); } return config; @@ -1208,5 +1222,18 @@ public class NetworkControllerImpl extends BroadcastReceiver TelephonyIcons.ICON_NAME_TO_ICON.get(value)); } } + + /** + * Set display gracefully period time(MS) depend on carrierConfig KEY + * KEY_5G_ICON_DISPLAY_GRACE_PERIOD_SEC_INT, and this function will convert to ms. + * {@link CarrierConfigManager}. + * + * @param time showing 5G icon gracefully in the period of the time(SECOND) + * @param config container that used to store the parsed configs. + */ + @VisibleForTesting + static void setDisplayGraceTime(int time, Config config) { + config.nrIconDisplayGracePeriodMs = time * DateUtils.SECOND_IN_MILLIS; + } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java index e691b7dec0d9..c03f07e59129 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java @@ -30,6 +30,7 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import android.app.Instrumentation; import android.content.Intent; import android.net.ConnectivityManager; import android.net.Network; @@ -48,6 +49,8 @@ import android.testing.TestableLooper; import android.testing.TestableResources; import android.util.Log; +import androidx.test.InstrumentationRegistry; + import com.android.internal.telephony.cdma.EriInfo; import com.android.settingslib.graph.SignalDrawable; import com.android.settingslib.net.DataUsageController; @@ -95,6 +98,7 @@ public class NetworkControllerBaseTest extends SysuiTestCase { protected SubscriptionDefaults mMockSubDefaults; protected DeviceProvisionedController mMockProvisionController; protected DeviceProvisionedListener mUserCallback; + protected Instrumentation mInstrumentation; protected int mSubId; @@ -116,6 +120,7 @@ public class NetworkControllerBaseTest extends SysuiTestCase { @Before public void setUp() throws Exception { + mInstrumentation = InstrumentationRegistry.getInstrumentation(); Settings.Global.putInt(mContext.getContentResolver(), Global.AIRPLANE_MODE_ON, 0); TestableResources res = mContext.getOrCreateTestableResources(); res.addOverride(R.string.cell_data_off_content_description, NO_DATA_STRING); @@ -240,6 +245,16 @@ public class NetworkControllerBaseTest extends SysuiTestCase { NetworkControllerImpl.Config.add5GIconMapping("not_restricted_rrc_idle:5g", mConfig); } + public void setupDefaultNr5GIconDisplayGracePeriodTime_enableThirtySeconds() { + final int enableDisplayGraceTimeSec = 30; + NetworkControllerImpl.Config.setDisplayGraceTime(enableDisplayGraceTimeSec, mConfig); + } + + public void setupDefaultNr5GIconDisplayGracePeriodTime_disabled() { + final int disableDisplayGraceTimeSec = 0; + NetworkControllerImpl.Config.setDisplayGraceTime(disableDisplayGraceTimeSec, mConfig); + } + public void setConnectivityViaBroadcast( int networkType, boolean validated, boolean isConnected) { setConnectivityCommon(networkType, validated, isConnected); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerDataTest.java index f0394da6f479..3ddfbdac6db8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerDataTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerDataTest.java @@ -1,5 +1,7 @@ package com.android.systemui.statusbar.policy; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.anyInt; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -244,6 +246,186 @@ public class NetworkControllerDataTest extends NetworkControllerBaseTest { verifyDataIndicators(TelephonyIcons.ICON_LTE); } + @Test + public void testNr5GIcon_displayGracePeriodTime_enabled() { + setupDefaultNr5GIconConfiguration(); + setupDefaultNr5GIconDisplayGracePeriodTime_enableThirtySeconds(); + setupDefaultSignal(); + mNetworkController.handleConfigurationChanged(); + mPhoneStateListener.onServiceStateChanged(mServiceState); + + ServiceState ss = Mockito.mock(ServiceState.class); + // While nrIconDisplayGracePeriodMs > 0 & is Nr5G, mIsShowingIconGracefully should be true + doReturn(NetworkRegistrationInfo.NR_STATE_CONNECTED).when(ss).getNrState(); + doReturn(ServiceState.FREQUENCY_RANGE_HIGH).when(ss).getNrFrequencyRange(); + mPhoneStateListener.onDataConnectionStateChanged(TelephonyManager.DATA_CONNECTED, + TelephonyManager.NETWORK_TYPE_LTE); + mPhoneStateListener.onServiceStateChanged(ss); + + assertTrue(mConfig.nrIconDisplayGracePeriodMs > 0); + assertTrue(mMobileSignalController.mIsShowingIconGracefully); + } + + @Test + public void testNr5GIcon_displayGracePeriodTime_disabled() { + setupDefaultNr5GIconConfiguration(); + setupDefaultNr5GIconDisplayGracePeriodTime_disabled(); + setupDefaultSignal(); + + assertTrue(mConfig.nrIconDisplayGracePeriodMs == 0); + + // While nrIconDisplayGracePeriodMs <= 0, mIsShowingIconGracefully should be false + doReturn(NetworkRegistrationInfo.NR_STATE_CONNECTED).when(mServiceState).getNrState(); + doReturn(ServiceState.FREQUENCY_RANGE_HIGH).when(mServiceState).getNrFrequencyRange(); + mPhoneStateListener.onDataConnectionStateChanged(TelephonyManager.DATA_CONNECTED, + TelephonyManager.NETWORK_TYPE_LTE); + + assertFalse(mMobileSignalController.mIsShowingIconGracefully); + } + + @Test + public void testNr5GIcon_enableDisplayGracePeriodTime_showIconGracefully() { + setupDefaultNr5GIconConfiguration(); + setupDefaultNr5GIconDisplayGracePeriodTime_enableThirtySeconds(); + setupDefaultSignal(); + mNetworkController.handleConfigurationChanged(); + mPhoneStateListener.onServiceStateChanged(mServiceState); + + ServiceState ss = Mockito.mock(ServiceState.class); + doReturn(NetworkRegistrationInfo.NR_STATE_CONNECTED).when(ss).getNrState(); + doReturn(ServiceState.FREQUENCY_RANGE_HIGH).when(ss).getNrFrequencyRange(); + mPhoneStateListener.onDataConnectionStateChanged(TelephonyManager.DATA_CONNECTED, + TelephonyManager.NETWORK_TYPE_LTE); + mPhoneStateListener.onServiceStateChanged(ss); + + verifyDataIndicators(TelephonyIcons.ICON_5G); + + // Enabled timer Nr5G switch to None Nr5G, showing 5G icon gracefully + ServiceState ssLte = Mockito.mock(ServiceState.class); + doReturn(NetworkRegistrationInfo.NR_STATE_NONE).when(ssLte).getNrState(); + doReturn(ServiceState.FREQUENCY_RANGE_UNKNOWN).when(ssLte).getNrFrequencyRange(); + mPhoneStateListener.onDataConnectionStateChanged(TelephonyManager.DATA_CONNECTED, + TelephonyManager.NETWORK_TYPE_LTE); + mPhoneStateListener.onServiceStateChanged(ssLte); + + verifyDataIndicators(TelephonyIcons.ICON_5G); + } + + @Test + public void testNr5GIcon_disableDisplayGracePeriodTime_showLatestIconImmediately() { + setupDefaultNr5GIconConfiguration(); + setupDefaultNr5GIconDisplayGracePeriodTime_disabled(); + setupDefaultSignal(); + mNetworkController.handleConfigurationChanged(); + + doReturn(NetworkRegistrationInfo.NR_STATE_CONNECTED).when(mServiceState).getNrState(); + doReturn(ServiceState.FREQUENCY_RANGE_HIGH).when(mServiceState).getNrFrequencyRange(); + mPhoneStateListener.onDataConnectionStateChanged(TelephonyManager.DATA_CONNECTED, + TelephonyManager.NETWORK_TYPE_LTE); + + verifyDataIndicators(TelephonyIcons.ICON_5G); + + doReturn(NetworkRegistrationInfo.NR_STATE_NONE).when(mServiceState).getNrState(); + doReturn(ServiceState.FREQUENCY_RANGE_UNKNOWN).when(mServiceState).getNrFrequencyRange(); + mPhoneStateListener.onDataConnectionStateChanged(TelephonyManager.DATA_CONNECTED, + TelephonyManager.NETWORK_TYPE_LTE); + + verifyDataIndicators(TelephonyIcons.ICON_LTE); + } + + @Test + public void testNr5GIcon_resetDisplayGracePeriodTime_whenDataDisconnected() { + setupDefaultNr5GIconConfiguration(); + setupDefaultNr5GIconDisplayGracePeriodTime_enableThirtySeconds(); + setupDefaultSignal(); + mNetworkController.handleConfigurationChanged(); + doReturn(NetworkRegistrationInfo.NR_STATE_CONNECTED).when(mServiceState).getNrState(); + doReturn(ServiceState.FREQUENCY_RANGE_HIGH).when(mServiceState).getNrFrequencyRange(); + mPhoneStateListener.onDataConnectionStateChanged(TelephonyManager.DATA_CONNECTED, + TelephonyManager.NETWORK_TYPE_LTE); + + verifyDataIndicators(TelephonyIcons.ICON_5G); + + // Disabled timer, when out of service, reset timer to display latest state + updateDataConnectionState(TelephonyManager.DATA_CONNECTED, + TelephonyManager.NETWORK_TYPE_LTE); + doReturn(NetworkRegistrationInfo.NR_STATE_NONE).when(mServiceState).getNrState(); + doReturn(ServiceState.FREQUENCY_RANGE_UNKNOWN).when(mServiceState).getNrFrequencyRange(); + mPhoneStateListener.onDataConnectionStateChanged(TelephonyManager.DATA_DISCONNECTED, + TelephonyManager.NETWORK_TYPE_UMTS); + + verifyDataIndicators(0); + } + + @Test + public void testNr5GIcon_enableDisplayGracePeriodTime_show5G_switching_5GPlus() { + setupDefaultNr5GIconConfiguration(); + setupDefaultNr5GIconDisplayGracePeriodTime_enableThirtySeconds(); + setupDefaultSignal(); + mNetworkController.handleConfigurationChanged(); + mPhoneStateListener.onServiceStateChanged(mServiceState); + + ServiceState ss5G = Mockito.mock(ServiceState.class); + doReturn(NetworkRegistrationInfo.NR_STATE_CONNECTED).when(ss5G).getNrState(); + doReturn(ServiceState.FREQUENCY_RANGE_HIGH).when(ss5G).getNrFrequencyRange(); + mPhoneStateListener.onDataConnectionStateChanged(TelephonyManager.DATA_CONNECTED, + TelephonyManager.NETWORK_TYPE_LTE); + mPhoneStateListener.onServiceStateChanged(ss5G); + + verifyDataIndicators(TelephonyIcons.ICON_5G); + + // When timeout enabled, 5G/5G+ switching should be updated immediately + ServiceState ss5GPlus = Mockito.mock(ServiceState.class); + doReturn(NetworkRegistrationInfo.NR_STATE_CONNECTED).when(ss5GPlus).getNrState(); + doReturn(ServiceState.FREQUENCY_RANGE_MMWAVE).when(ss5GPlus).getNrFrequencyRange(); + mPhoneStateListener.onDataConnectionStateChanged(TelephonyManager.DATA_CONNECTED, + TelephonyManager.NETWORK_TYPE_LTE); + mPhoneStateListener.onServiceStateChanged(ss5GPlus); + + verifyDataIndicators(TelephonyIcons.ICON_5G_PLUS); + } + + @Test + public void testNr5GIcon_carrierDisabledDisplayGracePeriodTime_shouldUpdateIconImmediately() { + setupDefaultNr5GIconConfiguration(); + setupDefaultNr5GIconDisplayGracePeriodTime_enableThirtySeconds(); + setupDefaultSignal(); + mNetworkController.handleConfigurationChanged(); + mPhoneStateListener.onServiceStateChanged(mServiceState); + + ServiceState ss5G = Mockito.mock(ServiceState.class); + doReturn(NetworkRegistrationInfo.NR_STATE_CONNECTED).when(ss5G).getNrState(); + doReturn(ServiceState.FREQUENCY_RANGE_HIGH).when(ss5G).getNrFrequencyRange(); + mPhoneStateListener.onDataConnectionStateChanged(TelephonyManager.DATA_CONNECTED, + TelephonyManager.NETWORK_TYPE_LTE); + mPhoneStateListener.onServiceStateChanged(ss5G); + + verifyDataIndicators(TelephonyIcons.ICON_5G); + + // State from NR_5G to NONE NR_5G with timeout, should show previous 5G icon + ServiceState ssLte = Mockito.mock(ServiceState.class); + doReturn(NetworkRegistrationInfo.NR_STATE_NONE).when(ssLte).getNrState(); + doReturn(ServiceState.FREQUENCY_RANGE_UNKNOWN).when(ssLte).getNrFrequencyRange(); + mPhoneStateListener.onDataConnectionStateChanged(TelephonyManager.DATA_CONNECTED, + TelephonyManager.NETWORK_TYPE_LTE); + mPhoneStateListener.onServiceStateChanged(ssLte); + + verifyDataIndicators(TelephonyIcons.ICON_5G); + + // Update nrIconDisplayGracePeriodMs to 0 + setupDefaultNr5GIconDisplayGracePeriodTime_disabled(); + mNetworkController.handleConfigurationChanged(); + + // State from NR_5G to NONE NR_STATE_RESTRICTED, showing corresponding icon + doReturn(NetworkRegistrationInfo.NR_STATE_RESTRICTED).when(mServiceState).getNrState(); + doReturn(TelephonyManager.NETWORK_TYPE_LTE).when(mServiceState).getDataNetworkType(); + mPhoneStateListener.onDataConnectionStateChanged(TelephonyManager.DATA_CONNECTED, + TelephonyManager.NETWORK_TYPE_LTE); + + assertTrue(mConfig.nrIconDisplayGracePeriodMs == 0); + verifyDataIndicators(TelephonyIcons.ICON_LTE); + } + @Test public void testDataDisabledIcon_UserNotSetup() { setupNetworkController(); -- cgit v1.2.3-59-g8ed1b