don't request contacts until necessary

Most commonly this will be after hitting the "compose" button

But also we'll do it in the SignalViewController once you've received a
message.

- get rid blocking contacts nag
- use Contacts framework simplifies logic
- remove dead AB code

// FREEBIE
This commit is contained in:
Michael Kirk 2017-05-01 14:28:37 -04:00
parent 931b6b4200
commit b24cf29189
15 changed files with 232 additions and 403 deletions

View File

@ -136,7 +136,7 @@ CHECKOUT OPTIONS:
:commit: 7054e4b13ee5bcd6d524adb6dc9a726e8c466308
:git: https://github.com/WhisperSystems/JSQMessagesViewController.git
SignalServiceKit:
:commit: d25a934039e3e14dcb128bd7b1648e3f514bbbf6
:commit: e336e0b34a40178ad0d96767fe9dc7f37ba97dc0
:git: https://github.com/WhisperSystems/SignalServiceKit.git
SocketRocket:
:commit: 877ac7438be3ad0b45ef5ca3969574e4b97112bf

View File

@ -105,6 +105,7 @@
453D28B71D32BA5F00D523F0 /* OWSDisplayedMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 453D28B61D32BA5F00D523F0 /* OWSDisplayedMessage.m */; };
453D28BA1D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m in Sources */ = {isa = PBXBuildFile; fileRef = 453D28B91D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m */; };
453D28BB1D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m in Sources */ = {isa = PBXBuildFile; fileRef = 453D28B91D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m */; };
4542F0941EB9372700C7EE92 /* SystemContactsFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4542F0931EB9372700C7EE92 /* SystemContactsFetcher.swift */; };
45464DBC1DFA041F001D3FD6 /* DataChannelMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45464DBB1DFA041F001D3FD6 /* DataChannelMessage.swift */; };
45666EC61D99483D008FE134 /* OWSAvatarBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 45666EC51D99483D008FE134 /* OWSAvatarBuilder.m */; };
45666EC91D994C0D008FE134 /* OWSGroupAvatarBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 45666EC81D994C0D008FE134 /* OWSGroupAvatarBuilder.m */; };
@ -504,6 +505,7 @@
453D28B61D32BA5F00D523F0 /* OWSDisplayedMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSDisplayedMessage.m; sourceTree = "<group>"; };
453D28B81D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessagesBubblesSizeCalculator.h; sourceTree = "<group>"; };
453D28B91D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessagesBubblesSizeCalculator.m; sourceTree = "<group>"; };
4542F0931EB9372700C7EE92 /* SystemContactsFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemContactsFetcher.swift; sourceTree = "<group>"; };
45464DBB1DFA041F001D3FD6 /* DataChannelMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataChannelMessage.swift; sourceTree = "<group>"; };
454B35071D08EED80026D658 /* mk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = mk; path = translations/mk.lproj/Localizable.strings; sourceTree = "<group>"; };
45666EC41D99483D008FE134 /* OWSAvatarBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSAvatarBuilder.h; sourceTree = "<group>"; };
@ -1192,6 +1194,7 @@
76EB040918170B33006006FC /* OWSContactsManager.m */,
45843D1D1D2236B30013E85A /* OWSContactsSearcher.h */,
45843D1E1D2236B30013E85A /* OWSContactsSearcher.m */,
4542F0931EB9372700C7EE92 /* SystemContactsFetcher.swift */,
);
path = contact;
sourceTree = "<group>";
@ -2010,6 +2013,7 @@
450DF2091E0DD2C6003D14BE /* UserNotificationsAdaptee.swift in Sources */,
340CB2241EAC155C0001CAA1 /* ContactsViewHelper.m in Sources */,
45CD81F21DC03A22004C9430 /* OWSLogger.m in Sources */,
4542F0941EB9372700C7EE92 /* SystemContactsFetcher.swift in Sources */,
B60C16651988999D00E97A6C /* VersionMigrations.m in Sources */,
B97940271832BD2400BD66CB /* UIUtil.m in Sources */,
34B3F8791E8DF1700035BE1A /* CountryCodeViewController.m in Sources */,
@ -2449,7 +2453,7 @@
"\"$(SRCROOT)/Libraries\"/**",
);
INFOPLIST_FILE = "$(SRCROOT)/Signal/Signal-Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
@ -2509,7 +2513,7 @@
"\"$(SRCROOT)/Libraries\"/**",
);
INFOPLIST_FILE = "$(SRCROOT)/Signal/Signal-Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",

View File

@ -113,11 +113,6 @@ static NSString *const kURLHostVerifyPrefix = @"verify";
return YES;
}
if ([TSAccountManager isRegistered]) {
[Environment.getCurrent.contactsManager doAfterEnvironmentInitSetup];
}
UIStoryboard *storyboard;
if ([TSAccountManager isRegistered]) {
storyboard = [UIStoryboard storyboardWithName:AppDelegateStoryboardMain bundle:[NSBundle mainBundle]];
@ -171,7 +166,7 @@ static NSString *const kURLHostVerifyPrefix = @"verify";
// sent before the app exited should be marked as failures.
[[[OWSFailedMessagesJob alloc] initWithStorageManager:[TSStorageManager sharedManager]] run];
[[[OWSFailedAttachmentDownloadsJob alloc] initWithStorageManager:[TSStorageManager sharedManager]] run];
[AppStoreRating setupRatingLibrary];
}];
@ -405,8 +400,12 @@ static NSString *const kURLHostVerifyPrefix = @"verify";
// can't verify in production env due to code
// signing.
[TSSocketManager requestSocketOpen];
[[Environment getCurrent].contactsManager verifyABPermission];
dispatch_async(dispatch_get_main_queue(), ^{
[[Environment getCurrent]
.contactsManager fetchSystemContactsIfAlreadyAuthorized];
});
// This will fetch new messages, if we're using domain
// fronting.
[[PushManager sharedManager] applicationDidBecomeActive];

View File

@ -1,10 +1,11 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
// Originally based on EPContacts
//
// Created by Prabaharan Elangovan on 12/10/15.
// Parts Copyright © 2015 Prabaharan Elangovan. All rights reserved.
//
// Modified for Signal by Michael Kirk on 11/25/2016
// Parts Copyright © 2016 Open Whisper Systems. All rights reserved.
// Parts Copyright © 2015 Prabaharan Elangovan. All rights reserved
import UIKit
import Contacts
@ -27,7 +28,7 @@ public extension ContactsPickerDelegate {
func contactsPicker(_: ContactsPicker, shouldSelectContact contact: Contact) -> Bool { return true }
}
public enum SubtitleCellValue{
public enum SubtitleCellValue {
case phoneNumber
case email
}
@ -89,17 +90,17 @@ open class ContactsPicker: UIViewController, UITableViewDelegate, UITableViewDat
func didChangePreferredContentSize() {
self.tableView.reloadData()
}
func initializeBarButtons() {
let cancelButton = UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(onTouchCancelButton))
self.navigationItem.leftBarButtonItem = cancelButton
if multiSelectEnabled {
let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(onTouchDoneButton))
self.navigationItem.rightBarButtonItem = doneButton
}
}
fileprivate func registerContactCell() {
tableView.register(ContactCell.nib, forCellReuseIdentifier: contactCellReuseIdentifier)
}
@ -119,14 +120,14 @@ open class ContactsPicker: UIViewController, UITableViewDelegate, UITableViewDat
convenience public init(delegate: ContactsPickerDelegate?) {
self.init(delegate: delegate, multiSelection: false)
}
convenience public init(delegate: ContactsPickerDelegate?, multiSelection : Bool) {
convenience public init(delegate: ContactsPickerDelegate?, multiSelection: Bool) {
self.init()
multiSelectEnabled = multiSelection
contactsPickerDelegate = delegate
}
convenience public init(delegate: ContactsPickerDelegate?, multiSelection : Bool, subtitleCellType: SubtitleCellValue) {
convenience public init(delegate: ContactsPickerDelegate?, multiSelection: Bool, subtitleCellType: SubtitleCellValue) {
self.init()
multiSelectEnabled = multiSelection
contactsPickerDelegate = delegate
@ -134,24 +135,24 @@ open class ContactsPicker: UIViewController, UITableViewDelegate, UITableViewDat
}
// MARK: - Contact Operations
open func reloadContacts() {
getContacts( onError: { error in
Logger.error("\(self.TAG) failed to reload contacts with error:\(error)")
})
}
func getContacts(onError errorHandler: @escaping (_ error: Error) -> Void) {
func getContacts(onError errorHandler: @escaping (_ error: Error) -> Void) {
switch CNContactStore.authorizationStatus(for: CNEntityType.contacts) {
case CNAuthorizationStatus.denied, CNAuthorizationStatus.restricted:
let title = NSLocalizedString("AB_PERMISSION_MISSING_TITLE", comment: "Alert title when contacts disabled")
let body = NSLocalizedString("ADDRESSBOOK_RESTRICTED_ALERT_BODY", comment: "Alert body when contacts disabled")
let alert = UIAlertController(title: title, message: body, preferredStyle: UIAlertControllerStyle.alert)
let dismissText = NSLocalizedString("DISMISS_BUTTON_TEXT", comment:"")
let okAction = UIAlertAction(title: dismissText, style: UIAlertActionStyle.default, handler: { action in
let okAction = UIAlertAction(title: dismissText, style: UIAlertActionStyle.default, handler: { _ in
let error = NSError(domain: "contactsPickerErrorDomain", code: 1, userInfo: [NSLocalizedDescriptionKey: "No Contacts Access"])
self.contactsPickerDelegate?.contactsPicker(self, didContactFetchFailed: error)
errorHandler(error)
@ -159,7 +160,7 @@ open class ContactsPicker: UIViewController, UITableViewDelegate, UITableViewDat
})
alert.addAction(okAction)
self.present(alert, animated: true, completion: nil)
case CNAuthorizationStatus.notDetermined:
//This case means the user is prompted for the first time for allowing contacts
contactStore.requestAccess(for: CNEntityType.contacts) { (granted, error) -> Void in
@ -170,14 +171,14 @@ open class ContactsPicker: UIViewController, UITableViewDelegate, UITableViewDat
errorHandler(error!)
}
}
case CNAuthorizationStatus.authorized:
//Authorization granted by user for this app.
var contacts = [CNContact]()
do {
let contactFetchRequest = CNContactFetchRequest(keysToFetch: allowedContactKeys)
try contactStore.enumerateContacts(with: contactFetchRequest) { (contact, stop) -> Void in
try contactStore.enumerateContacts(with: contactFetchRequest) { (contact, _) -> Void in
contacts.append(contact)
}
self.sections = collatedContacts(contacts)
@ -198,13 +199,12 @@ open class ContactsPicker: UIViewController, UITableViewDelegate, UITableViewDat
return collated
}
// MARK: - Table View DataSource
open func numberOfSections(in tableView: UITableView) -> Int {
return self.collation.sectionTitles.count
}
open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let dataSource = filteredSections
@ -218,7 +218,7 @@ open class ContactsPicker: UIViewController, UITableViewDelegate, UITableViewDat
let dataSource = filteredSections
let cnContact = dataSource[indexPath.section][indexPath.row]
let contact = Contact(contact: cnContact)
let contact = Contact(systemContact: cnContact)
cell.updateContactsinUI(contact, subtitleType: subtitleCellValue, contactsManager: self.contactsManager)
let isSelected = selectedContacts.contains(where: { $0.uniqueId == contact.uniqueId })
@ -239,7 +239,7 @@ open class ContactsPicker: UIViewController, UITableViewDelegate, UITableViewDat
let cell = tableView.cellForRow(at: indexPath) as! ContactCell
let deselectedContact = cell.contact!
selectedContacts = selectedContacts.filter() {
selectedContacts = selectedContacts.filter {
return $0.uniqueId != deselectedContact.uniqueId
}
}
@ -262,11 +262,11 @@ open class ContactsPicker: UIViewController, UITableViewDelegate, UITableViewDat
}
}
}
open func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
return collation.section(forSectionIndexTitle: index)
}
open func sectionIndexTitles(for tableView: UITableView) -> [String]? {
return collation.sectionIndexTitles
}
@ -280,24 +280,24 @@ open class ContactsPicker: UIViewController, UITableViewDelegate, UITableViewDat
return nil
}
}
// MARK: - Button Actions
func onTouchCancelButton() {
contactsPickerDelegate?.contactsPicker(self, didCancel: NSError(domain: "contactsPickerErrorDomain", code: 2, userInfo: [ NSLocalizedDescriptionKey: "User Canceled Selection"]))
dismiss(animated: true, completion: nil)
}
func onTouchDoneButton() {
contactsPickerDelegate?.contactsPicker(self, didSelectMultipleContacts: selectedContacts)
dismiss(animated: true, completion: nil)
}
// MARK: - Search Actions
open func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
updateSearchResults(searchText: searchText)
}
open func updateSearchResults(searchText: String) {
let predicate: NSPredicate
if searchText.characters.count == 0 {
@ -305,7 +305,7 @@ open class ContactsPicker: UIViewController, UITableViewDelegate, UITableViewDat
} else {
do {
predicate = CNContact.predicateForContacts(matchingName: searchText)
let filteredContacts = try contactStore.unifiedContacts(matching: predicate,keysToFetch: allowedContactKeys)
let filteredContacts = try contactStore.unifiedContacts(matching: predicate, keysToFetch: allowedContactKeys)
filteredSections = collatedContacts(filteredContacts)
} catch let error as NSError {
Logger.error("\(self.TAG) updating search results failed with error: \(error)")
@ -349,7 +349,7 @@ fileprivate extension CNContact {
if self.familyName.isEmpty && self.givenName.isEmpty {
return self.emailAddresses.first?.value as? String ?? ""
}
let compositeName: String
if ContactSortOrder == .familyName {
compositeName = "\(self.familyName) \(self.givenName)"

View File

@ -8,6 +8,7 @@
#import "SignalAccount.h"
#import <SignalServiceKit/Contact.h>
#import <SignalServiceKit/OWSBlockingManager.h>
#import <SignalServiceKit/PhoneNumber.h>
#import <SignalServiceKit/TSAccountManager.h>
NS_ASSUME_NONNULL_BEGIN

View File

@ -165,6 +165,11 @@ NSString *const MessageComposeTableViewControllerCellContact = @"ContactTableVie
{
[super viewWillAppear:animated];
// Make sure we have requested contact access at this point if, e.g.
// the user has no messages in their inbox and they choose to compose
// a message.
[self.contactsManager requestSystemContactsOnce];
[self showEmptyBackgroundViewIfNecessary];
}
@ -729,6 +734,8 @@ NSString *const MessageComposeTableViewControllerCellContact = @"ContactTableVie
self.contacts = [self filteredContacts];
[self updateSearchResultsForSearchController:self.searchController];
[self.tableView reloadData];
// TODO revisit this after https://github.com/WhisperSystems/Signal-iOS/pull/2058 is merged
[self showEmptyBackgroundViewIfNecessary];
}
- (BOOL)isContactHidden:(Contact *)contact

View File

@ -528,6 +528,10 @@ typedef enum : NSUInteger {
// restart any animations that were stopped e.g. while inspecting the contact info screens.
[self startExpirationTimerAnimations];
// We should have already requested contact access at this point, so this should be a no-op
// unless it ever becomes possible to to load this VC without going via the SignalsViewController
[self.contactsManager requestSystemContactsOnce];
OWSDisappearingMessagesConfiguration *configuration =
[OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:self.thread.uniqueId];
[self setBarButtonItemsForDisappearingMessagesConfiguration:configuration];

View File

@ -16,6 +16,7 @@
#import "UIColor+OWS.h"
#import "UIFont+OWS.h"
#import "UIView+OWS.h"
#import <SignalServiceKit/PhoneNumber.h>
#import <SignalServiceKit/TSAccountManager.h>
#import <SignalServiceKit/TSContactThread.h>
#import <SignalServiceKit/TSThread.h>

View File

@ -276,7 +276,9 @@ NSString *const SignalsViewControllerSegueShowIncomingCall = @"ShowIncomingCallS
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self checkIfEmptyView];
if ([TSThread numberOfKeysInCollection] > 0) {
[self.contactsManager requestSystemContactsOnce];
}
[self updateInboxCountLabel];
[[self tableView] reloadData];
}
@ -287,8 +289,7 @@ NSString *const SignalsViewControllerSegueShowIncomingCall = @"ShowIncomingCallS
[self.editingDbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
[self.experienceUpgradeFinder markAllAsSeenWithTransaction:transaction];
}];
[self didAppearForNewlyRegisteredUser];
[self ensureNotificationsUpToDate];
} else {
[self displayAnyUnseenUpgradeExperience];
}
@ -296,39 +297,6 @@ NSString *const SignalsViewControllerSegueShowIncomingCall = @"ShowIncomingCallS
#pragma mark - startup
- (void)didAppearForNewlyRegisteredUser
{
ABAuthorizationStatus status = ABAddressBookGetAuthorizationStatus();
switch (status) {
case kABAuthorizationStatusNotDetermined:
case kABAuthorizationStatusRestricted: {
UIAlertController *controller =
[UIAlertController alertControllerWithTitle:NSLocalizedString(@"REGISTER_CONTACTS_WELCOME", nil)
message:NSLocalizedString(@"REGISTER_CONTACTS_BODY", nil)
preferredStyle:UIAlertControllerStyleAlert];
[controller
addAction:[UIAlertAction
actionWithTitle:NSLocalizedString(@"REGISTER_CONTACTS_CONTINUE", nil)
style:UIAlertActionStyleCancel
handler:^(UIAlertAction *action) {
[self ensureNotificationsUpToDate];
[[Environment getCurrent].contactsManager doAfterEnvironmentInitSetup];
}]];
[self presentViewController:controller animated:YES completion:nil];
break;
}
default: {
DDLogError(@"%@ Unexpected for new user to have kABAuthorizationStatus:%ld", self.tag, status);
[self ensureNotificationsUpToDate];
[[Environment getCurrent].contactsManager doAfterEnvironmentInitSetup];
break;
}
}
}
- (void)displayAnyUnseenUpgradeExperience
{
AssertIsOnMainThread();
@ -683,6 +651,12 @@ NSString *const SignalsViewControllerSegueShowIncomingCall = @"ShowIncomingCallS
NSArray *sectionChanges = nil;
NSArray *rowChanges = nil;
// If the user hasn't already granted contact access
// we don't want to request until they receive a message.
if ([TSThread numberOfKeysInCollection] > 0) {
[self.contactsManager requestSystemContactsOnce];
}
[[self.uiDatabaseConnection ext:TSThreadDatabaseViewExtensionName] getSectionChanges:&sectionChanges
rowChanges:&rowChanges
forNotifications:notifications

View File

@ -2,12 +2,8 @@
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
#import <Contacts/Contacts.h>
#import <Foundation/Foundation.h>
#import <SignalServiceKit/ContactsManagerProtocol.h>
#import <SignalServiceKit/PhoneNumber.h>
#import "CollapsingFutures.h"
#import "Contact.h"
#import <SignalServiceKit/ContactsManagerProtocol.h>
NS_ASSUME_NONNULL_BEGIN
@ -33,13 +29,14 @@ extern NSString *const OWSContactsManagerSignalAccountsDidChangeNotification;
- (Contact *)getOrBuildContactForPhoneIdentifier:(NSString *)identifier;
- (void)verifyABPermission;
#pragma mark - System Contact Fetching
- (void)requestSystemContactsOnce;
- (void)fetchSystemContactsIfAlreadyAuthorized;
// TODO: Remove this method.
- (NSArray<Contact *> *)signalContacts;
- (void)doAfterEnvironmentInitSetup;
- (NSString *)displayNameForPhoneIdentifier:(nullable NSString *)identifier;
- (NSString *)displayNameForContact:(Contact *)contact;
- (NSString *)displayNameForSignalAccount:(SignalAccount *)signalAccount;
@ -48,6 +45,7 @@ extern NSString *const OWSContactsManagerSignalAccountsDidChangeNotification;
- (NSAttributedString *)formattedFullNameForContact:(Contact *)contact font:(UIFont *)font;
- (NSAttributedString *)formattedFullNameForRecipientId:(NSString *)recipientId font:(UIFont *)font;
// TODO migrate to CNContact?
- (BOOL)hasAddressBook;
+ (NSComparator _Nonnull)contactComparator;

View File

@ -4,21 +4,19 @@
#import "OWSContactsManager.h"
#import "Environment.h"
#import "Signal-Swift.h"
#import "SignalAccount.h"
#import "Util.h"
#import <SignalServiceKit/ContactsUpdater.h>
#import <SignalServiceKit/OWSError.h>
#define ADDRESSBOOK_QUEUE dispatch_get_main_queue()
typedef BOOL (^ContactSearchBlock)(id, NSUInteger, BOOL *);
@import Contacts;
NSString *const OWSContactsManagerSignalAccountsDidChangeNotification =
@"OWSContactsManagerSignalAccountsDidChangeNotification";
@interface OWSContactsManager ()
@interface OWSContactsManager () <SystemContactsFetcherDelegate>
@property (atomic, nullable) CNContactStore *contactStore;
@property (atomic) id addressBookReference;
@property (atomic) TOCFuture *futureAddressBook;
@property (nonatomic) BOOL isContactsUpdateInFlight;
@ -28,7 +26,7 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification =
@property (atomic) NSDictionary<NSString *, Contact *> *allContactsMap;
@property (atomic) NSArray<SignalAccount *> *signalAccounts;
@property (atomic) NSDictionary<NSString *, SignalAccount *> *signalAccountMap;
@property (nonatomic, readonly) SystemContactsFetcher *systemContactsFetcher;
@end
@implementation OWSContactsManager
@ -43,89 +41,37 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification =
_allContacts = @[];
_signalAccountMap = @{};
_signalAccounts = @[];
_systemContactsFetcher = [SystemContactsFetcher new];
_systemContactsFetcher.delegate = self;
OWSSingletonAssert();
return self;
}
- (void)doAfterEnvironmentInitSetup {
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(9, 0) &&
!self.contactStore) {
OWSAssert(!self.contactStore);
self.contactStore = [[CNContactStore alloc] init];
[self.contactStore requestAccessForEntityType:CNEntityTypeContacts
completionHandler:^(BOOL granted, NSError *_Nullable error) {
if (!granted) {
// We're still using the old addressbook API.
// User warned if permission not granted in that setup.
}
}];
}
#pragma mark - System Contact Fetching
[self setupAddressBookIfNecessary];
}
- (void)verifyABPermission {
[self setupAddressBookIfNecessary];
}
#pragma mark - Address Book callbacks
void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef info, void *context);
void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef info, void *context) {
OWSContactsManager *contactsManager = (__bridge OWSContactsManager *)context;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[contactsManager handleAddressBookChanged];
});
}
- (void)handleAddressBookChanged
// Request contacts access if you haven't asked recently.
- (void)requestSystemContactsOnce
{
[self pullLatestAddressBook];
[self.systemContactsFetcher requestOnce];
}
#pragma mark - Setup
- (void)setupAddressBookIfNecessary
- (void)fetchSystemContactsIfAlreadyAuthorized
{
dispatch_async(ADDRESSBOOK_QUEUE, ^{
// De-bounce address book setup.
if (self.isContactsUpdateInFlight) {
return;
}
// We only need to set up our address book once;
// after that we only need to respond to onAddressBookChanged.
if (self.addressBookReference) {
return;
}
self.isContactsUpdateInFlight = YES;
TOCFuture *future = [OWSContactsManager asyncGetAddressBook];
[future thenDo:^(id addressBook) {
// Success.
OWSAssert(self.isContactsUpdateInFlight);
OWSAssert(!self.addressBookReference);
self.addressBookReference = addressBook;
self.isContactsUpdateInFlight = NO;
ABAddressBookRef cfAddressBook = (__bridge ABAddressBookRef)addressBook;
ABAddressBookRegisterExternalChangeCallback(cfAddressBook, onAddressBookChanged, (__bridge void *)self);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self handleAddressBookChanged];
});
}];
[future catchDo:^(id failure) {
// Failure.
OWSAssert(self.isContactsUpdateInFlight);
OWSAssert(!self.addressBookReference);
self.isContactsUpdateInFlight = NO;
}];
});
[self.systemContactsFetcher fetchIfAlreadyAuthorized];
}
#pragma mark SystemContactsFetcherDelegate
- (void)systemContactsFetcher:(SystemContactsFetcher *)systemsContactsFetcher
updatedContacts:(NSArray<Contact *> *)contacts
{
[self updateWithContacts:contacts];
}
#pragma mark - Intersection
- (void)intersectContacts
{
[self intersectContactsWithRetryDelay:1];
@ -159,21 +105,6 @@ void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef in
failure:failure];
}
- (void)pullLatestAddressBook {
dispatch_async(ADDRESSBOOK_QUEUE, ^{
CFErrorRef creationError = nil;
ABAddressBookRef addressBookRef = ABAddressBookCreateWithOptions(NULL, &creationError);
checkOperationDescribe(nil == creationError, [((__bridge NSError *)creationError)localizedDescription]);
ABAddressBookRequestAccessWithCompletion(addressBookRef, ^(bool granted, CFErrorRef error) {
if (!granted) {
[OWSContactsManager blockingContactDialog];
}
});
NSArray<Contact *> *contacts = [self getContactsFromAddressBook:addressBookRef];
[self updateWithContacts:contacts];
});
}
- (void)updateWithContacts:(NSArray<Contact *> *)contacts
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@ -249,6 +180,8 @@ void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef in
});
}
#pragma mark - View Helpers
// TODO move into Contact class.
+ (NSString *)accountLabelForContact:(Contact *)contact recipientId:(NSString *)recipientId
{
OWSAssert(contact);
@ -324,207 +257,10 @@ void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef in
return phoneNumberLabel;
}
+ (void)blockingContactDialog {
switch (ABAddressBookGetAuthorizationStatus()) {
case kABAuthorizationStatusRestricted: {
UIAlertController *controller =
[UIAlertController alertControllerWithTitle:NSLocalizedString(@"AB_PERMISSION_MISSING_TITLE", nil)
message:NSLocalizedString(@"ADDRESSBOOK_RESTRICTED_ALERT_BODY", nil)
preferredStyle:UIAlertControllerStyleAlert];
[controller
addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"ADDRESSBOOK_RESTRICTED_ALERT_BUTTON", nil)
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *action) {
[DDLog flushLog];
exit(0);
}]];
[[UIApplication sharedApplication]
.keyWindow.rootViewController presentViewController:controller
animated:YES
completion:nil];
break;
}
case kABAuthorizationStatusDenied: {
UIAlertController *controller =
[UIAlertController alertControllerWithTitle:NSLocalizedString(@"AB_PERMISSION_MISSING_TITLE", nil)
message:NSLocalizedString(@"AB_PERMISSION_MISSING_BODY", nil)
preferredStyle:UIAlertControllerStyleAlert];
[controller addAction:[UIAlertAction
actionWithTitle:NSLocalizedString(@"AB_PERMISSION_MISSING_ACTION", nil)
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *action) {
[[UIApplication sharedApplication]
openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]];
}]];
[[[UIApplication sharedApplication] keyWindow]
.rootViewController presentViewController:controller
animated:YES
completion:nil];
break;
}
case kABAuthorizationStatusNotDetermined: {
DDLogInfo(@"AddressBook access not granted but status undetermined.");
[[Environment getCurrent].contactsManager pullLatestAddressBook];
break;
}
case kABAuthorizationStatusAuthorized: {
DDLogInfo(@"AddressBook access not granted but status authorized.");
break;
}
default:
break;
}
}
#pragma mark - Address Book utils
+ (TOCFuture *)asyncGetAddressBook {
CFErrorRef creationError = nil;
ABAddressBookRef addressBookRef = ABAddressBookCreateWithOptions(NULL, &creationError);
assert((addressBookRef == nil) == (creationError != nil));
if (creationError != nil) {
[self blockingContactDialog];
return [TOCFuture futureWithFailure:(__bridge_transfer id)creationError];
}
TOCFutureSource *futureAddressBookSource = [TOCFutureSource new];
id addressBook = (__bridge_transfer id)addressBookRef;
ABAddressBookRequestAccessWithCompletion(addressBookRef, ^(bool granted, CFErrorRef requestAccessError) {
if (granted && ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusAuthorized) {
dispatch_async(ADDRESSBOOK_QUEUE, ^{
[futureAddressBookSource trySetResult:addressBook];
});
} else {
[self blockingContactDialog];
[futureAddressBookSource trySetFailure:(__bridge id)requestAccessError];
}
});
return futureAddressBookSource.future;
}
- (NSArray<Contact *> *)getContactsFromAddressBook:(ABAddressBookRef _Nonnull)addressBook
{
CFArrayRef allPeople = ABAddressBookCopyArrayOfAllPeople(addressBook);
CFMutableArrayRef allPeopleMutable =
CFArrayCreateMutableCopy(kCFAllocatorDefault, CFArrayGetCount(allPeople), allPeople);
CFArraySortValues(allPeopleMutable,
CFRangeMake(0, CFArrayGetCount(allPeopleMutable)),
(CFComparatorFunction)ABPersonComparePeopleByName,
(void *)(unsigned long)ABPersonGetSortOrdering());
NSArray *sortedPeople = (__bridge_transfer NSArray *)allPeopleMutable;
// This predicate returns all contacts from the addressbook having at least one phone number
NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id record, NSDictionary *bindings) {
ABMultiValueRef phoneNumbers = ABRecordCopyValue((__bridge ABRecordRef)record, kABPersonPhoneProperty);
BOOL result = NO;
for (CFIndex i = 0; i < ABMultiValueGetCount(phoneNumbers); i++) {
NSString *phoneNumber = (__bridge_transfer NSString *)ABMultiValueCopyValueAtIndex(phoneNumbers, i);
if (phoneNumber.length > 0) {
result = YES;
break;
}
}
CFRelease(phoneNumbers);
return result;
}];
CFRelease(allPeople);
NSArray *filteredContacts = [sortedPeople filteredArrayUsingPredicate:predicate];
return [filteredContacts map:^id(id item) {
Contact *contact = [self contactForRecord:(__bridge ABRecordRef)item];
return contact;
}];
}
#pragma mark - Contact/Phone Number util
- (Contact *)contactForRecord:(ABRecordRef)record {
ABRecordID recordID = ABRecordGetRecordID(record);
NSString *firstName = (__bridge_transfer NSString *)ABRecordCopyValue(record, kABPersonFirstNameProperty);
NSString *lastName = (__bridge_transfer NSString *)ABRecordCopyValue(record, kABPersonLastNameProperty);
NSDictionary<NSString *, NSNumber *> *phoneNumberTypeMap = [self phoneNumbersForRecord:record];
NSArray *phoneNumbers = [phoneNumberTypeMap.allKeys sortedArrayUsingSelector:@selector(compare:)];
if (!firstName && !lastName) {
NSString *companyName = (__bridge_transfer NSString *)ABRecordCopyValue(record, kABPersonOrganizationProperty);
if (companyName) {
firstName = companyName;
} else if (phoneNumbers.count) {
firstName = phoneNumbers.firstObject;
}
}
NSData *imageData
= (__bridge_transfer NSData *)ABPersonCopyImageDataWithFormat(record, kABPersonImageFormatThumbnail);
UIImage *img = [UIImage imageWithData:imageData];
return [[Contact alloc] initWithContactWithFirstName:firstName
andLastName:lastName
andUserTextPhoneNumbers:phoneNumbers
phoneNumberTypeMap:phoneNumberTypeMap
andImage:img
andContactID:recordID];
}
- (BOOL)phoneNumber:(PhoneNumber *)phoneNumber1 matchesNumber:(PhoneNumber *)phoneNumber2 {
return [phoneNumber1.toE164 isEqualToString:phoneNumber2.toE164];
}
- (NSDictionary<NSString *, NSNumber *> *)phoneNumbersForRecord:(ABRecordRef)record
{
ABMultiValueRef phoneNumberRefs = NULL;
@try {
phoneNumberRefs = ABRecordCopyValue(record, kABPersonPhoneProperty);
CFIndex phoneNumberCount = ABMultiValueGetCount(phoneNumberRefs);
NSMutableDictionary<NSString *, NSNumber *> *result = [NSMutableDictionary new];
for (int i = 0; i < phoneNumberCount; i++) {
NSString *phoneNumberLabel = (__bridge_transfer NSString *)ABMultiValueCopyLabelAtIndex(phoneNumberRefs, i);
NSString *phoneNumber = (__bridge_transfer NSString *)ABMultiValueCopyValueAtIndex(phoneNumberRefs, i);
if ([phoneNumberLabel isEqualToString:(NSString *)kABPersonPhoneMobileLabel]) {
result[phoneNumber] = @(OWSPhoneNumberTypeMobile);
} else if ([phoneNumberLabel isEqualToString:(NSString *)kABPersonPhoneIPhoneLabel]) {
result[phoneNumber] = @(OWSPhoneNumberTypeIPhone);
} else if ([phoneNumberLabel isEqualToString:(NSString *)kABPersonPhoneMainLabel]) {
result[phoneNumber] = @(OWSPhoneNumberTypeMain);
} else if ([phoneNumberLabel isEqualToString:(NSString *)kABPersonPhoneHomeFAXLabel]) {
result[phoneNumber] = @(OWSPhoneNumberTypeHomeFAX);
} else if ([phoneNumberLabel isEqualToString:(NSString *)kABPersonPhoneWorkFAXLabel]) {
result[phoneNumber] = @(OWSPhoneNumberTypeWorkFAX);
} else if ([phoneNumberLabel isEqualToString:(NSString *)kABPersonPhoneOtherFAXLabel]) {
result[phoneNumber] = @(OWSPhoneNumberTypeOtherFAX);
} else if ([phoneNumberLabel isEqualToString:(NSString *)kABPersonPhonePagerLabel]) {
result[phoneNumber] = @(OWSPhoneNumberTypePager);
} else {
result[phoneNumber] = @(OWSPhoneNumberTypeUnknown);
}
}
return [result copy];
} @finally {
if (phoneNumberRefs) {
CFRelease(phoneNumberRefs);
}
}
}
#pragma mark - Whisper User Management
- (NSArray *)getSignalUsersFromContactsArray:(NSArray *)contacts {
@ -567,6 +303,7 @@ void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef in
return displayName;
}
// TODO move into Contact class.
- (NSString *_Nonnull)displayNameForContact:(Contact *)contact
{
OWSAssert(contact);
@ -617,6 +354,7 @@ void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef in
}
}
// TODO move into Contact class.
- (NSAttributedString *_Nonnull)formattedFullNameForContact:(Contact *)contact font:(UIFont *_Nonnull)font
{
UIFont *boldFont = [UIFont ows_mediumFontWithSize:font.pointSize];

View File

@ -0,0 +1,110 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
import Foundation
import Contacts
@objc protocol SystemContactsFetcherDelegate: class {
func systemContactsFetcher(_ systemContactsFetcher: SystemContactsFetcher, updatedContacts contacts: [Contact])
}
@objc
class SystemContactsFetcher: NSObject {
private let TAG = "[SystemContactsFetcher]"
public weak var delegate: SystemContactsFetcherDelegate?
public var authorizationStatus: CNAuthorizationStatus {
return CNContactStore.authorizationStatus(for: CNEntityType.contacts)
}
private let contactStore = CNContactStore()
private var systemContactsHaveBeenRequestedAtLeastOnce = false
private let allowedContactKeys: [CNKeyDescriptor] = [
CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
CNContactThumbnailImageDataKey as CNKeyDescriptor, // TODO full image instead of thumbnail?
CNContactPhoneNumbersKey as CNKeyDescriptor,
CNContactEmailAddressesKey as CNKeyDescriptor
]
public func requestOnce() {
AssertIsOnMainThread()
guard !systemContactsHaveBeenRequestedAtLeastOnce else {
Logger.debug("\(TAG) already requested system contacts")
return
}
systemContactsHaveBeenRequestedAtLeastOnce = true
self.startObservingContactChanges()
switch authorizationStatus {
case .notDetermined:
contactStore.requestAccess(for: .contacts, completionHandler: { (granted, error) in
if let error = error {
Logger.error("\(self.TAG) error fetching contacts: \(error)")
assertionFailure()
}
if !granted {
// TODO, make this a one time dismissable admonishment
// e.g. remember across launches that the user has dismissed.
self.displayMissingContactsPermissionAlert()
} else {
self.updateContacts()
}
})
case .authorized:
// TODO reset onetime admonishment reminder, so that we remind user again (once) if they've since toggled permissions.
self.updateContacts()
case .denied, .restricted:
Logger.debug("\(TAG) contacts were \(self.authorizationStatus)")
}
}
public func fetchIfAlreadyAuthorized() {
AssertIsOnMainThread()
guard authorizationStatus == .authorized else {
return
}
updateContacts()
}
private func displayMissingContactsPermissionAlert() {
let foo = UIApplication.shared.frontmostViewController
Logger.error("TODO")
}
private func updateContacts() {
systemContactsHaveBeenRequestedAtLeastOnce = true
var systemContacts = [CNContact]()
do {
let contactFetchRequest = CNContactFetchRequest(keysToFetch: allowedContactKeys)
try contactStore.enumerateContacts(with: contactFetchRequest) { (contact, _) -> Void in
systemContacts.append(contact)
}
} catch let error as NSError {
Logger.error("\(self.TAG) Failed to fetch contacts with error:\(error)")
assertionFailure()
}
let contacts = systemContacts.map { Contact(systemContact: $0) }
self.delegate?.systemContactsFetcher(self, updatedContacts: contacts)
}
private func startObservingContactChanges() {
NotificationCenter.default.addObserver(self,
selector: #selector(contactStoreDidChange),
name: .CNContactStoreDidChange,
object: nil)
}
@objc
private func contactStoreDidChange() {
updateContacts()
}
}

View File

@ -1,25 +1,22 @@
// Originally based on EPContacts
//
// Created by Prabaharan Elangovan on 13/10/15.
// Copyright © 2015 Prabaharan Elangovan. All rights reserved.
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
// Modified for Signal by Michael Kirk on 11/25/2016
// Parts Copyright © 2016 Open Whisper Systems. All rights reserved.
import UIKit
import Contacts
@available(iOS 9.0, *)
class ContactCell: UITableViewCell {
static let nib = UINib(nibName:"ContactCell", bundle: nil)
@IBOutlet weak var contactTextLabel: UILabel!
@IBOutlet weak var contactDetailTextLabel: UILabel!
@IBOutlet weak var contactImageView: UIImageView!
@IBOutlet weak var contactContainerView: UIView!
var contact: Contact?
override func awakeFromNib() {
super.awakeFromNib()
@ -28,7 +25,7 @@ class ContactCell: UITableViewCell {
contactContainerView.layer.masksToBounds = true
contactContainerView.layer.cornerRadius = contactContainerView.frame.size.width/2
NotificationCenter.default.addObserver(self, selector: #selector(self.didChangePreferredContentSize), name: NSNotification.Name.UIContentSizeCategoryDidChange, object: nil)
}
@ -52,7 +49,7 @@ class ContactCell: UITableViewCell {
if contactTextLabel != nil {
contactTextLabel.attributedText = contact.cnContact?.formattedFullName(font:contactTextLabel.font)
}
updateSubtitleBasedonType(subtitleType, contact: contact)
if contact.image == nil {
@ -66,15 +63,15 @@ class ContactCell: UITableViewCell {
let avatarBuilder = OWSContactAvatarBuilder(contactId:contactIdForDeterminingBackgroundColor,
name:contact.fullName,
contactsManager:contactsManager)
self.contactImageView?.image = avatarBuilder.buildDefaultImage();
self.contactImageView?.image = avatarBuilder.buildDefaultImage()
} else {
self.contactImageView?.image = contact.image
}
}
func updateSubtitleBasedonType(_ subtitleType: SubtitleCellValue , contact: Contact) {
func updateSubtitleBasedonType(_ subtitleType: SubtitleCellValue, contact: Contact) {
switch subtitleType {
case SubtitleCellValue.phoneNumber:
if contact.userTextPhoneNumbers.count > 0 {
self.contactDetailTextLabel.text = "\(contact.userTextPhoneNumbers[0])"
@ -106,7 +103,7 @@ fileprivate extension CNContact {
if let attributedName = CNContactFormatter.attributedString(from: self, style: .fullName, defaultAttributes: nil) {
let highlightedName = attributedName.mutableCopy() as! NSMutableAttributedString
highlightedName.enumerateAttributes(in: NSMakeRange(0, highlightedName.length), options: [], using: { (attrs, range, stop) in
highlightedName.enumerateAttributes(in: NSMakeRange(0, highlightedName.length), options: [], using: { (attrs, range, _) in
if let property = attrs[CNContactPropertyAttribute] as? String, property == keyToHighlight {
highlightedName.addAttributes(boldAttributes, range: range)
}

View File

@ -8,8 +8,13 @@ pushd $SSK_DIR
CURRENT_SSK_BRANCH=$(git status|awk 'NR==1{print $3}')
if [ $CURRENT_SSK_BRANCH != "master" ]
then
echo "[!] Error - SSK must be on master to be sure we're generating up-to-date strings"
exit 1
if [[ $* == *--non-master* ]]
then
echo "[!] Note - generating from non-master SSK."
else
echo "[!] Error - SSK must be on master to be sure we're generating up-to-date strings, or use '--non-master'."
exit 1
fi
fi
popd

View File

@ -1,5 +1,5 @@
/* No comment provided by engineer. */
"AB_PERMISSION_MISSING_ACTION" = "Give access";
/* Button text to dismiss missing contacts permission alert */
"AB_PERMISSION_MISSING_ACTION_NOT_NOW" = "Not Now";
/* No comment provided by engineer. */
"AB_PERMISSION_MISSING_BODY" = "Signal requires access to your contacts. We do not store your contacts on our servers.";
@ -28,9 +28,6 @@
/* Alert body when contacts disabled */
"ADDRESSBOOK_RESTRICTED_ALERT_BODY" = "Signal requires access to your contacts. Access to contacts is restricted. Signal will close. You can disable the restriction temporarily to let Signal access your contacts by going the Settings app >> General >> Restrictions >> Contacts >> Allow Changes.";
/* No comment provided by engineer. */
"ADDRESSBOOK_RESTRICTED_ALERT_BUTTON" = "Close";
/* The label for the 'discard' button in alerts and action sheets. */
"ALERT_DISCARD_BUTTON" = "Discard";
@ -928,12 +925,6 @@
/* No comment provided by engineer. */
"REGISTER_CC_ERR_ALERT_VIEW_TITLE" = "Country Code Error";
/* No comment provided by engineer. */
"REGISTER_CONTACTS_BODY" = "Signal allows you to have private conversations with your existing contacts. To use Signal, please allow access to your contacts.";
/* No comment provided by engineer. */
"REGISTER_CONTACTS_CONTINUE" = "Continue";
/* No comment provided by engineer. */
"REGISTER_CONTACTS_WELCOME" = "Welcome!";