Search code examples
c#winapiknown-folders

How do I detect changes to Known Folder Redirection


When OneDrive is installed with Known Folder Move enabled, the first logon of a user starts with no redirection of known folders then midway through the session, they are redirected to the OneDrive folders.

How can I detect that?

I've already tried WM_SETTINGCHANGE and SHChangeNotifyRegister on CSIDL_Desktop to no avail.


Solution

  • It is a dirty undocumented hack, but if you monitor HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders for changes, it will trigger on redirection.

    A C# example of how to do so is:

    public sealed class KnownFolderWatcher : IDisposable
    {
        private readonly SynchronizationContext SyncCtx;
        private readonly RegistryKey Key;
        private readonly Thread thRead;
        private readonly AutoResetEvent mreTriggered;
    
        public event EventHandler KeyChanged;
    
        private Exception Exception;
        private bool isDisposed;
    
    
        public KnownFolderWatcher(SynchronizationContext syncCtx)
        {
            this.SyncCtx = syncCtx ?? throw new ArgumentNullException(nameof(syncCtx));
    
            this.Key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders", false)
                ?? throw new InvalidOperationException("Could not open User Shell Folders key");
    
            this.mreTriggered = new AutoResetEvent(false);
    
            this.thRead = new Thread(thRead_Main);
            this.thRead.Name = "Registry Change Reader";
            this.thRead.IsBackground = true;
            this.thRead.Start();
        }
    
        private void thRead_Main()
        {
            try
            {
                while (true)
                {
                    NativeMethods.RegNotifyChangeKeyValue(Key.Handle, false, 4 /* REG_NOTIFY_CHANGE_LAST_SET */, mreTriggered.SafeWaitHandle, true);
                    mreTriggered.WaitOne();
                    if (isDisposed)
                    {
                        break;
                    }
    
                    SyncCtx.Post(_1 =>
                    {
                        KeyChanged?.Invoke(this, EventArgs.Empty);
                    }, null);
                }
            }
            catch (Exception ex)
            {
                this.Exception = ex;
            }
        }
    
        public void Dispose()
        {
            if (isDisposed)
            {
                throw new ObjectDisposedException(nameof(KnownFolderWatcher));
            }
            isDisposed = true;
    
            mreTriggered.Set();
            thRead.Join();
    
            if (this.Exception != null)
            {
                throw new InvalidOperationException("Exception from read thread", Exception);
            }
        }
    }
    
    internal static class NativeMethods
    {
        [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)]
        internal static extern uint RegNotifyChangeKeyValue(SafeRegistryHandle key, bool watchSubTree, uint notifyFilter, SafeWaitHandle regEvent, bool async);
    }