{"action":"created","comment":{"url":"https://api.github.com/repos/opendatakit/collect/pulls/comments/199363475","pull_request_review_id":133467393,"id":199363475,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDE5OTM2MzQ3NQ==","diff_hunk":"@@ -0,0 +1,88 @@\n+package org.odk.collect.android.tasks.sms;\n+\n+import android.app.PendingIntent;\n+import android.content.Context;\n+import android.content.SharedPreferences;\n+import android.preference.PreferenceManager;\n+import android.telephony.PhoneNumberUtils;\n+import android.telephony.SmsManager;\n+\n+import org.odk.collect.android.application.Collect;\n+import org.odk.collect.android.events.RxEventBus;\n+import org.odk.collect.android.events.SmsEvent;\n+import org.odk.collect.android.preferences.PreferenceKeys;\n+import org.odk.collect.android.tasks.sms.contracts.SmsSubmissionManagerContract;\n+import org.odk.collect.android.tasks.sms.models.Message;\n+import org.odk.collect.android.tasks.sms.models.MessageStatus;\n+import org.odk.collect.android.tasks.sms.models.SmsSubmission;\n+\n+import java.util.ArrayList;\n+import java.util.Locale;\n+\n+import javax.inject.Inject;\n+\n+import timber.log.Timber;\n+\n+import static org.odk.collect.android.tasks.sms.SmsUtils.getDeliveryPendingIntent;\n+import static org.odk.collect.android.tasks.sms.SmsUtils.getSentPendingIntent;\n+\n+public class SmsSender {\n+    private final Context context;\n+    private final String instanceId;\n+\n+    @Inject\n+    SmsManager smsManager;\n+    @Inject\n+    SmsSubmissionManagerContract submissionManager;\n+    @Inject\n+    RxEventBus eventBus;\n+\n+    public SmsSender(Context context, String instanceId) {\n+        this.context = context;\n+        this.instanceId = instanceId;\n+\n+        Collect.getInstance().getComponent().inject(this);\n+    }\n+\n+    public boolean send() {\n+\n+        SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context);\n+\n+        String gateway = settings.getString(PreferenceKeys.KEY_SMS_GATEWAY, null);\n+\n+        if (!PhoneNumberUtils.isGlobalPhoneNumber(gateway)) {\n+            eventBus.post(new SmsEvent(instanceId, MessageStatus.InvalidGateway));\n+            return false;\n+        }\n+\n+        SmsSubmission model = submissionManager.getSubmissionModel(instanceId);\n+\n+        ArrayList<PendingIntent> sentIntents = new ArrayList<>();\n+        ArrayList<PendingIntent> deliveryIntents = new ArrayList<>();\n+        ArrayList<String> messages = new ArrayList<>();\n+\n+        for (Message message : model.getMessages()) {\n+\n+            if (message.isSent() || message.isSent()) {\n+                continue;\n+            }\n+\n+            PendingIntent sentPendingIntent = getSentPendingIntent(context, instanceId, message.getId());\n+            sentIntents.add(sentPendingIntent);\n+\n+            PendingIntent deliveryPendingIntent = getDeliveryPendingIntent(context, instanceId, message.getId());\n+            deliveryIntents.add(deliveryPendingIntent);\n+\n+            messages.add(message.getText());\n+\n+            submissionManager.markMessageAsSending(instanceId, message.getId());\n+        }\n+\n+        smsManager.sendMultipartTextMessage(gateway, null, messages, sentIntents, deliveryIntents);\n+\n+        String log = String.format(Locale.getDefault(), \"Sending a SMS of instance id %s\", instanceId);\n+        Timber.i(log);","path":"collect_app/src/main/java/org/odk/collect/android/tasks/sms/SmsSender.java","position":84,"original_position":84,"commit_id":"8362b5fccf6a42b361513a3f523914a3e86f1fa9","original_commit_id":"bb3ecda027a3c050eb32c7533ae35a0daf6f5422","user":{"login":"jd-alexander","id":1509205,"node_id":"MDQ6VXNlcjE1MDkyMDU=","avatar_url":"https://avatars1.githubusercontent.com/u/1509205?v=4","gravatar_id":"","url":"https://api.github.com/users/jd-alexander","html_url":"https://github.com/jd-alexander","followers_url":"https://api.github.com/users/jd-alexander/followers","following_url":"https://api.github.com/users/jd-alexander/following{/other_user}","gists_url":"https://api.github.com/users/jd-alexander/gists{/gist_id}","starred_url":"https://api.github.com/users/jd-alexander/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jd-alexander/subscriptions","organizations_url":"https://api.github.com/users/jd-alexander/orgs","repos_url":"https://api.github.com/users/jd-alexander/repos","events_url":"https://api.github.com/users/jd-alexander/events{/privacy}","received_events_url":"https://api.github.com/users/jd-alexander/received_events","type":"User","site_admin":false},"body":"Yeah, I did that because I was getting a lint error I think about using `String.format` inside Timber. I am gonna remove this variable and double check that.","created_at":"2018-07-01T22:19:59Z","updated_at":"2018-07-01T22:20:00Z","html_url":"https://github.com/opendatakit/collect/pull/2163#discussion_r199363475","pull_request_url":"https://api.github.com/repos/opendatakit/collect/pulls/2163","author_association":"COLLABORATOR","_links":{"self":{"href":"https://api.github.com/repos/opendatakit/collect/pulls/comments/199363475"},"html":{"href":"https://github.com/opendatakit/collect/pull/2163#discussion_r199363475"},"pull_request":{"href":"https://api.github.com/repos/opendatakit/collect/pulls/2163"}},"in_reply_to_id":198460721},"pull_request":{"url":"https://api.github.com/repos/opendatakit/collect/pulls/2163","id":184703070,"node_id":"MDExOlB1bGxSZXF1ZXN0MTg0NzAzMDcw","html_url":"https://github.com/opendatakit/collect/pull/2163","diff_url":"https://github.com/opendatakit/collect/pull/2163.diff","patch_url":"https://github.com/opendatakit/collect/pull/2163.patch","issue_url":"https://api.github.com/repos/opendatakit/collect/issues/2163","number":2163,"state":"open","locked":false,"title":"SMS submission","user":{"login":"jd-alexander","id":1509205,"node_id":"MDQ6VXNlcjE1MDkyMDU=","avatar_url":"https://avatars1.githubusercontent.com/u/1509205?v=4","gravatar_id":"","url":"https://api.github.com/users/jd-alexander","html_url":"https://github.com/jd-alexander","followers_url":"https://api.github.com/users/jd-alexander/followers","following_url":"https://api.github.com/users/jd-alexander/following{/other_user}","gists_url":"https://api.github.com/users/jd-alexander/gists{/gist_id}","starred_url":"https://api.github.com/users/jd-alexander/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jd-alexander/subscriptions","organizations_url":"https://api.github.com/users/jd-alexander/orgs","repos_url":"https://api.github.com/users/jd-alexander/repos","events_url":"https://api.github.com/users/jd-alexander/events{/privacy}","received_events_url":"https://api.github.com/users/jd-alexander/received_events","type":"User","site_admin":false},"body":"Closes #242\r\n\r\nThis PR is a WIP of the whole feature implementation. It contains the components that are required for sending a form as an SMS to a destination. Below is a diagram of the architecture that was designed.\r\n\r\n![sms architecture 2](https://user-images.githubusercontent.com/1509205/39378120-f071d3b8-4a46-11e8-8f9f-9db8e6c473db.png)\r\n\r\nThe architecture revolves around the [SMS Service](https://github.com/jd-alexander/collect/blob/sms_submission/collect_app/src/main/java/org/odk/collect/android/tasks/sms/SmsService.java) that acts as the main component with the responsibility for sending forms and dispatching events to the UI. \r\n\r\nThis design was chosen for due to the nature of how SMS functionality works on devices and how form submissions should be handled based on the discussions on the [ODK forum](https://forum.opendatakit.org/t/send-submissions-via-sms/12166/34). Some points that stood out from the discussion that inspired this approach are : \r\n\r\n- The case for proper background sending due to the number of messages that need to be sent when multiple form submissions are in place. [details](https://forum.opendatakit.org/t/send-submissions-via-sms/12166/32)\r\n- The ability to track each message to ensure that it's sent and no data is lost. [details](https://forum.opendatakit.org/t/send-submissions-via-sms/12166/24)\r\n-  Network outages, insufficient funds for sending SMS & dead batteries. \r\n- Sending one message at a time so that submissions can be cancelled, and messages are only be sent if there's success with the previous one.\r\n\r\nWhen a form upload is triggered the first thing that takes place is it's persistence to storage.\r\nThe [SMS Submission Manager](https://github.com/jd-alexander/collect/blob/sms_submission/collect_app/src/main/java/org/odk/collect/android/tasks/sms/SmsSubmissionManagerImpl.java) is a persistence layer powered by Shared Preferences that takes the divided message from the SMS Manager and creates a submission so that the messages and their state can be tracked. This tracking allows the app to remember the progress of a form upload after restarts or when the app closes. This eliminates possibility for loss of data and ensures that the events for the UI that can be informative such as the progress of the submission.\r\n\r\nOnce the messages have been saved [a background job](https://github.com/jd-alexander/collect/blob/sms_submission/collect_app/src/main/java/org/odk/collect/android/tasks/sms/SmsSenderJob.java) to send the message is then created and the job is added to the queue. Background jobs were utilized due to dynamic behavior that's expected when it comes unto submissions. There are several different constraints that are necessary to send a job and most importantly to retry when there's failure. To accomplish this task I opted for a [Job Queue component](https://github.com/jd-alexander/android-priority-jobqueue) which I forked, updated to simplify its dependencies. Using a job queue allows us to specify those constraints and ensure that a text is only sent when necessary. This also allows us to save resources because a message won't be resent if it has failed due to a lack of network. A `NetworkListener` could be plugged into the job queue with ease to only fire a message when the device has regained signal, the same can be done if the device was in airplane mode. Another important point is the execution of jobs can be controlled as the job manager can be instructed to only execute `x` amount of jobs so that the app isn't flooded with too many events and failures can be handled more gracefully.  \r\n\r\nOnce the background job sends a message, the [SentBroadcastReceiver](https://github.com/jd-alexander/collect/blob/sms_submission/collect_app/src/main/java/org/odk/collect/android/tasks/sms/SentBroadcastReceiver.java) is triggered when it was successfully sent. Each message is tagged with a unique id that identifies each `PendingIntent` so each message can be marked as sending/sent and the system can react likewise. \r\n\r\nThe results from the `BroadcastReceiver` are then processed by the `SMSService` so that the app can react based on the result. Some of the reactions are : \r\n\r\n- If a message was sent, then check to see if there's another message that needs to be sent, add it to the queue. (Implemented). \r\n- If the device currently has no signal then the `JobManager` can delay the job and attach a network listener so that the submission can continue later ( Not Yet Implemented)\r\n- If there's a generic failure of a message then the user is updated via the UI and the submission is halted until the user triggers it again. (Not Yet Implemented) \r\n\r\nThe UI layer for this hasn't been built out as yet because I wanted to have a discussion about how it should be done. The approach I had in mind was to simply extend the row template of the `InstanceUploaderList`. It would be able to : \r\n- Display the progress of a form upload by listening to events from the `SMSService`. \r\n- Display buttons that are able to cancel the form submission or retry when necessary. \r\n- Update the existing status that's set by the `InstancesDao`\r\n\r\nI realized that with the current app instances are selected and then taken to another screen for upload. With this architecture design, the user would simply see updates on the list items. \r\n\r\nLet me know your thoughts on this so I can know if I should proceed in this manner. \r\n\r\n#### What has been done to verify that this works as intended?\r\nAll layers have been tested. [Tests](https://github.com/jd-alexander/collect/tree/sms_submission/collect_app/src/test/java/org/odk/collect/android/sms)\r\n\r\n- There are unit tests for the `SubmissionManager` that checks if a submission can be created, deleted and if messages can be marked as sent while returning the next message that needs to be sent.\r\n\r\n- The `SmsSenderJob` has been tested as well to ensure that when the job is added and later executed the message has been sent. \r\n\r\n- The `SentBroadcastReceiver` was verified to be able to respond to the intent created by the SMS Job and its registration within the manifest was verified. \r\n\r\n- The `SmsService` was tested by submitting a form using mocked versions of all the subcomponents above. \r\n\r\nMore tests are coming :) \r\n\r\n#### Why is this the best possible solution? Were any other approaches considered?\r\nThis is the best solution because there are several specifications, shortfalls, and issues that can arise when using SMS as a transportation layer. Nonetheless, most of these issues can be easily mitigated with a proper architecture that primarily addresses the handling of failures and this approach was designed for just that. Also, the nature of this design ensures that it can be easily extended at each layer. The `JobManager` is the most used in the Android community and has several configurations for grouping and many retry policies. The `SubmissionManager` is powered by SharedPreferences because the data being stored for it is temporarily and only necessary to track a submission while it's in progress. I thought to extend the `Instances` database but that would break a lot of things and take a \r\n lot more time so I opted for this approach instead. Nonetheless, it's backed by a contract so if necessary an `SQLite `implementation could be used and a script could be written for migration. The other approaches that were considered were the implementations present within Google's SMS app and the approach by Medic. While these approaches work, they aren't fully compatible with the requirements and discussions taking place on the forum so I used them as a guide.\r\n\r\n**UPDATE**\r\nI was taking a closer look at the job manager I used and I am comparing it with the [job manager that Evernote maintains](https://github.com/evernote/android-job) and I realized it's more actively maintained. I had used the other one in the past but the maintainer doesn't do much on it so I might switch it out for this one also. \r\n\r\n#### Are there any risks to merging this code? If so, what are they?\r\nNot at this moment. This entire architecture is self-contained and will only have interactions with the other components when the UI layer is built out. \r\n\r\n#### Do we need any specific form for testing your changes? If so, please attach one.\r\n[Elephant Death.xml.txt](https://github.com/opendatakit/collect/files/2085321/Elephant.Death.xml.txt)\r\n\r\n\r\n#### Does this change require updates to documentation? If so, please file an issue at https://github.com/opendatakit/docs/issues/new and include the link below.","created_at":"2018-04-27T19:05:57Z","updated_at":"2018-07-01T22:20:00Z","closed_at":null,"merged_at":null,"merge_commit_sha":"8f7e047a63de9ad6c66e82ea3ec61c5c95141c7f","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/opendatakit/collect/pulls/2163/commits","review_comments_url":"https://api.github.com/repos/opendatakit/collect/pulls/2163/comments","review_comment_url":"https://api.github.com/repos/opendatakit/collect/pulls/comments{/number}","comments_url":"https://api.github.com/repos/opendatakit/collect/issues/2163/comments","statuses_url":"https://api.github.com/repos/opendatakit/collect/statuses/8362b5fccf6a42b361513a3f523914a3e86f1fa9","head":{"label":"jd-alexander:sms_submission","ref":"sms_submission","sha":"8362b5fccf6a42b361513a3f523914a3e86f1fa9","user":{"login":"jd-alexander","id":1509205,"node_id":"MDQ6VXNlcjE1MDkyMDU=","avatar_url":"https://avatars1.githubusercontent.com/u/1509205?v=4","gravatar_id":"","url":"https://api.github.com/users/jd-alexander","html_url":"https://github.com/jd-alexander","followers_url":"https://api.github.com/users/jd-alexander/followers","following_url":"https://api.github.com/users/jd-alexander/following{/other_user}","gists_url":"https://api.github.com/users/jd-alexander/gists{/gist_id}","starred_url":"https://api.github.com/users/jd-alexander/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jd-alexander/subscriptions","organizations_url":"https://api.github.com/users/jd-alexander/orgs","repos_url":"https://api.github.com/users/jd-alexander/repos","events_url":"https://api.github.com/users/jd-alexander/events{/privacy}","received_events_url":"https://api.github.com/users/jd-alexander/received_events","type":"User","site_admin":false},"repo":{"id":128535639,"node_id":"MDEwOlJlcG9zaXRvcnkxMjg1MzU2Mzk=","name":"collect","full_name":"jd-alexander/collect","owner":{"login":"jd-alexander","id":1509205,"node_id":"MDQ6VXNlcjE1MDkyMDU=","avatar_url":"https://avatars1.githubusercontent.com/u/1509205?v=4","gravatar_id":"","url":"https://api.github.com/users/jd-alexander","html_url":"https://github.com/jd-alexander","followers_url":"https://api.github.com/users/jd-alexander/followers","following_url":"https://api.github.com/users/jd-alexander/following{/other_user}","gists_url":"https://api.github.com/users/jd-alexander/gists{/gist_id}","starred_url":"https://api.github.com/users/jd-alexander/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jd-alexander/subscriptions","organizations_url":"https://api.github.com/users/jd-alexander/orgs","repos_url":"https://api.github.com/users/jd-alexander/repos","events_url":"https://api.github.com/users/jd-alexander/events{/privacy}","received_events_url":"https://api.github.com/users/jd-alexander/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/jd-alexander/collect","description":"ODK Collect is an Android app for filling out forms. It's been used to collect billions of data points in challenging environments around the world. Contribute and make the world a better place! ✨📋✨","fork":true,"url":"https://api.github.com/repos/jd-alexander/collect","forks_url":"https://api.github.com/repos/jd-alexander/collect/forks","keys_url":"https://api.github.com/repos/jd-alexander/collect/keys{/key_id}","collaborators_url":"https://api.github.com/repos/jd-alexander/collect/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/jd-alexander/collect/teams","hooks_url":"https://api.github.com/repos/jd-alexander/collect/hooks","issue_events_url":"https://api.github.com/repos/jd-alexander/collect/issues/events{/number}","events_url":"https://api.github.com/repos/jd-alexander/collect/events","assignees_url":"https://api.github.com/repos/jd-alexander/collect/assignees{/user}","branches_url":"https://api.github.com/repos/jd-alexander/collect/branches{/branch}","tags_url":"https://api.github.com/repos/jd-alexander/collect/tags","blobs_url":"https://api.github.com/repos/jd-alexander/collect/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/jd-alexander/collect/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/jd-alexander/collect/git/refs{/sha}","trees_url":"https://api.github.com/repos/jd-alexander/collect/git/trees{/sha}","statuses_url":"https://api.github.com/repos/jd-alexander/collect/statuses/{sha}","languages_url":"https://api.github.com/repos/jd-alexander/collect/languages","stargazers_url":"https://api.github.com/repos/jd-alexander/collect/stargazers","contributors_url":"https://api.github.com/repos/jd-alexander/collect/contributors","subscribers_url":"https://api.github.com/repos/jd-alexander/collect/subscribers","subscription_url":"https://api.github.com/repos/jd-alexander/collect/subscription","commits_url":"https://api.github.com/repos/jd-alexander/collect/commits{/sha}","git_commits_url":"https://api.github.com/repos/jd-alexander/collect/git/commits{/sha}","comments_url":"https://api.github.com/repos/jd-alexander/collect/comments{/number}","issue_comment_url":"https://api.github.com/repos/jd-alexander/collect/issues/comments{/number}","contents_url":"https://api.github.com/repos/jd-alexander/collect/contents/{+path}","compare_url":"https://api.github.com/repos/jd-alexander/collect/compare/{base}...{head}","merges_url":"https://api.github.com/repos/jd-alexander/collect/merges","archive_url":"https://api.github.com/repos/jd-alexander/collect/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/jd-alexander/collect/downloads","issues_url":"https://api.github.com/repos/jd-alexander/collect/issues{/number}","pulls_url":"https://api.github.com/repos/jd-alexander/collect/pulls{/number}","milestones_url":"https://api.github.com/repos/jd-alexander/collect/milestones{/number}","notifications_url":"https://api.github.com/repos/jd-alexander/collect/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/jd-alexander/collect/labels{/name}","releases_url":"https://api.github.com/repos/jd-alexander/collect/releases{/id}","deployments_url":"https://api.github.com/repos/jd-alexander/collect/deployments","created_at":"2018-04-07T14:22:42Z","updated_at":"2018-06-06T03:39:47Z","pushed_at":"2018-06-27T23:36:40Z","git_url":"git://github.com/jd-alexander/collect.git","ssh_url":"git@github.com:jd-alexander/collect.git","clone_url":"https://github.com/jd-alexander/collect.git","svn_url":"https://github.com/jd-alexander/collect","homepage":"https://docs.opendatakit.org/collect-intro","size":40871,"stargazers_count":0,"watchers_count":0,"language":"Java","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":0,"license":{"key":"other","name":"Other","spdx_id":null,"url":null,"node_id":"MDc6TGljZW5zZTA="},"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"opendatakit:master","ref":"master","sha":"d008b039e235e57d6daaf5b44f6c227471f9122b","user":{"login":"opendatakit","id":6222985,"node_id":"MDEyOk9yZ2FuaXphdGlvbjYyMjI5ODU=","avatar_url":"https://avatars0.githubusercontent.com/u/6222985?v=4","gravatar_id":"","url":"https://api.github.com/users/opendatakit","html_url":"https://github.com/opendatakit","followers_url":"https://api.github.com/users/opendatakit/followers","following_url":"https://api.github.com/users/opendatakit/following{/other_user}","gists_url":"https://api.github.com/users/opendatakit/gists{/gist_id}","starred_url":"https://api.github.com/users/opendatakit/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/opendatakit/subscriptions","organizations_url":"https://api.github.com/users/opendatakit/orgs","repos_url":"https://api.github.com/users/opendatakit/repos","events_url":"https://api.github.com/users/opendatakit/events{/privacy}","received_events_url":"https://api.github.com/users/opendatakit/received_events","type":"Organization","site_admin":false},"repo":{"id":40213809,"node_id":"MDEwOlJlcG9zaXRvcnk0MDIxMzgwOQ==","name":"collect","full_name":"opendatakit/collect","owner":{"login":"opendatakit","id":6222985,"node_id":"MDEyOk9yZ2FuaXphdGlvbjYyMjI5ODU=","avatar_url":"https://avatars0.githubusercontent.com/u/6222985?v=4","gravatar_id":"","url":"https://api.github.com/users/opendatakit","html_url":"https://github.com/opendatakit","followers_url":"https://api.github.com/users/opendatakit/followers","following_url":"https://api.github.com/users/opendatakit/following{/other_user}","gists_url":"https://api.github.com/users/opendatakit/gists{/gist_id}","starred_url":"https://api.github.com/users/opendatakit/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/opendatakit/subscriptions","organizations_url":"https://api.github.com/users/opendatakit/orgs","repos_url":"https://api.github.com/users/opendatakit/repos","events_url":"https://api.github.com/users/opendatakit/events{/privacy}","received_events_url":"https://api.github.com/users/opendatakit/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/opendatakit/collect","description":"ODK Collect is an Android app for filling out forms. It's been used to collect billions of data points in challenging environments around the world. Contribute and make the world a better place! ✨📋✨","fork":false,"url":"https://api.github.com/repos/opendatakit/collect","forks_url":"https://api.github.com/repos/opendatakit/collect/forks","keys_url":"https://api.github.com/repos/opendatakit/collect/keys{/key_id}","collaborators_url":"https://api.github.com/repos/opendatakit/collect/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/opendatakit/collect/teams","hooks_url":"https://api.github.com/repos/opendatakit/collect/hooks","issue_events_url":"https://api.github.com/repos/opendatakit/collect/issues/events{/number}","events_url":"https://api.github.com/repos/opendatakit/collect/events","assignees_url":"https://api.github.com/repos/opendatakit/collect/assignees{/user}","branches_url":"https://api.github.com/repos/opendatakit/collect/branches{/branch}","tags_url":"https://api.github.com/repos/opendatakit/collect/tags","blobs_url":"https://api.github.com/repos/opendatakit/collect/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/opendatakit/collect/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/opendatakit/collect/git/refs{/sha}","trees_url":"https://api.github.com/repos/opendatakit/collect/git/trees{/sha}","statuses_url":"https://api.github.com/repos/opendatakit/collect/statuses/{sha}","languages_url":"https://api.github.com/repos/opendatakit/collect/languages","stargazers_url":"https://api.github.com/repos/opendatakit/collect/stargazers","contributors_url":"https://api.github.com/repos/opendatakit/collect/contributors","subscribers_url":"https://api.github.com/repos/opendatakit/collect/subscribers","subscription_url":"https://api.github.com/repos/opendatakit/collect/subscription","commits_url":"https://api.github.com/repos/opendatakit/collect/commits{/sha}","git_commits_url":"https://api.github.com/repos/opendatakit/collect/git/commits{/sha}","comments_url":"https://api.github.com/repos/opendatakit/collect/comments{/number}","issue_comment_url":"https://api.github.com/repos/opendatakit/collect/issues/comments{/number}","contents_url":"https://api.github.com/repos/opendatakit/collect/contents/{+path}","compare_url":"https://api.github.com/repos/opendatakit/collect/compare/{base}...{head}","merges_url":"https://api.github.com/repos/opendatakit/collect/merges","archive_url":"https://api.github.com/repos/opendatakit/collect/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/opendatakit/collect/downloads","issues_url":"https://api.github.com/repos/opendatakit/collect/issues{/number}","pulls_url":"https://api.github.com/repos/opendatakit/collect/pulls{/number}","milestones_url":"https://api.github.com/repos/opendatakit/collect/milestones{/number}","notifications_url":"https://api.github.com/repos/opendatakit/collect/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/opendatakit/collect/labels{/name}","releases_url":"https://api.github.com/repos/opendatakit/collect/releases{/id}","deployments_url":"https://api.github.com/repos/opendatakit/collect/deployments","created_at":"2015-08-04T23:21:33Z","updated_at":"2018-06-30T00:56:45Z","pushed_at":"2018-07-01T21:26:46Z","git_url":"git://github.com/opendatakit/collect.git","ssh_url":"git@github.com:opendatakit/collect.git","clone_url":"https://github.com/opendatakit/collect.git","svn_url":"https://github.com/opendatakit/collect","homepage":"https://docs.opendatakit.org/collect-intro","size":40837,"stargazers_count":305,"watchers_count":305,"language":"Java","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":650,"mirror_url":null,"archived":false,"open_issues_count":208,"license":{"key":"other","name":"Other","spdx_id":null,"url":null,"node_id":"MDc6TGljZW5zZTA="},"forks":650,"open_issues":208,"watchers":305,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/opendatakit/collect/pulls/2163"},"html":{"href":"https://github.com/opendatakit/collect/pull/2163"},"issue":{"href":"https://api.github.com/repos/opendatakit/collect/issues/2163"},"comments":{"href":"https://api.github.com/repos/opendatakit/collect/issues/2163/comments"},"review_comments":{"href":"https://api.github.com/repos/opendatakit/collect/pulls/2163/comments"},"review_comment":{"href":"https://api.github.com/repos/opendatakit/collect/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/opendatakit/collect/pulls/2163/commits"},"statuses":{"href":"https://api.github.com/repos/opendatakit/collect/statuses/8362b5fccf6a42b361513a3f523914a3e86f1fa9"}},"author_association":"COLLABORATOR"}}
{"actor":{"display_login":"jd-alexander"}}