]> git.saurik.com Git - cydget.git/blob - LockScreen.mm
Split WebCycript functionality to its own package.
[cydget.git] / LockScreen.mm
1 /* Cydget - open-source AwayView plugin multiplexer
2 * Copyright (C) 2009-2014 Jay Freeman (saurik)
3 */
4
5 /* GNU General Public License, Version 3 {{{ */
6 /*
7 * Cydia is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published
9 * by the Free Software Foundation, either version 3 of the License,
10 * or (at your option) any later version.
11 *
12 * Cydia is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with Cydia. If not, see <http://www.gnu.org/licenses/>.
19 **/
20 /* }}} */
21
22 #include <CydiaSubstrate/CydiaSubstrate.h>
23 #include <UIKit/UIKit.h>
24
25 #include <pthread.h>
26
27 #include <SpringBoardUI/SBAwayViewPluginController.h>
28
29 #include <WebKit/WebFrame.h>
30 #include <WebKit/WebView.h>
31 #include <WebKit/WebPreferences-WebPrivate.h>
32
33 #include <WebCycript.h>
34
35 #ifdef USE_ICU_REGEX
36 #include <unicode/uregex.h>
37 #else
38 #include <pcre.h>
39 #endif
40
41 _disused static unsigned trace_;
42
43 #define _trace() do { \
44 NSLog(@"_trace(%u)@%s:%u[%s](%p)\n", \
45 trace_++, __FILE__, __LINE__, __FUNCTION__, pthread_self() \
46 ); \
47 } while (false)
48
49 typedef uint16_t UChar;
50
51 @interface TPBottomLockBar
52 - (CGFloat) defaultHeight;
53 @end
54
55 @interface UIScroller : UIView
56 - (void) setDirectionalScrolling:(BOOL)directional;
57 - (void) setScrollDecelerationFactor:(CGFloat)factor;
58 - (void) setScrollHysteresis:(CGFloat)hysteresis;
59 - (void) setThumbDetectionEnabled:(BOOL)enabled;
60 @end
61
62 @interface UIWebDocumentView : UIView
63 - (CGRect) documentBounds;
64 - (void) enableReachability;
65 - (void) loadRequest:(NSURLRequest *)request;
66 - (void) redrawScaledDocument;
67 - (void) setAllowsImageSheet:(BOOL)allows;
68 - (void) setAllowsMessaging:(BOOL)allows;
69 - (void) setAutoresizes:(BOOL)autoresizes;
70 - (void) setContentsPosition:(NSInteger)position;
71 - (void) setDrawsBackground:(BOOL)draws;
72 - (void) _setDocumentType:(NSInteger)type;
73 - (void) setDrawsGrid:(BOOL)draws;
74 - (void) setInitialScale:(float)scale forDocumentTypes:(NSInteger)types;
75 - (void) setLogsTilingChanges:(BOOL)logs;
76 - (void) setMinimumScale:(float)scale forDocumentTypes:(NSInteger)types;
77 - (void) setMinimumSize:(CGSize)size;
78 - (void) setMaximumScale:(float)scale forDocumentTypes:(NSInteger)tpyes;
79 - (void) setSmoothsFonts:(BOOL)smooths;
80 - (void) setTileMinificationFilter:(NSString *)filter;
81 - (void) setTileSize:(CGSize)size;
82 - (void) setTilingEnabled:(BOOL)enabled;
83 - (void) setViewportSize:(CGSize)size forDocumentTypes:(NSInteger)types;
84 - (void) setZoomsFocusedFormControl:(BOOL)zooms;
85 - (void) useSelectionAssistantWithMode:(NSInteger)mode;
86 - (WebView *) webView;
87 @end
88
89 @interface UIView (Apple)
90 - (UIScroller *) _scroller;
91 - (void) setClipsSubviews:(BOOL)clips;
92 - (void) setEnabledGestures:(NSInteger)gestures;
93 - (void) setFixedBackgroundPattern:(BOOL)fixed;
94 - (void) setGestureDelegate:(id)delegate;
95 - (void) setNeedsDisplayOnBoundsChange:(BOOL)needs;
96 - (void) setValue:(NSValue *)value forGestureAttribute:(NSInteger)attribute;
97 - (void) setZoomScale:(float)scale duration:(double)duration;
98 - (void) _setZoomScale:(float)scale duration:(double)duration;
99 @end
100
101 @interface SBLockScreenView : UIView
102 - (BOOL) mediaControlsHidden;
103 @end
104
105 @interface SBLockScreenNotificationListController : NSObject
106 - (BOOL) hasAnyContent;
107 @end
108
109 @interface SBLockScreenViewController : UIViewController
110 - (SBLockScreenView *) lockScreenView;
111 - (BOOL) isShowingMediaControls;
112 - (void) _setMediaControlsVisible:(BOOL)visible;
113 - (SBLockScreenNotificationListController *) _notificationController;
114 @end
115
116 @interface SBLockScreenManager : NSObject
117 + (SBLockScreenManager *) sharedInstance;
118 - (SBLockScreenViewController *) lockScreenViewController;
119 @end
120
121 @protocol CydgetController
122 - (NSDictionary *) currentConfiguration;
123 - (NSString *) currentPath;
124 @end
125
126 static Class $CydgetController(objc_getClass("CydgetController"));
127
128 MSClassHook(SBLockScreenManager)
129
130 #ifdef USE_ICU_REGEX
131 /* ICU Regular Expression {{{ */
132 class RegEx {
133 private:
134 URegularExpression *regex_;
135
136 public:
137 RegEx(const char *regex) {
138 UParseError error;
139 UErrorCode status(U_ZERO_ERROR);
140 regex_ = uregex_openC(regex, 0, &error, &status);
141 if (U_FAILURE(status))
142 @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"*** RegEx(): [%u] %s", error.offset, u_errorName(status)] userInfo:nil];
143 }
144
145 ~RegEx() {
146 uregex_close(regex_);
147 }
148
149 bool operator ()(NSString *string) {
150 return operator ()(reinterpret_cast<const uint16_t *>([string cStringUsingEncoding:NSUTF16StringEncoding]), [string length]);
151 }
152
153 bool operator ()(const UChar *data, size_t size) {
154 UErrorCode status(U_ZERO_ERROR);
155 uregex_setText(regex_, data, size, &status);
156 if (!U_SUCCESS(status))
157 return false;
158 bool matches(uregex_matches(regex_, 0, &status));
159 if (!U_SUCCESS(status))
160 return false;
161 return matches;
162 }
163 };
164 /* }}} */
165 #else
166 /* Perl-Compatible RegEx {{{ */
167 class RegEx {
168 private:
169 pcre *code_;
170 pcre_extra *study_;
171 int capture_;
172 int *matches_;
173 const char *data_;
174
175 public:
176 RegEx(const char *regex) :
177 study_(NULL)
178 {
179 const char *error;
180 int offset;
181 code_ = pcre_compile(regex, 0, &error, &offset, NULL);
182
183 if (code_ == NULL)
184 @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"*** RegEx(): [%u] %s", offset, error] userInfo:nil];
185
186 pcre_fullinfo(code_, study_, PCRE_INFO_CAPTURECOUNT, &capture_);
187 matches_ = new int[(capture_ + 1) * 3];
188 }
189
190 ~RegEx() {
191 pcre_free(code_);
192 delete matches_;
193 }
194
195 bool operator ()(NSString *data) {
196 // XXX: length is for characters, not for bytes
197 return operator ()([data UTF8String], [data length]);
198 }
199
200 bool operator ()(const char *data, size_t size) {
201 data_ = data;
202 return pcre_exec(code_, study_, data, size, 0, 0, matches_, (capture_ + 1) * 3) >= 0;
203 }
204 };
205 /* }}} */
206 #endif
207
208 static float CYScrollViewDecelerationRateNormal;
209
210 @interface UIScrollView (Apple)
211 - (void) setDecelerationRate:(CGFloat)value;
212 - (void) setScrollingEnabled:(BOOL)enabled;
213 - (void) setShowBackgroundShadow:(BOOL)show;
214 @end
215
216 @interface UIWebView (Apple)
217 - (UIWebDocumentView *) _documentView;
218 - (void) setDataDetectorTypes:(NSInteger)types;
219 - (void) _setDrawInWebThread:(BOOL)draw;
220 - (UIScrollView *) _scrollView;
221 - (UIScroller *) _scroller;
222 - (void) webView:(WebView *)view addMessageToConsole:(NSDictionary *)message;
223 - (void) webView:(WebView *)view didClearWindowObject:(WebScriptObject *)window forFrame:(WebFrame *)frame;
224 @end
225
226 @interface WebView (Apple)
227 - (void) _setLayoutInterval:(float)interval;
228 - (void) _setAllowsMessaging:(BOOL)allows;
229 - (void) setShouldUpdateWhileOffscreen:(BOOL)update;
230 @end
231
232 @protocol CydgetWebViewDelegate <UIWebViewDelegate>
233 - (void) webView:(WebView *)view didClearWindowObject:(WebScriptObject *)window forFrame:(WebFrame *)frame;
234 @end
235
236 @class UIWebViewWebViewDelegate;
237
238 @interface UIWebView (WebCycript)
239 - (void) updateStyles;
240 @end
241
242 @interface CydgetWebView : UIWebView {
243 }
244
245 @end
246
247 @implementation CydgetWebView
248
249 - (void) webView:(WebView *)view didClearWindowObject:(WebScriptObject *)window forFrame:(WebFrame *)frame {
250 NSObject<CydgetWebViewDelegate> *delegate((NSObject<CydgetWebViewDelegate> *) [self delegate]);
251 if ([delegate respondsToSelector:@selector(webView:didClearWindowObject:forFrame:)])
252 [delegate webView:view didClearWindowObject:window forFrame:frame];
253 if ([UIWebView instancesRespondToSelector:@selector(webView:didClearWindowObject:forFrame:)])
254 [super webView:view didClearWindowObject:window forFrame:frame];
255 }
256
257 - (void) webView:(WebView *)view addMessageToConsole:(NSDictionary *)message {
258 NSLog(@"addMessageToConsole:%@", message);
259
260 if ([UIWebView instancesRespondToSelector:@selector(webView:addMessageToConsole:)])
261 [super webView:view addMessageToConsole:message];
262 }
263
264 @end
265
266 @interface WebCydgetLockScreenView : UIView <UIWebViewDelegate> {
267 CydgetWebView *webview_;
268 UIScrollView *scroller_;
269 NSString *cycript_;
270 }
271
272 - (void) updateStyles;
273
274 @end
275
276 @implementation WebCydgetLockScreenView
277
278 //#include "UICaboodle/UCInternal.h"
279
280 - (void) dealloc {
281 [[NSNotificationCenter defaultCenter] removeObserver:self];
282 [webview_ setDelegate:nil];
283 [webview_ release];
284 [super dealloc];
285 }
286
287 - (void) loadRequest:(NSURLRequest *)request {
288 [webview_ loadRequest:request];
289 }
290
291 - (void) loadURL:(NSURL *)url cachePolicy:(NSURLRequestCachePolicy)policy {
292 [self loadRequest:[NSURLRequest
293 requestWithURL:url
294 cachePolicy:policy
295 timeoutInterval:30.0
296 ]];
297 }
298
299 - (void) loadURL:(NSURL *)url {
300 [self loadURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy];
301 }
302
303 - (id) initWithURL:(NSURL *)url {
304 CGRect frame = [[UIScreen mainScreen] bounds];
305 if (kCFCoreFoundationVersionNumber < 800)
306 frame.size.height -= 20; //[[[$SBStatusBarController sharedStatusBarController] statusBarView] frame].size.height;
307
308 if ((self = [super initWithFrame:frame]) != nil) {
309 CGRect bounds([self bounds]);
310 if (kCFCoreFoundationVersionNumber < 800)
311 bounds.size.height -= [objc_getClass("TPBottomLockBar") defaultHeight];
312
313 webview_ = [[CydgetWebView alloc] initWithFrame:bounds];
314 [webview_ setDelegate:self];
315 [self addSubview:webview_];
316
317 if ([webview_ respondsToSelector:@selector(setDataDetectorTypes:)])
318 [webview_ setDataDetectorTypes:0x80000000];
319 else
320 [webview_ setDetectsPhoneNumbers:NO];
321
322 [webview_ setScalesPageToFit:YES];
323
324 if (kCFCoreFoundationVersionNumber < 478.61)
325 if ([webview_ respondsToSelector:@selector(_setDrawInWebThread:)])
326 [webview_ _setDrawInWebThread:NO];
327
328 UIWebDocumentView *document([webview_ _documentView]);
329 WebView *webview([document webView]);
330 WebPreferences *preferences([webview preferences]);
331
332 [document setTileSize:CGSizeMake(bounds.size.width, 500)];
333
334 [document setBackgroundColor:[UIColor clearColor]];
335 [document setDrawsBackground:NO];
336
337 [webview setPreferencesIdentifier:@"WebCycript"];
338
339 if ([webview respondsToSelector:@selector(_setLayoutInterval:)])
340 [webview _setLayoutInterval:0];
341 else
342 [preferences _setLayoutInterval:0];
343
344 [preferences setCacheModel:WebCacheModelDocumentViewer];
345 [preferences setJavaScriptCanOpenWindowsAutomatically:YES];
346 [preferences setOfflineWebApplicationCacheEnabled:YES];
347
348 if ([webview respondsToSelector:@selector(setShouldUpdateWhileOffscreen:)])
349 [webview setShouldUpdateWhileOffscreen:NO];
350
351 if ([document respondsToSelector:@selector(setAllowsMessaging:)])
352 [document setAllowsMessaging:YES];
353 if ([webview respondsToSelector:@selector(_setAllowsMessaging:)])
354 [webview _setAllowsMessaging:YES];
355
356 if ([webview_ respondsToSelector:@selector(_scrollView)]) {
357 scroller_ = [webview_ _scrollView];
358
359 [scroller_ setDirectionalLockEnabled:YES];
360 [scroller_ setDecelerationRate:CYScrollViewDecelerationRateNormal];
361 [scroller_ setDelaysContentTouches:NO];
362
363 [scroller_ setCanCancelContentTouches:YES];
364
365 [scroller_ setAlwaysBounceVertical:NO];
366 } else if ([webview_ respondsToSelector:@selector(_scroller)]) {
367 UIScroller *scroller([webview_ _scroller]);
368 scroller_ = (UIScrollView *) scroller;
369
370 [scroller setDirectionalScrolling:YES];
371 [scroller setScrollDecelerationFactor:CYScrollViewDecelerationRateNormal]; /* 0.989324 */
372 [scroller setScrollHysteresis:0]; /* 8 */
373
374 [scroller setThumbDetectionEnabled:NO];
375 }
376
377 [webview_ setOpaque:NO];
378 [webview_ setBackgroundColor:[UIColor clearColor]];
379
380 [scroller_ setFixedBackgroundPattern:YES];
381 [scroller_ setBackgroundColor:[UIColor clearColor]];
382 [scroller_ setClipsSubviews:NO];
383
384 [scroller_ setBounces:YES];
385 [scroller_ setShowBackgroundShadow:NO]; /* YES */
386
387 [self setAutoresizingMask:(UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight)];
388 [webview_ setAutoresizingMask:(UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight)];
389
390 NSDictionary *configuration([$CydgetController currentConfiguration]);
391 cycript_ = [configuration objectForKey:@"CycriptURLs"];
392
393 [scroller_ setScrollingEnabled:[[configuration objectForKey:@"Scrollable"] boolValue]];
394
395 [self loadURL:url];
396
397 [[NSNotificationCenter defaultCenter]
398 addObserver:self
399 selector:@selector(mediaControlsDidSomething:)
400 name:@"SBLockScreenViewControllerMediaControlsDidShow"
401 object:nil
402 ];
403
404 [[NSNotificationCenter defaultCenter]
405 addObserver:self
406 selector:@selector(mediaControlsDidSomething:)
407 name:@"SBLockScreenViewControllerMediaControlsDidHide"
408 object:nil
409 ];
410 } return self;
411 }
412
413 - (void) mediaControlsDidSomething:(NSNotification *)notification {
414 [self updateStyles];
415 }
416
417 - (void) webView:(WebView *)webview didClearWindowObject:(WebScriptObject *)window forFrame:(WebFrame *)frame {
418 if (cycript_ != nil)
419 if (NSString *href = [[[[frame dataSource] request] URL] absoluteString])
420 if (RegEx([cycript_ UTF8String])(href))
421 WebCycriptSetupView(webview);
422 }
423
424 - (void) updateStyles {
425 [webview_ updateStyles];
426 }
427
428 @end
429
430 @interface WebCycriptLockScreenController : SBAwayViewPluginController {
431 NSDictionary *configuration_;
432 bool legacy_;
433 bool media_;
434 bool items_;
435 WebCydgetLockScreenView *background_;
436 WebCydgetLockScreenView *foreground_;
437 }
438
439 @end
440
441 static BOOL CYHaveMediaControls() {
442 SBLockScreenView *view([[[$SBLockScreenManager sharedInstance] lockScreenViewController] lockScreenView]);
443 return view != nil && ![view mediaControlsHidden];
444 //return [[[$SBLockScreenManager sharedInstance] lockScreenViewController] isShowingMediaControls];
445 }
446
447 static BOOL CYHaveNotificationList() {
448 SBLockScreenNotificationListController *controller([[[$SBLockScreenManager sharedInstance] lockScreenViewController] _notificationController]);
449 return controller != nil && [controller hasAnyContent];
450 }
451
452 static void $UIWebViewWebViewDelegate$webView$addMessageToConsole$(UIWebViewWebViewDelegate *self, SEL sel, WebView *view, NSDictionary *message) {
453 UIWebView *uiWebView(MSHookIvar<UIWebView *>(self, "uiWebView"));
454 if ([uiWebView respondsToSelector:@selector(webView:addMessageToConsole:)])
455 [uiWebView webView:view addMessageToConsole:message];
456 }
457
458 static void $UIWebViewWebViewDelegate$webView$didClearWindowObject$forFrame$(UIWebViewWebViewDelegate *self, SEL sel, WebView *view, WebScriptObject *window, WebFrame *frame) {
459 UIWebView *uiWebView(MSHookIvar<UIWebView *>(self, "uiWebView"));
460 if ([uiWebView respondsToSelector:@selector(webView:didClearWindowObject:forFrame:)])
461 [uiWebView webView:view didClearWindowObject:window forFrame:frame];
462 }
463
464 MSInitialize {
465 if (Class $UIWebViewWebViewDelegate = objc_getClass("UIWebViewWebViewDelegate")) {
466 class_addMethod($UIWebViewWebViewDelegate, @selector(webView:addMessageToConsole:), (IMP) &$UIWebViewWebViewDelegate$webView$addMessageToConsole$, "v16@0:4@8@12");
467 class_addMethod($UIWebViewWebViewDelegate, @selector(webView:didClearWindowObject:forFrame:), (IMP) &$UIWebViewWebViewDelegate$webView$didClearWindowObject$forFrame$, "v20@0:4@8@12@16");
468 }
469
470 if (CGFloat *_UIScrollViewDecelerationRateNormal = reinterpret_cast<CGFloat *>(dlsym(RTLD_DEFAULT, "UIScrollViewDecelerationRateNormal")))
471 CYScrollViewDecelerationRateNormal = *_UIScrollViewDecelerationRateNormal;
472 else // XXX: this actually might be fast on some older systems: we should look into this
473 CYScrollViewDecelerationRateNormal = 0.998;
474
475 if (kCFCoreFoundationVersionNumber >= 800) {
476 WebCycriptRegisterStyle("-cydget-media-controls", &CYHaveMediaControls);
477 WebCycriptRegisterStyle("-cydget-notification-list", &CYHaveNotificationList);
478 }
479 }
480
481 @implementation WebCycriptLockScreenController
482
483 + (id) rootViewController {
484 return [[[self alloc] init] autorelease];
485 }
486
487 - (void) dealloc {
488 [configuration_ release];
489 [background_ release];
490 [foreground_ release];
491 [super dealloc];
492 }
493
494 - (id) init {
495 if ((self = [super init]) != nil) {
496 configuration_ = [[$CydgetController currentConfiguration] retain];
497 legacy_ = [configuration_ objectForKey:@"Background"] == nil;
498 media_ = [[configuration_ objectForKey:@"MediaControls"] boolValue];
499 items_ = [[configuration_ objectForKey:@"NotificationList"] boolValue];
500 } return self;
501 }
502
503 - (void) loadView {
504 NSURL *base([NSURL fileURLWithPath:[$CydgetController currentPath]]);
505
506 if (NSString *background = [configuration_ objectForKey:@"Background"])
507 background_ = [[WebCydgetLockScreenView alloc] initWithURL:[NSURL URLWithString:background relativeToURL:base]];
508
509 if (NSString *homepage = [configuration_ objectForKey:@"Homepage"]) {
510 foreground_ = [[WebCydgetLockScreenView alloc] initWithURL:[NSURL URLWithString:homepage relativeToURL:base]];
511 [self setView:foreground_];
512 }
513 }
514
515 - (void) purgeView {
516 [background_ removeFromSuperview];
517 [background_ release];
518 background_ = nil;
519 [foreground_ removeFromSuperview];
520 [foreground_ release];
521 foreground_ = nil;
522 [super purgeView];
523 }
524
525 - (void) updateStyles {
526 [foreground_ updateStyles];
527 [background_ updateStyles];
528 }
529
530 - (UIView *) backgroundView {
531 return background_;
532 }
533
534 - (BOOL) showAwayItems {
535 return YES;
536 }
537
538 - (BOOL) updateHidden {
539 if (foreground_ == nil)
540 return true;
541 bool media(CYHaveMediaControls());
542 [foreground_ setHidden:media];
543 return media;
544 }
545
546 - (BOOL) showDateView {
547 if (kCFCoreFoundationVersionNumber < 800)
548 return false;
549 [self updateStyles];
550 if (!legacy_ && foreground_ == nil)
551 return true;
552 if (!items_ && CYHaveNotificationList())
553 return true;
554 return false;
555 }
556
557 - (BOOL) allowsLockScreenMediaControls {
558 if (kCFCoreFoundationVersionNumber < 800)
559 return true;
560 return media_;
561 }
562
563 /*- (BOOL) showHeaderView {
564 return YES;
565 }*/
566
567 // 0: view is rendered above head
568 // 1: view moves as one with head
569 // 2: view moves up and down only
570 // 3: view simply never does move
571 - (NSUInteger) presentationStyle {
572 return 1;
573 }
574
575 // 1: light blur
576 // 2: heavy blur
577 // 3: just black
578 // 4: legibility
579 // 5: no overlay
580 - (NSUInteger) overlayStyle {
581 return legacy_ ? 1 : 4;
582 }
583
584 // 1: blur -> view -> list
585 // 2: view -> blur -> list
586 // 3: view. unblur below?!
587 // 4: blur -> list -> view
588 - (NSUInteger) notificationBehavior {
589 return items_ ? 1 : 2;
590 }
591
592 - (BOOL) viewWantsFullscreenLayout {
593 return kCFCoreFoundationVersionNumber >= 800;
594 }
595
596 /*- (BOOL) viewWantsOverlayLayout {
597 return kCFCoreFoundationVersionNumber >= 800;
598 }*/
599
600 - (BOOL) shouldDisableOnUnlock {
601 return YES;
602 }
603
604 - (BOOL) canBeAlwaysFullscreen {
605 return YES;
606 }
607
608 /*- (BOOL) wantsSwipeGestureRecognizer {
609 return YES;
610 }
611
612 - (BOOL) handleGesture:(int)arg1 fingerCount:(NSUInteger)fingers {
613 return NO;
614 return YES;
615 }*/
616
617 // - (void) lockScreenMediaControlsShown:(BOOL)shown;
618
619 - (BOOL) handleMenuButtonDoubleTap {
620 if (kCFCoreFoundationVersionNumber >= 800) {
621 SBLockScreenViewController *controller([[$SBLockScreenManager sharedInstance] lockScreenViewController]);
622 [controller _setMediaControlsVisible:![controller isShowingMediaControls]];
623 }
624
625 return [super handleMenuButtonDoubleTap];
626 }
627
628 @end