macOS: Restore window state and spaces.

Use Cocoa UI Preservation to encode and restore window state, like its
space and fullscreen state. Like other Mac apps, windows will be
restored with full fidelity (i.e. space and all) after a system restart
or Chrome relaunch (via chrome://restart, an update, etc.), or will be
created on the active space if Chrome is quit and relaunched. AppKit
drives this behavior.

This change calls encodeRestorableStateWithCoder: and
restoreStateWithCoder:, as advised by Apple in
https://crbug.com/74812#c56, even though those methods are minimally
documented and have warnings in the headers that they're okay to
override but you should never call them yourself.

Issues I ran into in using them:

1. Encoding can fail and throw an exception if done at the wrong time.
   Only encoding state on the main thread, in the default run loop mode,
   and making sure the window is ready by calling (undocumented)
   -[NSWindow _isConsideredOpenForPersistentState], seem to cover those
   cases.
2. The window tries to encode some objects that can't be encoded, like
   itself when it's the first responder, or NSViews. AppKit appears to
   solve this with a custom encoder that knows what to skip, but being
   an NSKeyedArchiverDelegate does the trick, too.

Things to keep an eye on:

1. I made a small change to window activation during session restore.
   Without it, session restore tries to activate windows on other
   spaces, which can cause switches as different windows appear. If this
   impacts other platforms, it can be restricted to macOS with a
   #define. It should be fine because the last-used browser still gets
   activated at the end of the process.
2. Window state gets encoded and stored in NativeWidgetMacNSWindowHost
   every time it changes. AppKit is much more gentle and only saves
   state after specific events/times. This might be worth looking into.
3. Windows which get restored miniaturized are blank until the first
   time they're un-miniaturized.

Bug: 74812
Change-Id: Id6fc50151f2762f5428f9e40877b089f7b475078
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1808708
Commit-Queue: Sidney San Martín <sdy@chromium.org>
Reviewed-by: Robert Sesek <rsesek@chromium.org>
Reviewed-by: Avi Drissman <avi@chromium.org>
Cr-Commit-Position: refs/heads/master@{#701411}
diff --git a/chrome/browser/chrome_browser_main_mac.mm b/chrome/browser/chrome_browser_main_mac.mm
index 4dd76ff..beb291f 100644
--- a/chrome/browser/chrome_browser_main_mac.mm
+++ b/chrome/browser/chrome_browser_main_mac.mm
@@ -37,6 +37,7 @@
 #include "chrome/common/chrome_paths.h"
 #include "chrome/common/chrome_switches.h"
 #include "chrome/common/mac/staging_watcher.h"
+#include "chrome/common/pref_names.h"
 #include "chrome/grit/chromium_strings.h"
 #include "components/crash/content/app/crashpad.h"
 #include "components/metrics/metrics_service.h"
@@ -454,10 +455,21 @@
                         l10n_util::GetStringUTF16(IDS_PRODUCT_NAME), false);
   [app_controller mainMenuCreated];
 
-  // Initialize the OSCrypt.
   PrefService* local_state = g_browser_process->local_state();
   DCHECK(local_state);
+
+  // Initialize the OSCrypt.
   OSCrypt::Init(local_state);
+
+  // AppKit only restores windows to their original spaces when relaunching
+  // apps after a restart, and puts them all on the current space when an app
+  // is manually quit and relaunched. If Chrome restarted itself, ask AppKit to
+  // treat this launch like a system restart and restore everything.
+  if (local_state->GetBoolean(prefs::kWasRestarted)) {
+    [NSUserDefaults.standardUserDefaults registerDefaults:@{
+      @"NSWindowRestoresWorkspaceAtLaunch" : @YES
+    }];
+  }
 }
 
 void ChromeBrowserMainPartsMac::PostMainMessageLoopStart() {
diff --git a/chrome/browser/sessions/session_restore.cc b/chrome/browser/sessions/session_restore.cc
index 9a54253..fd9e8d9b 100644
--- a/chrome/browser/sessions/session_restore.cc
+++ b/chrome/browser/sessions/session_restore.cc
@@ -649,7 +649,8 @@
     if (browser_ == browser)
       return;
 
-    browser->window()->Show();
+    if (!browser->window()->IsVisible() && !browser->window()->IsMinimized())
+      browser->window()->Show();
     browser->set_is_session_restore(false);
   }
 
diff --git a/chrome/browser/ui/browser_tabrestore.cc b/chrome/browser/ui/browser_tabrestore.cc
index 2179d42a..4502f27 100644
--- a/chrome/browser/ui/browser_tabrestore.cc
+++ b/chrome/browser/ui/browser_tabrestore.cc
@@ -127,7 +127,7 @@
 
   if (select) {
     if (!browser->window()->IsMinimized())
-      browser->window()->Activate();
+      browser->window()->Show();
   } else {
     // We set the size of the view here, before Blink does its initial layout.
     // If we don't, the initial layout of background tabs will be performed
diff --git a/components/remote_cocoa/app_shim/native_widget_mac_nswindow.mm b/components/remote_cocoa/app_shim/native_widget_mac_nswindow.mm
index bf4cd6d..8f2f73d2 100644
--- a/components/remote_cocoa/app_shim/native_widget_mac_nswindow.mm
+++ b/components/remote_cocoa/app_shim/native_widget_mac_nswindow.mm
@@ -18,13 +18,14 @@
 + (Class)frameViewClassForStyleMask:(NSWindowStyleMask)windowStyle;
 - (BOOL)hasKeyAppearance;
 - (long long)_resizeDirectionForMouseLocation:(CGPoint)location;
+- (BOOL)_isConsideredOpenForPersistentState;
 
 // Available in later point releases of 10.10. On 10.11+, use the public
 // -performWindowDragWithEvent: instead.
 - (void)beginWindowDragWithEvent:(NSEvent*)event;
 @end
 
-@interface NativeWidgetMacNSWindow ()
+@interface NativeWidgetMacNSWindow () <NSKeyedArchiverDelegate>
 - (ViewsNSWindowDelegate*)viewsNSWindowDelegate;
 - (BOOL)hasViewsMenuActive;
 - (id<NSAccessibility>)rootAccessibilityObject;
@@ -84,6 +85,7 @@
   id<WindowTouchBarDelegate> touchBarDelegate_;  // Weak.
   uint64_t bridgedNativeWidgetId_;
   remote_cocoa::NativeWidgetNSWindowBridge* bridge_;
+  BOOL willUpdateRestorableState_;
 }
 @synthesize bridgedNativeWidgetId = bridgedNativeWidgetId_;
 @synthesize bridge = bridge_;
@@ -102,9 +104,12 @@
   return self;
 }
 
-// This override doesn't do anything, but keeping it helps diagnose lifetime
-// issues in crash stacktraces by inserting a symbol on NativeWidgetMacNSWindow.
+// This override helps diagnose lifetime issues in crash stacktraces by
+// inserting a symbol on NativeWidgetMacNSWindow and should be kept even if it
+// does nothing.
 - (void)dealloc {
+  willUpdateRestorableState_ = YES;
+  [NSObject cancelPreviousPerformRequestsWithTarget:self];
   [super dealloc];
 }
 
@@ -300,6 +305,54 @@
   return touchBarDelegate_ ? [touchBarDelegate_ makeTouchBar] : nil;
 }
 
+// Called when the window is the delegate of the archiver passed to
+// |-encodeRestorableStateWithCoder:|, below. It prevents the archiver from
+// trying to encode the window or an NSView, say, to represent the first
+// responder. When AppKit calls |-encodeRestorableStateWithCoder:|, it
+// accomplishes the same thing by passing a custom coder.
+- (id)archiver:(NSKeyedArchiver*)archiver willEncodeObject:(id)object {
+  if (object == self)
+    return nil;
+  if ([object isKindOfClass:[NSView class]])
+    return nil;
+  return object;
+}
+
+- (void)saveRestorableState {
+  if (![self _isConsideredOpenForPersistentState])
+    return;
+  base::scoped_nsobject<NSMutableData> restorableStateData(
+      [[NSMutableData alloc] init]);
+  base::scoped_nsobject<NSKeyedArchiver> encoder([[NSKeyedArchiver alloc]
+      initForWritingWithMutableData:restorableStateData]);
+  encoder.get().delegate = self;
+  [self encodeRestorableStateWithCoder:encoder];
+  [encoder finishEncoding];
+
+  auto* bytes = static_cast<uint8_t const*>(restorableStateData.get().bytes);
+  bridge_->host()->OnWindowStateRestorationDataChanged(
+      std::vector<uint8_t>(bytes, bytes + restorableStateData.get().length));
+  willUpdateRestorableState_ = NO;
+}
+
+// AppKit calls -invalidateRestorableState when a property of the window which
+// affects its restorable state changes.
+- (void)invalidateRestorableState {
+  [super invalidateRestorableState];
+  if ([self _isConsideredOpenForPersistentState]) {
+    if (willUpdateRestorableState_)
+      return;
+    willUpdateRestorableState_ = YES;
+    [self performSelectorOnMainThread:@selector(saveRestorableState)
+                           withObject:nil
+                        waitUntilDone:NO
+                                modes:@[ NSDefaultRunLoopMode ]];
+  } else if (willUpdateRestorableState_) {
+    willUpdateRestorableState_ = NO;
+    [NSObject cancelPreviousPerformRequestsWithTarget:self];
+  }
+}
+
 // On newer SDKs, _canMiniaturize respects NSMiniaturizableWindowMask in the
 // window's styleMask. Views assumes that Widgets can always be minimized,
 // regardless of their window style, so override that behavior here.
diff --git a/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h b/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h
index 60294cd..f57e033 100644
--- a/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h
+++ b/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h
@@ -366,6 +366,10 @@
   // shadow needs to be invalidated when a frame is received for the new shape.
   bool invalidate_shadow_on_frame_swap_ = false;
 
+  // A blob representing the window's saved state, which is applied and cleared
+  // the first time it's shown.
+  std::vector<uint8_t> pending_restoration_data_;
+
   mojo::AssociatedBinding<remote_cocoa::mojom::NativeWidgetNSWindow>
       bridge_mojo_binding_;
   DISALLOW_COPY_AND_ASSIGN(NativeWidgetNSWindowBridge);
diff --git a/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm b/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm
index 25a6f864..edd3830 100644
--- a/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm
+++ b/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm
@@ -422,6 +422,7 @@
   is_translucent_window_ = params->is_translucent;
   widget_is_top_level_ = params->widget_is_top_level;
   position_window_in_screen_coords_ = params->position_window_in_screen_coords;
+  pending_restoration_data_ = params->state_restoration_data;
 
   // Register for application hide notifications so that visibility can be
   // properly tracked. This is not done in the delegate so that the lifetime is
@@ -689,6 +690,22 @@
       return;
   }
 
+  if (!pending_restoration_data_.empty()) {
+    NSData* restore_ns_data =
+        [NSData dataWithBytes:pending_restoration_data_.data()
+                       length:pending_restoration_data_.size()];
+    base::scoped_nsobject<NSKeyedUnarchiver> decoder(
+        [[NSKeyedUnarchiver alloc] initForReadingWithData:restore_ns_data]);
+    [window_ restoreStateWithCoder:decoder];
+    pending_restoration_data_.clear();
+
+    // When first showing a window with restoration data, don't activate it.
+    // This avoids switching spaces or un-miniaturizing it right away.
+    // Additional activations act normally.
+    if (new_state == WindowVisibilityState::kShowAndActivateWindow)
+      new_state = WindowVisibilityState::kShowInactive;
+  }
+
   if (IsWindowModalSheet()) {
     ShowAsModalSheet();
     return;
@@ -700,8 +717,11 @@
   if (new_state == WindowVisibilityState::kShowAndActivateWindow) {
     [window_ makeKeyAndOrderFront:nil];
     [NSApp activateIgnoringOtherApps:YES];
-  } else if (!parent_) {
-    [window_ orderFront:nil];
+  } else if (!parent_ && ![window_ isMiniaturized]) {
+    // When showing a window without activation, avoid making it the front
+    // window (with e.g. orderFront:), which can cause a space switch.
+    [window_ orderWindow:NSWindowBelow
+              relativeTo:NSApp.mainWindow.windowNumber];
   }
 
   // For non-sheet modal types, use the constrained window animations to make
diff --git a/components/remote_cocoa/common/native_widget_ns_window.mojom b/components/remote_cocoa/common/native_widget_ns_window.mojom
index 694ea6a..830bee0 100644
--- a/components/remote_cocoa/common/native_widget_ns_window.mojom
+++ b/components/remote_cocoa/common/native_widget_ns_window.mojom
@@ -77,6 +77,10 @@
   // NSWindowCollectionBehaviorParticipatesInCycle (this is not the
   // default for NSWindows with NSBorderlessWindowMask).
   bool force_into_collection_cycle;
+  // An opaque blob of AppKit data which includes, among other things, a
+  // window's workspace and fullscreen state, and can be retrieved from or
+  // applied to a window.
+  array<uint8> state_restoration_data;
 };
 
 // The interface through which a NativeWidgetMac may interact with an NSWindow
diff --git a/components/remote_cocoa/common/native_widget_ns_window_host.mojom b/components/remote_cocoa/common/native_widget_ns_window_host.mojom
index a374235..a525aedb 100644
--- a/components/remote_cocoa/common/native_widget_ns_window_host.mojom
+++ b/components/remote_cocoa/common/native_widget_ns_window_host.mojom
@@ -151,6 +151,10 @@
                            bool is_content_first_responder,
                            bool full_keyboard_access_enabled);
 
+  // Called when the blob of data that represents the NSWindow's restorable
+  // state has changed.
+  OnWindowStateRestorationDataChanged(array<uint8> data);
+
   // Accept or cancel the current dialog window (depending on the value of
   // |button|), if a current dialog exists.
   DoDialogButtonAction(ui.mojom.DialogButton button);
diff --git a/ui/views/cocoa/native_widget_mac_ns_window_host.h b/ui/views/cocoa/native_widget_mac_ns_window_host.h
index 528ec88..fe91d6d 100644
--- a/ui/views/cocoa/native_widget_mac_ns_window_host.h
+++ b/ui/views/cocoa/native_widget_mac_ns_window_host.h
@@ -197,6 +197,11 @@
   // fullscreen or transitioning between fullscreen states.
   gfx::Rect GetRestoredBounds() const;
 
+  // An opaque blob of AppKit data which includes, among other things, a
+  // window's workspace and fullscreen state, and can be retrieved from or
+  // applied to a window.
+  const std::vector<uint8_t>& GetWindowStateRestorationData() const;
+
   // Set |parent_| and update the old and new parents' |children_|. It is valid
   // to set |new_parent| to nullptr. Propagate this to the BridgedNativeWidget.
   void SetParent(NativeWidgetMacNSWindowHost* new_parent);
@@ -284,6 +289,8 @@
   void OnWindowKeyStatusChanged(bool is_key,
                                 bool is_content_first_responder,
                                 bool full_keyboard_access_enabled) override;
+  void OnWindowStateRestorationDataChanged(
+      const std::vector<uint8_t>& data) override;
   void DoDialogButtonAction(ui::DialogButton button) override;
   bool GetDialogButtonInfo(ui::DialogButton type,
                            bool* button_exists,
@@ -455,6 +462,7 @@
   // The geometry of the window and its contents view, in screen coordinates.
   gfx::Rect window_bounds_in_screen_;
   gfx::Rect content_bounds_in_screen_;
+  std::vector<uint8_t> state_restoration_data_;
   bool is_visible_ = false;
   bool target_fullscreen_state_ = false;
   bool in_fullscreen_transition_ = false;
diff --git a/ui/views/cocoa/native_widget_mac_ns_window_host.mm b/ui/views/cocoa/native_widget_mac_ns_window_host.mm
index 6aa9e71..93f295f 100644
--- a/ui/views/cocoa/native_widget_mac_ns_window_host.mm
+++ b/ui/views/cocoa/native_widget_mac_ns_window_host.mm
@@ -6,6 +6,7 @@
 
 #include <utility>
 
+#include "base/base64.h"
 #include "base/mac/foundation_util.h"
 #include "base/strings/sys_string_conversions.h"
 #include "components/remote_cocoa/app_shim/mouse_capture.h"
@@ -82,6 +83,8 @@
   void OnWindowKeyStatusChanged(bool is_key,
                                 bool is_content_first_responder,
                                 bool full_keyboard_access_enabled) override {}
+  void OnWindowStateRestorationDataChanged(
+      const std::vector<uint8_t>& data) override {}
   void DoDialogButtonAction(ui::DialogButton button) override {}
   void OnFocusWindowToolbar() override {}
   void SetRemoteAccessibilityTokens(
@@ -428,6 +431,16 @@
   widget_type_ = params.type;
   tooltip_manager_ = std::make_unique<TooltipManagerMac>(GetNSWindowMojo());
 
+  if (params.workspace.length()) {
+    std::string restoration_data;
+    if (base::Base64Decode(params.workspace, &restoration_data)) {
+      state_restoration_data_ = std::vector<uint8_t>(restoration_data.begin(),
+                                                     restoration_data.end());
+    } else {
+      DLOG(ERROR) << "Failed to decode a window's state restoration data.";
+    }
+  }
+
   // Initialize the window.
   {
     auto window_params = NativeWidgetNSWindowInitParams::New();
@@ -461,6 +474,7 @@
     window_params->force_into_collection_cycle =
         widget_type_ == Widget::InitParams::TYPE_WINDOW &&
         params.remove_standard_frame;
+    window_params->state_restoration_data = state_restoration_data_;
 
     GetNSWindowMojo()->InitWindow(std::move(window_params));
   }
@@ -667,6 +681,11 @@
   return window_bounds_in_screen_;
 }
 
+const std::vector<uint8_t>&
+NativeWidgetMacNSWindowHost::GetWindowStateRestorationData() const {
+  return state_restoration_data_;
+}
+
 void NativeWidgetMacNSWindowHost::SetNativeWindowProperty(const char* name,
                                                           void* value) {
   if (value)
@@ -1105,6 +1124,12 @@
   }
 }
 
+void NativeWidgetMacNSWindowHost::OnWindowStateRestorationDataChanged(
+    const std::vector<uint8_t>& data) {
+  state_restoration_data_ = data;
+  native_widget_mac_->GetWidget()->OnNativeWidgetWorkspaceChanged();
+}
+
 void NativeWidgetMacNSWindowHost::DoDialogButtonAction(
     ui::DialogButton button) {
   views::DialogDelegate* dialog =
diff --git a/ui/views/widget/native_widget_mac.mm b/ui/views/widget/native_widget_mac.mm
index c440731..83a2512 100644
--- a/ui/views/widget/native_widget_mac.mm
+++ b/ui/views/widget/native_widget_mac.mm
@@ -10,6 +10,7 @@
 
 #include <utility>
 
+#include "base/base64.h"
 #include "base/bind.h"
 #include "base/lazy_instance.h"
 #include "base/mac/scoped_nsobject.h"
@@ -393,7 +394,9 @@
 }
 
 std::string NativeWidgetMac::GetWorkspace() const {
-  return std::string();
+  return ns_window_host_ ? base::Base64Encode(
+                               ns_window_host_->GetWindowStateRestorationData())
+                         : std::string();
 }
 
 void NativeWidgetMac::SetBounds(const gfx::Rect& bounds) {
diff --git a/ui/views/widget/widget.h b/ui/views/widget/widget.h
index 4d622c3..30232d12 100644
--- a/ui/views/widget/widget.h
+++ b/ui/views/widget/widget.h
@@ -490,7 +490,8 @@
   // Retrieves the restored bounds for the window.
   gfx::Rect GetRestoredBounds() const;
 
-  // Retrieves the current workspace for the window.
+  // Retrieves the current workspace for the window. (On macOS: an opaque
+  // binary blob that encodes the workspace and other window state.)
   std::string GetWorkspace() const;
 
   // Sizes and/or places the widget to the specified bounds, size or position.