Compare commits

..

1 Commits

Author SHA1 Message Date
Neeraj Gupta
6146350aae [docs] Update custom server section for cli 2024-03-13 12:02:36 +05:30
69 changed files with 2118 additions and 102 deletions

View File

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

7
.gitmodules vendored
View File

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

View File

@@ -1 +0,0 @@
{}

View File

@@ -144,7 +144,6 @@
"enterCodeHint": "Geben Sie den 6-stelligen Code \naus Ihrer Authentifikator-App ein.",
"lostDeviceTitle": "Gerät verloren?",
"twoFactorAuthTitle": "Zwei-Faktor-Authentifizierung",
"passkeyAuthTitle": "Passkey Authentifizierung",
"recoverAccount": "Konto wiederherstellen",
"enterRecoveryKeyHint": "Geben Sie Ihren Wiederherstellungsschlüssel ein",
"recover": "Wiederherstellen",
@@ -405,15 +404,5 @@
"signOutOtherDevices": "Andere Geräte abmelden",
"doNotSignOut": "Nicht abmelden",
"hearUsWhereTitle": "Wie hast du von Ente erfahren? (optional)",
"hearUsExplanation": "Wir tracken keine App-Installationen. Es würde uns jedoch helfen, wenn du uns mitteilst, wie du von uns erfahren hast!",
"waitingForBrowserRequest": "Warten auf Browseranfrage...",
"launchPasskeyUrlAgain": "Passwort-URL erneut starten",
"passkey": "Passkey",
"developerSettingsWarning": "Sind Sie sicher, dass Sie die Entwicklereinstellungen ändern möchten?",
"developerSettings": "Entwicklereinstellungen",
"serverEndpoint": "Server Endpunkt",
"invalidEndpoint": "Ungültiger Endpunkt",
"invalidEndpointMessage": "Der eingegebene Endpunkt ist ungültig. Bitte geben Sie einen gültigen Endpunkt ein und versuchen Sie es erneut.",
"endpointUpdatedMessage": "Endpunkt erfolgreich aktualisiert",
"customEndpoint": "Mit {endpoint} verbunden"
"hearUsExplanation": "Wir tracken keine App-Installationen. Es würde uns jedoch helfen, wenn du uns mitteilst, wie du von uns erfahren hast!"
}

View File

@@ -144,7 +144,6 @@
"enterCodeHint": "認証アプリに表示された 6 桁のコードを入力してください",
"lostDeviceTitle": "デバイスを紛失しましたか?",
"twoFactorAuthTitle": "2 要素認証",
"passkeyAuthTitle": "パスキー認証",
"recoverAccount": "アカウントを回復",
"enterRecoveryKeyHint": "回復キーを入力",
"recover": "回復",
@@ -405,15 +404,5 @@
"signOutOtherDevices": "他のデバイスからサインアウトする",
"doNotSignOut": "サインアウトしない",
"hearUsWhereTitle": "Ente についてどのようにお聞きになりましたか?(任意)",
"hearUsExplanation": "私たちはアプリのインストールを追跡していません。私たちをお知りになった場所を教えてください!",
"waitingForBrowserRequest": "ブラウザのリクエストを待っています...",
"launchPasskeyUrlAgain": "パスキーのURLを再度起動する",
"passkey": "パスキー",
"developerSettingsWarning": "開発者向け設定を変更してもよろしいですか?",
"developerSettings": "開発者向け設定",
"serverEndpoint": "サーバーエンドポイント",
"invalidEndpoint": "無効なエンドポイントです",
"invalidEndpointMessage": "入力されたエンドポイントは無効です。有効なエンドポイントを入力して再試行してください。",
"endpointUpdatedMessage": "エンドポイントの更新に成功しました",
"customEndpoint": "{endpoint} に接続しました"
"hearUsExplanation": "私たちはアプリのインストールを追跡していません。私たちをお知りになった場所を教えてください!"
}

View File

@@ -144,7 +144,6 @@
"enterCodeHint": "Digite o código de 6 dígitos de\nseu aplicativo autenticador",
"lostDeviceTitle": "Perdeu seu dispositivo?",
"twoFactorAuthTitle": "Autenticação de dois fatores",
"passkeyAuthTitle": "Autenticação via Chave de acesso",
"recoverAccount": "Recuperar conta",
"enterRecoveryKeyHint": "Digite sua chave de recuperação",
"recover": "Recuperar",
@@ -405,15 +404,5 @@
"signOutOtherDevices": "Terminar sessão em outros dispositivos",
"doNotSignOut": "Não encerrar sessão",
"hearUsWhereTitle": "Como você ouviu sobre o Ente? (opcional)",
"hearUsExplanation": "Não rastreamos instalações do aplicativo. Seria útil se você nos contasse onde nos encontrou!",
"waitingForBrowserRequest": "Aguardando solicitação do navegador...",
"launchPasskeyUrlAgain": "Iniciar a URL de chave de acesso novamente",
"passkey": "Chave de acesso",
"developerSettingsWarning": "Tem certeza de que deseja modificar as configurações de Desenvolvedor?",
"developerSettings": "Configurações de desenvolvedor",
"serverEndpoint": "Endpoint do servidor",
"invalidEndpoint": "Endpoint inválido",
"invalidEndpointMessage": "Desculpe, o endpoint que você inseriu é inválido. Por favor, insira um endpoint válido e tente novamente.",
"endpointUpdatedMessage": "Endpoint atualizado com sucesso",
"customEndpoint": "Conectado a {endpoint}"
"hearUsExplanation": "Não rastreamos instalações do aplicativo. Seria útil se você nos contasse onde nos encontrou!"
}

View File

@@ -131,16 +131,6 @@
"about": "Om",
"terms": "Villkor",
"warning": "Varning",
"importSuccessDesc": "Du har importerat {count} koder!",
"@importSuccessDesc": {
"placeholders": {
"count": {
"description": "The number of codes imported",
"type": "int",
"example": "1"
}
}
},
"pendingSyncs": "Varning",
"activeSessions": "Aktiva sessioner",
"enterPassword": "Ange lösenord",
@@ -153,7 +143,5 @@
"iOSOkButton": "OK",
"@iOSOkButton": {
"description": "Message showed on a button that the user can click to leave the current dialog. It is used on iOS side. Maximum 30 characters."
},
"noInternetConnection": "Ingen internetanslutning",
"pleaseCheckYourInternetConnectionAndTryAgain": "Kontrollera din internetanslutning och försök igen."
}
}

View File

@@ -408,12 +408,5 @@
"hearUsExplanation": "我们不跟踪应用程序安装情况。如果您告诉我们您是在哪里找到我们的,将会有所帮助!",
"waitingForBrowserRequest": "正在等待浏览器请求...",
"launchPasskeyUrlAgain": "再次启动 通行密钥 URL",
"passkey": "通行密钥",
"developerSettingsWarning": "您确定要修改开发者设置吗?",
"developerSettings": "开发者设置",
"serverEndpoint": "服务器端点",
"invalidEndpoint": "端点无效",
"invalidEndpointMessage": "抱歉,您输入的端点无效。请输入有效的端点,然后重试。",
"endpointUpdatedMessage": "端点更新成功",
"customEndpoint": "已连接至 {endpoint}"
"passkey": "通行密钥"
}

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
ente é um aplicativo simples para fazer backup e compartilhar suas fotos e vídeos.
Se você está procurando uma alternativa ao Google Fotos com foco em privacidade, você veio ao lugar certo. Com ente, eles são armazenados com criptografia de ponta a ponta (e2ee). Isso significa que só você pode vê-los.
Se você está procurando uma alternativa ao Google Photos com foco em privacidade, veio ao lugar certo. Com ente, eles são armazenados com criptografia de ponta a ponta (e2ee). Isso significa que só você pode vê-los.
Temos aplicativos de código aberto em todas as plataformas, Android, iOS, web e desktop, e suas fotos irão sincronizar perfeitamente entre todas elas de forma criptografada (e2ee).

View File

@@ -1,12 +1,12 @@
ente 是一个简单的应用程序来备份和分享您的照片和视频。
如果你一直在寻找一个隐私友好的Google Photos替代品那么你就来对地方了。 使用 Ente它们以端到端加密 (e2ee) 的方式存储。 这意味着只有您可以查看它们。 使用 Ente它们以端到端加密 (e2ee) 的方式存储。 这意味着只有您可以查看它们。
如果你一直在寻找一个隐私友好的Google Photos替代品那么你就来对地方了。 使用 Ente它们以端到端加密 (e2ee) 的方式存储。 这意味着只有您可以查看它们。
我们在Android、iOS、web 和桌面上有开源应用, 和您的照片将以端到端加密方式 (e2ee) 无缝同步。
ente也使分享相册给自己的爱人、亲人变得轻而易举即使他们可能并不使用ente。 您可以分享可公开查看的链接使他们可以查看您的相册并通过添加照片来协作而不需要注册账户或下载app。 权限
ente也使分享相册给自己的爱人、亲人变得轻而易举即使他们可能并不使用ente。 您可以分享可公开查看的链接使他们可以查看您的相册并通过添加照片来协作而不需要注册账户或下载app。
您的加密数据已复制到三个不同的地点,包括巴黎的一个安全屋。 我们认真对待子孙后代,并确保您的回忆比您长寿。 我们认真对待子孙后代,并确保您的回忆比您长寿。
您的加密数据已复制到三个不同的地点,包括巴黎的一个安全屋。 我们认真对待子孙后代,并确保您的回忆比您长寿。
我们来这里是为了打造有史以来最安全的照片应用,来和我们一起前行!
@@ -30,7 +30,7 @@ ente也使分享相册给自己的爱人、亲人变得轻而易举即使他
ente需要特定权限以执行作为图像存储提供商的职责相关内容可以在此链接查阅https://github.com/ente-io/photos-app/blob/f-droid/android/permissions.md
价格
我们不会提供永久免费计划,因为我们必须保持可持续性,经受住时间的考验。 相反,我们向您提供了价格实惠、可自由分享的订阅计划。 您可以在 ente.io 找到更多信息。 相反,我们向您提供了价格实惠、可自由分享的订阅计划。 您可以在 ente.io 找到更多信息。
我们不会提供永久免费计划,因为我们必须保持可持续性,经受住时间的考验。 相反,我们向您提供了价格实惠、可自由分享的订阅计划。 您可以在 ente.io 找到更多信息。
支持
我们对提供真人支持感到自豪。 我们对提供真人支持感到自豪。 如果您是我们的付费客户,您可以联系 team@ente.io 并在24小时内收到来自我们团队的回复。
我们对提供真人支持感到自豪。 如果您是我们的付费客户,您可以联系 team@ente.io 并在24小时内收到来自我们团队的回复。

View File

@@ -1,6 +1,6 @@
Ente é um aplicativo simples para fazer backup e compartilhar suas fotos e vídeos.
Se você esteve procurando uma alternativa amigável à privacidade para preservar suas memórias, você veio ao lugar certo. Com Ente, elas são armazenadas com criptografia de ponta a ponta (e2ee). Isso significa que só você pode vê-las.
Se você esteve procurando uma alternativa amigável à privacidade para preservar suas memórias, você veio ao lugar certo. Com ente, eles são armazenados com criptografia de ponta a ponta (e2ee). Isso significa que só você pode vê-los.
Temos aplicativos de código aberto em Android, iOS, web e desktop, e suas fotos irão sincronizar perfeitamente entre todas elas de forma criptografada (e2ee).

View File

@@ -1,6 +1,6 @@
Ente é um aplicativo simples para fazer backup e compartilhar suas fotos e vídeos.
Se você esteve procurando uma alternativa amigável à privacidade para preservar suas memórias, você veio ao lugar certo. Com Ente, elas são armazenadas com criptografia de ponta a ponta (e2ee). Isso significa que só você pode vê-las.
Se você esteve procurando uma alternativa amigável à privacidade para preservar suas memórias, você veio ao lugar certo. Com ente, eles são armazenados com criptografados de ponta a ponta (e2ee). Isso significa que só você pode vê-los.
Temos aplicativos de código aberto em todas as plataformas, Android, iOS, web e desktop, e suas fotos irão sincronizar perfeitamente entre todas elas de forma criptografada (e2ee).

View File

@@ -406,15 +406,6 @@
},
"photoGridSize": "Fotorastergröße",
"manageDeviceStorage": "Gerätespeicher verwalten",
"machineLearning": "Maschinelles Lernen",
"magicSearch": "Magische Suche",
"magicSearchDescription": "Bitte beachten Sie, dass dies mehr Bandbreite nutzt und zu einem höheren Akkuverbrauch führt, bis alle Elemente indiziert sind.",
"loadingModel": "Lade Modelle herunter...",
"waitingForWifi": "Warte auf WLAN...",
"status": "Status",
"indexedItems": "Indizierte Elemente",
"pendingItems": "Ausstehende Elemente",
"clearIndexes": "Indexe löschen",
"selectFoldersForBackup": "Ordner für Sicherung auswählen",
"selectedFoldersWillBeEncryptedAndBackedUp": "Ausgewählte Ordner werden verschlüsselt und gesichert",
"unselectAll": "Alle demarkieren",
@@ -1187,17 +1178,7 @@
"changeLocationOfSelectedItems": "Standort der gewählten Elemente ändern?",
"editsToLocationWillOnlyBeSeenWithinEnte": "Änderungen des Standorts werden nur in ente sichtbar sein",
"cleanUncategorized": "Unkategorisiert leeren",
"cleanUncategorizedDescription": "Entferne alle Dateien von \"Unkategorisiert\" die in anderen Alben vorhanden sind",
"waitingForVerification": "Warte auf Bestätigung...",
"passkey": "Passkey",
"passkeyAuthTitle": "Passkey-Verifizierung",
"verifyPasskey": "Passkey verifizieren",
"playOnTv": "Album auf dem Fernseher wiedergeben",
"pair": "Koppeln",
"deviceNotFound": "Gerät nicht gefunden",
"castInstruction": "Besuche cast.ente.io auf dem Gerät, das du verbinden möchtest.\n\nGib den unten angegebenen Code ein, um das Album auf deinem Fernseher abzuspielen.",
"deviceCodeHint": "Code eingeben",
"joinDiscord": "Discord beitreten",
"locations": "Orte",
"descriptions": "Beschreibungen"
"joinDiscord": "Join Discord",
"locations": "Locations",
"descriptions": "Descriptions"
}

View File

@@ -1187,17 +1187,16 @@
"changeLocationOfSelectedItems": "Alterar o local dos itens selecionados?",
"editsToLocationWillOnlyBeSeenWithinEnte": "Edições para local só serão vistas dentro do Ente",
"cleanUncategorized": "Limpar Sem Categoria",
"cleanUncategorizedDescription": "Remover todos os arquivos de Não Categorizados que estão presentes em outros álbuns",
"waitingForVerification": "Esperando por verificação...",
"waitingForBrowserRequest": "Aguardando solicitação do navegador...",
"launchPasskeyUrlAgain": "Iniciar a URL de chave de acesso novamente",
"passkey": "Chave de acesso",
"passkeyAuthTitle": "Autenticação via Chave de acesso",
"verifyPasskey": "Verificar chave de acesso",
"playOnTv": "Reproduzir álbum na TV",
"pair": "Parear",
"deviceNotFound": "Dispositivo não encontrado",
"castInstruction": "Visite cast.ente.io no dispositivo que você deseja parear.\n\ndigite o código abaixo para reproduzir o álbum em sua TV.",
"deviceCodeHint": "Insira o código",
"joinDiscord": "Junte-se ao Discord",
"locations": "Locais",
"descriptions": "Descrições"
"locations": "Locations",
"descriptions": "Descriptions"
}

View File

@@ -1187,17 +1187,16 @@
"changeLocationOfSelectedItems": "确定要更改所选项目的位置吗?",
"editsToLocationWillOnlyBeSeenWithinEnte": "对位置的编辑只能在 Ente 内看到",
"cleanUncategorized": "清除未分类的",
"cleanUncategorizedDescription": "从“未分类”中删除其他相册中存在的所有文件",
"waitingForVerification": "等待验证...",
"waitingForBrowserRequest": "正在等待浏览器请求...",
"launchPasskeyUrlAgain": "再次启动 通行密钥 URL",
"passkey": "通行密钥",
"passkeyAuthTitle": "通行密钥认证",
"verifyPasskey": "验证通行密钥",
"playOnTv": "在电视上播放相册",
"pair": "配对",
"deviceNotFound": "未发现设备",
"castInstruction": "在您要配对的设备上访问 cast.ente.io。\n输入下面的代码即可在电视上播放相册。",
"deviceCodeHint": "输入代码",
"joinDiscord": "加入 Discord",
"locations": "位置",
"descriptions": "描述"
"locations": "Locations",
"descriptions": "Descriptions"
}

View File

@@ -68,7 +68,6 @@ class GalleryFileWidget extends StatelessWidget {
: _onLongPressNoSelectionLimit(context, file);
},
child: Stack(
clipBehavior: Clip.none,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(1),

View File

@@ -12,7 +12,7 @@ description: ente photos application
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.8.70+590
version: 0.8.68+588
publish_to: none
environment:

1
mobile/thirdparty/flutter vendored Submodule

1
mobile/thirdparty/isar vendored Submodule

Submodule mobile/thirdparty/isar added at 6643d064ab

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.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 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.0 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 @@
#Thu Feb 09 18:40:48 IST 2023
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
distributionSha256Sum=10065868c78f1207afb3a92176f99a37d753a513dff453abb6b5cceda4058cda

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,136 @@
apply plugin: 'com.android.library'
apply plugin: 'maven'
apply plugin: 'maven-publish'
android {
compileSdkVersion rootProject.compileSdkVersion
defaultConfig {
minSdkVersion 16
targetSdkVersion rootProject.targetSdkVersion
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
publishing {
publications {
tslocationmanager(MavenPublication) {
groupId 'com.transistorsoft'
artifactId 'tsbackgroundfetch'
version VERSION_NAME
artifact("$buildDir/outputs/aar/tsbackgroundfetch-release.aar")
}
}
repositories {
mavenLocal()
}
}
}
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);
}
}