This is the continuation of “how I built the Friends Roster app” saga. If you missed the previous parts in this series, do check them out — part 1part 2part 3part 4part 5, part 6. To recap, I was describing how I built various screens using flutter.


The final screen in the app is the "next friend to call" screen. It has the meat of the logic. The first thing the app does is fetch the list of friends from Firebase Cloud Storage. Then it looks up the call log on your phone and updates all your friends call history. This is made possible by yet another Flutter plugin called call_log. The plugin lets you query the call logs on the phone (after getting the requisite permissions from the user in android). You can query by start date, end date and the duration of the call.


Call logs

In my code I fetch the call logs for the past one year where the call duration is more than 5 minutes. The assumption is that if you talked for more than 5 minutes, you had a good talk. As opposed to a quick chat and "I will call you later" kind of thing. Of course you can still mark the call as a completed in the app if you so desire. May be you don't like to talk much with this friend?


_updateFriendsWithCallHistory(List<Friend> friends) async {
  var now = DateTime.now();
  int from = now.subtract(Duration(days: 365))
      .millisecondsSinceEpoch;
  int to = now.millisecondsSinceEpoch;
  final callLogs = await CallLog.query(
    dateFrom: from,
    dateTo: to,
    durationFrom: 5 * 60,  // call duration > 5 minutes
  );

  List<CallLogEntry> entries = callLogs.toList().reversed.toList();
  await fireStoreUtil.addOrUpdateFireStoreFriends(
      context, entries, 
      (friend) => friend.name, (remoteFriends, entry) {
    if (remoteFriends.containsKey(entry.name)) {
      Friend friend = remoteFriends[entry.name];
      if (friend.updateCallInfo(entry)) {
        return FireStoreInfo(friend, FireStoreState.UPDATE);
      }
    }
    return FireStoreInfo(null, FireStoreState.NONE);
  }, false);
}


After updating the friends with call logs the app will try to find the next friend to call. I will not bore you with the details of the code, but all it does is it tries to find a friend that you should call next. This friend will be the one who you did not call for the longest time. The code is copy-pasted below if you like to figure out the logic. I highlighted some lines for you to help understand the idea.


_findNextFriendToCall(List<Friend> friends) {
  // 1. Find people who were never called and select someone randomly
  List<Friend> neverCalledFriends = [];
  List<Friend> calledFriends = [];
  friends.forEach((friend) {
    final lastCallInfo = friend.lastCallInfo;
    if (lastCallInfo == null) {
      neverCalledFriends.add(friend);
    } else if (lastCallInfo.state != CallState.UNKNOWN &&
        lastCallInfo.state != CallState.DELETED) {
      calledFriends.add(friend);
    }
  });
  if (neverCalledFriends.length > 0) {
    print('Found ${neverCalledFriends.length} friends who I never called');
    Random random = Random(clock.now().day);
    return neverCalledFriends[random.nextInt(neverCalledFriends.length)];
  }

  // 2. If we didn't find any then find the oldest missed call person who we
  //    missed called more than a month ago
  Friend friend = _findOldestFriend(calledFriends, CallState.MISSED, 30);
  if (friend != null) {
    print('Found oldest missed call friend ${friend.name}');
    return friend;
  }

  // 3. If we didn't find any then find the oldest skipped call person who we
  //    skipped more then 3 months ago
  friend = _findOldestFriend(calledFriends, CallState.SKIPPED, 90);
  if (friend != null) {
    print('Found oldest skipped friend ${friend.name}');
    return friend;
  }

  // 4. Find the person we called the longest time ago
  friend = _findOldestFriend(calledFriends, CallState.CALLED, 30);
  if (friend != null) {
    print('Found oldest called friend ${friend.name}');
    return friend;
  }

  print('Did not find any friends to call!');
  return null;
}

Friend _findOldestFriend(
    List<Friend> friends, CallState callState, int minDaysAgo) {
  Friend oldestFriend;
  DateTime oldestTime = clock.now();
  DateTime minDaysAgoDate = oldestTime.subtract(Duration(days: minDaysAgo));
  friends.forEach((friend) {
    DateTime date = friend.lastCallInfo.date;
    if (friend.lastCallInfo.state == callState &&
        date.isBefore(minDaysAgoDate) &&
        date.isBefore(oldestTime)) {
      oldestTime = date;
      oldestFriend = friend;
    }
  });
  return oldestFriend;
}


Calling the friend

Once the friend is found, it will show up as a card in the app. You have 4 buttons to take actions on the card.



The first one is OPEN button. Clicking on it will open your contacts app with the correct contact open for calling. From there you can just press the call button to call the friend. This is the only time I had to write my own plugin to open the contact in android. Perhaps there is already a plugin, but I wanted to try my hand at a simple one.


class MainActivity : FlutterActivity() {
  private val CHANNEL = "chandan.pitta.com/friends_roster"

  override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)
    MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
      .setMethodCallHandler {call, result -> if (call.method == "openContact") {
        val uri: Uri = Uri.withAppendedPath(
            ContactsContract.Contacts.CONTENT_URI, call.argument("id"))
        val intent = Intent(Intent.ACTION_VIEW, uri)
        startActivity(intent)
      } else {
        result.notImplemented()
      }
    }
  }
}


That simple plugin just starts an activity with a intent handler to open a contact. Basically in android that opens the requested contact in your contacts app. I did not bother to write an equivalent plugin for iOS because I don't need one.


Other actions

The rest of the actions "missed", "skipped" and "deleted" will just add a log to your call history. If you mark a friend as "missed" then the card will go away and show you another friend to call. Don't worry the missed call friend will show up again in a month. But more importantly when that friend calls you anytime, the log history is updated with that that information. So the friend will go to the bottom of the call list.


Likewise "skipped" will not show the friend for 3 months and finally "delete" will remove the friends from call list completely. These are friends from your contacts that you never want to call like some customer service number etc.


Conclusion

That concludes the app architecture in detail. But there is still more to come in this series. In the up coming posts I will describe other interesting features of flutter such as unit tests, feature tests, integration tests, code coverage and continuous builds.