diff --git a/windows/src/desktop/kmshell/kmshell.dpr b/windows/src/desktop/kmshell/kmshell.dpr
index 0f6bf22dd52..e2d28b88d2b 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},
+ Keyman.Configuration.Util.NetworkConnection in 'util\Keyman.Configuration.Util.NetworkConnection.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..371ce4cd495 100644
--- a/windows/src/desktop/kmshell/kmshell.dproj
+++ b/windows/src/desktop/kmshell/kmshell.dproj
@@ -357,6 +357,7 @@
+
Cfg_2
@@ -418,15 +419,15 @@
False
-
+
- kmshell.rsm
+ .\
true
-
+
- kmshell.exe
+ kmshell.rsm
true
@@ -436,12 +437,6 @@
true
-
-
- .\
- true
-
-
1
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..78578e66c08 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
@@ -19,32 +17,50 @@ interface
Winapi.Messages,
Winapi.Windows,
UfrmKeymanBase,
- UserMessages, Vcl.Imaging.pngimage;
+ UserMessages,
+ Vcl.Imaging.pngimage;
type
+ // 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,
+ icRestartRequiredNotMetered,
+ icReadyToInstallNotMetered, // Metered warning never needed if ReadyToInstall
+ icNoInstallMessageMetered
+ );
+
TfrmStartInstall = class(TfrmKeymanBase)
cmdInstall: TButton;
cmdLater: TButton;
lblUpdateMessage: TLabel;
imgKeymanLogo: TImage;
+ shpMeteredWarning: TShape;
+ lblMeteredWarning: TLabel;
procedure FormCreate(Sender: TObject);
private
- FRestartRequired: Boolean;
+ FScenario: TInstallCase;
public
- constructor Create(AOwner: TComponent; const RestartRequired: Boolean); reintroduce;
+ constructor Create(
+ AOwner: TComponent;
+ const AScenario: TInstallCase); reintroduce;
end;
implementation
+
uses
MessageIdentifiers,
MessageIdentifierConsts;
{$R *.dfm}
-constructor TfrmStartInstall.Create(AOwner: TComponent; const RestartRequired: Boolean);
+constructor TfrmStartInstall.Create(
+ AOwner: TComponent;
+ const AScenario: TInstallCase);
begin
+ Assert(AScenario <> icNone, 'Invalid install case');
+ FScenario := AScenario;
inherited Create(AOwner);
- FRestartRequired := RestartRequired;
end;
procedure TfrmStartInstall.FormCreate(Sender: TObject);
@@ -52,10 +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);
+
+ // 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/Keyman.System.UpdateStateMachine.pas b/windows/src/desktop/kmshell/main/Keyman.System.UpdateStateMachine.pas
index c50ecf14db9..700fe322a45 100644
--- a/windows/src/desktop/kmshell/main/Keyman.System.UpdateStateMachine.pas
+++ b/windows/src/desktop/kmshell/main/Keyman.System.UpdateStateMachine.pas
@@ -14,6 +14,7 @@ interface
Sentry.Client,
KeymanPaths,
+ Keyman.Configuration.Util.NetworkConnection,
Keyman.System.ExecutionHistory,
Keyman.System.UpdateCheckResponse,
utilkmshell;
@@ -719,7 +720,9 @@ procedure UpdateAvailableState.EnterState;
begin
// Enter UpdateAvailableState
bucStateContext.SetRegistryState(usUpdateAvailable);
- if bucStateContext.FAutomaticUpdate then
+
+ if bucStateContext.FAutomaticUpdate and
+ not TNetworkConnection.IsBackgroundUpdateBlocked then
begin
StartDownloadProcess;
end;
@@ -751,7 +754,8 @@ procedure UpdateAvailableState.HandleCheck;
function UpdateAvailableState.HandleKmShell;
begin
- if bucStateContext.FAutomaticUpdate then
+ if bucStateContext.FAutomaticUpdate and
+ not TNetworkConnection.IsBackgroundUpdateBlocked 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..4be5c20525e 100644
--- a/windows/src/desktop/kmshell/main/UfrmMain.pas
+++ b/windows/src/desktop/kmshell/main/UfrmMain.pas
@@ -190,6 +190,7 @@ implementation
Keyman.System.KeymanSentryClient,
Keyman.System.UpdateStateMachine,
OptionsXMLRenderer,
+ Keyman.Configuration.Util.NetworkConnection,
Keyman.Configuration.System.UmodWebHttpServer,
Keyman.Configuration.System.HttpServer.App.ConfigMain,
Keyman.Configuration.UI.InstallFile,
@@ -817,12 +818,34 @@ procedure TfrmMain.Update_ApplyNow;
ShellPath : string;
FResult, InstallNow: Boolean;
frmStartInstallNow: TfrmStartInstall;
+ IsMetered: Boolean;
+ InstallCase: TInstallCase;
begin
InstallNow := True;
- // Confirm User is ok that this will require a reset
- if HasKeymanRun then
+ IsMetered := TNetworkConnection.IsMetered;
+
+ // 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.
+ InstallCase := TInstallCase.icNone;
+ if HasKeymanRun and not IsMetered then
begin
- frmStartInstallNow := TfrmStartInstall.Create(nil, true);
+ InstallCase := TInstallCase.icRestartRequiredNotMetered;
+ end
+ else if HasKeymanRun and IsMetered then
+ begin
+ InstallCase := TInstallCase.icRestartRequiredMetered;
+ end
+ else if (not HasKeymanRun) and IsMetered then
+ begin
+ InstallCase := TInstallCase.icNoInstallMessageMetered;
+ end;
+
+ // Render dialog if conditions require it
+ if InstallCase <> TInstallCase.icNone then
+ begin
+ frmStartInstallNow := TfrmStartInstall.Create(nil, InstallCase);
try
if frmStartInstallNow.ShowModal = mrOk then
InstallNow := True
@@ -833,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 359130f27e7..bb34fedef9e 100644
--- a/windows/src/desktop/kmshell/main/initprog.pas
+++ b/windows/src/desktop/kmshell/main/initprog.pas
@@ -677,7 +677,8 @@ function ShouldSendToBUpdateSM(FSilent: Boolean; BUpdateSM: TUpdateStateMachine;
(FMode in [fmStart, fmSplash, fmMain, fmAbout,
fmHelp, fmShowHelp, fmSettings, fmBoot]) then
begin
- frmStartInstall := TfrmStartInstall.Create(nil, false);
+ // 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
diff --git a/windows/src/desktop/kmshell/util/Keyman.Configuration.Util.NetworkConnection.pas b/windows/src/desktop/kmshell/util/Keyman.Configuration.Util.NetworkConnection.pas
new file mode 100644
index 00000000000..ef9824fa48a
--- /dev/null
+++ b/windows/src/desktop/kmshell/util/Keyman.Configuration.Util.NetworkConnection.pas
@@ -0,0 +1,106 @@
+(*
+ * Keyman is copyright (C) SIL Global. MIT License.
+ *
+ * Notes: Enable checking for metered connection and background data restrictions.
+*)
+unit Keyman.Configuration.Util.NetworkConnection;
+
+interface
+
+
+type
+ TNetworkConnection = class
+ (**
+ * 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.
+ *)
+ class function IsMetered: Boolean; static;
+ (**
+ * Checks if background data usage is explicitly restricted by the current network profile.
+ *
+ * @returns True if background data usage is restricted, False otherwise.
+ *)
+ class function IsBackgroundDataRestricted: Boolean; static;
+ (**
+ * Determines whether background updates are blocked.
+ *
+ * @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
+ *)
+ class function IsBackgroundUpdateBlocked: Boolean; static;
+ end;
+implementation
+
+uses
+ System.SysUtils,
+ Winapi.CommonTypes,
+ Winapi.WinRT,
+ Winapi.Networking.Connectivity;
+
+class function TNetworkConnection.IsMetered: Boolean;
+var
+ Profile: IConnectionProfile;
+ CostLevel: IConnectionCost;
+begin
+ Result := False;
+ try
+ // Get the profile currently providing internet access
+ Profile := TNetworkInformation.GetInternetConnectionProfile;
+ if Assigned(Profile) 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;
+
+class function TNetworkConnection.IsBackgroundDataRestricted: Boolean;
+var
+ Profile: IConnectionProfile;
+ CostLevel: IConnectionCost;
+ DataRestriction: IConnectionCost2;
+begin
+ Result := False;
+ try
+ Profile := TNetworkInformation.GetInternetConnectionProfile;
+ if Profile <> nil then
+ begin
+ CostLevel := Profile.GetConnectionCost;
+ if (CostLevel <> nil) and Supports(CostLevel, IConnectionCost2, DataRestriction) then
+ Result := DataRestriction.BackgroundDataUsageRestricted;
+ end;
+ except
+ on E: Exception do
+ // See IsMetered: default to not-restricted if the WinRT APIs throw.
+ Result := False;
+ 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
+class function TNetworkConnection.IsBackgroundUpdateBlocked: 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.
+