update_service.dart 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import 'dart:io';
  2. import 'package:ente_auth/core/constants.dart';
  3. import 'package:ente_auth/core/network.dart';
  4. import 'package:ente_auth/services/notification_service.dart';
  5. import 'package:logging/logging.dart';
  6. import 'package:package_info_plus/package_info_plus.dart';
  7. import 'package:shared_preferences/shared_preferences.dart';
  8. import 'package:tuple/tuple.dart';
  9. import 'package:url_launcher/url_launcher_string.dart';
  10. class UpdateService {
  11. UpdateService._privateConstructor();
  12. static final UpdateService instance = UpdateService._privateConstructor();
  13. static const kUpdateAvailableShownTimeKey = "update_available_shown_time_key";
  14. static const String flavor = String.fromEnvironment('app.flavor');
  15. LatestVersionInfo? _latestVersion;
  16. final _logger = Logger("UpdateService");
  17. late PackageInfo _packageInfo;
  18. late SharedPreferences _prefs;
  19. Future<void> init() async {
  20. _packageInfo = await PackageInfo.fromPlatform();
  21. _prefs = await SharedPreferences.getInstance();
  22. }
  23. Future<bool> shouldUpdate() async {
  24. if (!isIndependent()) {
  25. return false;
  26. }
  27. try {
  28. _latestVersion = await _getLatestVersionInfo();
  29. final currentVersionCode = int.parse(_packageInfo.buildNumber);
  30. return currentVersionCode < _latestVersion!.code!;
  31. } catch (e) {
  32. _logger.severe(e);
  33. return false;
  34. }
  35. }
  36. bool shouldForceUpdate(LatestVersionInfo? info) {
  37. if (!isIndependent()) {
  38. return false;
  39. }
  40. try {
  41. final currentVersionCode = int.parse(_packageInfo.buildNumber);
  42. return currentVersionCode < info!.lastSupportedVersionCode;
  43. } catch (e) {
  44. _logger.severe(e);
  45. return false;
  46. }
  47. }
  48. LatestVersionInfo? getLatestVersionInfo() {
  49. return _latestVersion;
  50. }
  51. Future<void> showUpdateNotification() async {
  52. if (!isIndependent()) {
  53. return;
  54. }
  55. final shouldUpdate = await this.shouldUpdate();
  56. final lastNotificationShownTime =
  57. _prefs.getInt(kUpdateAvailableShownTimeKey) ?? 0;
  58. final now = DateTime.now().microsecondsSinceEpoch;
  59. final hasBeen3DaysSinceLastNotification =
  60. (now - lastNotificationShownTime) > (3 * microSecondsInDay);
  61. if (shouldUpdate &&
  62. hasBeen3DaysSinceLastNotification &&
  63. _latestVersion!.shouldNotify!) {
  64. NotificationService.instance.showNotification(
  65. "Update available",
  66. "Click to install our best version yet",
  67. );
  68. await _prefs.setInt(kUpdateAvailableShownTimeKey, now);
  69. } else {
  70. _logger.info("Debouncing notification");
  71. }
  72. }
  73. Future<LatestVersionInfo> _getLatestVersionInfo() async {
  74. final response = await Network.instance
  75. .getDio()
  76. .get("https://ente.io/release-info/auth-independent.json");
  77. return LatestVersionInfo.fromMap(response.data["latestVersion"]);
  78. }
  79. // getRateDetails returns details about the place
  80. Tuple2<String, String> getRateDetails() {
  81. // Note: in auth, currently we don't have a way to identify if the
  82. // app was installed from play store, f-droid or github based on pkg name
  83. if (Platform.isAndroid) {
  84. if (flavor == "playstore") {
  85. return const Tuple2(
  86. "Play Store",
  87. "market://details??id=io.ente.auth",
  88. );
  89. }
  90. return const Tuple2(
  91. "AlternativeTo",
  92. "https://alternativeto.net/software/ente-authenticator/about/",
  93. );
  94. }
  95. return const Tuple2(
  96. "App Store",
  97. "https://apps.apple.com/in/app/ente-photos/id6444121398",
  98. );
  99. }
  100. Future<void> launchReviewUrl() async {
  101. final String url = getRateDetails().item2;
  102. try {
  103. await launchUrlString(url, mode: LaunchMode.externalApplication);
  104. } catch (e) {
  105. _logger.severe("Failed top open launch url $url", e);
  106. // Fall back if we fail to open play-store market app on android
  107. if (Platform.isAndroid && url.startsWith("market://")) {
  108. launchUrlString(
  109. "https://play.google.com/store/apps/details?id=io.ente.auth",
  110. mode: LaunchMode.externalApplication,
  111. ).ignore();
  112. }
  113. }
  114. }
  115. bool isIndependent() {
  116. return flavor == "independent" ||
  117. _packageInfo.packageName.endsWith("independent");
  118. }
  119. }
  120. class LatestVersionInfo {
  121. final String? name;
  122. final int? code;
  123. final List<String> changelog;
  124. final bool? shouldForceUpdate;
  125. final int lastSupportedVersionCode;
  126. final String? url;
  127. final int? size;
  128. final bool? shouldNotify;
  129. LatestVersionInfo(
  130. this.name,
  131. this.code,
  132. this.changelog,
  133. this.shouldForceUpdate,
  134. this.lastSupportedVersionCode,
  135. this.url,
  136. this.size,
  137. this.shouldNotify,
  138. );
  139. factory LatestVersionInfo.fromMap(Map<String, dynamic> map) {
  140. return LatestVersionInfo(
  141. map['name'],
  142. map['code'],
  143. List<String>.from(map['changelog']),
  144. map['shouldForceUpdate'],
  145. map['lastSupportedVersionCode'] ?? 1,
  146. map['url'],
  147. map['size'],
  148. map['shouldNotify'],
  149. );
  150. }
  151. }