From 3ca5ec27261d4e9bef0c12e89c6f39b0d1ad6305 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 17 Jan 2018 15:41:08 -0500 Subject: [PATCH] Ensure media-library permissions when accessing media library // FREEBIE --- Signal/src/ViewControllers/AvatarViewHelper.m | 38 ++++++--- .../src/ViewControllers/ContactsPicker.swift | 4 +- .../ConversationViewController.m | 32 +++---- .../src/ViewControllers/HomeViewController.m | 4 +- .../src/util/UIViewController+Permissions.h | 4 +- .../src/util/UIViewController+Permissions.m | 85 ++++++++++++++++++- .../translations/en.lproj/Localizable.strings | 9 +- SignalMessaging/Views/ContactsViewHelper.m | 5 +- SignalMessaging/views/CommonStrings.swift | 4 +- SignalMessaging/views/OWSAlerts.swift | 4 +- 10 files changed, 142 insertions(+), 47 deletions(-) diff --git a/Signal/src/ViewControllers/AvatarViewHelper.m b/Signal/src/ViewControllers/AvatarViewHelper.m index dbb11e00c..f58f34b30 100644 --- a/Signal/src/ViewControllers/AvatarViewHelper.m +++ b/Signal/src/ViewControllers/AvatarViewHelper.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "AvatarViewHelper.h" @@ -69,17 +69,22 @@ NS_ASSUME_NONNULL_BEGIN OWSAssertIsOnMainThread(); OWSAssert(self.delegate); - UIImagePickerController *picker = [[UIImagePickerController alloc] init]; - picker.delegate = self; - picker.allowsEditing = NO; - picker.sourceType = UIImagePickerControllerSourceTypeCamera; + [self.delegate.fromViewController ows_askForCameraPermissions:^(BOOL granted) { + if (!granted) { + DDLogWarn(@"%@ Camera permission denied.", self.logTag); + return; + } + + UIImagePickerController *picker = [UIImagePickerController new]; + picker.delegate = self; + picker.allowsEditing = NO; + picker.sourceType = UIImagePickerControllerSourceTypeCamera; + picker.mediaTypes = @[ (__bridge NSString *)kUTTypeImage ]; - if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { - picker.mediaTypes = [[NSArray alloc] initWithObjects:(NSString *)kUTTypeImage, nil]; [self.delegate.fromViewController presentViewController:picker animated:YES completion:[UIUtil modalCompletionBlock]]; - } + }]; } - (void)chooseFromLibrary @@ -87,16 +92,21 @@ NS_ASSUME_NONNULL_BEGIN OWSAssertIsOnMainThread(); OWSAssert(self.delegate); - UIImagePickerController *picker = [[UIImagePickerController alloc] init]; - picker.delegate = self; - picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; + [self.delegate.fromViewController ows_askForMediaLibraryPermissions:^(BOOL granted) { + if (!granted) { + DDLogWarn(@"%@ Media Library permission denied.", self.logTag); + return; + } + + UIImagePickerController *picker = [UIImagePickerController new]; + picker.delegate = self; + picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; + picker.mediaTypes = @[ (__bridge NSString *)kUTTypeImage ]; - if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary]) { - picker.mediaTypes = [[NSArray alloc] initWithObjects:(NSString *)kUTTypeImage, nil]; [self.delegate.fromViewController presentViewController:picker animated:YES completion:[UIUtil modalCompletionBlock]]; - } + }]; } /* diff --git a/Signal/src/ViewControllers/ContactsPicker.swift b/Signal/src/ViewControllers/ContactsPicker.swift index 3deb4eb5f..2236b6242 100644 --- a/Signal/src/ViewControllers/ContactsPicker.swift +++ b/Signal/src/ViewControllers/ContactsPicker.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // // Originally based on EPContacts @@ -161,7 +161,7 @@ open class ContactsPicker: OWSViewController, UITableViewDelegate, UITableViewDa }) alert.addAction(cancelAction) - let settingsText = NSLocalizedString("OPEN_SETTINGS_BUTTON", comment:"Button text which opens the settings app") + let settingsText = CommonStrings.openSettingsButton let openSettingsAction = UIAlertAction(title: settingsText, style: .default, handler: { (_) in UIApplication.shared.openSystemSettings() }) diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 96da9da63..0db85d2aa 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -2466,18 +2466,18 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { { [self ows_askForCameraPermissions:^(BOOL granted) { if (!granted) { + DDLogWarn(@"%@ camera permission denied.", self.logTag); return; } - UIImagePickerController *picker = [[UIImagePickerController alloc] init]; + + UIImagePickerController *picker = [UIImagePickerController new]; picker.sourceType = UIImagePickerControllerSourceTypeCamera; picker.mediaTypes = @[ (__bridge NSString *)kUTTypeImage, (__bridge NSString *)kUTTypeMovie ]; picker.allowsEditing = NO; picker.delegate = self; - dispatch_async(dispatch_get_main_queue(), ^{ - [self dismissKeyBoard]; - [self presentViewController:picker animated:YES completion:[UIUtil modalCompletionBlock]]; - }); + [self dismissKeyBoard]; + [self presentViewController:picker animated:YES completion:[UIUtil modalCompletionBlock]]; }]; } @@ -2485,18 +2485,20 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { { OWSAssertIsOnMainThread(); - if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary]) { - DDLogError(@"PhotoLibrary ImagePicker source not available"); - return; - } + [self ows_askForMediaLibraryPermissions:^(BOOL granted) { + if (!granted) { + DDLogWarn(@"%@ Media Library permission denied.", self.logTag); + return; + } - UIImagePickerController *picker = [[UIImagePickerController alloc] init]; - picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; - picker.delegate = self; - picker.mediaTypes = @[ (__bridge NSString *)kUTTypeImage, (__bridge NSString *)kUTTypeMovie ]; + UIImagePickerController *picker = [UIImagePickerController new]; + picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; + picker.delegate = self; + picker.mediaTypes = @[ (__bridge NSString *)kUTTypeImage, (__bridge NSString *)kUTTypeMovie ]; - [self dismissKeyBoard]; - [self presentViewController:picker animated:YES completion:[UIUtil modalCompletionBlock]]; + [self dismissKeyBoard]; + [self presentViewController:picker animated:YES completion:[UIUtil modalCompletionBlock]]; + }]; } /* diff --git a/Signal/src/ViewControllers/HomeViewController.m b/Signal/src/ViewControllers/HomeViewController.m index 90e59c2e5..63984c626 100644 --- a/Signal/src/ViewControllers/HomeViewController.m +++ b/Signal/src/ViewControllers/HomeViewController.m @@ -314,8 +314,8 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; imageEdgeInsets.top = round((kBarButtonSize - image.size.height) * 0.5f); imageEdgeInsets.bottom = round(kBarButtonSize - (image.size.height + imageEdgeInsets.top)); button.imageEdgeInsets = imageEdgeInsets; - button.accessibilityLabel - = NSLocalizedString(@"OPEN_SETTINGS_BUTTON", "Label for button which opens the settings UI"); + button.accessibilityLabel = CommonStrings.openSettingsButton; + [button addTarget:self action:@selector(settingsButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; button.frame = CGRectMake(0, 0, diff --git a/Signal/src/util/UIViewController+Permissions.h b/Signal/src/util/UIViewController+Permissions.h index 7919ad045..9e109d468 100644 --- a/Signal/src/util/UIViewController+Permissions.h +++ b/Signal/src/util/UIViewController+Permissions.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import @@ -9,7 +9,7 @@ NS_ASSUME_NONNULL_BEGIN @interface UIViewController (Permissions) - (void)ows_askForCameraPermissions:(void (^)(BOOL granted))callback; - +- (void)ows_askForMediaLibraryPermissions:(void (^)(BOOL granted))callbackParam; - (void)ows_askForMicrophonePermissions:(void (^)(BOOL granted))callback; @end diff --git a/Signal/src/util/UIViewController+Permissions.m b/Signal/src/util/UIViewController+Permissions.m index a9dc43888..dbc8db66b 100644 --- a/Signal/src/util/UIViewController+Permissions.m +++ b/Signal/src/util/UIViewController+Permissions.m @@ -1,10 +1,11 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "Signal-Swift.h" #import "UIViewController+Permissions.h" #import +#import #import #import @@ -42,10 +43,8 @@ NS_ASSUME_NONNULL_BEGIN message:NSLocalizedString(@"MISSING_CAMERA_PERMISSION_MESSAGE", @"Alert body") preferredStyle:UIAlertControllerStyleAlert]; - NSString *settingsTitle - = NSLocalizedString(@"OPEN_SETTINGS_BUTTON", @"Button text which opens the settings app"); UIAlertAction *openSettingsAction = - [UIAlertAction actionWithTitle:settingsTitle + [UIAlertAction actionWithTitle:CommonStrings.openSettingsButton style:UIAlertActionStyleDefault handler:^(UIAlertAction *_Nonnull action) { [[UIApplication sharedApplication] openSystemSettings]; @@ -72,6 +71,84 @@ NS_ASSUME_NONNULL_BEGIN } } +- (void)ows_askForMediaLibraryPermissions:(void (^)(BOOL granted))callbackParam +{ + DDLogVerbose(@"[%@] ows_askForMediaLibraryPermissions", NSStringFromClass(self.class)); + + // Ensure callback is invoked on main thread. + void (^completionCallback)(BOOL) = ^(BOOL granted) { + DispatchMainThreadSafe(^{ + callbackParam(granted); + }); + }; + + void (^presentSettingsDialog)(void) = ^(void) { + UIAlertController *alert = [UIAlertController + alertControllerWithTitle:NSLocalizedString(@"MISSING_MEDIA_LIBRARY_PERMISSION_TITLE", + @"Alert title when user has previously denied media library access") + message:NSLocalizedString(@"MISSING_MEDIA_LIBRARY_PERMISSION_MESSAGE", + @"Alert body when user has previously denied media library access") + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction *openSettingsAction = + [UIAlertAction actionWithTitle:CommonStrings.openSettingsButton + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *_Nonnull action) { + [[UIApplication sharedApplication] openSystemSettings]; + completionCallback(NO); + }]; + [alert addAction:openSettingsAction]; + + UIAlertAction *dismissAction = [UIAlertAction actionWithTitle:CommonStrings.dismissButton + style:UIAlertActionStyleCancel + handler:^(UIAlertAction *action) { + completionCallback(NO); + }]; + [alert addAction:dismissAction]; + + [self presentViewController:alert animated:YES completion:nil]; + }; + + if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground) { + DDLogError(@"Skipping media library permissions request when app is in background."); + completionCallback(NO); + return; + } + + if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary]) { + DDLogError(@"%@ PhotoLibrary ImagePicker source not available", self.logTag); + completionCallback(NO); + } + + PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus]; + + switch (status) { + case PHAuthorizationStatusAuthorized: { + completionCallback(YES); + return; + } + case PHAuthorizationStatusDenied: { + presentSettingsDialog(); + return; + } + case PHAuthorizationStatusNotDetermined: { + [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus newStatus) { + if (newStatus == PHAuthorizationStatusAuthorized) { + completionCallback(YES); + } else { + presentSettingsDialog(); + } + }]; + return; + } + case PHAuthorizationStatusRestricted: { + // when does this happen? + OWSFail(@"PHAuthorizationStatusRestricted"); + return; + } + } +} + - (void)ows_askForMicrophonePermissions:(void (^)(BOOL granted))callbackParam { DDLogVerbose(@"[%@] ows_askForMicrophonePermissions", NSStringFromClass(self.class)); diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index a77024537..3e693ec26 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -1041,6 +1041,12 @@ Alert title when camera is not authorized */ "MISSING_CAMERA_PERMISSION_TITLE" = "Signal needs to access your camera."; +/* Alert body when user has previously denied media library access */ +"MISSING_MEDIA_LIBRARY_PERMISSION_MESSAGE" = "You can grant this permission in the Settings app."; + +/* Alert title when user has previously denied media library access */ +"MISSING_MEDIA_LIBRARY_PERMISSION_TITLE" = "Signal Requires Access to your Media Library to do this."; + /* notification title. Embeds {{caller's name or phone number}} */ "MSGVIEW_MISSED_CALL_WITH_NAME" = "Missed call from %@."; @@ -1146,8 +1152,7 @@ /* No comment provided by engineer. */ "OK" = "OK"; -/* Button text which opens the settings app - Label for button which opens the settings UI */ +/* Button text which opens the settings app */ "OPEN_SETTINGS_BUTTON" = "Settings"; /* Info Message when {{other user}} disables or doesn't support disappearing messages */ diff --git a/SignalMessaging/Views/ContactsViewHelper.m b/SignalMessaging/Views/ContactsViewHelper.m index ae58ff3d7..858a6c04b 100644 --- a/SignalMessaging/Views/ContactsViewHelper.m +++ b/SignalMessaging/Views/ContactsViewHelper.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "ContactsViewHelper.h" @@ -282,8 +282,7 @@ NS_ASSUME_NONNULL_BEGIN style:UIAlertActionStyleCancel handler:nil]]; - [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OPEN_SETTINGS_BUTTON", - @"Button text which opens the settings app") + [alertController addAction:[UIAlertAction actionWithTitle:CommonStrings.openSettingsButton style:UIAlertActionStyleDefault handler:^(UIAlertAction *_Nonnull action) { [CurrentAppContext() openSystemSettings]; diff --git a/SignalMessaging/views/CommonStrings.swift b/SignalMessaging/views/CommonStrings.swift index 99d546b9c..799ea97a2 100644 --- a/SignalMessaging/views/CommonStrings.swift +++ b/SignalMessaging/views/CommonStrings.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // import Foundation @@ -15,6 +15,8 @@ import Foundation static public let cancelButton = NSLocalizedString("TXT_CANCEL_TITLE", comment:"Label for the cancel button in an alert or action sheet.") @objc static public let retryButton = NSLocalizedString("RETRY_BUTTON_TEXT", comment:"Generic text for button that retries whatever the last action was.") + @objc + static public let openSettingsButton = NSLocalizedString("OPEN_SETTINGS_BUTTON", comment: "Button text which opens the settings app") } @objc public class MessageStrings: NSObject { diff --git a/SignalMessaging/views/OWSAlerts.swift b/SignalMessaging/views/OWSAlerts.swift index 898830b0f..0a21d58ae 100644 --- a/SignalMessaging/views/OWSAlerts.swift +++ b/SignalMessaging/views/OWSAlerts.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // import Foundation @@ -14,7 +14,7 @@ import Foundation let alertMessage = NSLocalizedString("CALL_AUDIO_PERMISSION_MESSAGE", comment:"Alert message when calling and permissions for microphone are missing") let alertController = UIAlertController(title: alertTitle, message: alertMessage, preferredStyle: .alert) let dismissAction = UIAlertAction(title: CommonStrings.dismissButton, style: .cancel) - let settingsString = NSLocalizedString("OPEN_SETTINGS_BUTTON", comment: "Button text which opens the settings app") + let settingsString = CommonStrings.openSettingsButton let settingsAction = UIAlertAction(title: settingsString, style: .default) { _ in CurrentAppContext().openSystemSettings() }