// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/safe_mode/ui_bundled/safe_mode_view_controller.h"
#import <QuartzCore/QuartzCore.h>
#import "base/strings/sys_string_conversions.h"
#import "ios/chrome/browser/crash_report/model/crash_helper.h"
#import "ios/chrome/browser/safe_mode/model/safe_mode_crashing_modules_config.h"
#import "ios/chrome/browser/safe_mode/model/safe_mode_util.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/common/crash_report/crash_helper.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/util/button_util.h"
#import "ios/chrome/grit/ios_branded_strings.h"
#import "ui/base/device_form_factor.h"
#import "ui/gfx/ios/NSString+CrStringDrawing.h"
namespace {
const CGFloat kVerticalSpacing = 20;
const CGFloat kUploadProgressSpacing = 5;
const NSTimeInterval kUploadPumpInterval = 0.1;
const NSTimeInterval kUploadTotalTime = 5;
} // anonymous namespace
@interface SafeModeViewController ()
// Returns `YES` if any third-party modifications are detected.
+ (BOOL)detectedThirdPartyMods;
// Returns `YES` if there are crash reports to upload.
+ (BOOL)hasReportToUpload;
// Returns a message explaining which, if any, 3rd party modules were detected
// that may cause Chrome to crash.
- (NSString*)startupCrashModuleText;
// Starts timer to update progress bar for crash report upload.
- (void)startUploadProgress;
// Updates progress bar for crash report upload.
- (void)pumpUploadProgress;
// Called when user taps on "Resume Chrome" button. Notifies the delegate to
// attempt to start the browser.
- (void)startBrowserFromSafeMode;
@end
@implementation SafeModeViewController {
__weak id<SafeModeViewControllerDelegate> _delegate;
UIView* _innerView;
UIButton* _startButton;
UILabel* _uploadDescription;
UIProgressView* _uploadProgress;
NSDate* _uploadStartTime;
NSTimer* _uploadTimer;
}
- (id)initWithDelegate:(id<SafeModeViewControllerDelegate>)delegate {
self = [super init];
if (self) {
_delegate = delegate;
}
return self;
}
+ (BOOL)hasSuggestions {
if ([SafeModeViewController detectedThirdPartyMods])
return YES;
static dispatch_once_t once_token = 0;
dispatch_once(&once_token, ^{
crash_helper::ProcessIntermediateReportsForSafeMode();
});
return [SafeModeViewController hasReportToUpload];
}
+ (BOOL)detectedThirdPartyMods {
std::vector<std::string> thirdPartyMods = safe_mode_util::GetLoadedImages(
"/Library/MobileSubstrate/DynamicLibraries/");
return (thirdPartyMods.size() > 0);
}
+ (BOOL)hasReportToUpload {
// If uploading is enabled and more than one report has stacked up, then we
// assume that the app may be in a state that is preventing crash report
// uploads before crashing again.
return crash_helper::common::UserEnabledUploading() &&
crash_helper::GetPendingCrashReportCount() > 1;
}
// Return any jailbroken library that appears in SafeModeCrashingModulesConfig.
- (NSArray*)startupCrashModules {
std::vector<std::string> modules = safe_mode_util::GetLoadedImages(
"/Library/MobileSubstrate/DynamicLibraries/");
NSMutableArray* array = [NSMutableArray arrayWithCapacity:modules.size()];
SafeModeCrashingModulesConfig* config =
[SafeModeCrashingModulesConfig sharedInstance];
for (size_t i = 0; i < modules.size(); i++) {
NSString* path = base::SysUTF8ToNSString(modules[i]);
NSString* friendlyName = [config startupCrashModuleFriendlyName:path];
if (friendlyName != nil)
[array addObject:friendlyName];
}
return array;
}
// Since we are still supporting iOS5, this is a helper for basic flow layout.
- (void)centerView:(UIView*)view afterView:(UIView*)afterView {
CGPoint center = [view center];
center.x = [_innerView frame].size.width / 2;
[view setCenter:center];
if (afterView) {
CGRect frame = view.frame;
frame.origin.y = CGRectGetMaxY(afterView.frame) + kVerticalSpacing;
view.frame = frame;
}
}
- (NSString*)startupCrashModuleText {
NSArray* knownModules = [self startupCrashModules];
if ([knownModules count]) {
NSString* wrongText =
NSLocalizedString(@"IDS_IOS_SAFE_MODE_NAMED_TWEAKS_FOUND", @"");
NSMutableString* text = [NSMutableString stringWithString:wrongText];
[text appendString:@"\n"];
for (NSString* module in knownModules) {
[text appendFormat:@"\n %@", module];
}
return text;
} else if ([SafeModeViewController detectedThirdPartyMods]) {
return NSLocalizedString(@"IDS_IOS_SAFE_MODE_TWEAKS_FOUND", @"");
} else {
return NSLocalizedString(@"IDS_IOS_SAFE_MODE_UNKNOWN_CAUSE", @"");
}
}
- (void)viewDidLoad {
[super viewDidLoad];
// Width of the inner view on iPhone.
const CGFloat kIPhoneWidth = 250;
// Width of the inner view on iPad.
const CGFloat kIPadWidth = 350;
// Horizontal buffer.
const CGFloat kHorizontalSpacing = 20;
self.view.autoresizesSubviews = YES;
CGRect mainBounds = [[UIScreen mainScreen] bounds];
// SafeModeViewController only supports portrait orientation (see
// implementation of supportedInterfaceOrientations: below) but if the app is
// launched from landscape mode (e.g. iPad or iPhone 6+) then the mainScreen's
// bounds will still be landscape at this point. Swap the height and width
// here so that the dimensions will be correct once the app rotates to
// portrait.
if (IsLandscape(self.view.window)) {
mainBounds.size = CGSizeMake(mainBounds.size.height, mainBounds.size.width);
}
UIScrollView* scrollView = [[UIScrollView alloc] initWithFrame:mainBounds];
self.view = scrollView;
self.view.backgroundColor = [UIColor colorNamed:kBackgroundColor];
const CGFloat kIPadInset =
(mainBounds.size.width - kIPadWidth - kHorizontalSpacing) / 2;
const CGFloat widthInset =
(ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET)
? kIPadInset
: kHorizontalSpacing;
_innerView = [[UIView alloc]
initWithFrame:CGRectInset(mainBounds, widthInset, kVerticalSpacing * 2)];
[scrollView addSubview:_innerView];
UIImage* fatalImage = [[UIImage imageNamed:@"fatal_error.png"]
imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
UIImageView* imageView = [[UIImageView alloc] initWithImage:fatalImage];
imageView.tintColor = [UIColor colorNamed:kPlaceholderImageTintColor];
// Shift the image down a bit.
CGRect imageFrame = [imageView frame];
imageFrame.origin.y = kVerticalSpacing;
[imageView setFrame:imageFrame];
[self centerView:imageView afterView:nil];
[_innerView addSubview:imageView];
UILabel* awSnap = [[UILabel alloc] init];
[awSnap setText:NSLocalizedString(@"IDS_IOS_SAFE_MODE_AW_SNAP", @"")];
awSnap.textColor = [UIColor colorNamed:kTextPrimaryColor];
[awSnap setFont:[UIFont boldSystemFontOfSize:21]];
[awSnap sizeToFit];
[self centerView:awSnap afterView:imageView];
[_innerView addSubview:awSnap];
UILabel* description = [[UILabel alloc] init];
[description setText:[self startupCrashModuleText]];
description.textColor = [UIColor colorNamed:kTextSecondaryColor];
[description setTextAlignment:NSTextAlignmentCenter];
[description setNumberOfLines:0];
[description setLineBreakMode:NSLineBreakByWordWrapping];
CGRect frame = [description frame];
frame.size.width =
(ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET)
? kIPadWidth
: kIPhoneWidth;
CGSize maxSize = CGSizeMake(frame.size.width, 999999.0f);
frame.size.height =
[[description text] cr_boundingSizeWithSize:maxSize
font:[description font]]
.height;
[description setFrame:frame];
[self centerView:description afterView:awSnap];
[_innerView addSubview:description];
_startButton = PrimaryActionButton(YES);
NSString* startText =
NSLocalizedString(@"IDS_IOS_SAFE_MODE_RELOAD_CHROME", @"");
SetConfigurationTitle(_startButton, startText);
UIButtonConfiguration* buttonConfiguration = _startButton.configuration;
buttonConfiguration.titleAlignment =
UIButtonConfigurationTitleAlignmentCenter;
buttonConfiguration.titleLineBreakMode = NSLineBreakByWordWrapping;
_startButton.configuration = buttonConfiguration;
frame = [_startButton frame];
frame.size.width =
(ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET)
? kIPadWidth
: kIPhoneWidth;
frame.size.height = _startButton.intrinsicContentSize.height;
[_startButton setFrame:frame];
[_startButton addTarget:self
action:@selector(startBrowserFromSafeMode)
forControlEvents:UIControlEventTouchUpInside];
[self centerView:_startButton afterView:description];
[_innerView addSubview:_startButton];
UIView* lastView = _startButton;
if ([SafeModeViewController hasReportToUpload]) {
crash_helper::StartUploadingReportsInRecoveryMode();
// If there are no jailbreak modifications, then present the "Sending crash
// report..." UI.
if (![SafeModeViewController detectedThirdPartyMods]) {
[_startButton setEnabled:NO];
_uploadDescription = [[UILabel alloc] init];
[_uploadDescription
setText:NSLocalizedString(@"IDS_IOS_SAFE_MODE_SENDING_CRASH_REPORT",
@"")];
[_uploadDescription setFont:[UIFont systemFontOfSize:13]];
_uploadDescription.textColor = [UIColor colorNamed:kTextSecondaryColor];
[_uploadDescription sizeToFit];
[self centerView:_uploadDescription afterView:_startButton];
[_innerView addSubview:_uploadDescription];
_uploadProgress = [[UIProgressView alloc]
initWithProgressViewStyle:UIProgressViewStyleDefault];
[self centerView:_uploadProgress afterView:nil];
frame = [_uploadProgress frame];
frame.origin.y =
CGRectGetMaxY([_uploadDescription frame]) + kUploadProgressSpacing;
[_uploadProgress setFrame:frame];
[_innerView addSubview:_uploadProgress];
lastView = _uploadProgress;
[self startUploadProgress];
}
}
CGSize scrollSize =
CGSizeMake(mainBounds.size.width,
CGRectGetMaxY([lastView frame]) + kVerticalSpacing);
frame = [_innerView frame];
frame.size.height = scrollSize.height;
[_innerView setFrame:frame];
scrollSize.height += frame.origin.y;
[scrollView setContentSize:scrollSize];
}
- (NSUInteger)supportedInterfaceOrientations {
return UIInterfaceOrientationMaskPortrait;
}
#pragma mark - Private
- (void)startUploadProgress {
_uploadStartTime = [NSDate date];
_uploadTimer =
[NSTimer scheduledTimerWithTimeInterval:kUploadPumpInterval
target:self
selector:@selector(pumpUploadProgress)
userInfo:nil
repeats:YES];
}
- (void)pumpUploadProgress {
NSTimeInterval elapsed =
[[NSDate date] timeIntervalSinceDate:_uploadStartTime];
// Theoretically we could stop early when the value returned by
// crash_helper::GetCrashReportCount() changes, but this is simpler. If we
// decide to look for a change in crash report count, then we also probably
// want to replace the UIProgressView with a UIActivityIndicatorView.
if (elapsed <= kUploadTotalTime) {
[_uploadProgress setProgress:elapsed / kUploadTotalTime animated:YES];
} else {
[_uploadTimer invalidate];
[_startButton setEnabled:YES];
[_uploadDescription
setText:NSLocalizedString(@"IDS_IOS_SAFE_MODE_CRASH_REPORT_SENT", @"")];
[_uploadDescription sizeToFit];
[self centerView:_uploadDescription afterView:_startButton];
[_uploadProgress setHidden:YES];
}
}
- (void)startBrowserFromSafeMode {
[_delegate startBrowserFromSafeMode];
}
@end