FLEXUtility.m 20 KB

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