1//*********************************************************
2//
3// Copyright (c) Microsoft. All rights reserved.
4// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
5// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
6// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
7// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
8//
9//*********************************************************
10
11//
12// SuspensionManager.cpp
13// Implementation of the SuspensionManager class
14//
15
16#include "pch.h"
17#include "SuspensionManager.h"
18
19#include <collection.h>
20#include <algorithm>
21
22using namespace SDKSample::Common;
23
24using namespace Concurrency;
25using namespace Platform;
26using namespace Platform::Collections;
27using namespace Windows::Foundation;
28using namespace Windows::Foundation::Collections;
29using namespace Windows::Storage;
30using namespace Windows::Storage::FileProperties;
31using namespace Windows::Storage::Streams;
32using namespace Windows::UI::Xaml;
33using namespace Windows::UI::Xaml::Controls;
34using namespace Windows::UI::Xaml::Interop;
35
36namespace
37{
38    Map<String^, Object^>^ _sessionState = ref new Map<String^, Object^>();
39    String^ sessionStateFilename = "_sessionState.dat";
40
41    // Forward declarations for object object read / write support
42    void WriteObject(Windows::Storage::Streams::DataWriter^ writer, Platform::Object^ object);
43    Platform::Object^ ReadObject(Windows::Storage::Streams::DataReader^ reader);
44}
45
46/// <summary>
47/// Provides access to global session state for the current session.  This state is serialized by
48/// <see cref="SaveAsync"/> and restored by <see cref="RestoreAsync"/> which require values to be
49/// one of the following: boxed values including integers, floating-point singles and doubles,
50/// wide characters, boolean, Strings and Guids, or Map<String^, Object^> where map values are
51/// subject to the same constraints.  Session state should be as compact as possible.
52/// </summary>
53IMap<String^, Object^>^ SuspensionManager::SessionState::get(void)
54{
55    return _sessionState;
56}
57
58/// <summary>
59/// Wrap a WeakReference as a reference object for use in a collection.
60/// </summary>
61private ref class WeakFrame sealed
62{
63private:
64    WeakReference _frameReference;
65
66internal:
67    WeakFrame(Frame^ frame) { _frameReference = frame; }
68    property Frame^ ResolvedFrame
69    {
70        Frame^ get(void) { return _frameReference.Resolve<Frame>(); }
71    };
72};
73
74namespace
75{
76    std::vector<WeakFrame^> _registeredFrames;
77    DependencyProperty^ FrameSessionStateKeyProperty =
78        DependencyProperty::RegisterAttached("_FrameSessionStateKeyProperty",
79        TypeName(String::typeid), TypeName(SuspensionManager::typeid), nullptr);
80    DependencyProperty^ FrameSessionStateProperty =
81        DependencyProperty::RegisterAttached("_FrameSessionStateProperty",
82        TypeName(IMap<String^, Object^>::typeid), TypeName(SuspensionManager::typeid), nullptr);
83}
84
85/// <summary>
86/// Registers a <see cref="Frame"/> instance to allow its navigation history to be saved to
87/// and restored from <see cref="SessionState"/>.  Frames should be registered once
88/// immediately after creation if they will participate in session state management.  Upon
89/// registration if state has already been restored for the specified key
90/// the navigation history will immediately be restored.  Subsequent invocations of
91/// <see cref="RestoreAsync(String)"/> will also restore navigation history.
92/// </summary>
93/// <param name="frame">An instance whose navigation history should be managed by
94/// <see cref="SuspensionManager"/></param>
95/// <param name="sessionStateKey">A unique key into <see cref="SessionState"/> used to
96/// store navigation-related information.</param>
97void SuspensionManager::RegisterFrame(Frame^ frame, String^ sessionStateKey)
98{
99    if (frame->GetValue(FrameSessionStateKeyProperty) != nullptr)
100    {
101        throw ref new FailureException("Frames can only be registered to one session state key");
102    }
103
104    if (frame->GetValue(FrameSessionStateProperty) != nullptr)
105    {
106        throw ref new FailureException("Frames must be either be registered before accessing frame session state, or not registered at all");
107    }
108
109    // Use a dependency property to associate the session key with a frame, and keep a list of frames whose
110    // navigation state should be managed
111    frame->SetValue(FrameSessionStateKeyProperty, sessionStateKey);
112    _registeredFrames.insert(_registeredFrames.begin(), ref new WeakFrame(frame));
113
114    // Check to see if navigation state can be restored
115    RestoreFrameNavigationState(frame);
116}
117
118/// <summary>
119/// Disassociates a <see cref="Frame"/> previously registered by <see cref="RegisterFrame"/>
120/// from <see cref="SessionState"/>.  Any navigation state previously captured will be
121/// removed.
122/// </summary>
123/// <param name="frame">An instance whose navigation history should no longer be
124/// managed.</param>
125void SuspensionManager::UnregisterFrame(Frame^ frame)
126{
127    // Remove session state and remove the frame from the list of frames whose navigation
128    // state will be saved (along with any weak references that are no longer reachable)
129    auto key = safe_cast<String^>(frame->GetValue(FrameSessionStateKeyProperty));
130    if (SessionState->HasKey(key)) SessionState->Remove(key);
131    _registeredFrames.erase(
132        std::remove_if(_registeredFrames.begin(), _registeredFrames.end(), [=](WeakFrame^& e)
133        {
134            auto testFrame = e->ResolvedFrame;
135            return testFrame == nullptr || testFrame == frame;
136        }),
137        _registeredFrames.end()
138    );
139}
140
141/// <summary>
142/// Provides storage for session state associated with the specified <see cref="Frame"/>.
143/// Frames that have been previously registered with <see cref="RegisterFrame"/> have
144/// their session state saved and restored automatically as a part of the global
145/// <see cref="SessionState"/>.  Frames that are not registered have transient state
146/// that can still be useful when restoring pages that have been discarded from the
147/// navigation cache.
148/// </summary>
149/// <remarks>Apps may choose to rely on <see cref="LayoutAwarePage"/> to manage
150/// page-specific state instead of working with frame session state directly.</remarks>
151/// <param name="frame">The instance for which session state is desired.</param>
152/// <returns>A collection of state subject to the same serialization mechanism as
153/// <see cref="SessionState"/>.</returns>
154IMap<String^, Object^>^ SuspensionManager::SessionStateForFrame(Frame^ frame)
155{
156    auto frameState = safe_cast<IMap<String^, Object^>^>(frame->GetValue(FrameSessionStateProperty));
157
158    if (frameState == nullptr)
159    {
160        auto frameSessionKey = safe_cast<String^>(frame->GetValue(FrameSessionStateKeyProperty));
161        if (frameSessionKey != nullptr)
162        {
163            // Registered frames reflect the corresponding session state
164            if (!_sessionState->HasKey(frameSessionKey))
165            {
166                _sessionState->Insert(frameSessionKey, ref new Map<String^, Object^>());
167            }
168            frameState = safe_cast<IMap<String^, Object^>^>(_sessionState->Lookup(frameSessionKey));
169        }
170        else
171        {
172            // Frames that aren't registered have transient state
173            frameState = ref new Map<String^, Object^>();
174        }
175        frame->SetValue(FrameSessionStateProperty, frameState);
176    }
177    return frameState;
178}
179
180void SuspensionManager::RestoreFrameNavigationState(Frame^ frame)
181{
182    auto frameState = SessionStateForFrame(frame);
183    if (frameState->HasKey("Navigation"))
184    {
185        frame->SetNavigationState(safe_cast<String^>(frameState->Lookup("Navigation")));
186    }
187}
188
189void SuspensionManager::SaveFrameNavigationState(Frame^ frame)
190{
191    auto frameState = SessionStateForFrame(frame);
192    frameState->Insert("Navigation", frame->GetNavigationState());
193}
194
195/// <summary>
196/// Save the current <see cref="SessionState"/>.  Any <see cref="Frame"/> instances
197/// registered with <see cref="RegisterFrame"/> will also preserve their current
198/// navigation stack, which in turn gives their active <see cref="Page"/> an opportunity
199/// to save its state.
200/// </summary>
201/// <returns>An asynchronous task that reflects when session state has been saved.</returns>
202task<void> SuspensionManager::SaveAsync(void)
203{
204    // Save the navigation state for all registered frames
205    for (auto&& weakFrame : _registeredFrames)
206    {
207        auto frame = weakFrame->ResolvedFrame;
208        if (frame != nullptr) SaveFrameNavigationState(frame);
209    }
210
211    // Serialize the session state synchronously to avoid asynchronous access to shared
212    // state
213    auto sessionData = ref new InMemoryRandomAccessStream();
214    auto sessionDataWriter = ref new DataWriter(sessionData->GetOutputStreamAt(0));
215    WriteObject(sessionDataWriter, _sessionState);
216
217    // Once session state has been captured synchronously, begin the asynchronous process
218    // of writing the result to disk
219    return task<unsigned int>(sessionDataWriter->StoreAsync()).then([=](unsigned int)
220    {
221        return sessionDataWriter->FlushAsync();
222    }).then([=](bool flushSucceeded)
223    {
224        (void)flushSucceeded; // Unused parameter
225        return ApplicationData::Current->LocalFolder->CreateFileAsync(sessionStateFilename,
226            CreationCollisionOption::ReplaceExisting);
227    }).then([=](StorageFile^ createdFile)
228    {
229        return createdFile->OpenAsync(FileAccessMode::ReadWrite);
230    }).then([=](IRandomAccessStream^ newStream)
231    {
232        return RandomAccessStream::CopyAndCloseAsync(
233            sessionData->GetInputStreamAt(0), newStream->GetOutputStreamAt(0));
234    }).then([=](UINT64 copiedBytes)
235    {
236        (void)copiedBytes; // Unused parameter
237        return;
238    });
239}
240
241/// <summary>
242/// Restores previously saved <see cref="SessionState"/>.  Any <see cref="Frame"/> instances
243/// registered with <see cref="RegisterFrame"/> will also restore their prior navigation
244/// state, which in turn gives their active <see cref="Page"/> an opportunity restore its
245/// state.
246/// </summary>
247/// <param name="version">A version identifer compared to the session state to prevent
248/// incompatible versions of session state from reaching app code.  Saved state with a
249/// different version will be ignored, resulting in an empty <see cref="SessionState"/>
250/// dictionary.</param>
251/// <returns>An asynchronous task that reflects when session state has been read.  The
252/// content of <see cref="SessionState"/> should not be relied upon until this task
253/// completes.</returns>
254task<void> SuspensionManager::RestoreAsync(void)
255{
256    _sessionState->Clear();
257
258    task<StorageFile^> getFileTask(ApplicationData::Current->LocalFolder->GetFileAsync(sessionStateFilename));
259    return getFileTask.then([=](StorageFile^ stateFile)
260    {
261        task<BasicProperties^> getBasicPropertiesTask(stateFile->GetBasicPropertiesAsync());
262        return getBasicPropertiesTask.then([=](BasicProperties^ stateFileProperties)
263        {
264            auto size = unsigned int(stateFileProperties->Size);
265            if (size != stateFileProperties->Size) throw ref new FailureException("Session state larger than 4GB");
266            task<IRandomAccessStreamWithContentType^> openReadTask(stateFile->OpenReadAsync());
267            return openReadTask.then([=](IRandomAccessStreamWithContentType^ stateFileStream)
268            {
269                auto stateReader = ref new DataReader(stateFileStream);
270                return task<unsigned int>(stateReader->LoadAsync(size)).then([=](unsigned int bytesRead)
271                {
272                    (void)bytesRead; // Unused parameter
273                    // Deserialize the Session State
274                    Object^ content = ReadObject(stateReader);
275                    _sessionState = (Map<String^, Object^>^)content;
276
277                    // Restore any registered frames to their saved state
278                    for (auto&& weakFrame : _registeredFrames)
279                    {
280                        auto frame = weakFrame->ResolvedFrame;
281                        if (frame != nullptr)
282                        {
283                            frame->ClearValue(FrameSessionStateProperty);
284                            RestoreFrameNavigationState(frame);
285                        }
286                    }
287                }, task_continuation_context::use_current());
288            });
289        });
290    });
291}
292
293#pragma region Object serialization for a known set of types
294
295namespace
296{
297    // Codes used for identifying serialized types
298    enum StreamTypes {
299        NullPtrType = 0,
300
301        // Supported IPropertyValue types
302        UInt8Type, UInt16Type, UInt32Type, UInt64Type, Int16Type, Int32Type, Int64Type,
303        SingleType, DoubleType, BooleanType, Char16Type, GuidType, StringType,
304
305        // Additional supported types
306        StringToObjectMapType,
307
308        // Marker values used to ensure stream integrity
309        MapEndMarker
310    };
311
312    void WriteString(DataWriter^ writer, String^ string)
313    {
314        writer->WriteByte(StringType);
315        writer->WriteUInt32(writer->MeasureString(string));
316        writer->WriteString(string);
317    }
318
319    void WriteProperty(DataWriter^ writer, IPropertyValue^ propertyValue)
320    {
321        switch (propertyValue->Type)
322        {
323        case PropertyType::UInt8:
324            writer->WriteByte(UInt8Type);
325            writer->WriteByte(propertyValue->GetUInt8());
326            return;
327        case PropertyType::UInt16:
328            writer->WriteByte(UInt16Type);
329            writer->WriteUInt16(propertyValue->GetUInt16());
330            return;
331        case PropertyType::UInt32:
332            writer->WriteByte(UInt32Type);
333            writer->WriteUInt32(propertyValue->GetUInt32());
334            return;
335        case PropertyType::UInt64:
336            writer->WriteByte(UInt64Type);
337            writer->WriteUInt64(propertyValue->GetUInt64());
338            return;
339        case PropertyType::Int16:
340            writer->WriteByte(Int16Type);
341            writer->WriteUInt16(propertyValue->GetInt16());
342            return;
343        case PropertyType::Int32:
344            writer->WriteByte(Int32Type);
345            writer->WriteUInt32(propertyValue->GetInt32());
346            return;
347        case PropertyType::Int64:
348            writer->WriteByte(Int64Type);
349            writer->WriteUInt64(propertyValue->GetInt64());
350            return;
351        case PropertyType::Single:
352            writer->WriteByte(SingleType);
353            writer->WriteSingle(propertyValue->GetSingle());
354            return;
355        case PropertyType::Double:
356            writer->WriteByte(DoubleType);
357            writer->WriteDouble(propertyValue->GetDouble());
358            return;
359        case PropertyType::Boolean:
360            writer->WriteByte(BooleanType);
361            writer->WriteBoolean(propertyValue->GetBoolean());
362            return;
363        case PropertyType::Char16:
364            writer->WriteByte(Char16Type);
365            writer->WriteUInt16(propertyValue->GetChar16());
366            return;
367        case PropertyType::Guid:
368            writer->WriteByte(GuidType);
369            writer->WriteGuid(propertyValue->GetGuid());
370            return;
371        case PropertyType::String:
372            WriteString(writer, propertyValue->GetString());
373            return;
374        default:
375            throw ref new InvalidArgumentException("Unsupported property type");
376        }
377    }
378
379    void WriteStringToObjectMap(DataWriter^ writer, IMap<String^, Object^>^ map)
380    {
381        writer->WriteByte(StringToObjectMapType);
382        writer->WriteUInt32(map->Size);
383        for (auto&& pair : map)
384        {
385            WriteObject(writer, pair->Key);
386            WriteObject(writer, pair->Value);
387        }
388        writer->WriteByte(MapEndMarker);
389    }
390
391    void WriteObject(DataWriter^ writer, Object^ object)
392    {
393        if (object == nullptr)
394        {
395            writer->WriteByte(NullPtrType);
396            return;
397        }
398
399        auto propertyObject = dynamic_cast<IPropertyValue^>(object);
400        if (propertyObject != nullptr)
401        {
402            WriteProperty(writer, propertyObject);
403            return;
404        }
405
406        auto mapObject = dynamic_cast<IMap<String^, Object^>^>(object);
407        if (mapObject != nullptr)
408        {
409            WriteStringToObjectMap(writer, mapObject);
410            return;
411        }
412
413        throw ref new InvalidArgumentException("Unsupported data type");
414    }
415
416    String^ ReadString(DataReader^ reader)
417    {
418        int length = reader->ReadUInt32();
419        String^ string = reader->ReadString(length);
420        return string;
421    }
422
423    IMap<String^, Object^>^ ReadStringToObjectMap(DataReader^ reader)
424    {
425        auto map = ref new Map<String^, Object^>();
426        auto size = reader->ReadUInt32();
427        for (unsigned int index = 0; index < size; index++)
428        {
429            auto key = safe_cast<String^>(ReadObject(reader));
430            auto value = ReadObject(reader);
431            map->Insert(key, value);
432        }
433        if (reader->ReadByte() != MapEndMarker)
434        {
435            throw ref new InvalidArgumentException("Invalid stream");
436        }
437        return map;
438    }
439
440    Object^ ReadObject(DataReader^ reader)
441    {
442        auto type = reader->ReadByte();
443        switch (type)
444        {
445        case NullPtrType:
446            return nullptr;
447        case UInt8Type:
448            return reader->ReadByte();
449        case UInt16Type:
450            return reader->ReadUInt16();
451        case UInt32Type:
452            return reader->ReadUInt32();
453        case UInt64Type:
454            return reader->ReadUInt64();
455        case Int16Type:
456            return reader->ReadInt16();
457        case Int32Type:
458            return reader->ReadInt32();
459        case Int64Type:
460            return reader->ReadInt64();
461        case SingleType:
462            return reader->ReadSingle();
463        case DoubleType:
464            return reader->ReadDouble();
465        case BooleanType:
466            return reader->ReadBoolean();
467        case Char16Type:
468            return (char16_t)reader->ReadUInt16();
469        case GuidType:
470            return reader->ReadGuid();
471        case StringType:
472            return ReadString(reader);
473        case StringToObjectMapType:
474            return ReadStringToObjectMap(reader);
475        default:
476            throw ref new InvalidArgumentException("Unsupported property type");
477        }
478    }
479}
480
481#pragma endregion
482