React-native-contacts: Search crashes after more than a few text changes. Exception if loaded as bundle.

Created on 10 May 2016  路  14Comments  路  Source: morenoh149/react-native-contacts

I've modeled the "Movies" example for my implementation, and am using a modified version of it's search functionality. In addition to the example code, I've added a function that will filter contacts by the search query text, as well as a function that sorts by givenName (first name) alphabetically.

When loading js code from URL, if I type more than a few letters into the search, or delete and add, the app will crash and XCode will lose connection. This happens every time.

If loading js as a bundle, when I type and then hit the delete key the app will throw an exception.

Communications error: { count = 1, contents =
"XPCErrorDescription" => { length = 22, contents = "Connection interrupted" }
}>

bug

Most helpful comment

Might be related to thumbnails. On one of our devices we had huge addressbook (few thousands contacts), and our app wasnt been able to read that at all, always crashing inside getAll(). When I commented out https://github.com/rt2zz/react-native-contacts/blob/master/ios/RCTContacts/RCTContacts.m#L162 line, it started to work. Didnt investigate further yet.

All 14 comments

Please share code

/**
 * ContactScreen.js
 *
 * [ 
 * {   emailAddresses: [],
 *  middleName: 'TOB',
 *  familyName: 'A&P',
 *  givenName: 'Wayne',
    thumbnailPath: '',
    phoneNumbers: [ { number: '1 507-374-6245', label: 'mobile' } ],
    recordID: 1201 },
   {   givenName: 'Howard',
    thumbnailPath: '/var/mobile/Containers/Data/Application/AA8988F2-48C3-4503-A1C1-D0CA78715E1E/tmp/thumbimage_scSrW.png',
    phoneNumbers: [],
    familyName: 'Abraham',
    emailAddresses: [{ email: '', label: '' }],
    recordID: 616 }]

    **NOTE: google maps want's coordinates in (latitude, longitude) format 
    location: {
      "coords":{"speed":-1,"longitude":-93.30477504916821,"latitude":44.94847732952694,"accuracy":65,
                "heading":-1,"altitude":271.9358520507812,"altitudeAccuracy":12.99220041904048},
      "timestamp":1462898120026.433} 
    uri: assets-library://asset/asset.JPG?id=F1AD60AE-571A-4614-97E1-057F198939F5&ext=JPG
 */
'use strict';

var React = require('react');
var ReactNative = require('react-native');
var {
  ActivityIndicatorIOS,
  Alert,
  Linking,
  ListView,
  Platform,
  ProgressBarAndroid,
  StyleSheet,
  Text,
  View,
} = ReactNative;

var sendSMS = require('./sendSMS');
var sendEmail = require('./sendEmail');

var Contacts = require('react-native-contacts');

var TimerMixin = require('react-timer-mixin');
var invariant = require('invariant');
var dismissKeyboard = require('dismissKeyboard');

var SearchBar = require('./SearchBar');
var ContactCell = require('./ContactCell');

// Results should be cached keyed by the query
// with values of null meaning "being fetched"
// and anything besides null and undefined
// as the result of a valid query
var resultsCache = {
  dataForQuery: {},
  nextPageNumberForQuery: {},
  totalForQuery: {},
};

var LOADING = {};

var sortByKey = function (array, key) {
    return array.sort(function(a, b) {
        var x = a[key]; var y = b[key];
        return ((x < y) ? -1 : ((x > y) ? 1 : 0));
    });
};

var filterByKey = function(array, query, key) {
  return array.filter(function (obj) {
    if(String(obj[key]).toLowerCase().includes(query)) {
      return true;
    }
    else {
      return false;
    }
  });
};

var ContactScreen = React.createClass({
  mixins: [TimerMixin],

  timeoutID: (null: any),

  getInitialState: function() {
    return {
      isLoading: false,
      isLoadingTail: false,
      dataSource: new ListView.DataSource({
        rowHasChanged: (row1, row2) => row1 !== row2,
      }),
      filter: '',
      queryNumber: 0,
    };
  },

  componentDidMount: function() {
    this.searchContacts('');
    //console.log('location: ' + this.props.location + ' uri: ' + this.props.uri);
  },

  searchContacts: function(query: string) {
    this.timeoutID = null;

    this.setState({filter: query});

    var cachedResultsForQuery = resultsCache.dataForQuery[query];
    if (cachedResultsForQuery) {
      if (!LOADING[query]) {
        this.setState({
          dataSource: this.getDataSource(cachedResultsForQuery),
          isLoading: false
        });
      } else {
        this.setState({isLoading: true});
      }
      return;
    }

    LOADING[query] = true;
    resultsCache.dataForQuery[query] = null;
    this.setState({
      isLoading: true,
      queryNumber: this.state.queryNumber + 1,
      isLoadingTail: false,
    });

    Contacts.getAll( (err, contacts) => {
      if(err && err.type === 'permissionDenied'){
        LOADING[query] = false;
        resultsCache.dataForQuery[query] = undefined;

        this.setState({
          dataSource: this.getDataSource([]),
          isLoading: false,
        });
      }
      else{
        // filter contacts array by first OR last name match to query string
        if(query != '') { contacts = filterByKey(contacts, query, 'givenName'); }

        LOADING[query] = false;
        resultsCache.totalForQuery[query] = contacts.length;
        resultsCache.dataForQuery[query] = sortByKey(contacts, 'givenName');
        resultsCache.nextPageNumberForQuery[query] = 2;

        if (this.state.filter !== query) {
          // do not update state if the query is stale
          return;
        }

        this.setState({
          isLoading: false,
          dataSource: this.getDataSource(contacts),
        });
      }
    });
  },

  hasMore: function(): boolean {
    var query = this.state.filter;
    if (!resultsCache.dataForQuery[query]) {
      return true;
    }
    return (
      resultsCache.totalForQuery[query] !==
      resultsCache.dataForQuery[query].length
    );
  },

  onEndReached: function() {
    var query = this.state.filter;
    if (!this.hasMore() || this.state.isLoadingTail) {
      // We're already fetching or have all the elements so noop
      return;
    }

    if (LOADING[query]) {
      return;
    }

    LOADING[query] = true;
    this.setState({
      queryNumber: this.state.queryNumber + 1,
      isLoadingTail: true,
    });

    var page = resultsCache.nextPageNumberForQuery[query];
    invariant(page != null, 'Next page number for "%s" is missing', query);
    Contacts.getAll( (err, contacts) => {
      if(err && err.type === 'permissionDenied'){
        console.error(error);
        LOADING[query] = false;
        this.setState({
          isLoadingTail: false,
        });
      }
      else{
        var contactsForQuery = resultsCache.dataForQuery[query].slice();

        LOADING[query] = false;
        // We reached the end of the list before the expected number of results
        if (!contacts) {
          resultsCache.totalForQuery[query] = contactsForQuery.length;
        } else {
          for (var i in contacts) {
            contactsForQuery.push(contacts[i]);
          }
          resultsCache.dataForQuery[query] = contactsForQuery;
          resultsCache.nextPageNumberForQuery[query] += 1;
        }

        if (this.state.filter !== query) {
          // do not update state if the query is stale
          return;
        }

        this.setState({
          isLoadingTail: false,
          dataSource: this.getDataSource(resultsCache.dataForQuery[query]),
        });
      }
    });
  },

  getDataSource: function(contacts: Array<any>): ListView.DataSource {
    return this.state.dataSource.cloneWithRows(contacts);
  },

  selectContact: function(contact: Object) {
    var cell = contact.phoneNumbers.filter(function(obj) {
      if(obj.label == 'mobile' || obj.label == 'iPhone') {
        return true;
      }
      else {
        return false;
      }
    });
    if(cell.length == 0) { 
        Alert.alert(
            'Not Found',
            'Phone number not found for this contact',
        );
        return;
    }
    var email = contact.emailAddresses.filter(function(obj){
      if(obj.label == 'work' || obj.label == 'home') {
        return true;
      }
      else {
        return false;
      }
    });
    if(email.length == 0) { 
        Alert.alert(
            'Not Found',
            'Email not found for this contact',
        );
        return;
    }
    var emailAddress = email[0].email;
    for(var i = 0; i < email.length; i++){
      if(email[i].label == 'work') emailAddress = email[i].email;
    }
    sendEmail(emailAddress, this.props.uri, this.props.location);
    sendSMS(cell[0].number);
  },

  onSearchChange: function(event: Object) {
    var filter = event.nativeEvent.text.toLowerCase();

    this.clearTimeout(this.timeoutID);
    this.timeoutID = this.setTimeout(() => this.searchContacts(filter), 100);
  },

  renderFooter: function() {
    if (!this.hasMore() || !this.state.isLoadingTail) {
      return <View style={styles.scrollSpinner} />;
    }
    if (Platform.OS === 'ios') {
      return <ActivityIndicatorIOS style={styles.scrollSpinner} />;
    } else {
      return (
        <View  style={{alignItems: 'center'}}>
          <ProgressBarAndroid styleAttr="Large"/>
        </View>
      );
    }
  },

  renderSeparator: function(
    sectionID: number | string,
    rowID: number | string,
    adjacentRowHighlighted: boolean
  ) {
    var style = styles.rowSeparator;
    if (adjacentRowHighlighted) {
        style = [style, styles.rowSeparatorHide];
    }
    return (
      <View key={'SEP_' + sectionID + '_' + rowID}  style={style}/>
    );
  },

  renderRow: function(
    contact: Object,
    sectionID: number | string,
    rowID: number | string,
    highlightRowFunc: (sectionID: ?number | string, rowID: ?number | string) => void,
  ) {
    return (
      <ContactCell
        key={contact.recordID}
        onSelect={() => this.selectContact(contact)}
        onHighlight={() => highlightRowFunc(sectionID, rowID)}
        onUnhighlight={() => highlightRowFunc(null, null)}
        contact={contact}
      />
    );
  },

  render: function() {
    var content = this.state.dataSource.getRowCount() === 0 ?
      <NoContacts
        filter={this.state.filter}
        isLoading={this.state.isLoading}
      /> :
      <ListView
        ref="listview"
        renderSeparator={this.renderSeparator}
        dataSource={this.state.dataSource}
        renderFooter={this.renderFooter}
        renderRow={this.renderRow}
        onEndReached={this.onEndReached}
        automaticallyAdjustContentInsets={false}
        keyboardDismissMode="on-drag"
        keyboardShouldPersistTaps={true}
        showsVerticalScrollIndicator={false}
      />;

    return (
      <View style={styles.container}>
        <SearchBar
          onSearchChange={this.onSearchChange}
          isLoading={this.state.isLoading}
          onFocus={() =>
            this.refs.listview && this.refs.listview.getScrollResponder().scrollTo({ x: 0, y: 0 })}
        />
        <View style={styles.separator} />{content}</View>
    );
  },
});

var NoContacts = React.createClass({
  render: function() {
    var text = '';
    if (this.props.filter) {
      text = `No results for "${this.props.filter}"`;
    } else if (!this.props.isLoading) {
      // If we're looking at the latest contacts, aren't currently loading, and
      // still have no results, show a message
      text = 'No contacts found';
    }

    return (
      <View style={[styles.container, styles.centerText]}>
        <Text style={styles.noContactsText}>{text}</Text>
      </View>
    );
  }
});

var styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: 'white',
  },
  centerText: {
    alignItems: 'center',
  },
  noContactsText: {
    marginTop: 80,
    color: '#888888',
  },
  separator: {
    height: 1,
    backgroundColor: '#eeeeee',
  },
  scrollSpinner: {
    marginVertical: 20,
  },
  rowSeparator: {
    backgroundColor: 'rgba(0, 0, 0, 0.1)',
    height: 1,
    marginLeft: 4,
  },
  rowSeparatorHide: {
    opacity: 0.0,
  },
});

module.exports = ContactScreen;

@ericcbohn any update on this? I'm experiencing a similar issue

Might be related to thumbnails. On one of our devices we had huge addressbook (few thousands contacts), and our app wasnt been able to read that at all, always crashing inside getAll(). When I commented out https://github.com/rt2zz/react-native-contacts/blob/master/ios/RCTContacts/RCTContacts.m#L162 line, it started to work. Didnt investigate further yet.

Hi

I'm also seeing the Communications error. I've tracked it down to react-native-contacts, if I comment out calls to getAll it doesn't happen. Interestingly, it only happens when there are a lot (like almost 5000) contacts in the users address book. Is there a memory leak possibly? @nopik has a point about the thumbnails, almost everyone of these contacts has a thumbnail associated to it. The other possibility is that it's a problem with just one of the thumbnails i suppose, and the large address book is a red herring?

When the app crashes, there's no error message at all other than the Communications error, and then a few seconds later it just stops - its been really difficult tracking this down.

It's just an idea, but rather than loading the whole thing into memory, perhaps streaming the contacts back one by one would be a better api for large address books. If the client wants them in an array, they can just collect them up themselves. Thoughts? I'll try it tomorrow.

Think this is the same issue as https://github.com/rt2zz/react-native-contacts/issues/72.

I can confirm commenting out the iOS code that reads the thumbnail works around the issue.

nice work. I'm not an iOS expert so I'll lurk and learn.

Indeed, this seems to be the same as #72. I've been debugging this today, and found out that mkstemp() is creating new empty file & returning file handle, but the code is ignoring this. So, if many thumbnails are created, iOS runs out of file handles eventually. Quick and dirty fix of close(mkstemp(template)), as in https://github.com/IDTLabs/react-native-contacts

Is that fix dirty? Or the right way to do it? If the latter I'll add it in. I looked into this a while ago now but couldn't see the issue.

Well, technically it is proper, and adding the fix is improving the code a lot, with no drawbacks.

What I don't like about the current solution, is that it creates and leaves empty file on the filesystem. And, for each entry there is another .png file next to it with the actual data. And, it seems, that those data are never cleaned (though I think they can be cleaned from RN app after thumbnail is not needed anymore).

Some mkstemps() usage instead of mkstemp() could be encouraged to avoid second file (just create .png version of name in one go). But, it is not relevant for this bug, rather an optimization to the prior code.

Why is it creating empty files!?

mkstemp() creates & opens file and return the handle of that opened file to the program. This is correct behavior in posix, to avoid race between 2 threads. In our case, however, we just want to randomize path, we do not need that empty file. So, maybe mkstemp() should not be used at all. Original code left that file opened (leaking file handle), adding close() at least releases the handle.

Ok I'm going to try your fix out today on my fork and then I'll merge it over. I'll double check the new android implementation to.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

caglardurmus picture caglardurmus  路  5Comments

grit96 picture grit96  路  6Comments

rafaumlemos picture rafaumlemos  路  4Comments

msutyak picture msutyak  路  9Comments

fabriziomoscon picture fabriziomoscon  路  8Comments