Date Mon 18 November 2019 Tags LSR

Introduction

Morphic takes an expression of a user's needs and wants and reconfigure a machine to reflect those needs and wants as well as possible. The user's needs and wants are expressed as "preferences", which may either be a "generic preference", or specific to a particular application. These are transformed into "settings", which are expressed in terms of the specific solutions available on a given machine.

A "solution" is how we represent a specific piece of software that a setting relates to. These may be features of the operating system, or third-party applications. Each entry within the Solutions Registry defines:

  1. The context (generally operating system) in which a solution may be found.
  2. How to detect whether the solution is running.
  3. How to start and stop the solution.
  4. How the solution's configuration options are controlled.
  5. The specific configuration options available for the solution.
  6. Transformations of the application-specific settings to and from "generic preference terms".
  7. How to describe the solution and settings when presenting information to an end user.
  8. How to install the solution if it is not available ("Install on Demand").

Each of these pieces of information is currently only defined for a single version of a given solution. We have long identified the need to support multiple versions of a single "solution", and this draft outlines a few related concerns and possible strategies for adding this key feature.

The Problem in More Detail

Let's start with a simple example in which we wish to configure a handful of settings for a single solution:

{
    "flat": {
        "name": "JAWS",
        "contexts": {
            "gpii-default": {
                "name": "Sample JAWS preferences",
                "preferences": {
                    "http://registry.gpii.net/applications/com.freedomscientific.jaws": {
                        "HTML.Acronyms": 1,
                        "ENU-Global.Volume": 95
                    }
                },
                "http://registry.gpii.net/applications/com.freedomscientific.jaws/enabled": true
            }
        }
    }
}

The com.freedomscientific.jaws portion of the two keys above is a unique identifier that correspond to an entry in the Windows portion of the Solutions Registry. Here is a greatly simplified version of that entry:

{
    "com.freedomscientific.jaws": {
        "name": "JAWS",
        "contexts": {
            "OS": [
                {
                    "id": "win32"
                }
            ]
        },
        "capabilities": [
            "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled"
        ],
        "settingsHandlers": {
            "configuration1": {
                "type": "gpii.settingsHandlers.INISettingsHandler",
                "liveness": "manualRestart",
                "options": {
                    "filename": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF"
                },
                "supportedSettings": {
                    "HTML.Acronyms": {
                        "schema": {
                            "title": "Acronyms",
                            "description": "If this is enabled, acronyms with a title attribute will have their title read instead of the on screen text.",
                            "type": "number",
                            "default": 0,
                            "enum": [
                                0,
                                1
                            ],
                            "enumLabels": [
                                "off",
                                "on"
                            ]
                        }
                    },
                }
            },
            "configuration2": {
                "type": "gpii.settingsHandlers.INISettingsHandler",
                "liveness": "manualRestart",
                "options": {
                    "filename": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\VoiceProfiles\\GPII.VPF"
                },
                "supportedSettings": {
                    "ENU-Global.Volume": {
                        "schema": {
                            "title": "Global Volume",
                            "description": "The volume for all announcements.",
                            "type": "integer",
                            "minimum": 10,
                            "maximum": 100,
                            "default": 100
                        }
                    }
                },
                "capabilitiesTransformations": {
                    "ENU-Global\\.Volume": {
                        "transform": {
                            "type": "fluid.transforms.linearScale",
                            "inputPath": "http://registry\\.gpii\\.net/common/volumeTTS",
                            "factor": 100
                        }
                    }
                },
                "inverseCapabilitiesTransformations": {
                    "http://registry\\.gpii\\.net/common/volumeTTS": {
                        "transform": {
                            "type": "fluid.transforms.linearScale",
                            "inputPath": "ENU-Global\\.Volume",
                            "factor": 0.01
                        }
                    }
                }
            }
        },
        "launchHandlers": {
            "launcher": {
                "type": "gpii.launchHandlers.flexibleHandler",
                "options": {
                    "getState": [
                        {
                            "type": "gpii.processReporter.find",
                            "command": "jfw.exe"
                        }
                    ],
                    "setTrue": [
                        {
                            "type": "gpii.launch.exec",
                            "command": "\"${{registry}.HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\JAWS2019.exe\\}\""
                        }
                    ],
                    "setFalse": [
                        {
                            "type": "gpii.windows.closeProcessByName",
                            "filename": "jfw.exe"
                        },
                        {
                            "type": "gpii.windows.closeProcessByName",
                            "filename": "fsSynth32.exe"
                        },
                        {
                            "type": "gpii.windows.closeProcessByName",
                            "filename": "jhookldr.exe"
                        },
                        // New for JAWS 2018 and higher.
                        {
                            "type": "gpii.windows.closeProcessByName",
                            "filename": "ScannerHandler.exe"
                        }
                    ]
                }
            }
        },
        "isInstalled": [
            {
                "type": "gpii.deviceReporter.registryKeyExists",
                "hKey": "HKEY_LOCAL_MACHINE",
                "path": "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\JAWS2019.exe",
                "subPath": "",
                "dataType": "REG_SZ"
            }
        ]
    }
}

That entry contains multiple key pieces of information that are specific to a particular version of JAWS:

  1. The location of the main INI file.
  2. The location of the voice profile INI file.
  3. The location in the registry that we use to confirm whether JAWS is installed.
  4. The path to the JAWS binary used by the launch handler.

Each of these mean that we can only work with a single version of JAWS (currently JAWS 2019). We have discussed strategies for creating secondary entries for sub versions, but this only solves the problem of sharing common settings between solutions, and does not address other issues.

We have discussed addressing this by defining multiple entries, each of which would use the normal Fluid options merging to overlay the information unique to their version. Once we have multiple solutions for com.freedomscientific.jaws.12, com.freedomscientific.jaws.2017, com.freedomscientific.jaws.2018, com.freedomscientific.jaws.2019, et cetera, we need to answer a few questions:

  1. How do users express their desire to have an application use a particular setting, and have that respected across versions?
  2. How do we ensure that only one version is launched? Which version?

Cross-version Settings for Applications

The solution id is currently part of the URL used as a key for the application settings. If we define a new entry for com.freedom.scientific.jaws.2020, our only way of expressing settings for that solution/version at the moment is to include that id in the URL. So, if we wanted to ensure that JAWS was enabled, we might express the need to launch each version:

{
    "flat": {
        "name": "JAWS",
        "contexts": {
            "gpii-default": {
                "name": "Enable JAWS",
                "http://registry.gpii.net/applications/com.freedomscientific.jaws.12/enabled": true,
                "http://registry.gpii.net/applications/com.freedomscientific.jaws.2017/enabled": true,
                "http://registry.gpii.net/applications/com.freedomscientific.jaws.2018/enabled": true,
                "http://registry.gpii.net/applications/com.freedomscientific.jaws.2019/enabled": true
            }
        }
    }
}

This is overly verbose and extremely brittle. We may be carrying around versions that have long vanished from the wild. We also have no way of knowing when there are new versions available, and that the previously expressed need to "enable JAWS" should apply to the new version without the user taking any action. We also can't always assume that only one version of a solution is installed on a given machine, and the above might result in our attempting to configure and launch more than one version of JAWS.

It seems clear to me that what we need is a way to represent an application more generally. Instead of the above, we ideally want for a user to be able to express then need to enable JAWS using something like the following payload:

{
    "flat": {
        "name": "JAWS",
        "contexts": {
            "gpii-default": {
                "name": "Enable JAWS",
                "http://registry.gpii.net/applications/com.freedomscientific.jaws/enabled": true
            }
        }
    }
}

In order for this to work, we need some way to make our way from the com.freedomscientific.jaws namespace to whichever version is actually available. Currently, the list of available solutions looks something like:

[
    {
        "id": "com.freedomscientific.jaws"
    },
    {
        "id": "org.nvda-project"
    }
]

We might simply choose to extend this to use a syntax like:

[
    {
        "id": "com.freedomscientific.jaws",
        "version": "com.freedomscientific.jaws.2020"
    },
    {
        "id": "org.nvda-project",
        "version": "org.nvda-project"
    }
]

In the first "versioned" example (JAWS), the version information indicates the grade name of a particular version of JAWS. In the second "unversioned" example, the version information indicates the "general" grade. This opens up the possibility of transforming the "general" namespace to and from the "versioned" namespace. It also allows us to keep individual solutions fairly simple. But how do we provide enough information to the device reporter to enable it to build the improved list of installed solutions? I can see a few ways to do this:

  1. "Version Holding Solutions": Make the "versions" child components of the "general" entry.
  2. "Application Capabilities": Express each version as a separate solution, but also express the relationship between a version and its "generic" equivalent.

Version Holding Solutions

In the first scenario, we store the relationship information in the "general" solution itself, perhaps as a simple array of IDs:

{
    "com.freedomscientific.jaws": {
        "name": "JAWS",
        "versions": [
            "http://registry\\.gpii\\.net/application/com\\.freedomscientific\\.jaws\\.2019",
            "http://registry\\.gpii\\.net/application/com\\.freedomscientific\\.jaws\\.2018"
        ]
        // ...        
    }
}

A key strength of this is that it allows us to express a preferred order of resolution, i.e. from newest to oldest. A disadvantage is that it requires you to know about versions and to update the "general" version when each version is added. Another key thing to note is that if the context of a "general" solution does not match one or more "versioned" equivalents, you might end up with references to solutions that cannot be installed, and which may have been filtered out of the set of solutions already. You would need some mechanism to guard against attempting to retrieve these "phantom limbs".

Application Capabilities

Another option here would be to expand the previous capabilities feature to also cover the "generic" version of the application. Here is a skeleton of a JAWS 2020 entry that might build on the example above.

{
    "com.freedomscientific.jaws.2020": {
        "name": "JAWS 2020",
        "contexts": {
            "OS": [{ "id": "win32" }]
        },
        "capabilities": [
            "http://registry\\.gpii\\.net/application/com\\.freedomscientific\\.jaws/enabled"
        ]        
    }
}

A key strength of this approach is that is that it better supports a "contrib" model, i.e. a volunteer may fill in an entry for JAWS 2020 in isolation, and can clearly express the relationship without modifying the original entry.

This approach is also much safer when filtering by context (generally operating system), in that the relationship between any "versioned" solutions is filtered out along with them when we filter to the context.

Changes to the Device Reporter

In order for either of these strategies to work, the device reporter (or something that coordinates with the device reporter) will have to scan through the full range of solution entries for a given platform, and make sense of the relationships between "versioned" and "general" solutions. It would also have to make the decision about which version to report as installed if multiple versions are installed. We should talk about this in some depth, my assumption is that we will generally prefer the latest version, and that there will almost always be only one version installed.

Which Version to Use/Install?

We can either allow people to "pin" settings to a "versioned" solution, or agree that the "general" version is meant to correspond to the "newest compatible version" for the given context. I would argue against the former unless we have strong use cases.

If we to use the "newest compatible" version of each "general" solution, then how do we define that? Versions may be semver, free strings, really anything. If we decided to have the "general" solution keep track of all its versions, then this can be handled with a prioritised map of versions or with simple array ordering.

An idea that would work with "versioned solutions" is to have our own "internal" version indicator, which indicates whether the version is older (lower in number) than its peers or newer (higher in number). The device reporter or any other part of the system that encounters multiple versions could use the "internal" version indicator to decide (for example) to install the newest compatible version, or to launch the newest installed version.

Install on Demand

Steve Grundell is currently working on adding the ability to install solutions on demand. This work needs to also be made aware of the concept of versions, and we need to make the same decision there about which version to install if there are multiples.

Conclusion

Those are my thoughts on how we might handle versions, I will discuss with the group and flesh things out further.


Comments

comments powered by Disqus