Skip to content

Commit

Permalink
feat(package_info_plus): implement install time
Browse files Browse the repository at this point in the history
for desktop platforms
  • Loading branch information
Andrew-Bekhiet committed Jan 16, 2025
1 parent 4b25c5f commit 7c16b7b
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -74,22 +74,43 @@ void main() {
expect(info.packageName, 'io.flutter.plugins.packageInfoExample');
expect(info.version, '1.2.3');
expect(info.installerStore, null);
expect(info.installTime, null);
expect(
info.installTime,
isA<DateTime>().having(
(d) => d.difference(DateTime.now()).inMinutes,
'Was just installed',
lessThanOrEqualTo(1),
),
);
} else if (Platform.isLinux) {
expect(info.appName, 'package_info_plus_example');
expect(info.buildNumber, '4');
expect(info.buildSignature, isEmpty);
expect(info.packageName, 'package_info_plus_example');
expect(info.version, '1.2.3');
expect(info.installTime, null);
expect(
info.installTime,
isA<DateTime>().having(
(d) => d.difference(DateTime.now()).inMinutes,
'Was just installed',
lessThanOrEqualTo(1),
),
);
} else if (Platform.isWindows) {
expect(info.appName, 'example');
expect(info.buildNumber, '4');
expect(info.buildSignature, isEmpty);
expect(info.packageName, 'example');
expect(info.version, '1.2.3');
expect(info.installerStore, null);
expect(info.installTime, null);
expect(
info.installTime,
isA<DateTime>().having(
(d) => d.difference(DateTime.now()).inMinutes,
'Was just installed',
lessThanOrEqualTo(1),
),
);
} else {
throw (UnsupportedError('platform not supported'));
}
Expand Down Expand Up @@ -150,20 +171,20 @@ void main() {
expect(find.text('1.2.3'), findsOneWidget);
expect(find.text('Not set'), findsOneWidget);
expect(find.text('not available'), findsOneWidget);
expect(find.text('Install time not available'), findsOneWidget);
expect(find.textContaining(installTimeRegex), findsOneWidget);
} 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.text('Install time not available'), findsOneWidget);
expect(find.textContaining(installTimeRegex), findsOneWidget);
} 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.text('Install time not available'), findsOneWidget);
expect(find.textContaining(installTimeRegex), findsOneWidget);
} else {
throw (UnsupportedError('platform not supported'));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,14 @@ class PackageInfo {
/// The installer store. Indicates through which store this application was installed.
final String? installerStore;

/// The time when the application was installed on the device.
///
/// Checks the creation date of the Documents directory on iOS
/// or returns `packageInfo.firstInstallTime` on Android.
/// Otherwise returns null.
/// The time when the application was installed.
///
/// - On Android, returns `PackageManager.firstInstallTime`
/// - On iOS and macOS, return the creation date of the app default `NSDocumentDirectory`
/// - On Windows and Linux, returns the creation date of the app executable.
/// If the creation date is not available, 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? installTime;

/// Initializes the application metadata with mock values for testing.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import 'dart:ffi';
import 'dart:io';

import 'package:ffi/ffi.dart';
import 'package:win32/win32.dart';

base class FILEATTRIBUTEDATA extends Struct {
@DWORD()
external int dwFileAttributes;

external FILETIME ftCreationTime;

external FILETIME ftLastAccessTime;

external FILETIME ftLastWriteTime;

@DWORD()
external int nFileSizeHigh;

@DWORD()
external int nFileSizeLow;
}

class FileAttributes {
final String filePath;

late final DateTime? creationTime;
late final DateTime? lastWriteTime;

FileAttributes(this.filePath) {
final attributesPtr = getFileAttributes(filePath);

if (attributesPtr != null) {
creationTime = fileTimeToDartDateTime(attributesPtr.ref.ftCreationTime);
lastWriteTime = fileTimeToDartDateTime(attributesPtr.ref.ftLastWriteTime);

free(attributesPtr);
} else {
creationTime = null;
lastWriteTime = null;
}
}

static Pointer<FILEATTRIBUTEDATA>? getFileAttributes(String filePath) {
if (!File(filePath).existsSync()) {
throw ArgumentError.value(filePath, 'filePath', 'File not present');
}

final lptstrFilename = TEXT(filePath);
final lpFileInformation = calloc<FILEATTRIBUTEDATA>();

try {
if (GetFileAttributesEx(lptstrFilename, 0, lpFileInformation) == 0) {
free(lpFileInformation);

return null;
}

return lpFileInformation;
} finally {}
}

static DateTime? fileTimeToDartDateTime(FILETIME? fileTime) {
if (fileTime == null) return null;

final high = fileTime.dwHighDateTime;
final low = fileTime.dwLowDateTime;

final fileTime64 = (high << 32) + low;

final unixTimeMs = ((fileTime64 ~/ 10000) - 11644473600000);

return DateTime.fromMillisecondsSinceEpoch(unixTimeMs);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class PackageInfoPlusLinuxPlugin extends PackageInfoPlatform {
buildNumber: versionJson['build_number'] ?? '',
packageName: versionJson['package_name'] ?? '',
buildSignature: '',
installTime: versionJson['install_time'],
);
}

Expand All @@ -32,9 +33,35 @@ class PackageInfoPlusLinuxPlugin extends PackageInfoPlatform {
final appPath = path.dirname(exePath);
final assetPath = path.join(appPath, 'data', 'flutter_assets');
final versionPath = path.join(assetPath, 'version.json');
return jsonDecode(await File(versionPath).readAsString());

final installTime = await _getInstallTime(exePath);

return {
...jsonDecode(await File(versionPath).readAsString()),
'install_time': installTime,
};
} catch (_) {
return <String, dynamic>{};
}
}

Future<DateTime?> _getInstallTime(String exePath) async {
try {
final statResult = await Process.run(
'stat',
['-c', '%W', exePath],
stdoutEncoding: utf8,
);

if (statResult.exitCode == 0 && int.tryParse(statResult.stdout) != null) {
final creationTimeSeconds = int.parse(statResult.stdout) * 1000;

return DateTime.fromMillisecondsSinceEpoch(creationTimeSeconds);
}

return await File(exePath).lastModified();
} catch (_) {
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
/// The Windows implementation of `package_info_plus`.
library package_info_plus_windows;
library;

import 'dart:io';

import 'package:package_info_plus_platform_interface/package_info_data.dart';
import 'package:package_info_plus_platform_interface/package_info_platform_interface.dart';

import 'file_attribute.dart';
import 'file_version_info.dart';

/// The Windows implementation of [PackageInfoPlatform].
Expand All @@ -27,13 +28,15 @@ class PackageInfoPlusWindowsPlugin extends PackageInfoPlatform {
}

final info = FileVersionInfo(resolvedExecutable);
final attributes = FileAttributes(resolvedExecutable);
final versions = info.productVersion.split('+');
final data = PackageInfoData(
appName: info.productName,
packageName: info.internalName,
version: versions.getOrNull(0) ?? '',
buildNumber: versions.getOrNull(1) ?? '',
buildSignature: '',
installTime: attributes.creationTime ?? attributes.lastWriteTime,
);
info.dispose();
return Future.value(data);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)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 = installDate ? @((long long)([installDate timeIntervalSince1970] * 1000)) : [NSNull null];

result(@{
@"appName" : [[NSBundle mainBundle]
objectForInfoDictionaryKey:@"CFBundleDisplayName"]
Expand All @@ -29,7 +34,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call
@"buildNumber" : [[NSBundle mainBundle]
objectForInfoDictionaryKey:@"CFBundleVersion"]
?: [NSNull null],
@"installerStore" : [NSNull null]
@"installerStore" : [NSNull null],
@"installTime" : installTimeMillis ? [installTimeMillis stringValue] : [NSNull null]
});
} else {
result(FlutterMethodNotImplemented);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
@TestOn('windows')
library package_info_plus_windows_test;
library;

import 'dart:io' show Platform;
import 'dart:io' show File, Platform;

import 'package:flutter_test/flutter_test.dart';
import 'package:package_info_plus/src/file_attribute.dart';
import 'package:package_info_plus/src/file_version_info.dart';
import 'package:package_info_plus/src/package_info_plus_windows.dart';
import 'package:package_info_plus_platform_interface/package_info_platform_interface.dart';
Expand Down Expand Up @@ -31,6 +32,32 @@ void main() {
kernelVersion.dispose();
});

test('File creation and modification time', () async {
final DateTime now = DateTime.now();
final testFile = await File('./test.txt').create();

final fileAttributes = FileAttributes(testFile.path);

expect(
fileAttributes.creationTime,
isA<DateTime>().having(
(d) => d.difference(now).inSeconds,
'Was just created',
lessThanOrEqualTo(1),
),
);
expect(
fileAttributes.lastWriteTime,
isA<DateTime>().having(
(d) => d.difference(now).inSeconds,
'Was just modified',
lessThanOrEqualTo(1),
),
);

await testFile.delete();
});

test('File version info for missing file', () {
const missingFile = 'C:\\macos\\system128\\colonel.dll';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ class PackageInfoData {
/// The installer store. Indicates through which store this application was installed.
final String? installerStore;

/// The time when the application was installed. The creation date of documents directory on iOS, `firstInstallTime` on Android, null otherwise.
/// The time when the application was installed.
///
/// - On Android, returns `PackageManager.firstInstallTime`
/// - On iOS and macOS, return the creation date of the app default `NSDocumentDirectory`
/// - On Windows and Linux, returns the creation date of the app executable.
/// If the creation date is not available, 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? installTime;
}

0 comments on commit 7c16b7b

Please sign in to comment.