summaryrefslogtreecommitdiff
path: root/SmartDeviceLink/SDLLockScreenPresenter.m
blob: d84b3bf95b669841f9e80f746f2b2594e5e48f32 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
//
//  SDLLockScreenPresenter.m
//  SmartDeviceLink-iOS
//
//  Created by Joel Fischer on 7/15/16.
//  Copyright © 2016 smartdevicelink. All rights reserved.
//

#import "SDLLockScreenPresenter.h"

#import "SDLLockScreenRootViewController.h"
#import "SDLLogMacros.h"
#import "SDLStreamingMediaManagerConstants.h"


NS_ASSUME_NONNULL_BEGIN

@interface SDLLockScreenPresenter ()

@property (strong, nonatomic, nullable) UIWindow *lockWindow;
@property (assign, nonatomic) BOOL shouldShowLockScreen;
@property (assign, nonatomic) BOOL isDismissing;
@property (assign, nonatomic) BOOL isPresentedOrPresenting;

@end


@implementation SDLLockScreenPresenter

#pragma mark - Lifecycle

- (instancetype)init {
    self = [super init];
    if (!self) { return nil; }

    _shouldShowLockScreen = NO;

    return self;
}

/// Resets the manager by dismissing and destroying the lockscreen.
- (void)stop {
    self.shouldShowLockScreen = NO;

    if (self.lockWindow == nil) {
        return;
    }

    // Dismiss and destroy the lockscreen window
    [self sdl_dismissWithCompletionHandler:^{
        self.lockWindow = nil;
    }];
}

/// Shows or hides the lockscreen with an animation. If the lockscreen is shown/dismissed in rapid succession the final state of the lockscreen may not match the expected state as the order in which the animations finish can be random. To guard against this scenario, store the expected state of the lockscreen. When the animation finishes, check the expected state to make sure that the final state of the lockscreen matches the expected state. If not, perform a final animation to the expected state.
/// @param show True if the lockscreen should be shown; false if it should be dismissed
- (void)updateLockScreenToShow:(BOOL)show {
    // Store the expected state of the lockscreen
    self.shouldShowLockScreen = show;

    if (show) {
        [self sdl_presentWithCompletionHandler:^{
            if (self.shouldShowLockScreen) { return; }

            SDLLogV(@"The lockscreen has been presented but needs to be dismissed");
            [self sdl_dismissWithCompletionHandler:nil];
        }];
    } else {
        [self sdl_dismissWithCompletionHandler:^{
            if (!self.shouldShowLockScreen) { return; }

            SDLLogV(@"The lockscreen has been dismissed but needs to be presented");
            [self sdl_presentLockscreenWithCompletionHandler:nil];
        }];
    }
}


#pragma mark - Present Lock Window

/// Checks if the lockscreen can be presented and if so presents the lockscreen on the main thread
/// @param completionHandler Called when the lockscreen has finished its animation or if the lockscreen can not be presented
- (void)sdl_presentWithCompletionHandler:(void (^ _Nullable)(void))completionHandler {
    if (self.lockViewController == nil) {
        SDLLogW(@"Attempted to present a lockscreen, but lockViewController is not set");
        if (completionHandler == nil) { return; }
        return completionHandler();
    }

    __weak typeof(self) weakSelf = self;
    dispatch_async(dispatch_get_main_queue(), ^{
        if (UIApplication.sharedApplication.applicationState != UIApplicationStateActive) {
            // If the the `UIWindow` is created while the app is backgrounded and the app is using `SceneDelegate` class (iOS 13+), then the window will not be created correctly. Wait until the app is foregrounded before creating the window.
            SDLLogV(@"Application is backgrounded. The lockscreen will not be shown until the application is brought to the foreground.");
            if (completionHandler == nil) { return; }
            return completionHandler();
        }
        [weakSelf sdl_presentLockscreenWithCompletionHandler:completionHandler];
    });
}

/// Handles the presentation of the lockscreen with animation.
/// @param completionHandler Called when the lockscreen is presented successfully or if it is already in the process of being presented
- (void)sdl_presentLockscreenWithCompletionHandler:(void (^ _Nullable)(void))completionHandler {
    if (self.lockWindow == nil) {
        self.lockWindow = [self.class sdl_createUIWindow];
        self.lockWindow.backgroundColor = [UIColor clearColor];
        self.lockWindow.windowLevel = UIWindowLevelAlert + 1;
        self.lockWindow.rootViewController = [[SDLLockScreenRootViewController alloc] init];
    }

    SDLLogD(@"Presenting the lockscreen window");
    [self.lockWindow makeKeyAndVisible];

    if (self.isPresentedOrPresenting) {
        // Call this right before attempting to present the view controller to make sure we are not already animating, otherwise the app may crash.
        SDLLogV(@"The lockscreen is already being presented");
        if (completionHandler == nil) { return; }
        return completionHandler();
    }

    // Let ourselves know that the lockscreen will present so we can pause video streaming for a few milliseconds - otherwise the animation to show the lockscreen will be very janky.
    [[NSNotificationCenter defaultCenter] postNotificationName:SDLLockScreenManagerWillPresentLockScreenViewController object:nil];

    [self.lockWindow.rootViewController presentViewController:self.lockViewController animated:YES completion:^{
        // Tell everyone we are done so video streaming can resume
        [[NSNotificationCenter defaultCenter] postNotificationName:SDLLockScreenManagerDidPresentLockScreenViewController object:nil];

        if (completionHandler == nil) { return; }
        return completionHandler();
    }];
}


#pragma mark - Dismiss Lock Window

/// Checks if the lockscreen can be dismissed and if so dismisses the lockscreen on the main thread.
/// @param completionHandler Called when the lockscreen has finished its animation or if the lockscreen can not be dismissed
- (void)sdl_dismissWithCompletionHandler:(void (^ _Nullable)(void))completionHandler {
    if (self.lockViewController == nil) {
        SDLLogW(@"Attempted to dismiss lockscreen, but lockViewController is not set");
        if (completionHandler == nil) { return; }
        return completionHandler();
    }

    __weak typeof(self) weakSelf = self;
    dispatch_async(dispatch_get_main_queue(), ^{
        if (UIApplication.sharedApplication.applicationState != UIApplicationStateActive) {
            SDLLogV(@"Application is backgrounded. The lockscreen will not be dismissed until the app is brought to the foreground.");
            if (completionHandler == nil) { return; }
            return completionHandler();
        }
        [weakSelf sdl_dismissLockscreenWithCompletionHandler:completionHandler];
    });
}

/// Handles the dismissal of the lockscreen with animation.
/// @param completionHandler Called when the lockscreen is dismissed successfully or if it is already in the process of being dismissed
- (void)sdl_dismissLockscreenWithCompletionHandler:(void (^ _Nullable)(void))completionHandler {
    if (self.isDismissing) {
        // Make sure we are not already animating, otherwise the app may crash
        SDLLogV(@"The lockscreen is already being dismissed");
        if (completionHandler == nil) { return; }
        return completionHandler();
    }

    // Let ourselves know that the lockscreen will dismiss so we can pause video streaming for a few milliseconds - otherwise the animation to dismiss the lockscreen will be very janky.
    [[NSNotificationCenter defaultCenter] postNotificationName:SDLLockScreenManagerWillDismissLockScreenViewController object:nil];

    SDLLogD(@"Dismissing the lockscreen window");
    __weak typeof(self) weakSelf = self;
    [self.lockViewController dismissViewControllerAnimated:YES completion:^{
        [weakSelf.lockWindow setHidden:YES];

        // Tell everyone we are done so video streaming can resume
        [[NSNotificationCenter defaultCenter] postNotificationName:SDLLockScreenManagerDidDismissLockScreenViewController object:nil];

        if (completionHandler == nil) { return; }
        return completionHandler();
    }];
}


#pragma mark - Custom Presented / Dismissed Getters

/// Returns whether or not the lockViewController is currently presented or currently animating the presentation of the lockscreen
- (BOOL)isPresentedOrPresenting {
    return (self.lockViewController.isViewLoaded && (self.lockViewController.view.window || self.lockViewController.isBeingPresented) && self.lockWindow.isKeyWindow);
}

/// Returns whether or not the lockViewController is currently animating the dismissal of the lockscreen
- (BOOL)isDismissing {
    return (self.lockViewController.isBeingDismissed || self.lockViewController.isMovingFromParentViewController);
}

#pragma mark - Window Helpers

/// If the app is using `SceneDelegate` class (iOS 13+), then the `UIWindow` must be initalized using the active `UIWindowScene`. Otherwise, the newly created window will not appear on the screen even though it is added to the `UIApplication`'s `windows` stack (This seems to be a bug as no official documentation says that a `UIWindow` must be created this way if using the `SceneDelegate` class)
+ (UIWindow *)sdl_createUIWindow {
    if (@available(iOS 13.0, *)) {
        for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
             // The scene is either foreground active / inactive, background, or unattached. If the latter three, we don't want to do anything with them. Also check that the scene is for the application and not an external display or CarPlay.
            if (scene.activationState != UISceneActivationStateForegroundActive ||
                ![scene.session.role isEqualToString:UIWindowSceneSessionRoleApplication] ||
                ![scene isKindOfClass:[UIWindowScene class]]) {
                continue;
            }

            return [[UIWindow alloc] initWithWindowScene:(UIWindowScene *)scene];
        }
    }

    return [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
}

@end

NS_ASSUME_NONNULL_END