diff --git a/browser/components/BrowserGlue.sys.mjs b/browser/components/BrowserGlue.sys.mjs index d17666cd94f6..c56cf190a681 100644 --- a/browser/components/BrowserGlue.sys.mjs +++ b/browser/components/BrowserGlue.sys.mjs @@ -461,6 +461,8 @@ let JSWINDOWACTORS = { "BackupUI:RestoreFromBackupChooseFile": { wantUntrusted: true }, "BackupUI:ToggleEncryption": { wantUntrusted: true }, "BackupUI:RerunEncryption": { wantUntrusted: true }, + "BackupUI:ShowBackupLocation": { wantUntrusted: true }, + "BackupUI:EditBackupLocation": { wantUntrusted: true }, }, }, matches: ["about:preferences*", "about:settings*"], diff --git a/browser/components/backup/BackupService.sys.mjs b/browser/components/backup/BackupService.sys.mjs index c97f827776cb..e76a56e2b545 100644 --- a/browser/components/backup/BackupService.sys.mjs +++ b/browser/components/backup/BackupService.sys.mjs @@ -23,6 +23,8 @@ const MINIMUM_TIME_BETWEEN_BACKUPS_SECONDS_PREF_NAME = "browser.backup.scheduled.minimum-time-between-backups-seconds"; const LAST_BACKUP_TIMESTAMP_PREF_NAME = "browser.backup.scheduled.last-backup-timestamp"; +const LAST_BACKUP_FILE_NAME_PREF_NAME = + "browser.backup.scheduled.last-backup-file"; const SCHEMAS = Object.freeze({ BACKUP_MANIFEST: 1, @@ -70,6 +72,10 @@ ChromeUtils.defineLazyGetter(lazy, "ZipReader", () => "open" ) ); +ChromeUtils.defineLazyGetter(lazy, "nsLocalFile", () => + Components.Constructor("@mozilla.org/file/local;1", "nsIFile", "initWithPath") +); + ChromeUtils.defineLazyGetter(lazy, "BinaryInputStream", () => Components.Constructor( "@mozilla.org/binaryinputstream;1", @@ -575,6 +581,7 @@ export class BackupService extends EventTarget { encryptionEnabled: false, /** @type {number?} Number of seconds since UNIX epoch */ lastBackupDate: null, + lastBackupFileName: "", }; /** @@ -951,7 +958,8 @@ export class BackupService extends EventTarget { } catch (e) { lazy.logConsole.warn("Could not create Home destination path: ", e); throw new Error( - "Could not resolve to a writable destination folder path." + "Could not resolve to a writable destination folder path.", + { cause: ERRORS.FILE_SYSTEM_ERROR } ); } } @@ -1251,6 +1259,11 @@ export class BackupService extends EventTarget { lazy.logConsole.log("Moving single-file archive to ", destPath); await IOUtils.move(sourcePath, destPath); + Services.prefs.setStringPref(LAST_BACKUP_FILE_NAME_PREF_NAME, FILENAME); + // It is expected that our caller will call stateUpdate(), so we skip doing + // that here. This is done via the backupInProgress setter in createBackup. + this.#_state.lastBackupFileName = FILENAME; + for (let childFilePath of existingChildren) { let childFileName = PathUtils.filename(childFilePath); // We check both the prefix and the suffix, because the prefix encodes @@ -2911,6 +2924,11 @@ export class BackupService extends EventTarget { this.#_state.lastBackupDate = lastBackupPrefValue; } + this.#_state.lastBackupFileName = Services.prefs.getStringPref( + LAST_BACKUP_FILE_NAME_PREF_NAME, + "" + ); + this.stateUpdate(); // We'll default to 5 minutes of idle time unless otherwise configured. @@ -3065,4 +3083,104 @@ export class BackupService extends EventTarget { }; this.stateUpdate(); } + + /* + * Attempts to open a native file explorer window at the last backup file's + * location on the filesystem. + */ + async showBackupLocation() { + let backupFilePath = PathUtils.join( + lazy.backupDirPref, + this.#_state.lastBackupFileName + ); + if (await IOUtils.exists(backupFilePath)) { + new lazy.nsLocalFile(backupFilePath).reveal(); + } else { + let archiveDestFolderPath = await this.resolveArchiveDestFolderPath( + lazy.backupDirPref + ); + new lazy.nsLocalFile(archiveDestFolderPath).reveal(); + } + } + + /** + * Shows a native folder picker to set the location to write the single-file + * archive files. + * + * @param {ChromeWindow} window + * The top-level browsing window to associate the file picker with. + * @returns {Promise} + */ + async editBackupLocation(window) { + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + let mode = Ci.nsIFilePicker.modeGetFolder; + fp.init(window.browsingContext, "", mode); + + let currentBackupDirPathParent = PathUtils.parent( + this.#_state.backupDirPath + ); + if (await IOUtils.exists(currentBackupDirPathParent)) { + fp.displayDirectory = await IOUtils.getDirectory( + currentBackupDirPathParent + ); + } + + let result = await new Promise(resolve => fp.open(resolve)); + + if (result === Ci.nsIFilePicker.returnCancel) { + return; + } + + let path = fp.file.path; + + // If the same parent directory was chosen, this is a no-op. + if ( + PathUtils.join(path, BackupService.BACKUP_DIR_NAME) == lazy.backupDirPref + ) { + return; + } + + // If the location changed, delete the last backup there if one exists. + await this.deleteLastBackup(); + this.setParentDirPath(path); + } + + /** + * Will attempt to delete the last created single-file archive if it exists. + * Once done, this method will also check the parent folder to see if it's + * empty. If so, then the folder is removed. + * + * @returns {Promise} + */ + async deleteLastBackup() { + if (this.#_state.lastBackupFileName) { + let backupFilePath = PathUtils.join( + lazy.backupDirPref, + this.#_state.lastBackupFileName + ); + + lazy.logConsole.log( + "Attempting to delete last backup file at ", + backupFilePath + ); + await IOUtils.remove(backupFilePath, { ignoreAbsent: true }); + + this.#_state.lastBackupDate = null; + this.#_state.lastBackupFileName = ""; + this.stateUpdate(); + } else { + lazy.logConsole.log( + "Not deleting last backup file, since none is known about." + ); + } + + if (await IOUtils.exists(lazy.backupDirPref)) { + // See if there are any other files lingering around in the destination + // folder. If not, delete that folder too. + let children = await IOUtils.getChildren(lazy.backupDirPref); + if (!children.length) { + await IOUtils.remove(lazy.backupDirPref); + } + } + } } diff --git a/browser/components/backup/actors/BackupUIChild.sys.mjs b/browser/components/backup/actors/BackupUIChild.sys.mjs index 42c87b01f4f1..1f27f7420148 100644 --- a/browser/components/backup/actors/BackupUIChild.sys.mjs +++ b/browser/components/backup/actors/BackupUIChild.sys.mjs @@ -75,6 +75,10 @@ export class BackupUIChild extends JSWindowActorChild { this.sendAsyncMessage("ToggleEncryption", event.detail); } else if (event.type == "BackupUI:RerunEncryption") { this.sendAsyncMessage("RerunEncryption", event.detail); + } else if (event.type == "BackupUI:ShowBackupLocation") { + this.sendAsyncMessage("ShowBackupLocation"); + } else if (event.type == "BackupUI:EditBackupLocation") { + this.sendAsyncMessage("EditBackupLocation"); } } diff --git a/browser/components/backup/actors/BackupUIParent.sys.mjs b/browser/components/backup/actors/BackupUIParent.sys.mjs index 05ec9ce4efc8..23669a121fa5 100644 --- a/browser/components/backup/actors/BackupUIParent.sys.mjs +++ b/browser/components/backup/actors/BackupUIParent.sys.mjs @@ -216,6 +216,11 @@ export class BackupUIParent extends JSWindowActorParent { * re-encryption. */ } + } else if (message.name == "ShowBackupLocation") { + this.#bs.showBackupLocation(); + } else if (message.name == "EditBackupLocation") { + const window = this.browsingContext.topChromeWindow; + this.#bs.editBackupLocation(window); } return null; diff --git a/browser/components/backup/content/backup-common.css b/browser/components/backup/content/backup-common.css new file mode 100644 index 000000000000..88e166305226 --- /dev/null +++ b/browser/components/backup/content/backup-common.css @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +/** + * This stylesheet is for shared rules across multiple backup components. + * It assumes that common.css has been imported into the same scope. + */ + +.backup-location-filepicker-input { + flex: 1; + margin-block: var(--space-xsmall); + margin-inline-start: 0; + padding-inline-start: var(--space-xxlarge); + background-repeat: no-repeat; + background-size: var(--icon-size-default); + background-position: var(--space-small) 50%; + + /* For the placeholder icon */ + -moz-context-properties: fill; + fill: currentColor; + + &:dir(rtl) { + background-position-x: right var(--space-small); + } +} diff --git a/browser/components/backup/content/backup-settings.css b/browser/components/backup/content/backup-settings.css index 77f8176a4a40..c0ffa13fdd29 100644 --- a/browser/components/backup/content/backup-settings.css +++ b/browser/components/backup/content/backup-settings.css @@ -2,10 +2,12 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ - @import url("chrome://global/skin/in-content/common.css"); +@import url("chrome://global/skin/in-content/common.css"); +@import url("chrome://browser/content/backup/backup-common.css"); :host { --margin-inline-start-checkbox-content: calc(var(--checkbox-margin-inline) + var(--checkbox-size)); + flex: 1; } #turn-on-scheduled-backups-dialog { @@ -36,3 +38,28 @@ #backup-sensitive-data-checkbox-description { margin-inline-start: var(--margin-inline-start-checkbox-content); } + +#scheduled-backups-control { + display: flex; +} + +#scheduled-backups-enabled { + flex: 1; + align-content: center; +} + +#last-backup-info { + color: var(--text-color-deemphasized); +} + +#last-backup-location-control, +#last-backup-info > div { + margin-block: 14px; +} + +#last-backup-location-control { + display: flex; + align-items: center; + gap: 10px; + text-overflow: ellipsis ellipsis; +} diff --git a/browser/components/backup/content/backup-settings.mjs b/browser/components/backup/content/backup-settings.mjs index 623f6458fe2f..7a38bf1813fe 100644 --- a/browser/components/backup/content/backup-settings.mjs +++ b/browser/components/backup/content/backup-settings.mjs @@ -21,6 +21,8 @@ import "chrome://browser/content/backup/disable-backup-encryption.mjs"; * document of about:settings / about:preferences. */ export default class BackupSettings extends MozLitElement { + #placeholderIconURL = "chrome://global/skin/icons/page-portrait.svg"; + static properties = { backupServiceState: { type: Object }, _enableEncryptionTypeAttr: { type: String }, @@ -42,6 +44,11 @@ export default class BackupSettings extends MozLitElement { restoreFromBackupButtonEl: "#backup-toggle-restore-button", restoreFromBackupDialogEl: "#restore-from-backup-dialog", sensitiveDataCheckboxInputEl: "#backup-sensitive-data-checkbox-input", + lastBackupLocationInputEl: "#last-backup-location", + lastBackupFileNameEl: "#last-backup-filename", + lastBackupDateEl: "#last-backup-date", + backupLocationShowButtonEl: "#backup-location-show", + backupLocationEditButtonEl: "#backup-location-edit", }; } @@ -55,7 +62,6 @@ export default class BackupSettings extends MozLitElement { backupDirPath: "", backupFileToRestore: null, backupFileInfo: null, - backupInProgress: false, defaultParent: { fileName: "", path: "", @@ -63,6 +69,8 @@ export default class BackupSettings extends MozLitElement { }, encryptionEnabled: false, scheduledBackupsEnabled: false, + lastBackupDate: null, + lastBackupFileName: "", }; this._enableEncryptionTypeAttr = ""; } @@ -285,6 +293,22 @@ export default class BackupSettings extends MozLitElement { } } + handleShowBackupLocation() { + this.dispatchEvent( + new CustomEvent("BackupUI:ShowBackupLocation", { + bubbles: true, + }) + ); + } + + handleEditBackupLocation() { + this.dispatchEvent( + new CustomEvent("BackupUI:EditBackupLocation", { + bubbles: true, + }) + ); + } + enableBackupEncryptionDialogTemplate() { return html` `; } + lastBackupInfoTemplate() { + // The lastBackupDate is stored in preferences, which only accepts + // 32-bit signed values, so we automatically divide it by 1000 before + // storing it. We need to re-multiply it by 1000 to get Fluent to render + // the right time. + let backupDateArgs = { + date: this.backupServiceState.lastBackupDate * 1000, + }; + let backupFileNameArgs = { + fileName: this.backupServiceState.lastBackupFileName, + }; + + return html` +
+
+
+
+ `; + } + + backupLocationTemplate() { + let iconURL = + this.backupServiceState.defaultParent.iconURL || this.#placeholderIconURL; + let { backupDirPath } = this.backupServiceState; + + return html` +
+ + + + +
+ `; + } + + updated() { + if (this.backupServiceState.scheduledBackupsEnabled) { + let input = this.lastBackupLocationInputEl; + input.setSelectionRange(input.value.length, input.value.length); + } + } + render() { + let scheduledBackupsEnabledL10nID = this.backupServiceState + .scheduledBackupsEnabled + ? "settings-data-backup-scheduled-backups-on" + : "settings-data-backup-scheduled-backups-off"; return html` + ${this.turnOnScheduledBackupsDialogTemplate()} + ${this.turnOffScheduledBackupsDialogTemplate()} + ${this.enableBackupEncryptionDialogTemplate()} + ${this.disableBackupEncryptionDialogTemplate()} +
-
- Backup in progress: - ${this.backupServiceState.backupInProgress ? "Yes" : "No"} +
+ + +
- ${this.turnOnScheduledBackupsDialogTemplate()} - ${this.turnOffScheduledBackupsDialogTemplate()} - ${this.enableBackupEncryptionDialogTemplate()} - ${this.disableBackupEncryptionDialogTemplate()} - - - - ${this.restoreFromBackupTemplate()} + ${this.backupServiceState.lastBackupDate + ? this.lastBackupInfoTemplate() + : null} + ${this.backupServiceState.scheduledBackupsEnabled + ? this.backupLocationTemplate() + : null}
@@ -361,6 +461,7 @@ export default class BackupSettings extends MozLitElement { >
+ ${this.backupServiceState.encryptionEnabled ? html`` : null} + ${this.restoreFromBackupTemplate()}
`; } } diff --git a/browser/components/backup/content/backup-settings.stories.mjs b/browser/components/backup/content/backup-settings.stories.mjs index 98d7a1876a7a..826865a848bd 100644 --- a/browser/components/backup/content/backup-settings.stories.mjs +++ b/browser/components/backup/content/backup-settings.stories.mjs @@ -19,24 +19,10 @@ const Template = ({ backupServiceState }) => html` `; -export const BackingUpNotInProgress = Template.bind({}); -BackingUpNotInProgress.args = { +export const ScheduledBackupsDisabled = Template.bind({}); +ScheduledBackupsDisabled.args = { backupServiceState: { backupDirPath: "/Some/User/Documents", - backupInProgress: false, - defaultParent: { - path: "/Some/User/Documents", - fileName: "Documents", - }, - scheduledBackupsEnabled: false, - }, -}; - -export const BackingUpInProgress = Template.bind({}); -BackingUpInProgress.args = { - backupServiceState: { - backupDirPath: "/Some/User/Documents", - backupInProgress: true, defaultParent: { path: "/Some/User/Documents", fileName: "Documents", @@ -49,7 +35,6 @@ export const ScheduledBackupsEnabled = Template.bind({}); ScheduledBackupsEnabled.args = { backupServiceState: { backupDirPath: "/Some/User/Documents", - backupInProgress: false, defaultParent: { path: "/Some/User/Documents", fileName: "Documents", @@ -58,16 +43,31 @@ ScheduledBackupsEnabled.args = { }, }; +export const ExistingBackup = Template.bind({}); +ExistingBackup.args = { + backupServiceState: { + backupDirPath: "/Some/User/Documents", + defaultParent: { + path: "/Some/User/Documents", + fileName: "Documents", + }, + scheduledBackupsEnabled: true, + lastBackupDate: 1719625747, + lastBackupFileName: "FirefoxBackup_default_123123123.html", + }, +}; + export const EncryptionEnabled = Template.bind({}); EncryptionEnabled.args = { backupServiceState: { backupDirPath: "/Some/User/Documents", - backupInProgress: false, defaultParent: { path: "/Some/User/Documents", fileName: "Documents", }, scheduledBackupsEnabled: true, encryptionEnabled: true, + lastBackupDate: 1719625747, + lastBackupFileName: "FirefoxBackup_default_123123123.html", }, }; diff --git a/browser/components/backup/content/turn-on-scheduled-backups.css b/browser/components/backup/content/turn-on-scheduled-backups.css index e6bb713d1c30..a08ed219d29c 100644 --- a/browser/components/backup/content/turn-on-scheduled-backups.css +++ b/browser/components/backup/content/turn-on-scheduled-backups.css @@ -3,6 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ @import url("chrome://global/skin/in-content/common.css"); +@import url("chrome://browser/content/backup/backup-common.css"); :host { --margin-inline-start-checkbox-content: calc(var(--checkbox-margin-inline) + var(--checkbox-size)); @@ -52,24 +53,6 @@ column-gap: var(--space-small); align-items: center; } - - .backup-location-filepicker-input { - flex: 1; - margin-block: var(--space-xsmall); - margin-inline-start: 0; - padding-inline-start: var(--space-xxlarge); - background-repeat: no-repeat; - background-size: var(--icon-size-default); - background-position: var(--space-small) 50%; - - /* For the placeholder icon */ - fill: currentColor; - -moz-context-properties: fill; - - &:dir(rtl) { - background-position: calc(100% - var(--space-small)) 50%; - } - } } #sensitive-data-controls { diff --git a/browser/components/backup/jar.mn b/browser/components/backup/jar.mn index 875f6b7c7b65..cc4ec0d278bc 100644 --- a/browser/components/backup/jar.mn +++ b/browser/components/backup/jar.mn @@ -9,6 +9,7 @@ browser.jar: #endif content/browser/backup/ArchiveJSONBlock.1.schema.json (content/ArchiveJSONBlock.1.schema.json) content/browser/backup/BackupManifest.1.schema.json (content/BackupManifest.1.schema.json) + content/browser/backup/backup-common.css (content/backup-common.css) content/browser/backup/backup-settings.css (content/backup-settings.css) content/browser/backup/backup-settings.mjs (content/backup-settings.mjs) content/browser/backup/disable-backup-encryption.css (content/disable-backup-encryption.css) diff --git a/browser/components/backup/tests/browser/browser_settings.js b/browser/components/backup/tests/browser/browser_settings.js index c61083fb4f0f..d9e7da0b4423 100644 --- a/browser/components/backup/tests/browser/browser_settings.js +++ b/browser/components/backup/tests/browser/browser_settings.js @@ -3,6 +3,10 @@ "use strict"; +const { MockRegistrar } = ChromeUtils.importESModule( + "resource://testing-common/MockRegistrar.sys.mjs" +); + add_setup(async () => { MockFilePicker.init(window.browsingContext); registerCleanupFunction(() => { @@ -260,3 +264,120 @@ add_task(async function test_restore_from_backup() { sandbox.restore(); }); }); + +/** + * Tests that the most recent backup information is shown inside of the + * component. Also tests that the "Show in folder" and "Edit" buttons open + * file pickers. + */ +add_task(async function test_last_backup_info_and_location() { + // We'll override the default location for writing backup archives so that + // we don't pollute this machine's Documents folder. + const TEST_PROFILE_PATH = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "testLastBackupInfo" + ); + + const SCHEDULED_BACKUPS_ENABLED_PREF = "browser.backup.scheduled.enabled"; + + await SpecialPowers.pushPrefEnv({ + set: [[SCHEDULED_BACKUPS_ENABLED_PREF, true]], + }); + + await BrowserTestUtils.withNewTab("about:preferences", async browser => { + let sandbox = sinon.createSandbox(); + let bs = BackupService.get(); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.backup.location", TEST_PROFILE_PATH]], + }); + + Assert.ok(bs.state.backupDirPath, "Backup Dir Path was set"); + + let settings = browser.contentDocument.querySelector("backup-settings"); + await settings.updateComplete; + + let stateUpdated = BrowserTestUtils.waitForEvent( + bs, + "BackupService:StateUpdate", + false, + () => { + return bs.state.lastBackupDate && bs.state.lastBackupFileName; + } + ); + let { archivePath } = await bs.createBackup(); + registerCleanupFunction(async () => { + // No matter what happens after this, make sure to clean this file up. + await IOUtils.remove(archivePath); + }); + await stateUpdated; + + await settings.updateComplete; + + let dateArgs = JSON.parse( + settings.lastBackupDateEl.getAttribute("data-l10n-args") + ); + // The lastBackupDate is stored in seconds, but Fluent expects milliseconds, + // so we'll check that it was converted. + Assert.equal( + dateArgs.date, + bs.state.lastBackupDate * 1000, + "Should have the backup date as a Fluent arg, in milliseconds" + ); + + let locationArgs = JSON.parse( + settings.lastBackupFileNameEl.getAttribute("data-l10n-args") + ); + Assert.equal( + locationArgs.fileName, + bs.state.lastBackupFileName, + "Should have the backup file name as a Fluent arg" + ); + + // Mocking out nsLocalFile isn't something that works very well, so we'll + // just stub out BackupService.showBackupLocation with something that'll + // resolve showBackupLocationPromise, and rely on manual testing for + // showing the location of the backup file. + let showBackupLocationPromise = new Promise(resolve => { + let showBackupLocationStub = sandbox.stub(bs, "showBackupLocation"); + showBackupLocationStub.callsFake(() => { + resolve(); + }); + }); + + settings.backupLocationShowButtonEl.click(); + await showBackupLocationPromise; + + const TEST_NEW_BACKUP_PARENT_PATH = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "testNewBackupParent" + ); + let newBackupParent = await IOUtils.getDirectory( + TEST_NEW_BACKUP_PARENT_PATH + ); + + stateUpdated = BrowserTestUtils.waitForEvent( + bs, + "BackupService:StateUpdate", + false, + () => { + return bs.state.backupDirPath.startsWith(TEST_NEW_BACKUP_PARENT_PATH); + } + ); + let filePickerShownPromise = new Promise(resolve => { + MockFilePicker.showCallback = async () => { + Assert.ok(true, "Filepicker shown"); + MockFilePicker.setFiles([newBackupParent]); + resolve(); + }; + }); + MockFilePicker.returnValue = MockFilePicker.returnOK; + + settings.backupLocationEditButtonEl.click(); + await filePickerShownPromise; + await stateUpdated; + + await IOUtils.remove(TEST_NEW_BACKUP_PARENT_PATH); + sandbox.restore(); + }); +}); diff --git a/browser/components/backup/tests/chrome/test_backup_settings.html b/browser/components/backup/tests/chrome/test_backup_settings.html index 63639ab45cfa..dd5f99e5b9a0 100644 --- a/browser/components/backup/tests/chrome/test_backup_settings.html +++ b/browser/components/backup/tests/chrome/test_backup_settings.html @@ -10,11 +10,14 @@ type="module" > + + diff --git a/browser/locales-preview/backupSettings.ftl b/browser/locales-preview/backupSettings.ftl index eefecae36da9..dae390403538 100644 --- a/browser/locales-preview/backupSettings.ftl +++ b/browser/locales-preview/backupSettings.ftl @@ -23,6 +23,17 @@ backup-file-name = { -brand-product-name }Backup settings-data-backup-header = Backup settings-data-backup-toggle = Manage backup +settings-data-backup-scheduled-backups-on = Backup: ON +settings-data-backup-scheduled-backups-off = Backup: OFF +settings-data-backup-last-backup-date = Last backup: { DATETIME($date, timeStyle: "short") }, { DATETIME($date, dateStyle: "short") } +# "Location" refers to the folder where backups are being written to. +settings-data-backup-last-backup-location = Location +settings-data-backup-last-backup-location-show-in-folder = Show in folder +settings-data-backup-last-backup-location-edit = Edit… + +# Variables: +# $fileName (String) - The file name of the last backup that was created. +settings-data-backup-last-backup-filename = Filename: { $fileName } settings-data-backup-restore-header = Restore your data settings-data-backup-restore-description = Use a { -brand-short-name } backup from another device to restore your data.