{"action":"created","comment":{"url":"https://api.github.com/repos/dimagi/commcare-odk/pulls/comments/42581614","id":42581614,"diff_hunk":"@@ -1,82 +1,260 @@\n package org.commcare.android.util;\r\n \r\n+import android.content.Context;\r\n import android.graphics.Bitmap;\r\n import android.graphics.BitmapFactory;\r\n+import android.util.DisplayMetrics;\r\n+import android.util.Log;\r\n+import android.view.Display;\r\n+import android.view.WindowManager;\r\n \r\n-import org.commcare.android.javarosa.AndroidLogger;\r\n+import org.commcare.dalvik.preferences.DeveloperPreferences;\r\n import org.javarosa.core.model.data.GeoPointData;\r\n import org.javarosa.core.model.data.UncastData;\r\n import org.javarosa.core.reference.InvalidReferenceException;\r\n-import org.javarosa.core.reference.Reference;\r\n import org.javarosa.core.reference.ReferenceManager;\r\n-import org.javarosa.core.services.Logger;\r\n \r\n-import java.io.IOException;\r\n+import java.io.File;\r\n \r\n /**\r\n  * @author ctsims\r\n  */\r\n public class MediaUtil {\r\n+\r\n     public static final String FORM_VIDEO = \"video\";\r\n     public static final String FORM_AUDIO = \"audio\";\r\n     public static final String FORM_IMAGE = \"image\";\r\n-\tpublic static Bitmap getScaledImageFromReference(String jrReference) {\r\n-\t\treturn getScaledImageFromReference(jrReference, 1);\r\n-\t}\r\n-\t\r\n-\t/*\r\n-\t * returns the file referenced by jrReference scaled by scaleFactor. scaleFactor should be a power of two;\r\n-\t * for example, a scale factor of 4 returns an image that is 1/4 the width/height of the original, and 1/16th\r\n-\t * the size in pixels\r\n-\t*/\r\n-\tpublic static Bitmap getScaledImageFromReference(String jrReference, int scaleFactor) {\r\n-        //TODO: Eventually we'll want to be able to deal with dymanic resources here.\r\n-        try {\r\n \r\n-        Reference imageRef = ReferenceManager._().DeriveReference(jrReference);\r\n-        if(!imageRef.doesBinaryExist()) {\r\n+    /**\r\n+     *\r\n+     * @return A bitmap representation of the given image file, potentially adjusted from the\r\n+     * image's original size such that its width is no larger than containerWidth, and its height\r\n+     * is no larger than containerHeight\r\n+     */\r\n+    public static Bitmap getBitmapScaledToContainer(File imageFile, int containerHeight,\r\n+                                                    int containerWidth) {\r\n+        Log.i(\"10/15\", \"scaling down to height \" + containerHeight + \" and width \" + containerWidth);\r\n+        // Determine dimensions of original image\r\n+        BitmapFactory.Options o = new BitmapFactory.Options();\r\n+        o.inJustDecodeBounds = true;\r\n+        BitmapFactory.decodeFile(imageFile.getAbsolutePath(), o);\r\n+        int imageHeight = o.outHeight;\r\n+        int imageWidth = o.outWidth;\r\n+\r\n+        // Get a scale-down factor -- Powers of 2 work faster according to the docs, but we're\r\n+        // just doing closest size that still fills the screen\r\n+        int heightScale = Math.round((float) imageHeight / containerHeight);\r\n+        int widthScale = Math.round((float) imageWidth / containerWidth);\r\n+        int scale = Math.max(widthScale, heightScale);\r\n+        if (scale == 0) {\r\n+            // Rounding could possibly have resulted in a scale factor of 0, which is invalid\r\n+            scale = 1;\r\n+        }\r\n+\r\n+        return performSafeScaleDown(imageFile, scale, 0);\r\n+    }\r\n+\r\n+    /**\r\n+     * @return A scaled-down bitmap for the given image file, progressively increasing the\r\n+     * scale-down factor by 1 until allocating memory for the bitmap does not cause an OOM error\r\n+     */\r\n+    private static Bitmap performSafeScaleDown(File f, int scale, int depth) {\r\n+        if (depth == 5) {\r\n+            // Limit the number of recursive calls\r\n             return null;\r\n         }\r\n-        \r\n         BitmapFactory.Options options = new BitmapFactory.Options();\r\n+        options.inSampleSize = scale;\r\n+        try {\r\n+            return BitmapFactory.decodeFile(f.getAbsolutePath(), options);\r\n+        } catch (OutOfMemoryError e) {\r\n+            return performSafeScaleDown(f, scale + 1, depth + 1);\r\n+        }\r\n+    }\r\n+\r\n+    /**\r\n+     * @return A bitmap representation of the given image file, scaled up as close as possible to\r\n+     * desiredWidth and desiredHeight, without exceeding either boundingHeight or boundingWidth\r\n+     */\r\n+    private static Bitmap attemptBoundedScaleUp(File imageFile, int desiredHeight, int desiredWidth,\r\n+                                         int boundingHeight, int boundingWidth) {\r\n+        if (boundingHeight < desiredHeight || boundingWidth < desiredWidth) {\r\n+            float heightScale = ((float)boundingHeight) / desiredHeight;\r\n+            float widthScale = ((float)boundingWidth) / desiredWidth;\r\n+            float boundingScaleDownFactor = Math.min(heightScale, widthScale);\r\n+            desiredHeight = Math.round(desiredHeight * boundingScaleDownFactor);\r\n+            desiredWidth = Math.round(desiredWidth * boundingScaleDownFactor);\r\n+        }\r\n+        Log.i(\"10/15\", \"scaling up to height \" + desiredHeight + \" and width \" + desiredWidth);\r\n+        try {\r\n+            BitmapFactory.Options o = new BitmapFactory.Options();\r\n+            o.inScaled = false;\r\n+            Bitmap originalBitmap = BitmapFactory.decodeFile(imageFile.getAbsolutePath(), o);\r\n+            try {\r\n+                return Bitmap.createScaledBitmap(originalBitmap, desiredWidth, desiredHeight, false);\r\n+            } catch (OutOfMemoryError e) {\r\n+                return originalBitmap;\r\n+            }\r\n+        }\r\n+        catch (OutOfMemoryError e) {\r\n+            // Just inflating the image at its original size caused an OOM error, don't have a\r\n+            // choice but to scale down\r\n+            return performSafeScaleDown(imageFile, 2, 1);\r\n+        }\r\n+    }\r\n \r\n-        options.inSampleSize = scaleFactor;\r\n-        \r\n-        return BitmapFactory.decodeStream(imageRef.getStream(), null, options);\r\n+    public static Bitmap inflateDisplayImage(Context context, String jrUri) {\r\n+        return inflateDisplayImage(context, jrUri, -1, -1);\r\n+    }\r\n \r\n-        } catch(InvalidReferenceException ire) {\r\n-            Logger.log(AndroidLogger.TYPE_ERROR_CONFIG_STRUCTURE, \"Invalid reference for an image: \" + ire.getReferenceString());\r\n-            return null;\r\n-        } catch(OutOfMemoryError oom) {\r\n-            Logger.log(AndroidLogger.TYPE_ERROR_ASSERTION, \"Out of memory loading reference: \" + jrReference);\r\n+    /**\r\n+     * Attempts to inflate an image from a CommCare UI definition source.\r\n+     *\r\n+     * @param jrUri The image to inflate\r\n+     * @param boundingWidth the width of the container this image is being inflated into, to serve\r\n+     *                      as a max width. If passed in as -1, gets set to screen width\r\n+     * @param boundingHeight the height fo the container this image is being inflated into, to\r\n+     *                       serve as a max height. If passed in as -1, gets set to screen height\r\n+     * @return A bitmap if one could be created. Null if error occurs or the image is unavailable.\r\n+     */\r\n+    public static Bitmap inflateDisplayImage(Context context, String jrUri,\r\n+                                             int boundingWidth, int boundingHeight) {\r\n+        if (jrUri == null || jrUri.equals(\"\")) {\r\n             return null;\r\n-        } catch(IOException uie){\r\n-            Logger.log(AndroidLogger.TYPE_ERROR_ASSERTION, \"IO Exception loading reference: \" + jrReference);\r\n+        }\r\n+        try {\r\n+            String imageFilename = ReferenceManager._().DeriveReference(jrUri).getLocalURI();\r\n+            final File imageFile = new File(imageFilename);\r\n+            if (!imageFile.exists()) {\r\n+                return null;\r\n+            }\r\n+\r\n+            Display display = ((WindowManager)\r\n+                    context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();\r\n+            if (boundingHeight == -1) {\r\n+                boundingHeight = display.getHeight();\r\n+            }\r\n+            if (boundingWidth == -1) {\r\n+                boundingWidth = display.getWidth();\r\n+            }\r\n+            Log.i(\"10/15\", \"bounding height: \" + boundingHeight + \", bounding width: \" + boundingWidth);\r\n+\r\n+            if (DeveloperPreferences.isSmartInflationEnabled()) {\r\n+                // scale based on native density AND bounding dimens\r\n+                return getBitmapScaledForNativeDensity(context, jrUri, boundingHeight, boundingWidth,\r\n+                        DeveloperPreferences.getTargetInflationDensity());\r\n+            } else {\r\n+                // just scaling down if the original image is too big for its container\r\n+                return getBitmapScaledToContainer(imageFile, boundingHeight, boundingWidth);\r\n+            }\r\n+        } catch (InvalidReferenceException e) {\r\n+            Log.e(\"ImageInflater\", \"image invalid reference exception for \" + e.getReferenceString());\r\n+            e.printStackTrace();\r\n+        }\r\n+        return null;\r\n+    }\r\n+\r\n+    /**\r\n+     * Attempt to inflate an image source into a bitmap whose final dimensions are based upon\r\n+     * 2 factors:\r\n+     *\r\n+     * 1) The application of a scaling factor, which is derived from the relative values of the\r\n+     * target density declared by the app and the current device's actual density\r\n+     * 2) The absolute dimensions of the bounding container into which this image is being inflated\r\n+     * (may just be the screen dimens)\r\n+     *\r\n+     * @return the bitmap, or null if none could be created from the source\r\n+     *\r\n+     */\r\n+    private static Bitmap getBitmapScaledForNativeDensity(Context context, String jrUri,\r\n+                                                          int containerHeight, int containerWidth,\r\n+                                                          int targetDensity) {\r\n+        if (jrUri == null || jrUri.equals(\"\")) {\r\n             return null;\r\n         }\r\n+        try {\r\n+            String imageFilename = ReferenceManager._().DeriveReference(jrUri).getLocalURI();\r\n+            final File imageFile = new File(imageFilename);\r\n+            if (imageFile.exists()) {\r\n+                Log.i(\"10/15\", \"src path: \" + imageFilename);\r\n+\r\n+                BitmapFactory.Options o = new BitmapFactory.Options();\r\n+                o.inJustDecodeBounds = true;\r\n+                o.inScaled = false;\r\n+                BitmapFactory.decodeFile(imageFile.getAbsolutePath(), o);\r\n+                int imageHeight = o.outHeight;\r\n+                int imageWidth = o.outWidth;\r\n+                Log.i(\"10/15\", \"original height: \" + imageHeight + \", original width: \" + imageWidth);\r\n+\r\n+                double scaleFactor = computeInflationScaleFactor(context, targetDensity);\r\n+                int calculatedHeight = Math.round((float)(imageHeight * scaleFactor));\r\n+                int calculatedWidth = Math.round((float)(imageWidth * scaleFactor));\r\n+                Log.i(\"10/15\", \"calculated height: \" + calculatedHeight + \", calculated width: \" + calculatedWidth);\r\n+\r\n+                int boundingHeight = Math.min(containerHeight, calculatedHeight);\r\n+                int boundingWidth = Math.min(containerWidth, calculatedWidth);\r\n+\r\n+                if (boundingHeight < imageHeight || boundingWidth < imageWidth) {\r\n+                    // scaling down\r\n+                    return getBitmapScaledToContainer(imageFile, boundingHeight, boundingWidth);\r\n+                } else {\r\n+                    // scaling up\r\n+                    return attemptBoundedScaleUp(imageFile, calculatedHeight, calculatedWidth,\r\n+                            containerHeight, containerWidth);\r\n+                }\r\n+            }\r\n+        } catch (InvalidReferenceException e) {\r\n+            Log.e(\"ImageInflater\", \"image invalid reference exception for \" + e.getReferenceString());\r\n+            e.printStackTrace();\r\n+        }\r\n+        return null;\r\n+    }\r\n+\r\n+    private static double computeInflationScaleFactor(Context context, int targetDensity) {\r\n+        DisplayMetrics metrics = context.getResources().getDisplayMetrics();\r\n+        final int SCREEN_DENSITY = metrics.densityDpi;\r\n+\r\n+        double actualNativeScaleFactor = metrics.density;\r\n+        // The formula below is what Android *usually* uses to compute the value of metrics.density\r\n+        // for a device. If this is in fact the value being used, we are not interested in it.\r\n+        // However, if the actual value differs at all from the standard calculation, it means\r\n+        // Android is taking other factors into consideration (such as straight up screen size),\r\n+        // and we want to incorporate that proportionally into our own version of the scale factor\r\n+        double standardNativeScaleFactor = (double)SCREEN_DENSITY / DisplayMetrics.DENSITY_DEFAULT;\r\n+        double proportionalAdjustmentFactor = 1;\r\n+        if (actualNativeScaleFactor > standardNativeScaleFactor) {\r\n+            proportionalAdjustmentFactor = 1 +\r\n+                    ((actualNativeScaleFactor - standardNativeScaleFactor) / standardNativeScaleFactor);\r\n+        } else if (actualNativeScaleFactor < standardNativeScaleFactor) {\r\n+            proportionalAdjustmentFactor = actualNativeScaleFactor / standardNativeScaleFactor;\r\n+        }\r\n+        Log.i(\"10/15\", \"proportional adjustment factor: \" + proportionalAdjustmentFactor);\r\n+\r\n+        // Get our custom scale factor, based on this device's density and what the image's target\r\n+        // density was\r\n+        Log.i(\"10/15\", \"Target dpi: \" + targetDensity);\r\n+        Log.i(\"10/15\", \"This screen's dpi: \" + SCREEN_DENSITY);\r\n+        double customDpiScaleFactor = (double)SCREEN_DENSITY / targetDensity;\r\n+        Log.i(\"10/15\", \"dpi scale factor: \" + customDpiScaleFactor);\r\n+\r\n+        Log.i(\"10/15\", \"FINAL scale factor: \" + (customDpiScaleFactor * proportionalAdjustmentFactor));\r\n+        return customDpiScaleFactor * proportionalAdjustmentFactor;\r\n     }\r\n \r\n+\r\n     /**\r\n-     * Pass in a string representing either a GeoPont or an address and get back a valid\r\n+     * Pass in a string representing either a GeoPoint or an address and get back a valid\r\n      * GeoURI that can be passed as an intent argument \r\n      */\r\n     public static String getGeoIntentURI(String rawInput){\r","path":"app/src/org/commcare/android/util/MediaUtil.java","position":272,"original_position":279,"commit_id":"3dcf706bc2df1e89882a8f81b96b9b24f0275ab7","original_commit_id":"3e516ad8c1728ca45cf77e007c91778c3d1176e5","user":{"login":"amstone326","id":3723143,"avatar_url":"https://avatars.githubusercontent.com/u/3723143?v=3","gravatar_id":"","url":"https://api.github.com/users/amstone326","html_url":"https://github.com/amstone326","followers_url":"https://api.github.com/users/amstone326/followers","following_url":"https://api.github.com/users/amstone326/following{/other_user}","gists_url":"https://api.github.com/users/amstone326/gists{/gist_id}","starred_url":"https://api.github.com/users/amstone326/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/amstone326/subscriptions","organizations_url":"https://api.github.com/users/amstone326/orgs","repos_url":"https://api.github.com/users/amstone326/repos","events_url":"https://api.github.com/users/amstone326/events{/privacy}","received_events_url":"https://api.github.com/users/amstone326/received_events","type":"User","site_admin":false},"body":"Sweet, thanks!","created_at":"2015-10-21T04:05:51Z","updated_at":"2015-10-21T04:05:51Z","html_url":"https://github.com/dimagi/commcare-odk/pull/702#discussion_r42581614","pull_request_url":"https://api.github.com/repos/dimagi/commcare-odk/pulls/702","_links":{"self":{"href":"https://api.github.com/repos/dimagi/commcare-odk/pulls/comments/42581614"},"html":{"href":"https://github.com/dimagi/commcare-odk/pull/702#discussion_r42581614"},"pull_request":{"href":"https://api.github.com/repos/dimagi/commcare-odk/pulls/702"}}},"pull_request":{"url":"https://api.github.com/repos/dimagi/commcare-odk/pulls/702","id":47875922,"html_url":"https://github.com/dimagi/commcare-odk/pull/702","diff_url":"https://github.com/dimagi/commcare-odk/pull/702.diff","patch_url":"https://github.com/dimagi/commcare-odk/pull/702.patch","issue_url":"https://api.github.com/repos/dimagi/commcare-odk/issues/702","number":702,"state":"open","locked":false,"title":"Improve inflation of display images","user":{"login":"amstone326","id":3723143,"avatar_url":"https://avatars.githubusercontent.com/u/3723143?v=3","gravatar_id":"","url":"https://api.github.com/users/amstone326","html_url":"https://github.com/amstone326","followers_url":"https://api.github.com/users/amstone326/followers","following_url":"https://api.github.com/users/amstone326/following{/other_user}","gists_url":"https://api.github.com/users/amstone326/gists{/gist_id}","starred_url":"https://api.github.com/users/amstone326/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/amstone326/subscriptions","organizations_url":"https://api.github.com/users/amstone326/orgs","repos_url":"https://api.github.com/users/amstone326/repos","events_url":"https://api.github.com/users/amstone326/events{/privacy}","received_events_url":"https://api.github.com/users/amstone326/received_events","type":"User","site_admin":false},"body":"Summary: Technique for predictable inflation of user-provided display images, applied across CommCare wherever it is applicable (Qualification: Not actually everywhere yet, there are probably places I missed. @ctsims, Would like to do a pass with you to determine if there is anywhere else in the app that should be using this). \r\n\r\nDetails: The key method is getBitmapScaledForNativeDensity() in MediaUtil, which implements the following essential logic (I'm simplifying slightly in words, the code makes the nuances clear):\r\n-Users can set a per-app target density, which indicates the screen density they are designing their images for\r\n-We then apply a scaling technique to up- or down-scale images if they are being shown on a screen whose density is different from the target density\r\n-We also have the option of providing bounding dimensions for the inflation -- if provided, the image will never be scaled up to larger than those dimens. This is useful for not wasting memory on inflating/keeping around large bitmaps in instances in which we know that an image is going into a container of a certain size, which is frequent (images in the case list, icons in GridMediaView, etc.). If explicit bounding dimens are not provided, the screen's width and height are used.\r\n\r\nOther Important Notes:\r\n-This is currently disabled by default, and can be enabled in Developer Settings. You can then also set the desired target density in the Developer Settings. Did this to make it easy for ICDS and other projects to play around with different target densities combined with different original image sizes at will.\r\n-I have set it up such that this strategy and ResizingImageView cannot be applied in tandem -- If this strategy is turned off, ResizingImageView will still be applied, assuming the app has it turned on. If this strategy is enabled, ResizingImageView will not get used no matter what.\r\n\r\nOne less important note: There are currently 4 calls to inflateDisplayImage() where we are NOT providing explicit bounding dimens (and thus defaulting to the screen dimens). In 2 of them I think this is fine/correct because there doesn't seem to be any clear bounding box. But in the other 2, I think there probably is a clear boundary, but I couldn't figure out how exactly to get at them from the code available. Those 2 places are:\r\n1. MenuAdapter.getView(), which is where icons for menu lists are inflated (always going to be small)\r\n2. EntityDetailAdapter.setParams(), which is where images in a basic case detail are inflated (always going to be ~somewhat small I think?)\r\nBoth of these seem like they could benefit from restricting the size of the image inflation based on container. If anyone is doing a really thorough review of this and has thoughts on where to get those values in these 2 instances, let me know!","created_at":"2015-10-16T06:43:07Z","updated_at":"2015-10-21T04:05:51Z","closed_at":null,"merged_at":null,"merge_commit_sha":"809a5a441191069271ff6af233a9aab987277cde","assignee":{"login":"ctsims","id":155066,"avatar_url":"https://avatars.githubusercontent.com/u/155066?v=3","gravatar_id":"","url":"https://api.github.com/users/ctsims","html_url":"https://github.com/ctsims","followers_url":"https://api.github.com/users/ctsims/followers","following_url":"https://api.github.com/users/ctsims/following{/other_user}","gists_url":"https://api.github.com/users/ctsims/gists{/gist_id}","starred_url":"https://api.github.com/users/ctsims/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ctsims/subscriptions","organizations_url":"https://api.github.com/users/ctsims/orgs","repos_url":"https://api.github.com/users/ctsims/repos","events_url":"https://api.github.com/users/ctsims/events{/privacy}","received_events_url":"https://api.github.com/users/ctsims/received_events","type":"User","site_admin":false},"milestone":{"url":"https://api.github.com/repos/dimagi/commcare-odk/milestones/4","html_url":"https://github.com/dimagi/commcare-odk/milestones/commcare_2.24","labels_url":"https://api.github.com/repos/dimagi/commcare-odk/milestones/4/labels","id":1276470,"number":4,"title":"commcare_2.24","description":null,"creator":{"login":"phillipm","id":94817,"avatar_url":"https://avatars.githubusercontent.com/u/94817?v=3","gravatar_id":"","url":"https://api.github.com/users/phillipm","html_url":"https://github.com/phillipm","followers_url":"https://api.github.com/users/phillipm/followers","following_url":"https://api.github.com/users/phillipm/following{/other_user}","gists_url":"https://api.github.com/users/phillipm/gists{/gist_id}","starred_url":"https://api.github.com/users/phillipm/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/phillipm/subscriptions","organizations_url":"https://api.github.com/users/phillipm/orgs","repos_url":"https://api.github.com/users/phillipm/repos","events_url":"https://api.github.com/users/phillipm/events{/privacy}","received_events_url":"https://api.github.com/users/phillipm/received_events","type":"User","site_admin":false},"open_issues":6,"closed_issues":60,"state":"open","created_at":"2015-08-27T20:11:24Z","updated_at":"2015-10-21T03:12:10Z","due_on":null,"closed_at":null},"commits_url":"https://api.github.com/repos/dimagi/commcare-odk/pulls/702/commits","review_comments_url":"https://api.github.com/repos/dimagi/commcare-odk/pulls/702/comments","review_comment_url":"https://api.github.com/repos/dimagi/commcare-odk/pulls/comments{/number}","comments_url":"https://api.github.com/repos/dimagi/commcare-odk/issues/702/comments","statuses_url":"https://api.github.com/repos/dimagi/commcare-odk/statuses/3dcf706bc2df1e89882a8f81b96b9b24f0275ab7","head":{"label":"dimagi:image-resizing-by-dpi","ref":"image-resizing-by-dpi","sha":"3dcf706bc2df1e89882a8f81b96b9b24f0275ab7","user":{"login":"dimagi","id":94514,"avatar_url":"https://avatars.githubusercontent.com/u/94514?v=3","gravatar_id":"","url":"https://api.github.com/users/dimagi","html_url":"https://github.com/dimagi","followers_url":"https://api.github.com/users/dimagi/followers","following_url":"https://api.github.com/users/dimagi/following{/other_user}","gists_url":"https://api.github.com/users/dimagi/gists{/gist_id}","starred_url":"https://api.github.com/users/dimagi/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dimagi/subscriptions","organizations_url":"https://api.github.com/users/dimagi/orgs","repos_url":"https://api.github.com/users/dimagi/repos","events_url":"https://api.github.com/users/dimagi/events{/privacy}","received_events_url":"https://api.github.com/users/dimagi/received_events","type":"Organization","site_admin":false},"repo":{"id":20939877,"name":"commcare-odk","full_name":"dimagi/commcare-odk","owner":{"login":"dimagi","id":94514,"avatar_url":"https://avatars.githubusercontent.com/u/94514?v=3","gravatar_id":"","url":"https://api.github.com/users/dimagi","html_url":"https://github.com/dimagi","followers_url":"https://api.github.com/users/dimagi/followers","following_url":"https://api.github.com/users/dimagi/following{/other_user}","gists_url":"https://api.github.com/users/dimagi/gists{/gist_id}","starred_url":"https://api.github.com/users/dimagi/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dimagi/subscriptions","organizations_url":"https://api.github.com/users/dimagi/orgs","repos_url":"https://api.github.com/users/dimagi/repos","events_url":"https://api.github.com/users/dimagi/events{/privacy}","received_events_url":"https://api.github.com/users/dimagi/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/dimagi/commcare-odk","description":"","fork":false,"url":"https://api.github.com/repos/dimagi/commcare-odk","forks_url":"https://api.github.com/repos/dimagi/commcare-odk/forks","keys_url":"https://api.github.com/repos/dimagi/commcare-odk/keys{/key_id}","collaborators_url":"https://api.github.com/repos/dimagi/commcare-odk/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/dimagi/commcare-odk/teams","hooks_url":"https://api.github.com/repos/dimagi/commcare-odk/hooks","issue_events_url":"https://api.github.com/repos/dimagi/commcare-odk/issues/events{/number}","events_url":"https://api.github.com/repos/dimagi/commcare-odk/events","assignees_url":"https://api.github.com/repos/dimagi/commcare-odk/assignees{/user}","branches_url":"https://api.github.com/repos/dimagi/commcare-odk/branches{/branch}","tags_url":"https://api.github.com/repos/dimagi/commcare-odk/tags","blobs_url":"https://api.github.com/repos/dimagi/commcare-odk/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/dimagi/commcare-odk/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/dimagi/commcare-odk/git/refs{/sha}","trees_url":"https://api.github.com/repos/dimagi/commcare-odk/git/trees{/sha}","statuses_url":"https://api.github.com/repos/dimagi/commcare-odk/statuses/{sha}","languages_url":"https://api.github.com/repos/dimagi/commcare-odk/languages","stargazers_url":"https://api.github.com/repos/dimagi/commcare-odk/stargazers","contributors_url":"https://api.github.com/repos/dimagi/commcare-odk/contributors","subscribers_url":"https://api.github.com/repos/dimagi/commcare-odk/subscribers","subscription_url":"https://api.github.com/repos/dimagi/commcare-odk/subscription","commits_url":"https://api.github.com/repos/dimagi/commcare-odk/commits{/sha}","git_commits_url":"https://api.github.com/repos/dimagi/commcare-odk/git/commits{/sha}","comments_url":"https://api.github.com/repos/dimagi/commcare-odk/comments{/number}","issue_comment_url":"https://api.github.com/repos/dimagi/commcare-odk/issues/comments{/number}","contents_url":"https://api.github.com/repos/dimagi/commcare-odk/contents/{+path}","compare_url":"https://api.github.com/repos/dimagi/commcare-odk/compare/{base}...{head}","merges_url":"https://api.github.com/repos/dimagi/commcare-odk/merges","archive_url":"https://api.github.com/repos/dimagi/commcare-odk/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/dimagi/commcare-odk/downloads","issues_url":"https://api.github.com/repos/dimagi/commcare-odk/issues{/number}","pulls_url":"https://api.github.com/repos/dimagi/commcare-odk/pulls{/number}","milestones_url":"https://api.github.com/repos/dimagi/commcare-odk/milestones{/number}","notifications_url":"https://api.github.com/repos/dimagi/commcare-odk/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/dimagi/commcare-odk/labels{/name}","releases_url":"https://api.github.com/repos/dimagi/commcare-odk/releases{/id}","created_at":"2014-06-17T21:17:26Z","updated_at":"2015-05-06T16:04:54Z","pushed_at":"2015-10-21T03:11:41Z","git_url":"git://github.com/dimagi/commcare-odk.git","ssh_url":"git@github.com:dimagi/commcare-odk.git","clone_url":"https://github.com/dimagi/commcare-odk.git","svn_url":"https://github.com/dimagi/commcare-odk","homepage":null,"size":67731,"stargazers_count":3,"watchers_count":3,"language":"Java","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":8,"mirror_url":null,"open_issues_count":13,"forks":8,"open_issues":13,"watchers":3,"default_branch":"master"}},"base":{"label":"dimagi:master","ref":"master","sha":"6491cba8e4c0ebd164ab56237e4fd207af860c32","user":{"login":"dimagi","id":94514,"avatar_url":"https://avatars.githubusercontent.com/u/94514?v=3","gravatar_id":"","url":"https://api.github.com/users/dimagi","html_url":"https://github.com/dimagi","followers_url":"https://api.github.com/users/dimagi/followers","following_url":"https://api.github.com/users/dimagi/following{/other_user}","gists_url":"https://api.github.com/users/dimagi/gists{/gist_id}","starred_url":"https://api.github.com/users/dimagi/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dimagi/subscriptions","organizations_url":"https://api.github.com/users/dimagi/orgs","repos_url":"https://api.github.com/users/dimagi/repos","events_url":"https://api.github.com/users/dimagi/events{/privacy}","received_events_url":"https://api.github.com/users/dimagi/received_events","type":"Organization","site_admin":false},"repo":{"id":20939877,"name":"commcare-odk","full_name":"dimagi/commcare-odk","owner":{"login":"dimagi","id":94514,"avatar_url":"https://avatars.githubusercontent.com/u/94514?v=3","gravatar_id":"","url":"https://api.github.com/users/dimagi","html_url":"https://github.com/dimagi","followers_url":"https://api.github.com/users/dimagi/followers","following_url":"https://api.github.com/users/dimagi/following{/other_user}","gists_url":"https://api.github.com/users/dimagi/gists{/gist_id}","starred_url":"https://api.github.com/users/dimagi/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dimagi/subscriptions","organizations_url":"https://api.github.com/users/dimagi/orgs","repos_url":"https://api.github.com/users/dimagi/repos","events_url":"https://api.github.com/users/dimagi/events{/privacy}","received_events_url":"https://api.github.com/users/dimagi/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/dimagi/commcare-odk","description":"","fork":false,"url":"https://api.github.com/repos/dimagi/commcare-odk","forks_url":"https://api.github.com/repos/dimagi/commcare-odk/forks","keys_url":"https://api.github.com/repos/dimagi/commcare-odk/keys{/key_id}","collaborators_url":"https://api.github.com/repos/dimagi/commcare-odk/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/dimagi/commcare-odk/teams","hooks_url":"https://api.github.com/repos/dimagi/commcare-odk/hooks","issue_events_url":"https://api.github.com/repos/dimagi/commcare-odk/issues/events{/number}","events_url":"https://api.github.com/repos/dimagi/commcare-odk/events","assignees_url":"https://api.github.com/repos/dimagi/commcare-odk/assignees{/user}","branches_url":"https://api.github.com/repos/dimagi/commcare-odk/branches{/branch}","tags_url":"https://api.github.com/repos/dimagi/commcare-odk/tags","blobs_url":"https://api.github.com/repos/dimagi/commcare-odk/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/dimagi/commcare-odk/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/dimagi/commcare-odk/git/refs{/sha}","trees_url":"https://api.github.com/repos/dimagi/commcare-odk/git/trees{/sha}","statuses_url":"https://api.github.com/repos/dimagi/commcare-odk/statuses/{sha}","languages_url":"https://api.github.com/repos/dimagi/commcare-odk/languages","stargazers_url":"https://api.github.com/repos/dimagi/commcare-odk/stargazers","contributors_url":"https://api.github.com/repos/dimagi/commcare-odk/contributors","subscribers_url":"https://api.github.com/repos/dimagi/commcare-odk/subscribers","subscription_url":"https://api.github.com/repos/dimagi/commcare-odk/subscription","commits_url":"https://api.github.com/repos/dimagi/commcare-odk/commits{/sha}","git_commits_url":"https://api.github.com/repos/dimagi/commcare-odk/git/commits{/sha}","comments_url":"https://api.github.com/repos/dimagi/commcare-odk/comments{/number}","issue_comment_url":"https://api.github.com/repos/dimagi/commcare-odk/issues/comments{/number}","contents_url":"https://api.github.com/repos/dimagi/commcare-odk/contents/{+path}","compare_url":"https://api.github.com/repos/dimagi/commcare-odk/compare/{base}...{head}","merges_url":"https://api.github.com/repos/dimagi/commcare-odk/merges","archive_url":"https://api.github.com/repos/dimagi/commcare-odk/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/dimagi/commcare-odk/downloads","issues_url":"https://api.github.com/repos/dimagi/commcare-odk/issues{/number}","pulls_url":"https://api.github.com/repos/dimagi/commcare-odk/pulls{/number}","milestones_url":"https://api.github.com/repos/dimagi/commcare-odk/milestones{/number}","notifications_url":"https://api.github.com/repos/dimagi/commcare-odk/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/dimagi/commcare-odk/labels{/name}","releases_url":"https://api.github.com/repos/dimagi/commcare-odk/releases{/id}","created_at":"2014-06-17T21:17:26Z","updated_at":"2015-05-06T16:04:54Z","pushed_at":"2015-10-21T03:11:41Z","git_url":"git://github.com/dimagi/commcare-odk.git","ssh_url":"git@github.com:dimagi/commcare-odk.git","clone_url":"https://github.com/dimagi/commcare-odk.git","svn_url":"https://github.com/dimagi/commcare-odk","homepage":null,"size":67731,"stargazers_count":3,"watchers_count":3,"language":"Java","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":8,"mirror_url":null,"open_issues_count":13,"forks":8,"open_issues":13,"watchers":3,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/dimagi/commcare-odk/pulls/702"},"html":{"href":"https://github.com/dimagi/commcare-odk/pull/702"},"issue":{"href":"https://api.github.com/repos/dimagi/commcare-odk/issues/702"},"comments":{"href":"https://api.github.com/repos/dimagi/commcare-odk/issues/702/comments"},"review_comments":{"href":"https://api.github.com/repos/dimagi/commcare-odk/pulls/702/comments"},"review_comment":{"href":"https://api.github.com/repos/dimagi/commcare-odk/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/dimagi/commcare-odk/pulls/702/commits"},"statuses":{"href":"https://api.github.com/repos/dimagi/commcare-odk/statuses/3dcf706bc2df1e89882a8f81b96b9b24f0275ab7"}}}}