Add background-fetch as a folder

This commit is contained in:
Vishnu Mohandas 2021-05-21 18:45:04 +05:30
parent f0e8862216
commit 1dc4f4ef90
72 changed files with 3944 additions and 1 deletions

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

View file

@ -0,0 +1,63 @@
# 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

@ -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,29 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 26
defaultConfig {
applicationId "com.transistorsoft.backgroundfetch"
minSdkVersion 16
targetSdkVersion 26
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'])
implementation 'com.android.support:appcompat-v7:26.1.0'
}

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()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
ext {
compileSdkVersion = 29
targetSdkVersion = 29
buildToolsVersion = "29.0.6"
appCompatVersion = "1.1.0"
}

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.0
VERSION_CODE=15
android.useAndroidX=true
android.enableJetifier=true

View file

@ -0,0 +1,7 @@
#Tue Apr 21 10:22:19 EDT 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
distributionSha256Sum=10065868c78f1207afb3a92176f99a37d753a513dff453abb6b5cceda4058cda

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,138 @@
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 {
maven {
url "$buildDir/repo"
}
}
}
}
dependencies {
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
implementation fileTree(dir: 'libs', include: ['*.jar'])
//implementation "androidx.appcompat:appcompat:$rootProject.appCompatVersion"
}
// Build Release
task buildRelease { task ->
task.dependsOn 'cordovaRelease'
task.dependsOn 'reactNativeRelease'
task.dependsOn 'nativeScriptRelease'
task.dependsOn 'flutterRelease'
}
// Publish Release.
task publishRelease { task ->
task.dependsOn 'assembleRelease'
}
tasks["publishRelease"].mustRunAfter("assembleRelease")
tasks["publishRelease"].finalizedBy("publish")
def WORKSPACE_PATH = "/Volumes/Glyph2TB/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/cordova/background-geolocation/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/react/background-geolocation/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)
}
}
}
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,17 @@
<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.GET_TASKS" />
<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">
<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,279 @@
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 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());
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 (adapter.isMainActivityActive()) {
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);
}
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,306 @@
package com.transistorsoft.tsbackgroundfetch;
import android.annotation.TargetApi;
import android.app.ActivityManager;
import android.content.Context;
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;
}
@SuppressWarnings({"unused"})
public void configure(BackgroundFetchConfig config, BackgroundFetch.Callback callback) {
Log.d(TAG, "- " + ACTION_CONFIGURE);
mFetchCallback = callback;
synchronized (mConfig) {
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) {
Log.d(TAG, "- registerTask: " + taskId);
BackgroundFetchConfig config = getConfig(taskId);
if (config == null) {
Log.e(TAG, "- registerTask failed to find BackgroundFetchConfig for taskId " + taskId);
return;
}
config.save(mContext);
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 (isMainActivityActive()) {
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());
}
}
@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;
}
}
/**
* @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,50 @@
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();
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,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);
}
}

View file

@ -0,0 +1,45 @@
//
// RNBackgroundFetchManager.h
// RNBackgroundFetch
//
// Created by Christopher Scott on 2016-08-02.
// Copyright © 2016 Christopher Scott. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <BackgroundTasks/BackgroundTasks.h>
@interface TSBackgroundFetch : NSObject
@property (nonatomic) BOOL stopOnTerminate;
@property (readonly) BOOL configured;
@property (readonly) BOOL active;
@property (readonly) NSString *fetchTaskId;
+ (TSBackgroundFetch *)sharedInstance;
-(void) didFinishLaunching;
-(void) registerAppRefreshTask;
-(void) registerBGProcessingTask:(NSString*)identifier;
-(void) configure:(NSTimeInterval)delay callback:(void(^)(UIBackgroundRefreshStatus status))callback;
-(NSError*) scheduleProcessingTaskWithIdentifier:(NSString*)identifier delay:(NSTimeInterval)delay periodic:(BOOL)periodic callback:(void (^)(NSString* taskId, BOOL timeout))callback;
-(NSError*) scheduleProcessingTaskWithIdentifier:(NSString*)identifier delay:(NSTimeInterval)delay periodic:(BOOL)periodic requiresExternalPower:(BOOL)requiresExternalPower requiresNetworkConnectivity:(BOOL)requiresNetworkConnectivity callback:(void (^)(NSString* taskId, BOOL timeout))callback;
-(void) addListener:(NSString*)componentName callback:(void (^)(NSString* componentName))callback;
-(void) addListener:(NSString*)componentName callback:(void (^)(NSString* componentName))callback timeout:(void (^)(NSString* componentName))timeout;
-(void) removeListener:(NSString*)componentName;
-(BOOL) hasListener:(NSString*)componentName;
-(NSError*) start:(NSString*)identifier;
-(void) stop:(NSString*)identifier;
-(void) finish:(NSString*)tag;
-(void) status:(void(^)(UIBackgroundRefreshStatus status))callback;
// @deprecated API
-(void) performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))handler applicationState:(UIApplicationState)state;
@end

View file

@ -0,0 +1,6 @@
framework module TSBackgroundFetch {
umbrella header "TSBackgroundFetch.h"
export *
module * { export * }
}

View file

@ -0,0 +1,151 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>files</key>
<dict>
<key>Headers/TSBackgroundFetch.h</key>
<data>
6IaHDuPW1C0nWBp03yKGD5PrgTY=
</data>
<key>Info.plist</key>
<data>
/q6b6t8hcxgtnV2U/r/pxyTm1is=
</data>
<key>Modules/module.modulemap</key>
<data>
4NbofBeHHFHeUWRZ1ZcIkl3/T3w=
</data>
</dict>
<key>files2</key>
<dict>
<key>Headers/TSBackgroundFetch.h</key>
<dict>
<key>hash</key>
<data>
6IaHDuPW1C0nWBp03yKGD5PrgTY=
</data>
<key>hash2</key>
<data>
Q6MbZjcbrHTACLwceQDEESRGhY9/uOOu7bCb71fOAfk=
</data>
</dict>
<key>Modules/module.modulemap</key>
<dict>
<key>hash</key>
<data>
4NbofBeHHFHeUWRZ1ZcIkl3/T3w=
</data>
<key>hash2</key>
<data>
ZZoRDGe9SOWekYXO71UHMmkagz+I18vRneUTVMd0WnY=
</data>
</dict>
</dict>
<key>rules</key>
<dict>
<key>^</key>
<true/>
<key>^.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Base\.lproj/</key>
<dict>
<key>weight</key>
<real>1010</real>
</dict>
<key>^version.plist$</key>
<true/>
</dict>
<key>rules2</key>
<dict>
<key>.*\.dSYM($|/)</key>
<dict>
<key>weight</key>
<real>11</real>
</dict>
<key>^</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
<key>^(.*/)?\.DS_Store$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>2000</real>
</dict>
<key>^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/</key>
<dict>
<key>nested</key>
<true/>
<key>weight</key>
<real>10</real>
</dict>
<key>^.*</key>
<true/>
<key>^.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Base\.lproj/</key>
<dict>
<key>weight</key>
<real>1010</real>
</dict>
<key>^Info\.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^PkgInfo$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^[^/]+$</key>
<dict>
<key>nested</key>
<true/>
<key>weight</key>
<real>10</real>
</dict>
<key>^embedded\.provisionprofile$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
<key>^version\.plist$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
</dict>
</dict>
</plist>

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:TSBackgroundFetch.xcodeproj">
</FileRef>
</Workspace>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildSystemType</key>
<string>Latest</string>
</dict>
</plist>

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>

View file

@ -0,0 +1,39 @@
//
// TSBGAppRefreshSubscriber.h
// TSBackgroundFetch
//
// Created by Christopher Scott on 2020-02-07.
// Copyright © 2020 Christopher Scott. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <BackgroundTasks/BackgroundTasks.h>
@interface TSBGAppRefreshSubscriber : NSObject
+(void)load;
+(NSMutableDictionary *)subscribers;
+(void) add:(TSBGAppRefreshSubscriber*)tsTask;
+(void) remove:(TSBGAppRefreshSubscriber*)tsTask;
+(TSBGAppRefreshSubscriber*) get:(NSString*)identifier;
+(void) execute;
+(BOOL) onTimeout;
+(void)registerTaskScheduler API_AVAILABLE(ios(13));
+(BOOL)useTaskScheduler;
@property (nonatomic) NSString* identifier;
@property (copy) void (^callback) (NSString*);
@property (copy) void (^timeout) (NSString*);
@property (nonatomic, readonly) BOOL enabled;
@property (nonatomic, readonly) BOOL executed;
@property (nonatomic, readonly) BOOL finished;
-(instancetype) initWithIdentifier:(NSString*)identifier callback:(void (^)(NSString* taskId))callback timeout:(void (^)(NSString* taskId))timeout;
-(void) execute;
-(void) onTimeout;
-(void) finish;
-(void) destroy;
@end

View file

@ -0,0 +1,188 @@
//
// TSBGAppRefreshSubscriber.m
// TSBackgroundFetch
//
// Created by Christopher Scott on 2020-02-07.
// Copyright © 2020 Christopher Scott. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "TSBGAppRefreshSubscriber.h"
#import "TSBackgroundFetch.h"
static NSString *const TAG = @"TSBGAppRefreshSubscriber";
static NSMutableDictionary *_subscribers;
static BOOL _hasRegisteredTaskScheduler = NO;
@implementation TSBGAppRefreshSubscriber {
}
+(void)load {
[[self class] subscribers];
}
+ (NSMutableDictionary*)subscribers
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
_subscribers = [NSMutableDictionary new];
// Load the set of taskIds, eg: ["foo, "bar"]
NSArray *subscribers = [defaults objectForKey:TAG];
// Foreach taskId, load TSBGTask config from NSDefaults, eg: "TSBackgroundFetch:foo"
for (NSString *identifier in subscribers) {
TSBGAppRefreshSubscriber *subscriber = [[TSBGAppRefreshSubscriber alloc] initWithIdentifier:identifier];
[_subscribers setObject:subscriber forKey:identifier];
}
NSLog(@"[%@ load]: %@", TAG, _subscribers);
});
return _subscribers;
}
+ (TSBGAppRefreshSubscriber*) get:(NSString*)identifier {
@synchronized (_subscribers) {
return [_subscribers objectForKey:identifier];
}
}
+ (void) add:(TSBGAppRefreshSubscriber*)subscriber {
@synchronized (_subscribers) {
[_subscribers setObject:subscriber forKey:subscriber.identifier];
}
}
+ (void) remove:(TSBGAppRefreshSubscriber*)subscriber {
@synchronized (_subscribers) {
[_subscribers removeObjectForKey:subscriber.identifier];
}
}
+(void)registerTaskScheduler{
_hasRegisteredTaskScheduler = YES;
}
+(BOOL)useTaskScheduler {
return _hasRegisteredTaskScheduler;
}
+(void) execute {
NSArray *subscribers = [[self subscribers] allValues];
for (TSBGAppRefreshSubscriber *subscriber in subscribers) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[subscriber execute];
});
}
}
+(BOOL) onTimeout {
BOOL foundTimeoutHandler = NO;
NSArray *subscribers = [[self subscribers] allValues];
for (TSBGAppRefreshSubscriber *subscriber in subscribers) {
foundTimeoutHandler = YES;
if (subscriber.timeout) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[subscriber onTimeout];
});
} else {
[[TSBackgroundFetch sharedInstance] finish:subscriber.identifier];
}
}
return foundTimeoutHandler;
}
-(instancetype)init {
self = [super init];
_enabled = YES;
_finished = NO;
_executed = NO;
return self;
}
-(instancetype) initWithIdentifier:(NSString*)identifier {
self = [self init];
if (self) {
_identifier = identifier;
}
return self;
}
-(instancetype) initWithIdentifier:(NSString*)identifier callback:(void (^)(NSString* taskId))callback timeout:(void (^)(NSString* taskId))timeout {
self = [self init];
if (self) {
_identifier = identifier;
_callback = callback;
_timeout = timeout;
[self save];
@synchronized (_subscribers) {
[_subscribers setObject:self forKey:identifier];
}
}
return self;
}
-(void) execute {
if (_executed || !_callback) return;
_executed = YES;
_finished = NO;
dispatch_async(dispatch_get_main_queue(), ^(void) {
self.callback(self.identifier);
});
}
-(void) onTimeout {
if (!_timeout) {
[self finish];
return;
}
if (!_finished) {
dispatch_async(dispatch_get_main_queue(), ^(void) {
self.timeout(self.identifier);
});
}
}
-(void) finish {
_finished = YES;
_executed = NO;
}
-(void) destroy {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSMutableArray *subscribers = [[defaults objectForKey:TAG] mutableCopy];
[TSBGAppRefreshSubscriber remove:self];
if (!subscribers) {
subscribers = [NSMutableArray new];
}
if ([subscribers containsObject:_identifier]) {
[subscribers removeObject:_identifier];
[defaults setObject:subscribers forKey:TAG];
}
}
-(void) save {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSMutableArray *subscribers = [[defaults objectForKey:TAG] mutableCopy];
if (!subscribers) {
subscribers = [NSMutableArray new];
}
if ([subscribers containsObject:_identifier]) {
return;
}
[subscribers addObject:_identifier];
[defaults setObject:subscribers forKey:TAG];
}
-(NSString*) description {
return [NSString stringWithFormat:@"<%@ identifier=%@, executed=%d, enabled=%d>", TAG, _identifier, _executed, _enabled];
}
@end

View file

@ -0,0 +1,52 @@
//
// TSBGTask.h
// TSBackgroundFetch
//
// Created by Christopher Scott on 2020-01-23.
// Copyright © 2020 Christopher Scott. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <BackgroundTasks/BackgroundTasks.h>
@interface TSBGTask : NSObject
@property (nonatomic) BGTask* task API_AVAILABLE(ios(13.0));
@property (nonatomic) NSString* identifier;
@property (copy) void (^callback)(NSString*, BOOL);
@property (nonatomic) NSTimeInterval delay;
@property (nonatomic, readonly) BOOL executed;
@property (nonatomic) BOOL periodic;
@property (nonatomic) BOOL enabled;
@property (nonatomic, readonly) BOOL finished;
@property (nonatomic) BOOL stopOnTerminate;
@property (nonatomic) BOOL requiresExternalPower;
@property (nonatomic) BOOL requiresNetworkConnectivity;
+(void)load;
+(NSMutableArray *)tasks;
+(void) add:(TSBGTask*)tsTask;
+(void) remove:(TSBGTask*)tsTask;
+(TSBGTask*) get:(NSString*)identifier;
+(void)registerForTaskWithIdentifier:(NSString*)identifier;
+(BOOL)useProcessingTaskScheduler;
-(instancetype) initWithIdentifier:(NSString*)identifier delay:(NSTimeInterval)delay periodic:(BOOL)periodic callback:(void (^)(NSString* taskId, BOOL timeout))callback;
-(instancetype) initWithIdentifier:(NSString*)identifier delay:(NSTimeInterval)delay periodic:(BOOL)periodic requiresExternalPower:(BOOL)requiresExternalPower requiresNetworkConnectivity:(BOOL)requiresNetworkConnectivity callback:(void (^)(NSString* taskId, BOOL timeout))callback;
-(instancetype) initWithDictionary:(NSDictionary*)config;
-(BOOL) execute;
-(void) finish:(BOOL)success;
-(NSError*) schedule;
-(void) stop;
-(void) setTask:(BGProcessingTask*)task API_AVAILABLE(ios(13));
-(void) destroy;
-(void) save;
@end

View file

@ -0,0 +1,297 @@
//
// TSBGTask.m
// TSBackgroundFetch
//
// Created by Christopher Scott on 2020-01-23.
// Copyright © 2020 Christopher Scott. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "TSBGTask.h"
static NSString *const TAG = @"TSBackgroundFetch";
static NSString *const TASKS_STORAGE_KEY = @"TSBackgroundFetch:tasks";
static BOOL _hasRegisteredProcessingTaskScheduler = NO;
static NSString *const ERROR_PROCESSING_TASK_NOT_REGISTERED = @"Background procssing task was not registered in AppDelegate didFinishLaunchingWithOptions. See iOS Setup Guide.";
static NSString *const ERROR_PROCESSING_TASK_NOT_AVAILABLE = @"Background procssing tasks are only available with iOS 13+";
static NSMutableArray *_tasks;
@implementation TSBGTask {
BOOL scheduled;
}
#pragma mark Class Methods
+(void)registerForTaskWithIdentifier:(NSString*)identifier API_AVAILABLE(ios(13)) {
_hasRegisteredProcessingTaskScheduler = YES;
NSLog(@"[%@ registerForTaskWithIdentifier: %@", TAG, identifier);
[[BGTaskScheduler sharedScheduler] registerForTaskWithIdentifier:identifier usingQueue:nil launchHandler:^(BGTask* task) {
TSBGTask *tsTask = [self get:task.identifier];
if (!tsTask) {
NSLog(@"[%@ registerForTaskWithIdentifier launchHandler] ERROR: Failed to find TSBGTask in Fetch event: %@", TAG, task.identifier);
[task setTaskCompletedWithSuccess:NO];
return;
}
[tsTask setTask:(BGProcessingTask*)task];
}];
}
+(BOOL)useProcessingTaskScheduler {
return _hasRegisteredProcessingTaskScheduler;
}
+(void)load {
[[self class] tasks];
}
+ (NSMutableArray*)tasks
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
_tasks = [NSMutableArray new];
// Load the set of taskIds, eg: ["foo, "bar"]
NSArray *taskIds = [defaults objectForKey:TASKS_STORAGE_KEY];
// Foreach taskId, load TSBGTask config from NSDefaults, eg: "TSBackgroundFetch:foo"
for (NSString *taskId in taskIds) {
NSString *key = [NSString stringWithFormat:@"%@:%@", TAG, taskId];
NSDictionary *config = [defaults objectForKey:key];
TSBGTask *tsTask = [[TSBGTask alloc] initWithDictionary:config];
[_tasks addObject:tsTask];
}
NSLog(@"[%@ load]: %@", TAG, _tasks);
});
@synchronized (_tasks) {
return [_tasks copy];
}
}
+ (TSBGTask*) get:(NSString*)identifier {
@synchronized (_tasks) {
for (TSBGTask *tsTask in _tasks) {
if ([tsTask.identifier isEqualToString:identifier]) {
return tsTask;
}
}
}
return nil;
}
+ (void) add:(TSBGTask*)tsTask {
@synchronized (_tasks) {
[_tasks addObject:tsTask];
}
}
+ (void) remove:(TSBGTask*)tsTask {
@synchronized (_tasks) {
[_tasks removeObject:tsTask];
}
}
# pragma mark Instance Methods
-(instancetype)init {
self = [super init];
scheduled = NO;
_enabled = NO;
_executed = NO;
_finished = NO;
_requiresNetworkConnectivity = NO;
_requiresExternalPower = NO;
return self;
}
/// @deprecated
-(instancetype) initWithIdentifier:(NSString*)identifier delay:(NSTimeInterval)delay periodic:(BOOL)periodic callback:(void (^)(NSString* taskId, BOOL timeout))callback {
return [self initWithIdentifier:identifier delay:delay periodic:periodic requiresExternalPower:NO requiresNetworkConnectivity:NO callback:callback];
}
-(instancetype) initWithIdentifier:(NSString*)identifier delay:(NSTimeInterval)delay periodic:(BOOL)periodic requiresExternalPower:(BOOL)requiresExternalPower requiresNetworkConnectivity:(BOOL)requiresNetworkConnectivity callback:(void (^)(NSString* taskId, BOOL timeout))callback {
self = [self init];
if (self) {
_identifier = identifier;
_delay = delay;
_periodic = periodic;
_requiresExternalPower = requiresExternalPower;
_requiresNetworkConnectivity = requiresNetworkConnectivity;
[TSBGTask add:self];
}
return self;
}
-(instancetype) initWithDictionary:(NSDictionary*)config {
self = [self init];
if (self) {
_identifier = [config objectForKey:@"identifier"];
_delay = [[config objectForKey:@"delay"] longValue];
_periodic = [[config objectForKey:@"periodic"] boolValue];
_enabled = [[config objectForKey:@"enabled"] boolValue];
if ([config objectForKey:@"requiresExternalPower"]) {
_requiresExternalPower = [[config objectForKey:@"requiresExternalPower"] boolValue];
}
if ([config objectForKey:@"requiresNetworkConnectivity"]) {
_requiresNetworkConnectivity = [[config objectForKey:@"requiresNetworkConnectivity"] boolValue];
}
}
return self;
}
- (NSError*) schedule {
if (@available (iOS 13.0, *)) {
if (![TSBGTask useProcessingTaskScheduler]) {
return [[NSError alloc] initWithDomain:TAG code:0 userInfo:@{NSLocalizedFailureReasonErrorKey:ERROR_PROCESSING_TASK_NOT_REGISTERED}];
}
NSLog(@"[%@ scheduleProcessingTask] %@", TAG, self);
BGTaskScheduler *scheduler = [BGTaskScheduler sharedScheduler];
if (scheduled) {
[[BGTaskScheduler sharedScheduler] cancelTaskRequestWithIdentifier:_identifier];
}
BGProcessingTaskRequest *request = [[BGProcessingTaskRequest alloc] initWithIdentifier:_identifier];
// TODO Configurable.
request.requiresExternalPower = _requiresExternalPower;
request.requiresNetworkConnectivity = _requiresNetworkConnectivity;
request.earliestBeginDate = [NSDate dateWithTimeIntervalSinceNow:_delay];
NSError *error = nil;
[scheduler submitTaskRequest:request error:&error];
if (!error) {
scheduled = YES;
if (!_enabled) {
_enabled = YES;
[self save];
}
}
return error;
} else {
return [[NSError alloc] initWithDomain:TAG code:-1 userInfo:@{NSLocalizedFailureReasonErrorKey:ERROR_PROCESSING_TASK_NOT_AVAILABLE}];
}
}
- (void) stop {
_enabled = NO;
scheduled = NO;
[self destroy];
if (@available(iOS 13.0, *)) {
[[BGTaskScheduler sharedScheduler] cancelTaskRequestWithIdentifier:_identifier];
}
}
-(void) setTask:(BGProcessingTask*)task {
scheduled = NO;
_task = task;
task.expirationHandler = ^{
NSLog(@"[%@ expirationHandler] WARNING: %@ '%@' expired before #finish was executed.", TAG, NSStringFromClass([self.task class]), self.identifier);
[self onTimeout];
// TODO Disabled with onTimeout implementation.
//[self finish:NO];
};
// If no callback registered for TSTask, the app was launched in background. The event will be handled once task is scheduled.
if (_callback) {
[self execute];
}
}
- (BOOL) execute {
if (@available(iOS 13.0, *)) {
if ([TSBGTask useProcessingTaskScheduler] && _periodic && !scheduled) {
[self schedule];
}
}
_finished = NO;
if (_callback) {
_callback(_identifier, NO);
_executed = YES;
return YES;
} else {
return NO;
}
}
- (BOOL) onTimeout {
if (_callback) {
_callback(_identifier, YES);
return YES;
} else {
return NO;
}
}
-(void) finish:(BOOL)success {
[_task setTaskCompletedWithSuccess:success];
_finished = YES;
_executed = NO;
_task = nil;
if (!_periodic) {
[self destroy];
}
}
-(void) save {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSMutableArray *taskIds = [[defaults objectForKey:TASKS_STORAGE_KEY] mutableCopy];
if (!taskIds) {
taskIds = [NSMutableArray new];
}
if (![taskIds containsObject:_identifier]) {
[taskIds addObject:_identifier];
[defaults setObject:taskIds forKey:TASKS_STORAGE_KEY];
}
NSString *key = [NSString stringWithFormat:@"%@:%@", TAG, _identifier];
NSLog(@"[TSBGTask save]: %@", self);
[defaults setObject:[self toDictionary] forKey:key];
}
-(void) destroy {
[TSBGTask remove:self];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSMutableArray *taskIds = [[defaults objectForKey:TASKS_STORAGE_KEY] mutableCopy];
if (!taskIds) {
taskIds = [NSMutableArray new];
}
if ([taskIds containsObject:_identifier]) {
[taskIds removeObject:_identifier];
[defaults setObject:taskIds forKey:TASKS_STORAGE_KEY];
}
NSString *key = [NSString stringWithFormat:@"%@:%@", TAG, _identifier];
if ([defaults objectForKey:key]) {
[defaults removeObjectForKey:key];
}
NSLog(@"[TSBGTask destroy] %@", _identifier);
}
-(NSDictionary*) toDictionary {
return @{
@"identifier": _identifier,
@"delay": @(_delay),
@"periodic": @(_periodic),
@"enabled": @(_enabled),
@"requiresExternalPower": @(_requiresExternalPower),
@"requiresNetworkConnectivity": @(_requiresNetworkConnectivity)
};
}
-(NSString*) description {
return [NSString stringWithFormat:@"<TSBGTask identifier=%@, delay=%ld, periodic=%d requiresNetworkConnectivity=%d requiresExternalPower=%d, enabled=%d>", _identifier, (long)_delay, _periodic, _requiresNetworkConnectivity, _requiresExternalPower, _enabled];
}
@end

View file

@ -0,0 +1,47 @@
//
// RNBackgroundFetchManager.h
// RNBackgroundFetch
//
// Created by Christopher Scott on 2016-08-02.
// Copyright © 2016 Christopher Scott. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <BackgroundTasks/BackgroundTasks.h>
@interface TSBackgroundFetch : NSObject
@property (nonatomic) BOOL stopOnTerminate;
@property (readonly) BOOL configured;
@property (readonly) BOOL active;
@property (readonly) NSString *fetchTaskId;
@property (copy) void (^completionHandler)(UIBackgroundFetchResult);
@property (nonatomic) UIBackgroundTaskIdentifier backgroundTask;
+ (TSBackgroundFetch *)sharedInstance;
-(void) didFinishLaunching;
-(void) registerAppRefreshTask;
-(void) registerBGProcessingTask:(NSString*)identifier;
-(void) configure:(NSTimeInterval)delay callback:(void(^)(UIBackgroundRefreshStatus status))callback;
-(NSError*) scheduleProcessingTaskWithIdentifier:(NSString*)identifier delay:(NSTimeInterval)delay periodic:(BOOL)periodic callback:(void (^)(NSString* taskId, BOOL timeout))callback;
-(NSError*) scheduleProcessingTaskWithIdentifier:(NSString*)identifier delay:(NSTimeInterval)delay periodic:(BOOL)periodic requiresExternalPower:(BOOL)requiresExternalPower requiresNetworkConnectivity:(BOOL)requiresNetworkConnectivity callback:(void (^)(NSString* taskId, BOOL timeout))callback;
-(void) addListener:(NSString*)componentName callback:(void (^)(NSString* componentName))callback;
-(void) addListener:(NSString*)componentName callback:(void (^)(NSString* componentName))callback timeout:(void (^)(NSString* componentName))timeout;
-(void) removeListener:(NSString*)componentName;
-(BOOL) hasListener:(NSString*)componentName;
-(NSError*) start:(NSString*)identifier;
-(void) stop:(NSString*)identifier;
-(void) finish:(NSString*)tag;
-(void) status:(void(^)(UIBackgroundRefreshStatus status))callback;
// @deprecated API
-(void) performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))handler applicationState:(UIApplicationState)state;
@end

View file

@ -0,0 +1,365 @@
//
// RNBackgroundFetchManager.m
// RNBackgroundFetch
//
// Created by Christopher Scott on 2016-08-02.
// Copyright © 2016 Christopher Scott. All rights reserved.
//
#import "TSBackgroundFetch.h"
#import "TSBGTask.h"
#import "TSBGAppRefreshSubscriber.h"
static NSString *const TAG = @"TSBackgroundFetch";
static NSString *const BACKGROUND_REFRESH_TASK_ID = @"com.transistorsoft.fetch";
static NSString *const PERMITTED_IDENTIFIERS_KEY = @"BGTaskSchedulerPermittedIdentifiers";
@implementation TSBackgroundFetch {
BOOL enabled;
NSTimeInterval minimumFetchInterval;
id bgAppRefreshTask;
BOOL fetchScheduled;
}
+ (TSBackgroundFetch *)sharedInstance
{
static TSBackgroundFetch *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[TSBGTask load];
[TSBGAppRefreshSubscriber load];
instance = [[self alloc] init];
});
return instance;
}
-(instancetype)init
{
self = [super init];
fetchScheduled = NO;
minimumFetchInterval = UIApplicationBackgroundFetchIntervalMinimum;
_fetchTaskId = BACKGROUND_REFRESH_TASK_ID;
_stopOnTerminate = YES;
_configured = NO;
_active = NO;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAppTerminate) name:UIApplicationWillTerminateNotification object:nil];
return self;
}
- (void) didFinishLaunching {
NSArray *permittedIdentifiers = [[NSBundle mainBundle] objectForInfoDictionaryKey:PERMITTED_IDENTIFIERS_KEY];
if (!permittedIdentifiers) return;
for (NSString *identifier in permittedIdentifiers) {
if ([identifier isEqualToString:BACKGROUND_REFRESH_TASK_ID]) {
[self registerAppRefreshTask];
} else {
[self registerBGProcessingTask:identifier];
}
}
}
- (void) registerAppRefreshTask {
if (@available(iOS 13.0, *)) {
[TSBGAppRefreshSubscriber registerTaskScheduler];
[[BGTaskScheduler sharedScheduler] registerForTaskWithIdentifier:BACKGROUND_REFRESH_TASK_ID usingQueue:nil launchHandler:^(BGTask* task) {
[self handleBGAppRefreshTask:(BGAppRefreshTask*)task];
}];
}
}
- (void) registerBGProcessingTask:(NSString *)identifier {
if (@available(iOS 13.0, *)) {
[TSBGTask registerForTaskWithIdentifier:identifier];
}
}
- (NSError*) scheduleBGAppRefresh {
if (fetchScheduled) return nil;
NSLog(@"[%@ scheduleBGAppRefresh] %@", TAG, BACKGROUND_REFRESH_TASK_ID);
NSError *error = nil;
if (@available (iOS 13.0, *)) {
if ([TSBGAppRefreshSubscriber useTaskScheduler]) {
BGTaskScheduler *scheduler = [BGTaskScheduler sharedScheduler];
BGAppRefreshTaskRequest *request = [[BGAppRefreshTaskRequest alloc] initWithIdentifier:BACKGROUND_REFRESH_TASK_ID];
request.earliestBeginDate = [NSDate dateWithTimeIntervalSinceNow:minimumFetchInterval];
[scheduler submitTaskRequest:request error:&error];
// Handle case for Simulator where BGTaskScheduler doesn't work.
if ((error != nil) && (error.code == BGTaskSchedulerErrorCodeUnavailable)) {
NSLog(@"[%@] BGTaskScheduler failed to register fetch-task and will fall-back to old API. This is likely due to running in the iOS Simulator (%@)", TAG, error);
[self setMinimumFetchInterval];
error = nil;
}
} else {
[self setMinimumFetchInterval];
}
} else {
[self setMinimumFetchInterval];
}
if (!error) {
fetchScheduled = YES;
}
return error;
}
-(void) cancelBGAppRefresh {
if (@available (iOS 13.0, *)) {
BGTaskScheduler *scheduler = [BGTaskScheduler sharedScheduler];
[scheduler cancelTaskRequestWithIdentifier:BACKGROUND_REFRESH_TASK_ID];
fetchScheduled = NO;
} else {
dispatch_async(dispatch_get_main_queue(), ^{
[[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalNever];
});
}
}
-(void) setMinimumFetchInterval {
__block NSTimeInterval interval = minimumFetchInterval;
dispatch_async(dispatch_get_main_queue(), ^{
[[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:interval];
});
}
/// Callback from BGTaskScheduler
-(void) handleBGAppRefreshTask:(BGAppRefreshTask*)task API_AVAILABLE(ios(13.0)) {
NSLog(@"[%@ handleBGAppRefreshTask]", TAG);
__block BGAppRefreshTask *weakTask = task;
task.expirationHandler = ^{
NSLog(@"[%@ handleBGAppRefreshTask] WARNING: expired before #finish was executed.", TAG);
// If any registered listeners has registered an onTimeout callback, let them run and execute #finish as desired. Otherwise, automatically setTaskCompleted immediately.
if (![TSBGAppRefreshSubscriber onTimeout] && weakTask) [weakTask setTaskCompletedWithSuccess:NO];
};
fetchScheduled = NO;
[self scheduleBGAppRefresh];
bgAppRefreshTask = task;
[TSBGAppRefreshSubscriber execute];
}
/// @deprecated Old-syle fetch callback.
- (void) performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))handler applicationState:(UIApplicationState)state {
NSLog(@"[%@ performFetchWithCompletionHandler]", TAG);
fetchScheduled = NO;
[self scheduleBGAppRefresh];
_completionHandler = handler;
if (_backgroundTask != UIBackgroundTaskInvalid) [[UIApplication sharedApplication] endBackgroundTask:_backgroundTask];
// Create a UIBackgroundTask for detecting task-expiration with old API.
_backgroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
if (self.completionHandler) {
[TSBGAppRefreshSubscriber onTimeout];
}
@synchronized (self) {
[[UIApplication sharedApplication] endBackgroundTask:self.backgroundTask];
self.backgroundTask = UIBackgroundTaskInvalid;
}
}];
[TSBGAppRefreshSubscriber execute];
}
-(void) status:(void(^)(UIBackgroundRefreshStatus status))callback
{
dispatch_async(dispatch_get_main_queue(), ^{
callback([[UIApplication sharedApplication] backgroundRefreshStatus]);
});
}
-(void) configure:(NSTimeInterval)delay callback:(void(^)(UIBackgroundRefreshStatus status))callback {
_configured = YES;
minimumFetchInterval = delay;
[self status:^(UIBackgroundRefreshStatus status) {
if (status == UIBackgroundRefreshStatusAvailable) {
[self scheduleBGAppRefresh];
}
callback(status);
}];
}
-(NSError*) scheduleProcessingTaskWithIdentifier:(NSString*)identifier delay:(NSTimeInterval)delay periodic:(BOOL)periodic callback:(void (^)(NSString* taskId, BOOL timeout))callback {
return [self scheduleProcessingTaskWithIdentifier:identifier delay:delay periodic:periodic requiresExternalPower:NO requiresNetworkConnectivity:NO callback:callback];
}
-(NSError*) scheduleProcessingTaskWithIdentifier:(NSString*)identifier delay:(NSTimeInterval)delay periodic:(BOOL)periodic requiresExternalPower:(BOOL)requiresExternalPower requiresNetworkConnectivity:(BOOL)requiresNetworkConnectivity callback:(void (^)(NSString* taskId, BOOL timeout))callback {
TSBGTask *tsTask = [TSBGTask get:identifier];
if (tsTask) {
tsTask.delay = delay;
tsTask.periodic = periodic;
tsTask.callback = callback;
tsTask.requiresNetworkConnectivity = requiresNetworkConnectivity;
tsTask.requiresExternalPower = requiresExternalPower;
if (@available(iOS 13.0, *)) {
if (tsTask.task && !tsTask.executed) {
[tsTask execute];
return nil;
} else {
return [tsTask schedule];
}
}
} else {
tsTask = [[TSBGTask alloc] initWithIdentifier:identifier
delay:delay
periodic:periodic
requiresExternalPower:requiresExternalPower
requiresNetworkConnectivity:requiresNetworkConnectivity
callback:callback];
tsTask.callback = callback;
}
NSError *error = [tsTask schedule];
if (error) {
NSLog(@"[%@ scheduleTask] ERROR: Failed to submit task request: %@", TAG, error);
}
return error;
}
/// @deprecated.
-(void) addListener:(NSString*)componentName callback:(void (^)(NSString* componentName))callback {
[self addListener:componentName callback:callback timeout:nil];
}
-(void) addListener:(NSString*)componentName callback:(void (^)(NSString* componentName))callback timeout:(void (^)(NSString* componentName))timeout {
TSBGAppRefreshSubscriber *subscriber = [TSBGAppRefreshSubscriber get:componentName];
if (subscriber) {
subscriber.callback = callback;
subscriber.timeout = timeout;
} else {
subscriber = [[TSBGAppRefreshSubscriber alloc] initWithIdentifier:componentName callback:callback timeout:timeout];
}
if (bgAppRefreshTask || _completionHandler) {
[subscriber execute];
}
}
-(BOOL) hasListener:(NSString*)identifier {
return ([TSBGAppRefreshSubscriber get:identifier] != nil);
}
-(void) removeListener:(NSString*)identifier {
TSBGAppRefreshSubscriber *subscriber = [TSBGAppRefreshSubscriber get:identifier];
if (!subscriber) {
NSLog(@"[%@ removeListener] WARNING: Failed to find listener for identifier: %@", TAG, identifier);
return;
}
[subscriber destroy];
if ([[TSBGAppRefreshSubscriber subscribers] count] < 1) {
[self cancelBGAppRefresh];
}
}
- (NSError*) start:(NSString*)identifier {
NSLog(@"[%@ start] %@", TAG, identifier);
if (!identifier) {
return [self scheduleBGAppRefresh];
} else {
TSBGTask *tsTask = [TSBGTask get:identifier];
if (!tsTask) {
NSString *msg = [NSString stringWithFormat:@"Could not find TSBGTask %@", identifier];
NSLog(@"[%@ start] ERROR: %@", TAG, msg);
NSError *error = [[NSError alloc] initWithDomain:TAG code:-2 userInfo:@{NSLocalizedFailureReasonErrorKey:msg}];
return error;
}
tsTask.enabled = YES;
[tsTask save];
return [tsTask schedule];
}
}
- (void) stop:(NSString*)identifier {
NSLog(@"[%@ stop] %@", TAG, identifier);
if (!identifier) {
NSArray *tsTasks = [TSBGTask tasks];
for (TSBGTask *tsTask in tsTasks) {
[tsTask stop];
}
} else {
TSBGTask *tsTask = [TSBGTask get:identifier];
[tsTask stop];
}
}
- (void) finish:(NSString*)taskId {
if (!taskId) { taskId = BACKGROUND_REFRESH_TASK_ID; }
TSBGTask *tsTask = [TSBGTask get:taskId];
// Is it a scheduled-task?
if (tsTask) {
if (@available(iOS 13.0, *)) {
[tsTask finish:YES];
}
// We're done.
return;
}
// Nope, it's a background-fetch event. We have to do subscriber-counting: when all subscribers have signalled #finish, we're done.
if (!bgAppRefreshTask && !_completionHandler) {
NSLog(@"[%@ finish] %@ Called without a task to finish. Ignoring.", TAG, taskId);
return;
}
TSBGAppRefreshSubscriber *subscriber = [TSBGAppRefreshSubscriber get:taskId];
if (subscriber) {
[subscriber finish];
NSArray *subscribers = [[TSBGAppRefreshSubscriber subscribers] allValues];
long total = [subscribers count];
long count = 0;
for (TSBGAppRefreshSubscriber *subscriber in subscribers) {
if (subscriber.finished) count++;
}
NSLog(@"[%@ finish] %@ (%ld of %ld)", TAG, subscriber.identifier, count, total);
if (total != count) return;
// If we arrive here without jumping out of foreach above, all subscribers are finished.
if (bgAppRefreshTask) {
// If we arrive here, all Fetch tasks must be finished.
if (@available(iOS 13.0, *)) {
[(BGAppRefreshTask*) bgAppRefreshTask setTaskCompletedWithSuccess:YES];
}
bgAppRefreshTask = nil;
} else if (_completionHandler) {
_completionHandler(UIBackgroundFetchResultNewData);
_completionHandler = nil;
@synchronized (self) {
if (_backgroundTask != UIBackgroundTaskInvalid) {
[[UIApplication sharedApplication] endBackgroundTask:_backgroundTask];
_backgroundTask = UIBackgroundTaskInvalid;
}
}
}
} else {
NSLog(@"[%@ finish] Failed to find Fetch subscriber %@", TAG, taskId);
}
}
- (void) onAppTerminate {
NSLog(@"[%@ onAppTerminate]", TAG);
if (_stopOnTerminate) {
//[self stop];
}
}
- (void) dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
@end

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>

View file

@ -0,0 +1,39 @@
//
// TSBackgroundFetchTests.m
// TSBackgroundFetchTests
//
// Created by Christopher Scott on 2016-08-03.
// Copyright © 2016 Christopher Scott. All rights reserved.
//
#import <XCTest/XCTest.h>
@interface TSBackgroundFetchTests : XCTestCase
@end
@implementation TSBackgroundFetchTests
- (void)setUp {
[super setUp];
// Put setup code here. This method is called before the invocation of each test method in the class.
}
- (void)tearDown {
// Put teardown code here. This method is called after the invocation of each test method in the class.
[super tearDown];
}
- (void)testExample {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
- (void)testPerformanceExample {
// This is an example of a performance test case.
[self measureBlock:^{
// Put the code you want to measure the time of here.
}];
}
@end