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
pull/1/head
Michael Kirk 7 years ago
parent 931b6b4200
commit b24cf29189

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

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

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

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

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

@ -165,6 +165,11 @@ NSString *const MessageComposeTableViewControllerCellContact = @"ContactTableVie
{ {
[super viewWillAppear:animated]; [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]; [self showEmptyBackgroundViewIfNecessary];
} }
@ -729,6 +734,8 @@ NSString *const MessageComposeTableViewControllerCellContact = @"ContactTableVie
self.contacts = [self filteredContacts]; self.contacts = [self filteredContacts];
[self updateSearchResultsForSearchController:self.searchController]; [self updateSearchResultsForSearchController:self.searchController];
[self.tableView reloadData]; [self.tableView reloadData];
// TODO revisit this after https://github.com/WhisperSystems/Signal-iOS/pull/2058 is merged
[self showEmptyBackgroundViewIfNecessary];
} }
- (BOOL)isContactHidden:(Contact *)contact - (BOOL)isContactHidden:(Contact *)contact

@ -528,6 +528,10 @@ typedef enum : NSUInteger {
// restart any animations that were stopped e.g. while inspecting the contact info screens. // restart any animations that were stopped e.g. while inspecting the contact info screens.
[self startExpirationTimerAnimations]; [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 *configuration =
[OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:self.thread.uniqueId]; [OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:self.thread.uniqueId];
[self setBarButtonItemsForDisappearingMessagesConfiguration:configuration]; [self setBarButtonItemsForDisappearingMessagesConfiguration:configuration];

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

@ -276,7 +276,9 @@ NSString *const SignalsViewControllerSegueShowIncomingCall = @"ShowIncomingCallS
- (void)viewWillAppear:(BOOL)animated { - (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated]; [super viewWillAppear:animated];
[self checkIfEmptyView]; [self checkIfEmptyView];
if ([TSThread numberOfKeysInCollection] > 0) {
[self.contactsManager requestSystemContactsOnce];
}
[self updateInboxCountLabel]; [self updateInboxCountLabel];
[[self tableView] reloadData]; [[self tableView] reloadData];
} }
@ -287,8 +289,7 @@ NSString *const SignalsViewControllerSegueShowIncomingCall = @"ShowIncomingCallS
[self.editingDbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) { [self.editingDbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
[self.experienceUpgradeFinder markAllAsSeenWithTransaction:transaction]; [self.experienceUpgradeFinder markAllAsSeenWithTransaction:transaction];
}]; }];
[self ensureNotificationsUpToDate];
[self didAppearForNewlyRegisteredUser];
} else { } else {
[self displayAnyUnseenUpgradeExperience]; [self displayAnyUnseenUpgradeExperience];
} }
@ -296,39 +297,6 @@ NSString *const SignalsViewControllerSegueShowIncomingCall = @"ShowIncomingCallS
#pragma mark - startup #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 - (void)displayAnyUnseenUpgradeExperience
{ {
AssertIsOnMainThread(); AssertIsOnMainThread();
@ -683,6 +651,12 @@ NSString *const SignalsViewControllerSegueShowIncomingCall = @"ShowIncomingCallS
NSArray *sectionChanges = nil; NSArray *sectionChanges = nil;
NSArray *rowChanges = 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 [[self.uiDatabaseConnection ext:TSThreadDatabaseViewExtensionName] getSectionChanges:&sectionChanges
rowChanges:&rowChanges rowChanges:&rowChanges
forNotifications:notifications forNotifications:notifications

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

@ -4,21 +4,19 @@
#import "OWSContactsManager.h" #import "OWSContactsManager.h"
#import "Environment.h" #import "Environment.h"
#import "Signal-Swift.h"
#import "SignalAccount.h" #import "SignalAccount.h"
#import "Util.h" #import "Util.h"
#import <SignalServiceKit/ContactsUpdater.h> #import <SignalServiceKit/ContactsUpdater.h>
#import <SignalServiceKit/OWSError.h> #import <SignalServiceKit/OWSError.h>
#define ADDRESSBOOK_QUEUE dispatch_get_main_queue() @import Contacts;
typedef BOOL (^ContactSearchBlock)(id, NSUInteger, BOOL *);
NSString *const OWSContactsManagerSignalAccountsDidChangeNotification = NSString *const OWSContactsManagerSignalAccountsDidChangeNotification =
@"OWSContactsManagerSignalAccountsDidChangeNotification"; @"OWSContactsManagerSignalAccountsDidChangeNotification";
@interface OWSContactsManager () @interface OWSContactsManager () <SystemContactsFetcherDelegate>
@property (atomic, nullable) CNContactStore *contactStore;
@property (atomic) id addressBookReference; @property (atomic) id addressBookReference;
@property (atomic) TOCFuture *futureAddressBook; @property (atomic) TOCFuture *futureAddressBook;
@property (nonatomic) BOOL isContactsUpdateInFlight; @property (nonatomic) BOOL isContactsUpdateInFlight;
@ -28,7 +26,7 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification =
@property (atomic) NSDictionary<NSString *, Contact *> *allContactsMap; @property (atomic) NSDictionary<NSString *, Contact *> *allContactsMap;
@property (atomic) NSArray<SignalAccount *> *signalAccounts; @property (atomic) NSArray<SignalAccount *> *signalAccounts;
@property (atomic) NSDictionary<NSString *, SignalAccount *> *signalAccountMap; @property (atomic) NSDictionary<NSString *, SignalAccount *> *signalAccountMap;
@property (nonatomic, readonly) SystemContactsFetcher *systemContactsFetcher;
@end @end
@implementation OWSContactsManager @implementation OWSContactsManager
@ -43,89 +41,37 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification =
_allContacts = @[]; _allContacts = @[];
_signalAccountMap = @{}; _signalAccountMap = @{};
_signalAccounts = @[]; _signalAccounts = @[];
_systemContactsFetcher = [SystemContactsFetcher new];
_systemContactsFetcher.delegate = self;
OWSSingletonAssert(); OWSSingletonAssert();
return self; return self;
} }
- (void)doAfterEnvironmentInitSetup { #pragma mark - System Contact Fetching
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.
}
}];
}
[self setupAddressBookIfNecessary];
}
- (void)verifyABPermission { // Request contacts access if you haven't asked recently.
[self setupAddressBookIfNecessary]; - (void)requestSystemContactsOnce
} {
[self.systemContactsFetcher requestOnce];
#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 - (void)fetchSystemContactsIfAlreadyAuthorized
{ {
[self pullLatestAddressBook]; [self.systemContactsFetcher fetchIfAlreadyAuthorized];
} }
#pragma mark - Setup #pragma mark SystemContactsFetcherDelegate
- (void)setupAddressBookIfNecessary - (void)systemContactsFetcher:(SystemContactsFetcher *)systemsContactsFetcher
updatedContacts:(NSArray<Contact *> *)contacts
{ {
dispatch_async(ADDRESSBOOK_QUEUE, ^{ [self updateWithContacts:contacts];
// 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;
}];
});
} }
#pragma mark - Intersection
- (void)intersectContacts - (void)intersectContacts
{ {
[self intersectContactsWithRetryDelay:1]; [self intersectContactsWithRetryDelay:1];
@ -159,21 +105,6 @@ void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef in
failure:failure]; 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 - (void)updateWithContacts:(NSArray<Contact *> *)contacts
{ {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 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 + (NSString *)accountLabelForContact:(Contact *)contact recipientId:(NSString *)recipientId
{ {
OWSAssert(contact); OWSAssert(contact);
@ -324,207 +257,10 @@ void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef in
return phoneNumberLabel; 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 { - (BOOL)phoneNumber:(PhoneNumber *)phoneNumber1 matchesNumber:(PhoneNumber *)phoneNumber2 {
return [phoneNumber1.toE164 isEqualToString:phoneNumber2.toE164]; 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 #pragma mark - Whisper User Management
- (NSArray *)getSignalUsersFromContactsArray:(NSArray *)contacts { - (NSArray *)getSignalUsersFromContactsArray:(NSArray *)contacts {
@ -567,6 +303,7 @@ void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef in
return displayName; return displayName;
} }
// TODO move into Contact class.
- (NSString *_Nonnull)displayNameForContact:(Contact *)contact - (NSString *_Nonnull)displayNameForContact:(Contact *)contact
{ {
OWSAssert(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 - (NSAttributedString *_Nonnull)formattedFullNameForContact:(Contact *)contact font:(UIFont *_Nonnull)font
{ {
UIFont *boldFont = [UIFont ows_mediumFontWithSize:font.pointSize]; UIFont *boldFont = [UIFont ows_mediumFontWithSize:font.pointSize];

@ -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()
}
}

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

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

@ -1,5 +1,5 @@
/* No comment provided by engineer. */ /* Button text to dismiss missing contacts permission alert */
"AB_PERMISSION_MISSING_ACTION" = "Give access"; "AB_PERMISSION_MISSING_ACTION_NOT_NOW" = "Not Now";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"AB_PERMISSION_MISSING_BODY" = "Signal requires access to your contacts. We do not store your contacts on our servers."; "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 */ /* 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."; "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. */ /* The label for the 'discard' button in alerts and action sheets. */
"ALERT_DISCARD_BUTTON" = "Discard"; "ALERT_DISCARD_BUTTON" = "Discard";
@ -928,12 +925,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"REGISTER_CC_ERR_ALERT_VIEW_TITLE" = "Country Code Error"; "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. */ /* No comment provided by engineer. */
"REGISTER_CONTACTS_WELCOME" = "Welcome!"; "REGISTER_CONTACTS_WELCOME" = "Welcome!";

Loading…
Cancel
Save