Handle special adb getprop output. (#580)

Cover edge cases in adb getprop output
diff --git a/mobly/controllers/android_device_lib/adb.py b/mobly/controllers/android_device_lib/adb.py
index 885539a..de20822 100644
--- a/mobly/controllers/android_device_lib/adb.py
+++ b/mobly/controllers/android_device_lib/adb.py
@@ -286,13 +286,21 @@
         Returns:
             dict, name-value pairs of the properties.
         """
-        output = output.decode('utf-8').strip()
-        clean_output = output.replace('[', '').replace(']', '')
+        output = output.decode('utf-8', errors='ignore')
         results = {}
-        for line in clean_output.split('\n'):
-            if line:
-                name, value = line.split(': ')
-                results[name] = value
+        for line in output.split(']\n'):
+            if not line:
+                continue
+            try:
+                name, value = line.split(': ', 1)
+            except ValueError:
+                logging.debug('Failed to parse adb getprop line %s', line)
+                continue
+            name = name.strip()[1:-1]
+            # Remove any square bracket from either end of the value string.
+            if value and value[0] == '[':
+                value = value[1:]
+            results[name] = value
         return results
 
     def getprop(self, prop_name):
diff --git a/tests/mobly/controllers/android_device_lib/adb_test.py b/tests/mobly/controllers/android_device_lib/adb_test.py
index 911c36f..9fda55d 100755
--- a/tests/mobly/controllers/android_device_lib/adb_test.py
+++ b/tests/mobly/controllers/android_device_lib/adb_test.py
@@ -307,8 +307,8 @@
 
     def test_construct_adb_cmd_with_special_characters(self):
         adb_cmd = adb.AdbProxy()._construct_adb_cmd(
-            'shell', ['a b', '"blah"', '\/\/'], shell=False)
-        self.assertEqual(adb_cmd, ['adb', 'shell', 'a b', '"blah"', "\/\/"])
+            'shell', ['a b', '"blah"', r'\/\/'], shell=False)
+        self.assertEqual(adb_cmd, ['adb', 'shell', 'a b', '"blah"', r"\/\/"])
 
     def test_construct_adb_cmd_with_serial(self):
         adb_cmd = adb.AdbProxy('12345')._construct_adb_cmd(
@@ -348,7 +348,7 @@
 
     def test_construct_adb_cmd_with_shell_true_with_auto_quotes(self):
         adb_cmd = adb.AdbProxy()._construct_adb_cmd(
-            'shell', ['a b', '"blah"', '\/\/'], shell=True)
+            'shell', ['a b', '"blah"', r'\/\/'], shell=True)
         self.assertEqual(adb_cmd, '"adb" shell \'a b\' \'"blah"\' \'\\/\\/\'')
 
     def test_construct_adb_cmd_with_shell_true_with_serial(self):
@@ -454,29 +454,55 @@
                 stderr=None,
                 timeout=adb.DEFAULT_GETPROP_TIMEOUT_SEC)
 
+    def test__parse_getprop_output_special_values(self):
+        mock_adb_output = (
+            b'[selinux.restorecon_recursive]: [/data/misc_ce/10]\n'
+            b'[selinux.abc]: [key: value]\n'  # "key: value" as value
+            b'[persist.sys.boot.reason.history]: [reboot,adb,1558549857\n'
+            b'reboot,factory_reset,1558483886\n'  # multi-line value
+            b'reboot,1558483823]\n'
+            b'[persist.something]: [haha\n'
+            b']\n'
+            b'[[wrapped.key]]: [[wrapped value]]\n'
+            b'[persist.byte]: [J\xaa\x8bb\xab\x9dP\x0f]\n'  # non-decodable
+        )
+        parsed_props = adb.AdbProxy()._parse_getprop_output(mock_adb_output)
+        expected_output = {
+            'persist.sys.boot.reason.history':
+            ('reboot,adb,1558549857\nreboot,factory_reset,1558483886\n'
+             'reboot,1558483823'),
+            'selinux.abc':
+            'key: value',
+            'persist.something':
+            'haha\n',
+            'selinux.restorecon_recursive':
+            '/data/misc_ce/10',
+            '[wrapped.key]':
+            '[wrapped value]',
+            'persist.byte':
+            'JbP\x0f',
+        }
+        self.assertEqual(parsed_props, expected_output)
+
+    def test__parse_getprop_output_malformat_output(self):
+        mock_adb_output = (
+            b'[selinux.restorecon_recursive][/data/misc_ce/10]\n'  # Malformat
+            b'[persist.sys.boot.reason]: [reboot,adb,1558549857]\n'
+            b'[persist.something]: [haha]\n')
+        parsed_props = adb.AdbProxy()._parse_getprop_output(mock_adb_output)
+        expected_output = {
+            'persist.sys.boot.reason': 'reboot,adb,1558549857',
+            'persist.something': 'haha'
+        }
+        self.assertEqual(parsed_props, expected_output)
+
     def test_getprops(self):
         with mock.patch.object(adb.AdbProxy, '_exec_cmd') as mock_exec_cmd:
-            mock_exec_cmd.return_value = b'''
-[selinux.restorecon_recursive]: [/data/misc_ce/10]
-[sendbug.preferred.domain]: [google.com]
-[service.bootanim.exit]: [1]
-[sys.boot_completed]: [1]
-[sys.dvr.performance]: [idle]
-[sys.logbootcomplete]: [1]
-[sys.oem_unlock_allowed]: [1]
-[sys.rescue_boot_count]: [1]
-[sys.retaildemo.enabled]: [0]
-[sys.sysctl.extra_free_kbytes]: [27337]
-[sys.sysctl.tcp_def_init_rwnd]: [60]
-[sys.uidcpupower]: []
-[sys.usb.config]: [adb]
-[sys.usb.configfs]: [2]
-[sys.usb.controller]: [a600000.dwc3]
-[sys.usb.ffs.ready]: [1]
-[sys.usb.mtp.device_type]: [3]
-[sys.user.0.ce_available]: [true]
-[sys.wifitracing.started]: [1]
-[telephony.lteOnCdmaDevice]: [1]'''
+            mock_exec_cmd.return_value = (
+                b'\n[sendbug.preferred.domain]: [google.com]\n'
+                b'[sys.uidcpupower]: []\n'
+                b'[sys.wifitracing.started]: [1]\n'
+                b'[telephony.lteOnCdmaDevice]: [1]\n\n')
             actual_output = adb.AdbProxy().getprops([
                 'sys.wifitracing.started',  # "numeric" value
                 'sys.uidcpupower',  # empty value