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 @@
+
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;