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 @@
frmStartInstall
+ 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. +