Compare commits
28 Commits
update_doc
...
fdroid-v0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e1633bb45 | ||
|
|
ba5686a07a | ||
|
|
1cacefa1fd | ||
|
|
c0f33de0c8 | ||
|
|
72719d2234 | ||
|
|
77276d8d6c | ||
|
|
c6c7b0ab32 | ||
|
|
df316463ef | ||
|
|
be9af355ce | ||
|
|
2faef37f4b | ||
|
|
417621b17c | ||
|
|
8322540732 | ||
|
|
2d61be37bb | ||
|
|
83aa3db795 | ||
|
|
5db0da9aaf | ||
|
|
3abc7249bd | ||
|
|
2a10aa7d61 | ||
|
|
6ef1da68e0 | ||
|
|
1c1c9bb0d7 | ||
|
|
b96e7341e3 | ||
|
|
163c5de1cc | ||
|
|
124ef86054 | ||
|
|
004eb310b3 | ||
|
|
ccb6a4a283 | ||
|
|
a3c80556d2 | ||
|
|
851ce5de73 | ||
|
|
f8d956d47f | ||
|
|
7543dc6b57 |
2
.github/workflows/mobile-lint.yml
vendored
2
.github/workflows/mobile-lint.yml
vendored
@@ -3,7 +3,7 @@ name: "Lint (mobile)"
|
||||
on:
|
||||
# Run on every push to a branch other than main that changes mobile/
|
||||
push:
|
||||
branches-ignore: [main]
|
||||
branches-ignore: [main, f-droid]
|
||||
paths:
|
||||
- "mobile/**"
|
||||
- ".github/workflows/mobile-lint.yml"
|
||||
|
||||
2
.github/workflows/mobile-release.yml
vendored
2
.github/workflows/mobile-release.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD_PHOTOS }}
|
||||
|
||||
- name: Checksum
|
||||
run: sha256sum build/app/outputs/flutter-apk/ente.apk > build/app/outputs/flutter-apk/sha256sum
|
||||
run: sha256sum build/app/outputs/flutter-apk/ente-${{ github.ref_name }}.apk > build/app/outputs/flutter-apk/sha256sum
|
||||
|
||||
- name: Create a draft GitHub release
|
||||
uses: ncipollo/release-action@v1
|
||||
|
||||
13
.gitmodules
vendored
13
.gitmodules
vendored
@@ -9,16 +9,9 @@
|
||||
[submodule "auth/assets/simple-icons"]
|
||||
path = auth/assets/simple-icons
|
||||
url = https://github.com/simple-icons/simple-icons.git
|
||||
[submodule "mobile/thirdparty/flutter"]
|
||||
path = mobile/thirdparty/flutter
|
||||
url = https://github.com/flutter/flutter.git
|
||||
branch = stable
|
||||
[submodule "mobile/plugins/clip_ggml"]
|
||||
path = mobile/plugins/clip_ggml
|
||||
url = https://github.com/ente-io/clip-ggml.git
|
||||
[submodule "mobile/thirdparty/isar"]
|
||||
path = mobile/thirdparty/isar
|
||||
url = https://github.com/isar/isar
|
||||
[submodule "web/apps/photos/thirdparty/ffmpeg-wasm"]
|
||||
path = web/apps/photos/thirdparty/ffmpeg-wasm
|
||||
url = https://github.com/abhinavkgrd/ffmpeg.wasm.git
|
||||
@@ -27,3 +20,9 @@
|
||||
path = web/apps/photos/thirdparty/photoswipe
|
||||
url = https://github.com/ente-io/PhotoSwipe.git
|
||||
branch = single-thread
|
||||
[submodule "mobile/thirdparty/isar"]
|
||||
path = mobile/thirdparty/isar
|
||||
url = https://github.com/isar/isar
|
||||
[submodule "mobile/thirdparty/flutter"]
|
||||
path = mobile/thirdparty/flutter
|
||||
url = https://github.com/flutter/flutter
|
||||
|
||||
1
auth/lib/l10n/arb/app_bg.arb
Normal file
1
auth/lib/l10n/arb/app_bg.arb
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -144,6 +144,7 @@
|
||||
"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",
|
||||
@@ -404,5 +405,15 @@
|
||||
"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!"
|
||||
"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"
|
||||
}
|
||||
@@ -144,6 +144,7 @@
|
||||
"enterCodeHint": "認証アプリに表示された 6 桁のコードを入力してください",
|
||||
"lostDeviceTitle": "デバイスを紛失しましたか?",
|
||||
"twoFactorAuthTitle": "2 要素認証",
|
||||
"passkeyAuthTitle": "パスキー認証",
|
||||
"recoverAccount": "アカウントを回復",
|
||||
"enterRecoveryKeyHint": "回復キーを入力",
|
||||
"recover": "回復",
|
||||
@@ -404,5 +405,15 @@
|
||||
"signOutOtherDevices": "他のデバイスからサインアウトする",
|
||||
"doNotSignOut": "サインアウトしない",
|
||||
"hearUsWhereTitle": "Ente についてどのようにお聞きになりましたか?(任意)",
|
||||
"hearUsExplanation": "私たちはアプリのインストールを追跡していません。私たちをお知りになった場所を教えてください!"
|
||||
"hearUsExplanation": "私たちはアプリのインストールを追跡していません。私たちをお知りになった場所を教えてください!",
|
||||
"waitingForBrowserRequest": "ブラウザのリクエストを待っています...",
|
||||
"launchPasskeyUrlAgain": "パスキーのURLを再度起動する",
|
||||
"passkey": "パスキー",
|
||||
"developerSettingsWarning": "開発者向け設定を変更してもよろしいですか?",
|
||||
"developerSettings": "開発者向け設定",
|
||||
"serverEndpoint": "サーバーエンドポイント",
|
||||
"invalidEndpoint": "無効なエンドポイントです",
|
||||
"invalidEndpointMessage": "入力されたエンドポイントは無効です。有効なエンドポイントを入力して再試行してください。",
|
||||
"endpointUpdatedMessage": "エンドポイントの更新に成功しました",
|
||||
"customEndpoint": "{endpoint} に接続しました"
|
||||
}
|
||||
@@ -144,6 +144,7 @@
|
||||
"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",
|
||||
@@ -404,5 +405,15 @@
|
||||
"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!"
|
||||
"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}"
|
||||
}
|
||||
@@ -131,6 +131,16 @@
|
||||
"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",
|
||||
@@ -143,5 +153,7 @@
|
||||
"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."
|
||||
}
|
||||
@@ -408,5 +408,12 @@
|
||||
"hearUsExplanation": "我们不跟踪应用程序安装情况。如果您告诉我们您是在哪里找到我们的,将会有所帮助!",
|
||||
"waitingForBrowserRequest": "正在等待浏览器请求...",
|
||||
"launchPasskeyUrlAgain": "再次启动 通行密钥 URL",
|
||||
"passkey": "通行密钥"
|
||||
"passkey": "通行密钥",
|
||||
"developerSettingsWarning": "您确定要修改开发者设置吗?",
|
||||
"developerSettings": "开发者设置",
|
||||
"serverEndpoint": "服务器端点",
|
||||
"invalidEndpoint": "端点无效",
|
||||
"invalidEndpointMessage": "抱歉,您输入的端点无效。请输入有效的端点,然后重试。",
|
||||
"endpointUpdatedMessage": "端点更新成功",
|
||||
"customEndpoint": "已连接至 {endpoint}"
|
||||
}
|
||||
@@ -45,8 +45,7 @@ You can alternatively install the build from PlayStore or F-Droid.
|
||||
|
||||
## 🧑💻 Building from source
|
||||
|
||||
1. [Install Flutter v3.13.4](https://flutter.dev/docs/get-started/install) or
|
||||
set the Path of Flutter SDK to `thirdparty/flutter/bin`.
|
||||
1. [Install Flutter v3.13.4](https://flutter.dev/docs/get-started/install).
|
||||
|
||||
2. Pull in all submodules with `git submodule update --init --recursive`
|
||||
|
||||
|
||||
@@ -18,10 +18,7 @@ allprojects {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
// mavenLocal() // for FDroid
|
||||
maven {
|
||||
url "${project(':background_fetch').projectDir}/libs"
|
||||
}
|
||||
mavenLocal() // for FDroid
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"dart.flutterSdkPath": "thirdparty/flutter/bin"
|
||||
}
|
||||
@@ -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 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.
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
ente 是一个简单的应用程序来备份和分享您的照片和视频。
|
||||
|
||||
如果你一直在寻找一个隐私友好的Google Photos替代品,那么你就来对地方了。 使用 Ente,它们以端到端加密 (e2ee) 的方式存储。 这意味着只有您可以查看它们。
|
||||
如果你一直在寻找一个隐私友好的Google Photos替代品,那么你就来对地方了。 使用 Ente,它们以端到端加密 (e2ee) 的方式存储。 这意味着只有您可以查看它们。 使用 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小时内收到来自我们团队的回复。
|
||||
|
||||
@@ -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, eles são armazenados com criptografia de ponta a ponta (e2ee). Isso significa que só você pode vê-los.
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
|
||||
@@ -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, eles são armazenados com criptografados de ponta a ponta (e2ee). Isso significa que só você pode vê-los.
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
|
||||
@@ -406,6 +406,15 @@
|
||||
},
|
||||
"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",
|
||||
@@ -1178,7 +1187,17 @@
|
||||
"changeLocationOfSelectedItems": "Standort der gewählten Elemente ändern?",
|
||||
"editsToLocationWillOnlyBeSeenWithinEnte": "Änderungen des Standorts werden nur in ente sichtbar sein",
|
||||
"cleanUncategorized": "Unkategorisiert leeren",
|
||||
"joinDiscord": "Join Discord",
|
||||
"locations": "Locations",
|
||||
"descriptions": "Descriptions"
|
||||
"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"
|
||||
}
|
||||
@@ -1187,16 +1187,17 @@
|
||||
"changeLocationOfSelectedItems": "Alterar o local dos itens selecionados?",
|
||||
"editsToLocationWillOnlyBeSeenWithinEnte": "Edições para local só serão vistas dentro do Ente",
|
||||
"cleanUncategorized": "Limpar Sem Categoria",
|
||||
"waitingForBrowserRequest": "Aguardando solicitação do navegador...",
|
||||
"launchPasskeyUrlAgain": "Iniciar a URL de chave de acesso novamente",
|
||||
"cleanUncategorizedDescription": "Remover todos os arquivos de Não Categorizados que estão presentes em outros álbuns",
|
||||
"waitingForVerification": "Esperando por verificação...",
|
||||
"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": "Locations",
|
||||
"descriptions": "Descriptions"
|
||||
"locations": "Locais",
|
||||
"descriptions": "Descrições"
|
||||
}
|
||||
@@ -1187,16 +1187,17 @@
|
||||
"changeLocationOfSelectedItems": "确定要更改所选项目的位置吗?",
|
||||
"editsToLocationWillOnlyBeSeenWithinEnte": "对位置的编辑只能在 Ente 内看到",
|
||||
"cleanUncategorized": "清除未分类的",
|
||||
"waitingForBrowserRequest": "正在等待浏览器请求...",
|
||||
"launchPasskeyUrlAgain": "再次启动 通行密钥 URL",
|
||||
"cleanUncategorizedDescription": "从“未分类”中删除其他相册中存在的所有文件",
|
||||
"waitingForVerification": "等待验证...",
|
||||
"passkey": "通行密钥",
|
||||
"passkeyAuthTitle": "通行密钥认证",
|
||||
"verifyPasskey": "验证通行密钥",
|
||||
"playOnTv": "在电视上播放相册",
|
||||
"pair": "配对",
|
||||
"deviceNotFound": "未发现设备",
|
||||
"castInstruction": "在您要配对的设备上访问 cast.ente.io。\n输入下面的代码即可在电视上播放相册。",
|
||||
"deviceCodeHint": "输入代码",
|
||||
"joinDiscord": "加入 Discord",
|
||||
"locations": "Locations",
|
||||
"descriptions": "Descriptions"
|
||||
"locations": "位置",
|
||||
"descriptions": "描述"
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import 'dart:io';
|
||||
import "package:adaptive_theme/adaptive_theme.dart";
|
||||
import 'package:background_fetch/background_fetch.dart';
|
||||
import "package:computer/computer.dart";
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:flutter/rendering.dart";
|
||||
@@ -34,7 +33,6 @@ import "package:photos/services/location_service.dart";
|
||||
import "package:photos/services/machine_learning/machine_learning_controller.dart";
|
||||
import 'package:photos/services/machine_learning/semantic_search/semantic_search_service.dart';
|
||||
import 'package:photos/services/memories_service.dart';
|
||||
import 'package:photos/services/push_service.dart';
|
||||
import 'package:photos/services/remote_sync_service.dart';
|
||||
import 'package:photos/services/search_service.dart';
|
||||
import "package:photos/services/storage_bonus_service.dart";
|
||||
@@ -216,14 +214,6 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
|
||||
unawaited(HomeWidgetService.instance.initHomeWidget());
|
||||
}
|
||||
|
||||
if (Platform.isIOS) {
|
||||
// ignore: unawaited_futures
|
||||
PushService.instance.init().then((_) {
|
||||
FirebaseMessaging.onBackgroundMessage(
|
||||
_firebaseMessagingBackgroundHandler,
|
||||
);
|
||||
});
|
||||
}
|
||||
unawaited(FeatureFlagService.instance.init());
|
||||
unawaited(SemanticSearchService.instance.init());
|
||||
MachineLearningController.instance.init();
|
||||
@@ -334,35 +324,6 @@ Future<void> _killBGTask([String? taskId]) async {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||
final bool isRunningInFG = await _isRunningInForeground(); // hb
|
||||
final bool isInForeground = AppLifecycleService.instance.isForeground;
|
||||
if (_isProcessRunning) {
|
||||
_logger.info(
|
||||
"Background push received when app is alive and runningInFS: $isRunningInFG inForeground: $isInForeground",
|
||||
);
|
||||
if (PushService.shouldSync(message)) {
|
||||
await _sync('firebaseBgSyncActiveProcess');
|
||||
}
|
||||
} else {
|
||||
// App is dead
|
||||
// ignore: unawaited_futures
|
||||
_runWithLogs(
|
||||
() async {
|
||||
_logger.info("Background push received");
|
||||
if (Platform.isIOS) {
|
||||
_scheduleSuicide(kBGPushTimeout); // To prevent OS from punishing us
|
||||
}
|
||||
await _init(true, via: 'firebasePush');
|
||||
if (PushService.shouldSync(message)) {
|
||||
await _sync('firebaseBgSyncNoActiveProcess');
|
||||
}
|
||||
},
|
||||
prefix: "[fbg]",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _logFGHeartBeatInfo() async {
|
||||
final bool isRunningInFG = await _isRunningInForeground();
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
// import 'package:flutter/foundation.dart';
|
||||
// import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/core/errors.dart';
|
||||
import 'package:photos/core/network/network.dart';
|
||||
@@ -35,6 +34,7 @@ class BillingService {
|
||||
final _logger = Logger("BillingService");
|
||||
final _enteDio = NetworkClient.instance.enteDio;
|
||||
|
||||
// ignore: unused_field
|
||||
bool _isOnSubscriptionPage = false;
|
||||
|
||||
Future<BillingPlans>? _future;
|
||||
@@ -44,23 +44,6 @@ class BillingService {
|
||||
// await FlutterInappPurchase.instance.initConnection;
|
||||
// FlutterInappPurchase.instance.clearTransactionIOS();
|
||||
// }
|
||||
InAppPurchase.instance.purchaseStream.listen((purchases) {
|
||||
if (_isOnSubscriptionPage) {
|
||||
return;
|
||||
}
|
||||
for (final purchase in purchases) {
|
||||
if (purchase.status == PurchaseStatus.purchased) {
|
||||
verifySubscription(
|
||||
purchase.productID,
|
||||
purchase.verificationData.serverVerificationData,
|
||||
).then((response) {
|
||||
InAppPurchase.instance.completePurchase(purchase);
|
||||
});
|
||||
} else if (Platform.isIOS && purchase.pendingCompletePurchase) {
|
||||
InAppPurchase.instance.completePurchase(purchase);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void clearCache() {
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/core/constants.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/core/network/network.dart';
|
||||
import 'package:photos/events/signed_in_event.dart';
|
||||
import 'package:photos/services/sync_service.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class PushService {
|
||||
static const kFCMPushToken = "fcm_push_token";
|
||||
static const kLastFCMTokenUpdationTime = "fcm_push_token_updation_time";
|
||||
static const kFCMTokenUpdationIntervalInMicroSeconds = 30 * microSecondsInDay;
|
||||
static const kPushAction = "action";
|
||||
static const kSync = "sync";
|
||||
|
||||
static final PushService instance = PushService._privateConstructor();
|
||||
static final _logger = Logger("PushService");
|
||||
|
||||
late SharedPreferences _prefs;
|
||||
|
||||
PushService._privateConstructor();
|
||||
|
||||
Future<void> init() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
await Firebase.initializeApp();
|
||||
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
|
||||
_logger.info("Got a message whilst in the foreground!");
|
||||
_handleForegroundPushMessage(message);
|
||||
});
|
||||
try {
|
||||
if (Configuration.instance.hasConfiguredAccount()) {
|
||||
await _configurePushToken();
|
||||
} else {
|
||||
Bus.instance.on<SignedInEvent>().listen((_) async {
|
||||
// ignore: unawaited_futures
|
||||
_configurePushToken();
|
||||
});
|
||||
}
|
||||
} catch (e, s) {
|
||||
_logger.severe("Could not configure push token", e, s);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _configurePushToken() async {
|
||||
final String? fcmToken = await FirebaseMessaging.instance.getToken();
|
||||
final shouldForceRefreshServerToken =
|
||||
DateTime.now().microsecondsSinceEpoch -
|
||||
(_prefs.getInt(kLastFCMTokenUpdationTime) ?? 0) >
|
||||
kFCMTokenUpdationIntervalInMicroSeconds;
|
||||
if (fcmToken != null &&
|
||||
(_prefs.getString(kFCMPushToken) != fcmToken ||
|
||||
shouldForceRefreshServerToken)) {
|
||||
final String? apnsToken = await FirebaseMessaging.instance.getAPNSToken();
|
||||
try {
|
||||
_logger.info("Updating token on server");
|
||||
await _setPushTokenOnServer(fcmToken, apnsToken);
|
||||
await _prefs.setString(kFCMPushToken, fcmToken);
|
||||
await _prefs.setInt(
|
||||
kLastFCMTokenUpdationTime,
|
||||
DateTime.now().microsecondsSinceEpoch,
|
||||
);
|
||||
_logger.info("Push token updated on server");
|
||||
} catch (e) {
|
||||
_logger.severe("Could not set push token", e, StackTrace.current);
|
||||
}
|
||||
} else {
|
||||
_logger.info("Skipping token update");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _setPushTokenOnServer(
|
||||
String fcmToken,
|
||||
String? apnsToken,
|
||||
) async {
|
||||
await NetworkClient.instance.enteDio.post(
|
||||
"/push/token",
|
||||
data: {
|
||||
"fcmToken": fcmToken,
|
||||
"apnsToken": apnsToken,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _handleForegroundPushMessage(RemoteMessage message) {
|
||||
_logger.info("Message data: ${message.data}");
|
||||
if (message.notification != null) {
|
||||
_logger.info(
|
||||
"Message also contained a notification: ${message.notification}",
|
||||
);
|
||||
}
|
||||
if (shouldSync(message)) {
|
||||
SyncService.instance.sync();
|
||||
}
|
||||
}
|
||||
|
||||
static bool shouldSync(RemoteMessage message) {
|
||||
return message.data.containsKey(kPushAction) &&
|
||||
message.data[kPushAction] == kSync;
|
||||
}
|
||||
}
|
||||
@@ -1,603 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import "package:flutter/foundation.dart";
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/core/errors.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/events/subscription_purchased_event.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import 'package:photos/models/billing_plan.dart';
|
||||
import 'package:photos/models/subscription.dart';
|
||||
import 'package:photos/models/user_details.dart';
|
||||
import 'package:photos/services/billing_service.dart';
|
||||
import "package:photos/services/update_service.dart";
|
||||
import 'package:photos/services/user_service.dart';
|
||||
import "package:photos/theme/colors.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import 'package:photos/ui/common/loading_widget.dart';
|
||||
import 'package:photos/ui/common/progress_dialog.dart';
|
||||
import "package:photos/ui/components/captioned_text_widget.dart";
|
||||
import "package:photos/ui/components/menu_item_widget/menu_item_widget.dart";
|
||||
import 'package:photos/ui/payment/child_subscription_widget.dart';
|
||||
import 'package:photos/ui/payment/skip_subscription_widget.dart';
|
||||
import 'package:photos/ui/payment/subscription_common_widgets.dart';
|
||||
import 'package:photos/ui/payment/subscription_plan_widget.dart';
|
||||
import "package:photos/ui/payment/view_add_on_widget.dart";
|
||||
import "package:photos/utils/data_util.dart";
|
||||
import 'package:photos/utils/dialog_util.dart';
|
||||
import 'package:photos/utils/toast_util.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class StoreSubscriptionPage extends StatefulWidget {
|
||||
final bool isOnboarding;
|
||||
|
||||
const StoreSubscriptionPage({
|
||||
this.isOnboarding = false,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StoreSubscriptionPage> createState() => _StoreSubscriptionPageState();
|
||||
}
|
||||
|
||||
class _StoreSubscriptionPageState extends State<StoreSubscriptionPage> {
|
||||
final _logger = Logger("SubscriptionPage");
|
||||
final _billingService = BillingService.instance;
|
||||
final _userService = UserService.instance;
|
||||
Subscription? _currentSubscription;
|
||||
late StreamSubscription _purchaseUpdateSubscription;
|
||||
late ProgressDialog _dialog;
|
||||
late UserDetails _userDetails;
|
||||
late bool _hasActiveSubscription;
|
||||
bool _hideCurrentPlanSelection = false;
|
||||
late FreePlan _freePlan;
|
||||
late List<BillingPlan> _plans;
|
||||
bool _hasLoadedData = false;
|
||||
bool _isLoading = false;
|
||||
late bool _isActiveStripeSubscriber;
|
||||
EnteColorScheme colorScheme = darkScheme;
|
||||
|
||||
// hasYearlyPlans is used to check if there are yearly plans for given store
|
||||
bool hasYearlyPlans = false;
|
||||
|
||||
// _showYearlyPlan is used to determine if we should show the yearly plans
|
||||
bool showYearlyPlan = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_billingService.setIsOnSubscriptionPage(true);
|
||||
_setupPurchaseUpdateStreamListener();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _setupPurchaseUpdateStreamListener() {
|
||||
_purchaseUpdateSubscription =
|
||||
InAppPurchase.instance.purchaseStream.listen((purchases) async {
|
||||
if (!_dialog.isShowing()) {
|
||||
await _dialog.show();
|
||||
}
|
||||
for (final purchase in purchases) {
|
||||
_logger.info("Purchase status " + purchase.status.toString());
|
||||
if (purchase.status == PurchaseStatus.purchased) {
|
||||
try {
|
||||
final newSubscription = await _billingService.verifySubscription(
|
||||
purchase.productID,
|
||||
purchase.verificationData.serverVerificationData,
|
||||
);
|
||||
await InAppPurchase.instance.completePurchase(purchase);
|
||||
String text = S.of(context).thankYouForSubscribing;
|
||||
if (!widget.isOnboarding) {
|
||||
final isUpgrade = _hasActiveSubscription &&
|
||||
newSubscription.storage > _currentSubscription!.storage;
|
||||
final isDowngrade = _hasActiveSubscription &&
|
||||
newSubscription.storage < _currentSubscription!.storage;
|
||||
if (isUpgrade) {
|
||||
text = S.of(context).yourPlanWasSuccessfullyUpgraded;
|
||||
} else if (isDowngrade) {
|
||||
text = S.of(context).yourPlanWasSuccessfullyDowngraded;
|
||||
}
|
||||
}
|
||||
showShortToast(context, text);
|
||||
_currentSubscription = newSubscription;
|
||||
_hasActiveSubscription = _currentSubscription!.isValid();
|
||||
setState(() {});
|
||||
await _dialog.hide();
|
||||
Bus.instance.fire(SubscriptionPurchasedEvent());
|
||||
if (widget.isOnboarding) {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
}
|
||||
} on SubscriptionAlreadyClaimedError catch (e) {
|
||||
_logger.warning("subscription is already claimed ", e);
|
||||
await _dialog.hide();
|
||||
final String title = Platform.isAndroid
|
||||
? S.of(context).playstoreSubscription
|
||||
: S.of(context).appstoreSubscription;
|
||||
final String id = Platform.isAndroid
|
||||
? S.of(context).googlePlayId
|
||||
: S.of(context).appleId;
|
||||
final String message = S.of(context).subAlreadyLinkedErrMessage(id);
|
||||
// ignore: unawaited_futures
|
||||
showErrorDialog(context, title, message);
|
||||
return;
|
||||
} catch (e) {
|
||||
_logger.warning("Could not complete payment ", e);
|
||||
await _dialog.hide();
|
||||
// ignore: unawaited_futures
|
||||
showErrorDialog(
|
||||
context,
|
||||
S.of(context).paymentFailed,
|
||||
S.of(context).paymentFailedTalkToProvider(
|
||||
Platform.isAndroid ? "PlayStore" : "AppStore",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else if (Platform.isIOS && purchase.pendingCompletePurchase) {
|
||||
await InAppPurchase.instance.completePurchase(purchase);
|
||||
await _dialog.hide();
|
||||
} else if (purchase.status == PurchaseStatus.error) {
|
||||
await _dialog.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_purchaseUpdateSubscription.cancel();
|
||||
_billingService.setIsOnSubscriptionPage(false);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
colorScheme = getEnteColorScheme(context);
|
||||
if (!_isLoading) {
|
||||
_isLoading = true;
|
||||
_fetchSubData();
|
||||
}
|
||||
_dialog = createProgressDialog(context, S.of(context).pleaseWait);
|
||||
final appBar = AppBar(
|
||||
title: widget.isOnboarding
|
||||
? null
|
||||
: Text("${S.of(context).subscription}${kDebugMode ? ' Store' : ''}"),
|
||||
);
|
||||
return Scaffold(
|
||||
appBar: appBar,
|
||||
body: _getBody(),
|
||||
);
|
||||
}
|
||||
|
||||
bool _isFreePlanUser() {
|
||||
return _currentSubscription != null &&
|
||||
freeProductID == _currentSubscription!.productID;
|
||||
}
|
||||
|
||||
Future<void> _fetchSubData() async {
|
||||
// ignore: unawaited_futures
|
||||
_userService.getUserDetailsV2(memoryCount: false).then((userDetails) async {
|
||||
_userDetails = userDetails;
|
||||
_currentSubscription = userDetails.subscription;
|
||||
|
||||
_hasActiveSubscription = _currentSubscription!.isValid();
|
||||
_hideCurrentPlanSelection =
|
||||
_currentSubscription?.attributes?.isCancelled ?? false;
|
||||
showYearlyPlan = _currentSubscription!.isYearlyPlan();
|
||||
final billingPlans = await _billingService.getBillingPlans();
|
||||
_isActiveStripeSubscriber =
|
||||
_currentSubscription!.paymentProvider == stripe &&
|
||||
_currentSubscription!.isValid();
|
||||
_plans = billingPlans.plans.where((plan) {
|
||||
final productID = _isActiveStripeSubscriber
|
||||
? plan.stripeID
|
||||
: Platform.isAndroid
|
||||
? plan.androidID
|
||||
: plan.iosID;
|
||||
return productID.isNotEmpty;
|
||||
}).toList();
|
||||
hasYearlyPlans = _plans.any((plan) => plan.period == 'year');
|
||||
if (showYearlyPlan && hasYearlyPlans) {
|
||||
_plans = _plans.where((plan) => plan.period == 'year').toList();
|
||||
} else {
|
||||
_plans = _plans.where((plan) => plan.period != 'year').toList();
|
||||
}
|
||||
_freePlan = billingPlans.freePlan;
|
||||
_hasLoadedData = true;
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Widget _getBody() {
|
||||
if (_hasLoadedData) {
|
||||
if (_userDetails.isPartOfFamily() && !_userDetails.isFamilyAdmin()) {
|
||||
return ChildSubscriptionWidget(userDetails: _userDetails);
|
||||
} else {
|
||||
return _buildPlans();
|
||||
}
|
||||
}
|
||||
return const EnteLoadingWidget();
|
||||
}
|
||||
|
||||
Widget _buildPlans() {
|
||||
final widgets = <Widget>[];
|
||||
widgets.add(
|
||||
SubscriptionHeaderWidget(
|
||||
isOnboarding: widget.isOnboarding,
|
||||
currentUsage: _userDetails.getFamilyOrPersonalUsage(),
|
||||
),
|
||||
);
|
||||
|
||||
widgets.addAll([
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: _isActiveStripeSubscriber
|
||||
? _getStripePlanWidgets()
|
||||
: _getMobilePlanWidgets(),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(8)),
|
||||
]);
|
||||
|
||||
if (hasYearlyPlans) {
|
||||
widgets.add(_showSubscriptionToggle());
|
||||
}
|
||||
|
||||
if (_currentSubscription != null) {
|
||||
widgets.add(
|
||||
ValidityWidget(
|
||||
currentSubscription: _currentSubscription,
|
||||
bonusData: _userDetails.bonusData,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_currentSubscription!.productID == freeProductID) {
|
||||
if (widget.isOnboarding) {
|
||||
widgets.add(SkipSubscriptionWidget(freePlan: _freePlan));
|
||||
}
|
||||
widgets.add(
|
||||
SubFaqWidget(isOnboarding: widget.isOnboarding),
|
||||
);
|
||||
}
|
||||
|
||||
if (_hasActiveSubscription &&
|
||||
_currentSubscription!.productID != freeProductID) {
|
||||
if (_isActiveStripeSubscriber) {
|
||||
widgets.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20),
|
||||
child: Text(
|
||||
S.of(context).visitWebToManage,
|
||||
style: getEnteTextTheme(context).small.copyWith(
|
||||
color: colorScheme.textMuted,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
widgets.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 40, 16, 4),
|
||||
child: MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
title: S.of(context).paymentDetails,
|
||||
),
|
||||
menuItemColor: colorScheme.fillFaint,
|
||||
trailingWidget: Icon(
|
||||
Icons.chevron_right_outlined,
|
||||
color: colorScheme.strokeBase,
|
||||
),
|
||||
singleBorderRadius: 4,
|
||||
alignCaptionedTextToLeft: true,
|
||||
onTap: () async {
|
||||
_onPlatformRestrictedPaymentDetailsClick();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!widget.isOnboarding) {
|
||||
widgets.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 0),
|
||||
child: MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
title: _isFreePlanUser()
|
||||
? S.of(context).familyPlans
|
||||
: S.of(context).manageFamily,
|
||||
),
|
||||
menuItemColor: colorScheme.fillFaint,
|
||||
trailingWidget: Icon(
|
||||
Icons.chevron_right_outlined,
|
||||
color: colorScheme.strokeBase,
|
||||
),
|
||||
singleBorderRadius: 4,
|
||||
alignCaptionedTextToLeft: true,
|
||||
onTap: () async {
|
||||
unawaited(
|
||||
_billingService.launchFamilyPortal(context, _userDetails),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
widgets.add(ViewAddOnButton(_userDetails.bonusData));
|
||||
widgets.add(const SizedBox(height: 80));
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: widgets,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onPlatformRestrictedPaymentDetailsClick() {
|
||||
final String paymentProvider = _currentSubscription!.paymentProvider;
|
||||
if (paymentProvider == appStore && !Platform.isAndroid) {
|
||||
launchUrlString("https://apps.apple.com/account/billing");
|
||||
} else if (paymentProvider == playStore && Platform.isAndroid) {
|
||||
launchUrlString(
|
||||
"https://play.google.com/store/account/subscriptions?sku=" +
|
||||
_currentSubscription!.productID +
|
||||
"&package=io.ente.photos",
|
||||
);
|
||||
} else if (paymentProvider == stripe) {
|
||||
showErrorDialog(
|
||||
context,
|
||||
S.of(context).sorry,
|
||||
S.of(context).visitWebToManage,
|
||||
);
|
||||
} else {
|
||||
final String capitalizedWord = paymentProvider.isNotEmpty
|
||||
? '${paymentProvider[0].toUpperCase()}${paymentProvider.substring(1).toLowerCase()}'
|
||||
: '';
|
||||
showErrorDialog(
|
||||
context,
|
||||
S.of(context).sorry,
|
||||
S.of(context).contactToManageSubscription(capitalizedWord),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _filterStorePlansForUi() async {
|
||||
final billingPlans = await _billingService.getBillingPlans();
|
||||
_plans = billingPlans.plans.where((plan) {
|
||||
final productID = _isActiveStripeSubscriber
|
||||
? plan.stripeID
|
||||
: Platform.isAndroid
|
||||
? plan.androidID
|
||||
: plan.iosID;
|
||||
return productID.isNotEmpty;
|
||||
}).toList();
|
||||
hasYearlyPlans = _plans.any((plan) => plan.period == 'year');
|
||||
if (showYearlyPlan) {
|
||||
_plans = _plans.where((plan) => plan.period == 'year').toList();
|
||||
} else {
|
||||
_plans = _plans.where((plan) => plan.period != 'year').toList();
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Widget _showSubscriptionToggle() {
|
||||
Widget planText(String title, bool reduceOpacity) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 4, right: 4),
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(reduceOpacity ? 0.5 : 1.0),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(left: 8, right: 8, top: 2, bottom: 2),
|
||||
margin: const EdgeInsets.only(bottom: 6),
|
||||
child: Column(
|
||||
children: [
|
||||
RepaintBoundary(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
planText(S.of(context).monthly, showYearlyPlan),
|
||||
Switch(
|
||||
value: showYearlyPlan,
|
||||
activeColor: Colors.white,
|
||||
inactiveThumbColor: Colors.white,
|
||||
activeTrackColor: getEnteColorScheme(context).strokeMuted,
|
||||
onChanged: (value) async {
|
||||
showYearlyPlan = value;
|
||||
await _filterStorePlansForUi();
|
||||
},
|
||||
),
|
||||
planText(S.of(context).yearly, !showYearlyPlan),
|
||||
],
|
||||
),
|
||||
),
|
||||
_isFreePlanUser() && !UpdateService.instance.isPlayStoreFlavor()
|
||||
? Text(
|
||||
S.of(context).twoMonthsFreeOnYearlyPlans,
|
||||
style: getEnteTextTheme(context).miniMuted,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
const Padding(padding: EdgeInsets.all(8)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _getStripePlanWidgets() {
|
||||
final List<Widget> planWidgets = [];
|
||||
bool foundActivePlan = false;
|
||||
for (final plan in _plans) {
|
||||
final productID = plan.stripeID;
|
||||
if (productID.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
final isActive = _hasActiveSubscription &&
|
||||
_currentSubscription!.productID == productID;
|
||||
if (isActive) {
|
||||
foundActivePlan = true;
|
||||
}
|
||||
planWidgets.add(
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
if (isActive) {
|
||||
return;
|
||||
}
|
||||
// ignore: unawaited_futures
|
||||
showErrorDialog(
|
||||
context,
|
||||
S.of(context).sorry,
|
||||
S.of(context).visitWebToManage,
|
||||
);
|
||||
},
|
||||
child: SubscriptionPlanWidget(
|
||||
storage: plan.storage,
|
||||
price: plan.price,
|
||||
period: plan.period,
|
||||
isActive: isActive && !_hideCurrentPlanSelection,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!foundActivePlan && _hasActiveSubscription) {
|
||||
_addCurrentPlanWidget(planWidgets);
|
||||
}
|
||||
return planWidgets;
|
||||
}
|
||||
|
||||
List<Widget> _getMobilePlanWidgets() {
|
||||
bool foundActivePlan = false;
|
||||
final List<Widget> planWidgets = [];
|
||||
if (_hasActiveSubscription &&
|
||||
_currentSubscription!.productID == freeProductID) {
|
||||
foundActivePlan = true;
|
||||
planWidgets.add(
|
||||
SubscriptionPlanWidget(
|
||||
storage: _freePlan.storage,
|
||||
price: S.of(context).freeTrial,
|
||||
period: "",
|
||||
isActive: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
for (final plan in _plans) {
|
||||
final productID = Platform.isAndroid ? plan.androidID : plan.iosID;
|
||||
final isActive = _hasActiveSubscription &&
|
||||
_currentSubscription!.productID == productID;
|
||||
if (isActive) {
|
||||
foundActivePlan = true;
|
||||
}
|
||||
planWidgets.add(
|
||||
Material(
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
if (isActive) {
|
||||
return;
|
||||
}
|
||||
final int addOnBonus =
|
||||
_userDetails.bonusData?.totalAddOnBonus() ?? 0;
|
||||
if (_userDetails.getFamilyOrPersonalUsage() >
|
||||
(plan.storage + addOnBonus)) {
|
||||
_logger.warning(
|
||||
" familyUsage ${convertBytesToReadableFormat(_userDetails.getFamilyOrPersonalUsage())}"
|
||||
" plan storage ${convertBytesToReadableFormat(plan.storage)} "
|
||||
"addOnBonus ${convertBytesToReadableFormat(addOnBonus)},"
|
||||
"overshooting by ${convertBytesToReadableFormat(_userDetails.getFamilyOrPersonalUsage() - (plan.storage + addOnBonus))}",
|
||||
);
|
||||
// ignore: unawaited_futures
|
||||
showErrorDialog(
|
||||
context,
|
||||
S.of(context).sorry,
|
||||
S.of(context).youCannotDowngradeToThisPlan,
|
||||
);
|
||||
return;
|
||||
}
|
||||
await _dialog.show();
|
||||
final ProductDetailsResponse response =
|
||||
await InAppPurchase.instance.queryProductDetails({productID});
|
||||
if (response.notFoundIDs.isNotEmpty) {
|
||||
final errMsg = "Could not find products: " +
|
||||
response.notFoundIDs.toString();
|
||||
_logger.severe(errMsg);
|
||||
await _dialog.hide();
|
||||
await showGenericErrorDialog(
|
||||
context: context,
|
||||
error: Exception(errMsg),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final isCrossGradingOnAndroid = Platform.isAndroid &&
|
||||
_hasActiveSubscription &&
|
||||
_currentSubscription!.productID != freeProductID &&
|
||||
_currentSubscription!.productID != plan.androidID;
|
||||
if (isCrossGradingOnAndroid) {
|
||||
await _dialog.hide();
|
||||
// ignore: unawaited_futures
|
||||
showErrorDialog(
|
||||
context,
|
||||
S.of(context).couldNotUpdateSubscription,
|
||||
S.of(context).pleaseContactSupportAndWeWillBeHappyToHelp,
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
await InAppPurchase.instance.buyNonConsumable(
|
||||
purchaseParam: PurchaseParam(
|
||||
productDetails: response.productDetails[0],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: SubscriptionPlanWidget(
|
||||
storage: plan.storage,
|
||||
price: plan.price,
|
||||
period: plan.period,
|
||||
isActive: isActive,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!foundActivePlan && _hasActiveSubscription) {
|
||||
_addCurrentPlanWidget(planWidgets);
|
||||
}
|
||||
return planWidgets;
|
||||
}
|
||||
|
||||
void _addCurrentPlanWidget(List<Widget> planWidgets) {
|
||||
int activePlanIndex = 0;
|
||||
for (; activePlanIndex < _plans.length; activePlanIndex++) {
|
||||
if (_plans[activePlanIndex].storage > _currentSubscription!.storage) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
planWidgets.insert(
|
||||
activePlanIndex,
|
||||
Material(
|
||||
child: InkWell(
|
||||
onTap: () {},
|
||||
child: SubscriptionPlanWidget(
|
||||
storage: _currentSubscription!.storage,
|
||||
price: _currentSubscription!.price,
|
||||
period: _currentSubscription!.period,
|
||||
isActive: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,6 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/services/feature_flag_service.dart';
|
||||
import 'package:photos/services/update_service.dart';
|
||||
import "package:photos/ui/payment/store_subscription_page.dart";
|
||||
import 'package:photos/ui/payment/stripe_subscription_page.dart';
|
||||
|
||||
StatefulWidget getSubscriptionPage({bool isOnBoarding = false}) {
|
||||
if (UpdateService.instance.isIndependentFlavor()) {
|
||||
return StripeSubscriptionPage(isOnboarding: isOnBoarding);
|
||||
}
|
||||
if (FeatureFlagService.instance.enableStripe() &&
|
||||
_isUserCreatedPostStripeSupport()) {
|
||||
return StripeSubscriptionPage(isOnboarding: isOnBoarding);
|
||||
} else {
|
||||
return StoreSubscriptionPage(isOnboarding: isOnBoarding);
|
||||
}
|
||||
}
|
||||
|
||||
// return true if the user was created after we added support for stripe payment
|
||||
// on frame. We do this check to avoid showing Stripe payment option for earlier
|
||||
// users who might have paid via playStore. This method should be removed once
|
||||
// we have better handling for active play/app store subscription & stripe plans.
|
||||
bool _isUserCreatedPostStripeSupport() {
|
||||
return Configuration.instance.getUserID()! > 1580559962386460;
|
||||
return StripeSubscriptionPage(isOnboarding: isOnBoarding);
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ class GalleryFileWidget extends StatelessWidget {
|
||||
: _onLongPressNoSelectionLimit(context, file);
|
||||
},
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(1),
|
||||
|
||||
@@ -12,7 +12,7 @@ description: ente photos application
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
|
||||
version: 0.8.68+588
|
||||
version: 0.8.71+591
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
@@ -64,8 +64,6 @@ dependencies:
|
||||
file_saver:
|
||||
# Use forked version till this PR is merged: https://github.com/incrediblezayed/file_saver/pull/87
|
||||
git: https://github.com/jesims/file_saver.git
|
||||
firebase_core: ^2.13.1
|
||||
firebase_messaging: ^14.6.2
|
||||
fk_user_agent: ^2.0.1
|
||||
flutter:
|
||||
sdk: flutter
|
||||
@@ -99,7 +97,6 @@ dependencies:
|
||||
http: ^1.1.0
|
||||
image: ^4.0.17
|
||||
image_editor: ^1.3.0
|
||||
in_app_purchase: ^3.0.7
|
||||
intl: ^0.18.0
|
||||
isar: ^3.1.0+1
|
||||
isar_flutter_libs: ^3.1.0+1
|
||||
@@ -207,7 +204,7 @@ flutter_icons:
|
||||
android: "launcher_icon"
|
||||
adaptive_icon_foreground: "assets/launcher_icon/ente-icon-foreground.png"
|
||||
adaptive_icon_background: "#ffffff"
|
||||
ios: true
|
||||
ios: false # F-Droid
|
||||
image_path: "assets/icon-light.png"
|
||||
|
||||
flutter_native_splash:
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
# TODO: add `rustup@1.25.2` to `srclibs`
|
||||
# TODO: verify if `gcc-multilib` or `libc-dev` is needed
|
||||
$$rustup$$/rustup-init.sh -y
|
||||
source $HOME/.cargo/env
|
||||
cd thirdparty/isar/
|
||||
bash tool/build_android.sh x86
|
||||
bash tool/build_android.sh x64
|
||||
@@ -15,3 +11,4 @@ mv libisar_android_x64.so libisar.so
|
||||
mv libisar.so $PUB_CACHE/hosted/pub.dev/isar_flutter_libs-*/android/src/main/jniLibs/x86_64/
|
||||
mv libisar_android_x86.so libisar.so
|
||||
mv libisar.so $PUB_CACHE/hosted/pub.dev/isar_flutter_libs-*/android/src/main/jniLibs/x86/
|
||||
cd ../../
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
# Eclipse
|
||||
.metadata
|
||||
|
||||
# Xcode
|
||||
#
|
||||
.DS_Store
|
||||
build/
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata
|
||||
*.xccheckout
|
||||
*.moved-aside
|
||||
DerivedData
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
|
||||
# CocoaPods
|
||||
#
|
||||
# We recommend against adding the Pods directory to your .gitignore. However
|
||||
# you should judge for yourself, the pros and cons are mentioned at:
|
||||
# http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control
|
||||
#
|
||||
#Pods/
|
||||
|
||||
# Eclipse
|
||||
|
||||
# built application files
|
||||
*.apk
|
||||
*.ap_
|
||||
|
||||
# files for the dex VM
|
||||
*.dex
|
||||
|
||||
# Java class files
|
||||
*.class
|
||||
|
||||
# generated files
|
||||
bin/
|
||||
gen/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
# Eclipse project files
|
||||
.classpath
|
||||
.project
|
||||
|
||||
# Proguard folder generated by Eclipse
|
||||
proguard/
|
||||
|
||||
# Intellij project files
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 26
|
||||
compileSdkVersion 30
|
||||
defaultConfig {
|
||||
applicationId "com.transistorsoft.backgroundfetch"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 26
|
||||
targetSdkVersion 30
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
@@ -24,6 +24,5 @@ dependencies {
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation 'com.android.support:appcompat-v7:26.1.0'
|
||||
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.5.0'
|
||||
classpath 'com.android.tools.build:gradle:4.1.3'
|
||||
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
@@ -18,7 +18,7 @@ buildscript {
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ task clean(type: Delete) {
|
||||
}
|
||||
|
||||
ext {
|
||||
compileSdkVersion = 29
|
||||
targetSdkVersion = 29
|
||||
compileSdkVersion = 32
|
||||
targetSdkVersion = 31
|
||||
buildToolsVersion = "29.0.6"
|
||||
appCompatVersion = "1.1.0"
|
||||
appCompatVersion = "1.4.1"
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ org.gradle.jvmargs=-Xmx1536m
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
|
||||
VERSION_NAME=0.5.0
|
||||
VERSION_CODE=15
|
||||
VERSION_NAME=0.5.6
|
||||
VERSION_CODE=21
|
||||
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#Thu Feb 09 18:40:48 IST 2023
|
||||
#Thu Jul 15 09:21:17 EDT 2021
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
distributionSha256Sum=10065868c78f1207afb3a92176f99a37d753a513dff453abb6b5cceda4058cda
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
|
||||
|
||||
@@ -32,6 +32,10 @@ android {
|
||||
mavenLocal()
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -40,15 +44,14 @@ dependencies {
|
||||
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
implementation "androidx.lifecycle:lifecycle-runtime:2.5.1"
|
||||
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
|
||||
//implementation "androidx.appcompat:appcompat:$rootProject.appCompatVersion"
|
||||
|
||||
}
|
||||
|
||||
// Build Release
|
||||
task buildRelease { task ->
|
||||
task.dependsOn 'cordovaRelease'
|
||||
task.dependsOn 'reactNativeRelease'
|
||||
task.dependsOn 'nativeScriptRelease'
|
||||
task.dependsOn 'flutterRelease'
|
||||
}
|
||||
|
||||
@@ -59,7 +62,7 @@ task publishRelease { task ->
|
||||
tasks["publishRelease"].mustRunAfter("assembleRelease")
|
||||
tasks["publishRelease"].finalizedBy("publish")
|
||||
|
||||
def WORKSPACE_PATH = "/Volumes/Glyph2TB/Users/chris/workspace"
|
||||
def WORKSPACE_PATH = "/Users/chris/workspace"
|
||||
|
||||
// Build local maven repo.
|
||||
def LIBRARY_PATH = "com/transistorsoft/tsbackgroundfetch"
|
||||
@@ -78,7 +81,7 @@ task buildLocalRepository { task ->
|
||||
}
|
||||
}
|
||||
|
||||
def cordovaDir = "$WORKSPACE_PATH/cordova/background-geolocation/cordova-plugin-background-fetch"
|
||||
def cordovaDir = "$WORKSPACE_PATH/background-geolocation/cordova/cordova-plugin-background-fetch"
|
||||
task cordovaRelease { task ->
|
||||
task.dependsOn 'buildLocalRepository'
|
||||
doLast {
|
||||
@@ -95,7 +98,7 @@ task cordovaRelease { task ->
|
||||
}
|
||||
}
|
||||
|
||||
def reactNativeDir = "$WORKSPACE_PATH/react/background-geolocation/react-native-background-fetch"
|
||||
def reactNativeDir = "$WORKSPACE_PATH/background-geolocation/react/react-native-background-fetch"
|
||||
task reactNativeRelease { task ->
|
||||
task.dependsOn 'buildLocalRepository'
|
||||
doLast {
|
||||
@@ -129,6 +132,19 @@ task flutterRelease { task ->
|
||||
}
|
||||
}
|
||||
|
||||
def capacitorDir = "$WORKSPACE_PATH/background-geolocation/capacitor/capacitor-background-fetch"
|
||||
task capacitorRelease { task ->
|
||||
task.dependsOn 'buildLocalRepository'
|
||||
doLast {
|
||||
delete "$capacitorDir/android/libs"
|
||||
copy {
|
||||
// Maven repo format.
|
||||
from("$buildDir/repo-local")
|
||||
into("$capacitorDir/android/libs")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task nativeScriptRelease(type: Copy) {
|
||||
from('./build/outputs/aar/tsbackgroundfetch-release.aar')
|
||||
into("$WORKSPACE_PATH/NativeScript/background-geolocation/nativescript-background-fetch/src/platforms/android/libs")
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.GET_TASKS" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
|
||||
<application>
|
||||
<receiver android:name="com.transistorsoft.tsbackgroundfetch.FetchAlarmReceiver" />
|
||||
<service android:name="com.transistorsoft.tsbackgroundfetch.FetchJobService" android:permission="android.permission.BIND_JOB_SERVICE" android:exported="true" />
|
||||
<receiver android:name="com.transistorsoft.tsbackgroundfetch.BootReceiver">
|
||||
<receiver android:name="com.transistorsoft.tsbackgroundfetch.BootReceiver" android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
|
||||
@@ -110,6 +110,16 @@ public class BGTask {
|
||||
removeTask(mTaskId);
|
||||
}
|
||||
|
||||
static void reschedule(Context context, BackgroundFetchConfig existing, BackgroundFetchConfig config) {
|
||||
BGTask existingTask = BGTask.getTask(existing.getTaskId());
|
||||
if (existingTask != null) {
|
||||
existingTask.finish();
|
||||
}
|
||||
cancel(context, existing.getTaskId(), existing.getJobId());
|
||||
|
||||
schedule(context, config);
|
||||
}
|
||||
|
||||
static void schedule(Context context, BackgroundFetchConfig config) {
|
||||
Log.d(BackgroundFetch.TAG, config.toString());
|
||||
|
||||
@@ -136,6 +146,8 @@ public class BGTask {
|
||||
}
|
||||
PersistableBundle extras = new PersistableBundle();
|
||||
extras.putString(BackgroundFetchConfig.FIELD_TASK_ID, config.getTaskId());
|
||||
extras.putLong("scheduled_at", System.currentTimeMillis());
|
||||
|
||||
builder.setExtras(extras);
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT >= 26) {
|
||||
@@ -172,7 +184,7 @@ public class BGTask {
|
||||
|
||||
BackgroundFetch adapter = BackgroundFetch.getInstance(context);
|
||||
|
||||
if (adapter.isMainActivityActive()) {
|
||||
if (!LifecycleManager.getInstance().isHeadless()) {
|
||||
BackgroundFetch.Callback callback = adapter.getFetchCallback();
|
||||
if (callback != null) {
|
||||
callback.onTimeout(mTaskId);
|
||||
@@ -246,7 +258,7 @@ public class BGTask {
|
||||
static PendingIntent getAlarmPI(Context context, String taskId) {
|
||||
Intent intent = new Intent(context, FetchAlarmReceiver.class);
|
||||
intent.setAction(taskId);
|
||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT|PendingIntent.FLAG_IMMUTABLE);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
|
||||
@@ -4,6 +4,8 @@ import android.annotation.TargetApi;
|
||||
import android.app.ActivityManager;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
@@ -76,6 +78,8 @@ public class BackgroundFetch {
|
||||
|
||||
private BackgroundFetch(Context context) {
|
||||
mContext = context;
|
||||
// Start Lifecycle Observer to be notified when app enters background.
|
||||
getUiHandler().post(LifecycleManager.getInstance());
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unused"})
|
||||
@@ -84,7 +88,16 @@ public class BackgroundFetch {
|
||||
mFetchCallback = callback;
|
||||
|
||||
synchronized (mConfig) {
|
||||
mConfig.put(config.getTaskId(), config);
|
||||
if (mConfig.containsKey(config.getTaskId())) {
|
||||
// Developer called `.configure` again. Re-configure the plugin by re-scheduling the fetch task.
|
||||
BackgroundFetchConfig existing = mConfig.get(config.getTaskId());
|
||||
Log.d(TAG, "Re-configured existing task");
|
||||
BGTask.reschedule(mContext, existing, config);
|
||||
mConfig.put(config.getTaskId(), config);
|
||||
return;
|
||||
} else {
|
||||
mConfig.put(config.getTaskId(), config);
|
||||
}
|
||||
}
|
||||
start(config.getTaskId());
|
||||
}
|
||||
@@ -224,8 +237,6 @@ public class BackgroundFetch {
|
||||
}
|
||||
|
||||
private void registerTask(String taskId) {
|
||||
Log.d(TAG, "- registerTask: " + taskId);
|
||||
|
||||
BackgroundFetchConfig config = getConfig(taskId);
|
||||
|
||||
if (config == null) {
|
||||
@@ -234,6 +245,12 @@ public class BackgroundFetch {
|
||||
}
|
||||
config.save(mContext);
|
||||
|
||||
String msg = "- registerTask: " + taskId;
|
||||
if (!config.getForceAlarmManager()) {
|
||||
msg += " (jobId: " + config.getJobId() + ")";
|
||||
}
|
||||
Log.d(TAG, msg);
|
||||
|
||||
BGTask.schedule(mContext, config);
|
||||
}
|
||||
|
||||
@@ -245,7 +262,7 @@ public class BackgroundFetch {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMainActivityActive()) {
|
||||
if (!LifecycleManager.getInstance().isHeadless()) {
|
||||
if (mFetchCallback != null) {
|
||||
mFetchCallback.onFetch(task.getTaskId());
|
||||
}
|
||||
@@ -267,29 +284,6 @@ public class BackgroundFetch {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"WeakerAccess", "deprecation"})
|
||||
public Boolean isMainActivityActive() {
|
||||
Boolean isActive = false;
|
||||
|
||||
if (mContext == null || mFetchCallback == null) {
|
||||
return false;
|
||||
}
|
||||
ActivityManager activityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
|
||||
try {
|
||||
List<ActivityManager.RunningTaskInfo> tasks = activityManager.getRunningTasks(Integer.MAX_VALUE);
|
||||
for (ActivityManager.RunningTaskInfo task : tasks) {
|
||||
if (mContext.getPackageName().equalsIgnoreCase(task.baseActivity.getPackageName())) {
|
||||
isActive = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (java.lang.SecurityException e) {
|
||||
Log.w(TAG, "TSBackgroundFetch attempted to determine if MainActivity is active but was stopped due to a missing permission. Please add the permission 'android.permission.GET_TASKS' to your AndroidManifest. See Installation steps for more information");
|
||||
throw e;
|
||||
}
|
||||
return isActive;
|
||||
}
|
||||
|
||||
BackgroundFetchConfig getConfig(String taskId) {
|
||||
synchronized (mConfig) {
|
||||
return (mConfig.containsKey(taskId)) ? mConfig.get(taskId) : null;
|
||||
|
||||
@@ -14,6 +14,15 @@ public class FetchJobService extends JobService {
|
||||
@Override
|
||||
public boolean onStartJob(final JobParameters params) {
|
||||
PersistableBundle extras = params.getExtras();
|
||||
long scheduleAt = extras.getLong("scheduled_at");
|
||||
long dt = System.currentTimeMillis() - scheduleAt;
|
||||
// Scheduled < 1s ago? Ignore.
|
||||
if (dt < 1000) {
|
||||
// JobScheduler always immediately fires an initial event on Periodic jobs -- We IGNORE these.
|
||||
jobFinished(params, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
final String taskId = extras.getString(BackgroundFetchConfig.FIELD_TASK_ID);
|
||||
|
||||
CompletionHandler completionHandler = new CompletionHandler() {
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
package com.transistorsoft.tsbackgroundfetch;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Component for managing app life-cycle changes, including headless-mode.
|
||||
*/
|
||||
public class LifecycleManager implements DefaultLifecycleObserver, Runnable {
|
||||
private static LifecycleManager sInstance;
|
||||
|
||||
public static LifecycleManager getInstance() {
|
||||
if (sInstance == null) {
|
||||
sInstance = getInstanceSynchronized();
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
private static synchronized LifecycleManager getInstanceSynchronized() {
|
||||
if (sInstance == null) sInstance = new LifecycleManager();
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
private final List<OnHeadlessChangeCallback> mHeadlessChangeCallbacks = new ArrayList<>();
|
||||
private final List<OnStateChangeCallback> mStateChangeCallbacks = new ArrayList<>();
|
||||
private final Handler mHandler;
|
||||
private Runnable mHeadlessChangeEvent;
|
||||
|
||||
private final AtomicBoolean mIsBackground = new AtomicBoolean(true);
|
||||
private final AtomicBoolean mIsHeadless = new AtomicBoolean(true);
|
||||
private final AtomicBoolean mStarted = new AtomicBoolean(false);
|
||||
private final AtomicBoolean mPaused = new AtomicBoolean(false);
|
||||
|
||||
private LifecycleManager() {
|
||||
mHandler = new Handler(Looper.getMainLooper());
|
||||
onHeadlessChange(isHeadless -> {
|
||||
if (isHeadless) {
|
||||
Log.d(BackgroundFetch.TAG, "☯️ HeadlessMode? " + isHeadless);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporarily disable responding to pause/resume events. This was placed here for handling TSLocationManagerActivity events
|
||||
* whose presentation causes onPause / onResume events that we don't want to react to.
|
||||
*/
|
||||
public void pause() {
|
||||
mPaused.set(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-engage responding to pause/resume events.
|
||||
*/
|
||||
public void resume() {
|
||||
mPaused.set(false);
|
||||
}
|
||||
/**
|
||||
* Are we in the background?
|
||||
* @return boolean
|
||||
*/
|
||||
public boolean isBackground() {
|
||||
return mIsBackground.get();
|
||||
}
|
||||
/**
|
||||
* Are we headless
|
||||
* @return boolean
|
||||
*/
|
||||
public boolean isHeadless() {
|
||||
return mIsHeadless.get();
|
||||
}
|
||||
/**
|
||||
* Explicitly state that we are headless. Probably called when MainActivity is known to have been destroyed.
|
||||
* @param value boolean
|
||||
*/
|
||||
public void setHeadless(boolean value) {
|
||||
mIsHeadless.set(value);
|
||||
if (mIsHeadless.get()) {
|
||||
Log.d(BackgroundFetch.TAG,"☯️ HeadlessMode? " + mIsHeadless);
|
||||
}
|
||||
if (mHeadlessChangeEvent != null) {
|
||||
mHandler.removeCallbacks(mHeadlessChangeEvent);
|
||||
mStarted.set(true);
|
||||
fireHeadlessChangeListeners();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Register Headless-mode change listener.
|
||||
*/
|
||||
public void onHeadlessChange(OnHeadlessChangeCallback callback) {
|
||||
if (mStarted.get()) {
|
||||
callback.onChange(mIsHeadless.get());
|
||||
return;
|
||||
}
|
||||
synchronized (mHeadlessChangeCallbacks) {
|
||||
mHeadlessChangeCallbacks.add(callback);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Register pause/resume listener.
|
||||
*/
|
||||
public void onStateChange(OnStateChangeCallback callback) {
|
||||
synchronized (mStateChangeCallbacks) {
|
||||
mStateChangeCallbacks.add(callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Regiser the LifecycleObserver
|
||||
*/
|
||||
@Override
|
||||
public void run() {
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@NonNull LifecycleOwner owner) {
|
||||
Log.d(BackgroundFetch.TAG,"☯️ onCreate");
|
||||
// If this 50ms Timer fires before onStart, we are headless
|
||||
mHeadlessChangeEvent = new Runnable() {
|
||||
@Override public void run() {
|
||||
mStarted.set(true);
|
||||
fireHeadlessChangeListeners();
|
||||
}
|
||||
};
|
||||
|
||||
mHandler.postDelayed(mHeadlessChangeEvent, 50);
|
||||
mIsHeadless.set(true);
|
||||
mIsBackground.set(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart(@NonNull LifecycleOwner owner) {
|
||||
Log.d(BackgroundFetch.TAG, "☯️ onStart");
|
||||
// Cancel StateChange Timer.
|
||||
if (mPaused.get()) {
|
||||
return;
|
||||
}
|
||||
if (mHeadlessChangeEvent != null) {
|
||||
mHandler.removeCallbacks(mHeadlessChangeEvent);
|
||||
}
|
||||
|
||||
mStarted.set(true);
|
||||
mIsHeadless.set(false);
|
||||
mIsBackground.set(false);
|
||||
|
||||
// Fire listeners.
|
||||
fireHeadlessChangeListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(@NonNull LifecycleOwner owner) {
|
||||
Log.d(BackgroundFetch.TAG, "☯️ onDestroy");
|
||||
mIsBackground.set(true);
|
||||
mIsHeadless.set(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop(@NonNull LifecycleOwner owner) {
|
||||
Log.d(BackgroundFetch.TAG, "☯️ onStop");
|
||||
if (mPaused.compareAndSet(true, false)) {
|
||||
return;
|
||||
}
|
||||
mIsBackground.set(true);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause(@NonNull LifecycleOwner owner) {
|
||||
Log.d(BackgroundFetch.TAG, "☯️ onPause");
|
||||
mIsBackground.set(true);
|
||||
fireStateChangeListeners(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume(@NonNull LifecycleOwner owner) {
|
||||
Log.d(BackgroundFetch.TAG, "☯️ onResume");
|
||||
if (mPaused.get()) {
|
||||
return;
|
||||
}
|
||||
mIsBackground.set(false);
|
||||
mIsHeadless.set(false);
|
||||
fireStateChangeListeners(true);
|
||||
}
|
||||
|
||||
/// Fire pause/resume change listeners
|
||||
private void fireStateChangeListeners(boolean isForeground) {
|
||||
synchronized (mStateChangeCallbacks) {
|
||||
for (OnStateChangeCallback callback : mStateChangeCallbacks) {
|
||||
callback.onChange(isForeground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fire headless mode change listeners.
|
||||
private void fireHeadlessChangeListeners() {
|
||||
if (mHeadlessChangeEvent != null) {
|
||||
mHandler.removeCallbacks(mHeadlessChangeEvent);
|
||||
mHeadlessChangeEvent = null;
|
||||
}
|
||||
synchronized (mHeadlessChangeCallbacks) {
|
||||
for (OnHeadlessChangeCallback callback : mHeadlessChangeCallbacks) {
|
||||
callback.onChange(mIsHeadless.get());
|
||||
}
|
||||
mHeadlessChangeCallbacks.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public interface OnHeadlessChangeCallback {
|
||||
void onChange(boolean isHeadless);
|
||||
}
|
||||
|
||||
public interface OnStateChangeCallback {
|
||||
void onChange(boolean isForeground);
|
||||
}
|
||||
}
|
||||
@@ -620,6 +620,7 @@ func main() {
|
||||
adminAPI.POST("/user/disable-2fa", adminHandler.DisableTwoFactor)
|
||||
adminAPI.POST("/user/disable-passkeys", adminHandler.RemovePasskeys)
|
||||
adminAPI.POST("/user/close-family", adminHandler.CloseFamily)
|
||||
adminAPI.PUT("/user/change-email", adminHandler.ChangeEmail)
|
||||
adminAPI.DELETE("/user/delete", adminHandler.DeleteUser)
|
||||
adminAPI.POST("/user/recover", adminHandler.RecoverAccount)
|
||||
adminAPI.GET("/email-hash", adminHandler.GetEmailHash)
|
||||
|
||||
@@ -46,6 +46,11 @@ type UpdateSubscriptionRequest struct {
|
||||
Attributes SubscriptionAttributes `json:"attributes"`
|
||||
}
|
||||
|
||||
type ChangeEmailRequest struct {
|
||||
UserID int64 `json:"userID" binding:"required"`
|
||||
Email string `json:"email" binding:"required"`
|
||||
}
|
||||
|
||||
type AddOnAction string
|
||||
|
||||
const (
|
||||
|
||||
@@ -306,6 +306,25 @@ func (h *AdminHandler) UpdateSubscription(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{})
|
||||
}
|
||||
|
||||
func (h *AdminHandler) ChangeEmail(c *gin.Context) {
|
||||
var r ente.ChangeEmailRequest
|
||||
if err := c.ShouldBindJSON(&r); err != nil {
|
||||
handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "Bad request"))
|
||||
return
|
||||
}
|
||||
adminID := auth.GetUserID(c.Request.Header)
|
||||
go h.DiscordController.NotifyAdminAction(
|
||||
fmt.Sprintf("Admin (%d) updating email for user: %d", adminID, r.UserID))
|
||||
err := h.UserController.UpdateEmail(c, r.UserID, r.Email)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to update email")
|
||||
handler.Error(c, stacktrace.Propagate(err, ""))
|
||||
return
|
||||
}
|
||||
logrus.Info("Updated email")
|
||||
c.JSON(http.StatusOK, gin.H{})
|
||||
}
|
||||
|
||||
func (h *AdminHandler) ReQueueItem(c *gin.Context) {
|
||||
var r ente.ReQueueItemRequest
|
||||
if err := c.ShouldBindJSON(&r); err != nil {
|
||||
|
||||
@@ -197,7 +197,13 @@ func (c *UserController) ChangeEmail(ctx *gin.Context, request ente.EmailVerific
|
||||
if err != nil {
|
||||
return stacktrace.Propagate(err, "")
|
||||
}
|
||||
_, err = c.UserRepo.GetUserIDWithEmail(email)
|
||||
|
||||
return c.UpdateEmail(ctx, auth.GetUserID(ctx.Request.Header), email)
|
||||
}
|
||||
|
||||
// UpdateEmail updates the email address of the user with the provided userID
|
||||
func (c *UserController) UpdateEmail(ctx *gin.Context, userID int64, email string) error {
|
||||
_, err := c.UserRepo.GetUserIDWithEmail(email)
|
||||
if err == nil {
|
||||
// email already owned by a user
|
||||
return stacktrace.Propagate(ente.ErrPermissionDenied, "")
|
||||
@@ -206,7 +212,6 @@ func (c *UserController) ChangeEmail(ctx *gin.Context, request ente.EmailVerific
|
||||
// unknown error, rethrow
|
||||
return stacktrace.Propagate(err, "")
|
||||
}
|
||||
userID := auth.GetUserID(ctx.Request.Header)
|
||||
user, err := c.UserRepo.Get(userID)
|
||||
if err != nil {
|
||||
return stacktrace.Propagate(err, "")
|
||||
|
||||
@@ -68,7 +68,12 @@
|
||||
#
|
||||
# NEXT_PUBLIC_ENTE_FAMILY_PORTAL_ENDPOINT = http://localhost:3003
|
||||
|
||||
# The path of the JSON file which contains the expected results of our
|
||||
# integration tests. See `upload.test.ts` for more details.
|
||||
# The JSON which describes the expected results of our integration tests. See
|
||||
# `upload.test.ts` for more details of the expected format.
|
||||
#
|
||||
# NEXT_PUBLIC_ENTE_TEST_EXPECTED_JSON_PATH = /path/to/dataset/expected.json
|
||||
# This is perhaps easier to specify as an environment variable, since then we
|
||||
# can directly read from the source file when running `yarn dev`. For example,
|
||||
#
|
||||
# NEXT_PUBLIC_ENTE_TEST_EXPECTED_JSON=`cat path/to/expected.json` yarn dev
|
||||
#
|
||||
# NEXT_PUBLIC_ENTE_TEST_EXPECTED_JSON = {}
|
||||
|
||||
@@ -100,13 +100,13 @@ const FILE_NAME_TO_JSON_NAME = [
|
||||
];
|
||||
|
||||
export async function testUpload() {
|
||||
const jsonPath = process.env.NEXT_PUBLIC_ENTE_TEST_EXPECTED_JSON_PATH;
|
||||
if (!jsonPath) {
|
||||
const jsonString = process.env.NEXT_PUBLIC_ENTE_TEST_EXPECTED_JSON;
|
||||
if (!jsonString) {
|
||||
throw Error(
|
||||
"Please specify the NEXT_PUBLIC_ENTE_TEST_EXPECTED_JSON_PATH to run the upload tests",
|
||||
"Please specify the NEXT_PUBLIC_ENTE_TEST_EXPECTED_JSON to run the upload tests",
|
||||
);
|
||||
}
|
||||
const expectedState = await import(jsonPath);
|
||||
const expectedState = JSON.parse(jsonString);
|
||||
if (!expectedState) {
|
||||
throw Error("upload test failed expectedState missing");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user