Correct for MediaControls-caused touch event hole.
[cydget.git] / CydgetLoader.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
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
86 static bool NowPlaying() {
87     if (kCFCoreFoundationVersionNumber < 800)
88         return false;
89     if (!nowplaying_)
90         return false;
91
92     SBLockScreenViewController *controller([[$SBLockScreenManager sharedInstance] lockScreenViewController]);
93     if (controller != nil) {
94         SBLockScreenNowPlayingPluginController *now(MSHookIvar<SBLockScreenNowPlayingPluginController *>(controller, "_nowPlayingController"));
95         if (now != nil && [now isNowPlayingPluginActive])
96             return true;
97     }
98
99     return false;
100 }
101
102 @interface CydgetController : NSObject {
103 }
104
105 + (NSDictionary *) currentConfiguration;
106 + (NSString *) currentPath;
107
108 @end
109
110 @implementation CydgetController
111
112 + (NSDictionary *) currentConfiguration {
113     NSDictionary *cydget([cydgets_ objectAtIndex:active_]);
114     return [cydget objectForKey:@"CYConfiguration"] ?: [cydget objectForKey:@"Configuration"];
115 }
116
117 + (NSDictionary *) currentPath {
118     NSDictionary *cydget([cydgets_ objectAtIndex:active_]);
119     return [cydget objectForKey:@"CYPath"];
120 }
121
122 @end
123
124 @interface SBUserAgent : NSObject
125 + (SBUserAgent *) sharedUserAgent;
126 - (void) enableLockScreenBundleNamed:(NSString *)bundle activationContext:(id)context;
127 - (void) disableLockScreenBundleNamed:(NSString *)bundle deactivationContext:(id)context;
128 @end
129
130 @interface UIPeripheralHost : NSObject
131 + (UIPeripheralHost *) sharedInstance;
132 + (void) _releaseSharedInstance;
133 @end
134
135 MSClassHook(UIPeripheralHost)
136
137 @interface UITextEffectsWindow : UIWindow
138 + (UIWindow *) sharedTextEffectsWindow;
139 @end
140
141 @implementation NSDictionary (CydgetLoader)
142
143 - (NSString *) cydget {
144     return [self objectForKey:@"CYPlugin"] ?: [self objectForKey:@"Plugin"];
145 }
146
147 - (void) enableCydget {
148     if (NSString *plugin = [self cydget]) {
149         ++online_;
150
151         if (kCFCoreFoundationVersionNumber < 600)
152             UIKeyboardEnableAutomaticAppearance();
153
154         [[UITextEffectsWindow sharedTextEffectsWindow] setWindowLevel:1000];
155
156         if (kCFCoreFoundationVersionNumber < 800)
157             [[$SBAwayController sharedAwayController] enableLockScreenBundleWithName:plugin];
158         else
159             [[$SBUserAgent sharedUserAgent] enableLockScreenBundleNamed:plugin activationContext:nil];
160     }
161 }
162
163 - (void) disableCydget {
164     if (NSString *plugin = [self cydget]) {
165         if (kCFCoreFoundationVersionNumber < 800)
166             [[$SBAwayController sharedAwayController] disableLockScreenBundleWithName:plugin];
167         else
168             [[$SBUserAgent sharedUserAgent] disableLockScreenBundleNamed:plugin deactivationContext:nil];
169
170         [$UIPeripheralHost _releaseSharedInstance];
171
172         if (kCFCoreFoundationVersionNumber < 600)
173             UIKeyboardDisableAutomaticAppearance();
174
175         --online_;
176     }
177 }
178
179 @end
180
181 // avoid rendering a keyboard onto the default SBAwayView while automatic keyboard is online
182 MSInstanceMessageHook0(UIView *, SBAwayView, inputView) {
183     // XXX: there is a conceptual error here
184     if (online_ == 0 && false || kCFCoreFoundationVersionNumber > 600)
185         return MSOldCall();
186
187     return [[[UIView alloc] init] autorelease];
188 }
189
190 // by default, keyboard actions are redirected to SBAwayController and press menu button
191 MSInstanceMessageHook1(void, SpringBoard, handleKeyEvent, GSEventRef, event) {
192     if (online_ == 0 || kCFCoreFoundationVersionNumber > 600)
193         return MSOldCall(event);
194
195     return MSSuperCall(event);
196 }
197
198 bool media_;
199
200 MSInstanceMessageHook0(void, SBLockScreenViewController, _toggleMediaControls) {
201     if (!media_)
202         MSOldCall();
203 }
204
205 MSInstanceMessageHook0(BOOL, SBLockScreenViewController, handleMenuButtonDoubleTap) {
206     menu_ = false;
207     BOOL value(MSOldCall());
208     if (kCFCoreFoundationVersionNumber >= 800)
209         [self _toggleMediaControls];
210     return value;
211 }
212
213 MSInstanceMessageHook0(BOOL, SBLockScreenManager, handleMenuButtonTap) {
214     media_ = true;
215     BOOL value(MSOldCall());
216     media_ = false;
217
218     if (!value && menu_) {
219         if (active_ != _not(size_t))
220             [[cydgets_ objectAtIndex:active_] disableCydget];
221         active_ = (active_ + 1) % [cydgets_ count];
222         [[cydgets_ objectAtIndex:active_] enableCydget];
223         // XXX: or siri doesn't disappear correctly
224         return kCFCoreFoundationVersionNumber >= 800 ? value : YES;
225     }
226
227     return value;
228 }
229
230 void Activate_() {
231     menu_ = false;
232     if (NowPlaying()) {
233         for (active_ = 0; active_ != [cydgets_ count]; ++active_)
234             if ([[[cydgets_ objectAtIndex:active_] objectForKey:@"CYName"] isEqualToString:@"AwayView"])
235                 break;
236         if (active_ == [cydgets_ count])
237             active_ = _not(size_t);
238     }
239     if (active_ != _not(size_t))
240         [[cydgets_ objectAtIndex:active_] enableCydget];
241 }
242
243 static void Undim_(SBAwayController *self) {
244     if ([self isDimmed]) {
245         Activate_();
246         [[[self awayView] window] makeKeyWindow];
247     }
248 }
249
250 MSInstanceMessageHook1(void, SBLockScreenViewController, startLockScreenFadeInAnimationForSource, int, source) {
251     Activate_();
252     MSOldCall(source);
253 }
254
255 MSInstanceMessageHook0(void, SBAwayController, undimScreen) {
256     Undim_(self);
257     MSOldCall();
258 }
259
260 MSInstanceMessageHook1(void, SBAwayController, undimScreen, BOOL, undim) {
261     Undim_(self);
262     MSOldCall(undim);
263 }
264
265 static void Deactivate_() {
266     if (active_ != _not(size_t))
267         [[cydgets_ objectAtIndex:active_] disableCydget];
268     active_ = 0;
269 }
270
271 MSHook(void, BKSDisplayServicesSetScreenBlanked, BOOL blanked) {
272     if (blanked)
273         Deactivate_();
274     _BKSDisplayServicesSetScreenBlanked(blanked);
275 }
276
277 MSInitialize {
278     MSHookFunction("_BKSDisplayServicesSetScreenBlanked", MSHake(BKSDisplayServicesSetScreenBlanked));
279 }
280
281 MSInstanceMessageHook1(void, SBUserAgent, dimScreen, BOOL, dim) {
282     Deactivate_();
283     MSOldCall(dim);
284 }
285
286 MSInstanceMessageHook0(void, SpringBoard, _menuButtonWasHeld) {
287     menu_ = false;
288     MSOldCall();
289 }
290
291 MSInstanceMessageHook1(void, SpringBoard, _menuButtonDown, GSEventRef, event) {
292     menu_ = true;
293     MSOldCall(event);
294 }
295
296 MSInstanceMessageHook1(void, SpringBoard, menuButtonDown, GSEventRef, event) {
297     menu_ = true;
298     MSOldCall(event);
299 }
300
301 MSInstanceMessageHook1(void, SBAwayView, addGestureRecognizer, id, recognizer) {
302     if (online_ == 0 || kCFCoreFoundationVersionNumber > 600)
303         return MSOldCall(recognizer);
304 }
305
306 MSInstanceMessageHook1(void, SBAwayWindow, sendGSEvent, GSEventRef, event) {
307     if (online_ == 0 || kCFCoreFoundationVersionNumber > 600)
308         return MSOldCall(event);
309
310     return MSSuperCall(event);
311 }
312
313 MSInstanceMessageHook2(void, SBLockScreenView, setMediaControlsHidden,forRequester, BOOL, hidden, id, requester) {
314     MSOldCall(hidden, requester);
315     UIView *&_mediaControlsContainerView(MSHookIvar<UIView *>(self, "_mediaControlsContainerView"));
316     UIView *&_mediaControlsView(MSHookIvar<UIView *>(self, "_mediaControlsView"));
317     if (&_mediaControlsContainerView != NULL && &_mediaControlsView != NULL)
318         [_mediaControlsContainerView setUserInteractionEnabled:([_mediaControlsView alpha] != 0)];
319 }
320
321 #define Cydgets_ @"/System/Library/LockCydgets"
322
323 static void CydgetSetup() {
324     NSString *plist([NSString stringWithFormat:@"%@/Library/Preferences/com.saurik.Cydget.plist", NSHomeDirectory()]);
325     settings_ = [NSMutableDictionary dictionaryWithContentsOfFile:plist] ?: [NSMutableDictionary dictionary];
326
327     nowplaying_ = [[settings_ objectForKey:@"NowPlaying"] boolValue];
328
329     NSArray *cydgets([settings_ objectForKey:@"LockCydgets"] ?: [NSArray arrayWithObjects:[NSDictionary dictionaryWithObjectsAndKeys:
330         @"Welcome", @"Name", [NSNumber numberWithBool:YES], @"Active", nil
331     ], [NSDictionary dictionaryWithObjectsAndKeys:
332         @"AwayView", @"Name", [NSNumber numberWithBool:YES], @"Active", nil
333     ], nil]);
334
335     cydgets_ = [NSMutableArray arrayWithCapacity:4];
336     for (NSDictionary *cydget in cydgets)
337         if ([[cydget objectForKey:@"Active"] boolValue]) {
338             NSString *name([cydget objectForKey:@"Name"]);
339             NSString *path([NSString stringWithFormat:@"%@/%@.cydget/Info.plist", Cydgets_, name]);
340             if (NSMutableDictionary *info = [NSMutableDictionary dictionaryWithContentsOfFile:path]) {
341                 [info setObject:name forKey:@"CYName"];
342                 [info setObject:path forKey:@"CYPath"];
343                 [cydgets_ addObject:info];
344             }
345         }
346
347     if ([cydgets_ count] == 0)
348         cydgets_ = nil;
349 }
350
351 // XXX: this could happen while it is unlocked
352 MSInstanceMessageHook1(id, SBLockScreenView, initWithFrame, CGRect, frame) {
353     self = MSOldCall(frame);
354     CydgetSetup();
355     Activate_();
356     return self;
357 }
358
359 MSInstanceMessageHook1(void, SBAwayController, _finishedUnlockAttemptWithStatus, BOOL, status) {
360     if (status)
361         Deactivate_();
362     MSOldCall(status);
363 }
364
365 // this is called occasionally by -[SBAwayView updateInterface] and takes away our keyboard
366 MSInstanceMessageHook0(void, SBAwayView, _fixupFirstResponder) {
367     if (online_ == 0)
368         return MSOldCall();
369 }
370
371 MSInstanceMessageHook0(void, SBAwayView, updateInterface) {
372     MSOldCall();
373
374     NSDictionary *cydget([cydgets_ objectAtIndex:active_]);
375
376     NSString *background([cydget objectForKey:@"CYBackground"]);
377     if ([background isEqualToString:@"Wallpaper"]) {
378         MSIvarHook(UIView *, _backgroundView);
379         [_backgroundView setAlpha:1.0f];
380     }
381
382     if ([[cydget objectForKey:@"CYShowDateTime"] boolValue])
383         [self addDateView];
384 }