Compare commits

...

14 Commits

Author SHA1 Message Date
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
1c1c9bb0d7 Update docs 2024-03-13 13:51:20 +05:30
vishnukvmd
b96e7341e3 Remove thirdparty dependency on transistor-background-fetch 2024-03-13 13:47:33 +05:30
vishnukvmd
163c5de1cc Remove Isar as a submodule 2024-03-13 13:46:59 +05:30
vishnukvmd
124ef86054 Remove flutter as a submodule 2024-03-13 13:45:44 +05:30
vishnukvmd
004eb310b3 Prepare for F-Droid 2024-03-13 13:43:46 +05:30
Vishnu Mohandas
ccb6a4a283 v0.8.70 (#1079) 2024-03-13 12:43:51 +05:30
vishnukvmd
a3c80556d2 v0.8.70 2024-03-13 12:43:31 +05:30
Vishnu Mohandas
851ce5de73 Fix path to APK (#1078) 2024-03-13 12:23:16 +05:30
vishnukvmd
f8d956d47f Fix path to APK 2024-03-13 12:22:11 +05:30
Neeraj Gupta
7543dc6b57 [docs] Update custom server section for cli 2024-03-13 12:03:56 +05:30
22 changed files with 326 additions and 199 deletions

View File

@@ -49,7 +49,7 @@ jobs:
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD_PHOTOS }}
- name: Checksum
run: sha256sum build/app/outputs/flutter-apk/ente.apk > build/app/outputs/flutter-apk/sha256sum
run: sha256sum build/app/outputs/flutter-apk/ente-${{ github.ref_name }}.apk > build/app/outputs/flutter-apk/sha256sum
- name: Create a draft GitHub release
uses: ncipollo/release-action@v1

13
.gitmodules vendored
View File

@@ -9,16 +9,9 @@
[submodule "auth/assets/simple-icons"]
path = auth/assets/simple-icons
url = https://github.com/simple-icons/simple-icons.git
[submodule "mobile/thirdparty/flutter"]
path = mobile/thirdparty/flutter
url = https://github.com/flutter/flutter.git
branch = stable
[submodule "mobile/plugins/clip_ggml"]
path = mobile/plugins/clip_ggml
url = https://github.com/ente-io/clip-ggml.git
[submodule "mobile/thirdparty/isar"]
path = mobile/thirdparty/isar
url = https://github.com/isar/isar
[submodule "web/apps/photos/thirdparty/ffmpeg-wasm"]
path = web/apps/photos/thirdparty/ffmpeg-wasm
url = https://github.com/abhinavkgrd/ffmpeg.wasm.git
@@ -27,3 +20,9 @@
path = web/apps/photos/thirdparty/photoswipe
url = https://github.com/ente-io/PhotoSwipe.git
branch = single-thread
[submodule "mobile/thirdparty/isar"]
path = mobile/thirdparty/isar
url = https://github.com/isar/isar
[submodule "mobile/thirdparty/flutter"]
path = mobile/thirdparty/flutter
url = https://github.com/flutter/flutter

View File

@@ -22,6 +22,10 @@ configure the endpoint the app should be connecting to.
# CLI
> [!WARNING]
> The new version of CLI that supports connecting to custom server is still in beta.
> You can download the beta version from [here](https://github.com/ente-io/ente/releases?q=tag%3Acli-v0&expanded=true)
Define a config.yaml and put it either in the same directory as CLI or path defined in env variable `ENTE_CLI_CONFIG_PATH`
```yaml

View File

@@ -45,8 +45,7 @@ You can alternatively install the build from PlayStore or F-Droid.
## 🧑‍💻 Building from source
1. [Install Flutter v3.13.4](https://flutter.dev/docs/get-started/install) or
set the Path of Flutter SDK to `thirdparty/flutter/bin`.
1. [Install Flutter v3.13.4](https://flutter.dev/docs/get-started/install).
2. Pull in all submodules with `git submodule update --init --recursive`

View File

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

View File

@@ -1,3 +0,0 @@
{
"dart.flutterSdkPath": "thirdparty/flutter/bin"
}

View File

@@ -4,7 +4,6 @@ import 'dart:io';
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";
@@ -34,7 +33,6 @@ import "package:photos/services/location_service.dart";
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";
@@ -216,14 +214,6 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
unawaited(HomeWidgetService.instance.initHomeWidget());
}
if (Platform.isIOS) {
// ignore: unawaited_futures
PushService.instance.init().then((_) {
FirebaseMessaging.onBackgroundMessage(
_firebaseMessagingBackgroundHandler,
);
});
}
unawaited(FeatureFlagService.instance.init());
unawaited(SemanticSearchService.instance.init());
MachineLearningController.instance.init();
@@ -334,35 +324,6 @@ Future<void> _killBGTask([String? taskId]) async {
}
}
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';
@@ -35,6 +34,7 @@ class BillingService {
final _logger = Logger("BillingService");
final _enteDio = NetworkClient.instance.enteDio;
// ignore: unused_field
bool _isOnSubscriptionPage = false;
Future<BillingPlans>? _future;
@@ -44,23 +44,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

@@ -6,7 +6,8 @@ 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()) {
if (UpdateService.instance.isIndependentFlavor() ||
UpdateService.instance.isFdroidFlavor()) {
return StripeSubscriptionPage(isOnboarding: isOnBoarding);
}
if (FeatureFlagService.instance.enableStripe() &&

View File

@@ -12,7 +12,7 @@ description: ente photos application
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.8.68+588
version: 0.8.70+590
publish_to: none
environment:
@@ -64,8 +64,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.13.1
firebase_messaging: ^14.6.2
fk_user_agent: ^2.0.1
flutter:
sdk: flutter
@@ -99,7 +97,6 @@ dependencies:
http: ^1.1.0
image: ^4.0.17
image_editor: ^1.3.0
in_app_purchase: ^3.0.7
intl: ^0.18.0
isar: ^3.1.0+1
isar_flutter_libs: ^3.1.0+1
@@ -207,7 +204,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:

View File

@@ -1,7 +1,3 @@
# TODO: add `rustup@1.25.2` to `srclibs`
# TODO: verify if `gcc-multilib` or `libc-dev` is needed
$$rustup$$/rustup-init.sh -y
source $HOME/.cargo/env
cd thirdparty/isar/
bash tool/build_android.sh x86
bash tool/build_android.sh x64
@@ -15,3 +11,4 @@ mv libisar_android_x64.so libisar.so
mv libisar.so $PUB_CACHE/hosted/pub.dev/isar_flutter_libs-*/android/src/main/jniLibs/x86_64/
mv libisar_android_x86.so libisar.so
mv libisar.so $PUB_CACHE/hosted/pub.dev/isar_flutter_libs-*/android/src/main/jniLibs/x86/
cd ../../

View File

@@ -1,63 +0,0 @@
# Eclipse
.metadata
# Xcode
#
.DS_Store
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control
#
#Pods/
# Eclipse
# built application files
*.apk
*.ap_
# files for the dex VM
*.dex
# Java class files
*.class
# generated files
bin/
gen/
# Local configuration file (sdk path, etc)
local.properties
# Eclipse project files
.classpath
.project
# Proguard folder generated by Eclipse
proguard/
# Intellij project files
*.iml
*.ipr
*.iws
.idea/

View File

@@ -1,11 +1,11 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 26
compileSdkVersion 30
defaultConfig {
applicationId "com.transistorsoft.backgroundfetch"
minSdkVersion 16
targetSdkVersion 26
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
@@ -24,6 +24,5 @@ dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:26.1.0'
}

View File

@@ -3,11 +3,11 @@
buildscript {
repositories {
google()
jcenter()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
classpath 'com.android.tools.build:gradle:4.1.3'
// NOTE: Do not place your application dependencies here; they belong
@@ -18,7 +18,7 @@ buildscript {
allprojects {
repositories {
google()
jcenter()
mavenCentral()
}
}
@@ -27,8 +27,8 @@ task clean(type: Delete) {
}
ext {
compileSdkVersion = 29
targetSdkVersion = 29
compileSdkVersion = 32
targetSdkVersion = 31
buildToolsVersion = "29.0.6"
appCompatVersion = "1.1.0"
appCompatVersion = "1.4.1"
}

View File

@@ -16,8 +16,8 @@ org.gradle.jvmargs=-Xmx1536m
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
VERSION_NAME=0.5.0
VERSION_CODE=15
VERSION_NAME=0.5.6
VERSION_CODE=21
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -1,7 +1,6 @@
#Thu Feb 09 18:40:48 IST 2023
#Thu Jul 15 09:21:17 EDT 2021
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
distributionSha256Sum=10065868c78f1207afb3a92176f99a37d753a513dff453abb6b5cceda4058cda
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip

View File

@@ -32,6 +32,10 @@ android {
mavenLocal()
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
@@ -40,15 +44,14 @@ dependencies {
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 'cordovaRelease'
task.dependsOn 'reactNativeRelease'
task.dependsOn 'nativeScriptRelease'
task.dependsOn 'flutterRelease'
}
@@ -59,7 +62,7 @@ task publishRelease { task ->
tasks["publishRelease"].mustRunAfter("assembleRelease")
tasks["publishRelease"].finalizedBy("publish")
def WORKSPACE_PATH = "/Volumes/Glyph2TB/Users/chris/workspace"
def WORKSPACE_PATH = "/Users/chris/workspace"
// Build local maven repo.
def LIBRARY_PATH = "com/transistorsoft/tsbackgroundfetch"
@@ -78,7 +81,7 @@ task buildLocalRepository { task ->
}
}
def cordovaDir = "$WORKSPACE_PATH/cordova/background-geolocation/cordova-plugin-background-fetch"
def cordovaDir = "$WORKSPACE_PATH/background-geolocation/cordova/cordova-plugin-background-fetch"
task cordovaRelease { task ->
task.dependsOn 'buildLocalRepository'
doLast {
@@ -95,7 +98,7 @@ task cordovaRelease { task ->
}
}
def reactNativeDir = "$WORKSPACE_PATH/react/background-geolocation/react-native-background-fetch"
def reactNativeDir = "$WORKSPACE_PATH/background-geolocation/react/react-native-background-fetch"
task reactNativeRelease { task ->
task.dependsOn 'buildLocalRepository'
doLast {
@@ -129,6 +132,19 @@ task flutterRelease { task ->
}
}
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")

View File

@@ -3,11 +3,12 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.GET_TASKS" />
<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">
<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" />

View File

@@ -110,6 +110,16 @@ public class BGTask {
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());
@@ -136,6 +146,8 @@ public class BGTask {
}
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) {
@@ -172,7 +184,7 @@ public class BGTask {
BackgroundFetch adapter = BackgroundFetch.getInstance(context);
if (adapter.isMainActivityActive()) {
if (!LifecycleManager.getInstance().isHeadless()) {
BackgroundFetch.Callback callback = adapter.getFetchCallback();
if (callback != null) {
callback.onTimeout(mTaskId);
@@ -246,7 +258,7 @@ public class BGTask {
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);
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT|PendingIntent.FLAG_IMMUTABLE);
}
public String toString() {

View File

@@ -4,6 +4,8 @@ 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;
@@ -76,6 +78,8 @@ public class BackgroundFetch {
private BackgroundFetch(Context context) {
mContext = context;
// Start Lifecycle Observer to be notified when app enters background.
getUiHandler().post(LifecycleManager.getInstance());
}
@SuppressWarnings({"unused"})
@@ -84,7 +88,16 @@ public class BackgroundFetch {
mFetchCallback = callback;
synchronized (mConfig) {
mConfig.put(config.getTaskId(), config);
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());
}
@@ -224,8 +237,6 @@ public class BackgroundFetch {
}
private void registerTask(String taskId) {
Log.d(TAG, "- registerTask: " + taskId);
BackgroundFetchConfig config = getConfig(taskId);
if (config == null) {
@@ -234,6 +245,12 @@ public class BackgroundFetch {
}
config.save(mContext);
String msg = "- registerTask: " + taskId;
if (!config.getForceAlarmManager()) {
msg += " (jobId: " + config.getJobId() + ")";
}
Log.d(TAG, msg);
BGTask.schedule(mContext, config);
}
@@ -245,7 +262,7 @@ public class BackgroundFetch {
return;
}
if (isMainActivityActive()) {
if (!LifecycleManager.getInstance().isHeadless()) {
if (mFetchCallback != null) {
mFetchCallback.onFetch(task.getTaskId());
}
@@ -267,29 +284,6 @@ public class BackgroundFetch {
}
}
@SuppressWarnings({"WeakerAccess", "deprecation"})
public Boolean isMainActivityActive() {
Boolean isActive = false;
if (mContext == null || mFetchCallback == null) {
return false;
}
ActivityManager activityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
try {
List<ActivityManager.RunningTaskInfo> tasks = activityManager.getRunningTasks(Integer.MAX_VALUE);
for (ActivityManager.RunningTaskInfo task : tasks) {
if (mContext.getPackageName().equalsIgnoreCase(task.baseActivity.getPackageName())) {
isActive = true;
break;
}
}
} catch (java.lang.SecurityException e) {
Log.w(TAG, "TSBackgroundFetch attempted to determine if MainActivity is active but was stopped due to a missing permission. Please add the permission 'android.permission.GET_TASKS' to your AndroidManifest. See Installation steps for more information");
throw e;
}
return isActive;
}
BackgroundFetchConfig getConfig(String taskId) {
synchronized (mConfig) {
return (mConfig.containsKey(taskId)) ? mConfig.get(taskId) : null;

View File

@@ -14,6 +14,15 @@ 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() {

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);
}
}