[device_info_platform_interface] handle null value from method channel (#3609)

diff --git a/packages/device_info/device_info_platform_interface/lib/device_info_platform_interface.dart b/packages/device_info/device_info_platform_interface/lib/device_info_platform_interface.dart
index 808b7ad..2dd41dc 100644
--- a/packages/device_info/device_info_platform_interface/lib/device_info_platform_interface.dart
+++ b/packages/device_info/device_info_platform_interface/lib/device_info_platform_interface.dart
@@ -7,10 +7,8 @@
 import 'package:plugin_platform_interface/plugin_platform_interface.dart';
 
 import 'method_channel/method_channel_device_info.dart';
-
 import 'model/android_device_info.dart';
 import 'model/ios_device_info.dart';
-
 export 'model/android_device_info.dart';
 export 'model/ios_device_info.dart';
 
diff --git a/packages/device_info/device_info_platform_interface/lib/method_channel/method_channel_device_info.dart b/packages/device_info/device_info_platform_interface/lib/method_channel/method_channel_device_info.dart
index 7bd02e9..331f718 100644
--- a/packages/device_info/device_info_platform_interface/lib/method_channel/method_channel_device_info.dart
+++ b/packages/device_info/device_info_platform_interface/lib/method_channel/method_channel_device_info.dart
@@ -1,8 +1,11 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
 import 'dart:async';
 
 import 'package:flutter/services.dart';
 import 'package:meta/meta.dart';
-
 import 'package:device_info_platform_interface/device_info_platform_interface.dart';
 
 /// An implementation of [DeviceInfoPlatform] that uses method channels.
@@ -13,16 +16,15 @@
 
   // Method channel for Android devices
   Future<AndroidDeviceInfo> androidInfo() async {
-    return AndroidDeviceInfo.fromMap(
-      (await channel.invokeMethod('getAndroidDeviceInfo'))
-          .cast<String, dynamic>(),
-    );
+    return AndroidDeviceInfo.fromMap((await channel
+            .invokeMapMethod<String, dynamic>('getAndroidDeviceInfo')) ??
+        <String, dynamic>{});
   }
 
   // Method channel for iOS devices
   Future<IosDeviceInfo> iosInfo() async {
     return IosDeviceInfo.fromMap(
-      (await channel.invokeMethod('getIosDeviceInfo')).cast<String, dynamic>(),
-    );
+        (await channel.invokeMapMethod<String, dynamic>('getIosDeviceInfo')) ??
+            <String, dynamic>{});
   }
 }
diff --git a/packages/device_info/device_info_platform_interface/lib/model/android_device_info.dart b/packages/device_info/device_info_platform_interface/lib/model/android_device_info.dart
index 4fb940c..c5210ab 100644
--- a/packages/device_info/device_info_platform_interface/lib/model/android_device_info.dart
+++ b/packages/device_info/device_info_platform_interface/lib/model/android_device_info.dart
@@ -38,39 +38,63 @@
   final AndroidBuildVersion version;
 
   /// The name of the underlying board, like "goldfish".
+  ///
+  /// The value is an empty String if it is not available.
   final String board;
 
   /// The system bootloader version number.
+  ///
+  /// The value is an empty String if it is not available.
   final String bootloader;
 
   /// The consumer-visible brand with which the product/hardware will be associated, if any.
+  ///
+  /// The value is an empty String if it is not available.
   final String brand;
 
   /// The name of the industrial design.
+  ///
+  /// The value is an empty String if it is not available.
   final String device;
 
   /// A build ID string meant for displaying to the user.
+  ///
+  /// The value is an empty String if it is not available.
   final String display;
 
   /// A string that uniquely identifies this build.
+  ///
+  /// The value is an empty String if it is not available.
   final String fingerprint;
 
   /// The name of the hardware (from the kernel command line or /proc).
+  ///
+  /// The value is an empty String if it is not available.
   final String hardware;
 
   /// Hostname.
+  ///
+  /// The value is an empty String if it is not available.
   final String host;
 
   /// Either a changelist number, or a label like "M4-rc20".
+  ///
+  /// The value is an empty String if it is not available.
   final String id;
 
   /// The manufacturer of the product/hardware.
+  ///
+  /// The value is an empty String if it is not available.
   final String manufacturer;
 
   /// The end-user-visible name for the end product.
+  ///
+  /// The value is an empty String if it is not available.
   final String model;
 
   /// The name of the overall product.
+  ///
+  /// The value is an empty String if it is not available.
   final String product;
 
   /// An ordered list of 32 bit ABIs supported by this device.
@@ -83,15 +107,23 @@
   final List<String> supportedAbis;
 
   /// Comma-separated tags describing the build, like "unsigned,debug".
+  ///
+  /// The value is an empty String if it is not available.
   final String tags;
 
   /// The type of build, like "user" or "eng".
+  ///
+  /// The value is an empty String if it is not available.
   final String type;
 
-  /// `false` if the application is running in an emulator, `true` otherwise.
+  /// The value is `true` if the application is running on a physical device.
+  ///
+  /// The value is `false` when the application is running on a emulator, or the value is unavailable.
   final bool isPhysicalDevice;
 
   /// The Android hardware device ID that is unique between the device + user and app signing.
+  ///
+  /// The value is an empty String if it is not available.
   final String androidId;
 
   /// Describes what features are available on the current device.
@@ -113,35 +145,41 @@
   /// Deserializes from the message received from [_kChannel].
   static AndroidDeviceInfo fromMap(Map<String, dynamic> map) {
     return AndroidDeviceInfo(
-      version:
-          AndroidBuildVersion._fromMap(map['version']!.cast<String, dynamic>()),
-      board: map['board']!,
-      bootloader: map['bootloader']!,
-      brand: map['brand']!,
-      device: map['device']!,
-      display: map['display']!,
-      fingerprint: map['fingerprint']!,
-      hardware: map['hardware']!,
-      host: map['host']!,
-      id: map['id']!,
-      manufacturer: map['manufacturer']!,
-      model: map['model']!,
-      product: map['product']!,
-      supported32BitAbis: _fromList(map['supported32BitAbis']!),
-      supported64BitAbis: _fromList(map['supported64BitAbis']!),
-      supportedAbis: _fromList(map['supportedAbis']!),
-      tags: map['tags']!,
-      type: map['type']!,
-      isPhysicalDevice: map['isPhysicalDevice']!,
-      androidId: map['androidId']!,
-      systemFeatures: _fromList(map['systemFeatures']!),
+      version: AndroidBuildVersion._fromMap(map['version'] != null
+          ? map['version'].cast<String, dynamic>()
+          : <String, dynamic>{}),
+      board: map['board'] ?? '',
+      bootloader: map['bootloader'] ?? '',
+      brand: map['brand'] ?? '',
+      device: map['device'] ?? '',
+      display: map['display'] ?? '',
+      fingerprint: map['fingerprint'] ?? '',
+      hardware: map['hardware'] ?? '',
+      host: map['host'] ?? '',
+      id: map['id'] ?? '',
+      manufacturer: map['manufacturer'] ?? '',
+      model: map['model'] ?? '',
+      product: map['product'] ?? '',
+      supported32BitAbis: _fromList(map['supported32BitAbis']),
+      supported64BitAbis: _fromList(map['supported64BitAbis']),
+      supportedAbis: _fromList(map['supportedAbis']),
+      tags: map['tags'] ?? '',
+      type: map['type'] ?? '',
+      isPhysicalDevice: map['isPhysicalDevice'] ?? false,
+      androidId: map['androidId'] ?? '',
+      systemFeatures: _fromList(map['systemFeatures']),
     );
   }
 
   /// Deserializes message as List<String>
   static List<String> _fromList(dynamic message) {
-    final List<dynamic> list = message;
-    return List<String>.from(list);
+    if (message == null) {
+      return <String>[];
+    }
+    assert(message is List<dynamic>);
+    final List<dynamic> list = List<dynamic>.from(message)
+      ..removeWhere((value) => value == null);
+    return list.cast<String>();
   }
 }
 
@@ -173,17 +211,25 @@
   final String? securityPatch;
 
   /// The current development codename, or the string "REL" if this is a release build.
+  ///
+  /// The value is an empty String if it is not available.
   final String codename;
 
   /// The internal value used by the underlying source control to represent this build.
+  ///
+  /// The value is an empty String if it is not available.
   final String incremental;
 
   /// The user-visible version string.
+  ///
+  /// The value is an empty String if it is not available.
   final String release;
 
   /// The user-visible SDK version of the framework.
   ///
   /// Possible values are defined in: https://developer.android.com/reference/android/os/Build.VERSION_CODES.html
+  ///
+  /// The value is -1 if it is unavailable.
   final int sdkInt;
 
   /// Deserializes from the map message received from [_kChannel].
@@ -192,10 +238,10 @@
       baseOS: map['baseOS'],
       previewSdkInt: map['previewSdkInt'],
       securityPatch: map['securityPatch'],
-      codename: map['codename']!,
-      incremental: map['incremental']!,
-      release: map['release']!,
-      sdkInt: map['sdkInt']!,
+      codename: map['codename'] ?? '',
+      incremental: map['incremental'] ?? '',
+      release: map['release'] ?? '',
+      sdkInt: map['sdkInt'] ?? -1,
     );
   }
 }
diff --git a/packages/device_info/device_info_platform_interface/lib/model/ios_device_info.dart b/packages/device_info/device_info_platform_interface/lib/model/ios_device_info.dart
index eb6e587..20ec836 100644
--- a/packages/device_info/device_info_platform_interface/lib/model/ios_device_info.dart
+++ b/packages/device_info/device_info_platform_interface/lib/model/ios_device_info.dart
@@ -19,40 +19,60 @@
   });
 
   /// Device name.
+  ///
+  /// The value is an empty String if it is not available.
   final String name;
 
   /// The name of the current operating system.
+  ///
+  /// The value is an empty String if it is not available.
   final String systemName;
 
   /// The current operating system version.
+  ///
+  /// The value is an empty String if it is not available.
   final String systemVersion;
 
   /// Device model.
+  ///
+  /// The value is an empty String if it is not available.
   final String model;
 
   /// Localized name of the device model.
+  ///
+  /// The value is an empty String if it is not available.
   final String localizedModel;
 
   /// Unique UUID value identifying the current device.
+  ///
+  /// The value is an empty String if it is not available.
   final String identifierForVendor;
 
-  /// `false` if the application is running in a simulator, `true` otherwise.
+  /// The value is `true` if the application is running on a physical device.
+  ///
+  /// The value is `false` when the application is running on a simulator, or the value is unavailable.
   final bool isPhysicalDevice;
 
   /// Operating system information derived from `sys/utsname.h`.
+  ///
+  /// The value is an empty String if it is not available.
   final IosUtsname utsname;
 
   /// Deserializes from the map message received from [_kChannel].
   static IosDeviceInfo fromMap(Map<String, dynamic> map) {
     return IosDeviceInfo(
-      name: map['name']!,
-      systemName: map['systemName']!,
-      systemVersion: map['systemVersion']!,
-      model: map['model']!,
-      localizedModel: map['localizedModel']!,
-      identifierForVendor: map['identifierForVendor']!,
-      isPhysicalDevice: map['isPhysicalDevice'] == 'true',
-      utsname: IosUtsname._fromMap(map['utsname']!.cast<String, dynamic>()),
+      name: map['name'] ?? '',
+      systemName: map['systemName'] ?? '',
+      systemVersion: map['systemVersion'] ?? '',
+      model: map['model'] ?? '',
+      localizedModel: map['localizedModel'] ?? '',
+      identifierForVendor: map['identifierForVendor'] ?? '',
+      isPhysicalDevice: map['isPhysicalDevice'] != null
+          ? map['isPhysicalDevice'] == 'true'
+          : false,
+      utsname: IosUtsname._fromMap(map['utsname'] != null
+          ? map['utsname'].cast<String, dynamic>()
+          : <String, dynamic>{}),
     );
   }
 }
@@ -69,28 +89,38 @@
   });
 
   /// Operating system name.
+  ///
+  /// The value is an empty String if it is not available.
   final String sysname;
 
   /// Network node name.
+  ///
+  /// The value is an empty String if it is not available.
   final String nodename;
 
   /// Release level.
+  ///
+  /// The value is an empty String if it is not available.
   final String release;
 
   /// Version level.
+  ///
+  /// The value is an empty String if it is not available.
   final String version;
 
   /// Hardware type (e.g. 'iPhone7,1' for iPhone 6 Plus).
+  ///
+  /// The value is an empty String if it is not available.
   final String machine;
 
   /// Deserializes from the map message received from [_kChannel].
   static IosUtsname _fromMap(Map<String, dynamic> map) {
     return IosUtsname._(
-      sysname: map['sysname']!,
-      nodename: map['nodename']!,
-      release: map['release']!,
-      version: map['version']!,
-      machine: map['machine']!,
+      sysname: map['sysname'] ?? '',
+      nodename: map['nodename'] ?? '',
+      release: map['release'] ?? '',
+      version: map['version'] ?? '',
+      machine: map['machine'] ?? '',
     );
   }
 }
diff --git a/packages/device_info/device_info_platform_interface/test/method_channel_device_info_test.dart b/packages/device_info/device_info_platform_interface/test/method_channel_device_info_test.dart
index 1596385..03ff4b5 100644
--- a/packages/device_info/device_info_platform_interface/test/method_channel_device_info_test.dart
+++ b/packages/device_info/device_info_platform_interface/test/method_channel_device_info_test.dart
@@ -158,4 +158,220 @@
       expect(result.utsname.machine, "x86_64");
     });
   });
+
+  group(
+      "$MethodChannelDeviceInfo handles null value in the map returned from method channel",
+      () {
+    MethodChannelDeviceInfo methodChannelDeviceInfo;
+
+    setUp(() async {
+      methodChannelDeviceInfo = MethodChannelDeviceInfo();
+
+      methodChannelDeviceInfo.channel
+          .setMockMethodCallHandler((MethodCall methodCall) async {
+        switch (methodCall.method) {
+          case 'getAndroidDeviceInfo':
+            return ({
+              "version": null,
+              "board": null,
+              "bootloader": null,
+              "brand": null,
+              "device": null,
+              "display": null,
+              "fingerprint": null,
+              "hardware": null,
+              "host": null,
+              "id": null,
+              "manufacturer": null,
+              "model": null,
+              "product": null,
+              "supported32BitAbis": null,
+              "supported64BitAbis": null,
+              "supportedAbis": null,
+              "tags": null,
+              "type": null,
+              "isPhysicalDevice": null,
+              "androidId": null,
+              "systemFeatures": null,
+            });
+          case 'getIosDeviceInfo':
+            return ({
+              "name": null,
+              "systemName": null,
+              "systemVersion": null,
+              "model": null,
+              "localizedModel": null,
+              "identifierForVendor": null,
+              "isPhysicalDevice": null,
+              "utsname": null,
+            });
+          default:
+            return null;
+        }
+      });
+    });
+
+    test("androidInfo hanels null", () async {
+      final AndroidDeviceInfo result =
+          await methodChannelDeviceInfo.androidInfo();
+
+      expect(result.version.securityPatch, null);
+      expect(result.version.sdkInt, -1);
+      expect(result.version.release, '');
+      expect(result.version.previewSdkInt, null);
+      expect(result.version.incremental, '');
+      expect(result.version.codename, '');
+      expect(result.board, '');
+      expect(result.bootloader, '');
+      expect(result.brand, '');
+      expect(result.device, '');
+      expect(result.display, '');
+      expect(result.fingerprint, '');
+      expect(result.hardware, '');
+      expect(result.host, '');
+      expect(result.id, '');
+      expect(result.manufacturer, '');
+      expect(result.model, '');
+      expect(result.product, '');
+      expect(result.supported32BitAbis, <String>[]);
+      expect(result.supported64BitAbis, <String>[]);
+      expect(result.supportedAbis, <String>[]);
+      expect(result.tags, '');
+      expect(result.type, '');
+      expect(result.isPhysicalDevice, false);
+      expect(result.androidId, '');
+      expect(result.systemFeatures, <String>[]);
+    });
+
+    test("iosInfo handles null", () async {
+      final IosDeviceInfo result = await methodChannelDeviceInfo.iosInfo();
+      expect(result.name, '');
+      expect(result.systemName, '');
+      expect(result.systemVersion, '');
+      expect(result.model, '');
+      expect(result.localizedModel, '');
+      expect(result.identifierForVendor, '');
+      expect(result.isPhysicalDevice, false);
+      expect(result.utsname.sysname, '');
+      expect(result.utsname.nodename, '');
+      expect(result.utsname.release, '');
+      expect(result.utsname.version, '');
+      expect(result.utsname.machine, '');
+    });
+  });
+
+  group("$MethodChannelDeviceInfo handles method channel returns null", () {
+    MethodChannelDeviceInfo methodChannelDeviceInfo;
+
+    setUp(() async {
+      methodChannelDeviceInfo = MethodChannelDeviceInfo();
+
+      methodChannelDeviceInfo.channel
+          .setMockMethodCallHandler((MethodCall methodCall) async {
+        switch (methodCall.method) {
+          case 'getAndroidDeviceInfo':
+            return null;
+          case 'getIosDeviceInfo':
+            return null;
+          default:
+            return null;
+        }
+      });
+    });
+
+    test("androidInfo handles null", () async {
+      final AndroidDeviceInfo result =
+          await methodChannelDeviceInfo.androidInfo();
+
+      expect(result.version.securityPatch, null);
+      expect(result.version.sdkInt, -1);
+      expect(result.version.release, '');
+      expect(result.version.previewSdkInt, null);
+      expect(result.version.incremental, '');
+      expect(result.version.codename, '');
+      expect(result.board, '');
+      expect(result.bootloader, '');
+      expect(result.brand, '');
+      expect(result.device, '');
+      expect(result.display, '');
+      expect(result.fingerprint, '');
+      expect(result.hardware, '');
+      expect(result.host, '');
+      expect(result.id, '');
+      expect(result.manufacturer, '');
+      expect(result.model, '');
+      expect(result.product, '');
+      expect(result.supported32BitAbis, <String>[]);
+      expect(result.supported64BitAbis, <String>[]);
+      expect(result.supportedAbis, <String>[]);
+      expect(result.tags, '');
+      expect(result.type, '');
+      expect(result.isPhysicalDevice, false);
+      expect(result.androidId, '');
+      expect(result.systemFeatures, <String>[]);
+    });
+
+    test("iosInfo handles null", () async {
+      final IosDeviceInfo result = await methodChannelDeviceInfo.iosInfo();
+      expect(result.name, '');
+      expect(result.systemName, '');
+      expect(result.systemVersion, '');
+      expect(result.model, '');
+      expect(result.localizedModel, '');
+      expect(result.identifierForVendor, '');
+      expect(result.isPhysicalDevice, false);
+      expect(result.utsname.sysname, '');
+      expect(result.utsname.nodename, '');
+      expect(result.utsname.release, '');
+      expect(result.utsname.version, '');
+      expect(result.utsname.machine, '');
+    });
+  });
+
+  group("$MethodChannelDeviceInfo android handles null values in list", () {
+    MethodChannelDeviceInfo methodChannelDeviceInfo;
+
+    setUp(() async {
+      methodChannelDeviceInfo = MethodChannelDeviceInfo();
+
+      methodChannelDeviceInfo.channel
+          .setMockMethodCallHandler((MethodCall methodCall) async {
+        switch (methodCall.method) {
+          case 'getAndroidDeviceInfo':
+            return ({
+              "supported32BitAbis": <String>["x86", null],
+              "supported64BitAbis": <String>["x86_64", null],
+              "supportedAbis": <String>["x86_64", "x86", null],
+              "systemFeatures": <String>[
+                "android.hardware.sensor.proximity",
+                "android.software.adoptable_storage",
+                "android.hardware.sensor.accelerometer",
+                "android.hardware.faketouch",
+                "android.software.backup",
+                "android.hardware.touchscreen",
+                null
+              ],
+            });
+          default:
+            return null;
+        }
+      });
+    });
+
+    test("androidInfo hanels null in list", () async {
+      final AndroidDeviceInfo result =
+          await methodChannelDeviceInfo.androidInfo();
+      expect(result.supported32BitAbis, <String>['x86']);
+      expect(result.supported64BitAbis, <String>['x86_64']);
+      expect(result.supportedAbis, <String>['x86_64', 'x86']);
+      expect(result.systemFeatures, <String>[
+        "android.hardware.sensor.proximity",
+        "android.software.adoptable_storage",
+        "android.hardware.sensor.accelerometer",
+        "android.hardware.faketouch",
+        "android.software.backup",
+        "android.hardware.touchscreen"
+      ]);
+    });
+  });
 }