From a9933abf479359a3825e1d391cadc2c0fb403ee9 Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Tue, 16 Jun 2026 10:18:46 +1000 Subject: [PATCH 1/7] feat(windows): add network connectivity tool This adds a helper unit which uses the windows API to obtain a ConnectionProfile. This profile provides information about the connection status and connectivity statistics. This includes the connection costs, roaming, restricted etc. It also can check to see if background networking activity has been restricted. Using the recommened example this commit combines these to determine if a network is metered. It also provides access to the background network being restricted. Fixes: #13566 --- windows/src/desktop/kmshell/kmshell.dpr | 3 +- windows/src/desktop/kmshell/kmshell.dproj | 15 +-- ...yman.Configuration.UI.UfrmStartInstall.dfm | 30 +++++- ...yman.Configuration.UI.UfrmStartInstall.pas | 14 ++- .../main/Keyman.System.UpdateStateMachine.pas | 12 ++- windows/src/desktop/kmshell/main/UfrmMain.pas | 8 +- .../kmshell/util/UtilNetworkConnection.pas | 94 +++++++++++++++++++ windows/src/desktop/kmshell/xml/strings.xml | 7 +- 8 files changed, 165 insertions(+), 18 deletions(-) create mode 100644 windows/src/desktop/kmshell/util/UtilNetworkConnection.pas diff --git a/windows/src/desktop/kmshell/kmshell.dpr b/windows/src/desktop/kmshell/kmshell.dpr index 0f6bf22dd52..214c8c86a3b 100644 --- a/windows/src/desktop/kmshell/kmshell.dpr +++ b/windows/src/desktop/kmshell/kmshell.dpr @@ -182,7 +182,8 @@ uses Keyman.System.UpdateStateMachine in 'main\Keyman.System.UpdateStateMachine.pas', Keyman.System.DownloadUpdate in 'main\Keyman.System.DownloadUpdate.pas', Keyman.System.ExecutionHistory in '..\..\..\..\common\windows\delphi\general\Keyman.System.ExecutionHistory.pas', - Keyman.Configuration.UI.UfrmStartInstall in 'main\Keyman.Configuration.UI.UfrmStartInstall.pas' {frmStartInstall}; + Keyman.Configuration.UI.UfrmStartInstall in 'main\Keyman.Configuration.UI.UfrmStartInstall.pas' {frmStartInstall}, + UtilNetworkConnection in 'util\UtilNetworkConnection.pas'; {$R VERSION.RES} {$R manifest.res} diff --git a/windows/src/desktop/kmshell/kmshell.dproj b/windows/src/desktop/kmshell/kmshell.dproj index ec44fc98ec4..fbe3df57027 100644 --- a/windows/src/desktop/kmshell/kmshell.dproj +++ b/windows/src/desktop/kmshell/kmshell.dproj @@ -357,6 +357,7 @@
frmStartInstall
+ Cfg_2 @@ -418,27 +419,21 @@ False - - - kmshell.rsm - true - - kmshell.exe true - + - kmshell.exe + .\ true - + - .\ + kmshell.rsm true diff --git a/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.dfm b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.dfm index 2f441431384..9bd38186315 100644 --- a/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.dfm +++ b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.dfm @@ -20,7 +20,7 @@ object frmStartInstall: TfrmStartInstall object lblUpdateMessage: TLabel Left = 64 Top = 89 - Width = 313 + Width = 303 Height = 34 Caption = 'An update to Keyman has been downloaded and is ready to install.' Font.Charset = DEFAULT_CHARSET @@ -146,6 +146,34 @@ object frmStartInstall: TfrmStartInstall 414B8B49C3A0A5C5A461D0D262FA3F82D7F60E256D51F10000000049454E44AE 426082} end + object shpMeteredWarning: TShape + Left = 60 + Top = 149 + Width = 314 + Height = 45 + Brush.Color = 15132415 + Pen.Color = clRed + Visible = False + end + object lblMeteredWarning: TLabel + Left = 64 + Top = 150 + Width = 303 + Height = 43 + AutoSize = False + Caption = 'Metered Warning' + Color = 15132415 + Font.Charset = DEFAULT_CHARSET + Font.Color = clWindowText + Font.Height = -13 + Font.Name = 'Segoe UI' + Font.Style = [] + ParentColor = False + ParentFont = False + Transparent = False + Visible = False + WordWrap = True + end object cmdInstall: TButton Left = 229 Top = 200 diff --git a/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.pas b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.pas index be1dbe31aad..dcf5bf0f3f9 100644 --- a/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.pas +++ b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.pas @@ -19,7 +19,9 @@ interface Winapi.Messages, Winapi.Windows, UfrmKeymanBase, - UserMessages, Vcl.Imaging.pngimage; + UserMessages, + UtilNetworkConnection, + Vcl.Imaging.pngimage; type TfrmStartInstall = class(TfrmKeymanBase) @@ -27,6 +29,8 @@ TfrmStartInstall = class(TfrmKeymanBase) cmdLater: TButton; lblUpdateMessage: TLabel; imgKeymanLogo: TImage; + shpMeteredWarning: TShape; + lblMeteredWarning: TLabel; procedure FormCreate(Sender: TObject); private FRestartRequired: Boolean; @@ -48,6 +52,8 @@ constructor TfrmStartInstall.Create(AOwner: TComponent; const RestartRequired: B end; procedure TfrmStartInstall.FormCreate(Sender: TObject); +var + IsMetered: Boolean; begin inherited; cmdInstall.Caption := MsgFromId(S_Update_Now); @@ -56,6 +62,12 @@ procedure TfrmStartInstall.FormCreate(Sender: TObject); lblUpdateMessage.Caption := MsgFromId(S_Update_Restart_Req) else lblUpdateMessage.Caption := MsgFromId(S_Ready_To_Install); + + IsMetered := UtilNetworkConnection.IsMetered; + lblMeteredWarning.Visible := IsMetered; + shpMeteredWarning.Visible := IsMetered; + if IsMetered then + lblMeteredWarning.Caption := MsgFromId(S_Metered_Warning); end; end. diff --git a/windows/src/desktop/kmshell/main/Keyman.System.UpdateStateMachine.pas b/windows/src/desktop/kmshell/main/Keyman.System.UpdateStateMachine.pas index c50ecf14db9..b6209fcf2c4 100644 --- a/windows/src/desktop/kmshell/main/Keyman.System.UpdateStateMachine.pas +++ b/windows/src/desktop/kmshell/main/Keyman.System.UpdateStateMachine.pas @@ -16,7 +16,8 @@ interface KeymanPaths, Keyman.System.ExecutionHistory, Keyman.System.UpdateCheckResponse, - utilkmshell; + utilkmshell, + UtilNetworkConnection; type EUpdateStateMachine = class(Exception); @@ -719,7 +720,9 @@ procedure UpdateAvailableState.EnterState; begin // Enter UpdateAvailableState bucStateContext.SetRegistryState(usUpdateAvailable); - if bucStateContext.FAutomaticUpdate then + + if bucStateContext.FAutomaticUpdate and + not UtilNetworkConnection.IsBackgroundUpdateAllowed then begin StartDownloadProcess; end; @@ -751,7 +754,8 @@ procedure UpdateAvailableState.HandleCheck; function UpdateAvailableState.HandleKmShell; begin - if bucStateContext.FAutomaticUpdate then + if bucStateContext.FAutomaticUpdate and + not UtilNetworkConnection.IsBackgroundUpdateAllowed then begin // we will use a new kmshell process to enable // the download as background process. @@ -773,6 +777,8 @@ procedure UpdateAvailableState.HandleAbort; procedure UpdateAvailableState.HandleInstallNow; begin + // This is deliberate action therefore no + // need to check if background update is allowed. bucStateContext.SetApplyNow(True); // A new kmshell process will be used to download StartDownloadProcess; diff --git a/windows/src/desktop/kmshell/main/UfrmMain.pas b/windows/src/desktop/kmshell/main/UfrmMain.pas index 6a631fb5461..cbf4032bc58 100644 --- a/windows/src/desktop/kmshell/main/UfrmMain.pas +++ b/windows/src/desktop/kmshell/main/UfrmMain.pas @@ -212,6 +212,7 @@ implementation utilexecute, utilkmshell, utilhttp, + UtilNetworkConnection, utiluac, utilxml, KeymanPaths; @@ -817,10 +818,15 @@ procedure TfrmMain.Update_ApplyNow; ShellPath : string; FResult, InstallNow: Boolean; frmStartInstallNow: TfrmStartInstall; + IsMetered: Boolean; begin InstallNow := True; // Confirm User is ok that this will require a reset - if HasKeymanRun then + // The metered warning has been added to the update pop up. In the future + // it would be better to have this warning in a banner on the configuration + // page. + IsMetered := UtilNetworkConnection.IsMetered; + if HasKeymanRun OR IsMetered then begin frmStartInstallNow := TfrmStartInstall.Create(nil, true); try diff --git a/windows/src/desktop/kmshell/util/UtilNetworkConnection.pas b/windows/src/desktop/kmshell/util/UtilNetworkConnection.pas new file mode 100644 index 00000000000..e5ade9826e9 --- /dev/null +++ b/windows/src/desktop/kmshell/util/UtilNetworkConnection.pas @@ -0,0 +1,94 @@ +(* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Notes: Enable checking for metered connection and background data restrictions. +*) +unit UtilNetworkConnection; + +interface + +(** + * Checks if the current internet connection is restricted, roaming, or over its + * data limit. + * This learn microsoft article shows how to combine network costs to determine + * if the connection is metered. + * https://learn.microsoft.com/en-us/uwp/api/windows.networking.connectivity.connectionprofile?view=winrt-28000 + * + * @returns True if the connection is metered, False otherwise. + *) +function IsMetered: Boolean; + +(** + * Checks if background data usage is explicitly restricted by the current network profile. + * + * @returns True if background data usage is restricted, False otherwise. + *) +function IsBackgroundDataRestricted: Boolean; + +(** + * Determines whether background updates are allowed. + * + * @returns True if updates can proceed, False if blocked by network constraints. + * + * Note: Currently this checks for metered connection OR background + data usage restricted. If a configuration item is added that + provides the option to download on metered connections then + this should be updated to include that logic + *) +function IsBackgroundUpdateAllowed: Boolean; + +implementation + +uses + System.SysUtils, + Winapi.CommonTypes, + Winapi.WinRT, + Winapi.Networking.Connectivity; + +function IsMetered: Boolean; +var + Profile: IConnectionProfile; + CostLevel: IConnectionCost; +begin + Result := False; + // Get the profile currently providing internet access + Profile := TNetworkInformation.GetInternetConnectionProfile; + + if Profile <> nil then + begin + CostLevel := Profile.GetConnectionCost; + Result := (CostLevel.NetworkCostType <> NetworkCostType.Unrestricted) + or CostLevel.Roaming + or CostLevel.OverDataLimit; + end; +end; + +function IsBackgroundDataRestricted: Boolean; +var + Profile: IConnectionProfile; + CostLevel: IConnectionCost; + DataRestriction: IConnectionCost2; +begin + Result := False; + Profile := TNetworkInformation.GetInternetConnectionProfile; + if Profile <> nil then + begin + CostLevel := Profile.GetConnectionCost; + if (CostLevel <> nil) and Supports(CostLevel, IConnectionCost2, DataRestriction) then + begin + Result := DataRestriction.BackgroundDataUsageRestricted; + Exit; + end; + end; +end; + +// Currently this checks for metered connection OR background +// data usage restricted. If a configuration item is added that +// provides the option to download on metered connections then +// this should be updated to include that logic +function IsBackgroundUpdateAllowed: Boolean; +begin + Result := IsMetered OR IsBackgroundDataRestricted; +end; + +end. diff --git a/windows/src/desktop/kmshell/xml/strings.xml b/windows/src/desktop/kmshell/xml/strings.xml index 49287614482..318b33f483d 100644 --- a/windows/src/desktop/kmshell/xml/strings.xml +++ b/windows/src/desktop/kmshell/xml/strings.xml @@ -608,11 +608,16 @@ Installing the update now will require a restart of your computer before you can use Keyman again. Update now anyway? - + An update to Keyman has been downloaded and is ready to install. + + + + You\'re on a metered connection. Downloading now may incur data charges. + From 94756fd850671145fffaf9fcf419d510bfde803c Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Wed, 17 Jun 2026 11:41:09 +1000 Subject: [PATCH 2/7] feat(windows): add intstall ready flag to pop up If the download has already occured before calling the pop up then there is no reason to warn about the metered connection. --- .../Keyman.Configuration.UI.UfrmStartInstall.pas | 16 ++++++++++++---- windows/src/desktop/kmshell/main/initprog.pas | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.pas b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.pas index dcf5bf0f3f9..e5b25c20525 100644 --- a/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.pas +++ b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.pas @@ -34,8 +34,12 @@ TfrmStartInstall = class(TfrmKeymanBase) procedure FormCreate(Sender: TObject); private FRestartRequired: Boolean; + FReadyToInstall: Boolean; public - constructor Create(AOwner: TComponent; const RestartRequired: Boolean); reintroduce; + constructor Create( + AOwner: TComponent; + const RestartRequired: Boolean; + const ReadyToInstall: Boolean = False); reintroduce; end; implementation @@ -45,10 +49,14 @@ implementation {$R *.dfm} -constructor TfrmStartInstall.Create(AOwner: TComponent; const RestartRequired: Boolean); +constructor TfrmStartInstall.Create( + AOwner: TComponent; + const RestartRequired: Boolean; + const ReadyToInstall: Boolean = False); begin inherited Create(AOwner); FRestartRequired := RestartRequired; + FReadyToInstall := ReadyToInstall; end; procedure TfrmStartInstall.FormCreate(Sender: TObject); @@ -64,8 +72,8 @@ procedure TfrmStartInstall.FormCreate(Sender: TObject); lblUpdateMessage.Caption := MsgFromId(S_Ready_To_Install); IsMetered := UtilNetworkConnection.IsMetered; - lblMeteredWarning.Visible := IsMetered; - shpMeteredWarning.Visible := IsMetered; + lblMeteredWarning.Visible := IsMetered and not FReadyToInstall; + shpMeteredWarning.Visible := IsMetered and not FReadyToInstall; if IsMetered then lblMeteredWarning.Caption := MsgFromId(S_Metered_Warning); end; diff --git a/windows/src/desktop/kmshell/main/initprog.pas b/windows/src/desktop/kmshell/main/initprog.pas index 359130f27e7..81c2fc7c827 100644 --- a/windows/src/desktop/kmshell/main/initprog.pas +++ b/windows/src/desktop/kmshell/main/initprog.pas @@ -677,7 +677,7 @@ function ShouldSendToBUpdateSM(FSilent: Boolean; BUpdateSM: TUpdateStateMachine; (FMode in [fmStart, fmSplash, fmMain, fmAbout, fmHelp, fmShowHelp, fmSettings, fmBoot]) then begin - frmStartInstall := TfrmStartInstall.Create(nil, false); + frmStartInstall := TfrmStartInstall.Create(nil, false, ValidateReadyToInstall); try Result := frmStartInstall.ShowModal = mrOk; finally From 293f8ec47a2480d01771562a19b5fca7f1ea5257 Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Tue, 23 Jun 2026 11:41:02 +1000 Subject: [PATCH 3/7] feat(windows): apply suggestions from code review Co-authored-by: Eberhard Beilharz --- .../kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.pas | 2 ++ 1 file changed, 2 insertions(+) diff --git a/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.pas b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.pas index e5b25c20525..f9a6ec630fc 100644 --- a/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.pas +++ b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.pas @@ -72,6 +72,8 @@ procedure TfrmStartInstall.FormCreate(Sender: TObject); lblUpdateMessage.Caption := MsgFromId(S_Ready_To_Install); IsMetered := UtilNetworkConnection.IsMetered; + // Show warning if on a metered connection. If FReadyToInstall the update is + // already downloaded, so no use in displaying the warning. lblMeteredWarning.Visible := IsMetered and not FReadyToInstall; shpMeteredWarning.Visible := IsMetered and not FReadyToInstall; if IsMetered then From 5e26d5dd08a609f39449ca244b0016dd1cbb31f4 Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Tue, 23 Jun 2026 15:56:34 +1000 Subject: [PATCH 4/7] feat(windows): address review comments --- .../Keyman.Configuration.UI.UfrmStartInstall.pas | 16 ++++++++-------- .../main/Keyman.System.UpdateStateMachine.pas | 4 ++-- windows/src/desktop/kmshell/main/UfrmMain.pas | 10 +++++----- windows/src/desktop/kmshell/main/initprog.pas | 5 ++++- .../kmshell/util/UtilNetworkConnection.pas | 8 ++++---- 5 files changed, 23 insertions(+), 20 deletions(-) diff --git a/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.pas b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.pas index f9a6ec630fc..28e456f6917 100644 --- a/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.pas +++ b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.pas @@ -20,7 +20,6 @@ interface Winapi.Windows, UfrmKeymanBase, UserMessages, - UtilNetworkConnection, Vcl.Imaging.pngimage; type @@ -34,11 +33,13 @@ TfrmStartInstall = class(TfrmKeymanBase) procedure FormCreate(Sender: TObject); private FRestartRequired: Boolean; + FIsMetered: Boolean; FReadyToInstall: Boolean; public constructor Create( AOwner: TComponent; const RestartRequired: Boolean; + const IsMetered: Boolean; const ReadyToInstall: Boolean = False); reintroduce; end; @@ -52,16 +53,16 @@ implementation constructor TfrmStartInstall.Create( AOwner: TComponent; const RestartRequired: Boolean; + const IsMetered: Boolean; const ReadyToInstall: Boolean = False); begin inherited Create(AOwner); FRestartRequired := RestartRequired; + FIsMetered := IsMetered; FReadyToInstall := ReadyToInstall; end; procedure TfrmStartInstall.FormCreate(Sender: TObject); -var - IsMetered: Boolean; begin inherited; cmdInstall.Caption := MsgFromId(S_Update_Now); @@ -71,12 +72,11 @@ procedure TfrmStartInstall.FormCreate(Sender: TObject); else lblUpdateMessage.Caption := MsgFromId(S_Ready_To_Install); - IsMetered := UtilNetworkConnection.IsMetered; - // Show warning if on a metered connection. If FReadyToInstall the update is + // Show warning if on a metered connection. If FReadyToInstall the update is // already downloaded, so no use in displaying the warning. - lblMeteredWarning.Visible := IsMetered and not FReadyToInstall; - shpMeteredWarning.Visible := IsMetered and not FReadyToInstall; - if IsMetered then + lblMeteredWarning.Visible := FIsMetered and not FReadyToInstall; + shpMeteredWarning.Visible := FIsMetered and not FReadyToInstall; + if FIsMetered then lblMeteredWarning.Caption := MsgFromId(S_Metered_Warning); end; diff --git a/windows/src/desktop/kmshell/main/Keyman.System.UpdateStateMachine.pas b/windows/src/desktop/kmshell/main/Keyman.System.UpdateStateMachine.pas index b6209fcf2c4..5c9e05e30a6 100644 --- a/windows/src/desktop/kmshell/main/Keyman.System.UpdateStateMachine.pas +++ b/windows/src/desktop/kmshell/main/Keyman.System.UpdateStateMachine.pas @@ -722,7 +722,7 @@ procedure UpdateAvailableState.EnterState; bucStateContext.SetRegistryState(usUpdateAvailable); if bucStateContext.FAutomaticUpdate and - not UtilNetworkConnection.IsBackgroundUpdateAllowed then + not UtilNetworkConnection.IsBackgroundUpdateBlocked then begin StartDownloadProcess; end; @@ -755,7 +755,7 @@ procedure UpdateAvailableState.HandleCheck; function UpdateAvailableState.HandleKmShell; begin if bucStateContext.FAutomaticUpdate and - not UtilNetworkConnection.IsBackgroundUpdateAllowed then + not UtilNetworkConnection.IsBackgroundUpdateBlocked then begin // we will use a new kmshell process to enable // the download as background process. diff --git a/windows/src/desktop/kmshell/main/UfrmMain.pas b/windows/src/desktop/kmshell/main/UfrmMain.pas index cbf4032bc58..485a199372e 100644 --- a/windows/src/desktop/kmshell/main/UfrmMain.pas +++ b/windows/src/desktop/kmshell/main/UfrmMain.pas @@ -821,14 +821,14 @@ procedure TfrmMain.Update_ApplyNow; IsMetered: Boolean; begin InstallNow := True; - // Confirm User is ok that this will require a reset - // The metered warning has been added to the update pop up. In the future - // it would be better to have this warning in a banner on the configuration - // page. IsMetered := UtilNetworkConnection.IsMetered; + // If a restarted is required (HasKeymanRun == True) + // OR it is a Metered connection warn the user and allow + // them to cancel their request to Install Now. + // Otherwise start installing. if HasKeymanRun OR IsMetered then begin - frmStartInstallNow := TfrmStartInstall.Create(nil, true); + frmStartInstallNow := TfrmStartInstall.Create(nil, HasKeymanRun, IsMetered); try if frmStartInstallNow.ShowModal = mrOk then InstallNow := True diff --git a/windows/src/desktop/kmshell/main/initprog.pas b/windows/src/desktop/kmshell/main/initprog.pas index 81c2fc7c827..e5b02c95f4b 100644 --- a/windows/src/desktop/kmshell/main/initprog.pas +++ b/windows/src/desktop/kmshell/main/initprog.pas @@ -145,6 +145,7 @@ implementation UILanguages, uninstall, UpgradeMnemonicLayout, + UtilNetworkConnection, utilfocusappwnd, utilkmshell, Keyman.System.UpdateStateMachine, @@ -669,6 +670,7 @@ function ShouldSendToBUpdateSM(FSilent: Boolean; BUpdateSM: TUpdateStateMachine; // UI elements from the state machine we have bring some of logic here. var frmStartInstall: TfrmStartInstall; + IsMetered: Boolean; ValidateReadyToInstall: Boolean; begin ValidateReadyToInstall := BUpdateSM.ValidateReadyToInstall; @@ -677,7 +679,8 @@ function ShouldSendToBUpdateSM(FSilent: Boolean; BUpdateSM: TUpdateStateMachine; (FMode in [fmStart, fmSplash, fmMain, fmAbout, fmHelp, fmShowHelp, fmSettings, fmBoot]) then begin - frmStartInstall := TfrmStartInstall.Create(nil, false, ValidateReadyToInstall); + IsMetered := UtilNetworkConnection.IsMetered; + frmStartInstall := TfrmStartInstall.Create(nil, false, IsMetered, ValidateReadyToInstall); try Result := frmStartInstall.ShowModal = mrOk; finally diff --git a/windows/src/desktop/kmshell/util/UtilNetworkConnection.pas b/windows/src/desktop/kmshell/util/UtilNetworkConnection.pas index e5ade9826e9..ad3615f1cc3 100644 --- a/windows/src/desktop/kmshell/util/UtilNetworkConnection.pas +++ b/windows/src/desktop/kmshell/util/UtilNetworkConnection.pas @@ -26,16 +26,16 @@ function IsMetered: Boolean; function IsBackgroundDataRestricted: Boolean; (** - * Determines whether background updates are allowed. + * Determines whether background updates are blocked. * - * @returns True if updates can proceed, False if blocked by network constraints. + * @returns True if background updates are blocked, False if allowed. * * Note: Currently this checks for metered connection OR background data usage restricted. If a configuration item is added that provides the option to download on metered connections then this should be updated to include that logic *) -function IsBackgroundUpdateAllowed: Boolean; +function IsBackgroundUpdateBlocked: Boolean; implementation @@ -86,7 +86,7 @@ function IsBackgroundDataRestricted: Boolean; // data usage restricted. If a configuration item is added that // provides the option to download on metered connections then // this should be updated to include that logic -function IsBackgroundUpdateAllowed: Boolean; +function IsBackgroundUpdateBlocked: Boolean; begin Result := IsMetered OR IsBackgroundDataRestricted; end; From 3974f035f9ba1b3fcb327770a4714aa117a47c96 Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Thu, 25 Jun 2026 10:49:35 +1000 Subject: [PATCH 5/7] feat(windows): use enum to drive installfrm layout --- ...yman.Configuration.UI.UfrmStartInstall.pas | 75 ++++++++++++------- windows/src/desktop/kmshell/main/UfrmMain.pas | 34 +++++++-- windows/src/desktop/kmshell/main/initprog.pas | 6 +- 3 files changed, 79 insertions(+), 36 deletions(-) diff --git a/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.pas b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.pas index 28e456f6917..d30cf44c6ab 100644 --- a/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.pas +++ b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.pas @@ -1,7 +1,5 @@ { Keyman is copyright (C) SIL Global. MIT License. - - // TODO: #12887 Localise all the labels and captions. } unit Keyman.Configuration.UI.UfrmStartInstall; interface @@ -23,6 +21,15 @@ interface Vcl.Imaging.pngimage; type + // The 4 valid installation form scenarios + TInstallCase = ( + icNone, // Not a valid case, can be used as check before calling creating form + icRestartRequiredMetered, + icRestartRequiredNotMetered, + icReadyToInstallNotMetered, // Metered warning never needed if ReadyToInstall + icNoInstallMessageMetered + ); + TfrmStartInstall = class(TfrmKeymanBase) cmdInstall: TButton; cmdLater: TButton; @@ -32,18 +39,15 @@ TfrmStartInstall = class(TfrmKeymanBase) lblMeteredWarning: TLabel; procedure FormCreate(Sender: TObject); private - FRestartRequired: Boolean; - FIsMetered: Boolean; - FReadyToInstall: Boolean; + FScenario: TInstallCase; public - constructor Create( - AOwner: TComponent; - const RestartRequired: Boolean; - const IsMetered: Boolean; - const ReadyToInstall: Boolean = False); reintroduce; + constructor Create( + AOwner: TComponent; + const AScenario: TInstallCase); reintroduce; end; implementation + uses MessageIdentifiers, MessageIdentifierConsts; @@ -52,14 +56,11 @@ implementation constructor TfrmStartInstall.Create( AOwner: TComponent; - const RestartRequired: Boolean; - const IsMetered: Boolean; - const ReadyToInstall: Boolean = False); + const AScenario: TInstallCase); begin + Assert(AScenario <> icNone, 'Invalid install case'); + FScenario := AScenario; inherited Create(AOwner); - FRestartRequired := RestartRequired; - FIsMetered := IsMetered; - FReadyToInstall := ReadyToInstall; end; procedure TfrmStartInstall.FormCreate(Sender: TObject); @@ -67,17 +68,39 @@ procedure TfrmStartInstall.FormCreate(Sender: TObject); inherited; cmdInstall.Caption := MsgFromId(S_Update_Now); cmdLater.Caption := MsgFromId(S_Later); - if FRestartRequired then - lblUpdateMessage.Caption := MsgFromId(S_Update_Restart_Req) - else - lblUpdateMessage.Caption := MsgFromId(S_Ready_To_Install); - // Show warning if on a metered connection. If FReadyToInstall the update is - // already downloaded, so no use in displaying the warning. - lblMeteredWarning.Visible := FIsMetered and not FReadyToInstall; - shpMeteredWarning.Visible := FIsMetered and not FReadyToInstall; - if FIsMetered then - lblMeteredWarning.Caption := MsgFromId(S_Metered_Warning); + // Default UI configuration state - metered warnings hidden initially + lblUpdateMessage.Visible := True; + lblMeteredWarning.Visible := False; + shpMeteredWarning.Visible := False; + + case FScenario of + icRestartRequiredMetered: + begin + lblUpdateMessage.Caption := MsgFromId(S_Update_Restart_Req); + lblMeteredWarning.Caption := MsgFromId(S_Metered_Warning); + lblMeteredWarning.Visible := True; + shpMeteredWarning.Visible := True; + end; + + icRestartRequiredNotMetered: + begin + lblUpdateMessage.Caption := MsgFromId(S_Update_Restart_Req); + end; + + icReadyToInstallNotMetered: + begin + lblUpdateMessage.Caption := MsgFromId(S_Ready_To_Install); + end; + + icNoInstallMessageMetered: + begin + lblUpdateMessage.Visible := False; + lblMeteredWarning.Caption := MsgFromId(S_Metered_Warning); + lblMeteredWarning.Visible := True; + shpMeteredWarning.Visible := True; + end; + end; end; end. diff --git a/windows/src/desktop/kmshell/main/UfrmMain.pas b/windows/src/desktop/kmshell/main/UfrmMain.pas index 485a199372e..b34007e0d1b 100644 --- a/windows/src/desktop/kmshell/main/UfrmMain.pas +++ b/windows/src/desktop/kmshell/main/UfrmMain.pas @@ -819,16 +819,33 @@ procedure TfrmMain.Update_ApplyNow; FResult, InstallNow: Boolean; frmStartInstallNow: TfrmStartInstall; IsMetered: Boolean; + EInstallScenario: TInstallCase; begin InstallNow := True; IsMetered := UtilNetworkConnection.IsMetered; + // If a restarted is required (HasKeymanRun == True) // OR it is a Metered connection warn the user and allow // them to cancel their request to Install Now. - // Otherwise start installing. - if HasKeymanRun OR IsMetered then + // Otherwise start installing with out pop-up warnings. + EInstallScenario := TInstallCase.icNone; + if HasKeymanRun and not IsMetered then + begin + EInstallScenario := TInstallCase.icRestartRequiredNotMetered; + end + else if HasKeymanRun and IsMetered then + begin + EInstallScenario := TInstallCase.icRestartRequiredMetered; + end + else if (not HasKeymanRun) and IsMetered then + begin + EInstallScenario := TInstallCase.icNoInstallMessageMetered; + end; + + // Render dialog if conditions require it + if EInstallScenario <> TInstallCase.icNone then begin - frmStartInstallNow := TfrmStartInstall.Create(nil, HasKeymanRun, IsMetered); + frmStartInstallNow := TfrmStartInstall.Create(nil, EInstallScenario); try if frmStartInstallNow.ShowModal = mrOk then InstallNow := True @@ -839,18 +856,23 @@ procedure TfrmMain.Update_ApplyNow; end; end; - if InstallNow = True then + // Process installation execution execution path + if InstallNow then begin ShellPath := TKeymanPaths.KeymanDesktopInstallPath(TKeymanPaths.S_KMShell); FResult := TUtilExecute.Shell(0, ShellPath, '', '-an'); if not FResult then + begin TKeymanSentryClient.Client.MessageEvent(Sentry.Client.SENTRY_LEVEL_ERROR, - 'TrmfMain: Shell Execute Update_ApplyNow Failed') + 'TrmfMain: Shell Execute Update_ApplyNow Failed'); + end else - ModalResult := mrAbort; + begin // If a splash screen is currently open when "Install Now" is executed, // setting mrAbort ensures the splash screen is closed on the // return of "Keyman Configuration". + ModalResult := mrAbort; + end; end; end; diff --git a/windows/src/desktop/kmshell/main/initprog.pas b/windows/src/desktop/kmshell/main/initprog.pas index e5b02c95f4b..997b791dafb 100644 --- a/windows/src/desktop/kmshell/main/initprog.pas +++ b/windows/src/desktop/kmshell/main/initprog.pas @@ -145,7 +145,6 @@ implementation UILanguages, uninstall, UpgradeMnemonicLayout, - UtilNetworkConnection, utilfocusappwnd, utilkmshell, Keyman.System.UpdateStateMachine, @@ -670,7 +669,6 @@ function ShouldSendToBUpdateSM(FSilent: Boolean; BUpdateSM: TUpdateStateMachine; // UI elements from the state machine we have bring some of logic here. var frmStartInstall: TfrmStartInstall; - IsMetered: Boolean; ValidateReadyToInstall: Boolean; begin ValidateReadyToInstall := BUpdateSM.ValidateReadyToInstall; @@ -679,8 +677,8 @@ function ShouldSendToBUpdateSM(FSilent: Boolean; BUpdateSM: TUpdateStateMachine; (FMode in [fmStart, fmSplash, fmMain, fmAbout, fmHelp, fmShowHelp, fmSettings, fmBoot]) then begin - IsMetered := UtilNetworkConnection.IsMetered; - frmStartInstall := TfrmStartInstall.Create(nil, false, IsMetered, ValidateReadyToInstall); + // We are ready to install Metered warning not needed even if on Metered connection + frmStartInstall := TfrmStartInstall.Create(nil, TInstallCase.icReadyToInstallNotMetered); try Result := frmStartInstall.ShowModal = mrOk; finally From 85da9dc03c7953e8a1e176dce0d32b2aabff1ef0 Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Thu, 25 Jun 2026 10:57:40 +1000 Subject: [PATCH 6/7] feat(windows): apply suggestions Co-authored-by: Eberhard Beilharz --- windows/src/desktop/kmshell/main/UfrmMain.pas | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/src/desktop/kmshell/main/UfrmMain.pas b/windows/src/desktop/kmshell/main/UfrmMain.pas index b34007e0d1b..c68a7d189ac 100644 --- a/windows/src/desktop/kmshell/main/UfrmMain.pas +++ b/windows/src/desktop/kmshell/main/UfrmMain.pas @@ -824,7 +824,7 @@ procedure TfrmMain.Update_ApplyNow; InstallNow := True; IsMetered := UtilNetworkConnection.IsMetered; - // If a restarted is required (HasKeymanRun == True) + // If a restart is required (HasKeymanRun == True) // OR it is a Metered connection warn the user and allow // them to cancel their request to Install Now. // Otherwise start installing with out pop-up warnings. From 448d63e15672be6cbb349ae2fe2400008cf698be Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Thu, 25 Jun 2026 11:31:15 +1000 Subject: [PATCH 7/7] feat(windows): apply exception check Keyman currenlty only supports Windows 10 and 11 so the exception was low risk. If it is not available we return false as metered connections werent possilble before this the functionality will be the consistent with those windows versions. i.e. always updating in the background --- ...yman.Configuration.UI.UfrmStartInstall.pas | 2 +- .../kmshell/util/UtilNetworkConnection.pas | 42 ++++++++++++------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.pas b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.pas index d30cf44c6ab..78578e66c08 100644 --- a/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.pas +++ b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.pas @@ -21,7 +21,7 @@ interface Vcl.Imaging.pngimage; type - // The 4 valid installation form scenarios + // The 4 valid installation form scenarios plus a None case for validation TInstallCase = ( icNone, // Not a valid case, can be used as check before calling creating form icRestartRequiredMetered, diff --git a/windows/src/desktop/kmshell/util/UtilNetworkConnection.pas b/windows/src/desktop/kmshell/util/UtilNetworkConnection.pas index ad3615f1cc3..02952cafb2a 100644 --- a/windows/src/desktop/kmshell/util/UtilNetworkConnection.pas +++ b/windows/src/desktop/kmshell/util/UtilNetworkConnection.pas @@ -51,18 +51,25 @@ function IsMetered: Boolean; CostLevel: IConnectionCost; begin Result := False; - // Get the profile currently providing internet access - Profile := TNetworkInformation.GetInternetConnectionProfile; - - if Profile <> nil then - begin - CostLevel := Profile.GetConnectionCost; - Result := (CostLevel.NetworkCostType <> NetworkCostType.Unrestricted) - or CostLevel.Roaming - or CostLevel.OverDataLimit; + try + // Get the profile currently providing internet access + Profile := TNetworkInformation.GetInternetConnectionProfile; + if Profile <> nil then + begin + CostLevel := Profile.GetConnectionCost; + if CostLevel <> nil then + Result := (CostLevel.NetworkCostType <> NetworkCostType.Unrestricted) + or CostLevel.Roaming + or CostLevel.OverDataLimit; + end; + except + on E: Exception do + // If the WinRT network APIs are unavailable or throw (e.g. unusual + // Windows SKUs, containers, Network List Manager service stopped), + // treat the connection as non-metered so updates are not blocked. + Result := False; end; end; - function IsBackgroundDataRestricted: Boolean; var Profile: IConnectionProfile; @@ -70,15 +77,18 @@ function IsBackgroundDataRestricted: Boolean; DataRestriction: IConnectionCost2; begin Result := False; - Profile := TNetworkInformation.GetInternetConnectionProfile; - if Profile <> nil then - begin - CostLevel := Profile.GetConnectionCost; - if (CostLevel <> nil) and Supports(CostLevel, IConnectionCost2, DataRestriction) then + try + Profile := TNetworkInformation.GetInternetConnectionProfile; + if Profile <> nil then begin + CostLevel := Profile.GetConnectionCost; + if (CostLevel <> nil) and Supports(CostLevel, IConnectionCost2, DataRestriction) then Result := DataRestriction.BackgroundDataUsageRestricted; - Exit; end; + except + on E: Exception do + // See IsMetered: default to not-restricted if the WinRT APIs throw. + Result := False; end; end;