Skip to content

Commit

Permalink
Notifying users of available add-on updates (#16636)
Browse files Browse the repository at this point in the history
Closes #15035

Summary of the issue:
Users would like a push notification on NVDA start-up letting them know if add-ons have available updates

Description of user facing changes
When starting NVDA, a dialog will appear notifying users of updatable add-ons if there are available updates.
This will only notify users if the available update is in the same channel as the installed add-on.
There is an "update all" button on the dialog and a list of add-ons with updates available.
There is also an "Open Add-on Store" button to open the add-on store to the updatable add-ons tab.

A setting to disable this. This is a combobox so that in future users can decide between:

notify on updates (default)
automatic update (See Ability to automatically update add-ons #3208)
disabled
Description of development approach
Created a scheduling module to schedule tasks on NVDA start up, using the schedule pip module.
This is so we can have conflict free scheduling. e.g. so NVDA's update notification won't clash with the add-on update notification.
Uses their suggested code to run a background thread for task scheduling: schedule.readthedocs.io/en/stable/background-execution.html

Changed much of the Add-on Store downloading code to be classmethods. This allows us to use it from the message dialog without creating a store instance.
  • Loading branch information
seanbudd authored Jun 12, 2024
1 parent 5c30524 commit d7facd1
Show file tree
Hide file tree
Showing 18 changed files with 891 additions and 101 deletions.
5 changes: 3 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@ pyserial==3.5
./miscDeps/python/wxPython-4.2.2a1-cp311-cp311-win32.whl
git+https://github.com/DiffSK/configobj@e2ba4457c4651fa54f8d59d8dcdd3da950e956b8#egg=configobj
requests==2.32.0
schedule==1.2.1
# Pillow is an implicit dependency and requires zlib and jpeg by default, but we don't need it
Pillow==10.3.0 -C "zlib=disable" -C "jpeg=disable"

#NVDA_DMP requires diff-match-patch
fast-diff-match-patch==2.1.0

# typing_extensions are required for specifying default value for `TypeVar`, which is not yet possible with any released version of Python (see PEP 696)
typing-extensions==4.9.0
typing-extensions==4.9.0

# pycaw is a Core Audio Windows Library used for sound split
pycaw==20240210
pycaw==20240210

# Packaging NVDA
git+https://github.com/py2exe/py2exe@4e7b2b2c60face592e67cb1bc935172a20fa371d#egg=py2exe
Expand Down
20 changes: 18 additions & 2 deletions source/addonStore/dataManager.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2022-2023 NV Access Limited
# Copyright (C) 2022-2024 NV Access Limited
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

Expand Down Expand Up @@ -37,6 +37,7 @@
_createStoreCollectionFromJson,
)
from .models.channel import Channel
from .models.status import AvailableAddonStatus, getStatus, _StatusFilterKey
from .network import (
_getCurrentApiVersionForURL,
_getAddonStoreURL,
Expand All @@ -47,7 +48,7 @@
if TYPE_CHECKING:
from addonHandler import Addon as AddonHandlerModel # noqa: F401
# AddonGUICollectionT must only be imported when TYPE_CHECKING
from .models.addon import AddonGUICollectionT, _AddonStoreModel # noqa: F401
from .models.addon import AddonGUICollectionT, _AddonGUIModel, _AddonStoreModel # noqa: F401
from gui.addonStoreGui.viewModels.addonList import AddonListItemVM # noqa: F401
from gui.message import DisplayableError # noqa: F401

Expand Down Expand Up @@ -308,6 +309,21 @@ def _getCachedInstalledAddonData(self, addonId: str) -> Optional[InstalledAddonS
return None
return _createInstalledStoreModelFromData(cacheData)

def _addonsPendingUpdate(self) -> list["_AddonGUIModel"]:
addonsPendingUpdate: list["_AddonGUIModel"] = []
compatibleAddons = self.getLatestCompatibleAddons()
for channel in compatibleAddons:
for addon in compatibleAddons[channel].values():
if (
getStatus(addon, _StatusFilterKey.UPDATE) == AvailableAddonStatus.UPDATE
# Only consider add-ons that have been installed through the Add-on Store
and addon._addonHandlerModel._addonStoreData is not None
):
# Only consider add-on updates for the same channel
if addon.channel == addon._addonHandlerModel._addonStoreData.channel:
addonsPendingUpdate.append(addon)
return addonsPendingUpdate


class _InstalledAddonsCache(AutoPropertyObject):
cachePropertiesByDefault = True
Expand Down
13 changes: 7 additions & 6 deletions source/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,12 +506,13 @@ class ConfigManager(object):

#: Sections that only apply to the base configuration;
#: i.e. they cannot be overridden in profiles.
BASE_ONLY_SECTIONS = {
"general",
"update",
"upgrade",
"development",
}
BASE_ONLY_SECTIONS = frozenset({
"general",
"update",
"upgrade",
"development",
"addonStore",
})

def __init__(self):
self.spec = confspec
Expand Down
20 changes: 19 additions & 1 deletion source/config/configFlags.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2022-2023 NV Access Limited, Cyrille Bougot
# Copyright (C) 2022-2024 NV Access Limited, Cyrille Bougot
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

Expand Down Expand Up @@ -202,3 +202,21 @@ def _displayStringLabels(self):
# document formatting settings panel.
ReportCellBorders.COLOR_AND_STYLE: _("Both Colors and Styles"),
}


class AddonsAutomaticUpdate(DisplayStringStrEnum):
NOTIFY = "notify"
# TODO: uncomment when implementing #3208
# UPDATE = "update"
DISABLED = "disabled"

@property
def _displayStringLabels(self):
return {
# Translators: This is a label for the automatic update behaviour for add-ons.
# It will notify the user when updates are available.
self.NOTIFY: _("Notify"),
# self.UPDATE: _("Update Automatically"),
# Translators: This is a label for the automatic update behaviour for add-ons.
self.DISABLED: _("Disabled"),
}
3 changes: 2 additions & 1 deletion source/config/configSpec.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2006-2023 NV Access Limited, Babbage B.V., Davy Kager, Bill Dengler, Julien Cochuyt,
# Copyright (C) 2006-2024 NV Access Limited, Babbage B.V., Davy Kager, Bill Dengler, Julien Cochuyt,
# Joseph Lee, Dawid Pieper, mltony, Bram Duvigneau, Cyrille Bougot, Rob Meredith,
# Burman's Computer and Education Ltd., Leonard de Ruijter, Łukasz Golonka
# This file is covered by the GNU General Public License.
Expand Down Expand Up @@ -325,6 +325,7 @@
[addonStore]
showWarning = boolean(default=true)
automaticUpdates = option("notify", "disabled", default="notify")
"""

#: The configuration specification
Expand Down
5 changes: 5 additions & 0 deletions source/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,8 @@ def main():
log.info(f"Windows version: {winVersion.getWinVer()}")
log.info("Using Python version %s"%sys.version)
log.info("Using comtypes version %s"%comtypes.__version__)
from utils import schedule
schedule.initialize()
import configobj
log.info("Using configobj version %s with validate version %s"%(configobj.__version__,configobj.validate.__version__))
# Set a reasonable timeout for any socket connections NVDA makes.
Expand All @@ -670,6 +672,8 @@ def main():
from addonStore import dataManager
dataManager.initialize()
addonHandler.initialize()
from gui import addonStoreGui
addonStoreGui.initialize()
if globalVars.appArgs.disableAddons:
log.info("Add-ons are disabled. Restart NVDA to enable them.")
import appModuleHandler
Expand Down Expand Up @@ -959,6 +963,7 @@ def _doPostNvdaStartupAction():
_terminate(addonHandler)
_terminate(dataManager, name="addon dataManager")
_terminate(garbageHandler)
_terminate(schedule, name="task scheduler")
# DMP is only started if needed.
# Terminate manually (and let it write to the log if necessary)
# as core._terminate always writes an entry.
Expand Down
17 changes: 16 additions & 1 deletion source/gui/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- coding: UTF-8 -*-
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2006-2023 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Mesar Hameed, Joseph Lee,
# Copyright (C) 2006-2024 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Mesar Hameed, Joseph Lee,
# Thomas Stivers, Babbage B.V., Accessolutions, Julien Cochuyt, Cyrille Bougot, Luke Davis
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
Expand Down Expand Up @@ -408,6 +408,21 @@ def onAddonStoreCommand(self, evt: wx.MenuEvent):
_storeVM.refresh()
self.popupSettingsDialog(AddonStoreDialog, _storeVM)

@blockAction.when(
blockAction.Context.SECURE_MODE,
blockAction.Context.MODAL_DIALOG_OPEN,
blockAction.Context.WINDOWS_LOCKED,
blockAction.Context.WINDOWS_STORE_VERSION,
blockAction.Context.RUNNING_LAUNCHER,
)
def onAddonStoreUpdatableCommand(self, evt: wx.MenuEvent | None):
from .addonStoreGui import AddonStoreDialog
from .addonStoreGui.viewModels.store import AddonStoreVM
from addonStore.models.status import _StatusFilterKey
_storeVM = AddonStoreVM()
_storeVM.refresh()
self.popupSettingsDialog(AddonStoreDialog, _storeVM, openToTab=_StatusFilterKey.UPDATE)

def onReloadPluginsCommand(self, evt):
import appModuleHandler, globalPluginHandler
from NVDAObjects import NVDAObject
Expand Down
13 changes: 12 additions & 1 deletion source/gui/addonStoreGui/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2022 NV Access Limited
# Copyright (C) 2022-2024 NV Access Limited
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

from utils.schedule import scheduleThread, ThreadTarget

from .controls.storeDialog import AddonStoreDialog
from .controls.messageDialogs import UpdatableAddonsDialog

__all__ = [
"AddonStoreDialog",
"initialize",
]


def initialize():
scheduleThread.scheduleDailyJobAtStartUp(
UpdatableAddonsDialog._checkForUpdatableAddons,
queueToThread=ThreadTarget.GUI,
)
Loading

0 comments on commit d7facd1

Please sign in to comment.