diff --git a/packages/package_info_plus/package_info_plus/android/src/main/kotlin/dev/fluttercommunity/plus/packageinfo/PackageInfoPlugin.kt b/packages/package_info_plus/package_info_plus/android/src/main/kotlin/dev/fluttercommunity/plus/packageinfo/PackageInfoPlugin.kt index b1e6870ef3..ff96497a48 100644 --- a/packages/package_info_plus/package_info_plus/android/src/main/kotlin/dev/fluttercommunity/plus/packageinfo/PackageInfoPlugin.kt +++ b/packages/package_info_plus/package_info_plus/android/src/main/kotlin/dev/fluttercommunity/plus/packageinfo/PackageInfoPlugin.kt @@ -39,7 +39,9 @@ class PackageInfoPlugin : MethodCallHandler, FlutterPlugin { val buildSignature = getBuildSignature(packageManager) val installerPackage = getInstallerPackageName() - val installTimeMillis = getInstallTimeMillis() + + val installTimeMillis = info.firstInstallTime + val updateTimeMillis = info.lastUpdateTime val infoMap = HashMap() infoMap.apply { @@ -49,7 +51,8 @@ class PackageInfoPlugin : MethodCallHandler, FlutterPlugin { put("buildNumber", getLongVersionCode(info).toString()) if (buildSignature != null) put("buildSignature", buildSignature) if (installerPackage != null) put("installerStore", installerPackage) - if (installTimeMillis != null) put("installTime", installTimeMillis.toString()) + put("installTime", installTimeMillis.toString()) + put("updateTime", updateTimeMillis.toString()) }.also { resultingMap -> result.success(resultingMap) } @@ -76,22 +79,6 @@ class PackageInfoPlugin : MethodCallHandler, FlutterPlugin { } } - private fun getInstallTimeMillis(): Long? { - return try { - val packageManager = applicationContext!!.packageManager - val packageName = applicationContext!!.packageName - val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - packageManager.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0)) - } else { - packageManager.getPackageInfo(packageName, 0) - } - - packageInfo.firstInstallTime - } catch (e: PackageManager.NameNotFoundException) { - null - } - } - @Suppress("deprecation") private fun getLongVersionCode(info: PackageInfo): Long { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { diff --git a/packages/package_info_plus/package_info_plus/example/integration_test/package_info_plus_test.dart b/packages/package_info_plus/package_info_plus/example/integration_test/package_info_plus_test.dart index 8023eeaa4b..8ea41c3de7 100644 --- a/packages/package_info_plus/package_info_plus/example/integration_test/package_info_plus_test.dart +++ b/packages/package_info_plus/package_info_plus/example/integration_test/package_info_plus_test.dart @@ -29,6 +29,7 @@ void main() { expect(info.version, '1.2.3'); expect(info.installerStore, null); expect(info.installTime, null); + expect(info.updateTime, null); } else { if (Platform.isAndroid) { final androidVersionInfo = await DeviceInfoPlugin().androidInfo; @@ -52,6 +53,14 @@ void main() { lessThanOrEqualTo(1), ), ); + expect( + info.updateTime, + isA().having( + (d) => d.difference(DateTime.now()).inMinutes, + 'Was just updated', + lessThanOrEqualTo(1), + ), + ); } else if (Platform.isIOS) { expect(info.appName, 'Package Info Plus Example'); expect(info.buildNumber, '4'); @@ -67,6 +76,14 @@ void main() { lessThanOrEqualTo(1), ), ); + expect( + info.updateTime, + isA().having( + (d) => d.difference(DateTime.now()).inMinutes, + 'Was just updated', + lessThanOrEqualTo(1), + ), + ); } else if (Platform.isMacOS) { expect(info.appName, 'Package Info Plus Example'); expect(info.buildNumber, '4'); @@ -82,6 +99,14 @@ void main() { lessThanOrEqualTo(1), ), ); + expect( + info.updateTime, + isA().having( + (d) => d.difference(DateTime.now()).inMinutes, + 'Was just updated', + lessThanOrEqualTo(1), + ), + ); } else if (Platform.isLinux) { expect(info.appName, 'package_info_plus_example'); expect(info.buildNumber, '4'); @@ -96,6 +121,14 @@ void main() { lessThanOrEqualTo(1), ), ); + expect( + info.updateTime, + isA().having( + (d) => d.difference(DateTime.now()).inMinutes, + 'Was just updated', + lessThanOrEqualTo(1), + ), + ); } else if (Platform.isWindows) { expect(info.appName, 'example'); expect(info.buildNumber, '4'); @@ -111,6 +144,14 @@ void main() { lessThanOrEqualTo(1), ), ); + expect( + info.updateTime, + isA().having( + (d) => d.difference(DateTime.now()).inMinutes, + 'Was just updated', + lessThanOrEqualTo(1), + ), + ); } else { throw (UnsupportedError('platform not supported')); } @@ -127,6 +168,7 @@ void main() { expect(find.text('Not set'), findsOneWidget); expect(find.text('not available'), findsOneWidget); expect(find.text('Install time not available'), findsOneWidget); + expect(find.text('Update time not available'), findsOneWidget); } else { final expectedInstallTimeIso = testStartTime.toIso8601String(); final installTimeRegex = RegExp( @@ -153,7 +195,7 @@ void main() { } else { expect(find.text('not available'), findsOneWidget); } - expect(find.textContaining(installTimeRegex), findsOneWidget); + expect(find.textContaining(installTimeRegex), findsNWidgets(2)); } else if (Platform.isIOS) { expect(find.text('Package Info Plus Example'), findsOneWidget); expect(find.text('4'), findsOneWidget); @@ -162,7 +204,7 @@ void main() { expect(find.text('1.2.3'), findsOneWidget); expect(find.text('Not set'), findsOneWidget); expect(find.text('com.apple.simulator'), findsOneWidget); - expect(find.textContaining(installTimeRegex), findsOneWidget); + expect(find.textContaining(installTimeRegex), findsNWidgets(2)); } else if (Platform.isMacOS) { expect(find.text('Package Info Plus Example'), findsOneWidget); expect(find.text('4'), findsOneWidget); @@ -171,20 +213,20 @@ void main() { expect(find.text('1.2.3'), findsOneWidget); expect(find.text('Not set'), findsOneWidget); expect(find.text('not available'), findsOneWidget); - expect(find.textContaining(installTimeRegex), findsOneWidget); + expect(find.textContaining(installTimeRegex), findsNWidgets(2)); } else if (Platform.isLinux) { expect(find.text('package_info_plus_example'), findsNWidgets(2)); expect(find.text('1.2.3'), findsOneWidget); expect(find.text('4'), findsOneWidget); expect(find.text('Not set'), findsOneWidget); - expect(find.textContaining(installTimeRegex), findsOneWidget); + expect(find.textContaining(installTimeRegex), findsNWidgets(2)); } else if (Platform.isWindows) { expect(find.text('example'), findsNWidgets(2)); expect(find.text('1.2.3'), findsOneWidget); expect(find.text('4'), findsOneWidget); expect(find.text('Not set'), findsOneWidget); expect(find.text('not available'), findsOneWidget); - expect(find.textContaining(installTimeRegex), findsOneWidget); + expect(find.textContaining(installTimeRegex), findsNWidgets(2)); } else { throw (UnsupportedError('platform not supported')); } diff --git a/packages/package_info_plus/package_info_plus/example/lib/main.dart b/packages/package_info_plus/package_info_plus/example/lib/main.dart index 9d3f5890b2..b260dd0c84 100644 --- a/packages/package_info_plus/package_info_plus/example/lib/main.dart +++ b/packages/package_info_plus/package_info_plus/example/lib/main.dart @@ -89,6 +89,11 @@ class _MyHomePageState extends State { _packageInfo.installTime?.toIso8601String() ?? 'Install time not available', ), + _infoTile( + 'Update time', + _packageInfo.updateTime?.toIso8601String() ?? + 'Update time not available', + ), ], ), ); diff --git a/packages/package_info_plus/package_info_plus/ios/package_info_plus/Sources/package_info_plus/FPPPackageInfoPlusPlugin.m b/packages/package_info_plus/package_info_plus/ios/package_info_plus/Sources/package_info_plus/FPPPackageInfoPlusPlugin.m index eaa3ff8edf..8dd5dcf28b 100644 --- a/packages/package_info_plus/package_info_plus/ios/package_info_plus/Sources/package_info_plus/FPPPackageInfoPlusPlugin.m +++ b/packages/package_info_plus/package_info_plus/ios/package_info_plus/Sources/package_info_plus/FPPPackageInfoPlusPlugin.m @@ -26,11 +26,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call ? @"com.apple.testflight" : @"com.apple"; - NSURL* urlToDocumentsFolder = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject]; - __autoreleasing NSError *error; - NSDate *installDate = [[[NSFileManager defaultManager] attributesOfItemAtPath:urlToDocumentsFolder.path error:&error] objectForKey:NSFileCreationDate]; - NSNumber *installTimeMillis = installDate ? @((long long)([installDate timeIntervalSince1970] * 1000)) : [NSNull null]; - + NSDate *installDate = [self getInstallDate]; + NSDate *updateDate = [self getUpdateDate]; result(@{ @"appName" : [[NSBundle mainBundle] @@ -46,7 +43,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call objectForInfoDictionaryKey:@"CFBundleVersion"] ?: [NSNull null], @"installerStore" : installerStore, - @"installTime" : installTimeMillis ? [installTimeMillis stringValue] : [NSNull null] + @"installTime" : [self getTimeMillisStringFromDate:installDate] ?: [NSNull null], + @"updateTime" : [self getTimeMillisStringFromDate:updateDate] ?: [NSNull null] }); } else { @@ -54,4 +52,37 @@ - (void)handleMethodCall:(FlutterMethodCall *)call } } +- (NSDate *)getInstallDate { + NSURL* urlToDocumentsFolder = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject]; + __autoreleasing NSError *error; + NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:urlToDocumentsFolder.path error:&error]; + + if (error) { + return nil; + } + + return [attributes objectForKey:NSFileCreationDate]; +} + +- (NSDate *)getUpdateDate { + __autoreleasing NSError *error; + NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[[NSBundle mainBundle] bundlePath] error:&error]; + NSDate *updateDate = [attributes fileModificationDate]; + + if (error) { + return nil; + } + + return updateDate; +} + +- (NSString *)getTimeMillisStringFromDate:(NSDate *)date { + if (!date) { + return nil; + } + + NSNumber *timeMillis = @((long long)([date timeIntervalSince1970] * 1000)); + return [timeMillis stringValue]; +} + @end diff --git a/packages/package_info_plus/package_info_plus/lib/package_info_plus.dart b/packages/package_info_plus/package_info_plus/lib/package_info_plus.dart index 6b988bf8ea..8cfcb669a6 100644 --- a/packages/package_info_plus/package_info_plus/lib/package_info_plus.dart +++ b/packages/package_info_plus/package_info_plus/lib/package_info_plus.dart @@ -28,6 +28,7 @@ class PackageInfo { this.buildSignature = '', this.installerStore, this.installTime, + this.updateTime, }); static PackageInfo? _fromPlatform; @@ -90,6 +91,7 @@ class PackageInfo { buildSignature: platformData.buildSignature, installerStore: platformData.installerStore, installTime: platformData.installTime, + updateTime: platformData.updateTime, ); return _fromPlatform!; } @@ -159,6 +161,15 @@ class PackageInfo { /// - On web, returns `null`. final DateTime? installTime; + /// The time when the application was last updated. + /// + /// - On Android, returns `PackageManager.lastUpdateTime` + /// - On iOS and macOS, return the last modified date of the app main bundle + /// - On Windows and Linux, returns the last modified date of the app executable. + /// If the last modified date is not available, returns `null`. + /// - On web, returns `null`. + final DateTime? updateTime; + /// Initializes the application metadata with mock values for testing. /// /// If the singleton instance has been initialized already, it is overwritten. @@ -171,6 +182,7 @@ class PackageInfo { required String buildSignature, String? installerStore, DateTime? installTime, + DateTime? updateTime, }) { _fromPlatform = PackageInfo( appName: appName, @@ -180,6 +192,7 @@ class PackageInfo { buildSignature: buildSignature, installerStore: installerStore, installTime: installTime, + updateTime: updateTime, ); } @@ -195,7 +208,8 @@ class PackageInfo { buildNumber == other.buildNumber && buildSignature == other.buildSignature && installerStore == other.installerStore && - installTime == other.installTime; + installTime == other.installTime && + updateTime == other.updateTime; /// Overwrite hashCode for value equality @override @@ -206,11 +220,12 @@ class PackageInfo { buildNumber.hashCode ^ buildSignature.hashCode ^ installerStore.hashCode ^ - installTime.hashCode; + installTime.hashCode ^ + updateTime.hashCode; @override String toString() { - return 'PackageInfo(appName: $appName, buildNumber: $buildNumber, packageName: $packageName, version: $version, buildSignature: $buildSignature, installerStore: $installerStore, installTime: $installTime)'; + return 'PackageInfo(appName: $appName, buildNumber: $buildNumber, packageName: $packageName, version: $version, buildSignature: $buildSignature, installerStore: $installerStore, installTime: $installTime, updateTime: $updateTime)'; } Map _toMap() { @@ -222,6 +237,7 @@ class PackageInfo { if (buildSignature.isNotEmpty) 'buildSignature': buildSignature, if (installerStore?.isNotEmpty ?? false) 'installerStore': installerStore, if (installTime != null) 'installTime': installTime!.toIso8601String(), + if (updateTime != null) 'updateTime': updateTime!.toIso8601String(), }; } diff --git a/packages/package_info_plus/package_info_plus/lib/src/package_info_plus_linux.dart b/packages/package_info_plus/package_info_plus/lib/src/package_info_plus_linux.dart index 9ba8e066fe..0a0764201d 100644 --- a/packages/package_info_plus/package_info_plus/lib/src/package_info_plus_linux.dart +++ b/packages/package_info_plus/package_info_plus/lib/src/package_info_plus_linux.dart @@ -19,7 +19,7 @@ class PackageInfoPlusLinuxPlugin extends PackageInfoPlatform { final exePath = await File('/proc/self/exe').resolveSymbolicLinks(); final versionJson = await _getVersionJson(exePath); - final installTime = await _getInstallTime(exePath); + final exeAttributes = await _getExeAttributes(exePath); return PackageInfoData( appName: versionJson['app_name'] ?? '', @@ -27,7 +27,8 @@ class PackageInfoPlusLinuxPlugin extends PackageInfoPlatform { buildNumber: versionJson['build_number'] ?? '', packageName: versionJson['package_name'] ?? '', buildSignature: '', - installTime: installTime, + installTime: exeAttributes.created, + updateTime: exeAttributes.modified, ); } @@ -43,23 +44,64 @@ class PackageInfoPlusLinuxPlugin extends PackageInfoPlatform { } } - Future _getInstallTime(String exePath) async { + Future<({DateTime? created, DateTime? modified})> _getExeAttributes( + String exePath) async { try { final statResult = await Process.run( 'stat', - ['-c', '%W', exePath], + // birth time and last modification time + ['-c', '%W,%Y', exePath], stdoutEncoding: utf8, ); - if (statResult.exitCode == 0 && int.tryParse(statResult.stdout) != null) { - final creationTimeSeconds = int.parse(statResult.stdout) * 1000; + if (statResult.exitCode != 0) { + return await _fallbackAttributes(exePath); + } + + final String stdout = + statResult.stdout is String ? statResult.stdout : ''; - return DateTime.fromMillisecondsSinceEpoch(creationTimeSeconds); + if (stdout.split(',').length != 2) { + return await _fallbackAttributes(exePath); } - return await File(exePath).lastModified(); + final [creationMillis, modificationMillis] = stdout.split(','); + + // birth time is 0 if it is unknown + final creationTime = _parseSecondsString( + creationMillis, + allowZero: false, + ); + final modificationTime = _parseSecondsString(modificationMillis); + + return ( + created: creationTime, + modified: modificationTime, + ); } catch (_) { + return (created: null, modified: null); + } + } + + Future<({DateTime created, DateTime modified})> _fallbackAttributes( + String exePath) async { + final modifiedTime = await File(exePath).lastModified(); + + return (created: modifiedTime, modified: modifiedTime); + } + + DateTime? _parseSecondsString(String? secondsString, + {bool allowZero = true}) { + if (secondsString == null) { + return null; + } + + final millis = int.tryParse(secondsString); + + if (millis == null || millis == 0 && !allowZero) { return null; } + + return DateTime.fromMillisecondsSinceEpoch(millis * 1000); } } diff --git a/packages/package_info_plus/package_info_plus/lib/src/package_info_plus_windows.dart b/packages/package_info_plus/package_info_plus/lib/src/package_info_plus_windows.dart index acab12ed71..dd7eb8c5b7 100644 --- a/packages/package_info_plus/package_info_plus/lib/src/package_info_plus_windows.dart +++ b/packages/package_info_plus/package_info_plus/lib/src/package_info_plus_windows.dart @@ -37,6 +37,7 @@ class PackageInfoPlusWindowsPlugin extends PackageInfoPlatform { buildNumber: versions.getOrNull(1) ?? '', buildSignature: '', installTime: attributes.creationTime ?? attributes.lastWriteTime, + updateTime: attributes.lastWriteTime, ); info.dispose(); return Future.value(data); diff --git a/packages/package_info_plus/package_info_plus/macos/package_info_plus/Sources/package_info_plus/FPPPackageInfoPlusPlugin.m b/packages/package_info_plus/package_info_plus/macos/package_info_plus/Sources/package_info_plus/FPPPackageInfoPlusPlugin.m index a4167f7f1b..08943a9e4e 100644 --- a/packages/package_info_plus/package_info_plus/macos/package_info_plus/Sources/package_info_plus/FPPPackageInfoPlusPlugin.m +++ b/packages/package_info_plus/package_info_plus/macos/package_info_plus/Sources/package_info_plus/FPPPackageInfoPlusPlugin.m @@ -16,16 +16,8 @@ + (void)registerWithRegistrar:(NSObject *)registrar { - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { if ([call.method isEqualToString:@"getAll"]) { - NSURL* urlToDocumentsFolder = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject]; - __autoreleasing NSError *error; - NSDate *installDate = [[[NSFileManager defaultManager] attributesOfItemAtPath:urlToDocumentsFolder.path error:&error] objectForKey:NSFileCreationDate]; - - NSNumber *installTimeMillis = nil; - if (installDate) { - installTimeMillis = @((long long)([installDate timeIntervalSince1970] * 1000)); - } else { - installTimeMillis = nil; - } + NSDate *installDate = [self getInstallDate]; + NSDate *updateDate = [self getUpdateDate]; result(@{ @"appName" : [[NSBundle mainBundle] @@ -41,11 +33,45 @@ - (void)handleMethodCall:(FlutterMethodCall *)call objectForInfoDictionaryKey:@"CFBundleVersion"] ?: [NSNull null], @"installerStore" : [NSNull null], - @"installTime" : installTimeMillis ? [installTimeMillis stringValue] : [NSNull null] + @"installTime" : [self getTimeMillisStringFromDate:installDate] ?: [NSNull null], + @"updateTime" : [self getTimeMillisStringFromDate:updateDate] ?: [NSNull null] }); } else { result(FlutterMethodNotImplemented); } } +- (NSDate *)getInstallDate { + NSURL* urlToDocumentsFolder = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject]; + __autoreleasing NSError *error; + NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:urlToDocumentsFolder.path error:&error]; + + if (error) { + return nil; + } + + return [attributes objectForKey:NSFileCreationDate]; +} + +- (NSDate *)getUpdateDate { + __autoreleasing NSError *error; + NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[[NSBundle mainBundle] bundlePath] error:&error]; + NSDate *updateDate = [attributes fileModificationDate]; + + if (error) { + return nil; + } + + return updateDate; +} + +- (NSString *)getTimeMillisStringFromDate:(NSDate *)date { + if (!date) { + return nil; + } + + NSNumber *timeMillis = @((long long)([date timeIntervalSince1970] * 1000)); + return [timeMillis stringValue]; +} + @end diff --git a/packages/package_info_plus/package_info_plus/test/package_info_test.dart b/packages/package_info_plus/package_info_plus/test/package_info_test.dart index 16228a647d..583cf4c508 100644 --- a/packages/package_info_plus/package_info_plus/test/package_info_test.dart +++ b/packages/package_info_plus/package_info_plus/test/package_info_test.dart @@ -16,6 +16,9 @@ void main() { final now = DateTime.now().copyWith(microsecond: 0); + final mockInstallTime = now.subtract(const Duration(days: 1)); + final mockUpdateTime = now; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler( channel, @@ -29,7 +32,8 @@ void main() { 'packageName': 'io.flutter.plugins.packageinfoexample', 'version': '1.0', 'installerStore': null, - 'installTime': now.millisecondsSinceEpoch.toString(), + 'installTime': mockInstallTime.millisecondsSinceEpoch.toString(), + 'updateTime': mockUpdateTime.millisecondsSinceEpoch.toString(), }; default: assert(false); @@ -49,7 +53,8 @@ void main() { expect(info.packageName, 'io.flutter.plugins.packageinfoexample'); expect(info.version, '1.0'); expect(info.installerStore, null); - expect(info.installTime, now); + expect(info.installTime, mockInstallTime); + expect(info.updateTime, mockUpdateTime); expect( log, [ @@ -69,8 +74,10 @@ void main() { buildNumber: '2', buildSignature: 'deadbeef', installerStore: null, - installTime: now, + installTime: mockInstallTime, + updateTime: mockUpdateTime, ); + final info = await PackageInfo.fromPlatform(); expect(info.appName, 'mock_package_info_example'); expect(info.buildNumber, '2'); @@ -78,7 +85,8 @@ void main() { expect(info.version, '1.1'); expect(info.buildSignature, 'deadbeef'); expect(info.installerStore, null); - expect(info.installTime, now); + expect(info.installTime, mockInstallTime); + expect(info.updateTime, mockUpdateTime); }); test('equals checks for value equality', () async { @@ -89,7 +97,8 @@ void main() { version: '1.0', buildSignature: '', installerStore: null, - installTime: now, + installTime: mockInstallTime, + updateTime: mockUpdateTime, ); final info2 = PackageInfo( appName: 'package_info_example', @@ -98,7 +107,8 @@ void main() { version: '1.0', buildSignature: '', installerStore: null, - installTime: now, + installTime: mockInstallTime, + updateTime: mockUpdateTime, ); expect(info1, info2); }); @@ -111,7 +121,8 @@ void main() { version: '1.0', buildSignature: '', installerStore: null, - installTime: now, + installTime: mockInstallTime, + updateTime: mockUpdateTime, ); final info2 = PackageInfo( appName: 'package_info_example', @@ -120,7 +131,8 @@ void main() { version: '1.0', buildSignature: '', installerStore: null, - installTime: now, + installTime: mockInstallTime, + updateTime: mockUpdateTime, ); expect(info1.hashCode, info2.hashCode); }); @@ -133,11 +145,12 @@ void main() { version: '1.0', buildSignature: '', installerStore: null, - installTime: now, + installTime: mockInstallTime, + updateTime: mockUpdateTime, ); expect( info.toString(), - 'PackageInfo(appName: package_info_example, buildNumber: 1, packageName: io.flutter.plugins.packageinfoexample, version: 1.0, buildSignature: , installerStore: null, installTime: $now)', + 'PackageInfo(appName: package_info_example, buildNumber: 1, packageName: io.flutter.plugins.packageinfoexample, version: 1.0, buildSignature: , installerStore: null, installTime: $mockInstallTime, updateTime: $mockUpdateTime)', ); }); @@ -149,7 +162,8 @@ void main() { buildNumber: '2', buildSignature: '', installerStore: null, - installTime: now, + installTime: mockInstallTime, + updateTime: mockUpdateTime, ); final info1 = await PackageInfo.fromPlatform(); expect(info1.data, { @@ -157,7 +171,8 @@ void main() { 'packageName': 'io.flutter.plugins.mockpackageinfoexample', 'version': '1.1', 'buildNumber': '2', - 'installTime': now.toIso8601String(), + 'installTime': mockInstallTime.toIso8601String(), + 'updateTime': mockUpdateTime.toIso8601String(), }); final nextWeek = now.add(const Duration(days: 7)); @@ -169,6 +184,7 @@ void main() { buildSignature: 'deadbeef', installerStore: 'testflight', installTime: nextWeek, + updateTime: nextWeek, ); final info2 = await PackageInfo.fromPlatform(); expect(info2.data, { @@ -179,6 +195,7 @@ void main() { 'buildSignature': 'deadbeef', 'installerStore': 'testflight', 'installTime': nextWeek.toIso8601String(), + 'updateTime': nextWeek.toIso8601String(), }); }); @@ -191,6 +208,7 @@ void main() { buildSignature: '', installerStore: null, installTime: null, + updateTime: null, ); final info1 = await PackageInfo.fromPlatform(); expect(info1.data, { @@ -209,7 +227,8 @@ void main() { buildNumber: '2', buildSignature: 'signature', installerStore: 'store', - installTime: now, + installTime: mockInstallTime, + updateTime: mockUpdateTime, ); final info1 = await PackageInfo.fromPlatform(); @@ -224,7 +243,8 @@ void main() { 'buildNumber': '2', 'buildSignature': 'signature', 'installerStore': 'store', - 'installTime': now.toIso8601String(), + 'installTime': mockInstallTime.toIso8601String(), + 'updateTime': mockUpdateTime.toIso8601String(), }); }); } diff --git a/packages/package_info_plus/package_info_plus_platform_interface/lib/method_channel_package_info.dart b/packages/package_info_plus/package_info_plus_platform_interface/lib/method_channel_package_info.dart index 45918a8a55..3bf014328a 100644 --- a/packages/package_info_plus/package_info_plus_platform_interface/lib/method_channel_package_info.dart +++ b/packages/package_info_plus/package_info_plus_platform_interface/lib/method_channel_package_info.dart @@ -12,10 +12,8 @@ class MethodChannelPackageInfo extends PackageInfoPlatform { Future getAll({String? baseUrl}) async { final map = await _channel.invokeMapMethod('getAll'); - final installTime = map?['installTime'] != null && - int.tryParse(map!['installTime']!) != null - ? DateTime.fromMillisecondsSinceEpoch(int.parse(map['installTime']!)) - : null; + final installTime = _parseNullableStringMillis(map?['installTime']); + final updateTime = _parseNullableStringMillis(map?['updateTime']); return PackageInfoData( appName: map!['appName'] ?? '', @@ -25,6 +23,13 @@ class MethodChannelPackageInfo extends PackageInfoPlatform { buildSignature: map['buildSignature'] ?? '', installerStore: map['installerStore'] as String?, installTime: installTime, + updateTime: updateTime, ); } + + DateTime? _parseNullableStringMillis(String? millis) { + return millis != null && int.tryParse(millis) != null + ? DateTime.fromMillisecondsSinceEpoch(int.parse(millis)) + : null; + } } diff --git a/packages/package_info_plus/package_info_plus_platform_interface/lib/package_info_data.dart b/packages/package_info_plus/package_info_plus_platform_interface/lib/package_info_data.dart index 8ac2712a25..098e91c0dc 100644 --- a/packages/package_info_plus/package_info_plus_platform_interface/lib/package_info_data.dart +++ b/packages/package_info_plus/package_info_plus_platform_interface/lib/package_info_data.dart @@ -11,6 +11,7 @@ class PackageInfoData { required this.buildSignature, this.installerStore, this.installTime, + this.updateTime, }); /// The app name. `CFBundleDisplayName` on iOS, `application/label` on Android. @@ -40,4 +41,13 @@ class PackageInfoData { /// If the last modified date is not available, returns `null`. /// - On web, returns `null`. final DateTime? installTime; + + /// The time when the application was last updated. + /// + /// - On Android, returns `PackageManager.lastUpdateTime` + /// - On iOS and macOS, return the last modified date of the app main bundle + /// - On Windows and Linux, returns the last modified date of the app executable. + /// If the last modified date is not available, returns `null`. + /// - On web, returns `null`. + final DateTime? updateTime; }