$InvocationPath = Split-Path -Path $MyInvocation.MyCommand.Path -Parent

Add-Type -TypeDefinition @'

using System;
using System.Runtime.InteropServices;

namespace DynaLoadAPI
{
    public class Kernel32DllCall
    {
        [System.Runtime.InteropServices.DllImport("kernel32.dll")]
        static extern bool Wow64DisableWow64FsRedirection( out int OldValue );

        [System.Runtime.InteropServices.DllImport("kernel32.dll")]
        static extern bool Wow64RevertWow64FsRedirection( int OldValue );

        public static int      ptrWow64OldValue;
        public static bool     blValid_Wow64OldValue;

        public Kernel32DllCall()
        {
            // Initialize for the class member(s)
            blValid_Wow64OldValue = false;
        }

        // Public Functions for PowerShell
        public bool dll_DisableWow64FsRedirection()
        {
            bool _blRetVal = false;

            if( blValid_Wow64OldValue == false ){
                _blRetVal = Wow64DisableWow64FsRedirection( out ptrWow64OldValue );
                if( _blRetVal != false ){
                    blValid_Wow64OldValue = true;
                }
            } else {
                _blRetVal = true;
            }

            return( _blRetVal );
        }

        public bool dll_RevertWow64FsRedirection()
        {
            bool _blRetVal = false;
            if( blValid_Wow64OldValue != false ){
                _blRetVal = Wow64RevertWow64FsRedirection( ptrWow64OldValue );
                if( _blRetVal != false ){
                    blValid_Wow64OldValue = false;
                }
            } else {
                _blRetVal = true;
            }

            return( _blRetVal );
        }


    } // End -- Kernel32DllCall Class
}

'@

$Kern32Dll  = New-Object -TypeName DynaLoadAPI.Kernel32DllCall

function DisableWow64FsRedirection
{
    if( $Kern32Dll ){
        $blRetVal = $Kern32Dll.dll_DisableWow64FsRedirection();
    }

} # EOF

function RevertWow64FsRedirection
{
    if( $Kern32Dll ){
        $blRetVal = $Kern32Dll.dll_RevertWow64FsRedirection();
    }

} # EOF

function Set-UwpVideoPlayerPolicy
{
    $_OSVer = [environment]::OSversion.version

    $_mmbSrc = $_OSVer.Major * 1000 + $_OSVer.Minor
    $_mmbTgt = 10 * 1000 + 0
    
    $_blApplyRegistryFix = $false

    if( $_mmbSrc -gt $_mmbTgt ){

        $_blApplyRegistryFix = $true

    } elseif( $_mmbSrc -eq $_mmbTgt ){

        if( $_OSVer.Build -ge 17000 ){
            $_blApplyRegistryFix = $true
        }
    }

    if( $_blApplyRegistryFix -eq $true ){
        #
        # New Policy Setting will be applied for Win 10.0.1700 or later
        #
        "Try to apply UWP VideoPlayer Policy..." | Log-Info
        $_blApplied = $true
        try{ $_iex_cmd = ". reg ADD " + '"HKCU\Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\documentsLibrary\Microsoft.ZuneVideo_8wekyb3d8bbwe"' + ' /v Value /t REG_SZ /d Allow /f'; Invoke-Expression -Command $_iex_cmd -ErrorAction SilentlyContinue 2>&1  } catch { $_blApplied = $false }
        try{ $_iex_cmd = ". reg ADD " + '"HKCU\Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\videosLibrary\Microsoft.ZuneVideo_8wekyb3d8bbwe"'    + ' /v Value /t REG_SZ /d Allow /f'; Invoke-Expression -Command $_iex_cmd -ErrorAction SilentlyContinue 2>&1  } catch { $_blApplied = $false }
        if( $_blApplied -eq $false ){
            "Fail to apply UWP VideoPlayer Policy." | Log-Info
        } else {
            "Success to apply UWP VideoPlayer Policy." | Log-Info
        }
    }

} # EOF

#region setup.ps1

Function Test-IsOnBattery {
    <#
      .SYNOPSIS
       This functions checks if the system is running on battery and if the battery is plugged in.
      
      .DESCRIPTION
       Uses the BatteryStatus WMI class to check if the system is battery powered and writes true/false to a text file
       accrodingly.

      .PARAMETER LogsPath
       Path where the result text file will be saved.
   
      .PARAMETER Keyword
       A keyword identifying scenario when the battery was tested. In our case, it will be BEFORE and AFTER the workload.
        
      .EXAMPLE
       Test-IsOnBattery -LogsPath $LogsPath -Keyword "BEFORE" 
    #>
    
    Param( 
        [parameter(Mandatory=$true)][string]$LogsPath,
        [parameter(Mandatory=$true)][string]$Keyword
    )

    $IsOnBattery = [BOOL](Get-WmiObject -Class BatteryStatus -Namespace root\wmi -ErrorAction SilentlyContinue).PowerOnLine 

    $PowerRoleLog="IsOnBattery.txt"

    "Running on DC $Keyword : $IsOnBattery" | out-file -FilePath "$LogsPath\$PowerRoleLog" -Append
}  

function Write-AuxQualityLogs {
    <#
      .SYNOPSIS
       Writes the output from eight commands to the specified directory.
       
      .DESCRIPTION
       The eight command are: dxdiag /t, dxdiag /x, dispdiag, msinfo32, wevtutil, onfirm-SecureBootUEFI, and HKLM for CodecSelection.
       
      .PARAMETER LogsPath
       The directory into which the eight text files are to be written.   
        
      .EXAMPLE
       Write-AuxQualityLogs -LogsPath 'c:\logs'

    #>
    
    Param( 
        [parameter(Mandatory=$true)][string]$LogsPath
    )

    # DXDiag Log
    dxdiag /t "$LogsPath\DxDiag.txt"
    (Get-Process dxdiag).WaitForExit()

    dxdiag /x "$LogsPath\DxDiag.xml"
    (Get-Process dxdiag).WaitForExit()

    #Dispdiag Log
    dispdiag -out "$LogsPath\DispDiag.dat"

    #MSInfo32 Log
    start msinfo32 -ArgumentList "/report $LogsPath\msinfo32.txt" -Wait

    #WLAN Log
    netsh wlan sho d > "$LogsPath\WLANLog.txt"

    #Code Integrity Log
    wevtutil qe Microsoft-Windows-CodeIntegrity/Operational /rd:true /c:20 /f:Text > "$LogsPath\CodeIntegrity.txt"

    #Secure Boot Status
    try {
        Confirm-SecureBootUEFI -ErrorAction SilentlyContinue > "$LogsPath\SecureBootUEFI.txt"
    }
    catch {}

    #CodecSelection in registry
    reg export "HKLM\SOFTWARE\Microsoft\Windows Media Foundation\CodecSelection\Transcode\Performance" "$LogsPath\CodecSelectionValuesOnImage.reg"
}

#endregion

#region E3.PS1

# Empty E3 database
#
function Move-E3logs {
    <#
      .SYNOPSIS
       Saves off the current Diagnostic Policy Service (DPS) database to the path specified.
       
      .DESCRIPTION
       Stops the DPS service, backups its database (srudb.dat), and then starts the service again.
       
      .PARAMETER ResultsPath
       Path where the dat file is to be backed-up.    
        
      .EXAMPLE
       Move-E3logs -ResultsPath $WorkloadResultsPath

    #>

    Param (
        [parameter(Mandatory=$true)][string]$ResultsPath
    )

    Stop-Service -Name DPS -Force
    Move-Item -Path "$env:WinDir\system32\sru\srudb.dat" -Destination "$ResultsPath\srudb.dat.bak" -Force
    Start-Service -Name DPS
}

# Generate E3 logs in XML and CSV formats
# 
function Write-AuxWorkloadLogs {
    <#
      .SYNOPSIS
       Writes output from the Power Configuration Energy Estimation to the path specified.
       
      .DESCRIPTION
       Enumerates the entire Energy Estimation data from the System Resource Usage Monitor (SRUM) in an XML or CSV file.
       
      .PARAMETER ResultsPath
       Path where the XML and CSV results are written.
        
      .EXAMPLE
       Write-AuxWorkloadLogs -ResultsPath c:\results

    #>
    
    Param (
        [parameter(Mandatory=$true)][string]$ResultsPath
    )

    Invoke-Expression "powercfg /srumutil /xml /output '$ResultsPath\E3-SRUMUTIL-XMLLog.xml'" -ErrorAction SilentlyContinue
    Invoke-Expression "powercfg /srumutil /csv /output '$ResultsPath\E3-SRUMUTIL-CSVLog.csv'" -ErrorAction SilentlyContinue
}
#endregion

#region PS1 Common
function Get-ProcessorArchitecture {
    <#
      .SYNOPSIS
       Returns the processor architecture of the system
   
      .DESCRIPTION
       If the system is AMD64, the architecture returned is AMD64. If the system is ARM64 based, then the result is X86 as
       we run emulated versions. If the system is X86, then X86 is returned.
              
      .EXAMPLE
       $proc_arch = Get-ProcessorArchitecture
    #> 

    return $env:AssessmentExecutionArchitecture
}

# Obtain screen resolution
function Get-ScreenResolution () {
    <#
      .SYNOPSIS
       Gets the screen resolution of the system
   
      .DESCRIPTION
       Gets the screen resolution and returns it as an integer
              
      .EXAMPLE
       $resolution = Get-ScreenResolution
    #> 

    $temp = [string](wmic path Win32_VideoController get CurrentVerticalResolution) -replace '\D+(\d+)','$1'
    return [convert]::ToInt32($temp)
}

function Initialize-AxeLogger {
    <#
      .SYNOPSIS
       Loads the AXE framework from the AXE DLL.
   
      .DESCRIPTION
       Initializes Runtime and creates the Logger helper object.
       
      .PARAMETER AxeCoreNet
       The path to Microsoft.Assessments.Core.dll
       
      .EXAMPLE
       $boolRunningUnderAxe = Initialize-AxeLogger(Get-Item env:\AssessmentAxeBinPath\Microsoft.Assessments.Core.dll)
       Return TRUE if the AXE framework is running and can be initialized.
    #>
    
    Param( 
        [parameter(Mandatory=$true)][string]$AxeCoreNet
    )

    $boolRunningUnderAxe = $false
    if ( Test-Path $AxeCoreNet ) {
        $axeAssembly = [Reflection.Assembly]::LoadFrom( $AxeCoreNet )
        if ( $axeAssembly ) {
            $Global:axeSupport = [Microsoft.Assessments.Runtime.Support]::Initialize()
            if ( $Global:axeSupport ) {
                $boolRunningUnderAxe = $Global:axeSupport.EngineRunning
                $Global:axeLogger = $Global:axeSupport.CreateLogger()
            }
        }
    }
    
    # Make sure we've set up the environment properly
    #
    if ( -not $boolRunningUnderAxe ) {
        "This script only works when run from the Axe framework." | Log-Error
        Exit 1
    }
    
    $boolRunningUnderAxe
}




# This function will write what is piped to it as a string to the default output
# (usually the host/console) and also to the log file if running under axe.
#
function Log-Info() {
    <#
      .SYNOPSIS
       Logs information to AxeLog.txt file
   
      .DESCRIPTION
       Function used to log informative/debug logs to AxeLog.txt file. Uses the AxeLogger set up by Initialize-AxeLogger.
              
      .EXAMPLE
       "Log this information" | Log-Info
    #> 

    PROCESS {
    if ( $_ ) {
        "$_" | Out-Default
        if ( $Global:axeLogger ) {
            $Global:axeLogger.LogMessage( "$_" )
        }
    }
    }}

function Log-Error([int32]$ErrorCode=1) {
    <#
      .SYNOPSIS
       Logs error information to AxeLog.txt file along with the HResult code
   
      .DESCRIPTION
       Function used to log informative/debug logs to AxeLog.txt file. Uses the AxeLogger set up by Initialize-AxeLogger.
              
      .EXAMPLE
       "Log this error" | Log-Error
    #> 
    PROCESS {
    if ( $_ ) {
    
        "$_" | Write-Error
        if ( $Global:axeLogger ) {
            $Global:axeLogger.LogErrorCode( $ErrorCode, "$_" )
        }
    }
    }}

function Get-INI {
    <#
      .SYNOPSIS
       Read the configuration file, MediaAssessment.ini, as a hash table of hash tables, and set $INI as a global variable.

      .DESCRIPTION
       Source: https://blogs.technet.microsoft.com/heyscriptingguy/2011/08/20/use-powershell-to-work-with-any-ini-file/
       Read in each line of the configuration file.
       Pattern match only "section" lines -- [SectionName] -- and "parameter" lines -- name=value -- skipping all other lines, including comments.
       Ignore leading and trailing white spaces in section, name, and value entities.
       Convert all section, name, and value entities to upper case for ease of reading (expecting the user script convention to be upper case, as in the EXAMPLE below).
       For this function to work properly, every name/value pair must be within (follow) a section line, otherwise the INI file is considered here to be malformed.
      
      .PARAMETER INIFilePath
       The absolute or relative path the MediaAssessment.ini file.
       
      .EXAMPLE
       Get-INI -INIFilePath 'C:\MediaQualityAndEnergyAssessment\resources\MediaAssessment.ini'
       [Windows.Forms.Cursor]::Position = "$INI['CURSOR']['X_POSITION'],$INI['CURSOR']['Y_POSITION']"

    #>
    [CmdletBinding()]
    Param(    [ValidateNotNullOrEmpty()]
              [ValidateScript({(Test-Path $_) -and ((Get-Item $_).Extension -eq ".ini")})]
              [Parameter(Mandatory=$True)]
              [string]
          $INIFilePath
          )

    # Make sure the $INI variable is not currently being used.
    if ($INI -ne $null) {
        throw "Conflicting variable name 'INI'. Cannot define $GLOBAL:INI."
    }
    
    # Initialize the GLOBAL variable.
    $GLOBAL:INI = @{}

    Write-Debug ("INI file path '{0}'" -f $INIFilePath)

    # Read the INI file
    Write-Verbose ("Start processing the INI file '{0}'; {1}." -f $INIFilePath, $($MyInvocation.MyCommand.Name) )
    Switch -regex -file $INIFilePath {
            "^\s*\[(.+)\]\s*$" # Section
            {
                $section = $matches[1].Trim()
                $INI[$section] = @{}
                $CommentCount = 0
                Write-Debug ("INI Section line = '{0}'." -f  $_)
            }
            "(.+?)\s*=\s*(.*)" # Parameter
            {
                $name,$value = $matches[1..2].Trim()
                $INI[$section][$name] = $value
                Write-Debug ("INI Parameter line = '{0}'." -f $_)
            }
        }
        Write-Verbose ("Finished processing the INI file '{0}'; {1}." -f $INIFilePath, $($MyInvocation.MyCommand.Name) )
}

#endregion

function Select-PlaybackSource {
<#
  .SYNOPSIS
   Returns an object containing an playback executable and it its arguments, given configuration table, playback source selection, and optional override locations.
   
  .DESCRIPTION
   Check the INI table for required entries. Typically, this parameter is simply taken from the (more extensive) MediaAssessment.ini file.
   Individual construction of the returned Playback object is attempted for the particular PlaybackSource enumeration or value given.
   Typically, the PlaybackSource comes from a user entry selection within the Windows Assessment Console.
   When the enumeration or value indicates a local media file, it is copied to the user's My Video or My Audio file after first verifying the path.
   When the enumeration or value indicates streaming media, an attempt is made to first verify its syntax.
   The emitted Playback object consists of an ExePath and an ArgumentList array.
   The ExePath points to the Scenario Automation Tool, or other workload tool, that plays the media.
   The ArgumentList contains the arguments passed to the tool at the ExePath which in turn tells dictates which media to play and how to play it.
   
  .PARAMETER INI
   Hash table of sections each of whose value is a hash tables of media source name/value pairs.
   
  .PARAMETER PlaybackSource
   A PlaybackSourceEnum value, such as -3, or a video resolution, such as 1080.
   
  .PARAMETER CustomPlaybackSource
   An optional path to a media file that is to be played for this Assessment.

   .PARAMETER Architecture
   The processor architecture of the machine on which the assessment is being run (e.g. x86, amd64).
   
  .EXAMPLE
   $Playback = Select-PlaybackSource -PlaybackSource $PlaybackSource -CustomPlaybackSource $CustomPlaybackSource
   ...
   $OutPut = Start-Executable $Playback.ExePath $Playback.ArgumentList

   In this example, a playback object is passed to the Start-Executable function.
     The playback source selection from the Windows Assessment Console,
     an optional file path to a custom media file that is to be played instead of the default one, also from the Windows Assessment Console.     
#>
    [cmdletbinding()]
    [OutputType('PSCustomObject')]
    param(    [parameter(Mandatory = $true
                        ,Position = 0
                        ,HelpMessage = 'A PlaybackSourceEnum value, such as -3, or a video resolution, such as 1080.')]
              [int32]
           $PlaybackSource,
           
              [parameter(Mandatory=$false
                        ,Position = 1
                        ,HelpMessage = 'An optional path to a media file that is to be played for this Assessment.')]
              [string]
           $CustomPlaybackSource,
           
                [parameter(Position=2,
                         Mandatory=$true,
                         HelpMessage='The processor architecture of the machine on which the assessment is being run')]                 
                [string]
           $Architecture

    )

    # Default return value, that does nothing but returns an error code. 
    $Playback = [PSCustomObject]@{ExePath='cmd.exe';ArgumentList=@('/c exit -1');Workload=[WorkloadEnum]'MOVIESTV'}
    
    # Pre-load ExePath for the ScenarioAutomationTool
    $ScenarioAutomationToolPath = Get-RootName -FileName ([System.IO.FileInfo]($INI['SCENARIO_AUTOMATION']["EXE_PATH_$Architecture"]))
    if (-not(Test-Path $ScenarioAutomationToolPath -PathType Leaf)) {
        throw [System.IO.FileNotFoundException] "$ScenarioAutomationToolPath ScenarioAutomationTool path not found." 
    }
    
    "Checking for required config entries..." | Log-Info
    # Validate that the Media Configuration hash-table of hash-tables contains all required sections and parameters for media selection.
    # Note the use of a question mark (as opposed to an empty string) as a dummy value. This allows the validation method to work properly. 
    $INI_Required  = @{'LOCAL'=@{'720'='?';'1080'='?'}} 
    $INI_Required += @{'STREAMING'=@{'VIDEO'='?';'VIDEO_LOGIN'='?';'VIDEO_PASSWORD'='?'`
                               ;'AUDIO'='?';'AUDIO_LOGIN'='?';'AUDIO_PASSWORD'='?'`
                               ;'NETFLIX'='?'}}
    $INI_Required += @{'SCENARIO_AUTOMATION'=@{'EXE_PATH_x86'='?';'EXE_PATH_amd64'='?';'VIDEO_APPUID'='?';'AUDIO_APPUID'='?';'EDGE_APPUID'='?'}}
    
    $errorMessage = ''
    foreach($section in $INI_Required.Keys) {
        Write-Debug ("Checking for required section='{0}'." -f $section)
        if(-not($INI[$section])) {
            $errorMessage += ("missing section '{0}' " -f $section)
        }
        else {
            foreach($property in $INI_Required[$section].Keys) {
                Write-Debug ("Checking for required section='{0}' property='{1}'." -f $section, $property)

                if(-not($INI[$section][$property])) {
                    $errorMessage += ("missing property '{1}' in sction '{0}' " -f $section, $property)
                }
                elseif( (($INI[$section][$property]).Trim()) -eq '') {
                    $errorMessage += ("empty property '{1}' in sction '{0}' " -f $section, $property)
                }
            }
        }
    }
    
    if ($errorMessage) {
        throw "INI: $errorMessage"
    }

    "Select playback source logic..." | Log-Info
    
    if ($PlaybackSource -gt 0) { # select a specific local content resolution

        "Selecting local media with specified resolution..." | Log-Info
            
        if (-not($mediaSource=$INI['LOCAL'][[string]$PlaybackSource])) {
            throw ("Could not find video content matching selected resolution={0}." -f $PlaybackSource)
        }
        
        $mediaSource = Get-RootName -FileName $mediaSource
        Test-MediaFilePath -FilePath $mediaSource
        Copy-ToMyVideos $mediaSource
        
        $show         = ([System.IO.FileInfo]$mediaSource).BaseName
        $appuid       = $INI['SCENARIO_AUTOMATION']['VIDEO_APPUID']
        $argumentList = @(" -appuid ""{0}"" -show ""{1}"" -clickthrusettings ""More;Repeat""" -f $appuid, $show)
        
        $Playback.ExePath      = $ScenarioAutomationToolPath
        $Playback.ArgumentList = $argumentList
        $Playback.Workload     = [WorkloadEnum]'MOVIESTV'
        
        Write-Debug ("Playback ExePath={0} ArgumentList={1}." -f $Playback.ExePath, $Playback.ArgumentList[0])
    }
    # auto-select content based on native system resolution
    elseif ($playbackSource -eq [PlaybackSourceEnum]::DEFAULT_LOCAL_AUTOSELECT) {

        "Auto-selecting local media..." | Log-Info

        # filter out content that is above the native system resolution
        $LocalContent = @($INI['LOCAL'].Keys | ForEach-Object {[convert]::ToInt32($_)} | sort)
        $SuitableContent = @($LocalContent | where {[convert]::ToInt32($_) -le $(Get-ScreenResolution)} | sort -Descending)

        # select the lowest resolution available
        # if nothing is suitable, default to the lower content resolution above the native one
        if ($SuitableContent.Count -gt 0) {
            $mediaSource = $INI['LOCAL'][[string]($SuitableContent[0])]
        }
        else {
            $mediaSource = $INI['LOCAL'][[string]($LocalContent[0])]
        }
        
        $mediaSource = Get-RootName -FileName $mediaSource
        Test-MediaFilePath -FilePath $mediaSource   
        Copy-ToMyVideos $mediaSource
        
        $show         = ([System.IO.FileInfo]$mediaSource).BaseName
        $appuid       = $INI['SCENARIO_AUTOMATION']['VIDEO_APPUID']
        $argumentList = @(" -appuid ""{0}"" -show ""{1}"" -clickthrusettings ""More;Repeat""" -f $appuid, $show)
        
        $Playback.ExePath      = $ScenarioAutomationToolPath
        $Playback.ArgumentList = $argumentList
        $Playback.Workload     = [WorkloadEnum]'MOVIESTV'
        
        Write-Debug ("Playback ExePath={0} ArgumentList={1}." -f $Playback.ExePath, $Playback.ArgumentList[0])          
    }
    elseif ($PlaybackSource -eq [PlaybackSourceEnum]::DEFAULT_STREAMING_AUDIO) {
                
        "Selecting default streaming audio..." | Log-Info
                
        $appLaunchURI = $INI['STREAMING']['AUDIO']
        if (-not(Test-ValidURI -URI $appLaunchURI -Schemes 'mswindowsmusic')) {
            throw ("Misconstrued streaming audio URI '{0}'. Please check the INI file STREAMING.AUDIO entry." -f $appLaunchURI)
        }
            
        $login        = Resolve-Credential -Cipher $INI['STREAMING']['AUDIO_LOGIN'] 
        $loginpwd     = Resolve-Credential -Cipher $INI['STREAMING']['AUDIO_PASSWORD']
        $argumentList = @(" -applaunchuri ""{0}"" -loginid {1} -loginpwd {2} " -f $appLaunchURI, $login, $loginpwd)

        $Playback.ExePath      = $ScenarioAutomationToolPath
        $Playback.ArgumentList = $argumentList
        $Playback.Workload     = [WorkloadEnum]'GROOVE'
        
        Write-Debug ("Playback ExePath={0} ArgumentList={1}." -f $Playback.ExePath, $Playback.ArgumentList[0])
    }
    elseif ($PlaybackSource -eq [PlaybackSourceEnum]::CUSTOM_STREAMING_AUDIO)  {
    
        "Selecting custom streaming audio..." | Log-Info
                
        if (-not(Test-ValidURI -URI $CustomPlaybackSource -Schemes 'mswindowsmusic')) {
            throw ("Misconstrued streaming audio URI '{0}'. Please check your 'Custom playback source:' input." -f $CustomPlaybackSource)
            }
            
        $login        = Resolve-Credential -Cipher $INI['STREAMING']['AUDIO_LOGIN']
        $loginpwd     = Resolve-Credential -Cipher $INI['STREAMING']['AUDIO_PASSWORD']
        $argumentList = @(" -applaunchuri ""{0}"" -loginid {1} -loginpwd {2} " -f $CustomPlaybackSource, $login, $loginpwd)

        $Playback.ExePath      = $ScenarioAutomationToolPath
        $Playback.ArgumentList = $argumentList
        $Playback.Workload     = [WorkloadEnum]'GROOVE'
        
        Write-Debug ("Playback ExePath={0} ArgumentList={1}." -f $Playback.ExePath, $Playback.ArgumentList[0])
    }
    elseif ($PlaybackSource -eq [PlaybackSourceEnum]::CUSTOM_LOCAL_VIDEO)      {
    
        "Selecting custom local video..." | Log-Info
            
        $CustomPlaybackSource = Get-RootName -FileName $CustomPlaybackSource
        Test-MediaFilePath -FilePath $CustomPlaybackSource
        Copy-ToMyVideos $CustomPlaybackSource

        $show         = ([System.IO.FileInfo]$CustomPlaybackSource).BaseName
        $appuid       = $INI['SCENARIO_AUTOMATION']['VIDEO_APPUID']
        $argumentList = @(" -appuid ""{0}"" -show ""{1}"" -clickthrusettings ""More;Repeat""" -f $appuid, $show)
        
        $Playback.ExePath      = $ScenarioAutomationToolPath
        $Playback.ArgumentList = $argumentList
        $Playback.Workload     = [WorkloadEnum]'MOVIESTV'
        
        Write-Debug ("Playback ExePath={0} ArgumentList={1}." -f $Playback.ExePath, $Playback.ArgumentList[0])
    }
    elseif ($PlaybackSource -eq [PlaybackSourceEnum]::CUSTOM_STREAMING_VIDEO)  {
    
        "Selecting custom streaming video..." | Log-Info
        if (-not(Test-ValidURI -URI $CustomPlaybackSource -Schemes 'mswindowsvideo')) {
            throw ("Misconstrued streaming video URI '{0}'. Please check your 'Custom playback source:' input." -f $CustomPlaybackSource)
        }
        
        $login        = Resolve-Credential -Cipher $INI['STREAMING']['VIDEO_LOGIN']
        $loginpwd     = Resolve-Credential -Cipher $INI['STREAMING']['VIDEO_PASSWORD']
        $argumentList = @(" -applaunchuri ""{0}"" -loginid {1} -loginpwd {2} -clickthrusettings ""More;Repeat""" -f $CustomPlaybackSource, $login, $loginpwd)
                
        $Playback.ExePath      = $ScenarioAutomationToolPath
        $Playback.ArgumentList = $argumentList
        $Playback.Workload     = [WorkloadEnum]'MOVIESTV'
        
        Write-Debug ("Playback ExePath={0} ArgumentList={1}." -f $Playback.ExePath, $Playback.ArgumentList[0])
    }
    elseif ($PlaybackSource -eq [PlaybackSourceEnum]::DEFAULT_STREAMING_VIDEO) {
    
        "Selecting default streaming video..." | Log-Info
        $appLaunchURI =  $INI['STREAMING']['VIDEO']
        if (-not(Test-ValidURI -URI $appLaunchURI -Schemes 'mswindowsvideo')) {
            throw ("Misconstrued streaming video URI '{0}'. Please check the INI file STREAMING.VIDEO entry." -f $appLaunchURI)
        }

        $login        = Resolve-Credential -Cipher $INI['STREAMING']['VIDEO_LOGIN']
        $loginpwd     = Resolve-Credential -Cipher $INI['STREAMING']['VIDEO_PASSWORD']  
        $argumentList = @(" -applaunchuri ""{0}"" -loginid {1} -loginpwd {2} -clickthrusettings ""More;Repeat""" -f $appLaunchURI, $login, $loginpwd)

        $Playback.ExePath      = $ScenarioAutomationToolPath
        $Playback.ArgumentList = $argumentList
        $Playback.Workload     = [WorkloadEnum]'MOVIESTV'
        
        Write-Debug ("Playback ExePath={0} ArgumentList={1}." -f $Playback.ExePath, $Playback.ArgumentList[0])                  
    }
    elseif ($PlaybackSource -eq [PlaybackSourceEnum]::NETFLIX)                 {
    
        "Selecting Netflix..." | Log-Info
                
        $appLaunchURI =  $INI['STREAMING']['NETFLIX']
        if (-not(Test-ValidURI -URI $appLaunchURI -Schemes 'netflix')) {
            throw ("Misconstrued Netflix URI '{0}'.. Please check the INI file STREAMING.NETFLIX entry." -f $appLaunchURI)
        }
        
        $argumentList = @(" -applaunchuri ""{0}"" " -f $appLaunchURI)

        $Playback.ExePath      = $ScenarioAutomationToolPath
        $Playback.ArgumentList = $argumentList
        $Playback.Workload     = [WorkloadEnum]'NETFLIX'
        
        Write-Debug ("Playback ExePath={0} ArgumentList={1}." -f $Playback.ExePath, $Playback.ArgumentList[0])                  
    }
    elseif ($PlaybackSource -eq [PlaybackSourceEnum]::YOUTUBE_CUSTOM) {

        "Selecting Youtube Custom..." | Log-Info

        if (-not(Test-ValidURI -URI $CustomPlaybackSource -Schemes 'https')) {
            throw ("Misconstrued Youtube Custom URI '{0}'. Please check your 'Custom playback source:' input." -f $CustomPlaybackSource)
        }

        $appuid       = $INI['SCENARIO_AUTOMATION']['EDGE_APPUID']
        $argumentList = @(" -appuid ""{0}"" -hyperlink ""{1}"" -clickthrusettings ""Settings; Quality; 1080p"" -operations ""Loop=on""" -f $appuid, $CustomPlaybackSource)

        $Playback.ExePath      = $ScenarioAutomationToolPath
        $Playback.ArgumentList = $argumentList
        $Playback.Workload     = [WorkloadEnum]'YOUTUBE'

        Write-Debug ("Playback ExePath={0} ArgumentList={1}." -f $Playback.ExePath, $Playback.ArgumentList[0])
    }
    elseif ($PlaybackSource -eq [PlaybackSourceEnum]::YOUTUBE_DEFAULT) {

        "Selecting Youtube Default..." | Log-Info

        $appLaunchURI =  $INI['STREAMING']['YOUTUBE']
        if (-not(Test-ValidURI -URI $appLaunchURI -Schemes 'https')) {
            throw ("Misconstrued Youtube Default URI '{0}'. Please check the INI file STREAMING.YOUTUBE entry." -f $appLaunchURI)
        }

        $appuid       = $INI['SCENARIO_AUTOMATION']['EDGE_APPUID']
        $argumentList = @(" -appuid ""{0}"" -hyperlink ""{1}"" -clickthrusettings ""Settings; Quality; 1080p"" -operations ""Loop=on""" -f $appuid, $appLaunchURI)

        $Playback.ExePath      = $ScenarioAutomationToolPath
        $Playback.ArgumentList = $argumentList
        $Playback.Workload     = [WorkloadEnum]'YOUTUBE'

        Write-Debug ("Playback ExePath={0} ArgumentList={1}." -f $Playback.ExePath, $Playback.ArgumentList[0])
    }
    # this should never happen because the select integer is outside of the defined range (undefined)
    else   {throw "Couldn't find media content matching user selection."}
    
return $Playback
}

function Stop-Workload {
<#
  .SYNOPSIS
   Check if known workloads are running and, if so, stop them. Optionally close specific workloads.

  .DESCRIPTION
   Assumption: for the purpose of any Assessment, we normally want to close all known workloads, even ones that are not the under current test.
   
   
  .PARAMETER Workload
   An optional array of workload enums that you want to stop. When this parameter is absent, all enumerated workloads will be stopped.


  .EXAMPLE
   Stop-Workload ([WorkloadEnum]'MOVIESTV')

   Stops any running Movies & TV workload currently playing. 
   Logs 

  .EXAMPLE
   Stop-Workload

   Stops all known workloads -- all those in the WorkloadEnum enumerator.
#>

    [cmdletbinding()]
    param(    [parameter(Position=0,
                         Mandatory=$false,
                         HelpMessage='An array of WorkloadEnum that you want to stop.')]
              [WorkloadEnum[]]
          $Workload
        )

    $errorArray = @()

    # Default: stop all known workloads
    if (-not($Workload)) {$Workload = [WorkloadEnum]::GetValues([WorkloadEnum])}

    switch ($Workload) {
        ([WorkloadEnum]'MOVIESTV') {if(-not(Stop-MoviesTV)) {$errorArray += 'Movies & TV'} }
        ([WorkloadEnum]'GROOVE')   {if(-not(Stop-Groove))   {$errorArray += 'Groove'} }
        ([WorkloadEnum]'NETFLIX')  {if(-not(Stop-Netflix))  {$errorArray += 'Netflix'} }
        ([WorkloadEnum]'YOUTUBE')  {if(-not(Stop-Youtube))  {$errorArray += 'Youtube'} }
    }      

    if ($errorArray) {
        $errorMessage = ("Error stopping workloads: {0}." -f $($errorArray -join ','))
        throw $errorMessage
    }
}

function Install-PlaybackPackage {
<#
  .SYNOPSIS
   Locate and run the install function, if any, for the workload provided by the Workload parameter.
   
  .DESCRIPTION
   For any workload that has an associated workload package that may need to be installed,
   run that installer package.
   
  .PARAMETER Workload
   The WorkloadEnum value of a workload whose package may need to be installed.
   
  .PARAMETER Architecture
   The processor architecture of the machine on which the assessment is being run (e.g. x86, amd64).   
#>
    [cmdletbinding()]
    param(    [parameter(Position=0,
                         Mandatory=$true,
                         HelpMessage="Playback workload, e.g. `$Playback.Workload")]
               [WorkloadEnum]
           $Workload,
           
                [parameter(Position=1,
                         Mandatory=$true,
                         HelpMessage='The processor architecture of the machine on which the assessment is being run')]                 
                [string]
           $Architecture                  
    )

    switch ($Workload) {
        ([WorkloadEnum]'NETFLIX')    {Install-NetflixPlaybackPackage $Architecture}
    }
}

function Get-ConfigValue {
<#
  .SYNOPSIS
   Recovers an entry from the configuration file. Returns an empty string if no entry is found and no default is given. Empty entries and defaults are treated as not found.
   
  .DESCRIPTION
   Read off the Value from the INI configuration file, using the supplied INI file Section and the supplied Key within the Section.
   The INI file format looks like...
     [SomeSectionName]
     Key1=Value1
     Key2=Value2
   If no Value is found, or an empty value is found,
   use the optional (non-empty) default value supplied.
   
  .PARAMETER Section
   The [Section] within the INI configuration file.
   
  .PARAMETER Key
   The Key whose Value is to be recovered. The Key=Value is located within the INI [Section].
  
  .PARAMETER Default
   An optional replacement Value to be used should a Key=Value pair be missing from within the INI [Section]. 
  
  .EXAMPLE
   [int](Get-ConfigValue -Section 'CURSOR' -Key 'X_POSITION' -Default '100')
   
   This returns the integer x-position for the windows cursor, or 100 if none is found in the INI configuration file.
   
  .EXAMPLE
   if (-not( $ExePath = (Get-ConfigValue 'SCENARIO_AUTOMATION' 'EXE_PATH_x86' )) ) {throw "Invalid or missing path to the ScenarioAutomationTool"}
   
   This returns the path to the Scenario Automation Tool from the INI configuration file. If a blank string is returned, an error is thrown.
  
#>
    [cmdletbinding()]
    [OutputType('string')]
param(      [ValidateNotNullOrEmpty()]
            [parameter(Position=0,
                       Mandatory=$true,
                      HelpMessage='The [Section] within the INI configuration file.')]
            [string]
        $Section,
            
            [ValidateNotNullOrEmpty()]
            [parameter(Position=1,
                       Mandatory=$true,
                       HelpMessage='The Key whose Value is to be recovered.')]
            [string]
        $Key,

            [parameter(Position=2,
                       Mandatory=$false,
                       HelpMessage='Optional replacement Value if Key=Value pair is missing.')]
            [string]
        $Default
    )
    
    "Recovering config file entry [$section][$key]..." | Log-Info
    try {
        $thisValue = $INI[$section][$key]
    }
    catch {
        $thisValue = ''
    }
    if ($thisValue) {
        "Config file entry $thisValue found..." | Log-Info
    }
    else {
        "Config file entry not found. Checking for default value..." | Log-Info
        if ( $default ) {
            "Using default  config value '$default'.." | Log-Info
            $thisValue = $default
        }
        else {
            "No default config value found..." | Log-Info
        }
    }
    
return $thisValue
}

function Test-ValidFileName {
<#
  .SYNOPSIS
   Returns FALSE if the supplied file name is NOT well-formatted. Returns FALSE if the file name extension is NOT among the optional array of expected extensions. TRUE otherwise.
   
  .DESCRIPTION
   Use .NET FileInfo create object to trap malformed file names. If create object fails, return FALSE.
   If the file name is well-formed and an array of expected extensions is supplied,
   then find the extension of the file name and test if it is among those expected. Return FALSE if it is not found.
   Otherwise return TRUE.
   
  .PARAMETER FileName
   The name of the file, absolute or relative, to be validated. The file need not exists.
   
  .PARAMETER Extension
   Optional array of file name extensions (including the dot prefix). E.g. '.etl', '.txt'.
   
  .EXAMPLE
   Test-ValidFileName -FileName 'MediaQuality.etl' -Extension '.etl'
   
   Returns TRUE because the string "MediaQuality.etl" does not contain any invalid characters (such as '<') or other file name oddities, and because its extension is ".etl". 
   
  .EXAMPLE
  Test-ValidFileName 'results.txt'
  
  Returns TRUE because System.IO.FileInfo('results.txt') does not throw and error (and because no optional array of expected file extensions was given).
  
#>

    [cmdletbinding()]
    [OutputType('bool')] 
param(      [parameter(Position=0,
                       Mandatory=$true,
                       HelpMessage='The name of the file, absolute or relative, to be validated. The file need not exists')]
            [string]
        $FileName,
            
            [parameter(Position=1,
                       Mandatory=$false,
                       HelpMessage="Optional array of file name extensions (including the dot prefix). E.g. '.etl', '.txt'.")]
            [string[]]
        $Extension
    )
                
    try{
        $FileInfo = New-Object System.IO.FileInfo($FileName) -ErrorAction Stop
        if ($Extension) {
            if ( $Extension -notcontains ($FileInfo.Extension) ) {
                return $false
            }
        }
    }
    catch {
        return $false
    }
    
    return $true
}

function Test-FileExists  {
<#
  .SYNOPSIS
   Verify that the given path to a file (relative or absolute) exits, and if not, handle it as an error or a warning, depending on the Optional flag.
   
  .PARAMETER Path
   Relative or absolute path to a file whose existence is to be tested.
   
   .PARAMETER FromInvocation
    Typically the $MyInvcation PowerShell Automatic Variable or equivelent InvocationInfo object.
    
  .PARAMETER Optional
   Flag indicating how a non-existence Path is to be handled; as an informational warning (if the flag is set) or a terminating error.
   
  .EXAMPLE
   Test-FileExists -Path C:\results\results.csv
   
   If the CSV file exists, nothing happens. Otherwise the function will throw an error
   (typically caught and written to the console and, if it exists, to the AXE log, followed by exiting the script).
   

  .EXAMPLE
   Test-FileExists -Path logs\results.log -FromInvocation $MyInvocation -Optional
   
   If the log file exists, nothing happens. 
   Otherwise, an informational message containing the name of the function calling the script will be written to the console 
   and, if it exits, to the AXE Error log.
 
  
#>
    [CmdletBinding()]
    param(
                [ValidateNotNullOrEmpty()]
                [parameter(Mandatory=$true
                          ,Position=0
                          ,HelpMessage='Relative or absolute path to a file whose existence is to be tested.')]
                [string]
            $Path,
            
                [parameter(Mandatory=$false
                          ,Position=1
                          ,HelpMessage='Typically $MyInvocation from the calling function.')]
                [object]
            $FromInvocation,
            
                [parameter(HelpMessage='Flag indicating that a non-existent path is only reported and not treated as a terminating error.')]
                [switch]
            $Optional
    )
    
    $fromInfo=''
    if($FromInvocation -and ($FromInvocation.GetType().Name -eq 'InvocationInfo')) {
        $fromInfo = $FromInvocation.MyCommand.Name
    }

    $message = ("file {0} does not exist; {1}." -f $Path, $fromInfo)
    
    if(-not(Test-Path -Path $Path -PathType Leaf)) {
        if($Optional) {
            $message | Log-Info
        }
        else {
            throw $message
        }
    }
}

function Test-IsMetric {
<#
  .SYNOPSIS
   Returns TRUE if the input Metric is a singular [string] (as opposed to an array) which can be converted to a [double].
   
  .DESCRIPTION
  
  .PARAMETER Metric
   The object under to be tested as a string representation of a [double].
   
  .EXAMPLE
   Test-IsMetric -Metric '1.23','1.34'
   
   Returns FALSE because the object under test is an array of strings.

  .EXAMPLE
   Test-IsMetric -Metric ''
   Returns FALSE because the object under test cannnot be converted to a [double].

  .EXAMPLE
   Test-IsMetric -Metric '3'

   Returns TRUE because the object under test is a string which can be converted to a double.   
   
#>
    param(
                [parameter(Mandatory=$true
                        ,Position=0
                        ,HelpMessage='The object under to be tested as a string representation of a [double].')]
                [object]
            $Metric
        )
        if (-not( ($Metric.GetType().Name) -eq 'String') ) {return $false}
        $d = [double]0
        if (-not([double]::TryParse($Metric,[ref]$d)))     {return $false}
        
return  $true
}

#region Helper Functions
function Install-NetflixPlaybackPackage {
<#
  .SYNOPSIS
   Workload-specific package installer for the Netflix Windows Store app.
   
  .DESCRIPTION
   Check if the workload-specific package is already installed.
   If not, attempt to install it.
   Check again afterwards, if the package actually got installed.

  .PARAMETER Architecture
   The processor architecture of the machine on which the assessment is being run (e.g. x86, amd64).
  
  .EXAMPLE
   Install-NetflixPlaybackPackage
   
   Attempts to install the Windows Store Netflix app.
   
#>

param(      [parameter(Position=0,
                       Mandatory=$true,
                       HelpMessage='The processor architecture of the machine on which the assessment is being run')]
            [string]
        $Architecture
    )
    
    # Get the Netflix-specific config file entries.
    try {
        
        $PackageFamilyName = (($INI['SCENARIO_AUTOMATION']['NETFLIX_APPUID'] -split '!')[0])
        $ExePath           =  ($INI['SCENARIO_AUTOMATION']["EXE_PATH_$Architecture"])
        $StoreUid          =  ($INI['SCENARIO_AUTOMATION']['STORE_UID'])
    }
    catch {
        throw ("Netflix SCENARIO_AUTOMATION section not found. Please check config file SCENARIO_AUTOMATION; {0}" -f $_.Exception.Message)
    }
     
    # Check that the config files actually contain something.
    if ( !$PackageFamilyName -or !$ExePath -or !$StoreUid ) {
        throw "Netflix configurations empty or missing. Please check config file SCENARIO_AUTOMATION section for NETFLIX_APPUID, EXE_PATH_x86, EXE_PATH_amd64, and NETFLIX_STOREUID."
    }
    
    # Translate executable path to absolute path if necessary.
    $ExePath = Get-RootName -FileName $ExePath
    
    "Checking if Windows Store Netflix app is already installed..." | Log-Info
    if (Test-AppxPackageInstalled -PackageFamilyName $PackageFamilyName) {
        "Windows Store Netflix app already installed. Skipping installation..." | Log-Info
        return
    } else {
        ### Netflix is not installed. Throw error asking user to install and login with valid credentials
        throw "Netflix app is not installed. Please install the app, login with valid credentials and re-run the assessment"
    }
    
    "Installing Windows Store Netflix app..." | Log-Info
    $OutPut = Start-Executable -ExePath $ExePath `
                 -ArgumentList (" -installonly Netflix -storeuid ""{0}""" -f $StoreUid)
        
    try {
        $seconds = [int](Get-ConfigValue -Section 'SCENARIO_AUTOMATION' -Key 'STORE_SYNC_SECONDS' -Default '30')
    }
    catch {
        throw "Missing or invalid config file value STORE_SYNC_SECONDS in section SCENARIO_AUTOMATION."
    }
        
    "Waiting $seconds seconds for Netflix install to stabilize..." | Log-Info
    Start-Sleep -Seconds $seconds
        
    "Post-install Netflix package check..." | Log-Info
    if (-not(Test-AppxPackageInstalled -PackageFamilyName $PackageFamilyName)) {
        throw ("Netflix Windows app not successfully installed (missing PackageFamilyName '{0}'). Please wait and try again." -f $PackageFamilyName)
    }
        
    "Windows Store Netflix app successfully installed..." | Log-Info
}

function Stop-MoviesTV {
<#
  .SYNOPSIS
   Stop any Movies & TV app video that is currently playing.

  .DESCRIPTION
   Log progress.
   Call Stop-WindowsStoreApp with Movies & TV specific parameters.

  .EXAMPLE
   
   Stop-MoviesTV

   Logs "Stopping workload Movies & TV..."
   Logs "Workload Movies & TV successfully stopped..."
   
#>
    "Stopping workload Movies & TV..." | Log-Info
    $PackageFamilyName = ($INI['SCENARIO_AUTOMATION']['VIDEO_APPUID'] -split '!')[0]
    $Name              =  $INI['SCENARIO_AUTOMATION']['VIDEO_PROC']
    Write-Debug ("Stopping appx PackageFamilyName '{0}' process Name '{1}'." -f $PackageFamilyName, $Name)
    
    if (Stop-WindowsStoreApp -PackageFamilyName $PackageFamilyName -Name $Name) {
        "Workload Movies & TV successfully stopped..." | Log-Info
        return $true
    }
    else {
        "Workload Movies & TV NOT stopped..." | Log-Info
        return $false
    }  
}

function Stop-Groove {
<#
  .SYNOPSIS
   Stop any Groove audio that is currently playing.

  .DESCRIPTION
   Log progress.
   Call Stop-WindowsStoreApp with Groove Music specific parameters.

  .EXAMPLE
   
   Stop-Groove

   Logs "Stopping workload Groove..."
   Logs "Workload Groove successfully stopped..."
   
#>

   "Stopping workload Groove..." | Log-Info

    $PackageFamilyName = ($INI['SCENARIO_AUTOMATION']['AUDIO_APPUID'] -split '!')[0]
    $Name              =  $INI['SCENARIO_AUTOMATION']['AUDIO_PROC']
    Write-Debug ("Stopping appx PackageFamilyName '{0}' process Name '{1}'." -f $PackageFamilyName, $Name)
        
    if (Stop-WindowsStoreApp -PackageFamilyName $PackageFamilyName -Name $Name) {
        "Workload Groove successfully stopped..." | Log-Info
        return $true
    }
    else {
        "Workload Groove NOT stopped..." | Log-Info
        return $false
    }     
}

function Stop-Netflix {
<#
  .SYNOPSIS
   Stop any Netflix movies that is currently playing.

  .DESCRIPTION
   Log progress.
   Call Stop-WindowsStoreApp with Netflix specific parameters.

  .EXAMPLE
   
   Stop-Netflix

   Logs "Stopping workload Netflix..."
   Logs "Workload Netflix successfully stopped..."
   
#>

   "Stopping workload Netflix..." | Log-Info

    $PackageFamilyName = ($INI['SCENARIO_AUTOMATION']['NETFLIX_APPUID'] -split '!')[0]
    $Name              =  $INI['SCENARIO_AUTOMATION']['NETFLIX_PROC']
    Write-Debug ("Stopping appx PackageFamilyName '{0}' process Name '{1}'." -f $PackageFamilyName, $Name)
        
    if (Stop-WindowsStoreApp -PackageFamilyName $PackageFamilyName -Name $Name) {
        "Workload Netflix successfully stopped..." | Log-Info
        return $true
    }
    else {
        "Workload Netflix NOT stopped..." | Log-Info
        return $false
    } 
}

function Stop-Youtube {
<#
  .SYNOPSIS
   Stop the Youtube that is currently playing.

  .DESCRIPTION
   Log progress.
   Call Stop-WindowsStoreApp with Youtube specific parameters.

  .EXAMPLE

   Stop-Youtube

   Logs "Stopping workload Youtube..."
   Logs "Workload Youtube successfully stopped..."

#>

   "Stopping workload Youtube..." | Log-Info

    $PackageFamilyName = ($INI['SCENARIO_AUTOMATION']['EDGE_APPUID'] -split '!')[0]
    $Name              =  $INI['SCENARIO_AUTOMATION']['EDGE_PROC']
    Write-Debug ("Stopping appx PackageFamilyName '{0}' process Name '{1}'." -f $PackageFamilyName, $Name)

    if (Stop-WindowsStoreApp -PackageFamilyName $PackageFamilyName -Name $Name) {
        "Workload Youtube successfully stopped..." | Log-Info
        return $true
    }
    else {
        "Workload Youtube NOT stopped..." | Log-Info
        return $false
    }
}

function Stop-WindowsStoreApp {
<#
  .SYNOPSIS
   Stop all running processes for the specified Windows Store app.

  .DESCRIPTION
   Determine if the Windows Store package is even installed for the user.
   If so, determine if any of the package's processes are running.
   and if so, stop them, waiting for their completion.

  .PARAMETER AppxPackage
   The Name of the app package (.appx) within the user profile (i.e. Get-AppxPackage) associated with the process name that is to be stopped.
    
  .PARAMETER Name
   The Name (i.e. from Get-Process) that is to be stopped.
    
  .EXAMPLE
   Stop-WindowsStoreApp 'Microsoft.ZuneVideo' 'Video.UI'

   Attempts to stop all Video.UI processes, which are part of the Windows Store app named Microsoft.ZuneVideo.

#>
    [cmdletbinding()]
    [OutputType('bool')]    
    param(    [ValidateNotNullOrEmpty()]
              [parameter(Position=0,
                         Mandatory=$true,
                         HelpMessage='The Package Family Name of the app package (.appx) that is to be stopped.')]
              [string]
          $PackageFamilyName,

              [ValidateNotNullOrEmpty()]
              [parameter(Position=1,
                         Mandatory=$true,
                         HelpMessage='The Process Name that is to be stopped.')]
              [string]
          $Name
          )
           
    # Initialize the WaitForExit Timeout maximum seconds to wait for a process to exit.
    $Timeout = 30

    # Determine if the Windows Store package is even installed for this user.
    if (Test-AppxPackageInstalled -PackageFamilyName $PackageFamilyName) {
        # Stop all processes for the package.
        Get-Process -Name $Name -ErrorAction SilentlyContinue | ForEach-Object {
            $Id = $_.Id
            Stop-Process -Id $Id                   -ErrorAction SilentlyContinue
            Wait-Process -Id $Id -Timeout $Timeout -ErrorAction SilentlyContinue
        }        
        
        try {
            $milliseconds = [int](Get-ConfigValue -Section 'SCENARIO_AUTOMATION' -Key 'STOP_PROC_WAITFOR_MILLISECOND' -Default '1500')
        }
        catch {
            throw "Missing or invalid config file value STOP_PROC_WAITFOR_MILLISECOND in section SCENARIO_AUTOMATION."
        }
    
        # Check that all the processes can be determined to have cleanly stopped (after waiting to avoid race condition).
        Start-Sleep -Milliseconds $milliseconds
        if (Get-Process -Name $Name -ErrorAction SilentlyContinue) {
            return $false
        }
    }

    return $true
}

function Test-AppxPackageInstalled {
<#
  .SYNOPSIS
    Determine if the Windows Store package is even installed for this user.
    
  .DESCRIPTION
   Recover the package name from INI configuration file. It is the first part of the APPUID.
   Use the PowerShell Get-AppxPackage cmdlet to determine if it is insalled for the current user.
   
  .PARAMETER
   PackageFamilyName
   
   The Package Family Name (i.e. PackageFamilyName column in Get-AppxPackage).
   
  .EXAMPLE
   Test-AppxPackageInstalled -PackageFamilyName 'Microsoft.ZuneVideo_8wekyb3d8bbwe'
   
   This returns TRUE if the Movies & TV Windows Store app is installed for the current user. Returns FALSE otherwise.
   
#>
param(    [ValidateNotNullOrEmpty()]
          [parameter(Position=0,
                     Mandatory=$true,
                     HelpMessage='The PackageFamilyName of an Appx Package to be tested for the current user.')]
          [string]
       $PackageFamilyName
       )

       if ( ((Get-AppxPackage | Where-Object {$_.PackageFamilyName -eq $PackageFamilyName}).count) -gt 0) {
           return $true
       } 
       else {
           return $false
       }
}

function Start-StreamingWorkload {
<#
  .SYNOPSIS
   When a streaming media has been selected as the workload, launch the appropriate streaming app once to synchronize the library collections.
   
  .DESCRIPTION
   From the configuration file, read off the number of seconds necessary to allow a library to synchronize.
   Test the "Playback source:" selected by the user and launch Movies & TV or Groove Music, if appropriate,
   and then pause the script for the seconds necessary to allow synchronization.
   Verify that a process corresponding to the streaming app can be found.
   
  .PARAMETER PlaybackSource
   The (integer) "Playback source:" selected by the user.
   
  .EXAMPLE
   Start-StreamingWorkload -PlaybackSource -5
   
   Starts the Movies & TV app for 30 seconds (configurable) to allow time for library sync. 
   
  .EXAMPLE
   Start-StreamingWorkload -PlaybackSource 1080
   
   Does nothing. The PlaybackSource is not streaming media.
#>
param (    [parameter(Position=0,
                      Mandatory=$true,
                      HelpMessage='The (integer) "Playback source:" selected by the user.')]
            [int]
        $PlaybackSource
    )
    
    # From the configuration file, read off the seconds to pause the script for library synchronization (or use a default value). 
    try {
        $seconds = [int]$INI['STREAMING']['LIBRARY_SYNC_SECONDS']
    }
    catch {
        "Missing or invalid config STREAMING.LIBRARY_SYNC_SECONDS..." | Log-Info
    }
    
    if (-not($seconds)) {
        $seconds = 30
        "Using default sleep seconds $seconds..." | Log-Info
    }
    
    # Determine the underlying Windows app process, if any, that is to be launched.
    $command = ''
    switch ($PlaybackSource) {
        ([int]([PlaybackSourceEnum]::CUSTOM_STREAMING_VIDEO))  {$command = 'mswindowsvideo:'; $process='Video.UI';break}
        ([int]([PlaybackSourceEnum]::DEFAULT_STREAMING_VIDEO)) {$command = 'mswindowsvideo:'; $process='Video.UI';break}
        ([int]([PlaybackSourceEnum]::CUSTOM_STREAMING_AUDIO))  {$command = 'mswindowsmusic:'; $process='Music.UI';break}
        ([int]([PlaybackSourceEnum]::DEFAULT_STREAMING_AUDIO)) {$command = 'mswindowsmusic:'; $process='Music.UI';break}
    }
    
    # Launch the process.
    if ($command) {
        try {
            start $command
        }
        catch {
            throw ("Error starting process '{0}'; {1}" -f $command, $_.Exception.Message)
        }
        
        Start-Sleep -Seconds $seconds
        
        if (-not(Get-Process -Name $process -ErrorAction SilentlyContinue)) {
            throw ("Process '{0}' not found after launching library synchronization command '{1}'." -f $process, $command)
        }
        

    }
}

function Test-ValidURI {
<#
  .SYNOPSIS
   Returns TRUE if the supplied URI is well-formatted of type Absolute (per .NET System.Uri class) and has a scheme of one of those supplied.
   
  .DESCRIPTION
   Validate that the scheme of the supplied URI is one of the expected Assessment-specific schemes.
   If so, simply call the appropriate .NET method.
   Ref. https://msdn.microsoft.com/en-us/library/system.uri.iswellformeduristring(v=vs.110).aspx
   
  .PARAMETER URI
   The URI under test.
   
  .PARAMETER Schemes
   An array of expected schemas for the URI under test.
   
  .EXAMPLE
   Test-ValidURI -URI 'mswindowsvideo://play/?bigCatId=8D6KGWZRNQ91&zestId={30b99908-0100-11db-89ca-0019b92a3933}&itemType=movie' -Schemes 'mswindowsvideo'
   
   True  
#>      
    [cmdletbinding()]
    [OutputType('bool')]
    param(    [ValidateNotNullOrEmpty()]
              [parameter(Mandatory=$true,
                         Position=0,
                         HelpMessage='The URI under test.')]
              [string]
          $URI,
               
               [ValidateNotNullOrEmpty()]
               [parameter(Mandatory=$false,
                          Position=1,
                          HelpMessage='Valid URI schemes')]
               [string[]]
          $Schemes
         )
    
    try {
        $uriObj = New-Object System.Uri($URI)
    }
    catch {
        throw "Failed creating Uri object. Please check the INI file STREAMING section for malformed URIs."
    }
    
    if ( [System.Uri]::IsWellFormedUriString($URI,'Absolute') ) {
        if ( $Schemes -and ($Schemes -notcontains ($uriObj.Scheme)) ) {
            return $false
        }
        return $true
    }
    else {return $false}
}

function Log-ToXmlErrorFile {
    <#
      .SYNOPSIS
       Logs the exception details to ErrorsAndWarning.xml file.
   
      .DESCRIPTION
       In case of fatal error or an exception, this function logs the error or exception details to ErrorsAndWarnings.xml file
       so that the details are displayed directly on the WAC UI for user to check.
              
      .EXAMPLE
       Log-ToXmlErrorFile -XmlErrorFile $xmlErrorFile -ErrorCode $_.Exception.Hresult -ErrorMessage $message
    #> 

    Param(
        [parameter(Mandatory=$true)][String]$XmlErrorFile,
        [parameter(Mandatory=$true)]$ErrorCode,
        [parameter(Mandatory=$true)][String]$ErrorMessage
    )
    
    try
    {
        $XmlWriter = $Global:axeSupport.CreateResultSnippet()
        $XmlWriter.AddError([Convert]::ToUInt32("{0:X0}" -f $ErrorCode), $ErrorMessage)
        $XmlWriter.Save($XmlErrorFile)
    }
    catch [Exception]
    {
        # Log the exception to AXE log
        "Error occured while writing the Errors XML file" | Log-Error
        $_.Exception | Format-List -Force | Log-Error
    }
}

function Start-Executable {
    <#
      .SYNOPSIS
       Executes the input exe as a process and captures the xonsole output of the exe and sets the LASTEXITCODE to te process exit code.

      .DESCRIPTION
       Executes the input exe. Reads the console output of exe and returns the console output.
       Once the process exits, this function sets the LASTEXITCODE to the process' exit code.
       If any exceptions occur, then returns the expception details.
       If the process does not exit within the specified period of wait time which is configurable through
       a INI file, then the fucntion sets the LASTEXITCODE to 1 indicating a failure.
        
      .EXAMPLE
       $ArgList = "-installonly Netflix -storeuid Microsoft.WindowsStore_8wekyb3d8bbwe!App -loginid <LoginId> -loginpwd <LoginPwd>"
       $Output = Start-Executable -ExePath C:\MediaQuality\x86\ScenarioAutomationTool.exe -ArgumentList $ArgList       
    #>
    Param(
        [String] $ExePath,
        [String[]] $ArgumentList
    )
    $process = New-Object System.Diagnostics.Process
    $process.StartInfo.FileName = $ExePath
    $process.StartInfo.Arguments = $ArgumentList
    $process.StartInfo.UseShellExecute = $false
    $process.StartInfo.CreateNoWindow = $true
    $process.StartInfo.RedirectStandardOutput = $true

    try {
        if ( $process.Start() ) {
            $output = $process.StandardOutput.ReadToEnd() -replace "\r\n$",""

            if ( $output ) {
                if ( $output.Contains("`r`n") ) {
                    $output -split "`r`n"
                }
                elseif ( $output.Contains("`n") ) {
                    $output -split "`n"
                }
                else {
                    $output
                }
            }
            
            $WaitForExit = Get-ConfigValue -Section EXECUTION -Key EXIT_WAIT_FOR -Default '3000'
            if (-not(Test-IsMetric -Metric $WaitForExit)) {
                throw "Invalid [EXECUTION]EXIT_WAIT_FOR value. Please check INI file for non-integer entry."
            }

            $process.WaitForExit([int]$WaitForExit)    ### Make this value configurable through INI file we are using
    
            # Ideally, the process should have exited. If for some reason it has not, then we verify the same
            # and exit accordingly
            if ($process.HasExited)
            {
                # Since Start-Process does not update the lastexitcode, we exit this process with the process exit code.
                # This way we can rely on the lastexitcode variable to check for any errors.
                & "$Env:SystemRoot\system32\cmd.exe" /c exit $process.ExitCode
            }
            else
            {
                # Exit the process with a non-zero exit code which is handled in the calling script
                "Process has not exited"
                & "$Env:SystemRoot\system32\cmd.exe" /c exit 1
            }      
        }
    }
    catch
    {
        "Exception details:"
        $details = $_.Exception.Message + [Environment]::NewLine + $_.InvocationInfo.PositionMessage
        $details
    }
}

function Copy-ToMyVideos {
<#
  .SYNOPSIS
   Copies one or more video files into the user's Video folder, which is where the Movies & TV app expects to find it.
   
  .DESCRIPTION
   Validates that the environment MyVideos folder for the user that is running the script exists.
   Copies the given file(s) to the MyVideos folder of the user running the script.
  
  .PARAMETER FilePath
   Absolute or relative path to the file that is to be copied into the Video folder of the user running the script.
   
  .EXAMPLE
   Copy-ToMyVideos -FilePath C:\local\videos\Test.mp4
   
   Copies C:\local\videos\Test.mp4 to C:\Users\runasname\Videos\Test.mp4, overwriting if necessary.
   Here, runasname is the user name running this script.
   
   .EXAMPLE
    Copy-ToMyVideo media\Test.mp4

    Copies C:\scripts\MediaAssessment\media\Test.mp4 to C:\Users\runasname\Videos\Test.mp4, overwriting if necessary.
    Here, MediaAssessment is the root folder from which this script is run,
    and runasname is the user name by which this script is run.
#>
    [CmdletBinding()]
    Param(
        [ValidateNotNullOrEmpty()]
        [ValidateScript({Test-Path -Path $_ -PathType Leaf})]
        [Parameter(ValueFromPipeline=$True
                  ,Mandatory=$True
                  ,Position=0)]
        [string[]]$FilePath
    )

    Begin {
        # Validate we can locate the environment special folder, MyVideos.
    
        if (-not(Test-Path -Path ($MyVideos = [Environment]::GetFolderPath([System.Environment+SpecialFolder]::MyVideos))  -PathType Container)) {
            throw [System.IO.FileNotFoundException] "$MyVideos MyVideos not found."
        }
    }
    Process {
        foreach ($Path in $FilePath) {
            # Copy the file(s)
            try {
                Copy-Item -Path $Path -Destination $MyVideos -Force -ErrorAction Stop
            }
            catch {
                throw [System.IO.IOException] "$Path copy to MyVideos failed."
            }
        }
    }
}

function Copy-ToMyMusic {
<#
  .SYNOPSIS
   Copies one or more audio files into the user's Music folder, which is where the Groove app expects to find it.
   
  .DESCRIPTION
   Copies the given file to the My Music folder of the user running the script.
  
  .PARAMETER FilePath
   Absolute or relative path to the file that is to be copied into the Music folder of the user running the script.
   
  .EXAMPLE
   Copy-ToMyMusic C:\local\audio\Test.mp3
   
   Copies C:\local\audio\Test.mp3 to C:\Users\runasname\Music\Test.mp3, overwriting if necessary.
   Here, runasname is the user name running this script.
   
   .EXAMPLE
    Copy-ToMyMusic media\Test.mp3

    Copies C:\scripts\MediaAssessment\media\Test.mp3 to C:\Users\runasname\Music\Test.mp3, overwriting if necessary.
    Here, MediaAssessment is the root folder from which this script is run,
    and runasname is the user name running this script.
#>
    [CmdletBinding()]
    Param(
        [ValidateNotNullOrEmpty()]
        [ValidateScript({Test-Path -Path $_ -PathType Leaf})]
        [Parameter(ValueFromPipeline=$True
                  ,Mandatory=$True
                  ,Position=0)]
        [string[]]$FilePath
    )

    # Validate we can locate the environment special folder, MyMusic.
    Begin {
        if (-not(Test-Path -Path ($MyMusic = [Environment]::GetFolderPath([System.Environment+SpecialFolder]::MyMusic))  -PathType Container)) {
            throw [System.IO.FileNotFoundException] "$MyMusic MyMusic not found."
        }
    }
    
    Process {
        foreach ($Path in $FilePath) {
            # Copy the file
            try {
                Copy-Item -Path $Path  -Destination $MyMusic -Force -ErrorAction Stop
            }
            catch {
                throw [System.IO.IOException] "$Path copy to MyMusic failed."
            }
        }
    }
}

function Test-MediaFilePath {
<#
  .SYNOPSIS
   Validate the existence of a video or music file at the path specified.
   
  .DESCRIPTION
   The IsMusic switch is used to fill an array of valid file extensions (e.g. '.mp4' for video and '.mp3' for music).
   Before the test, a beginning message is sent to the Log.
   If the test succeeds, a success message is sent to the Log.
   If the test fails, an error is thrown.
  
  .PARAMETER FilePath
   The relative or absolute path to the media file.

  .PARAMETER IsMusic
   True if the media file path should resolve to file with an extension consistent with music. False if it should resolve to video extension.
   
  .EXAMPLE
   Test-MediaFilePath -FilePath '1080p5min.mp4'
   
   Logs "Test media file path..." before starting.
   Logs "Media file with valid extension found..." if valid.
   Throws "<FILEPATH> not found or not a valid <music|video> file." upon failure to find a file with a valid video or music extension, as appropriate.
#>
    [CmdletBinding()]
    Param(    [ValidateNotNullOrEmpty()]
              [Parameter(Mandatory=$True
                        ,Position=0)]
              [string]
          $FilePath,
         
              [Parameter()]
              [switch]
          $IsMusic
    )

        $validExtenstions = @()
        if ($IsMusic) {
            $validExtenstions += '.mp3'
        }
        else {
            $validExtenstions += '.mp4'
        }
        
        "Test media file path..."| Log-Info

        # Make sure a file with a valid media extention exists.
        if (-not ( (Test-Path -Path $FilePath -PathType Leaf) -and ($validExtenstions -contains ((Get-Item $FilePath).Extension)) )) {
            throw [System.IO.FileNotFoundException] ("'{0}' not found or not a valid {1} file." -f $FilePath, $(if($IsMusic) {'music'} else {'video'}) )
        }
        else {
            "Media file with valid extension found..." | Log-Info
        }
}

function Resolve-Credential {
<#
  .SYNOPSIS
   Resolves an encrypted login or login password via a cretificate.

  .DESCRIPTION
   
  .PARAMETER Cipher
   
   
  .EXAMPLE
   $loginPwd = Resolve-Credential $INI['STREAMING']['VIDEO_PASSWORD']

   Returns a plain text login or login password from a configuration file encryped entry.   

#>
    [CmdletBinding()]
    Param(    [ValidateNotNullOrEmpty()]
              [Parameter(Mandatory=$True
                        ,Position=0
                        ,HelpMessage='Encrypted credential that is to be decrypted.')]
              [string]
          $Cipher,
          
              [Parameter()]
              [switch]
          $NoDecrypt 
        
    )
    #TBD for now just returns the value passed.
    
    "Resolving credentials..." | Log-Info
    Write-Debug ("Resolving credentials '{0}' with NoDecrypt={1}." -f $Cipher, $NoDecrypt)
    
    if ($NoDecrypt) {
        $Text = $Cipher
    }
    else {
        $Text = $Cipher 
    }
    "Credentials resolved..." | Log-Info
return $Text
}

function Get-RootName {
<#
  .SYNOPSIS
   Returns the absolute path given a file name: the full path relative to the script root if the file name is relative, the given file name otherwise.
   
  .DESCRIPTION
   
  .PARAMETER FileName
   The relative or absolute path of a file.
   
  .EXAMPLE Get-RootName -FileName Test.mp4
   
   Returns, for example, C:\MediaQuality\1Test.mp4, where C:\MediaQuality is the location of the calling script.
   
  .EXAMPLE Get-RootName -FileName C:\local\videos\Test.mp4

   Returns C:\local\videos\Test.mp4, exactly the path given as input.
#>
    Param(    [ValidateNotNullOrEmpty()]
              [Parameter(Mandatory=$True
                        ,Position=0)]
              [string]
          $FileName
         )
    
    if ( (([System.IO.FileInfo]$FileName).FullName) -eq $FileName) {
        return $FileName
    }
    else {
        return (join-path -Path $PSScriptRoot -ChildPath $FileName)
    }
}

function Add-StreamingContent {
<#
  .SYNOPSIS
   Adds the default streaming content to user's library through Windows Store app
   
  .DESCRIPTION
   
  .PARAMETER VideoSearchString
   Name of the video to be searched in Windows Store app
   
  .PARAMETER Playback
   The Playback custom PS object that contains the absolute path of scenario automation tool executable
   
  .EXAMPLE Add-StreamingContent -VideoSearchString "Remaking the Legend"
#>
    [CmdletBinding()]
    Param(    [ValidateNotNullOrEmpty()]
              [Parameter(Mandatory=$True
                        ,Position=0)]
              [string]
          $VideoSearchString,
          [ValidateNotNullOrEmpty()]
          [Parameter()]
          $Playback
         )
    
    $StoreUid = Get-ConfigValue -Section 'SCENARIO_AUTOMATION' -Key 'STORE_UID' -Default 'Microsoft.WindowsStore_8wekyb3d8bbwe!App'
    $StoreProcessName = Get-ConfigValue -Section 'SCENARIO_AUTOMATION' -Key 'STORE_PROC' -Default 'WinStore.App'

    $ArgList = @(" -installonly ""{0}"" -storeuid ""{1}""" -f $VideoSearchString, $StoreUid)
    $Out = Start-Executable $Playback.ExePath $ArgList
    
    $Out | Log-Info
    
    ## Wait for few seconds before we close the app.
    $milliseconds = [int](Get-ConfigValue -Section 'SCENARIO_AUTOMATION' -Key 'STOP_PROC_WAITFOR_MILLISECOND' -Default '1500')  
    Start-Sleep -Milliseconds ($milliseconds * 2)
    $PackageName = Get-AppxPackage| Where {$_.Name -match "Microsoft.WindowsStore" } | Select PackageFamilyName
    Stop-WindowsStoreApp -PackageFamilyName $PackageName.PackageFamilyName -Name $StoreProcessName
}

function Configure-GrooveRepeat {
<#
  .SYNOPSIS
   Sets the repeat option in Groove Music app to 'Repeat all'
   
  .DESCRIPTION
   Before running the workload, the Groove Music app repeat option needs to be set to 'Repeat All'. We load the settings.dat file of the Groove Music app where
   all presets are defined. We load the file into Windows registry, set the 'RepeatMode' to 'Repeat all' and then save it. These changes will reflect in the
   settings.dat file. When the Groove Music app is launched next time, the app will read the updated presets from the settings.dat file.
   
  .EXAMPLE Configure-GrooveRepeat
#>
    try {
        $seconds = [int](Get-ConfigValue -Section 'SCENARIO_AUTOMATION' -Key 'WAIT_FOR_REGISTRY_LOAD_SECONDS' -Default '2')
        "Configuring Groove Music app for repeat all option" | Log-Info
	    ## We need to stop the Groove Music process if it's already running. Otherwise the settings.dat file will not be accessible.
        $PackageName = Get-AppxPackage| Where {$_.Name -match "Microsoft.ZuneMusic" } | Select PackageFamilyName
        Stop-WindowsStoreApp -PackageFamilyName $PackageName.PackageFamilyName -Name "Music.UI"
            
        REG LOAD "HKLM\GrooveReg" "$($env:LOCALAPPDATA)\Packages\Microsoft.ZuneMusic_8wekyb3d8bbwe\Settings\settings.dat"
        Start-Sleep -Seconds $seconds   ## It takes time to load registry
        $GrooveRegPath = Join-Path -Path "$PSScriptRoot\resources" -ChildPath GrooveRepeat.reg
        REGEDIT /s $GrooveRegPath
        Start-Sleep -Seconds $seconds  ## Wait for changes done above to reflect in registry
        REG UNLOAD HKLM\GrooveReg
		"Succesfully set the repeat option for Groove Music app" | Log-Info
	} catch {
        $AXEResultsPath = (Get-Item env:\AssessmentResultsPath).value
		$xmlErrorFile = "$AXEResultsPath\ErrorsAndWarnings.xml"
        Log-ToXmlErrorFile -XmlErrorFile $xmlErrorFile -ErrorCode $_.Exception.Hresult -ErrorMessage "'Repeat all' option for Groove Music app could not be set"
        $message = $_.Exception.Message + [Environment]::NewLine + $_.InvocationInfo.PositionMessage
        $message | Log-Info
	}
}
#endregion


#region Export
Export-ModuleMember -Function DisableWow64FsRedirection
Export-ModuleMember -Function RevertWow64FsRedirection
Export-ModuleMember -Function Set-UwpVideoPlayerPolicy
Export-ModuleMember Write-AuxQualityLogs
Export-ModuleMember Write-AuxWorkloadLogs
Export-ModuleMember Start-Executable
Export-ModuleMember Get-ConfigValue
Export-ModuleMember Get-ProcessorArchitecture
Export-ModuleMember Get-INI
Export-ModuleMember Get-RootName
Export-ModuleMember Initialize-AxeLogger
Export-ModuleMember Install-PlaybackPackage
Export-ModuleMember Log-Error
Export-ModuleMember Log-Info
Export-ModuleMember Log-ToXmlErrorFile
Export-ModuleMember Move-E3logs
Export-ModuleMember Get-ScreenResolution
Export-ModuleMember Select-PlaybackSource
Export-ModuleMember Start-StreamingWorkload
Export-ModuleMember Stop-Workload
Export-ModuleMember Test-AppxPackageInstalled
Export-ModuleMember Test-IsOnBattery
Export-ModuleMember Test-ValidFileName
Export-ModuleMember Test-FileExists
Export-ModuleMember Test-IsMetric
Export-ModuleMember Add-StreamingContent
Export-ModuleMember Configure-GrooveRepeat
#endregion
