CDVThemeableBrowser.m 67 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661
  1. /*
  2. Licensed to the Apache Software Foundation (ASF) under one
  3. or more contributor license agreements. See the NOTICE file
  4. distributed with this work for additional information
  5. regarding copyright ownership. The ASF licenses this file
  6. to you under the Apache License, Version 2.0 (the
  7. "License"); you may not use this file except in compliance
  8. with the License. You may obtain a copy of the License at
  9. http://www.apache.org/licenses/LICENSE-2.0
  10. Unless required by applicable law or agreed to in writing,
  11. software distributed under the License is distributed on an
  12. "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  13. KIND, either express or implied. See the License for the
  14. specific language governing permissions and limitations
  15. under the License.
  16. */
  17. #import "CDVThemeableBrowser.h"
  18. #import <Cordova/CDVPluginResult.h>
  19. #import <Cordova/CDVUserAgentUtil.h>
  20. #define kThemeableBrowserTargetSelf @"_self"
  21. #define kThemeableBrowserTargetSystem @"_system"
  22. #define kThemeableBrowserTargetBlank @"_blank"
  23. #define kThemeableBrowserToolbarBarPositionBottom @"bottom"
  24. #define kThemeableBrowserToolbarBarPositionTop @"top"
  25. #define kThemeableBrowserAlignLeft @"left"
  26. #define kThemeableBrowserAlignRight @"right"
  27. #define kThemeableBrowserPropEvent @"event"
  28. #define kThemeableBrowserPropLabel @"label"
  29. #define kThemeableBrowserPropColor @"color"
  30. #define kThemeableBrowserPropHeight @"height"
  31. #define kThemeableBrowserPropImage @"image"
  32. #define kThemeableBrowserPropWwwImage @"wwwImage"
  33. #define kThemeableBrowserPropImagePressed @"imagePressed"
  34. #define kThemeableBrowserPropWwwImagePressed @"wwwImagePressed"
  35. #define kThemeableBrowserPropWwwImageDensity @"wwwImageDensity"
  36. #define kThemeableBrowserPropStaticText @"staticText"
  37. #define kThemeableBrowserPropShowPageTitle @"showPageTitle"
  38. #define kThemeableBrowserPropAlign @"align"
  39. #define kThemeableBrowserPropTitle @"title"
  40. #define kThemeableBrowserPropCancel @"cancel"
  41. #define kThemeableBrowserPropItems @"items"
  42. #define kThemeableBrowserEmitError @"ThemeableBrowserError"
  43. #define kThemeableBrowserEmitWarning @"ThemeableBrowserWarning"
  44. #define kThemeableBrowserEmitCodeCritical @"critical"
  45. #define kThemeableBrowserEmitCodeLoadFail @"loadfail"
  46. #define kThemeableBrowserEmitCodeUnexpected @"unexpected"
  47. #define kThemeableBrowserEmitCodeUndefined @"undefined"
  48. #define TOOLBAR_DEF_HEIGHT 44.0
  49. #define LOCATIONBAR_HEIGHT 21.0
  50. #define FOOTER_HEIGHT ((TOOLBAR_HEIGHT) + (LOCATIONBAR_HEIGHT))
  51. #pragma mark CDVThemeableBrowser
  52. @interface CDVThemeableBrowser () {
  53. BOOL _isShown;
  54. int _framesOpened; // number of frames opened since the last time browser exited
  55. NSURL *initUrl; // initial URL ThemeableBrowser opened with
  56. NSURL *originalUrl;
  57. }
  58. @end
  59. @implementation CDVThemeableBrowser
  60. #ifdef __CORDOVA_4_0_0
  61. - (void)pluginInitialize
  62. {
  63. _isShown = NO;
  64. _framesOpened = 0;
  65. _callbackIdPattern = nil;
  66. }
  67. #else
  68. - (CDVThemeableBrowser*)initWithWebView:(UIWebView*)theWebView
  69. {
  70. self = [super initWithWebView:theWebView];
  71. if (self != nil) {
  72. _isShown = NO;
  73. _framesOpened = 0;
  74. _callbackIdPattern = nil;
  75. }
  76. return self;
  77. }
  78. #endif
  79. - (void)onReset
  80. {
  81. [self close:nil];
  82. }
  83. - (void)close:(CDVInvokedUrlCommand*)command
  84. {
  85. if (self.themeableBrowserViewController == nil) {
  86. [self emitWarning:kThemeableBrowserEmitCodeUnexpected
  87. withMessage:@"Close called but already closed."];
  88. return;
  89. }
  90. // Things are cleaned up in browserExit.
  91. [self.themeableBrowserViewController close];
  92. }
  93. - (BOOL) isSystemUrl:(NSURL*)url
  94. {
  95. NSDictionary *systemUrls = @{
  96. @"itunes.apple.com": @YES,
  97. @"search.itunes.apple.com": @YES,
  98. @"appsto.re": @YES
  99. };
  100. if (systemUrls[[url host]]) {
  101. return YES;
  102. }
  103. return NO;
  104. }
  105. - (void)open:(CDVInvokedUrlCommand*)command
  106. {
  107. CDVPluginResult* pluginResult;
  108. NSString* url = [command argumentAtIndex:0];
  109. NSString* target = [command argumentAtIndex:1 withDefault:kThemeableBrowserTargetSelf];
  110. NSString* options = [command argumentAtIndex:2 withDefault:@"" andClass:[NSString class]];
  111. self.callbackId = command.callbackId;
  112. if (url != nil) {
  113. #ifdef __CORDOVA_4_0_0
  114. NSURL* baseUrl = [self.webViewEngine URL];
  115. #else
  116. NSURL* baseUrl = [self.webView.request URL];
  117. #endif
  118. NSURL* absoluteUrl = [[NSURL URLWithString:url relativeToURL:baseUrl] absoluteURL];
  119. initUrl = absoluteUrl;
  120. if ([self isSystemUrl:absoluteUrl]) {
  121. target = kThemeableBrowserTargetSystem;
  122. }
  123. if ([target isEqualToString:kThemeableBrowserTargetSelf]) {
  124. [self openInCordovaWebView:absoluteUrl withOptions:options];
  125. } else if ([target isEqualToString:kThemeableBrowserTargetSystem]) {
  126. [self openInSystem:absoluteUrl];
  127. } else { // _blank or anything else
  128. [self openInThemeableBrowser:absoluteUrl withOptions:options];
  129. }
  130. pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
  131. } else {
  132. pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"incorrect number of arguments"];
  133. }
  134. [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]];
  135. [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
  136. }
  137. - (void)reload:(CDVInvokedUrlCommand*)command
  138. {
  139. if (self.themeableBrowserViewController) {
  140. [self.themeableBrowserViewController reload];
  141. }
  142. }
  143. - (CDVThemeableBrowserOptions*)parseOptions:(NSString*)options
  144. {
  145. CDVThemeableBrowserOptions* obj = [[CDVThemeableBrowserOptions alloc] init];
  146. if (options && [options length] > 0) {
  147. // Min support, iOS 5. We will use the JSON parser that comes with iOS
  148. // 5.
  149. NSError *error = nil;
  150. NSData *data = [options dataUsingEncoding:NSUTF8StringEncoding];
  151. id jsonObj = [NSJSONSerialization
  152. JSONObjectWithData:data
  153. options:0
  154. error:&error];
  155. if(error) {
  156. [self emitError:kThemeableBrowserEmitCodeCritical
  157. withMessage:[NSString stringWithFormat:@"Invalid JSON %@", error]];
  158. } else if([jsonObj isKindOfClass:[NSDictionary class]]) {
  159. NSDictionary *dict = jsonObj;
  160. for (NSString *key in dict) {
  161. if ([obj respondsToSelector:NSSelectorFromString(key)]) {
  162. [obj setValue:dict[key] forKey:key];
  163. }
  164. }
  165. }
  166. } else {
  167. [self emitWarning:kThemeableBrowserEmitCodeUndefined
  168. withMessage:@"No config was given, defaults will be used, which is quite boring."];
  169. }
  170. return obj;
  171. }
  172. - (void)openInThemeableBrowser:(NSURL*)url withOptions:(NSString*)options
  173. {
  174. CDVThemeableBrowserOptions* browserOptions = [self parseOptions:options];
  175. // Among all the options, there are a few that ThemedBrowser would like to
  176. // disable, since ThemedBrowser's purpose is to provide an integrated look
  177. // and feel that is consistent across platforms. We'd do this hack to
  178. // minimize changes from the original ThemeableBrowser so when merge from the
  179. // ThemeableBrowser is needed, it wouldn't be super pain in the ass.
  180. browserOptions.toolbarposition = kThemeableBrowserToolbarBarPositionTop;
  181. if (browserOptions.clearcache) {
  182. NSHTTPCookie *cookie;
  183. NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
  184. for (cookie in [storage cookies])
  185. {
  186. if (![cookie.domain isEqual: @".^filecookies^"]) {
  187. [storage deleteCookie:cookie];
  188. }
  189. }
  190. }
  191. if (browserOptions.clearsessioncache) {
  192. NSHTTPCookie *cookie;
  193. NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
  194. for (cookie in [storage cookies])
  195. {
  196. if (![cookie.domain isEqual: @".^filecookies^"] && cookie.isSessionOnly) {
  197. [storage deleteCookie:cookie];
  198. }
  199. }
  200. }
  201. if (self.themeableBrowserViewController == nil) {
  202. NSString* originalUA = [CDVUserAgentUtil originalUserAgent];
  203. self.themeableBrowserViewController = [[CDVThemeableBrowserViewController alloc]
  204. initWithUserAgent:originalUA prevUserAgent:[self.commandDelegate userAgent]
  205. browserOptions: browserOptions
  206. navigationDelete:self
  207. statusBarStyle:[UIApplication sharedApplication].statusBarStyle];
  208. if ([self.viewController conformsToProtocol:@protocol(CDVScreenOrientationDelegate)]) {
  209. self.themeableBrowserViewController.orientationDelegate = (UIViewController <CDVScreenOrientationDelegate>*)self.viewController;
  210. }
  211. }
  212. [self.themeableBrowserViewController showLocationBar:browserOptions.location];
  213. [self.themeableBrowserViewController showToolBar:YES:browserOptions.toolbarposition];
  214. if (browserOptions.closebuttoncaption != nil) {
  215. // [self.themeableBrowserViewController setCloseButtonTitle:browserOptions.closebuttoncaption];
  216. }
  217. // Set Presentation Style
  218. UIModalPresentationStyle presentationStyle = UIModalPresentationFullScreen; // default
  219. if (browserOptions.presentationstyle != nil) {
  220. if ([[browserOptions.presentationstyle lowercaseString] isEqualToString:@"pagesheet"]) {
  221. presentationStyle = UIModalPresentationPageSheet;
  222. } else if ([[browserOptions.presentationstyle lowercaseString] isEqualToString:@"formsheet"]) {
  223. presentationStyle = UIModalPresentationFormSheet;
  224. }
  225. }
  226. self.themeableBrowserViewController.modalPresentationStyle = presentationStyle;
  227. // Set Transition Style
  228. UIModalTransitionStyle transitionStyle = UIModalTransitionStyleCoverVertical; // default
  229. if (browserOptions.transitionstyle != nil) {
  230. if ([[browserOptions.transitionstyle lowercaseString] isEqualToString:@"fliphorizontal"]) {
  231. transitionStyle = UIModalTransitionStyleFlipHorizontal;
  232. } else if ([[browserOptions.transitionstyle lowercaseString] isEqualToString:@"crossdissolve"]) {
  233. transitionStyle = UIModalTransitionStyleCrossDissolve;
  234. }
  235. }
  236. self.themeableBrowserViewController.modalTransitionStyle = transitionStyle;
  237. // prevent webView from bouncing
  238. if (browserOptions.disallowoverscroll) {
  239. if ([self.themeableBrowserViewController.webView respondsToSelector:@selector(scrollView)]) {
  240. ((UIScrollView*)[self.themeableBrowserViewController.webView scrollView]).bounces = NO;
  241. } else {
  242. for (id subview in self.themeableBrowserViewController.webView.subviews) {
  243. if ([[subview class] isSubclassOfClass:[UIScrollView class]]) {
  244. ((UIScrollView*)subview).bounces = NO;
  245. }
  246. }
  247. }
  248. }
  249. // UIWebView options
  250. self.themeableBrowserViewController.webView.scalesPageToFit = browserOptions.zoom;
  251. self.themeableBrowserViewController.webView.mediaPlaybackRequiresUserAction = browserOptions.mediaplaybackrequiresuseraction;
  252. self.themeableBrowserViewController.webView.allowsInlineMediaPlayback = browserOptions.allowinlinemediaplayback;
  253. if (IsAtLeastiOSVersion(@"6.0")) {
  254. self.themeableBrowserViewController.webView.keyboardDisplayRequiresUserAction = browserOptions.keyboarddisplayrequiresuseraction;
  255. self.themeableBrowserViewController.webView.suppressesIncrementalRendering = browserOptions.suppressesincrementalrendering;
  256. }
  257. [self.themeableBrowserViewController navigateTo:url];
  258. if (!browserOptions.hidden) {
  259. [self show:nil withAnimation:!browserOptions.disableAnimation];
  260. }
  261. }
  262. - (void)show:(CDVInvokedUrlCommand*)command
  263. {
  264. [self show:command withAnimation:YES];
  265. }
  266. - (void)show:(CDVInvokedUrlCommand*)command withAnimation:(BOOL)animated
  267. {
  268. if (self.themeableBrowserViewController == nil) {
  269. [self emitWarning:kThemeableBrowserEmitCodeUnexpected
  270. withMessage:@"Show called but already closed."];
  271. return;
  272. }
  273. if (_isShown) {
  274. [self emitWarning:kThemeableBrowserEmitCodeUnexpected
  275. withMessage:@"Show called but already shown"];
  276. return;
  277. }
  278. _isShown = YES;
  279. CDVThemeableBrowserNavigationController* nav = [[CDVThemeableBrowserNavigationController alloc]
  280. initWithRootViewController:self.themeableBrowserViewController];
  281. nav.orientationDelegate = self.themeableBrowserViewController;
  282. nav.navigationBarHidden = YES;
  283. // Run later to avoid the "took a long time" log message.
  284. dispatch_async(dispatch_get_main_queue(), ^{
  285. if (self.themeableBrowserViewController != nil) {
  286. [self.viewController presentViewController:nav animated:animated completion:nil];
  287. }
  288. });
  289. }
  290. - (void)openInCordovaWebView:(NSURL*)url withOptions:(NSString*)options
  291. {
  292. NSURLRequest* request = [NSURLRequest requestWithURL:url];
  293. #ifdef __CORDOVA_4_0_0
  294. // the webview engine itself will filter for this according to <allow-navigation> policy
  295. // in config.xml for cordova-ios-4.0
  296. [self.webViewEngine loadRequest:request];
  297. #else
  298. if ([self.commandDelegate URLIsWhitelisted:url]) {
  299. [self.webView loadRequest:request];
  300. } else { // this assumes the openInThemeableBrowser can be excepted from the white-list
  301. [self openInThemeableBrowser:url withOptions:options];
  302. }
  303. #endif
  304. }
  305. - (void)openInSystem:(NSURL*)url
  306. {
  307. if ([[UIApplication sharedApplication] canOpenURL:url]) {
  308. [[UIApplication sharedApplication] openURL:url];
  309. } else { // handle any custom schemes to plugins
  310. [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:CDVPluginHandleOpenURLNotification object:url]];
  311. }
  312. }
  313. // This is a helper method for the inject{Script|Style}{Code|File} API calls, which
  314. // provides a consistent method for injecting JavaScript code into the document.
  315. //
  316. // If a wrapper string is supplied, then the source string will be JSON-encoded (adding
  317. // quotes) and wrapped using string formatting. (The wrapper string should have a single
  318. // '%@' marker).
  319. //
  320. // If no wrapper is supplied, then the source string is executed directly.
  321. - (void)injectDeferredObject:(NSString*)source withWrapper:(NSString*)jsWrapper
  322. {
  323. if (!_injectedIframeBridge) {
  324. _injectedIframeBridge = YES;
  325. // Create an iframe bridge in the new document to communicate with the CDVThemeableBrowserViewController
  326. [self.themeableBrowserViewController.webView stringByEvaluatingJavaScriptFromString:@"(function(d){var e = _cdvIframeBridge = d.createElement('iframe');e.style.display='none';d.body.appendChild(e);})(document)"];
  327. }
  328. if (jsWrapper != nil) {
  329. NSData* jsonData = [NSJSONSerialization dataWithJSONObject:@[source] options:0 error:nil];
  330. NSString* sourceArrayString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
  331. if (sourceArrayString) {
  332. NSString* sourceString = [sourceArrayString substringWithRange:NSMakeRange(1, [sourceArrayString length] - 2)];
  333. NSString* jsToInject = [NSString stringWithFormat:jsWrapper, sourceString];
  334. [self.themeableBrowserViewController.webView stringByEvaluatingJavaScriptFromString:jsToInject];
  335. }
  336. } else {
  337. [self.themeableBrowserViewController.webView stringByEvaluatingJavaScriptFromString:source];
  338. }
  339. }
  340. - (void)injectScriptCode:(CDVInvokedUrlCommand*)command
  341. {
  342. NSString* jsWrapper = nil;
  343. if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) {
  344. jsWrapper = [NSString stringWithFormat:@"_cdvIframeBridge.src='gap-iab://%@/'+encodeURIComponent(JSON.stringify([eval(%%@)]));", command.callbackId];
  345. }
  346. [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper];
  347. }
  348. - (void)injectScriptFile:(CDVInvokedUrlCommand*)command
  349. {
  350. NSString* jsWrapper;
  351. if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) {
  352. jsWrapper = [NSString stringWithFormat:@"(function(d) { var c = d.createElement('script'); c.src = %%@; c.onload = function() { _cdvIframeBridge.src='gap-iab://%@'; }; d.body.appendChild(c); })(document)", command.callbackId];
  353. } else {
  354. jsWrapper = @"(function(d) { var c = d.createElement('script'); c.src = %@; d.body.appendChild(c); })(document)";
  355. }
  356. [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper];
  357. }
  358. - (void)injectStyleCode:(CDVInvokedUrlCommand*)command
  359. {
  360. NSString* jsWrapper;
  361. if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) {
  362. jsWrapper = [NSString stringWithFormat:@"(function(d) { var c = d.createElement('style'); c.innerHTML = %%@; c.onload = function() { _cdvIframeBridge.src='gap-iab://%@'; }; d.body.appendChild(c); })(document)", command.callbackId];
  363. } else {
  364. jsWrapper = @"(function(d) { var c = d.createElement('style'); c.innerHTML = %@; d.body.appendChild(c); })(document)";
  365. }
  366. [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper];
  367. }
  368. - (void)injectStyleFile:(CDVInvokedUrlCommand*)command
  369. {
  370. NSString* jsWrapper;
  371. if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) {
  372. jsWrapper = [NSString stringWithFormat:@"(function(d) { var c = d.createElement('link'); c.rel='stylesheet'; c.type='text/css'; c.href = %%@; c.onload = function() { _cdvIframeBridge.src='gap-iab://%@'; }; d.body.appendChild(c); })(document)", command.callbackId];
  373. } else {
  374. jsWrapper = @"(function(d) { var c = d.createElement('link'); c.rel='stylesheet', c.type='text/css'; c.href = %@; d.body.appendChild(c); })(document)";
  375. }
  376. [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper];
  377. }
  378. - (BOOL)isValidCallbackId:(NSString *)callbackId
  379. {
  380. NSError *err = nil;
  381. // Initialize on first use
  382. if (self.callbackIdPattern == nil) {
  383. self.callbackIdPattern = [NSRegularExpression regularExpressionWithPattern:@"^ThemeableBrowser[0-9]{1,10}$" options:0 error:&err];
  384. if (err != nil) {
  385. // Couldn't initialize Regex; No is safer than Yes.
  386. return NO;
  387. }
  388. }
  389. if ([self.callbackIdPattern firstMatchInString:callbackId options:0 range:NSMakeRange(0, [callbackId length])]) {
  390. return YES;
  391. }
  392. return NO;
  393. }
  394. /**
  395. * The iframe bridge provided for the ThemeableBrowser is capable of executing any oustanding callback belonging
  396. * to the ThemeableBrowser plugin. Care has been taken that other callbacks cannot be triggered, and that no
  397. * other code execution is possible.
  398. *
  399. * To trigger the bridge, the iframe (or any other resource) should attempt to load a url of the form:
  400. *
  401. * gap-iab://<callbackId>/<arguments>
  402. *
  403. * where <callbackId> is the string id of the callback to trigger (something like "ThemeableBrowser0123456789")
  404. *
  405. * If present, the path component of the special gap-iab:// url is expected to be a URL-escaped JSON-encoded
  406. * value to pass to the callback. [NSURL path] should take care of the URL-unescaping, and a JSON_EXCEPTION
  407. * is returned if the JSON is invalid.
  408. */
  409. - (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType
  410. {
  411. NSURL* url = request.URL;
  412. BOOL isTopLevelNavigation = [request.URL isEqual:[request mainDocumentURL]];
  413. // See if the url uses the 'gap-iab' protocol. If so, the host should be the id of a callback to execute,
  414. // and the path, if present, should be a JSON-encoded value to pass to the callback.
  415. if ([[url scheme] isEqualToString:@"gap-iab"]) {
  416. NSString* scriptCallbackId = [url host];
  417. CDVPluginResult* pluginResult = nil;
  418. if ([self isValidCallbackId:scriptCallbackId]) {
  419. NSString* scriptResult = [url path];
  420. NSError* __autoreleasing error = nil;
  421. // The message should be a JSON-encoded array of the result of the script which executed.
  422. if ((scriptResult != nil) && ([scriptResult length] > 1)) {
  423. scriptResult = [scriptResult substringFromIndex:1];
  424. NSData* decodedResult = [NSJSONSerialization JSONObjectWithData:[scriptResult dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:&error];
  425. if ((error == nil) && [decodedResult isKindOfClass:[NSArray class]]) {
  426. pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:(NSArray*)decodedResult];
  427. } else {
  428. pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_JSON_EXCEPTION];
  429. }
  430. } else {
  431. pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:@[]];
  432. }
  433. [self.commandDelegate sendPluginResult:pluginResult callbackId:scriptCallbackId];
  434. return NO;
  435. }
  436. } else if ([self isSystemUrl:url]) {
  437. // Do not allow iTunes store links from ThemeableBrowser as they do not work
  438. // instead open them with App Store app or Safari
  439. [[UIApplication sharedApplication] openURL:url];
  440. // only in the case where a redirect link is opened in a freshly started
  441. // ThemeableBrowser frame, trigger ThemeableBrowserRedirectExternalOnOpen
  442. // event. This event can be handled in the app-side -- for instance, to
  443. // close the ThemeableBrowser as the frame will contain a blank page
  444. if (
  445. originalUrl != nil
  446. && [[originalUrl absoluteString] isEqualToString:[initUrl absoluteString]]
  447. && _framesOpened == 1
  448. ) {
  449. NSDictionary *event = @{
  450. @"type": @"ThemeableBrowserRedirectExternalOnOpen",
  451. @"message": @"ThemeableBrowser redirected to open an external app on fresh start"
  452. };
  453. [self emitEvent:event];
  454. }
  455. // do not load content in the web view since this URL is handled by an
  456. // external app
  457. return NO;
  458. } else if ((self.callbackId != nil) && isTopLevelNavigation) {
  459. // Send a loadstart event for each top-level navigation (includes redirects).
  460. CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK
  461. messageAsDictionary:@{@"type":@"loadstart", @"url":[url absoluteString]}];
  462. [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]];
  463. [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId];
  464. }
  465. // originalUrl is used to detect redirect. This works by storing the
  466. // request URL of the original frame when it's about to be loaded. A redirect
  467. // will cause shouldStartLoadWithRequest to be called again before the
  468. // original frame finishes loading (originalUrl becomes nil upon the frame
  469. // finishing loading). On second time shouldStartLoadWithRequest
  470. // is called, this stored original frame's URL can be compared against
  471. // the URL of the new request. A mismatch implies redirect.
  472. originalUrl = request.URL;
  473. return YES;
  474. }
  475. - (void)webViewDidStartLoad:(UIWebView*)theWebView
  476. {
  477. _injectedIframeBridge = NO;
  478. _framesOpened++;
  479. }
  480. - (void)webViewDidFinishLoad:(UIWebView*)theWebView
  481. {
  482. if (self.callbackId != nil) {
  483. // TODO: It would be more useful to return the URL the page is actually on (e.g. if it's been redirected).
  484. NSString* url = [self.themeableBrowserViewController.currentURL absoluteString];
  485. CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK
  486. messageAsDictionary:@{@"type":@"loadstop", @"url":url}];
  487. [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]];
  488. // once a web view finished loading a frame, reset the stored original
  489. // URL of the frame so that it can be used to detect next redirection
  490. originalUrl = nil;
  491. [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId];
  492. }
  493. }
  494. - (void)webView:(UIWebView*)theWebView didFailLoadWithError:(NSError*)error
  495. {
  496. if (self.callbackId != nil) {
  497. NSString* url = [self.themeableBrowserViewController.currentURL absoluteString];
  498. CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
  499. messageAsDictionary:@{@"type":@"loaderror", @"url":url, @"code": [NSNumber numberWithInteger:error.code], @"message": error.localizedDescription}];
  500. [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]];
  501. [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId];
  502. }
  503. }
  504. - (void)browserExit
  505. {
  506. if (self.callbackId != nil) {
  507. CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK
  508. messageAsDictionary:@{@"type":@"exit"}];
  509. [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId];
  510. self.callbackId = nil;
  511. }
  512. // Set navigationDelegate to nil to ensure no callbacks are received from it.
  513. self.themeableBrowserViewController.navigationDelegate = nil;
  514. // Don't recycle the ViewController since it may be consuming a lot of memory.
  515. // Also - this is required for the PDF/User-Agent bug work-around.
  516. self.themeableBrowserViewController = nil;
  517. self.callbackId = nil;
  518. self.callbackIdPattern = nil;
  519. _framesOpened = 0;
  520. _isShown = NO;
  521. }
  522. - (void)emitEvent:(NSDictionary*)event
  523. {
  524. if (self.callbackId != nil) {
  525. CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK
  526. messageAsDictionary:event];
  527. [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]];
  528. [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId];
  529. }
  530. }
  531. - (void)emitError:(NSString*)code withMessage:(NSString*)message
  532. {
  533. NSDictionary *event = @{
  534. @"type": kThemeableBrowserEmitError,
  535. @"code": code,
  536. @"message": message
  537. };
  538. [self emitEvent:event];
  539. }
  540. - (void)emitWarning:(NSString*)code withMessage:(NSString*)message
  541. {
  542. NSDictionary *event = @{
  543. @"type": kThemeableBrowserEmitWarning,
  544. @"code": code,
  545. @"message": message
  546. };
  547. [self emitEvent:event];
  548. }
  549. @end
  550. #pragma mark CDVThemeableBrowserViewController
  551. @implementation CDVThemeableBrowserViewController
  552. @synthesize currentURL;
  553. - (id)initWithUserAgent:(NSString*)userAgent prevUserAgent:(NSString*)prevUserAgent browserOptions: (CDVThemeableBrowserOptions*) browserOptions navigationDelete:(CDVThemeableBrowser*) navigationDelegate statusBarStyle:(UIStatusBarStyle) statusBarStyle
  554. {
  555. self = [super init];
  556. if (self != nil) {
  557. _userAgent = userAgent;
  558. _prevUserAgent = prevUserAgent;
  559. _browserOptions = browserOptions;
  560. #ifdef __CORDOVA_4_0_0
  561. _webViewDelegate = [[CDVUIWebViewDelegate alloc] initWithDelegate:self];
  562. #else
  563. _webViewDelegate = [[CDVWebViewDelegate alloc] initWithDelegate:self];
  564. #endif
  565. _navigationDelegate = navigationDelegate;
  566. _statusBarStyle = statusBarStyle;
  567. [self createViews];
  568. }
  569. return self;
  570. }
  571. - (void)createViews
  572. {
  573. // We create the views in code for primarily for ease of upgrades and not requiring an external .xib to be included
  574. CGRect webViewBounds = self.view.bounds;
  575. BOOL toolbarIsAtBottom = ![_browserOptions.toolbarposition isEqualToString:kThemeableBrowserToolbarBarPositionTop];
  576. NSDictionary* toolbarProps = _browserOptions.toolbar;
  577. CGFloat toolbarHeight = [self getFloatFromDict:toolbarProps withKey:kThemeableBrowserPropHeight withDefault:TOOLBAR_DEF_HEIGHT];
  578. if (!_browserOptions.fullscreen) {
  579. webViewBounds.size.height -= toolbarHeight;
  580. }
  581. self.webView = [[UIWebView alloc] initWithFrame:webViewBounds];
  582. self.webView.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);
  583. [self.view addSubview:self.webView];
  584. [self.view sendSubviewToBack:self.webView];
  585. self.webView.delegate = _webViewDelegate;
  586. self.webView.backgroundColor = [UIColor whiteColor];
  587. self.webView.clearsContextBeforeDrawing = YES;
  588. self.webView.clipsToBounds = YES;
  589. self.webView.contentMode = UIViewContentModeScaleToFill;
  590. self.webView.multipleTouchEnabled = YES;
  591. self.webView.opaque = YES;
  592. self.webView.scalesPageToFit = NO;
  593. self.webView.userInteractionEnabled = YES;
  594. self.spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
  595. self.spinner.alpha = 1.000;
  596. self.spinner.autoresizesSubviews = YES;
  597. self.spinner.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin;
  598. self.spinner.clearsContextBeforeDrawing = NO;
  599. self.spinner.clipsToBounds = NO;
  600. self.spinner.contentMode = UIViewContentModeScaleToFill;
  601. self.spinner.frame = CGRectMake(454.0, 231.0, 20.0, 20.0);
  602. self.spinner.hidden = YES;
  603. self.spinner.hidesWhenStopped = YES;
  604. self.spinner.multipleTouchEnabled = NO;
  605. self.spinner.opaque = NO;
  606. self.spinner.userInteractionEnabled = NO;
  607. [self.spinner stopAnimating];
  608. CGFloat toolbarY = toolbarIsAtBottom ? self.view.bounds.size.height - toolbarHeight : 0.0;
  609. CGRect toolbarFrame = CGRectMake(0.0, toolbarY, self.view.bounds.size.width, toolbarHeight);
  610. self.toolbar = [[UIView alloc] initWithFrame:toolbarFrame];
  611. self.toolbar.alpha = 1.000;
  612. self.toolbar.autoresizesSubviews = YES;
  613. self.toolbar.autoresizingMask = toolbarIsAtBottom ? (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin) : UIViewAutoresizingFlexibleWidth;
  614. self.toolbar.clearsContextBeforeDrawing = NO;
  615. self.toolbar.clipsToBounds = YES;
  616. self.toolbar.contentMode = UIViewContentModeScaleToFill;
  617. self.toolbar.hidden = NO;
  618. self.toolbar.multipleTouchEnabled = NO;
  619. self.toolbar.opaque = NO;
  620. self.toolbar.userInteractionEnabled = YES;
  621. self.toolbar.backgroundColor = [CDVThemeableBrowserViewController colorFromRGBA:[self getStringFromDict:toolbarProps withKey:kThemeableBrowserPropColor withDefault:@"#ffffffff"]];
  622. if (toolbarProps[kThemeableBrowserPropImage] || toolbarProps[kThemeableBrowserPropWwwImage]) {
  623. UIImage *image = [self getImage:toolbarProps[kThemeableBrowserPropImage]
  624. altPath:toolbarProps[kThemeableBrowserPropWwwImage]
  625. altDensity:[toolbarProps[kThemeableBrowserPropWwwImageDensity] doubleValue]];
  626. if (image) {
  627. self.toolbar.backgroundColor = [UIColor colorWithPatternImage:image];
  628. } else {
  629. [self.navigationDelegate emitError:kThemeableBrowserEmitCodeLoadFail
  630. withMessage:[NSString stringWithFormat:@"Image for toolbar, %@, failed to load.",
  631. toolbarProps[kThemeableBrowserPropImage]
  632. ? toolbarProps[kThemeableBrowserPropImage] : toolbarProps[kThemeableBrowserPropWwwImage]]];
  633. }
  634. }
  635. CGFloat labelInset = 5.0;
  636. float locationBarY = self.view.bounds.size.height - LOCATIONBAR_HEIGHT;
  637. self.addressLabel = [[UILabel alloc] initWithFrame:CGRectMake(labelInset, locationBarY, self.view.bounds.size.width - labelInset, LOCATIONBAR_HEIGHT)];
  638. self.addressLabel.adjustsFontSizeToFitWidth = NO;
  639. self.addressLabel.alpha = 1.000;
  640. self.addressLabel.autoresizesSubviews = YES;
  641. self.addressLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin;
  642. self.addressLabel.backgroundColor = [UIColor clearColor];
  643. self.addressLabel.baselineAdjustment = UIBaselineAdjustmentAlignCenters;
  644. self.addressLabel.clearsContextBeforeDrawing = YES;
  645. self.addressLabel.clipsToBounds = YES;
  646. self.addressLabel.contentMode = UIViewContentModeScaleToFill;
  647. self.addressLabel.enabled = YES;
  648. self.addressLabel.hidden = NO;
  649. self.addressLabel.lineBreakMode = NSLineBreakByTruncatingTail;
  650. if ([self.addressLabel respondsToSelector:NSSelectorFromString(@"setMinimumScaleFactor:")]) {
  651. [self.addressLabel setValue:@(10.0/[UIFont labelFontSize]) forKey:@"minimumScaleFactor"];
  652. } else if ([self.addressLabel respondsToSelector:NSSelectorFromString(@"setMinimumFontSize:")]) {
  653. [self.addressLabel setValue:@(10.0) forKey:@"minimumFontSize"];
  654. }
  655. self.addressLabel.multipleTouchEnabled = NO;
  656. self.addressLabel.numberOfLines = 1;
  657. self.addressLabel.opaque = NO;
  658. self.addressLabel.shadowOffset = CGSizeMake(0.0, -1.0);
  659. self.addressLabel.text = NSLocalizedString(@"Loading...", nil);
  660. self.addressLabel.textAlignment = NSTextAlignmentLeft;
  661. self.addressLabel.textColor = [UIColor colorWithWhite:1.000 alpha:1.000];
  662. self.addressLabel.userInteractionEnabled = NO;
  663. self.closeButton = [self createButton:_browserOptions.closeButton action:@selector(close) withDescription:@"close button"];
  664. self.backButton = [self createButton:_browserOptions.backButton action:@selector(goBack:) withDescription:@"back button"];
  665. self.forwardButton = [self createButton:_browserOptions.forwardButton action:@selector(goForward:) withDescription:@"forward button"];
  666. self.menuButton = [self createButton:_browserOptions.menu action:@selector(goMenu:) withDescription:@"menu button"];
  667. // Arramge toolbar buttons with respect to user configuration.
  668. CGFloat leftWidth = 0;
  669. CGFloat rightWidth = 0;
  670. // Both left and right side buttons will be ordered from outside to inside.
  671. NSMutableArray* leftButtons = [NSMutableArray new];
  672. NSMutableArray* rightButtons = [NSMutableArray new];
  673. if (self.closeButton) {
  674. CGFloat width = [self getWidthFromButton:self.closeButton];
  675. if ([kThemeableBrowserAlignRight isEqualToString:_browserOptions.closeButton[kThemeableBrowserPropAlign]]) {
  676. [rightButtons addObject:self.closeButton];
  677. rightWidth += width;
  678. } else {
  679. [leftButtons addObject:self.closeButton];
  680. leftWidth += width;
  681. }
  682. }
  683. if (self.menuButton) {
  684. CGFloat width = [self getWidthFromButton:self.menuButton];
  685. if ([kThemeableBrowserAlignRight isEqualToString:_browserOptions.menu[kThemeableBrowserPropAlign]]) {
  686. [rightButtons addObject:self.menuButton];
  687. rightWidth += width;
  688. } else {
  689. [leftButtons addObject:self.menuButton];
  690. leftWidth += width;
  691. }
  692. }
  693. // Back and forward buttons must be added with special ordering logic such
  694. // that back button is always on the left of forward button if both buttons
  695. // are on the same side.
  696. if (self.backButton && ![kThemeableBrowserAlignRight isEqualToString:_browserOptions.backButton[kThemeableBrowserPropAlign]]) {
  697. CGFloat width = [self getWidthFromButton:self.backButton];
  698. [leftButtons addObject:self.backButton];
  699. leftWidth += width;
  700. }
  701. if (self.forwardButton && [kThemeableBrowserAlignRight isEqualToString:_browserOptions.forwardButton[kThemeableBrowserPropAlign]]) {
  702. CGFloat width = [self getWidthFromButton:self.forwardButton];
  703. [rightButtons addObject:self.forwardButton];
  704. rightWidth += width;
  705. }
  706. if (self.forwardButton && ![kThemeableBrowserAlignRight isEqualToString:_browserOptions.forwardButton[kThemeableBrowserPropAlign]]) {
  707. CGFloat width = [self getWidthFromButton:self.forwardButton];
  708. [leftButtons addObject:self.forwardButton];
  709. leftWidth += width;
  710. }
  711. if (self.backButton && [kThemeableBrowserAlignRight isEqualToString:_browserOptions.backButton[kThemeableBrowserPropAlign]]) {
  712. CGFloat width = [self getWidthFromButton:self.backButton];
  713. [rightButtons addObject:self.backButton];
  714. rightWidth += width;
  715. }
  716. NSArray* customButtons = _browserOptions.customButtons;
  717. if (customButtons) {
  718. NSInteger cnt = 0;
  719. // Reverse loop because we are laying out from outer to inner.
  720. for (NSDictionary* customButton in [customButtons reverseObjectEnumerator]) {
  721. UIButton* button = [self createButton:customButton action:@selector(goCustomButton:) withDescription:[NSString stringWithFormat:@"custom button at %ld", (long)cnt]];
  722. if (button) {
  723. button.tag = cnt;
  724. CGFloat width = [self getWidthFromButton:button];
  725. if ([kThemeableBrowserAlignRight isEqualToString:customButton[kThemeableBrowserPropAlign]]) {
  726. [rightButtons addObject:button];
  727. rightWidth += width;
  728. } else {
  729. [leftButtons addObject:button];
  730. leftWidth += width;
  731. }
  732. }
  733. cnt += 1;
  734. }
  735. }
  736. self.rightButtons = rightButtons;
  737. self.leftButtons = leftButtons;
  738. for (UIButton* button in self.leftButtons) {
  739. [self.toolbar addSubview:button];
  740. }
  741. for (UIButton* button in self.rightButtons) {
  742. [self.toolbar addSubview:button];
  743. }
  744. [self layoutButtons];
  745. self.titleOffset = fmaxf(leftWidth, rightWidth);
  746. // The correct positioning of title is not that important right now, since
  747. // rePositionViews will take care of it a bit later.
  748. self.titleLabel = nil;
  749. if (_browserOptions.title) {
  750. self.titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 0, 10, toolbarHeight)];
  751. self.titleLabel.textAlignment = NSTextAlignmentCenter;
  752. self.titleLabel.numberOfLines = 1;
  753. self.titleLabel.lineBreakMode = NSLineBreakByTruncatingTail;
  754. self.titleLabel.textColor = [CDVThemeableBrowserViewController colorFromRGBA:[self getStringFromDict:_browserOptions.title withKey:kThemeableBrowserPropColor withDefault:@"#000000ff"]];
  755. if (_browserOptions.title[kThemeableBrowserPropStaticText]) {
  756. self.titleLabel.text = _browserOptions.title[kThemeableBrowserPropStaticText];
  757. }
  758. [self.toolbar addSubview:self.titleLabel];
  759. }
  760. self.view.backgroundColor = [CDVThemeableBrowserViewController colorFromRGBA:[self getStringFromDict:_browserOptions.statusbar withKey:kThemeableBrowserPropColor withDefault:@"#ffffffff"]];
  761. [self.view addSubview:self.toolbar];
  762. // [self.view addSubview:self.addressLabel];
  763. // [self.view addSubview:self.spinner];
  764. }
  765. /**
  766. * This is a rather unintuitive helper method to load images. The reason why this method exists
  767. * is because due to some service limitations, one may not be able to add images to native
  768. * resource bundle. So this method offers a way to load image from www contents instead.
  769. * However loading from native resource bundle is already preferred over loading from www. So
  770. * if name is given, then it simply loads from resource bundle and the other two parameters are
  771. * ignored. If name is not given, then altPath is assumed to be a file path _under_ www and
  772. * altDensity is the desired density of the given image file, because without native resource
  773. * bundle, we can't tell what densitiy the image is supposed to be so it needs to be given
  774. * explicitly.
  775. */
  776. - (UIImage*) getImage:(NSString*) name altPath:(NSString*) altPath altDensity:(CGFloat) altDensity
  777. {
  778. UIImage* result = nil;
  779. if (name) {
  780. result = [UIImage imageNamed:name];
  781. } else if (altPath) {
  782. NSString* path = [[[NSBundle mainBundle] bundlePath]
  783. stringByAppendingPathComponent:[NSString pathWithComponents:@[@"www", altPath]]];
  784. if (!altDensity) {
  785. altDensity = 1.0;
  786. }
  787. NSData* data = [NSData dataWithContentsOfFile:path];
  788. result = [UIImage imageWithData:data scale:altDensity];
  789. }
  790. return result;
  791. }
  792. - (UIButton*) createButton:(NSDictionary*) buttonProps action:(SEL)action withDescription:(NSString*)description
  793. {
  794. UIButton* result = nil;
  795. if (buttonProps) {
  796. UIImage *buttonImage = nil;
  797. if (buttonProps[kThemeableBrowserPropImage] || buttonProps[kThemeableBrowserPropWwwImage]) {
  798. buttonImage = [self getImage:buttonProps[kThemeableBrowserPropImage]
  799. altPath:buttonProps[kThemeableBrowserPropWwwImage]
  800. altDensity:[buttonProps[kThemeableBrowserPropWwwImageDensity] doubleValue]];
  801. if (!buttonImage) {
  802. [self.navigationDelegate emitError:kThemeableBrowserEmitCodeLoadFail
  803. withMessage:[NSString stringWithFormat:@"Image for %@, %@, failed to load.",
  804. description,
  805. buttonProps[kThemeableBrowserPropImage]
  806. ? buttonProps[kThemeableBrowserPropImage] : buttonProps[kThemeableBrowserPropWwwImage]]];
  807. }
  808. } else {
  809. [self.navigationDelegate emitWarning:kThemeableBrowserEmitCodeUndefined
  810. withMessage:[NSString stringWithFormat:@"Image for %@ is not defined. Button will not be shown.", description]];
  811. }
  812. UIImage *buttonImagePressed = nil;
  813. if (buttonProps[kThemeableBrowserPropImagePressed] || buttonProps[kThemeableBrowserPropWwwImagePressed]) {
  814. buttonImagePressed = [self getImage:buttonProps[kThemeableBrowserPropImagePressed]
  815. altPath:buttonProps[kThemeableBrowserPropWwwImagePressed]
  816. altDensity:[buttonProps[kThemeableBrowserPropWwwImageDensity] doubleValue]];;
  817. if (!buttonImagePressed) {
  818. [self.navigationDelegate emitError:kThemeableBrowserEmitCodeLoadFail
  819. withMessage:[NSString stringWithFormat:@"Pressed image for %@, %@, failed to load.",
  820. description,
  821. buttonProps[kThemeableBrowserPropImagePressed]
  822. ? buttonProps[kThemeableBrowserPropImagePressed] : buttonProps[kThemeableBrowserPropWwwImagePressed]]];
  823. }
  824. } else {
  825. [self.navigationDelegate emitWarning:kThemeableBrowserEmitCodeUndefined
  826. withMessage:[NSString stringWithFormat:@"Pressed image for %@ is not defined.", description]];
  827. }
  828. if (buttonImage) {
  829. result = [UIButton buttonWithType:UIButtonTypeCustom];
  830. result.bounds = CGRectMake(0, 0, buttonImage.size.width, buttonImage.size.height);
  831. if (buttonImagePressed) {
  832. [result setImage:buttonImagePressed forState:UIControlStateHighlighted];
  833. result.adjustsImageWhenHighlighted = NO;
  834. }
  835. [result setImage:buttonImage forState:UIControlStateNormal];
  836. [result addTarget:self action:action forControlEvents:UIControlEventTouchUpInside];
  837. }
  838. } else if (!buttonProps) {
  839. [self.navigationDelegate emitWarning:kThemeableBrowserEmitCodeUndefined
  840. withMessage:[NSString stringWithFormat:@"%@ is not defined. Button will not be shown.", description]];
  841. } else if (!buttonProps[kThemeableBrowserPropImage]) {
  842. }
  843. return result;
  844. }
  845. - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
  846. {
  847. [super didRotateFromInterfaceOrientation:fromInterfaceOrientation];
  848. // Reposition views.
  849. [self rePositionViews];
  850. }
  851. - (void) setWebViewFrame : (CGRect) frame {
  852. [self.webView setFrame:frame];
  853. }
  854. - (void)layoutButtons
  855. {
  856. CGFloat screenWidth = CGRectGetWidth(self.view.frame);
  857. CGFloat toolbarHeight = self.toolbar.frame.size.height;
  858. // Layout leftButtons and rightButtons from outer to inner.
  859. CGFloat left = 0;
  860. for (UIButton* button in self.leftButtons) {
  861. CGSize size = button.frame.size;
  862. button.frame = CGRectMake(left, floorf((toolbarHeight - size.height) / 2), size.width, size.height);
  863. left += size.width;
  864. }
  865. CGFloat right = 0;
  866. for (UIButton* button in self.rightButtons) {
  867. CGSize size = button.frame.size;
  868. button.frame = CGRectMake(screenWidth - right - size.width, floorf((toolbarHeight - size.height) / 2), size.width, size.height);
  869. right += size.width;
  870. }
  871. }
  872. - (void)setCloseButtonTitle:(NSString*)title
  873. {
  874. // This method is not used by ThemeableBrowser. It is inherited from
  875. // InAppBrowser and is kept for merge purposes.
  876. // the advantage of using UIBarButtonSystemItemDone is the system will localize it for you automatically
  877. // but, if you want to set this yourself, knock yourself out (we can't set the title for a system Done button, so we have to create a new one)
  878. // self.closeButton = nil;
  879. // self.closeButton = [[UIBarButtonItem alloc] initWithTitle:title style:UIBarButtonItemStyleBordered target:self action:@selector(close)];
  880. // self.closeButton.enabled = YES;
  881. // self.closeButton.tintColor = [UIColor colorWithRed:60.0 / 255.0 green:136.0 / 255.0 blue:230.0 / 255.0 alpha:1];
  882. // NSMutableArray* items = [self.toolbar.items mutableCopy];
  883. // [items replaceObjectAtIndex:0 withObject:self.closeButton];
  884. // [self.toolbar setItems:items];
  885. }
  886. - (void)showLocationBar:(BOOL)show
  887. {
  888. CGRect locationbarFrame = self.addressLabel.frame;
  889. CGFloat toolbarHeight = [self getFloatFromDict:_browserOptions.toolbar withKey:kThemeableBrowserPropHeight withDefault:TOOLBAR_DEF_HEIGHT];
  890. BOOL toolbarVisible = !self.toolbar.hidden;
  891. // prevent double show/hide
  892. if (show == !(self.addressLabel.hidden)) {
  893. return;
  894. }
  895. if (show) {
  896. self.addressLabel.hidden = NO;
  897. if (toolbarVisible) {
  898. // toolBar at the bottom, leave as is
  899. // put locationBar on top of the toolBar
  900. CGRect webViewBounds = self.view.bounds;
  901. if (!_browserOptions.fullscreen) {
  902. webViewBounds.size.height -= toolbarHeight;
  903. }
  904. [self setWebViewFrame:webViewBounds];
  905. locationbarFrame.origin.y = webViewBounds.size.height;
  906. self.addressLabel.frame = locationbarFrame;
  907. } else {
  908. // no toolBar, so put locationBar at the bottom
  909. CGRect webViewBounds = self.view.bounds;
  910. webViewBounds.size.height -= LOCATIONBAR_HEIGHT;
  911. [self setWebViewFrame:webViewBounds];
  912. locationbarFrame.origin.y = webViewBounds.size.height;
  913. self.addressLabel.frame = locationbarFrame;
  914. }
  915. } else {
  916. self.addressLabel.hidden = YES;
  917. if (toolbarVisible) {
  918. // locationBar is on top of toolBar, hide locationBar
  919. // webView take up whole height less toolBar height
  920. CGRect webViewBounds = self.view.bounds;
  921. if (!_browserOptions.fullscreen) {
  922. webViewBounds.size.height -= toolbarHeight;
  923. }
  924. [self setWebViewFrame:webViewBounds];
  925. } else {
  926. // no toolBar, expand webView to screen dimensions
  927. [self setWebViewFrame:self.view.bounds];
  928. }
  929. }
  930. }
  931. - (void)showToolBar:(BOOL)show : (NSString *) toolbarPosition
  932. {
  933. CGRect toolbarFrame = self.toolbar.frame;
  934. CGRect locationbarFrame = self.addressLabel.frame;
  935. CGFloat toolbarHeight = [self getFloatFromDict:_browserOptions.toolbar withKey:kThemeableBrowserPropHeight withDefault:TOOLBAR_DEF_HEIGHT];
  936. BOOL locationbarVisible = !self.addressLabel.hidden;
  937. // prevent double show/hide
  938. if (show == !(self.toolbar.hidden)) {
  939. return;
  940. }
  941. if (show) {
  942. self.toolbar.hidden = NO;
  943. CGRect webViewBounds = self.view.bounds;
  944. if (locationbarVisible) {
  945. // locationBar at the bottom, move locationBar up
  946. // put toolBar at the bottom
  947. if (!_browserOptions.fullscreen) {
  948. webViewBounds.size.height -= toolbarHeight;
  949. }
  950. locationbarFrame.origin.y = webViewBounds.size.height;
  951. self.addressLabel.frame = locationbarFrame;
  952. self.toolbar.frame = toolbarFrame;
  953. } else {
  954. // no locationBar, so put toolBar at the bottom
  955. self.toolbar.frame = toolbarFrame;
  956. }
  957. if ([toolbarPosition isEqualToString:kThemeableBrowserToolbarBarPositionTop]) {
  958. toolbarFrame.origin.y = 0;
  959. if (!_browserOptions.fullscreen) {
  960. webViewBounds.origin.y += toolbarFrame.size.height;
  961. }
  962. [self setWebViewFrame:webViewBounds];
  963. } else {
  964. toolbarFrame.origin.y = (webViewBounds.size.height + LOCATIONBAR_HEIGHT);
  965. }
  966. [self setWebViewFrame:webViewBounds];
  967. } else {
  968. self.toolbar.hidden = YES;
  969. if (locationbarVisible) {
  970. // locationBar is on top of toolBar, hide toolBar
  971. // put locationBar at the bottom
  972. // webView take up whole height less locationBar height
  973. CGRect webViewBounds = self.view.bounds;
  974. webViewBounds.size.height -= LOCATIONBAR_HEIGHT;
  975. [self setWebViewFrame:webViewBounds];
  976. // move locationBar down
  977. locationbarFrame.origin.y = webViewBounds.size.height;
  978. self.addressLabel.frame = locationbarFrame;
  979. } else {
  980. // no locationBar, expand webView to screen dimensions
  981. [self setWebViewFrame:self.view.bounds];
  982. }
  983. }
  984. }
  985. - (void)viewDidLoad
  986. {
  987. [super viewDidLoad];
  988. }
  989. - (void)viewDidUnload
  990. {
  991. [self.webView loadHTMLString:nil baseURL:nil];
  992. [CDVUserAgentUtil releaseLock:&_userAgentLockToken];
  993. [super viewDidUnload];
  994. }
  995. - (UIStatusBarStyle)preferredStatusBarStyle
  996. {
  997. return _statusBarStyle;
  998. }
  999. - (void)close
  1000. {
  1001. [self emitEventForButton:_browserOptions.closeButton];
  1002. [CDVUserAgentUtil releaseLock:&_userAgentLockToken];
  1003. self.currentURL = nil;
  1004. if ((self.navigationDelegate != nil) && [self.navigationDelegate respondsToSelector:@selector(browserExit)]) {
  1005. [self.navigationDelegate browserExit];
  1006. }
  1007. // Run later to avoid the "took a long time" log message.
  1008. dispatch_async(dispatch_get_main_queue(), ^{
  1009. if ([self respondsToSelector:@selector(presentingViewController)]) {
  1010. [[self presentingViewController] dismissViewControllerAnimated:!_browserOptions.disableAnimation completion:nil];
  1011. } else {
  1012. [[self parentViewController] dismissViewControllerAnimated:!_browserOptions.disableAnimation completion:nil];
  1013. }
  1014. });
  1015. }
  1016. - (void)reload
  1017. {
  1018. [self.webView reload];
  1019. }
  1020. - (void)navigateTo:(NSURL*)url
  1021. {
  1022. NSURLRequest* request = [NSURLRequest requestWithURL:url];
  1023. if (_userAgentLockToken != 0) {
  1024. [self.webView loadRequest:request];
  1025. } else {
  1026. [CDVUserAgentUtil acquireLock:^(NSInteger lockToken) {
  1027. _userAgentLockToken = lockToken;
  1028. [CDVUserAgentUtil setUserAgent:_userAgent lockToken:lockToken];
  1029. [self.webView loadRequest:request];
  1030. }];
  1031. }
  1032. }
  1033. - (void)goBack:(id)sender
  1034. {
  1035. [self emitEventForButton:_browserOptions.backButton];
  1036. if (self.webView.canGoBack) {
  1037. [self.webView goBack];
  1038. [self updateButtonDelayed:self.webView];
  1039. } else if (_browserOptions.backButtonCanClose) {
  1040. [self close];
  1041. }
  1042. }
  1043. - (void)goForward:(id)sender
  1044. {
  1045. [self emitEventForButton:_browserOptions.forwardButton];
  1046. [self.webView goForward];
  1047. [self updateButtonDelayed:self.webView];
  1048. }
  1049. - (void)goCustomButton:(id)sender
  1050. {
  1051. UIButton* button = sender;
  1052. NSInteger index = button.tag;
  1053. [self emitEventForButton:_browserOptions.customButtons[index] withIndex:[NSNumber numberWithLong:index]];
  1054. }
  1055. - (void)goMenu:(id)sender
  1056. {
  1057. [self emitEventForButton:_browserOptions.menu];
  1058. if (_browserOptions.menu && _browserOptions.menu[kThemeableBrowserPropItems]) {
  1059. NSArray* menuItems = _browserOptions.menu[kThemeableBrowserPropItems];
  1060. if (IsAtLeastiOSVersion(@"8.0")) {
  1061. // iOS > 8 implementation using UIAlertController, which is the new way
  1062. // to do this going forward.
  1063. UIAlertController *alertController = [UIAlertController
  1064. alertControllerWithTitle:_browserOptions.menu[kThemeableBrowserPropTitle]
  1065. message:nil
  1066. preferredStyle:UIAlertControllerStyleActionSheet];
  1067. alertController.popoverPresentationController.sourceView
  1068. = self.menuButton;
  1069. alertController.popoverPresentationController.sourceRect
  1070. = self.menuButton.bounds;
  1071. for (NSInteger i = 0; i < menuItems.count; i++) {
  1072. NSInteger index = i;
  1073. NSDictionary *item = menuItems[index];
  1074. UIAlertAction *a = [UIAlertAction
  1075. actionWithTitle:item[@"label"]
  1076. style:UIAlertActionStyleDefault
  1077. handler:^(UIAlertAction *action) {
  1078. [self menuSelected:index];
  1079. }];
  1080. [alertController addAction:a];
  1081. }
  1082. if (_browserOptions.menu[kThemeableBrowserPropCancel]) {
  1083. UIAlertAction *cancelAction = [UIAlertAction
  1084. actionWithTitle:_browserOptions.menu[kThemeableBrowserPropCancel]
  1085. style:UIAlertActionStyleCancel
  1086. handler:nil];
  1087. [alertController addAction:cancelAction];
  1088. }
  1089. [self presentViewController:alertController animated:YES completion:nil];
  1090. } else {
  1091. // iOS < 8 implementation using UIActionSheet, which is deprecated.
  1092. UIActionSheet *popup = [[UIActionSheet alloc]
  1093. initWithTitle:_browserOptions.menu[kThemeableBrowserPropTitle]
  1094. delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles:nil];
  1095. for (NSDictionary *item in menuItems) {
  1096. [popup addButtonWithTitle:item[@"label"]];
  1097. }
  1098. if (_browserOptions.menu[kThemeableBrowserPropCancel]) {
  1099. [popup addButtonWithTitle:_browserOptions.menu[kThemeableBrowserPropCancel]];
  1100. popup.cancelButtonIndex = menuItems.count;
  1101. }
  1102. [popup showFromRect:self.menuButton.frame inView:self.view animated:YES];
  1103. }
  1104. } else {
  1105. [self.navigationDelegate emitWarning:kThemeableBrowserEmitCodeUndefined
  1106. withMessage:@"Menu items undefined. No menu will be shown."];
  1107. }
  1108. }
  1109. - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
  1110. {
  1111. [self menuSelected:buttonIndex];
  1112. }
  1113. - (void) menuSelected:(NSInteger)index
  1114. {
  1115. NSArray* menuItems = _browserOptions.menu[kThemeableBrowserPropItems];
  1116. if (index < menuItems.count) {
  1117. [self emitEventForButton:menuItems[index] withIndex:[NSNumber numberWithLong:index]];
  1118. }
  1119. }
  1120. - (void)viewWillAppear:(BOOL)animated
  1121. {
  1122. if (IsAtLeastiOSVersion(@"7.0")) {
  1123. [[UIApplication sharedApplication] setStatusBarStyle:[self preferredStatusBarStyle]];
  1124. }
  1125. [self rePositionViews];
  1126. [super viewWillAppear:animated];
  1127. }
  1128. //
  1129. // On iOS 7 the status bar is part of the view's dimensions, therefore it's height has to be taken into account.
  1130. // The height of it could be hardcoded as 20 pixels, but that would assume that the upcoming releases of iOS won't
  1131. // change that value.
  1132. //
  1133. - (float) getStatusBarOffset {
  1134. CGRect statusBarFrame = [[UIApplication sharedApplication] statusBarFrame];
  1135. float statusBarOffset = IsAtLeastiOSVersion(@"7.0") ? MIN(statusBarFrame.size.width, statusBarFrame.size.height) : 0.0;
  1136. return statusBarOffset;
  1137. }
  1138. - (void) rePositionViews {
  1139. if (@available(iOS 11, *)) {
  1140. CGFloat toolbarHeight = [self getFloatFromDict:_browserOptions.toolbar withKey:kThemeableBrowserPropHeight withDefault:TOOLBAR_DEF_HEIGHT];
  1141. CGFloat statusBarOffset = [self getStatusBarOffset];
  1142. CGFloat webviewOffset = _browserOptions.fullscreen ? 0.0 : toolbarHeight + statusBarOffset;
  1143. if ([_browserOptions.toolbarposition isEqualToString:kThemeableBrowserToolbarBarPositionTop]) {
  1144. // The webview height calculated did not take the status bar into account. Thus we need to remove status bar height to the webview height.
  1145. [self.webView setFrame:CGRectMake(self.webView.frame.origin.x, webviewOffset, self.webView.frame.size.width, (self.webView.frame.size.height-statusBarOffset))];
  1146. [self.toolbar setFrame:CGRectMake(self.toolbar.frame.origin.x, [self getStatusBarOffset], self.toolbar.frame.size.width, self.toolbar.frame.size.height)];
  1147. }
  1148. // When positionning the iphone to landscape mode, status bar is hidden. The problem is that we set the webview height just before with removing the status bar height. We need to adjust the phenomen by adding the preview status bar height. We had to add manually 20 (pixel) because in landscape mode, the status bar height is equal to 0.
  1149. if (statusBarOffset == 0) {
  1150. [self.webView setFrame:CGRectMake(self.webView.frame.origin.x, webviewOffset, self.webView.frame.size.width, (self.webView.frame.size.height+20))];
  1151. }
  1152. CGFloat screenWidth = CGRectGetWidth(self.view.frame);
  1153. NSInteger width = floorf(screenWidth - self.titleOffset * 2.0f);
  1154. if (self.titleLabel) {
  1155. self.titleLabel.frame = CGRectMake(floorf((screenWidth - width) / 2.0f), 0, width, toolbarHeight);
  1156. }
  1157. [self layoutButtons];
  1158. } else {
  1159. CGFloat toolbarHeight = [self getFloatFromDict:_browserOptions.toolbar withKey:kThemeableBrowserPropHeight withDefault:TOOLBAR_DEF_HEIGHT];
  1160. CGFloat webviewOffset = _browserOptions.fullscreen ? 0.0 : toolbarHeight;
  1161. if ([_browserOptions.toolbarposition isEqualToString:kThemeableBrowserToolbarBarPositionTop]) {
  1162. [self.webView setFrame:CGRectMake(self.webView.frame.origin.x, webviewOffset, self.webView.frame.size.width, self.webView.frame.size.height)];
  1163. [self.toolbar setFrame:CGRectMake(self.toolbar.frame.origin.x, [self getStatusBarOffset], self.toolbar.frame.size.width, self.toolbar.frame.size.height)];
  1164. }
  1165. CGFloat screenWidth = CGRectGetWidth(self.view.frame);
  1166. NSInteger width = floorf(screenWidth - self.titleOffset * 2.0f);
  1167. if (self.titleLabel) {
  1168. self.titleLabel.frame = CGRectMake(floorf((screenWidth - width) / 2.0f), 0, width, toolbarHeight);
  1169. }
  1170. [self layoutButtons];
  1171. }
  1172. }
  1173. - (CGFloat) getFloatFromDict:(NSDictionary*)dict withKey:(NSString*)key withDefault:(CGFloat)def
  1174. {
  1175. CGFloat result = def;
  1176. if (dict && dict[key]) {
  1177. result = [(NSNumber*) dict[key] floatValue];
  1178. }
  1179. return result;
  1180. }
  1181. - (NSString*) getStringFromDict:(NSDictionary*)dict withKey:(NSString*)key withDefault:(NSString*)def
  1182. {
  1183. NSString* result = def;
  1184. if (dict && dict[key]) {
  1185. result = dict[key];
  1186. }
  1187. return result;
  1188. }
  1189. - (BOOL) getBoolFromDict:(NSDictionary*)dict withKey:(NSString*)key
  1190. {
  1191. BOOL result = NO;
  1192. if (dict && dict[key]) {
  1193. result = [(NSNumber*) dict[key] boolValue];
  1194. }
  1195. return result;
  1196. }
  1197. - (CGFloat) getWidthFromButton:(UIButton*)button
  1198. {
  1199. return button.frame.size.width;
  1200. }
  1201. - (void)emitEventForButton:(NSDictionary*)buttonProps
  1202. {
  1203. [self emitEventForButton:buttonProps withIndex:nil];
  1204. }
  1205. - (void)emitEventForButton:(NSDictionary*)buttonProps withIndex:(NSNumber*)index
  1206. {
  1207. if (buttonProps) {
  1208. NSString* event = buttonProps[kThemeableBrowserPropEvent];
  1209. if (event) {
  1210. NSMutableDictionary* dict = [NSMutableDictionary new];
  1211. [dict setObject:event forKey:@"type"];
  1212. [dict setObject:[self.navigationDelegate.themeableBrowserViewController.currentURL absoluteString] forKey:@"url"];
  1213. if (index) {
  1214. [dict setObject:index forKey:@"index"];
  1215. }
  1216. [self.navigationDelegate emitEvent:dict];
  1217. } else {
  1218. [self.navigationDelegate emitWarning:kThemeableBrowserEmitCodeUndefined
  1219. withMessage:@"Button clicked, but event property undefined. No event will be raised."];
  1220. }
  1221. }
  1222. }
  1223. #pragma mark UIWebViewDelegate
  1224. - (void)webViewDidStartLoad:(UIWebView*)theWebView
  1225. {
  1226. // loading url, start spinner
  1227. self.addressLabel.text = NSLocalizedString(@"Loading...", nil);
  1228. [self.spinner startAnimating];
  1229. return [self.navigationDelegate webViewDidStartLoad:theWebView];
  1230. }
  1231. - (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType
  1232. {
  1233. BOOL isTopLevelNavigation = [request.URL isEqual:[request mainDocumentURL]];
  1234. if (isTopLevelNavigation) {
  1235. self.currentURL = request.URL;
  1236. }
  1237. [self updateButtonDelayed:theWebView];
  1238. return [self.navigationDelegate webView:theWebView shouldStartLoadWithRequest:request navigationType:navigationType];
  1239. }
  1240. - (void)webViewDidFinishLoad:(UIWebView*)theWebView
  1241. {
  1242. // update url, stop spinner, update back/forward
  1243. self.addressLabel.text = [self.currentURL absoluteString];
  1244. [self updateButton:theWebView];
  1245. if (self.titleLabel && _browserOptions.title
  1246. && !_browserOptions.title[kThemeableBrowserPropStaticText]
  1247. && [self getBoolFromDict:_browserOptions.title withKey:kThemeableBrowserPropShowPageTitle]) {
  1248. // Update title text to page title when title is shown and we are not
  1249. // required to show a static text.
  1250. self.titleLabel.text = [self.webView stringByEvaluatingJavaScriptFromString:@"document.title"];
  1251. }
  1252. [self.spinner stopAnimating];
  1253. // Work around a bug where the first time a PDF is opened, all UIWebViews
  1254. // reload their User-Agent from NSUserDefaults.
  1255. // This work-around makes the following assumptions:
  1256. // 1. The app has only a single Cordova Webview. If not, then the app should
  1257. // take it upon themselves to load a PDF in the background as a part of
  1258. // their start-up flow.
  1259. // 2. That the PDF does not require any additional network requests. We change
  1260. // the user-agent here back to that of the CDVViewController, so requests
  1261. // from it must pass through its white-list. This *does* break PDFs that
  1262. // contain links to other remote PDF/websites.
  1263. // More info at https://issues.apache.org/jira/browse/CB-2225
  1264. BOOL isPDF = [@"true" isEqualToString :[theWebView stringByEvaluatingJavaScriptFromString:@"document.body==null"]];
  1265. if (isPDF) {
  1266. [CDVUserAgentUtil setUserAgent:_prevUserAgent lockToken:_userAgentLockToken];
  1267. }
  1268. [self.navigationDelegate webViewDidFinishLoad:theWebView];
  1269. }
  1270. - (void)webView:(UIWebView*)theWebView didFailLoadWithError:(NSError*)error
  1271. {
  1272. [self updateButton:theWebView];
  1273. [self.spinner stopAnimating];
  1274. self.addressLabel.text = NSLocalizedString(@"Load Error", nil);
  1275. [self.navigationDelegate webView:theWebView didFailLoadWithError:error];
  1276. }
  1277. - (void)updateButton:(UIWebView*)theWebView
  1278. {
  1279. if (self.backButton) {
  1280. self.backButton.enabled = _browserOptions.backButtonCanClose || theWebView.canGoBack;
  1281. }
  1282. if (self.forwardButton) {
  1283. self.forwardButton.enabled = theWebView.canGoForward;
  1284. }
  1285. }
  1286. /**
  1287. * The reason why this method exists at all is because UIWebView is quite
  1288. * terrible with dealing this hash change, which IS a history change. However
  1289. * when moving to a new hash, only shouldStartLoadWithRequest will be called.
  1290. * Even then it's being called too early such that canGoback and canGoForward
  1291. * hasn't been updated yet. What makes it worse is that when navigating history
  1292. * involving hash by goBack and goForward, no callback is called at all, so we
  1293. * will have to depend on the back and forward button to give us hints when to
  1294. * change button states.
  1295. */
  1296. - (void)updateButtonDelayed:(UIWebView*)theWebView
  1297. {
  1298. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
  1299. [self updateButton:theWebView];
  1300. });
  1301. }
  1302. #pragma mark CDVScreenOrientationDelegate
  1303. - (BOOL)shouldAutorotate
  1304. {
  1305. if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotate)]) {
  1306. return [self.orientationDelegate shouldAutorotate];
  1307. }
  1308. return YES;
  1309. }
  1310. - (NSUInteger)supportedInterfaceOrientations
  1311. {
  1312. if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(supportedInterfaceOrientations)]) {
  1313. return [self.orientationDelegate supportedInterfaceOrientations];
  1314. }
  1315. return 1 << UIInterfaceOrientationPortrait;
  1316. }
  1317. - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
  1318. {
  1319. if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotateToInterfaceOrientation:)]) {
  1320. return [self.orientationDelegate shouldAutorotateToInterfaceOrientation:interfaceOrientation];
  1321. }
  1322. return YES;
  1323. }
  1324. + (UIColor *)colorFromRGBA:(NSString *)rgba {
  1325. unsigned rgbaVal = 0;
  1326. if ([[rgba substringWithRange:NSMakeRange(0, 1)] isEqualToString:@"#"]) {
  1327. // First char is #, get rid of that.
  1328. rgba = [rgba substringFromIndex:1];
  1329. }
  1330. if (rgba.length < 8) {
  1331. // If alpha is not given, just append ff.
  1332. rgba = [NSString stringWithFormat:@"%@ff", rgba];
  1333. }
  1334. NSScanner *scanner = [NSScanner scannerWithString:rgba];
  1335. [scanner setScanLocation:0];
  1336. [scanner scanHexInt:&rgbaVal];
  1337. return [UIColor colorWithRed:(rgbaVal >> 24 & 0xFF) / 255.0f
  1338. green:(rgbaVal >> 16 & 0xFF) / 255.0f
  1339. blue:(rgbaVal >> 8 & 0xFF) / 255.0f
  1340. alpha:(rgbaVal & 0xFF) / 255.0f];
  1341. }
  1342. @end
  1343. @implementation CDVThemeableBrowserOptions
  1344. - (id)init
  1345. {
  1346. if (self = [super init]) {
  1347. // default values
  1348. self.location = YES;
  1349. self.closebuttoncaption = nil;
  1350. self.toolbarposition = kThemeableBrowserToolbarBarPositionBottom;
  1351. self.clearcache = NO;
  1352. self.clearsessioncache = NO;
  1353. self.zoom = YES;
  1354. self.mediaplaybackrequiresuseraction = NO;
  1355. self.allowinlinemediaplayback = NO;
  1356. self.keyboarddisplayrequiresuseraction = YES;
  1357. self.suppressesincrementalrendering = NO;
  1358. self.hidden = NO;
  1359. self.disallowoverscroll = NO;
  1360. self.statusbar = nil;
  1361. self.toolbar = nil;
  1362. self.title = nil;
  1363. self.backButton = nil;
  1364. self.forwardButton = nil;
  1365. self.closeButton = nil;
  1366. self.menu = nil;
  1367. self.backButtonCanClose = NO;
  1368. self.disableAnimation = NO;
  1369. self.fullscreen = NO;
  1370. }
  1371. return self;
  1372. }
  1373. @end
  1374. #pragma mark CDVScreenOrientationDelegate
  1375. @implementation CDVThemeableBrowserNavigationController : UINavigationController
  1376. - (BOOL)shouldAutorotate
  1377. {
  1378. if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotate)]) {
  1379. return [self.orientationDelegate shouldAutorotate];
  1380. }
  1381. return YES;
  1382. }
  1383. - (NSUInteger)supportedInterfaceOrientations
  1384. {
  1385. if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(supportedInterfaceOrientations)]) {
  1386. return [self.orientationDelegate supportedInterfaceOrientations];
  1387. }
  1388. return 1 << UIInterfaceOrientationPortrait;
  1389. }
  1390. - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
  1391. {
  1392. if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotateToInterfaceOrientation:)]) {
  1393. return [self.orientationDelegate shouldAutorotateToInterfaceOrientation:interfaceOrientation];
  1394. }
  1395. return YES;
  1396. }
  1397. @end