FLEXUtility.m 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. //
  2. // FLEXUtility.m
  3. // Flipboard
  4. //
  5. // Created by Ryan Olson on 4/18/14.
  6. // Copyright (c) 2020 FLEX Team. All rights reserved.
  7. //
  8. #import "FLEXColor.h"
  9. #import "FLEXUtility.h"
  10. #import "FLEXResources.h"
  11. #import "FLEXWindow.h"
  12. #import <ImageIO/ImageIO.h>
  13. #import <objc/runtime.h>
  14. #import <zlib.h>
  15. BOOL FLEXConstructorsShouldRun() {
  16. static BOOL _FLEXConstructorsShouldRun_storage = YES;
  17. static dispatch_once_t onceToken;
  18. dispatch_once(&onceToken, ^{
  19. NSString *key = @"FLEX_SKIP_INIT";
  20. if (getenv(key.UTF8String) || [NSUserDefaults.standardUserDefaults boolForKey:key]) {
  21. _FLEXConstructorsShouldRun_storage = NO;
  22. }
  23. });
  24. return _FLEXConstructorsShouldRun_storage;
  25. }
  26. @implementation FLEXUtility
  27. + (UIWindow *)appKeyWindow {
  28. // First, check UIApplication.keyWindow
  29. FLEXWindow *window = (id)UIApplication.sharedApplication.keyWindow;
  30. if (window) {
  31. if ([window isKindOfClass:[FLEXWindow class]]) {
  32. return window.previousKeyWindow;
  33. }
  34. return window;
  35. }
  36. // As of iOS 13, UIApplication.keyWindow does not return nil,
  37. // so this is more of a safeguard against it returning nil in the future.
  38. //
  39. // Also, these are obviously not all FLEXWindows; FLEXWindow is used
  40. // so we can call window.previousKeyWindow without an ugly cast
  41. for (FLEXWindow *window in UIApplication.sharedApplication.windows) {
  42. if (window.isKeyWindow) {
  43. if ([window isKindOfClass:[FLEXWindow class]]) {
  44. return window.previousKeyWindow;
  45. }
  46. return window;
  47. }
  48. }
  49. return nil;
  50. }
  51. #if FLEX_AT_LEAST_IOS13_SDK
  52. + (UIWindowScene *)activeScene {
  53. for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
  54. // Look for an active UIWindowScene
  55. if (scene.activationState == UISceneActivationStateForegroundActive &&
  56. [scene isKindOfClass:[UIWindowScene class]]) {
  57. return (UIWindowScene *)scene;
  58. }
  59. }
  60. return nil;
  61. }
  62. #endif
  63. + (UIViewController *)topViewControllerInWindow:(UIWindow *)window {
  64. UIViewController *topViewController = window.rootViewController;
  65. while (topViewController.presentedViewController) {
  66. topViewController = topViewController.presentedViewController;
  67. }
  68. return topViewController;
  69. }
  70. + (UIColor *)consistentRandomColorForObject:(id)object {
  71. CGFloat hue = (((NSUInteger)object >> 4) % 256) / 255.0;
  72. return [UIColor colorWithHue:hue saturation:1.0 brightness:1.0 alpha:1.0];
  73. }
  74. + (NSString *)descriptionForView:(UIView *)view includingFrame:(BOOL)includeFrame {
  75. NSString *description = [[view class] description];
  76. NSString *viewControllerDescription = [[[self viewControllerForView:view] class] description];
  77. if (viewControllerDescription.length > 0) {
  78. description = [description stringByAppendingFormat:@" (%@)", viewControllerDescription];
  79. }
  80. if (includeFrame) {
  81. description = [description stringByAppendingFormat:@" %@", [self stringForCGRect:view.frame]];
  82. }
  83. if (view.accessibilityLabel.length > 0) {
  84. description = [description stringByAppendingFormat:@" · %@", view.accessibilityLabel];
  85. }
  86. return description;
  87. }
  88. + (NSString *)stringForCGRect:(CGRect)rect {
  89. return [NSString stringWithFormat:@"{(%g, %g), (%g, %g)}",
  90. rect.origin.x, rect.origin.y, rect.size.width, rect.size.height
  91. ];
  92. }
  93. + (UIViewController *)viewControllerForView:(UIView *)view {
  94. NSString *viewDelegate = @"_viewDelegate";
  95. if ([view respondsToSelector:NSSelectorFromString(viewDelegate)]) {
  96. return [view valueForKey:viewDelegate];
  97. }
  98. return nil;
  99. }
  100. + (UIViewController *)viewControllerForAncestralView:(UIView *)view {
  101. NSString *_viewControllerForAncestor = @"_viewControllerForAncestor";
  102. if ([view respondsToSelector:NSSelectorFromString(_viewControllerForAncestor)]) {
  103. return [view valueForKey:_viewControllerForAncestor];
  104. }
  105. return nil;
  106. }
  107. + (UIImage *)previewImageForView:(UIView *)view {
  108. if (CGRectIsEmpty(view.bounds)) {
  109. return nil;
  110. }
  111. CGSize viewSize = view.bounds.size;
  112. UIGraphicsBeginImageContextWithOptions(viewSize, NO, 0.0);
  113. [view drawViewHierarchyInRect:CGRectMake(0, 0, viewSize.width, viewSize.height) afterScreenUpdates:YES];
  114. UIImage *previewImage = UIGraphicsGetImageFromCurrentImageContext();
  115. UIGraphicsEndImageContext();
  116. return previewImage;
  117. }
  118. + (UIImage *)previewImageForLayer:(CALayer *)layer {
  119. if (CGRectIsEmpty(layer.bounds)) {
  120. return nil;
  121. }
  122. UIGraphicsBeginImageContextWithOptions(layer.bounds.size, NO, 0.0);
  123. CGContextRef imageContext = UIGraphicsGetCurrentContext();
  124. [layer renderInContext:imageContext];
  125. UIImage *previewImage = UIGraphicsGetImageFromCurrentImageContext();
  126. UIGraphicsEndImageContext();
  127. return previewImage;
  128. }
  129. + (NSString *)detailDescriptionForView:(UIView *)view {
  130. return [NSString stringWithFormat:@"frame %@", [self stringForCGRect:view.frame]];
  131. }
  132. + (UIImage *)circularImageWithColor:(UIColor *)color radius:(CGFloat)radius {
  133. CGFloat diameter = radius * 2.0;
  134. UIGraphicsBeginImageContextWithOptions(CGSizeMake(diameter, diameter), NO, 0.0);
  135. CGContextRef imageContext = UIGraphicsGetCurrentContext();
  136. CGContextSetFillColorWithColor(imageContext, color.CGColor);
  137. CGContextFillEllipseInRect(imageContext, CGRectMake(0, 0, diameter, diameter));
  138. UIImage *circularImage = UIGraphicsGetImageFromCurrentImageContext();
  139. UIGraphicsEndImageContext();
  140. return circularImage;
  141. }
  142. + (UIColor *)hierarchyIndentPatternColor {
  143. static UIColor *patternColor = nil;
  144. static dispatch_once_t onceToken;
  145. dispatch_once(&onceToken, ^{
  146. UIImage *indentationPatternImage = FLEXResources.hierarchyIndentPattern;
  147. patternColor = [UIColor colorWithPatternImage:indentationPatternImage];
  148. #if FLEX_AT_LEAST_IOS13_SDK
  149. if (@available(iOS 13.0, *)) {
  150. // Create a dark mode version
  151. UIGraphicsBeginImageContextWithOptions(
  152. indentationPatternImage.size, NO, indentationPatternImage.scale
  153. );
  154. [FLEXColor.iconColor set];
  155. [indentationPatternImage drawInRect:CGRectMake(
  156. 0, 0, indentationPatternImage.size.width, indentationPatternImage.size.height
  157. )];
  158. UIImage *darkModePatternImage = UIGraphicsGetImageFromCurrentImageContext();
  159. UIGraphicsEndImageContext();
  160. // Create dynamic color provider
  161. patternColor = [UIColor colorWithDynamicProvider:^UIColor *(UITraitCollection *traitCollection) {
  162. return (traitCollection.userInterfaceStyle == UIUserInterfaceStyleLight
  163. ? [UIColor colorWithPatternImage:indentationPatternImage]
  164. : [UIColor colorWithPatternImage:darkModePatternImage]);
  165. }];
  166. }
  167. #endif
  168. });
  169. return patternColor;
  170. }
  171. + (NSString *)applicationImageName {
  172. return NSBundle.mainBundle.executablePath;
  173. }
  174. + (NSString *)applicationName {
  175. return FLEXUtility.applicationImageName.lastPathComponent;
  176. }
  177. + (NSString *)pointerToString:(void *)ptr {
  178. return [NSString stringWithFormat:@"%p", ptr];
  179. }
  180. + (NSString *)addressOfObject:(id)object {
  181. return [NSString stringWithFormat:@"%p", object];
  182. }
  183. + (NSString *)stringByEscapingHTMLEntitiesInString:(NSString *)originalString {
  184. static NSDictionary<NSString *, NSString *> *escapingDictionary = nil;
  185. static NSRegularExpression *regex = nil;
  186. static dispatch_once_t onceToken;
  187. dispatch_once(&onceToken, ^{
  188. escapingDictionary = @{ @" " : @"&nbsp;",
  189. @">" : @"&gt;",
  190. @"<" : @"&lt;",
  191. @"&" : @"&amp;",
  192. @"'" : @"&apos;",
  193. @"\"" : @"&quot;",
  194. @"«" : @"&laquo;",
  195. @"»" : @"&raquo;"
  196. };
  197. regex = [NSRegularExpression regularExpressionWithPattern:@"(&|>|<|'|\"|«|»)" options:0 error:NULL];
  198. });
  199. NSMutableString *mutableString = originalString.mutableCopy;
  200. NSArray<NSTextCheckingResult *> *matches = [regex
  201. matchesInString:mutableString options:0 range:NSMakeRange(0, mutableString.length)
  202. ];
  203. for (NSTextCheckingResult *result in matches.reverseObjectEnumerator) {
  204. NSString *foundString = [mutableString substringWithRange:result.range];
  205. NSString *replacementString = escapingDictionary[foundString];
  206. if (replacementString) {
  207. [mutableString replaceCharactersInRange:result.range withString:replacementString];
  208. }
  209. }
  210. return [mutableString copy];
  211. }
  212. + (UIInterfaceOrientationMask)infoPlistSupportedInterfaceOrientationsMask {
  213. NSArray<NSString *> *supportedOrientations = NSBundle.mainBundle.infoDictionary[@"UISupportedInterfaceOrientations"];
  214. UIInterfaceOrientationMask supportedOrientationsMask = 0;
  215. if ([supportedOrientations containsObject:@"UIInterfaceOrientationPortrait"]) {
  216. supportedOrientationsMask |= UIInterfaceOrientationMaskPortrait;
  217. }
  218. if ([supportedOrientations containsObject:@"UIInterfaceOrientationMaskLandscapeRight"]) {
  219. supportedOrientationsMask |= UIInterfaceOrientationMaskLandscapeRight;
  220. }
  221. if ([supportedOrientations containsObject:@"UIInterfaceOrientationMaskPortraitUpsideDown"]) {
  222. supportedOrientationsMask |= UIInterfaceOrientationMaskPortraitUpsideDown;
  223. }
  224. if ([supportedOrientations containsObject:@"UIInterfaceOrientationLandscapeLeft"]) {
  225. supportedOrientationsMask |= UIInterfaceOrientationMaskLandscapeLeft;
  226. }
  227. return supportedOrientationsMask;
  228. }
  229. + (UIImage *)thumbnailedImageWithMaxPixelDimension:(NSInteger)dimension fromImageData:(NSData *)data {
  230. UIImage *thumbnail = nil;
  231. CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)data, 0);
  232. if (imageSource) {
  233. NSDictionary<NSString *, id> *options = @{
  234. (__bridge id)kCGImageSourceCreateThumbnailWithTransform : @YES,
  235. (__bridge id)kCGImageSourceCreateThumbnailFromImageAlways : @YES,
  236. (__bridge id)kCGImageSourceThumbnailMaxPixelSize : @(dimension)
  237. };
  238. CGImageRef scaledImageRef = CGImageSourceCreateThumbnailAtIndex(
  239. imageSource, 0, (__bridge CFDictionaryRef)options
  240. );
  241. if (scaledImageRef) {
  242. thumbnail = [UIImage imageWithCGImage:scaledImageRef];
  243. CFRelease(scaledImageRef);
  244. }
  245. CFRelease(imageSource);
  246. }
  247. return thumbnail;
  248. }
  249. + (NSString *)stringFromRequestDuration:(NSTimeInterval)duration {
  250. NSString *string = @"0s";
  251. if (duration > 0.0) {
  252. if (duration < 1.0) {
  253. string = [NSString stringWithFormat:@"%dms", (int)(duration * 1000)];
  254. } else if (duration < 10.0) {
  255. string = [NSString stringWithFormat:@"%.2fs", duration];
  256. } else {
  257. string = [NSString stringWithFormat:@"%.1fs", duration];
  258. }
  259. }
  260. return string;
  261. }
  262. + (NSString *)statusCodeStringFromURLResponse:(NSURLResponse *)response {
  263. NSString *httpResponseString = nil;
  264. if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
  265. NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
  266. NSString *statusCodeDescription = nil;
  267. if (httpResponse.statusCode == 200) {
  268. // Prefer OK to the default "no error"
  269. statusCodeDescription = @"OK";
  270. } else {
  271. statusCodeDescription = [NSHTTPURLResponse localizedStringForStatusCode:httpResponse.statusCode];
  272. }
  273. httpResponseString = [NSString stringWithFormat:@"%ld %@", (long)httpResponse.statusCode, statusCodeDescription];
  274. }
  275. return httpResponseString;
  276. }
  277. + (BOOL)isErrorStatusCodeFromURLResponse:(NSURLResponse *)response {
  278. if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
  279. NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
  280. return httpResponse.statusCode >= 400;
  281. }
  282. return NO;
  283. }
  284. + (NSArray<NSURLQueryItem *> *)itemsFromQueryString:(NSString *)query {
  285. NSMutableArray<NSURLQueryItem *> *items = [NSMutableArray new];
  286. // [a=1, b=2, c=3]
  287. NSArray<NSString *> *queryComponents = [query componentsSeparatedByString:@"&"];
  288. for (NSString *keyValueString in queryComponents) {
  289. // [a, 1]
  290. NSArray<NSString *> *components = [keyValueString componentsSeparatedByString:@"="];
  291. if (components.count == 2) {
  292. NSString *key = components.firstObject.stringByRemovingPercentEncoding;
  293. NSString *value = components.lastObject.stringByRemovingPercentEncoding;
  294. [items addObject:[NSURLQueryItem queryItemWithName:key value:value]];
  295. }
  296. }
  297. return items.copy;
  298. }
  299. + (NSString *)prettyJSONStringFromData:(NSData *)data {
  300. NSString *prettyString = nil;
  301. id jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];
  302. if ([NSJSONSerialization isValidJSONObject:jsonObject]) {
  303. prettyString = [NSString stringWithCString:[NSJSONSerialization
  304. dataWithJSONObject:jsonObject options:NSJSONWritingPrettyPrinted error:NULL
  305. ].bytes encoding:NSUTF8StringEncoding];
  306. // NSJSONSerialization escapes forward slashes.
  307. // We want pretty json, so run through and unescape the slashes.
  308. prettyString = [prettyString stringByReplacingOccurrencesOfString:@"\\/" withString:@"/"];
  309. } else {
  310. prettyString = [NSString stringWithCString:data.bytes encoding:NSUTF8StringEncoding];
  311. }
  312. return prettyString;
  313. }
  314. + (BOOL)isValidJSONData:(NSData *)data {
  315. return [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL] ? YES : NO;
  316. }
  317. // Thanks to the following links for help with this method
  318. // https://www.cocoanetics.com/2012/02/decompressing-files-into-memory/
  319. // https://github.com/nicklockwood/GZIP
  320. + (NSData *)inflatedDataFromCompressedData:(NSData *)compressedData {
  321. NSData *inflatedData = nil;
  322. NSUInteger compressedDataLength = compressedData.length;
  323. if (compressedDataLength > 0) {
  324. z_stream stream;
  325. stream.zalloc = Z_NULL;
  326. stream.zfree = Z_NULL;
  327. stream.avail_in = (uInt)compressedDataLength;
  328. stream.next_in = (void *)compressedData.bytes;
  329. stream.total_out = 0;
  330. stream.avail_out = 0;
  331. NSMutableData *mutableData = [NSMutableData dataWithLength:compressedDataLength * 1.5];
  332. if (inflateInit2(&stream, 15 + 32) == Z_OK) {
  333. int status = Z_OK;
  334. while (status == Z_OK) {
  335. if (stream.total_out >= mutableData.length) {
  336. mutableData.length += compressedDataLength / 2;
  337. }
  338. stream.next_out = (uint8_t *)[mutableData mutableBytes] + stream.total_out;
  339. stream.avail_out = (uInt)(mutableData.length - stream.total_out);
  340. status = inflate(&stream, Z_SYNC_FLUSH);
  341. }
  342. if (inflateEnd(&stream) == Z_OK) {
  343. if (status == Z_STREAM_END) {
  344. mutableData.length = stream.total_out;
  345. inflatedData = [mutableData copy];
  346. }
  347. }
  348. }
  349. }
  350. return inflatedData;
  351. }
  352. + (NSArray<UIWindow *> *)allWindows {
  353. BOOL includeInternalWindows = YES;
  354. BOOL onlyVisibleWindows = NO;
  355. // Obfuscating selector allWindowsIncludingInternalWindows:onlyVisibleWindows:
  356. NSArray<NSString *> *allWindowsComponents = @[
  357. @"al", @"lWindo", @"wsIncl", @"udingInt", @"ernalWin", @"dows:o", @"nlyVisi", @"bleWin", @"dows:"
  358. ];
  359. SEL allWindowsSelector = NSSelectorFromString([allWindowsComponents componentsJoinedByString:@""]);
  360. NSMethodSignature *methodSignature = [[UIWindow class] methodSignatureForSelector:allWindowsSelector];
  361. NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
  362. invocation.target = [UIWindow class];
  363. invocation.selector = allWindowsSelector;
  364. [invocation setArgument:&includeInternalWindows atIndex:2];
  365. [invocation setArgument:&onlyVisibleWindows atIndex:3];
  366. [invocation invoke];
  367. __unsafe_unretained NSArray<UIWindow *> *windows = nil;
  368. [invocation getReturnValue:&windows];
  369. return windows;
  370. }
  371. + (UIAlertController *)alert:(NSString *)title message:(NSString *)message {
  372. return [UIAlertController
  373. alertControllerWithTitle:title
  374. message:message
  375. preferredStyle:UIAlertControllerStyleAlert
  376. ];
  377. }
  378. + (SEL)swizzledSelectorForSelector:(SEL)selector {
  379. return NSSelectorFromString([NSString stringWithFormat:
  380. @"_flex_swizzle_%x_%@", arc4random(), NSStringFromSelector(selector)
  381. ]);
  382. }
  383. + (BOOL)instanceRespondsButDoesNotImplementSelector:(SEL)selector class:(Class)cls {
  384. if ([cls instancesRespondToSelector:selector]) {
  385. unsigned int numMethods = 0;
  386. Method *methods = class_copyMethodList(cls, &numMethods);
  387. BOOL implementsSelector = NO;
  388. for (int index = 0; index < numMethods; index++) {
  389. SEL methodSelector = method_getName(methods[index]);
  390. if (selector == methodSelector) {
  391. implementsSelector = YES;
  392. break;
  393. }
  394. }
  395. free(methods);
  396. if (!implementsSelector) {
  397. return YES;
  398. }
  399. }
  400. return NO;
  401. }
  402. + (void)replaceImplementationOfKnownSelector:(SEL)originalSelector
  403. onClass:(Class)class
  404. withBlock:(id)block
  405. swizzledSelector:(SEL)swizzledSelector {
  406. // This method is only intended for swizzling methods that are know to exist on the class.
  407. // Bail if that isn't the case.
  408. Method originalMethod = class_getInstanceMethod(class, originalSelector);
  409. if (!originalMethod) {
  410. return;
  411. }
  412. IMP implementation = imp_implementationWithBlock(block);
  413. class_addMethod(class, swizzledSelector, implementation, method_getTypeEncoding(originalMethod));
  414. Method newMethod = class_getInstanceMethod(class, swizzledSelector);
  415. method_exchangeImplementations(originalMethod, newMethod);
  416. }
  417. + (void)replaceImplementationOfSelector:(SEL)selector
  418. withSelector:(SEL)swizzledSelector
  419. forClass:(Class)cls
  420. withMethodDescription:(struct objc_method_description)methodDescription
  421. implementationBlock:(id)implementationBlock undefinedBlock:(id)undefinedBlock {
  422. if ([self instanceRespondsButDoesNotImplementSelector:selector class:cls]) {
  423. return;
  424. }
  425. IMP implementation = imp_implementationWithBlock((id)(
  426. [cls instancesRespondToSelector:selector] ? implementationBlock : undefinedBlock)
  427. );
  428. Method oldMethod = class_getInstanceMethod(cls, selector);
  429. const char *types = methodDescription.types;
  430. if (oldMethod) {
  431. if (!types) {
  432. types = method_getTypeEncoding(oldMethod);
  433. }
  434. class_addMethod(cls, swizzledSelector, implementation, types);
  435. Method newMethod = class_getInstanceMethod(cls, swizzledSelector);
  436. method_exchangeImplementations(oldMethod, newMethod);
  437. } else {
  438. if (!types) {
  439. // Some protocol method descriptions don't have .types populated
  440. // Set the return type to void and ignore arguments
  441. types = "v@:";
  442. }
  443. class_addMethod(cls, selector, implementation, types);
  444. }
  445. }
  446. @end