Avoid modifying stylesheets when updating @medias.
[cydget.git] / CydgetLoader.mm
1 /* Cydget - open-source AwayView plugin multiplexer
2  * Copyright (C) 2009-2015  Jay Freeman (saurik)
3 */
4
5 /* GNU General Public License, Version 3 {{{ */
6 /*
7  * Cydget 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  * Cydget 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 Cydget.  If not, see <http://www.gnu.org/licenses/>.
19 **/
20 /* }}} */
21
22 #include <CydiaSubstrate/CydiaSubstrate.h>
23
24 #include <Foundation/Foundation.h>
25 #include <UIKit/UIKit.h>
26
27 #import <SpringBoard/SBAwayController.h>
28 #import <SpringBoard/SBAwayView.h>
29 #import <SpringBoard/SBAwayWindow.h>
30 #import <SpringBoard/SpringBoard.h>
31
32 #include "Handle.hpp"
33
34 #define _trace() \
35     NSLog(@"_trace(%s:%u)@%s %zd", __FILE__, __LINE__, __FUNCTION__, active_)
36 #define _not(type) \
37     static_cast<type>(~type())
38
39 typedef void *GSEventRef;
40
41 extern "C" void UIKeyboardEnableAutomaticAppearance();
42 extern "C" void UIKeyboardDisableAutomaticAppearance();
43
44 MSClassHook(SpringBoard)
45
46 MSClassHook(SBAwayController)
47 MSClassHook(SBAwayView)
48 MSClassHook(SBAwayWindow)
49 MSClassHook(SBLockScreenManager)
50 MSClassHook(SBLockScreenView)
51 MSClassHook(SBLockScreenViewController)
52 MSClassHook(SBScreenFadeAnimationController)
53 MSClassHook(SBUserAgent)
54
55 MSInitialize {
56     if (kCFCoreFoundationVersionNumber < 800) {
57         $SBUserAgent = $SBAwayController;
58         $SBLockScreenManager = $SBAwayController;
59         $SBLockScreenView = $SBAwayView;
60     }
61 }
62
63 @interface SBLockScreenNowPlayingPluginController : NSObject
64 - (BOOL) isNowPlayingPluginActive;
65 @end
66
67 @interface SBLockScreenViewController : UIViewController
68 - (SBLockScreenView *) lockScreenView;
69 - (BOOL) isShowingMediaControls;
70 - (void) _toggleMediaControls;
71 @end
72
73 @interface SBLockScreenManager : NSObject
74 + (SBLockScreenManager *) sharedInstance;
75 - (SBLockScreenViewController *) lockScreenViewController;
76 @end
77
78 static bool menu_;
79
80 static _H<NSDictionary> settings_;
81 static _H<NSMutableArray> cydgets_;
82 static size_t active_;
83 static unsigned online_;
84 static bool nowplaying_;
85 static bool remember_;
86
87 static bool NowPlaying() {
88     if (kCFCoreFoundationVersionNumber < 800)
89         return false;
90     if (!nowplaying_)
91         return false;
92
93     SBLockScreenViewController *controller([[$SBLockScreenManager sharedInstance] lockScreenViewController]);
94     if (controller != nil) {
95         SBLockScreenNowPlayingPluginController *now(MSHookIvar<SBLockScreenNowPlayingPluginController *>(controller, "_nowPlayingController"));
96         if (now != nil && [now isNowPlayingPluginActive])
97             return true;
98     }
99
100     return false;
101 }
102
103 @interface CydgetController : NSObject {
104 }
105
106 + (NSDictionary *) currentConfiguration;
107 + (NSString *) currentPath;
108
109 @end
110
111 @implementation CydgetController
112
113 + (NSDictionary *) currentConfiguration {
114     NSDictionary *cydget([cydgets_ objectAtIndex:active_]);
115     return [cydget objectForKey:@"CYConfiguration"] ?: [cydget objectForKey:@"Configuration"];
116 }
117
118 + (NSDictionary *) currentPath {
119     NSDictionary *cydget([cydgets_ objectAtIndex:active_]);
120     return [cydget objectForKey:@"CYPath"];
121 }
122
123 @end
124
125 @interface SBUserAgent : NSObject
126 + (SBUserAgent *) sharedUserAgent;
127 - (void) enableLockScreenBundleNamed:(NSString *)bundle activationContext:(id)context;
128 - (void) disableLockScreenBundleNamed:(NSString *)bundle deactivationContext:(id)context;
129 @end
130
131 @interface UIPeripheralHost : NSObject
132 + (UIPeripheralHost *) sharedInstance;
133 + (void) _releaseSharedInstance;
134 @end
135
136 MSClassHook(UIPeripheralHost)
137
138 @interface UITextEffectsWindow : UIWindow
139 + (UIWindow *) sharedTextEffectsWindow;
140 @end
141
142 @implementation NSDictionary (CydgetLoader)
143
144 - (NSString *) cydget {
145     return [self objectForKey:@"CYPlugin"] ?: [self objectForKey:@"Plugin"];
146 }
147
148 - (void) enableCydget {
149     if (NSString *plugin = [self cydget]) {
150         ++online_;
151
152         if (kCFCoreFoundationVersionNumber < 600)
153             UIKeyboardEnableAutomaticAppearance();
154
155         [[UITextEffectsWindow sharedTextEffectsWindow] setWindowLevel:1000];
156
157         if (kCFCoreFoundationVersionNumber < 800)
158             [[$SBAwayController sharedAwayController] enableLockScreenBundleWithName:plugin];
159         else
160             [[$SBUserAgent sharedUserAgent] enableLockScreenBundleNamed:plugin activationContext:nil];
161     }
162 }
163
164 - (void) disableCydget {
165     if (NSString *plugin = [self cydget]) {
166         if (kCFCoreFoundationVersionNumber < 800)
167             [[$SBAwayController sharedAwayController] disableLockScreenBundleWithName:plugin];
168         else
169             [[$SBUserAgent sharedUserAgent] disableLockScreenBundleNamed:plugin deactivationContext:nil];
170
171         [$UIPeripheralHost _releaseSharedInstance];
172
173         if (kCFCoreFoundationVersionNumber < 600)
174             UIKeyboardDisableAutomaticAppearance();
175
176         --online_;
177     }
178 }
179
180 @end
181
182 // avoid rendering a keyboard onto the default SBAwayView while automatic keyboard is online
183 MSInstanceMessageHook0(UIView *, SBAwayView, inputView) {
184     // XXX: there is a conceptual error here
185     if (online_ == 0 && false || kCFCoreFoundationVersionNumber > 600)
186         return MSOldCall();
187
188     return [[[UIView alloc] init] autorelease];
189 }
190
191 // by default, keyboard actions are redirected to SBAwayController and press menu button
192 MSInstanceMessageHook1(void, SpringBoard, handleKeyEvent, GSEventRef, event) {
193     if (online_ == 0 || kCFCoreFoundationVersionNumber > 600)
194         return MSOldCall(event);
195
196     return MSSuperCall(event);
197 }
198
199 bool media_;
200
201 MSInstanceMessageHook0(void, SBLockScreenViewController, _toggleMediaControls) {
202     if (!media_)
203         MSOldCall();
204 }
205
206 MSInstanceMessageHook0(BOOL, SBLockScreenViewController, handleMenuButtonDoubleTap) {
207     menu_ = false;
208     BOOL value(MSOldCall());
209     if (kCFCoreFoundationVersionNumber >= 800)
210         [self _toggleMediaControls];
211     return value;
212 }
213
214 MSInstanceMessageHook0(BOOL, SBLockScreenManager, handleMenuButtonTap) {
215     media_ = true;
216     BOOL value(MSOldCall());
217     media_ = false;
218
219     if (!value && menu_) {
220         if (active_ != _not(size_t))
221             [[cydgets_ objectAtIndex:active_] disableCydget];
222         active_ = (active_ + 1) % [cydgets_ count];
223         [[cydgets_ objectAtIndex:active_] enableCydget];
224         // XXX: or siri doesn't disappear correctly
225         return kCFCoreFoundationVersionNumber >= 800 ? value : YES;
226     }
227
228     return value;
229 }
230
231 void Activate_() {
232     menu_ = false;
233     if (NowPlaying()) {
234         for (active_ = 0; active_ != [cydgets_ count]; ++active_)
235             if ([[[cydgets_ objectAtIndex:active_] objectForKey:@"CYName"] isEqualToString:@"AwayView"])
236                 break;
237         if (active_ == [cydgets_ count])
238             active_ = _not(size_t);
239     }
240     if (active_ != _not(size_t))
241         [[cydgets_ objectAtIndex:active_] enableCydget];
242 }
243
244 static void Undim_(SBAwayController *self) {
245     if ([self isDimmed]) {
246         Activate_();
247         [[[self awayView] window] makeKeyWindow];
248     }
249 }
250
251 MSInstanceMessageHook1(void, SBLockScreenViewController, startLockScreenFadeInAnimationForSource, int, source) {
252     Activate_();
253     MSOldCall(source);
254 }
255
256 MSInstanceMessageHook0(void, SBAwayController, undimScreen) {
257     Undim_(self);
258     MSOldCall();
259 }
260
261 MSInstanceMessageHook1(void, SBAwayController, undimScreen, BOOL, undim) {
262     Undim_(self);
263     MSOldCall(undim);
264 }
265
266 static void Deactivate_() {
267     if (active_ != _not(size_t))
268         [[cydgets_ objectAtIndex:active_] disableCydget];
269     if (!remember_)
270         active_ = 0;
271 }
272
273 MSHook(void, BKSDisplayServicesSetScreenBlanked, BOOL blanked) {
274     if (blanked)
275         Deactivate_();
276     _BKSDisplayServicesSetScreenBlanked(blanked);
277 }
278
279 MSInitialize {
280     MSHookFunction("_BKSDisplayServicesSetScreenBlanked", MSHake(BKSDisplayServicesSetScreenBlanked));
281 }
282
283 MSInstanceMessageHook1(void, SBUserAgent, dimScreen, BOOL, dim) {
284     Deactivate_();
285     MSOldCall(dim);
286 }
287
288 MSInstanceMessageHook0(void, SpringBoard, _menuButtonWasHeld) {
289     menu_ = false;
290     MSOldCall();
291 }
292
293 MSInstanceMessageHook1(void, SpringBoard, _menuButtonDown, GSEventRef, event) {
294     menu_ = true;
295     MSOldCall(event);
296 }
297
298 MSInstanceMessageHook1(void, SpringBoard, menuButtonDown, GSEventRef, event) {
299     menu_ = true;
300     MSOldCall(event);
301 }
302
303 MSInstanceMessageHook1(void, SBAwayView, addGestureRecognizer, id, recognizer) {
304     if (online_ == 0 || kCFCoreFoundationVersionNumber > 600)
305         return MSOldCall(recognizer);
306 }
307
308 MSInstanceMessageHook1(void, SBAwayWindow, sendGSEvent, GSEventRef, event) {
309     if (online_ == 0 || kCFCoreFoundationVersionNumber > 600)
310         return MSOldCall(event);
311
312     return MSSuperCall(event);
313 }
314
315 MSInstanceMessageHook2(void, SBLockScreenView, setMediaControlsHidden,forRequester, BOOL, hidden, id, requester) {
316     MSOldCall(hidden, requester);
317     UIView *&_mediaControlsContainerView(MSHookIvar<UIView *>(self, "_mediaControlsContainerView"));
318     UIView *&_mediaControlsView(MSHookIvar<UIView *>(self, "_mediaControlsView"));
319     if (&_mediaControlsContainerView != NULL && &_mediaControlsView != NULL)
320         [_mediaControlsContainerView setUserInteractionEnabled:([_mediaControlsView alpha] != 0)];
321 }
322
323 #define Cydgets_ @"/System/Library/LockCydgets"
324
325 static void CydgetSetup() {
326     NSString *plist([NSString stringWithFormat:@"%@/Library/Preferences/com.saurik.Cydget.plist", NSHomeDirectory()]);
327     settings_ = [NSMutableDictionary dictionaryWithContentsOfFile:plist] ?: [NSMutableDictionary dictionary];
328
329     nowplaying_ = [[settings_ objectForKey:@"NowPlaying"] boolValue];
330     remember_ = [[settings_ objectForKey:@"RememberPosition"] boolValue];
331
332     NSArray *cydgets([settings_ objectForKey:@"LockCydgets"] ?: [NSArray arrayWithObjects:[NSDictionary dictionaryWithObjectsAndKeys:
333         @"Welcome", @"Name", [NSNumber numberWithBool:YES], @"Active", nil
334     ], [NSDictionary dictionaryWithObjectsAndKeys:
335         @"AwayView", @"Name", [NSNumber numberWithBool:YES], @"Active", nil
336     ], nil]);
337
338     cydgets_ = [NSMutableArray arrayWithCapacity:4];
339     for (NSDictionary *cydget in cydgets)
340         if ([[cydget objectForKey:@"Active"] boolValue]) {
341             NSString *name([cydget objectForKey:@"Name"]);
342             NSString *path([NSString stringWithFormat:@"%@/%@.cydget/Info.plist", Cydgets_, name]);
343             if (NSMutableDictionary *info = [NSMutableDictionary dictionaryWithContentsOfFile:path]) {
344                 [info setObject:name forKey:@"CYName"];
345                 [info setObject:path forKey:@"CYPath"];
346                 [cydgets_ addObject:info];
347             }
348         }
349
350     if ([cydgets_ count] == 0)
351         cydgets_ = nil;
352 }
353
354 // XXX: this could happen while it is unlocked
355 MSInstanceMessageHook1(id, SBLockScreenView, initWithFrame, CGRect, frame) {
356     self = MSOldCall(frame);
357
358     static bool loaded(false);
359     if (!loaded) {
360         loaded = true;
361         CydgetSetup();
362         Activate_();
363     }
364
365     // XXX: this is just wrong, I don't know what to do :(
366     else if (kCFCoreFoundationVersionNumber < 800)
367         Activate_();
368
369     return self;
370 }
371
372 MSInstanceMessageHook1(void, SBAwayController, _finishedUnlockAttemptWithStatus, BOOL, status) {
373     if (status)
374         Deactivate_();
375     MSOldCall(status);
376 }
377
378 // this is called occasionally by -[SBAwayView updateInterface] and takes away our keyboard
379 MSInstanceMessageHook0(void, SBAwayView, _fixupFirstResponder) {
380     if (online_ == 0)
381         return MSOldCall();
382 }
383
384 MSInstanceMessageHook0(void, SBAwayView, updateInterface) {
385     MSOldCall();
386
387     NSDictionary *cydget([cydgets_ objectAtIndex:active_]);
388
389     NSString *background([cydget objectForKey:@"CYBackground"]);
390     if ([background isEqualToString:@"Wallpaper"]) {
391         MSIvarHook(UIView *, _backgroundView);
392         [_backgroundView setAlpha:1.0f];
393     }
394
395     if ([[cydget objectForKey:@"CYShowDateTime"] boolValue])
396         [self addDateView];
397 }