Compare commits

...

18 commits

Author SHA1 Message Date
Neeraj Gupta ca0474faca Updated submodule mobile/thirdparty/flutter to 3.22.1 2024-05-23 17:00:33 +05:30
Neeraj Gupta b469985277 Removed submodule mobile/thirdparty/isar 2024-05-23 16:58:51 +05:30
Neeraj Gupta 2a5dacb460 Merge branch 'main' into f-droid 2024-05-23 16:55:27 +05:30
vishnukvmd d16f98cf07 v0.8.95 2024-05-12 08:44:26 +05:30
vishnukvmd 8677cbb4f8 Increase JVM allocation pool 2024-05-12 08:43:55 +05:30
vishnukvmd 0e33299863 Merge branch 'main' into f-droid 2024-05-07 12:54:44 +05:30
ashilkn 93ba4e011a Merge branch 'main' into f-droid 2024-04-20 15:23:14 +05:30
vishnukvmd 7977bebcaa Update Flutter to v3.19.3 2024-04-16 11:35:32 +05:30
ashilkn f28f49d724 Merge main 2024-04-15 11:20:03 +05:30
ashilkn d9a93ddad6 Merge branch 'main' into f-droid 2024-04-13 15:24:56 +05:30
ashilkn 07808d6139 Merge branch 'main' into f-droid 2024-04-02 17:22:34 +05:30
vishnukvmd 1e1633bb45 Merge branch 'main' into f-droid 2024-03-13 21:57:19 +05:30
vishnukvmd c0f33de0c8 Remove dead code 2024-03-13 21:56:09 +05:30
vishnukvmd 417621b17c Pull code for transistor-background-fetch 2024-03-13 14:14:19 +05:30
vishnukvmd 8322540732 Add submodule for Flutter 2024-03-13 14:13:40 +05:30
vishnukvmd 2d61be37bb Add submodule for Isar 2024-03-13 14:12:23 +05:30
vishnukvmd 2a10aa7d61 Merge branch 'fdroid_cleanup' into f-droid 2024-03-13 13:52:25 +05:30
vishnukvmd 004eb310b3 Prepare for F-Droid 2024-03-13 13:43:46 +05:30
62 changed files with 2855 additions and 895 deletions

3
.gitmodules vendored
View file

@ -20,3 +20,6 @@
path = web/apps/photos/thirdparty/photoswipe
url = https://github.com/ente-io/PhotoSwipe.git
branch = single-thread
[submodule "mobile/thirdparty/flutter"]
path = mobile/thirdparty/flutter
url = https://github.com/flutter/flutter

View file

@ -7,10 +7,7 @@ allprojects {
google()
jcenter()
mavenCentral()
// mavenLocal() // for FDroid
maven {
url "${project(':background_fetch').projectDir}/libs"
}
mavenLocal() // for FDroid
}
}

View file

@ -1,4 +1,4 @@
org.gradle.jvmargs=-Xmx4608m
org.gradle.jvmargs=-Xmx6144m
android.enableR8=true
android.useAndroidX=true
android.enableJetifier=true

View file

@ -98,7 +98,7 @@ class MessageLookup extends MessageLookupByLibrary {
"${storageAmountInGB} GB cada vez que alguém se inscrever para um plano pago e aplica o seu código";
static String m25(freeAmount, storageUnit) =>
"${freeAmount} ${storageUnit} grátis";
"${freeAmount} ${storageUnit} livre";
static String m26(endDate) => "Teste gratuito acaba em ${endDate}";
@ -225,6 +225,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Eu entendo que se eu perder minha senha, posso perder meus dados, já que meus dados são <underline>criptografados de ponta a ponta</underline>."),
"activeSessions":
MessageLookupByLibrary.simpleMessage("Sessões ativas"),
"addAName": MessageLookupByLibrary.simpleMessage("Adicione um nome"),
"addANewEmail":
MessageLookupByLibrary.simpleMessage("Adicionar um novo email"),
"addCollaborator":
@ -446,7 +447,7 @@ class MessageLookup extends MessageLookupByLibrary {
"clubByFileName": MessageLookupByLibrary.simpleMessage(
"Agrupar pelo nome de arquivo"),
"clusteringProgress":
MessageLookupByLibrary.simpleMessage("Clustering progress"),
MessageLookupByLibrary.simpleMessage("Progresso de agrupamento"),
"codeAppliedPageTitle":
MessageLookupByLibrary.simpleMessage("Código aplicado"),
"codeCopiedToClipboard": MessageLookupByLibrary.simpleMessage(
@ -692,6 +693,8 @@ class MessageLookup extends MessageLookupByLibrary {
"enterPassword": MessageLookupByLibrary.simpleMessage("Digite a senha"),
"enterPasswordToEncrypt": MessageLookupByLibrary.simpleMessage(
"Insira a senha para criptografar seus dados"),
"enterPersonName":
MessageLookupByLibrary.simpleMessage("Inserir nome da pessoa"),
"enterReferralCode": MessageLookupByLibrary.simpleMessage(
"Insira o código de referência"),
"enterThe6digitCodeFromnyourAuthenticatorApp":
@ -717,9 +720,9 @@ class MessageLookup extends MessageLookupByLibrary {
"exportYourData":
MessageLookupByLibrary.simpleMessage("Exportar seus dados"),
"faceRecognition":
MessageLookupByLibrary.simpleMessage("Face recognition"),
MessageLookupByLibrary.simpleMessage("Reconhecimento facial"),
"faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage(
"Please note that this will result in a higher bandwidth and battery usage until all items are indexed."),
"Por favor, note que isso resultará em uma largura de banda maior e uso de bateria até que todos os itens sejam indexados."),
"faces": MessageLookupByLibrary.simpleMessage("Rostos"),
"failedToApplyCode":
MessageLookupByLibrary.simpleMessage("Falha ao aplicar o código"),
@ -761,12 +764,15 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Arquivos excluídos"),
"filesSavedToGallery":
MessageLookupByLibrary.simpleMessage("Arquivos salvos na galeria"),
"findPeopleByName": MessageLookupByLibrary.simpleMessage(
"Encontre pessoas rapidamente por nome"),
"flip": MessageLookupByLibrary.simpleMessage("Inverter"),
"forYourMemories":
MessageLookupByLibrary.simpleMessage("para suas memórias"),
"forgotPassword":
MessageLookupByLibrary.simpleMessage("Esqueceu sua senha"),
"foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"),
"foundFaces":
MessageLookupByLibrary.simpleMessage("Rostos encontrados"),
"freeStorageClaimed": MessageLookupByLibrary.simpleMessage(
"Armazenamento gratuito reivindicado"),
"freeStorageOnReferralSuccess": m24,
@ -1064,6 +1070,7 @@ class MessageLookup extends MessageLookupByLibrary {
"pendingItems": MessageLookupByLibrary.simpleMessage("Itens pendentes"),
"pendingSync":
MessageLookupByLibrary.simpleMessage("Sincronização pendente"),
"people": MessageLookupByLibrary.simpleMessage("Pessoas"),
"peopleUsingYourCode":
MessageLookupByLibrary.simpleMessage("Pessoas que usam seu código"),
"permDeleteWarning": MessageLookupByLibrary.simpleMessage(
@ -1197,6 +1204,8 @@ class MessageLookup extends MessageLookupByLibrary {
"removeParticipant":
MessageLookupByLibrary.simpleMessage("Remover participante"),
"removeParticipantBody": m43,
"removePersonLabel":
MessageLookupByLibrary.simpleMessage("Remover etiqueta da pessoa"),
"removePublicLink":
MessageLookupByLibrary.simpleMessage("Remover link público"),
"removeShareItemsWarning": MessageLookupByLibrary.simpleMessage(
@ -1260,7 +1269,7 @@ class MessageLookup extends MessageLookupByLibrary {
"searchDatesEmptySection": MessageLookupByLibrary.simpleMessage(
"Pesquisar por data, mês ou ano"),
"searchFaceEmptySection": MessageLookupByLibrary.simpleMessage(
"Encontre todas as fotos de uma pessoa"),
"Pessoas serão exibidas aqui uma vez que a indexação é feita"),
"searchFileTypesAndNamesEmptySection":
MessageLookupByLibrary.simpleMessage("Tipos de arquivo e nomes"),
"searchHint1": MessageLookupByLibrary.simpleMessage(

View file

@ -5,7 +5,6 @@ import "dart:isolate";
import "package:adaptive_theme/adaptive_theme.dart";
import 'package:background_fetch/background_fetch.dart';
import "package:computer/computer.dart";
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import "package:flutter/rendering.dart";
@ -39,7 +38,6 @@ import 'package:photos/services/machine_learning/file_ml/remote_fileml_service.d
import "package:photos/services/machine_learning/machine_learning_controller.dart";
import 'package:photos/services/machine_learning/semantic_search/semantic_search_service.dart';
import 'package:photos/services/memories_service.dart';
import 'package:photos/services/push_service.dart';
import 'package:photos/services/remote_sync_service.dart';
import 'package:photos/services/search_service.dart';
import "package:photos/services/storage_bonus_service.dart";
@ -223,16 +221,6 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
await HomeWidgetService.instance.countHomeWidgets() == 0) {
unawaited(HomeWidgetService.instance.initHomeWidget());
}
if (Platform.isIOS) {
// ignore: unawaited_futures
PushService.instance.init().then((_) {
FirebaseMessaging.onBackgroundMessage(
_firebaseMessagingBackgroundHandler,
);
});
}
unawaited(SemanticSearchService.instance.init());
MachineLearningController.instance.init();
// Can not including existing tf/ml binaries as they are not being built
@ -359,35 +347,6 @@ Future<void> _killBGTask([String? taskId]) async {
Isolate.current.kill();
}
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
final bool isRunningInFG = await _isRunningInForeground(); // hb
final bool isInForeground = AppLifecycleService.instance.isForeground;
if (_isProcessRunning) {
_logger.info(
"Background push received when app is alive and runningInFS: $isRunningInFG inForeground: $isInForeground",
);
if (PushService.shouldSync(message)) {
await _sync('firebaseBgSyncActiveProcess');
}
} else {
// App is dead
// ignore: unawaited_futures
_runWithLogs(
() async {
_logger.info("Background push received");
if (Platform.isIOS) {
_scheduleSuicide(kBGPushTimeout); // To prevent OS from punishing us
}
await _init(true, via: 'firebasePush');
if (PushService.shouldSync(message)) {
await _sync('firebaseBgSyncNoActiveProcess');
}
},
prefix: "[fbg]",
);
}
}
Future<void> _logFGHeartBeatInfo() async {
final bool isRunningInFG = await _isRunningInForeground();
final prefs = await SharedPreferences.getInstance();

View file

@ -4,7 +4,6 @@ import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
// import 'package:flutter/foundation.dart';
// import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:logging/logging.dart';
import 'package:photos/core/errors.dart';
import 'package:photos/core/network/network.dart';
@ -39,6 +38,7 @@ class BillingService {
final _logger = Logger("BillingService");
final _enteDio = NetworkClient.instance.enteDio;
// ignore: unused_field
bool _isOnSubscriptionPage = false;
Future<BillingPlans>? _future;
@ -48,23 +48,6 @@ class BillingService {
// await FlutterInappPurchase.instance.initConnection;
// FlutterInappPurchase.instance.clearTransactionIOS();
// }
InAppPurchase.instance.purchaseStream.listen((purchases) {
if (_isOnSubscriptionPage) {
return;
}
for (final purchase in purchases) {
if (purchase.status == PurchaseStatus.purchased) {
verifySubscription(
purchase.productID,
purchase.verificationData.serverVerificationData,
).then((response) {
InAppPurchase.instance.completePurchase(purchase);
});
} else if (Platform.isIOS && purchase.pendingCompletePurchase) {
InAppPurchase.instance.completePurchase(purchase);
}
}
});
}
void clearCache() {

View file

@ -1,103 +0,0 @@
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:logging/logging.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/core/constants.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/core/network/network.dart';
import 'package:photos/events/signed_in_event.dart';
import 'package:photos/services/sync_service.dart';
import 'package:shared_preferences/shared_preferences.dart';
class PushService {
static const kFCMPushToken = "fcm_push_token";
static const kLastFCMTokenUpdationTime = "fcm_push_token_updation_time";
static const kFCMTokenUpdationIntervalInMicroSeconds = 30 * microSecondsInDay;
static const kPushAction = "action";
static const kSync = "sync";
static final PushService instance = PushService._privateConstructor();
static final _logger = Logger("PushService");
late SharedPreferences _prefs;
PushService._privateConstructor();
Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
await Firebase.initializeApp();
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
_logger.info("Got a message whilst in the foreground!");
_handleForegroundPushMessage(message);
});
try {
if (Configuration.instance.hasConfiguredAccount()) {
await _configurePushToken();
} else {
Bus.instance.on<SignedInEvent>().listen((_) async {
// ignore: unawaited_futures
_configurePushToken();
});
}
} catch (e, s) {
_logger.severe("Could not configure push token", e, s);
}
}
Future<void> _configurePushToken() async {
final String? fcmToken = await FirebaseMessaging.instance.getToken();
final shouldForceRefreshServerToken =
DateTime.now().microsecondsSinceEpoch -
(_prefs.getInt(kLastFCMTokenUpdationTime) ?? 0) >
kFCMTokenUpdationIntervalInMicroSeconds;
if (fcmToken != null &&
(_prefs.getString(kFCMPushToken) != fcmToken ||
shouldForceRefreshServerToken)) {
final String? apnsToken = await FirebaseMessaging.instance.getAPNSToken();
try {
_logger.info("Updating token on server");
await _setPushTokenOnServer(fcmToken, apnsToken);
await _prefs.setString(kFCMPushToken, fcmToken);
await _prefs.setInt(
kLastFCMTokenUpdationTime,
DateTime.now().microsecondsSinceEpoch,
);
_logger.info("Push token updated on server");
} catch (e) {
_logger.severe("Could not set push token", e, StackTrace.current);
}
} else {
_logger.info("Skipping token update");
}
}
Future<void> _setPushTokenOnServer(
String fcmToken,
String? apnsToken,
) async {
await NetworkClient.instance.enteDio.post(
"/push/token",
data: {
"fcmToken": fcmToken,
"apnsToken": apnsToken,
},
);
}
void _handleForegroundPushMessage(RemoteMessage message) {
_logger.info("Message data: ${message.data}");
if (message.notification != null) {
_logger.info(
"Message also contained a notification: ${message.notification}",
);
}
if (shouldSync(message)) {
SyncService.instance.sync();
}
}
static bool shouldSync(RemoteMessage message) {
return message.data.containsKey(kPushAction) &&
message.data[kPushAction] == kSync;
}
}

View file

@ -1,610 +0,0 @@
import 'dart:async';
import 'dart:io';
import "package:flutter/cupertino.dart";
import "package:flutter/foundation.dart";
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:logging/logging.dart';
import 'package:photos/core/errors.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/events/subscription_purchased_event.dart';
import "package:photos/generated/l10n.dart";
import 'package:photos/models/billing_plan.dart';
import 'package:photos/models/subscription.dart';
import 'package:photos/models/user_details.dart';
import 'package:photos/services/billing_service.dart';
import "package:photos/services/update_service.dart";
import 'package:photos/services/user_service.dart';
import "package:photos/theme/colors.dart";
import "package:photos/theme/ente_theme.dart";
import 'package:photos/ui/common/loading_widget.dart';
import 'package:photos/ui/common/progress_dialog.dart';
import "package:photos/ui/components/captioned_text_widget.dart";
import "package:photos/ui/components/menu_item_widget/menu_item_widget.dart";
import 'package:photos/ui/payment/child_subscription_widget.dart';
import 'package:photos/ui/payment/skip_subscription_widget.dart';
import 'package:photos/ui/payment/subscription_common_widgets.dart';
import 'package:photos/ui/payment/subscription_plan_widget.dart';
import "package:photos/ui/payment/view_add_on_widget.dart";
import "package:photos/utils/data_util.dart";
import 'package:photos/utils/dialog_util.dart';
import 'package:photos/utils/toast_util.dart';
import 'package:url_launcher/url_launcher_string.dart';
class StoreSubscriptionPage extends StatefulWidget {
final bool isOnboarding;
const StoreSubscriptionPage({
this.isOnboarding = false,
Key? key,
}) : super(key: key);
@override
State<StoreSubscriptionPage> createState() => _StoreSubscriptionPageState();
}
class _StoreSubscriptionPageState extends State<StoreSubscriptionPage> {
final _logger = Logger("SubscriptionPage");
final _billingService = BillingService.instance;
final _userService = UserService.instance;
Subscription? _currentSubscription;
late StreamSubscription _purchaseUpdateSubscription;
late ProgressDialog _dialog;
late UserDetails _userDetails;
late bool _hasActiveSubscription;
bool _hideCurrentPlanSelection = false;
late FreePlan _freePlan;
late List<BillingPlan> _plans;
bool _hasLoadedData = false;
bool _isLoading = false;
late bool _isActiveStripeSubscriber;
EnteColorScheme colorScheme = darkScheme;
// hasYearlyPlans is used to check if there are yearly plans for given store
bool hasYearlyPlans = false;
// _showYearlyPlan is used to determine if we should show the yearly plans
bool showYearlyPlan = false;
@override
void initState() {
_billingService.setIsOnSubscriptionPage(true);
_setupPurchaseUpdateStreamListener();
super.initState();
}
void _setupPurchaseUpdateStreamListener() {
_purchaseUpdateSubscription =
InAppPurchase.instance.purchaseStream.listen((purchases) async {
if (!_dialog.isShowing()) {
await _dialog.show();
}
for (final purchase in purchases) {
_logger.info("Purchase status " + purchase.status.toString());
if (purchase.status == PurchaseStatus.purchased) {
try {
final newSubscription = await _billingService.verifySubscription(
purchase.productID,
purchase.verificationData.serverVerificationData,
);
await InAppPurchase.instance.completePurchase(purchase);
String text = S.of(context).thankYouForSubscribing;
if (!widget.isOnboarding) {
final isUpgrade = _hasActiveSubscription &&
newSubscription.storage > _currentSubscription!.storage;
final isDowngrade = _hasActiveSubscription &&
newSubscription.storage < _currentSubscription!.storage;
if (isUpgrade) {
text = S.of(context).yourPlanWasSuccessfullyUpgraded;
} else if (isDowngrade) {
text = S.of(context).yourPlanWasSuccessfullyDowngraded;
}
}
showShortToast(context, text);
_currentSubscription = newSubscription;
_hasActiveSubscription = _currentSubscription!.isValid();
setState(() {});
await _dialog.hide();
Bus.instance.fire(SubscriptionPurchasedEvent());
if (widget.isOnboarding) {
Navigator.of(context).popUntil((route) => route.isFirst);
}
} on SubscriptionAlreadyClaimedError catch (e) {
_logger.warning("subscription is already claimed ", e);
await _dialog.hide();
final String title = Platform.isAndroid
? S.of(context).playstoreSubscription
: S.of(context).appstoreSubscription;
final String id = Platform.isAndroid
? S.of(context).googlePlayId
: S.of(context).appleId;
final String message = S.of(context).subAlreadyLinkedErrMessage(id);
// ignore: unawaited_futures
showErrorDialog(context, title, message);
return;
} catch (e) {
_logger.warning("Could not complete payment ", e);
await _dialog.hide();
// ignore: unawaited_futures
showErrorDialog(
context,
S.of(context).paymentFailed,
S.of(context).paymentFailedTalkToProvider(
Platform.isAndroid ? "PlayStore" : "AppStore",
),
);
return;
}
} else if (Platform.isIOS && purchase.pendingCompletePurchase) {
await InAppPurchase.instance.completePurchase(purchase);
await _dialog.hide();
} else if (purchase.status == PurchaseStatus.error) {
await _dialog.hide();
}
}
});
}
@override
void dispose() {
_purchaseUpdateSubscription.cancel();
_billingService.setIsOnSubscriptionPage(false);
super.dispose();
}
@override
Widget build(BuildContext context) {
colorScheme = getEnteColorScheme(context);
if (!_isLoading) {
_isLoading = true;
_fetchSubData();
}
_dialog = createProgressDialog(context, S.of(context).pleaseWait);
final appBar = AppBar(
title: widget.isOnboarding
? null
: Text("${S.of(context).subscription}${kDebugMode ? ' Store' : ''}"),
);
return Scaffold(
appBar: appBar,
body: _getBody(),
);
}
bool _isFreePlanUser() {
return _currentSubscription != null &&
freeProductID == _currentSubscription!.productID;
}
Future<void> _fetchSubData() async {
// ignore: unawaited_futures
_userService.getUserDetailsV2(memoryCount: false).then((userDetails) async {
_userDetails = userDetails;
_currentSubscription = userDetails.subscription;
_hasActiveSubscription = _currentSubscription!.isValid();
_hideCurrentPlanSelection =
_currentSubscription?.attributes?.isCancelled ?? false;
showYearlyPlan = _currentSubscription!.isYearlyPlan();
final billingPlans = await _billingService.getBillingPlans();
_isActiveStripeSubscriber =
_currentSubscription!.paymentProvider == stripe &&
_currentSubscription!.isValid();
_plans = billingPlans.plans.where((plan) {
final productID = _isActiveStripeSubscriber
? plan.stripeID
: Platform.isAndroid
? plan.androidID
: plan.iosID;
return productID.isNotEmpty;
}).toList();
hasYearlyPlans = _plans.any((plan) => plan.period == 'year');
if (showYearlyPlan && hasYearlyPlans) {
_plans = _plans.where((plan) => plan.period == 'year').toList();
} else {
_plans = _plans.where((plan) => plan.period != 'year').toList();
}
_freePlan = billingPlans.freePlan;
_hasLoadedData = true;
if (mounted) {
setState(() {});
}
});
}
Widget _getBody() {
if (_hasLoadedData) {
if (_userDetails.isPartOfFamily() && !_userDetails.isFamilyAdmin()) {
return ChildSubscriptionWidget(userDetails: _userDetails);
} else {
return _buildPlans();
}
}
return const EnteLoadingWidget();
}
Widget _buildPlans() {
final widgets = <Widget>[];
widgets.add(
SubscriptionHeaderWidget(
isOnboarding: widget.isOnboarding,
currentUsage: _userDetails.getFamilyOrPersonalUsage(),
),
);
widgets.addAll([
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: _isActiveStripeSubscriber
? _getStripePlanWidgets()
: _getMobilePlanWidgets(),
),
const Padding(padding: EdgeInsets.all(8)),
]);
if (hasYearlyPlans) {
widgets.add(_showSubscriptionToggle());
}
if (_currentSubscription != null) {
widgets.add(
ValidityWidget(
currentSubscription: _currentSubscription,
bonusData: _userDetails.bonusData,
),
);
}
if (_currentSubscription!.productID == freeProductID) {
if (widget.isOnboarding) {
widgets.add(SkipSubscriptionWidget(freePlan: _freePlan));
}
widgets.add(
SubFaqWidget(isOnboarding: widget.isOnboarding),
);
}
if (_hasActiveSubscription &&
_currentSubscription!.productID != freeProductID) {
if (_isActiveStripeSubscriber) {
widgets.add(
Padding(
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20),
child: Text(
S.of(context).visitWebToManage,
style: getEnteTextTheme(context).small.copyWith(
color: colorScheme.textMuted,
),
),
),
);
} else {
widgets.add(
Padding(
padding: const EdgeInsets.fromLTRB(16, 40, 16, 4),
child: MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: S.of(context).paymentDetails,
),
menuItemColor: colorScheme.fillFaint,
trailingWidget: Icon(
Icons.chevron_right_outlined,
color: colorScheme.strokeBase,
),
singleBorderRadius: 4,
alignCaptionedTextToLeft: true,
onTap: () async {
_onPlatformRestrictedPaymentDetailsClick();
},
),
),
);
}
}
if (!widget.isOnboarding) {
widgets.add(
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 0),
child: MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: _isFreePlanUser()
? S.of(context).familyPlans
: S.of(context).manageFamily,
),
menuItemColor: colorScheme.fillFaint,
trailingWidget: Icon(
Icons.chevron_right_outlined,
color: colorScheme.strokeBase,
),
singleBorderRadius: 4,
alignCaptionedTextToLeft: true,
onTap: () async {
unawaited(
_billingService.launchFamilyPortal(context, _userDetails),
);
},
),
),
);
widgets.add(ViewAddOnButton(_userDetails.bonusData));
widgets.add(const SizedBox(height: 80));
}
return SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: widgets,
),
);
}
void _onPlatformRestrictedPaymentDetailsClick() {
final String paymentProvider = _currentSubscription!.paymentProvider;
if (paymentProvider == appStore && !Platform.isAndroid) {
launchUrlString("https://apps.apple.com/account/billing");
} else if (paymentProvider == playStore && Platform.isAndroid) {
launchUrlString(
"https://play.google.com/store/account/subscriptions?sku=" +
_currentSubscription!.productID +
"&package=io.ente.photos",
);
} else if (paymentProvider == stripe) {
showErrorDialog(
context,
S.of(context).sorry,
S.of(context).visitWebToManage,
);
} else {
final String capitalizedWord = paymentProvider.isNotEmpty
? '${paymentProvider[0].toUpperCase()}${paymentProvider.substring(1).toLowerCase()}'
: '';
showErrorDialog(
context,
S.of(context).sorry,
S.of(context).contactToManageSubscription(capitalizedWord),
);
}
}
Future<void> _filterStorePlansForUi() async {
final billingPlans = await _billingService.getBillingPlans();
_plans = billingPlans.plans.where((plan) {
final productID = _isActiveStripeSubscriber
? plan.stripeID
: Platform.isAndroid
? plan.androidID
: plan.iosID;
return productID.isNotEmpty;
}).toList();
hasYearlyPlans = _plans.any((plan) => plan.period == 'year');
if (showYearlyPlan) {
_plans = _plans.where((plan) => plan.period == 'year').toList();
} else {
_plans = _plans.where((plan) => plan.period != 'year').toList();
}
setState(() {});
}
Widget _showSubscriptionToggle() {
return Container(
padding: const EdgeInsets.only(left: 8, right: 8, top: 2, bottom: 2),
margin: const EdgeInsets.only(bottom: 6),
child: Column(
children: [
RepaintBoundary(
child: SizedBox(
width: 250,
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: SegmentedButton(
style: SegmentedButton.styleFrom(
selectedBackgroundColor:
getEnteColorScheme(context).fillMuted,
selectedForegroundColor:
getEnteColorScheme(context).textBase,
side: BorderSide(
color: getEnteColorScheme(context).strokeMuted,
width: 1,
),
),
segments: <ButtonSegment<bool>>[
ButtonSegment(
label: Text(S.of(context).monthly),
value: false,
),
ButtonSegment(
label: Text(S.of(context).yearly),
value: true,
),
],
selected: {showYearlyPlan},
onSelectionChanged: (p0) {
showYearlyPlan = p0.first;
_filterStorePlansForUi();
},
),
),
],
),
),
),
_isFreePlanUser() && !UpdateService.instance.isPlayStoreFlavor()
? Text(
S.of(context).twoMonthsFreeOnYearlyPlans,
style: getEnteTextTheme(context).miniMuted,
)
: const SizedBox.shrink(),
const Padding(padding: EdgeInsets.all(8)),
],
),
);
}
List<Widget> _getStripePlanWidgets() {
final List<Widget> planWidgets = [];
bool foundActivePlan = false;
for (final plan in _plans) {
final productID = plan.stripeID;
if (productID.isEmpty) {
continue;
}
final isActive = _hasActiveSubscription &&
_currentSubscription!.productID == productID;
if (isActive) {
foundActivePlan = true;
}
planWidgets.add(
Material(
color: Colors.transparent,
child: InkWell(
onTap: () async {
if (isActive) {
return;
}
// ignore: unawaited_futures
showErrorDialog(
context,
S.of(context).sorry,
S.of(context).visitWebToManage,
);
},
child: SubscriptionPlanWidget(
storage: plan.storage,
price: plan.price,
period: plan.period,
isActive: isActive && !_hideCurrentPlanSelection,
),
),
),
);
}
if (!foundActivePlan && _hasActiveSubscription) {
_addCurrentPlanWidget(planWidgets);
}
return planWidgets;
}
List<Widget> _getMobilePlanWidgets() {
bool foundActivePlan = false;
final List<Widget> planWidgets = [];
if (_hasActiveSubscription &&
_currentSubscription!.productID == freeProductID) {
foundActivePlan = true;
planWidgets.add(
SubscriptionPlanWidget(
storage: _freePlan.storage,
price: S.of(context).freeTrial,
period: "",
isActive: true,
),
);
}
for (final plan in _plans) {
final productID = Platform.isAndroid ? plan.androidID : plan.iosID;
final isActive = _hasActiveSubscription &&
_currentSubscription!.productID == productID;
if (isActive) {
foundActivePlan = true;
}
planWidgets.add(
Material(
child: InkWell(
onTap: () async {
if (isActive) {
return;
}
final int addOnBonus =
_userDetails.bonusData?.totalAddOnBonus() ?? 0;
if (_userDetails.getFamilyOrPersonalUsage() >
(plan.storage + addOnBonus)) {
_logger.warning(
" familyUsage ${convertBytesToReadableFormat(_userDetails.getFamilyOrPersonalUsage())}"
" plan storage ${convertBytesToReadableFormat(plan.storage)} "
"addOnBonus ${convertBytesToReadableFormat(addOnBonus)},"
"overshooting by ${convertBytesToReadableFormat(_userDetails.getFamilyOrPersonalUsage() - (plan.storage + addOnBonus))}",
);
// ignore: unawaited_futures
showErrorDialog(
context,
S.of(context).sorry,
S.of(context).youCannotDowngradeToThisPlan,
);
return;
}
await _dialog.show();
final ProductDetailsResponse response =
await InAppPurchase.instance.queryProductDetails({productID});
if (response.notFoundIDs.isNotEmpty) {
final errMsg = "Could not find products: " +
response.notFoundIDs.toString();
_logger.severe(errMsg);
await _dialog.hide();
await showGenericErrorDialog(
context: context,
error: Exception(errMsg),
);
return;
}
final isCrossGradingOnAndroid = Platform.isAndroid &&
_hasActiveSubscription &&
_currentSubscription!.productID != freeProductID &&
_currentSubscription!.productID != plan.androidID;
if (isCrossGradingOnAndroid) {
await _dialog.hide();
// ignore: unawaited_futures
showErrorDialog(
context,
S.of(context).couldNotUpdateSubscription,
S.of(context).pleaseContactSupportAndWeWillBeHappyToHelp,
);
return;
} else {
await InAppPurchase.instance.buyNonConsumable(
purchaseParam: PurchaseParam(
productDetails: response.productDetails[0],
),
);
}
},
child: SubscriptionPlanWidget(
storage: plan.storage,
price: plan.price,
period: plan.period,
isActive: isActive,
),
),
),
);
}
if (!foundActivePlan && _hasActiveSubscription) {
_addCurrentPlanWidget(planWidgets);
}
return planWidgets;
}
void _addCurrentPlanWidget(List<Widget> planWidgets) {
int activePlanIndex = 0;
for (; activePlanIndex < _plans.length; activePlanIndex++) {
if (_plans[activePlanIndex].storage > _currentSubscription!.storage) {
break;
}
}
planWidgets.insert(
activePlanIndex,
Material(
child: InkWell(
onTap: () {},
child: SubscriptionPlanWidget(
storage: _currentSubscription!.storage,
price: _currentSubscription!.price,
period: _currentSubscription!.period,
isActive: true,
),
),
),
);
}
}

View file

@ -1,25 +1,6 @@
import 'package:flutter/cupertino.dart';
import 'package:photos/core/configuration.dart';
import "package:photos/service_locator.dart";
import 'package:photos/services/update_service.dart';
import "package:photos/ui/payment/store_subscription_page.dart";
import 'package:photos/ui/payment/stripe_subscription_page.dart';
StatefulWidget getSubscriptionPage({bool isOnBoarding = false}) {
if (UpdateService.instance.isIndependentFlavor()) {
return StripeSubscriptionPage(isOnboarding: isOnBoarding);
}
if (flagService.enableStripe && _isUserCreatedPostStripeSupport()) {
return StripeSubscriptionPage(isOnboarding: isOnBoarding);
} else {
return StoreSubscriptionPage(isOnboarding: isOnBoarding);
}
}
// return true if the user was created after we added support for stripe payment
// on frame. We do this check to avoid showing Stripe payment option for earlier
// users who might have paid via playStore. This method should be removed once
// we have better handling for active play/app store subscription & stripe plans.
bool _isUserCreatedPostStripeSupport() {
return Configuration.instance.getUserID()! > 1580559962386460;
return StripeSubscriptionPage(isOnboarding: isOnBoarding);
}

View file

@ -0,0 +1,277 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
characters:
dependency: transitive
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
collection:
dependency: "direct main"
description:
name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.dev"
source: hosted
version: "1.18.0"
dio:
dependency: "direct main"
description:
name: dio
sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8"
url: "https://pub.dev"
source: hosted
version: "4.0.6"
ffi:
dependency: transitive
description:
name: ffi
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
file:
dependency: transitive
description:
name: file
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
url: "https://pub.dev"
source: hosted
version: "4.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
url: "https://pub.dev"
source: hosted
version: "3.0.0"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
url: "https://pub.dev"
source: hosted
version: "0.8.0"
meta:
dependency: transitive
description:
name: meta
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
url: "https://pub.dev"
source: hosted
version: "1.11.0"
path:
dependency: transitive
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.dev"
source: hosted
version: "1.9.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
platform:
dependency: transitive
description:
name: platform
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
url: "https://pub.dev"
source: hosted
version: "3.1.4"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
url: "https://pub.dev"
source: hosted
version: "2.2.3"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2"
url: "https://pub.dev"
source: hosted
version: "2.2.2"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_span:
dependency: transitive
description:
name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.dev"
source: hosted
version: "1.10.0"
stack_trace:
dependency: "direct main"
description:
name: stack_trace
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
url: "https://pub.dev"
source: hosted
version: "1.11.1"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
url: "https://pub.dev"
source: hosted
version: "1.2.1"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
url: "https://pub.dev"
source: hosted
version: "1.3.2"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
web:
dependency: transitive
description:
name: web
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
url: "https://pub.dev"
source: hosted
version: "0.5.1"
win32:
dependency: transitive
description:
name: win32
sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb"
url: "https://pub.dev"
source: hosted
version: "5.5.0"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
url: "https://pub.dev"
source: hosted
version: "1.0.4"
sdks:
dart: ">=3.3.0 <4.0.0"
flutter: ">=3.19.0"

View file

@ -0,0 +1,284 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
characters:
dependency: transitive
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
collection:
dependency: transitive
description:
name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.dev"
source: hosted
version: "1.18.0"
dio:
dependency: transitive
description:
name: dio
sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8"
url: "https://pub.dev"
source: hosted
version: "4.0.6"
ente_cast:
dependency: "direct main"
description:
path: "../ente_cast"
relative: true
source: path
version: "0.0.1"
ffi:
dependency: transitive
description:
name: ffi
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
file:
dependency: transitive
description:
name: file
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
url: "https://pub.dev"
source: hosted
version: "4.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
url: "https://pub.dev"
source: hosted
version: "3.0.0"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
url: "https://pub.dev"
source: hosted
version: "0.8.0"
meta:
dependency: transitive
description:
name: meta
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
url: "https://pub.dev"
source: hosted
version: "1.11.0"
path:
dependency: transitive
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.dev"
source: hosted
version: "1.9.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
platform:
dependency: transitive
description:
name: platform
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
url: "https://pub.dev"
source: hosted
version: "3.1.4"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
shared_preferences:
dependency: transitive
description:
name: shared_preferences
sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
url: "https://pub.dev"
source: hosted
version: "2.2.3"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2"
url: "https://pub.dev"
source: hosted
version: "2.2.2"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_span:
dependency: transitive
description:
name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.dev"
source: hosted
version: "1.10.0"
stack_trace:
dependency: "direct main"
description:
name: stack_trace
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
url: "https://pub.dev"
source: hosted
version: "1.11.1"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
url: "https://pub.dev"
source: hosted
version: "1.2.1"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
url: "https://pub.dev"
source: hosted
version: "1.3.2"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
web:
dependency: transitive
description:
name: web
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
url: "https://pub.dev"
source: hosted
version: "0.5.1"
win32:
dependency: transitive
description:
name: win32
sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb"
url: "https://pub.dev"
source: hosted
version: "5.5.0"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
url: "https://pub.dev"
source: hosted
version: "1.0.4"
sdks:
dart: ">=3.3.0 <4.0.0"
flutter: ">=3.19.0"

View file

@ -9,14 +9,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "61.0.0"
_flutterfire_internals:
dependency: transitive
description:
name: _flutterfire_internals
sha256: "0cb43f83f36ba8cb20502dee0c205e3f3aafb751732d724aeac3f2e044212cc2"
url: "https://pub.dev"
source: hosted
version: "1.3.29"
adaptive_theme:
dependency: "direct main"
description:
@ -577,54 +569,6 @@ packages:
url: "https://github.com/jesims/file_saver.git"
source: git
version: "0.2.9"
firebase_core:
dependency: "direct main"
description:
name: firebase_core
sha256: "6b1152a5af3b1cfe7e45309e96fc1aa14873f410f7aadb3878aa7812acfa7531"
url: "https://pub.dev"
source: hosted
version: "2.30.0"
firebase_core_platform_interface:
dependency: transitive
description:
name: firebase_core_platform_interface
sha256: c437ae5d17e6b5cc7981cf6fd458a5db4d12979905f9aafd1fea930428a9fe63
url: "https://pub.dev"
source: hosted
version: "5.0.0"
firebase_core_web:
dependency: transitive
description:
name: firebase_core_web
sha256: c8b02226e548f35aace298e2bb2e6c24e34e8a203d614e742bb1146e5a4ad3c8
url: "https://pub.dev"
source: hosted
version: "2.15.0"
firebase_messaging:
dependency: "direct main"
description:
name: firebase_messaging
sha256: "87e3eda0ecdfeadb5fd1cf0dc5153aea5307a0cfca751c4b1ac97bfdd805660e"
url: "https://pub.dev"
source: hosted
version: "14.8.1"
firebase_messaging_platform_interface:
dependency: transitive
description:
name: firebase_messaging_platform_interface
sha256: "80b4ccf20066b0579ebc88d4678230a5f53ab282fe040e31671af745db1588f9"
url: "https://pub.dev"
source: hosted
version: "4.5.31"
firebase_messaging_web:
dependency: transitive
description:
name: firebase_messaging_web
sha256: "9224aa4db1ce6f08d96a82978453d37e9980204a20e410a11d9b774b24c6841c"
url: "https://pub.dev"
source: hosted
version: "3.8.1"
fixnum:
dependency: transitive
description:
@ -1063,38 +1007,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.1"
in_app_purchase:
dependency: "direct main"
description:
name: in_app_purchase
sha256: def70fbaa2a274f4d835677459f6f7afc5469de912438f86076e51cbd4cbd5b4
url: "https://pub.dev"
source: hosted
version: "3.1.13"
in_app_purchase_android:
dependency: transitive
description:
name: in_app_purchase_android
sha256: b9d4ecf70c51ab46222502c050b1535f6249caf9d768c4abd856ea16a18a882d
url: "https://pub.dev"
source: hosted
version: "0.3.3+1"
in_app_purchase_platform_interface:
dependency: transitive
description:
name: in_app_purchase_platform_interface
sha256: "412efce2b9238c5ace4f057acad43f793ed06880e366d26ae322e796cadb051a"
url: "https://pub.dev"
source: hosted
version: "1.3.7"
in_app_purchase_storekit:
dependency: transitive
description:
name: in_app_purchase_storekit
sha256: e0f860e760488dbd666e0f27e239d128cba744607fc62434dc76c19d1c292439
url: "https://pub.dev"
source: hosted
version: "0.3.13+1"
integration_test:
dependency: "direct dev"
description: flutter

View file

@ -66,8 +66,6 @@ dependencies:
file_saver:
# Use forked version till this PR is merged: https://github.com/incrediblezayed/file_saver/pull/87
git: https://github.com/jesims/file_saver.git
firebase_core: ^2.30.0
firebase_messaging: ^14.8.0
fk_user_agent: ^2.0.1
flutter:
sdk: flutter
@ -98,7 +96,6 @@ dependencies:
http: ^1.1.0
image: ^4.0.17
image_editor: ^1.3.0
in_app_purchase: ^3.0.7
intl: ^0.19.0
json_annotation: ^4.8.0
latlong2: ^0.9.0
@ -203,7 +200,7 @@ flutter_icons:
android: "launcher_icon"
adaptive_icon_foreground: "assets/launcher_icon/ente-icon-foreground.png"
adaptive_icon_background: "#ffffff"
ios: true
ios: false # F-Droid
image_path: "assets/icon-light.png"
flutter_native_splash:

1
mobile/thirdparty/flutter vendored Submodule

@ -0,0 +1 @@
Subproject commit a14f74ff3a1cbd521163c5f03d68113d50af93d3

View file

@ -0,0 +1,19 @@
Copyright (c) 2017 Transistor Software <info@transistorsoft.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -0,0 +1,22 @@
Transistor Background Fetch
===========================================================================
Copyright (c) 2017 Transistor Software <info@transistorsoft.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -0,0 +1,28 @@
#
# Be sure to run `pod lib lint TSBackgroundFetch.podspec' to ensure this is a
# valid spec before submitting.
#
# Any lines starting with a # are optional, but their use is encouraged
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
#
Pod::Spec.new do |s|
s.name = 'TSBackgroundFetch'
s.version = '0.0.1'
s.summary = 'iOS Background Fetch API Manager'
s.description = <<-DESC
iOS Background Fetch API Manager with ability to handle multiple listeners.
DESC
s.homepage = 'http://www.transistorsoft.com'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'christocracy' => 'christocracy@gmail.com' }
s.source = { :git => 'https://github.com/transistorsoft/transistor-background-fetch.git', :tag => s.version.to_s }
s.social_media_url = 'https://twitter.com/christocracy'
s.ios.deployment_target = '8.0'
s.source_files = 'ios/TSBackgroundFetch/TSBackgroundFetch/*.{h,m}'
s.vendored_frameworks = 'ios/TSBackgroundFetch/TSBackgroundFetch.framework'
end

View file

@ -0,0 +1,9 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.externalNativeBuild

View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,28 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 30
defaultConfig {
applicationId "com.transistorsoft.backgroundfetch"
minSdkVersion 16
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
implementation fileTree(dir: 'libs', include: ['*.jar'])
}

View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1,27 @@
package com.transistorsoft.backgroundfetch;
import android.content.Context;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals("com.transistorsoft.backgroundfetch", appContext.getPackageName());
}
}

View file

@ -0,0 +1,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.transistorsoft.backgroundfetch">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme" />
</manifest>

View file

@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

View file

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
</resources>

View file

@ -0,0 +1,3 @@
<resources>
<string name="app_name">BackgroundFetch</string>
</resources>

View file

@ -0,0 +1,11 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>

View file

@ -0,0 +1,17 @@
package com.transistorsoft.backgroundfetch;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}

View file

@ -0,0 +1,34 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.3'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
ext {
compileSdkVersion = 32
targetSdkVersion = 31
buildToolsVersion = "29.0.6"
appCompatVersion = "1.4.1"
}

View file

@ -0,0 +1,23 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
VERSION_NAME=0.5.6
VERSION_CODE=21
android.useAndroidX=true
android.enableJetifier=true

View file

@ -0,0 +1,6 @@
#Thu Jul 15 09:21:17 EDT 2021
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip

View file

@ -0,0 +1,160 @@
#!/usr/bin/env bash
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

View file

@ -0,0 +1,90 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View file

@ -0,0 +1 @@
include ':app', ':tsbackgroundfetch'

View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,152 @@
apply plugin: 'com.android.library'
apply plugin: 'maven'
apply plugin: 'maven-publish'
android {
compileSdkVersion rootProject.compileSdkVersion
defaultConfig {
minSdkVersion 16
targetSdkVersion rootProject.targetSdkVersion
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
publishing {
publications {
tslocationmanager(MavenPublication) {
groupId 'com.transistorsoft'
artifactId 'tsbackgroundfetch'
version VERSION_NAME
artifact("$buildDir/outputs/aar/tsbackgroundfetch-release.aar")
}
}
repositories {
mavenLocal()
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "androidx.lifecycle:lifecycle-runtime:2.5.1"
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
//implementation "androidx.appcompat:appcompat:$rootProject.appCompatVersion"
}
// Build Release
task buildRelease { task ->
task.dependsOn 'flutterRelease'
}
// Publish Release.
task publishRelease { task ->
task.dependsOn 'assembleRelease'
}
tasks["publishRelease"].mustRunAfter("assembleRelease")
tasks["publishRelease"].finalizedBy("publish")
def WORKSPACE_PATH = "/Users/chris/workspace"
// Build local maven repo.
def LIBRARY_PATH = "com/transistorsoft/tsbackgroundfetch"
task buildLocalRepository { task ->
task.dependsOn 'publishRelease'
doLast {
delete "$buildDir/repo-local"
copy {
from "$buildDir/repo/$LIBRARY_PATH/$VERSION_NAME"
into "$buildDir/repo-local/$LIBRARY_PATH/$VERSION_NAME"
}
copy {
from("$buildDir/repo/$LIBRARY_PATH/maven-metadata.xml")
into("$buildDir/repo-local/$LIBRARY_PATH")
}
}
}
def cordovaDir = "$WORKSPACE_PATH/background-geolocation/cordova/cordova-plugin-background-fetch"
task cordovaRelease { task ->
task.dependsOn 'buildLocalRepository'
doLast {
delete "$cordovaDir/src/android/libs"
copy {
// Maven repo format.
from("$buildDir/repo-local")
into("$cordovaDir/src/android/libs")
// OLD FORMAT
//from("$buildDir/outputs/aar/tsbackgroundfetch-release.aar")
//into("$cordovaDir/src/android/libs/tsbackgroundfetch")
//rename(/(.*)-release/, '$1-' + VERSION_NAME)
}
}
}
def reactNativeDir = "$WORKSPACE_PATH/background-geolocation/react/react-native-background-fetch"
task reactNativeRelease { task ->
task.dependsOn 'buildLocalRepository'
doLast {
delete "$reactNativeDir/android/libs"
copy {
// Maven repo format.
from("$buildDir/repo-local")
into("$reactNativeDir/android/libs")
// OLD format.
//from("$buildDir/outputs/aar/tsbackgroundfetch-release.aar")
//into("$reactNativeDir/android/libs")
//rename(/(.*)-release/, '$1-' + VERSION_NAME)
}
}
}
def flutterDir = "$WORKSPACE_PATH/background-geolocation/flutter/flutter_background_fetch"
task flutterRelease { task ->
task.dependsOn 'buildLocalRepository'
doLast {
delete "$flutterDir/android/libs"
copy {
// Maven repo format.
from("$buildDir/repo-local")
into("$flutterDir/android/libs")
// OLD format.
//from("$buildDir/outputs/aar/tsbackgroundfetch-release.aar")
//into("$flutterDir/android/libs")
//rename(/(.*)-release/, '$1-' + VERSION_NAME)
}
}
}
def capacitorDir = "$WORKSPACE_PATH/background-geolocation/capacitor/capacitor-background-fetch"
task capacitorRelease { task ->
task.dependsOn 'buildLocalRepository'
doLast {
delete "$capacitorDir/android/libs"
copy {
// Maven repo format.
from("$buildDir/repo-local")
into("$capacitorDir/android/libs")
}
}
}
task nativeScriptRelease(type: Copy) {
from('./build/outputs/aar/tsbackgroundfetch-release.aar')
into("$WORKSPACE_PATH/NativeScript/background-geolocation/nativescript-background-fetch/src/platforms/android/libs")
rename('tsbackgroundfetch-release.aar', 'tsbackgroundfetch.aar')
}

View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1,26 @@
package com.transistorsoft.tsbackgroundfetch;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals("com.transistorsoft.tsbackgroundfetch.test", appContext.getPackageName());
}
}

View file

@ -0,0 +1,18 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.transistorsoft.tsbackgroundfetch">
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<application>
<receiver android:name="com.transistorsoft.tsbackgroundfetch.FetchAlarmReceiver" />
<service android:name="com.transistorsoft.tsbackgroundfetch.FetchJobService" android:permission="android.permission.BIND_JOB_SERVICE" android:exported="true" />
<receiver android:name="com.transistorsoft.tsbackgroundfetch.BootReceiver" android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
</application>
</manifest>

View file

@ -0,0 +1,291 @@
package com.transistorsoft.tsbackgroundfetch;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.job.JobInfo;
import android.app.job.JobScheduler;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.PersistableBundle;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class BGTask {
static int MAX_TIME = 60000;
private static final List<BGTask> mTasks = new ArrayList<>();
static BGTask getTask(String taskId) {
synchronized (mTasks) {
for (BGTask task : mTasks) {
if (task.hasTaskId(taskId)) return task;
}
}
return null;
}
static void addTask(BGTask task) {
synchronized (mTasks) {
mTasks.add(task);
}
}
static void removeTask(String taskId) {
synchronized (mTasks) {
BGTask found = null;
for (BGTask task : mTasks) {
if (task.hasTaskId(taskId)) {
found = task;
break;
}
}
if (found != null) {
mTasks.remove(found);
}
}
}
static void clear() {
synchronized (mTasks) {
mTasks.clear();
}
}
private FetchJobService.CompletionHandler mCompletionHandler;
private String mTaskId;
private int mJobId;
private Runnable mTimeoutTask;
private boolean mTimedout = false;
BGTask(final Context context, String taskId, FetchJobService.CompletionHandler handler, int jobId) {
mTaskId = taskId;
mCompletionHandler = handler;
mJobId = jobId;
mTimeoutTask = new Runnable() {
@Override public void run() {
onTimeout(context);
}
};
BackgroundFetch.getUiHandler().postDelayed(mTimeoutTask, MAX_TIME);
}
public boolean getTimedOut() {
return mTimedout;
}
public String getTaskId() { return mTaskId; }
int getJobId() { return mJobId; }
boolean hasTaskId(String taskId) {
return ((mTaskId != null) && mTaskId.equalsIgnoreCase(taskId));
}
void setCompletionHandler(FetchJobService.CompletionHandler handler) {
mCompletionHandler = handler;
}
void finish() {
if (mCompletionHandler != null) {
mCompletionHandler.finish();
}
if (mTimeoutTask != null) {
BackgroundFetch.getUiHandler().removeCallbacks(mTimeoutTask);
}
mCompletionHandler = null;
removeTask(mTaskId);
}
static void reschedule(Context context, BackgroundFetchConfig existing, BackgroundFetchConfig config) {
BGTask existingTask = BGTask.getTask(existing.getTaskId());
if (existingTask != null) {
existingTask.finish();
}
cancel(context, existing.getTaskId(), existing.getJobId());
schedule(context, config);
}
static void schedule(Context context, BackgroundFetchConfig config) {
Log.d(BackgroundFetch.TAG, config.toString());
long interval = (config.isFetchTask()) ? (TimeUnit.MINUTES.toMillis(config.getMinimumFetchInterval())) : config.getDelay();
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !config.getForceAlarmManager()) {
// API 21+ uses new JobScheduler API
JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
JobInfo.Builder builder = new JobInfo.Builder(config.getJobId(), new ComponentName(context, FetchJobService.class))
.setRequiredNetworkType(config.getRequiredNetworkType())
.setRequiresDeviceIdle(config.getRequiresDeviceIdle())
.setRequiresCharging(config.getRequiresCharging())
.setPersisted(config.getStartOnBoot() && !config.getStopOnTerminate());
if (config.getPeriodic()) {
if (android.os.Build.VERSION.SDK_INT >= 24) {
builder.setPeriodic(interval, interval);
} else {
builder.setPeriodic(interval);
}
} else {
builder.setMinimumLatency(interval);
}
PersistableBundle extras = new PersistableBundle();
extras.putString(BackgroundFetchConfig.FIELD_TASK_ID, config.getTaskId());
extras.putLong("scheduled_at", System.currentTimeMillis());
builder.setExtras(extras);
if (android.os.Build.VERSION.SDK_INT >= 26) {
builder.setRequiresStorageNotLow(config.getRequiresStorageNotLow());
builder.setRequiresBatteryNotLow(config.getRequiresBatteryNotLow());
}
if (jobScheduler != null) {
jobScheduler.schedule(builder.build());
}
} else {
// Everyone else get AlarmManager
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
if (alarmManager != null) {
PendingIntent pi = getAlarmPI(context, config.getTaskId());
long delay = System.currentTimeMillis() + interval;
if (config.getPeriodic()) {
alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, delay, interval, pi);
} else {
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, delay, pi);
} else if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
alarmManager.setExact(AlarmManager.RTC_WAKEUP, delay, pi);
} else {
alarmManager.set(AlarmManager.RTC_WAKEUP, delay, pi);
}
}
}
}
}
void onTimeout(Context context) {
mTimedout = true;
Log.d(BackgroundFetch.TAG, "[BGTask] timeout: " + mTaskId);
BackgroundFetch adapter = BackgroundFetch.getInstance(context);
if (!LifecycleManager.getInstance().isHeadless()) {
BackgroundFetch.Callback callback = adapter.getFetchCallback();
if (callback != null) {
callback.onTimeout(mTaskId);
}
} else {
BackgroundFetchConfig config = adapter.getConfig(mTaskId);
if (config != null) {
if (config.getJobService() != null) {
fireHeadlessEvent(context, config);
} else {
adapter.finish(mTaskId);
}
} else {
Log.e(BackgroundFetch.TAG, "[BGTask] failed to load config for taskId: " + mTaskId);
adapter.finish(mTaskId);
}
}
}
// Fire a headless background-fetch event by reflecting an instance of Config.jobServiceClass.
// Will attempt to reflect upon two different forms of Headless class:
// 1: new HeadlessTask(context, taskId)
// or
// 2: new HeadlessTask().onFetch(context, taskId);
//
void fireHeadlessEvent(Context context, BackgroundFetchConfig config) throws Error {
try {
// Get class via reflection.
Class<?> HeadlessClass = Class.forName(config.getJobService());
Class[] types = { Context.class, BGTask.class };
Object[] params = { context, this};
try {
// 1: new HeadlessTask(context, taskId);
Constructor<?> constructor = HeadlessClass.getDeclaredConstructor(types);
constructor.newInstance(params);
} catch (NoSuchMethodException e) {
// 2: new HeadlessTask().onFetch(context, taskId);
Constructor<?> constructor = HeadlessClass.getConstructor();
Object instance = constructor.newInstance();
Method onFetch = instance.getClass().getDeclaredMethod("onFetch", types);
onFetch.invoke(instance, params);
}
} catch (ClassNotFoundException e) {
throw new Error(e.getMessage());
} catch (NoSuchMethodException e) {
throw new Error(e.getMessage());
} catch (IllegalAccessException e) {
throw new Error(e.getMessage());
} catch (InstantiationException e) {
throw new Error(e.getMessage());
} catch (InvocationTargetException e) {
throw new Error(e.getMessage());
}
}
static void cancel(Context context, String taskId, int jobId) {
Log.i(BackgroundFetch.TAG, "- cancel taskId=" + taskId + ", jobId=" + jobId);
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && (jobId != 0)) {
JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
if (jobScheduler != null) {
jobScheduler.cancel(jobId);
}
} else {
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
if (alarmManager != null) {
alarmManager.cancel(BGTask.getAlarmPI(context, taskId));
}
}
}
static PendingIntent getAlarmPI(Context context, String taskId) {
Intent intent = new Intent(context, FetchAlarmReceiver.class);
intent.setAction(taskId);
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT|PendingIntent.FLAG_IMMUTABLE);
}
public String toString() {
return "[BGTask taskId=" + mTaskId + "]";
}
public Map<String, Object> toMap() {
Map<String, Object> map = new HashMap<>();
map.put("taskId", mTaskId);
map.put("timeout", mTimedout);
return map;
}
public JSONObject toJson() {
JSONObject json = new JSONObject();
try {
json.put("taskId", mTaskId);
json.put("timeout", mTimedout);
} catch (JSONException e) {
e.printStackTrace();
}
return json;
}
static class Error extends RuntimeException {
public Error(String msg) {
super(msg);
}
}
}

View file

@ -0,0 +1,300 @@
package com.transistorsoft.tsbackgroundfetch;
import android.annotation.TargetApi;
import android.app.ActivityManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Created by chris on 2018-01-11.
*/
public class BackgroundFetch {
public static final String TAG = "TSBackgroundFetch";
public static final String ACTION_CONFIGURE = "configure";
public static final String ACTION_START = "start";
public static final String ACTION_STOP = "stop";
public static final String ACTION_FINISH = "finish";
public static final String ACTION_STATUS = "status";
public static final String ACTION_FORCE_RELOAD = TAG + "-forceReload";
public static final String EVENT_FETCH = ".event.BACKGROUND_FETCH";
public static final int STATUS_AVAILABLE = 2;
private static BackgroundFetch mInstance = null;
private static ExecutorService sThreadPool;
private static Handler uiHandler;
@SuppressWarnings({"WeakerAccess"})
public static Handler getUiHandler() {
if (uiHandler == null) {
uiHandler = new Handler(Looper.getMainLooper());
}
return uiHandler;
}
@SuppressWarnings({"WeakerAccess"})
public static ExecutorService getThreadPool() {
if (sThreadPool == null) {
sThreadPool = Executors.newCachedThreadPool();
}
return sThreadPool;
}
@SuppressWarnings({"WeakerAccess"})
public static BackgroundFetch getInstance(Context context) {
if (mInstance == null) {
mInstance = getInstanceSynchronized(context.getApplicationContext());
}
return mInstance;
}
private static synchronized BackgroundFetch getInstanceSynchronized(Context context) {
if (mInstance == null) mInstance = new BackgroundFetch(context.getApplicationContext());
return mInstance;
}
private Context mContext;
private BackgroundFetch.Callback mFetchCallback;
private final Map<String, BackgroundFetchConfig> mConfig = new HashMap<>();
private BackgroundFetch(Context context) {
mContext = context;
// Start Lifecycle Observer to be notified when app enters background.
getUiHandler().post(LifecycleManager.getInstance());
}
@SuppressWarnings({"unused"})
public void configure(BackgroundFetchConfig config, BackgroundFetch.Callback callback) {
Log.d(TAG, "- " + ACTION_CONFIGURE);
mFetchCallback = callback;
synchronized (mConfig) {
if (mConfig.containsKey(config.getTaskId())) {
// Developer called `.configure` again. Re-configure the plugin by re-scheduling the fetch task.
BackgroundFetchConfig existing = mConfig.get(config.getTaskId());
Log.d(TAG, "Re-configured existing task");
BGTask.reschedule(mContext, existing, config);
mConfig.put(config.getTaskId(), config);
return;
} else {
mConfig.put(config.getTaskId(), config);
}
}
start(config.getTaskId());
}
void onBoot() {
BackgroundFetchConfig.load(mContext, new BackgroundFetchConfig.OnLoadCallback() {
@Override public void onLoad(List<BackgroundFetchConfig> result) {
for (BackgroundFetchConfig config : result) {
if (!config.getStartOnBoot() || config.getStopOnTerminate()) {
config.destroy(mContext);
continue;
}
synchronized (mConfig) {
mConfig.put(config.getTaskId(), config);
}
if ((android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) || config.getForceAlarmManager()) {
if (config.isFetchTask()) {
start(config.getTaskId());
} else {
scheduleTask(config);
}
}
}
}
});
}
@SuppressWarnings({"WeakerAccess"})
@TargetApi(21)
public void start(String fetchTaskId) {
Log.d(TAG, "- " + ACTION_START);
BGTask task = BGTask.getTask(fetchTaskId);
if (task != null) {
Log.e(TAG, "[" + TAG + " start] Task " + fetchTaskId + " already registered");
return;
}
registerTask(fetchTaskId);
}
@SuppressWarnings({"WeakerAccess"})
public void stop(String taskId) {
String msg = "- " + ACTION_STOP;
if (taskId != null) {
msg += ": " + taskId;
}
Log.d(TAG, msg);
if (taskId == null) {
synchronized (mConfig) {
for (BackgroundFetchConfig config : mConfig.values()) {
BGTask task = BGTask.getTask(config.getTaskId());
if (task != null) {
task.finish();
BGTask.removeTask(config.getTaskId());
}
BGTask.cancel(mContext, config.getTaskId(), config.getJobId());
config.destroy(mContext);
}
BGTask.clear();
}
} else {
BGTask task = BGTask.getTask(taskId);
if (task != null) {
task.finish();
BGTask.removeTask(task.getTaskId());
}
BackgroundFetchConfig config = getConfig(taskId);
if (config != null) {
config.destroy(mContext);
BGTask.cancel(mContext, config.getTaskId(), config.getJobId());
}
}
}
@SuppressWarnings({"WeakerAccess"})
public void scheduleTask(BackgroundFetchConfig config) {
synchronized (mConfig) {
if (mConfig.containsKey(config.getTaskId())) {
// This BackgroundFetchConfig already exists? Should we halt any existing Job/Alarm here?
}
config.save(mContext);
mConfig.put(config.getTaskId(), config);
}
String taskId = config.getTaskId();
registerTask(taskId);
}
@SuppressWarnings({"WeakerAccess"})
public void finish(String taskId) {
Log.d(TAG, "- " + ACTION_FINISH + ": " + taskId);
BGTask task = BGTask.getTask(taskId);
if (task != null) {
task.finish();
}
BackgroundFetchConfig config = getConfig(taskId);
if ((config != null) && !config.getPeriodic()) {
config.destroy(mContext);
synchronized (mConfig) {
mConfig.remove(taskId);
}
}
}
public int status() {
return STATUS_AVAILABLE;
}
BackgroundFetch.Callback getFetchCallback() {
return mFetchCallback;
}
void onFetch(final BGTask task) {
BGTask.addTask(task);
Log.d(TAG, "- Background Fetch event received: " + task.getTaskId());
synchronized (mConfig) {
if (mConfig.isEmpty()) {
BackgroundFetchConfig.load(mContext, new BackgroundFetchConfig.OnLoadCallback() {
@Override
public void onLoad(List<BackgroundFetchConfig> result) {
synchronized (mConfig) {
for (BackgroundFetchConfig config : result) {
mConfig.put(config.getTaskId(), config);
}
}
doFetch(task);
}
});
return;
}
}
doFetch(task);
}
private void registerTask(String taskId) {
BackgroundFetchConfig config = getConfig(taskId);
if (config == null) {
Log.e(TAG, "- registerTask failed to find BackgroundFetchConfig for taskId " + taskId);
return;
}
config.save(mContext);
String msg = "- registerTask: " + taskId;
if (!config.getForceAlarmManager()) {
msg += " (jobId: " + config.getJobId() + ")";
}
Log.d(TAG, msg);
BGTask.schedule(mContext, config);
}
private void doFetch(BGTask task) {
BackgroundFetchConfig config = getConfig(task.getTaskId());
if (config == null) {
BGTask.cancel(mContext, task.getTaskId(), task.getJobId());
return;
}
if (!LifecycleManager.getInstance().isHeadless()) {
if (mFetchCallback != null) {
mFetchCallback.onFetch(task.getTaskId());
}
} else if (config.getStopOnTerminate()) {
Log.d(TAG, "- Stopping on terminate");
stop(task.getTaskId());
} else if (config.getJobService() != null) {
try {
task.fireHeadlessEvent(mContext, config);
} catch (BGTask.Error e) {
Log.e(TAG, "Headless task error: " + e.getMessage());
e.printStackTrace();
}
} else {
// {stopOnTerminate: false, forceReload: false} with no Headless JobService?? Don't know what else to do here but stop
Log.w(TAG, "- BackgroundFetch event has occurred while app is terminated but there's no jobService configured to handle the event. BackgroundFetch will terminate.");
finish(task.getTaskId());
stop(task.getTaskId());
}
}
BackgroundFetchConfig getConfig(String taskId) {
synchronized (mConfig) {
return (mConfig.containsKey(taskId)) ? mConfig.get(taskId) : null;
}
}
/**
* @interface BackgroundFetch.Callback
*/
public interface Callback {
void onFetch(String taskId);
void onTimeout(String taskId);
}
}

View file

@ -0,0 +1,362 @@
package com.transistorsoft.tsbackgroundfetch;
import android.app.job.JobInfo;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Created by chris on 2018-01-11.
*/
public class BackgroundFetchConfig {
private Builder config;
private static final int MINIMUM_FETCH_INTERVAL = 1;
private static final int DEFAULT_FETCH_INTERVAL = 15;
public static final String FIELD_TASK_ID = "taskId";
public static final String FIELD_MINIMUM_FETCH_INTERVAL = "minimumFetchInterval";
public static final String FIELD_START_ON_BOOT = "startOnBoot";
public static final String FIELD_REQUIRED_NETWORK_TYPE = "requiredNetworkType";
public static final String FIELD_REQUIRES_BATTERY_NOT_LOW = "requiresBatteryNotLow";
public static final String FIELD_REQUIRES_CHARGING = "requiresCharging";
public static final String FIELD_REQUIRES_DEVICE_IDLE = "requiresDeviceIdle";
public static final String FIELD_REQUIRES_STORAGE_NOT_LOW = "requiresStorageNotLow";
public static final String FIELD_STOP_ON_TERMINATE = "stopOnTerminate";
public static final String FIELD_JOB_SERVICE = "jobService";
public static final String FIELD_FORCE_ALARM_MANAGER = "forceAlarmManager";
public static final String FIELD_PERIODIC = "periodic";
public static final String FIELD_DELAY = "delay";
public static final String FIELD_IS_FETCH_TASK = "isFetchTask";
public static class Builder {
private String taskId;
private int minimumFetchInterval = DEFAULT_FETCH_INTERVAL;
private long delay = -1;
private boolean periodic = false;
private boolean forceAlarmManager = false;
private boolean stopOnTerminate = true;
private boolean startOnBoot = false;
private int requiredNetworkType = 0;
private boolean requiresBatteryNotLow = false;
private boolean requiresCharging = false;
private boolean requiresDeviceIdle = false;
private boolean requiresStorageNotLow = false;
private boolean isFetchTask = false;
private String jobService = null;
public Builder setTaskId(String taskId) {
this.taskId = taskId;
return this;
}
public Builder setIsFetchTask(boolean value) {
this.isFetchTask = value;
return this;
}
public Builder setMinimumFetchInterval(int fetchInterval) {
if (fetchInterval >= MINIMUM_FETCH_INTERVAL) {
this.minimumFetchInterval = fetchInterval;
}
return this;
}
public Builder setStopOnTerminate(boolean stopOnTerminate) {
this.stopOnTerminate = stopOnTerminate;
return this;
}
public Builder setStartOnBoot(boolean startOnBoot) {
this.startOnBoot = startOnBoot;
return this;
}
public Builder setRequiredNetworkType(int networkType) {
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
if (
(networkType != JobInfo.NETWORK_TYPE_ANY) &&
(networkType != JobInfo.NETWORK_TYPE_CELLULAR) &&
(networkType != JobInfo.NETWORK_TYPE_NONE) &&
(networkType != JobInfo.NETWORK_TYPE_NOT_ROAMING) &&
(networkType != JobInfo.NETWORK_TYPE_UNMETERED)
) {
Log.e(BackgroundFetch.TAG, "[ERROR] Invalid " + FIELD_REQUIRED_NETWORK_TYPE + ": " + networkType + "; Defaulting to NETWORK_TYPE_NONE");
networkType = JobInfo.NETWORK_TYPE_NONE;
}
this.requiredNetworkType = networkType;
}
return this;
}
public Builder setRequiresBatteryNotLow(boolean value) {
this.requiresBatteryNotLow = value;
return this;
}
public Builder setRequiresCharging(boolean value) {
this.requiresCharging = value;
return this;
}
public Builder setRequiresDeviceIdle(boolean value) {
this.requiresDeviceIdle = value;
return this;
}
public Builder setRequiresStorageNotLow(boolean value) {
this.requiresStorageNotLow = value;
return this;
}
public Builder setJobService(String className) {
this.jobService = className;
return this;
}
public Builder setForceAlarmManager(boolean value) {
this.forceAlarmManager = value;
return this;
}
public Builder setPeriodic(boolean value) {
this.periodic = value;
return this;
}
public Builder setDelay(long value) {
this.delay = value;
return this;
}
public BackgroundFetchConfig build() {
return new BackgroundFetchConfig(this);
}
public BackgroundFetchConfig load(Context context, String taskId) {
SharedPreferences preferences = context.getSharedPreferences(BackgroundFetch.TAG + ":" + taskId, 0);
if (preferences.contains(FIELD_TASK_ID)) {
setTaskId(preferences.getString(FIELD_TASK_ID, taskId));
}
if (preferences.contains(FIELD_IS_FETCH_TASK)) {
setIsFetchTask(preferences.getBoolean(FIELD_IS_FETCH_TASK, isFetchTask));
}
if (preferences.contains(FIELD_MINIMUM_FETCH_INTERVAL)) {
setMinimumFetchInterval(preferences.getInt(FIELD_MINIMUM_FETCH_INTERVAL, minimumFetchInterval));
}
if (preferences.contains(FIELD_STOP_ON_TERMINATE)) {
setStopOnTerminate(preferences.getBoolean(FIELD_STOP_ON_TERMINATE, stopOnTerminate));
}
if (preferences.contains(FIELD_REQUIRED_NETWORK_TYPE)) {
setRequiredNetworkType(preferences.getInt(FIELD_REQUIRED_NETWORK_TYPE, requiredNetworkType));
}
if (preferences.contains(FIELD_REQUIRES_BATTERY_NOT_LOW)) {
setRequiresBatteryNotLow(preferences.getBoolean(FIELD_REQUIRES_BATTERY_NOT_LOW, requiresBatteryNotLow));
}
if (preferences.contains(FIELD_REQUIRES_CHARGING)) {
setRequiresCharging(preferences.getBoolean(FIELD_REQUIRES_CHARGING, requiresCharging));
}
if (preferences.contains(FIELD_REQUIRES_DEVICE_IDLE)) {
setRequiresDeviceIdle(preferences.getBoolean(FIELD_REQUIRES_DEVICE_IDLE, requiresDeviceIdle));
}
if (preferences.contains(FIELD_REQUIRES_STORAGE_NOT_LOW)) {
setRequiresStorageNotLow(preferences.getBoolean(FIELD_REQUIRES_STORAGE_NOT_LOW, requiresStorageNotLow));
}
if (preferences.contains(FIELD_START_ON_BOOT)) {
setStartOnBoot(preferences.getBoolean(FIELD_START_ON_BOOT, startOnBoot));
}
if (preferences.contains(FIELD_JOB_SERVICE)) {
setJobService(preferences.getString(FIELD_JOB_SERVICE, null));
}
if (preferences.contains(FIELD_FORCE_ALARM_MANAGER)) {
setForceAlarmManager(preferences.getBoolean(FIELD_FORCE_ALARM_MANAGER, forceAlarmManager));
}
if (preferences.contains(FIELD_PERIODIC)) {
setPeriodic(preferences.getBoolean(FIELD_PERIODIC, periodic));
}
if (preferences.contains(FIELD_DELAY)) {
setDelay(preferences.getLong(FIELD_DELAY, delay));
}
return new BackgroundFetchConfig(this);
}
}
private BackgroundFetchConfig(Builder builder) {
config = builder;
// Validate config
if (config.jobService == null) {
if (!config.stopOnTerminate) {
Log.w(BackgroundFetch.TAG, "- Configuration error: In order to use stopOnTerminate: false, you must set enableHeadless: true");
config.setStopOnTerminate(true);
}
if (config.startOnBoot) {
Log.w(BackgroundFetch.TAG, "- Configuration error: In order to use startOnBoot: true, you must enableHeadless: true");
config.setStartOnBoot(false);
}
}
}
void save(Context context) {
SharedPreferences preferences = context.getSharedPreferences(BackgroundFetch.TAG, 0);
Set<String> taskIds = preferences.getStringSet("tasks", new HashSet<String>());
if (taskIds == null) {
taskIds = new HashSet<>();
}
if (!taskIds.contains(config.taskId)) {
Set<String> newIds = new HashSet<>(taskIds);
newIds.add(config.taskId);
SharedPreferences.Editor editor = preferences.edit();
editor.putStringSet("tasks", newIds);
editor.apply();
}
SharedPreferences.Editor editor = context.getSharedPreferences(BackgroundFetch.TAG + ":" + config.taskId, 0).edit();
editor.putString(FIELD_TASK_ID, config.taskId);
editor.putBoolean(FIELD_IS_FETCH_TASK, config.isFetchTask);
editor.putInt(FIELD_MINIMUM_FETCH_INTERVAL, config.minimumFetchInterval);
editor.putBoolean(FIELD_STOP_ON_TERMINATE, config.stopOnTerminate);
editor.putBoolean(FIELD_START_ON_BOOT, config.startOnBoot);
editor.putInt(FIELD_REQUIRED_NETWORK_TYPE, config.requiredNetworkType);
editor.putBoolean(FIELD_REQUIRES_BATTERY_NOT_LOW, config.requiresBatteryNotLow);
editor.putBoolean(FIELD_REQUIRES_CHARGING, config.requiresCharging);
editor.putBoolean(FIELD_REQUIRES_DEVICE_IDLE, config.requiresDeviceIdle);
editor.putBoolean(FIELD_REQUIRES_STORAGE_NOT_LOW, config.requiresStorageNotLow);
editor.putString(FIELD_JOB_SERVICE, config.jobService);
editor.putBoolean(FIELD_FORCE_ALARM_MANAGER, config.forceAlarmManager);
editor.putBoolean(FIELD_PERIODIC, config.periodic);
editor.putLong(FIELD_DELAY, config.delay);
editor.apply();
}
void destroy(Context context) {
SharedPreferences preferences = context.getSharedPreferences(BackgroundFetch.TAG, 0);
Set<String> taskIds = preferences.getStringSet("tasks", new HashSet<String>());
if (taskIds == null) {
taskIds = new HashSet<>();
}
if (taskIds.contains(config.taskId)) {
Set<String> newIds = new HashSet<>(taskIds);
newIds.remove(config.taskId);
SharedPreferences.Editor editor = preferences.edit();
editor.putStringSet("tasks", newIds);
editor.apply();
}
if (!config.isFetchTask) {
SharedPreferences.Editor editor = context.getSharedPreferences(BackgroundFetch.TAG + ":" + config.taskId, 0).edit();
editor.clear();
editor.apply();
}
}
static int FETCH_JOB_ID = 999;
boolean isFetchTask() {
return config.isFetchTask;
}
public String getTaskId() { return config.taskId; }
public int getMinimumFetchInterval() {
return config.minimumFetchInterval;
}
public int getRequiredNetworkType() { return config.requiredNetworkType; }
public boolean getRequiresBatteryNotLow() { return config.requiresBatteryNotLow; }
public boolean getRequiresCharging() { return config.requiresCharging; }
public boolean getRequiresDeviceIdle() { return config.requiresDeviceIdle; }
public boolean getRequiresStorageNotLow() { return config.requiresStorageNotLow; }
public boolean getStopOnTerminate() {
return config.stopOnTerminate;
}
public boolean getStartOnBoot() {
return config.startOnBoot;
}
public String getJobService() { return config.jobService; }
public boolean getForceAlarmManager() {
return config.forceAlarmManager;
}
public boolean getPeriodic() {
return config.periodic || isFetchTask();
}
public long getDelay() {
return config.delay;
}
int getJobId() {
if (config.forceAlarmManager) {
return 0;
} else {
return (isFetchTask()) ? FETCH_JOB_ID : config.taskId.hashCode();
}
}
public String toString() {
JSONObject output = new JSONObject();
try {
output.put(FIELD_TASK_ID, config.taskId);
output.put(FIELD_IS_FETCH_TASK, config.isFetchTask);
output.put(FIELD_MINIMUM_FETCH_INTERVAL, config.minimumFetchInterval);
output.put(FIELD_STOP_ON_TERMINATE, config.stopOnTerminate);
output.put(FIELD_REQUIRED_NETWORK_TYPE, config.requiredNetworkType);
output.put(FIELD_REQUIRES_BATTERY_NOT_LOW, config.requiresBatteryNotLow);
output.put(FIELD_REQUIRES_CHARGING, config.requiresCharging);
output.put(FIELD_REQUIRES_DEVICE_IDLE, config.requiresDeviceIdle);
output.put(FIELD_REQUIRES_STORAGE_NOT_LOW, config.requiresStorageNotLow);
output.put(FIELD_START_ON_BOOT, config.startOnBoot);
output.put(FIELD_JOB_SERVICE, config.jobService);
output.put(FIELD_FORCE_ALARM_MANAGER, config.forceAlarmManager);
output.put(FIELD_PERIODIC, getPeriodic());
output.put(FIELD_DELAY, config.delay);
return output.toString(2);
} catch (JSONException e) {
e.printStackTrace();
return output.toString();
}
}
static void load(final Context context, final OnLoadCallback callback) {
BackgroundFetch.getThreadPool().execute(new Runnable() {
@Override
public void run() {
final List<BackgroundFetchConfig> result = new ArrayList<>();
SharedPreferences preferences = context.getSharedPreferences(BackgroundFetch.TAG, 0);
Set<String> taskIds = preferences.getStringSet("tasks", new HashSet<String>());
if (taskIds != null) {
for (String taskId : taskIds) {
result.add(new BackgroundFetchConfig.Builder().load(context, taskId));
}
}
BackgroundFetch.getUiHandler().post(new Runnable() {
@Override public void run() {
callback.onLoad(result);
}
});
}
});
}
interface OnLoadCallback {
void onLoad(List<BackgroundFetchConfig>config);
}
}

View file

@ -0,0 +1,24 @@
package com.transistorsoft.tsbackgroundfetch;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
/**
* Created by chris on 2018-01-15.
*/
public class BootReceiver extends BroadcastReceiver {
@Override
public void onReceive(final Context context, Intent intent) {
String action = intent.getAction();
Log.d(BackgroundFetch.TAG, "BootReceiver: " + action);
BackgroundFetch.getThreadPool().execute(new Runnable() {
@Override public void run() {
BackgroundFetch.getInstance(context.getApplicationContext()).onBoot();
}
});
}
}

View file

@ -0,0 +1,40 @@
package com.transistorsoft.tsbackgroundfetch;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.PowerManager;
import android.util.Log;
import static android.content.Context.POWER_SERVICE;
/**
* Created by chris on 2018-01-11.
*/
public class FetchAlarmReceiver extends BroadcastReceiver {
@Override
public void onReceive(final Context context, Intent intent) {
PowerManager powerManager = (PowerManager) context.getSystemService(POWER_SERVICE);
final PowerManager.WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, BackgroundFetch.TAG + "::" + intent.getAction());
// WakeLock expires in MAX_TIME + 4s buffer.
wakeLock.acquire((BGTask.MAX_TIME + 4000));
final String taskId = intent.getAction();
final FetchJobService.CompletionHandler completionHandler = new FetchJobService.CompletionHandler() {
@Override
public void finish() {
if (wakeLock.isHeld()) {
wakeLock.release();
Log.d(BackgroundFetch.TAG, "- FetchAlarmReceiver finish");
}
}
};
BGTask task = new BGTask(context, taskId, completionHandler, 0);
BackgroundFetch.getInstance(context.getApplicationContext()).onFetch(task);
}
}

View file

@ -0,0 +1,59 @@
package com.transistorsoft.tsbackgroundfetch;
import android.annotation.TargetApi;
import android.app.job.JobParameters;
import android.app.job.JobService;
import android.os.PersistableBundle;
import android.util.Log;
/**
* Created by chris on 2018-01-11.
*/
@TargetApi(21)
public class FetchJobService extends JobService {
@Override
public boolean onStartJob(final JobParameters params) {
PersistableBundle extras = params.getExtras();
long scheduleAt = extras.getLong("scheduled_at");
long dt = System.currentTimeMillis() - scheduleAt;
// Scheduled < 1s ago? Ignore.
if (dt < 1000) {
// JobScheduler always immediately fires an initial event on Periodic jobs -- We IGNORE these.
jobFinished(params, false);
return true;
}
final String taskId = extras.getString(BackgroundFetchConfig.FIELD_TASK_ID);
CompletionHandler completionHandler = new CompletionHandler() {
@Override
public void finish() {
Log.d(BackgroundFetch.TAG, "- jobFinished");
jobFinished(params, false);
}
};
BGTask task = new BGTask(this, taskId, completionHandler, params.getJobId());
BackgroundFetch.getInstance(getApplicationContext()).onFetch(task);
return true;
}
@Override
public boolean onStopJob(final JobParameters params) {
Log.d(BackgroundFetch.TAG, "- onStopJob");
PersistableBundle extras = params.getExtras();
final String taskId = extras.getString(BackgroundFetchConfig.FIELD_TASK_ID);
BGTask task = BGTask.getTask(taskId);
if (task != null) {
task.onTimeout(getApplicationContext());
}
jobFinished(params, false);
return true;
}
public interface CompletionHandler {
void finish();
}
}

View file

@ -0,0 +1,225 @@
package com.transistorsoft.tsbackgroundfetch;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Component for managing app life-cycle changes, including headless-mode.
*/
public class LifecycleManager implements DefaultLifecycleObserver, Runnable {
private static LifecycleManager sInstance;
public static LifecycleManager getInstance() {
if (sInstance == null) {
sInstance = getInstanceSynchronized();
}
return sInstance;
}
private static synchronized LifecycleManager getInstanceSynchronized() {
if (sInstance == null) sInstance = new LifecycleManager();
return sInstance;
}
private final List<OnHeadlessChangeCallback> mHeadlessChangeCallbacks = new ArrayList<>();
private final List<OnStateChangeCallback> mStateChangeCallbacks = new ArrayList<>();
private final Handler mHandler;
private Runnable mHeadlessChangeEvent;
private final AtomicBoolean mIsBackground = new AtomicBoolean(true);
private final AtomicBoolean mIsHeadless = new AtomicBoolean(true);
private final AtomicBoolean mStarted = new AtomicBoolean(false);
private final AtomicBoolean mPaused = new AtomicBoolean(false);
private LifecycleManager() {
mHandler = new Handler(Looper.getMainLooper());
onHeadlessChange(isHeadless -> {
if (isHeadless) {
Log.d(BackgroundFetch.TAG, "☯️ HeadlessMode? " + isHeadless);
}
});
}
/**
* Temporarily disable responding to pause/resume events. This was placed here for handling TSLocationManagerActivity events
* whose presentation causes onPause / onResume events that we don't want to react to.
*/
public void pause() {
mPaused.set(true);
}
/**
* Re-engage responding to pause/resume events.
*/
public void resume() {
mPaused.set(false);
}
/**
* Are we in the background?
* @return boolean
*/
public boolean isBackground() {
return mIsBackground.get();
}
/**
* Are we headless
* @return boolean
*/
public boolean isHeadless() {
return mIsHeadless.get();
}
/**
* Explicitly state that we are headless. Probably called when MainActivity is known to have been destroyed.
* @param value boolean
*/
public void setHeadless(boolean value) {
mIsHeadless.set(value);
if (mIsHeadless.get()) {
Log.d(BackgroundFetch.TAG,"☯️ HeadlessMode? " + mIsHeadless);
}
if (mHeadlessChangeEvent != null) {
mHandler.removeCallbacks(mHeadlessChangeEvent);
mStarted.set(true);
fireHeadlessChangeListeners();
}
}
/**
* Register Headless-mode change listener.
*/
public void onHeadlessChange(OnHeadlessChangeCallback callback) {
if (mStarted.get()) {
callback.onChange(mIsHeadless.get());
return;
}
synchronized (mHeadlessChangeCallbacks) {
mHeadlessChangeCallbacks.add(callback);
}
}
/**
* Register pause/resume listener.
*/
public void onStateChange(OnStateChangeCallback callback) {
synchronized (mStateChangeCallbacks) {
mStateChangeCallbacks.add(callback);
}
}
/**
* Regiser the LifecycleObserver
*/
@Override
public void run() {
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
}
@Override
public void onCreate(@NonNull LifecycleOwner owner) {
Log.d(BackgroundFetch.TAG,"☯️ onCreate");
// If this 50ms Timer fires before onStart, we are headless
mHeadlessChangeEvent = new Runnable() {
@Override public void run() {
mStarted.set(true);
fireHeadlessChangeListeners();
}
};
mHandler.postDelayed(mHeadlessChangeEvent, 50);
mIsHeadless.set(true);
mIsBackground.set(true);
}
@Override
public void onStart(@NonNull LifecycleOwner owner) {
Log.d(BackgroundFetch.TAG, "☯️ onStart");
// Cancel StateChange Timer.
if (mPaused.get()) {
return;
}
if (mHeadlessChangeEvent != null) {
mHandler.removeCallbacks(mHeadlessChangeEvent);
}
mStarted.set(true);
mIsHeadless.set(false);
mIsBackground.set(false);
// Fire listeners.
fireHeadlessChangeListeners();
}
@Override
public void onDestroy(@NonNull LifecycleOwner owner) {
Log.d(BackgroundFetch.TAG, "☯️ onDestroy");
mIsBackground.set(true);
mIsHeadless.set(true);
}
@Override
public void onStop(@NonNull LifecycleOwner owner) {
Log.d(BackgroundFetch.TAG, "☯️ onStop");
if (mPaused.compareAndSet(true, false)) {
return;
}
mIsBackground.set(true);
}
@Override
public void onPause(@NonNull LifecycleOwner owner) {
Log.d(BackgroundFetch.TAG, "☯️ onPause");
mIsBackground.set(true);
fireStateChangeListeners(false);
}
@Override
public void onResume(@NonNull LifecycleOwner owner) {
Log.d(BackgroundFetch.TAG, "☯️ onResume");
if (mPaused.get()) {
return;
}
mIsBackground.set(false);
mIsHeadless.set(false);
fireStateChangeListeners(true);
}
/// Fire pause/resume change listeners
private void fireStateChangeListeners(boolean isForeground) {
synchronized (mStateChangeCallbacks) {
for (OnStateChangeCallback callback : mStateChangeCallbacks) {
callback.onChange(isForeground);
}
}
}
/// Fire headless mode change listeners.
private void fireHeadlessChangeListeners() {
if (mHeadlessChangeEvent != null) {
mHandler.removeCallbacks(mHeadlessChangeEvent);
mHeadlessChangeEvent = null;
}
synchronized (mHeadlessChangeCallbacks) {
for (OnHeadlessChangeCallback callback : mHeadlessChangeCallbacks) {
callback.onChange(mIsHeadless.get());
}
mHeadlessChangeCallbacks.clear();
}
}
public interface OnHeadlessChangeCallback {
void onChange(boolean isHeadless);
}
public interface OnStateChangeCallback {
void onChange(boolean isForeground);
}
}

View file

@ -0,0 +1,3 @@
<resources>
<string name="app_name">TSBackgroundFetch</string>
</resources>

View file

@ -0,0 +1,17 @@
package com.transistorsoft.tsbackgroundfetch;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}