Compare commits

..

49 Commits

Author SHA1 Message Date
Neeraj Gupta
d67c6aef53 Merge branch 'main' into f-droid 2024-11-28 11:01:39 +05:30
Neeraj Gupta
6ebb5d5bf4 Merge branch 'f-droid' of https://github.com/ente-io/auth into f-droid 2024-11-28 11:00:11 +05:30
ashilkn
224b79b648 Merge tag 'photos-v0.9.58' into f-droid 2024-11-08 16:08:08 +05:30
Neeraj Gupta
7e0a3cdd6c Merge branch 'main' into f-droid 2024-10-24 13:29:54 +05:30
ashilkn
f6db381e20 [mob][photos] Resolve merge conflicts and merge main 2024-10-23 11:25:54 +05:30
ashilkn
f0c29fef5c Merge branch 'main' into f-droid 2024-10-16 17:06:01 +05:30
Neeraj Gupta
2a3e317725 Merge branch 'main' into f-droid 2024-10-15 21:01:21 +05:30
ashilkn
1a1b3ebf12 [mob][photos] Resolve merge conflicts and merge main 2024-10-09 13:52:19 +05:30
Neeraj Gupta
f995589a02 Merge branch 'main' into f-droid 2024-09-29 12:04:26 +05:30
Neeraj Gupta
6e0990d658 Merge branch 'main' into f-droid 2024-09-20 15:56:08 +05:30
Neeraj Gupta
4da4261f4c Update flutter to 3.24.3 2024-09-20 15:00:23 +05:30
Neeraj Gupta
0abe66ea8c Merge branch 'main' into f-droid 2024-09-20 14:49:17 +05:30
Neeraj Gupta
193b27a186 Merge commit '0a1e062c' into f-droid 2024-09-06 15:30:52 +05:30
Neeraj Gupta
e323096172 Merge tag 'photos-v0.9.30' into f-droid 2024-08-27 17:20:23 +05:30
ashilkn
e41f306ac8 [mob][photos] Resolve merge conflicts and merge main 2024-07-31 12:02:25 +05:30
Neeraj Gupta
01d45d7c14 Merge branch 'main' into f-droid 2024-07-19 15:53:08 +05:30
ashilkn
d55a29336f Merge branch 'main' into f-droid 2024-07-08 20:50:35 +05:30
Neeraj Gupta
cfcbd0fbb2 Merge branch 'f-droid' of https://github.com/ente-io/auth into f-droid 2024-06-17 11:47:58 +05:30
Neeraj Gupta
21174548b5 Merge branch 'main' into f-droid 2024-06-17 11:47:42 +05:30
Neeraj Gupta
910f13e9a8 [mob][fdroid] Update flutter to v3.22.0 2024-06-17 11:31:36 +05:30
ashilkn
762688db28 Merge branch 'main' into f-droid 2024-06-13 10:29:55 +05:30
ashilkn
9df1ea0c57 Merge branch 'main' into f-droid 2024-06-12 17:33:12 +05:30
ashilkn
e48ab71fa4 [mob][photos] f-droid: upgrade flutter submodule to version 3.22.2 2024-06-12 17:33:02 +05:30
ashilkn
246314367a [mob][photos] Update flutter submodule on f-droid 2024-06-04 13:14:24 +05:30
ashilkn
ad70bbb571 Merge branch 'main' into f-droid 2024-06-04 13:11:17 +05:30
Neeraj Gupta
3962c55140 Update flutter submodule: v3.22.0 2024-06-03 11:26:02 +05:30
Neeraj Gupta
82e478bb12 Merge branch 'f-droid' of https://github.com/ente-io/auth into f-droid 2024-06-03 11:25:26 +05:30
Neeraj Gupta
63c8e98492 Merge branch 'main' into f-droid 2024-06-03 11:21:35 +05:30
ashilkn
ae92d2f759 Merge branch 'main' into f-droid 2024-05-28 12:37:14 +05:30
ashilkn
761c3e6ac2 [mob][photos] Update flutter submodule on f-droid branch 2024-05-28 12:34:37 +05:30
ashilkn
f9a3009c60 [mob][photos] Resolve merge conflicts and merge 2024-05-28 12:28:03 +05:30
Neeraj Gupta
ca0474faca Updated submodule mobile/thirdparty/flutter to 3.22.1 2024-05-23 17:00:33 +05:30
Neeraj Gupta
b469985277 Removed submodule mobile/thirdparty/isar 2024-05-23 16:58:51 +05:30
Neeraj Gupta
2a5dacb460 Merge branch 'main' into f-droid 2024-05-23 16:55:27 +05:30
vishnukvmd
d16f98cf07 v0.8.95 2024-05-12 08:44:26 +05:30
vishnukvmd
8677cbb4f8 Increase JVM allocation pool 2024-05-12 08:43:55 +05:30
vishnukvmd
0e33299863 Merge branch 'main' into f-droid 2024-05-07 12:54:44 +05:30
ashilkn
93ba4e011a Merge branch 'main' into f-droid 2024-04-20 15:23:14 +05:30
vishnukvmd
7977bebcaa Update Flutter to v3.19.3 2024-04-16 11:35:32 +05:30
ashilkn
f28f49d724 Merge main 2024-04-15 11:20:03 +05:30
ashilkn
d9a93ddad6 Merge branch 'main' into f-droid 2024-04-13 15:24:56 +05:30
ashilkn
07808d6139 Merge branch 'main' into f-droid 2024-04-02 17:22:34 +05:30
vishnukvmd
1e1633bb45 Merge branch 'main' into f-droid 2024-03-13 21:57:19 +05:30
vishnukvmd
c0f33de0c8 Remove dead code 2024-03-13 21:56:09 +05:30
vishnukvmd
417621b17c Pull code for transistor-background-fetch 2024-03-13 14:14:19 +05:30
vishnukvmd
8322540732 Add submodule for Flutter 2024-03-13 14:13:40 +05:30
vishnukvmd
2d61be37bb Add submodule for Isar 2024-03-13 14:12:23 +05:30
vishnukvmd
2a10aa7d61 Merge branch 'fdroid_cleanup' into f-droid 2024-03-13 13:52:25 +05:30
vishnukvmd
004eb310b3 Prepare for F-Droid 2024-03-13 13:43:46 +05:30
166 changed files with 3904 additions and 2346 deletions

3
.gitmodules vendored
View File

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

View File

@@ -66,19 +66,16 @@ best to start small. Consider some well-scoped changes, say like adding more
[custom icons to auth](auth/docs/adding-icons.md).
Each of the individual product/platform specific directories in this repository
have instructions on setting up a dev environment.
have instructions on setting up a dev environment and making changes. The issues
and discussions (feature requests) labelled "good first issues" should be good
starting points. Once you have a bearing, you can head on to issues or
discussions labelled "help wanted".
For anything beyond trivial bug fixes, please use [features requests and
discussions](https://github.com/ente-io/ente/discussions) instead of performing
code changes directly.
> [!TIP]
>
> Please remember that code is a important, but small, part of the overall big
> picture that makes a product a joy to use. Something that's easy in code is
> not necessarily the right choice for the product as a whole. So we'll repeat -
> there are other ways to contribute than code that we'd request you to
> consider.
If you're planning on adding a new feature or making any other substantial
change, please [discuss it with
us](https://github.com/ente-io/ente/discussions). Discussing your idea with us
first ensures that everyone is on the same page before you start working on your
change.
## Leave a review or star

View File

@@ -342,15 +342,6 @@
"Government Gateway"
]
},
{
"title": "Government of Canada",
"slug": "canada_flag",
"altNames": [
"CRA",
"CRA/ARC",
"Canada Revenue Agency"
]
},
{
"title": "Guideline"
},

View File

@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" viewBox="0 0 9600 4800">
<title>Flag of Canada</title>
<path fill="#f00" d="m0 0h2400l99 99h4602l99-99h2400v4800h-2400l-99-99h-4602l-99 99H0z"/>
<path fill="#fff" d="m2400 0h4800v4800h-4800zm2490 4430-45-863a95 95 0 0 1 111-98l859 151-116-320a65 65 0 0 1 20-73l941-762-212-99a65 65 0 0 1-34-79l186-572-542 115a65 65 0 0 1-73-38l-105-247-423 454a65 65 0 0 1-111-57l204-1052-327 189a65 65 0 0 1-91-27l-332-652-332 652a65 65 0 0 1-91 27l-327-189 204 1052a65 65 0 0 1-111 57l-423-454-105 247a65 65 0 0 1-73 38l-542-115 186 572a65 65 0 0 1-34 79l-212 99 941 762a65 65 0 0 1 20 73l-116 320 859-151a95 95 0 0 1 111 98l-45 863z"/>
</svg>

Before

Width:  |  Height:  |  Size: 699 B

View File

@@ -49,12 +49,8 @@ class _LoginPageState extends State<LoginPage> {
),
);
} else {
await UserService.instance.sendOtt(
context,
_email!,
isCreateAccountScreen: false,
purpose: 'login',
);
await UserService.instance
.sendOtt(context, _email!, isCreateAccountScreen: false);
}
FocusScope.of(context).unfocus();
}

View File

@@ -1,72 +0,0 @@
package cmd
import (
"fmt"
"os"
"net/url"
"strings"
"github.com/spf13/viper"
"github.com/spf13/cobra"
"github.com/ente-io/cli/pkg"
)
var setupCmd = &cobra.Command {
Use: "setup",
Short: "Manage setup/configuration settings",
}
// Command to set the public albums url in configurations/<environment>.yaml file
// Reads value from environment variable "ENTE_MUSEUM_DIR"
var addPublicAlbumsUrl = &cobra.Command {
Use: "public-albums-url account add-public-url [url]",
Short: "Set the public-albums URL in Museum YAML configuraiton file",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
// Get environment to make changes to valid file
environment := os.Getenv("ENVIRONMENT")
if environment == "" {
environment = "local"
}
dir := pkg.ConfigureServerDir()
// Adding path to the museum config file
viper.AddConfigPath(dir)
viper.SetConfigName(environment)
viper.SetConfigType("yaml")
err := viper.ReadInConfig()
if err != nil {
fmt.Errorf("%v", err)
return
}
albumsUrl := strings.TrimSuffix(args[0], "/")
_, err = url.ParseRequestURI(albumsUrl)
if err != nil {
// Report Error and exit
fmt.Printf("Invalid URL: %s\nPlease enter a valid endpoint for public albums", err)
os.Exit(1)
} else {
viper.Set("apps.public-albums", albumsUrl)
}
// Overwrite the public album url in Configuration File
err = viper.WriteConfig()
if err != nil {
fmt.Println("Error saving config: %v\n", err)
} else {
fmt.Println("Public Albums URL set to", albumsUrl)
}
},
}
func init() {
// `ente setup` will be the initial root command
rootCmd.AddCommand(setupCmd)
rootCmd.Flags().String("public-albums-url", "", "Sets the public-albums url in your environments configuration file")
setupCmd.AddCommand(addPublicAlbumsUrl)
}

View File

@@ -41,17 +41,3 @@ func GetCLITempPath() (string) {
}
return os.TempDir()
}
// Configure Museum Server Directory to find proper <environment.yaml> file.
// Made for and used in command `ente account public-albums-url [url]`
func ConfigureServerDir() (string) {
serverEnv := os.Getenv("ENTE_SERVER_DIR")
if serverEnv != "" {
fmt.Errorf(`ENTE_SERVER_DIR environment is not set, please setup it with\n
export ENTE_SERVER_DIR=/path/to/ente/
`)
}
configDir := filepath.Join(serverEnv, "server", "configurations")
return configDir
}

View File

@@ -1,13 +1,10 @@
# CHANGELOG
## v1.7.8 (Unreleased)
- .
## v1.7.7
## v1.7.7 (Unreleased)
- Retain JPEG originals even on date modifications.
- Support Portuguese and Vietnamese translations.
- .
## v1.7.6

View File

@@ -1,6 +1,6 @@
{
"name": "ente",
"version": "1.7.8-beta",
"version": "1.7.7-beta",
"private": true,
"description": "Desktop client for Ente Photos",
"repository": "github:ente-io/photos-desktop",
@@ -30,14 +30,14 @@
"clip-bpe-js": "^0.0.6",
"comlink": "^4.4.2",
"compare-versions": "^6.1.1",
"electron-log": "^5.2.3",
"electron-log": "^5.2.2",
"electron-store": "^8.2.0",
"electron-updater": "^6.3.9",
"ffmpeg-static": "^5.2.0",
"lru-cache": "^11.0.2",
"next-electron-server": "^1.0.0",
"node-stream-zip": "^1.15.0",
"onnxruntime-node": "^1.20.1"
"onnxruntime-node": "^1.20.0"
},
"devDependencies": {
"@eslint/js": "^9.15.0",
@@ -48,15 +48,15 @@
"ajv": "^8.17.1",
"concurrently": "^9.1.0",
"cross-env": "^7.0.3",
"electron": "^33.2.1",
"electron": "^33.2.0",
"electron-builder": "^25.1.8",
"eslint": "^9",
"prettier": "3.3.3",
"prettier": "^3.3.3",
"prettier-plugin-organize-imports": "^4.1.0",
"prettier-plugin-packagejson": "^2.5.6",
"prettier-plugin-packagejson": "^2.5.3",
"shx": "^0.3.4",
"typescript": "^5.7.2",
"typescript-eslint": "^8.16.0"
"typescript": "^5.6.3",
"typescript-eslint": "^8.14.0"
},
"packageManager": "yarn@1.22.22",
"productName": "ente"

View File

@@ -384,62 +384,62 @@
dependencies:
"@types/node" "*"
"@typescript-eslint/eslint-plugin@8.16.0":
version "8.16.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.16.0.tgz#ac56825bcdf3b392fc76a94b1315d4a162f201a6"
integrity sha512-5YTHKV8MYlyMI6BaEG7crQ9BhSc8RxzshOReKwZwRWN0+XvvTOm+L/UYLCYxFpfwYuAAqhxiq4yae0CMFwbL7Q==
"@typescript-eslint/eslint-plugin@8.14.0":
version "8.14.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.14.0.tgz#7dc0e419c87beadc8f554bf5a42e5009ed3748dc"
integrity sha512-tqp8H7UWFaZj0yNO6bycd5YjMwxa6wIHOLZvWPkidwbgLCsBMetQoGj7DPuAlWa2yGO3H48xmPwjhsSPPCGU5w==
dependencies:
"@eslint-community/regexpp" "^4.10.0"
"@typescript-eslint/scope-manager" "8.16.0"
"@typescript-eslint/type-utils" "8.16.0"
"@typescript-eslint/utils" "8.16.0"
"@typescript-eslint/visitor-keys" "8.16.0"
"@typescript-eslint/scope-manager" "8.14.0"
"@typescript-eslint/type-utils" "8.14.0"
"@typescript-eslint/utils" "8.14.0"
"@typescript-eslint/visitor-keys" "8.14.0"
graphemer "^1.4.0"
ignore "^5.3.1"
natural-compare "^1.4.0"
ts-api-utils "^1.3.0"
"@typescript-eslint/parser@8.16.0":
version "8.16.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.16.0.tgz#ee5b2d6241c1ab3e2e53f03fd5a32d8e266d8e06"
integrity sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==
"@typescript-eslint/parser@8.14.0":
version "8.14.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.14.0.tgz#0a7e9dbc11bc07716ab2d7b1226217e9f6b51fc8"
integrity sha512-2p82Yn9juUJq0XynBXtFCyrBDb6/dJombnz6vbo6mgQEtWHfvHbQuEa9kAOVIt1c9YFwi7H6WxtPj1kg+80+RA==
dependencies:
"@typescript-eslint/scope-manager" "8.16.0"
"@typescript-eslint/types" "8.16.0"
"@typescript-eslint/typescript-estree" "8.16.0"
"@typescript-eslint/visitor-keys" "8.16.0"
"@typescript-eslint/scope-manager" "8.14.0"
"@typescript-eslint/types" "8.14.0"
"@typescript-eslint/typescript-estree" "8.14.0"
"@typescript-eslint/visitor-keys" "8.14.0"
debug "^4.3.4"
"@typescript-eslint/scope-manager@8.16.0":
version "8.16.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.16.0.tgz#ebc9a3b399a69a6052f3d88174456dd399ef5905"
integrity sha512-mwsZWubQvBki2t5565uxF0EYvG+FwdFb8bMtDuGQLdCCnGPrDEDvm1gtfynuKlnpzeBRqdFCkMf9jg1fnAK8sg==
"@typescript-eslint/scope-manager@8.14.0":
version "8.14.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.14.0.tgz#01f37c147a735cd78f0ff355e033b9457da1f373"
integrity sha512-aBbBrnW9ARIDn92Zbo7rguLnqQ/pOrUguVpbUwzOhkFg2npFDwTgPGqFqE0H5feXcOoJOfX3SxlJaKEVtq54dw==
dependencies:
"@typescript-eslint/types" "8.16.0"
"@typescript-eslint/visitor-keys" "8.16.0"
"@typescript-eslint/types" "8.14.0"
"@typescript-eslint/visitor-keys" "8.14.0"
"@typescript-eslint/type-utils@8.16.0":
version "8.16.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.16.0.tgz#585388735f7ac390f07c885845c3d185d1b64740"
integrity sha512-IqZHGG+g1XCWX9NyqnI/0CX5LL8/18awQqmkZSl2ynn8F76j579dByc0jhfVSnSnhf7zv76mKBQv9HQFKvDCgg==
"@typescript-eslint/type-utils@8.14.0":
version "8.14.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.14.0.tgz#455c6af30c336b24a1af28bc4f81b8dd5d74d94d"
integrity sha512-Xcz9qOtZuGusVOH5Uk07NGs39wrKkf3AxlkK79RBK6aJC1l03CobXjJbwBPSidetAOV+5rEVuiT1VSBUOAsanQ==
dependencies:
"@typescript-eslint/typescript-estree" "8.16.0"
"@typescript-eslint/utils" "8.16.0"
"@typescript-eslint/typescript-estree" "8.14.0"
"@typescript-eslint/utils" "8.14.0"
debug "^4.3.4"
ts-api-utils "^1.3.0"
"@typescript-eslint/types@8.16.0":
version "8.16.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.16.0.tgz#49c92ae1b57942458ab83d9ec7ccab3005e64737"
integrity sha512-NzrHj6thBAOSE4d9bsuRNMvk+BvaQvmY4dDglgkgGC0EW/tB3Kelnp3tAKH87GEwzoxgeQn9fNGRyFJM/xd+GQ==
"@typescript-eslint/types@8.14.0":
version "8.14.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.14.0.tgz#0d33d8d0b08479c424e7d654855fddf2c71e4021"
integrity sha512-yjeB9fnO/opvLJFAsPNYlKPnEM8+z4og09Pk504dkqonT02AyL5Z9SSqlE0XqezS93v6CXn49VHvB2G7XSsl0g==
"@typescript-eslint/typescript-estree@8.16.0":
version "8.16.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.16.0.tgz#9d741e56e5b13469b5190e763432ce5551a9300c"
integrity sha512-E2+9IzzXMc1iaBy9zmo+UYvluE3TW7bCGWSF41hVWUE01o8nzr1rvOQYSxelxr6StUvRcTMe633eY8mXASMaNw==
"@typescript-eslint/typescript-estree@8.14.0":
version "8.14.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.14.0.tgz#a7a3a5a53a6c09313e12fb4531d4ff582ee3c312"
integrity sha512-OPXPLYKGZi9XS/49rdaCbR5j/S14HazviBlUQFvSKz3npr3NikF+mrgK7CFVur6XEt95DZp/cmke9d5i3vtVnQ==
dependencies:
"@typescript-eslint/types" "8.16.0"
"@typescript-eslint/visitor-keys" "8.16.0"
"@typescript-eslint/types" "8.14.0"
"@typescript-eslint/visitor-keys" "8.14.0"
debug "^4.3.4"
fast-glob "^3.3.2"
is-glob "^4.0.3"
@@ -447,23 +447,23 @@
semver "^7.6.0"
ts-api-utils "^1.3.0"
"@typescript-eslint/utils@8.16.0":
version "8.16.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.16.0.tgz#c71264c437157feaa97842809836254a6fc833c3"
integrity sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA==
"@typescript-eslint/utils@8.14.0":
version "8.14.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.14.0.tgz#ac2506875e03aba24e602364e43b2dfa45529dbd"
integrity sha512-OGqj6uB8THhrHj0Fk27DcHPojW7zKwKkPmHXHvQ58pLYp4hy8CSUdTKykKeh+5vFqTTVmjz0zCOOPKRovdsgHA==
dependencies:
"@eslint-community/eslint-utils" "^4.4.0"
"@typescript-eslint/scope-manager" "8.16.0"
"@typescript-eslint/types" "8.16.0"
"@typescript-eslint/typescript-estree" "8.16.0"
"@typescript-eslint/scope-manager" "8.14.0"
"@typescript-eslint/types" "8.14.0"
"@typescript-eslint/typescript-estree" "8.14.0"
"@typescript-eslint/visitor-keys@8.16.0":
version "8.16.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.16.0.tgz#d5086afc060b01ff7a4ecab8d49d13d5a7b07705"
integrity sha512-pq19gbaMOmFE3CbL0ZB8J8BFCo2ckfHBfaIsaOZgBIF4EoISJIdLX5xRhd0FGB0LlHReNRuzoJoMGpTjq8F2CQ==
"@typescript-eslint/visitor-keys@8.14.0":
version "8.14.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.14.0.tgz#2418d5a54669af9658986ade4e6cfb7767d815ad"
integrity sha512-vG0XZo8AdTH9OE6VFRwAZldNc7qtJ/6NLGWak+BtENuEUXGZgFpihILPiBvKXvJ2nFu27XNGC6rKiwuaoMbYzQ==
dependencies:
"@typescript-eslint/types" "8.16.0"
eslint-visitor-keys "^4.2.0"
"@typescript-eslint/types" "8.14.0"
eslint-visitor-keys "^3.4.3"
"@xmldom/xmldom@^0.8.8":
version "0.8.10"
@@ -1163,6 +1163,13 @@ dir-compare@^4.2.0:
minimatch "^3.0.5"
p-limit "^3.1.0 "
dir-glob@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
dependencies:
path-type "^4.0.0"
dmg-builder@25.1.8:
version "25.1.8"
resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-25.1.8.tgz#41f3b725edd896156e891016a44129e1bd580430"
@@ -1233,10 +1240,10 @@ electron-builder@^25.1.8:
simple-update-notifier "2.0.0"
yargs "^17.6.2"
electron-log@^5.2.3:
version "5.2.3"
resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-5.2.3.tgz#b58447b2158b9aa5faa07c4d15340113069d3f15"
integrity sha512-BabCiEV+p362LzY0EFE8hyzeGknzKDWSbhS0VFfRYQGA4FHWXWSfaKJlvTR9LFepNoORXxc/BWvqBXIPgsVFgA==
electron-log@^5.2.2:
version "5.2.2"
resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-5.2.2.tgz#cdb0a6dc48178a7cbacb434a268ab097ad5198dc"
integrity sha512-fgvx6srjIHDowJD8WAAjoAXmiTyOz6JnGQoxOtk1mXw7o4S+HutuPHLCsk24xTXqWZgy4uO63NbedG+oEvldLw==
electron-publish@25.1.7:
version "25.1.7"
@@ -1273,10 +1280,10 @@ electron-updater@^6.3.9:
semver "^7.6.3"
tiny-typed-emitter "^2.1.0"
electron@^33.2.1:
version "33.2.1"
resolved "https://registry.yarnpkg.com/electron/-/electron-33.2.1.tgz#d0d7bba7a7abf4f14881d0a6e03c498b301a2d5f"
integrity sha512-SG/nmSsK9Qg1p6wAW+ZfqU+AV8cmXMTIklUL18NnOKfZLlum4ZsDoVdmmmlL39ZmeCaq27dr7CgslRPahfoVJg==
electron@^33.2.0:
version "33.2.0"
resolved "https://registry.yarnpkg.com/electron/-/electron-33.2.0.tgz#2a7098653eaf1a53c7311a01d5636783019f2354"
integrity sha512-PVw1ICAQDPsnnsmpNFX/b1i/49h67pbSPxuIENd9K9WpGO1tsRaQt+K2bmXqTuoMJsbzIc75Ce8zqtuwBPqawA==
dependencies:
"@electron/get" "^2.0.0"
"@types/node" "^20.9.0"
@@ -1346,7 +1353,7 @@ eslint-scope@^8.0.2:
esrecurse "^4.3.0"
estraverse "^5.2.0"
eslint-visitor-keys@^3.3.0:
eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.3:
version "3.4.3"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
@@ -1356,11 +1363,6 @@ eslint-visitor-keys@^4.0.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz#e3adc021aa038a2a8e0b2f8b0ce8f66b9483b1fb"
integrity sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==
eslint-visitor-keys@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45"
integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==
eslint@^9:
version "9.9.1"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.9.1.tgz#147ac9305d56696fb84cf5bdecafd6517ddc77ec"
@@ -1460,7 +1462,7 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-glob@^3.3.2:
fast-glob@^3.3.0, fast-glob@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==
@@ -1500,11 +1502,6 @@ fd-slicer@~1.1.0:
dependencies:
pend "~1.2.0"
fdir@^6.4.2:
version "6.4.2"
resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.2.tgz#ddaa7ce1831b161bc3657bb99cb36e1622702689"
integrity sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==
ffmpeg-static@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/ffmpeg-static/-/ffmpeg-static-5.2.0.tgz#6ca64a5ed6e69ec4896d175c1f69dd575db7c5ef"
@@ -1761,6 +1758,17 @@ globalthis@^1.0.1:
define-properties "^1.2.1"
gopd "^1.0.1"
globby@^13.1.2:
version "13.2.2"
resolved "https://registry.yarnpkg.com/globby/-/globby-13.2.2.tgz#63b90b1bf68619c2135475cbd4e71e66aa090592"
integrity sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==
dependencies:
dir-glob "^3.0.1"
fast-glob "^3.3.0"
ignore "^5.2.4"
merge2 "^1.4.1"
slash "^4.0.0"
gopd@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
@@ -1916,7 +1924,7 @@ ieee754@^1.1.13:
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
ignore@^5.2.0, ignore@^5.3.1:
ignore@^5.2.0, ignore@^5.2.4, ignore@^5.3.1:
version "5.3.2"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
@@ -2259,7 +2267,7 @@ matcher@^3.0.0:
dependencies:
escape-string-regexp "^4.0.0"
merge2@^1.3.0:
merge2@^1.3.0, merge2@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
@@ -2543,17 +2551,17 @@ onetime@^5.1.0, onetime@^5.1.2:
dependencies:
mimic-fn "^2.1.0"
onnxruntime-common@1.20.1:
version "1.20.1"
resolved "https://registry.yarnpkg.com/onnxruntime-common/-/onnxruntime-common-1.20.1.tgz#b42e317d4d6728745b9e8089617c8cd938d312dc"
integrity sha512-YiU0s0IzYYC+gWvqD1HzLc46Du1sXpSiwzKb63PACIJr6LfL27VsXSXQvt68EzD3V0D5Bc0vyJTjmMxp0ylQiw==
onnxruntime-common@1.20.0:
version "1.20.0"
resolved "https://registry.yarnpkg.com/onnxruntime-common/-/onnxruntime-common-1.20.0.tgz#e1774cf76ede788838ff7bd4debc8df6feca91f1"
integrity sha512-9ehS4ul5fBszIcHhfxuDgk45lO+Fqrxmrgwk1Pxb1JRvbQiCB/v9Royv95SRCWHktLMviqNjBsEd/biJhd39cg==
onnxruntime-node@^1.20.1:
version "1.20.1"
resolved "https://registry.yarnpkg.com/onnxruntime-node/-/onnxruntime-node-1.20.1.tgz#a5ba0bd160aeccdb4b7d36fbc2f6a97bde1f7843"
integrity sha512-di/I4HDXRw+FLgq+TyHmQEDd3cEp9iFFZm0r4uJ1Wd7b/WE1VXtKWo8yemex347c6GNF/3Pv86ZfPhIWxORr0w==
onnxruntime-node@^1.20.0:
version "1.20.0"
resolved "https://registry.yarnpkg.com/onnxruntime-node/-/onnxruntime-node-1.20.0.tgz#16dcbe06e7683eee37ccbd3f39ad2beac36c1a24"
integrity sha512-mjLge++8WHfyCZ4IqZ1FbUbtFAfGht7BLCkOeBL1L9PFV27YHwluXkNt7m0Pgf6TR2P5pqVZsD3zqFbFP6QTMw==
dependencies:
onnxruntime-common "1.20.1"
onnxruntime-common "1.20.0"
tar "^7.0.1"
optionator@^0.9.3:
@@ -2678,6 +2686,11 @@ path-scurry@^1.11.1:
lru-cache "^10.2.0"
minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
path-type@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
pe-library@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/pe-library/-/pe-library-0.4.1.tgz#e269be0340dcb13aa6949d743da7d658c3e2fbea"
@@ -2693,11 +2706,6 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
picomatch@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab"
integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==
pkg-up@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5"
@@ -2724,15 +2732,15 @@ prettier-plugin-organize-imports@^4.1.0:
resolved "https://registry.yarnpkg.com/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.1.0.tgz#f3d3764046a8e7ba6491431158b9be6ffd83b90f"
integrity sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A==
prettier-plugin-packagejson@^2.5.6:
version "2.5.6"
resolved "https://registry.yarnpkg.com/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.6.tgz#cd3cca60e1aa87ee3ce3b4032c1c999798d9f714"
integrity sha512-TY7KiLtyt6Tlf53BEbXUWkN0+TRdHKgIMmtXtDCyHH6yWnZ50Lwq6Vb6lyjapZrhDTXooC4EtlY5iLe1sCgi5w==
prettier-plugin-packagejson@^2.5.3:
version "2.5.3"
resolved "https://registry.yarnpkg.com/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.3.tgz#a3f9eb02ece197db6b7696be5df43ddc2397ad81"
integrity sha512-ATMEEXr+ywls1kgrZEWl4SBPEm0uDdyDAjyNzUC0/Z8WZTD3RqbJcQDR+Dau+wYkW9KHK6zqQIsFyfn+9aduWg==
dependencies:
sort-package-json "2.12.0"
sort-package-json "2.10.1"
synckit "0.9.2"
prettier@3.3.3:
prettier@^3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105"
integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==
@@ -3009,6 +3017,11 @@ simple-update-notifier@2.0.0:
dependencies:
semver "^7.5.3"
slash@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7"
integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==
slice-ansi@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787"
@@ -3045,19 +3058,19 @@ sort-object-keys@^1.1.3:
resolved "https://registry.yarnpkg.com/sort-object-keys/-/sort-object-keys-1.1.3.tgz#bff833fe85cab147b34742e45863453c1e190b45"
integrity sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==
sort-package-json@2.12.0:
version "2.12.0"
resolved "https://registry.yarnpkg.com/sort-package-json/-/sort-package-json-2.12.0.tgz#4196a1ba82ba63c4a512add1d00ab39026bf8ab7"
integrity sha512-/HrPQAeeLaa+vbAH/znjuhwUluuiM/zL5XX9kop8UpDgjtyWKt43hGDk2vd/TBdDpzIyzIHVUgmYofzYrAQjew==
sort-package-json@2.10.1:
version "2.10.1"
resolved "https://registry.yarnpkg.com/sort-package-json/-/sort-package-json-2.10.1.tgz#18e7fa0172233cb2d4d926f7c99e6bfcf4d1d25c"
integrity sha512-d76wfhgUuGypKqY72Unm5LFnMpACbdxXsLPcL27pOsSrmVqH3PztFp1uq+Z22suk15h7vXmTesuh2aEjdCqb5w==
dependencies:
detect-indent "^7.0.1"
detect-newline "^4.0.0"
get-stdin "^9.0.0"
git-hooks-list "^3.0.0"
globby "^13.1.2"
is-plain-obj "^4.1.0"
semver "^7.6.0"
sort-object-keys "^1.1.3"
tinyglobby "^0.2.9"
source-map-support@^0.5.19:
version "0.5.21"
@@ -3193,14 +3206,6 @@ tiny-typed-emitter@^2.1.0:
resolved "https://registry.yarnpkg.com/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz#b3b027fdd389ff81a152c8e847ee2f5be9fad7b5"
integrity sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==
tinyglobby@^0.2.9:
version "0.2.10"
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.10.tgz#e712cf2dc9b95a1f5c5bbd159720e15833977a0f"
integrity sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==
dependencies:
fdir "^6.4.2"
picomatch "^4.0.2"
tmp-promise@^3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/tmp-promise/-/tmp-promise-3.0.3.tgz#60a1a1cc98c988674fcbfd23b6e3367bdeac4ce7"
@@ -3264,24 +3269,24 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
typescript-eslint@^8.16.0:
version "8.16.0"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.16.0.tgz#d608c972d6b2461ca10ec30fd3fa62a080baba19"
integrity sha512-wDkVmlY6O2do4V+lZd0GtRfbtXbeD0q9WygwXXSJnC1xorE8eqyC2L1tJimqpSeFrOzRlYtWnUp/uzgHQOgfBQ==
typescript-eslint@^8.14.0:
version "8.14.0"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.14.0.tgz#2435c0628e90303544fdd63ae311e9bf6d149a5d"
integrity sha512-K8fBJHxVL3kxMmwByvz8hNdBJ8a0YqKzKDX6jRlrjMuNXyd5T2V02HIq37+OiWXvUUOXgOOGiSSOh26Mh8pC3w==
dependencies:
"@typescript-eslint/eslint-plugin" "8.16.0"
"@typescript-eslint/parser" "8.16.0"
"@typescript-eslint/utils" "8.16.0"
"@typescript-eslint/eslint-plugin" "8.14.0"
"@typescript-eslint/parser" "8.14.0"
"@typescript-eslint/utils" "8.14.0"
typescript@^5.4.3:
version "5.5.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba"
integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==
typescript@^5.7.2:
version "5.7.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6"
integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==
typescript@^5.6.3:
version "5.6.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.3.tgz#5f3449e31c9d94febb17de03cc081dd56d81db5b"
integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==
undici-types@~6.19.2:
version "6.19.8"

View File

@@ -1,7 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="io.ente.photos">
<application android:name="${applicationName}"
<application
tools:replace="android:label"
android:name="${applicationName}"
android:label="@string/app_name"
android:icon="@mipmap/launcher_icon"
android:usesCleartextTraffic="true"

View File

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

View File

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

View File

@@ -21,8 +21,6 @@ const List<Locale> appSupportedLocales = <Locale>[
Locale("pt", "BR"),
Locale("ru"),
Locale("tr"),
Locale("uk"),
Locale("vi"),
Locale("zh", "CN"),
];

View File

@@ -4,7 +4,6 @@ import 'dart:io';
import "package:adaptive_theme/adaptive_theme.dart";
import 'package:background_fetch/background_fetch.dart';
import "package:computer/computer.dart";
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import "package:flutter/rendering.dart";
@@ -39,7 +38,7 @@ import 'package:photos/services/machine_learning/ml_service.dart';
import 'package:photos/services/machine_learning/semantic_search/semantic_search_service.dart';
import 'package:photos/services/memories_service.dart';
import "package:photos/services/notification_service.dart";
import 'package:photos/services/push_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/sync_service.dart";
@@ -273,11 +272,11 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
if (Platform.isIOS) {
// ignore: unawaited_futures
PushService.instance.init().then((_) {
FirebaseMessaging.onBackgroundMessage(
_firebaseMessagingBackgroundHandler,
);
});
// PushService.instance.init().then((_) {
// FirebaseMessaging.onBackgroundMessage(
// _firebaseMessagingBackgroundHandler,
// );
// });
}
_logger.info("PushService/HomeWidget done $tlog");
unawaited(SemanticSearchService.instance.init());
@@ -401,35 +400,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(SharedPreferences prefs) async {
final bool isRunningInFG = await _isRunningInForeground();
await prefs.reload();

View File

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

View File

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

View File

@@ -93,12 +93,8 @@ class _LoginPageState extends State<LoginPage> {
),
);
} else {
await UserService.instance.sendOtt(
context,
_email!,
isCreateAccountScreen: false,
purpose: "login",
);
await UserService.instance
.sendOtt(context, _email!, isCreateAccountScreen: false);
}
FocusScope.of(context).unfocus();
},

View File

@@ -1,650 +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/service_locator.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/divider_widget.dart";
import "package:photos/ui/components/menu_item_widget/menu_item_widget.dart";
import "package:photos/ui/components/title_bar_title_widget.dart";
import 'package:photos/ui/payment/child_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/ui/tabs/home_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,
super.key,
});
@override
State<StoreSubscriptionPage> createState() => _StoreSubscriptionPageState();
}
class _StoreSubscriptionPageState extends State<StoreSubscriptionPage> {
final _logger = Logger("SubscriptionPage");
late final _billingService = billingService;
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() {
super.initState();
_billingService.setIsOnSubscriptionPage(true);
_setupPurchaseUpdateStreamListener();
}
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) {
final textTheme = getEnteTextTheme(context);
colorScheme = getEnteColorScheme(context);
if (!_isLoading) {
_isLoading = true;
_fetchSubData();
}
_dialog = createProgressDialog(context, S.of(context).pleaseWait);
return Scaffold(
appBar: AppBar(),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TitleBarTitleWidget(
title: widget.isOnboarding
? S.of(context).selectYourPlan
: "${S.of(context).subscription}${kDebugMode ? ' Store' : ''}",
),
_isFreePlanUser() || !_hasLoadedData
? const SizedBox.shrink()
: Text(
convertBytesToReadableFormat(
_userDetails.getTotalStorage(),
),
style: textTheme.smallMuted,
),
],
),
),
Expanded(child: _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(),
),
);
if (hasYearlyPlans) {
widgets.add(
SubscriptionToggle(
onToggle: (p0) {
showYearlyPlan = p0;
_filterStorePlansForUi();
},
),
);
}
widgets.addAll([
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: _isActiveStripeSubscriber
? _getStripePlanWidgets()
: _getMobilePlanWidgets(),
),
const Padding(padding: EdgeInsets.all(4)),
]);
if (_currentSubscription != null) {
widgets.add(
ValidityWidget(
currentSubscription: _currentSubscription,
bonusData: _userDetails.bonusData,
),
);
widgets.add(const DividerWidget(dividerType: DividerType.bottomBar));
widgets.add(const SizedBox(height: 20));
} else {
widgets.add(const DividerWidget(dividerType: DividerType.bottomBar));
const SizedBox(height: 56);
}
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: const CaptionedTextWidget(
title: "Manage payment method",
),
menuItemColor: colorScheme.fillFaint,
trailingWidget: Icon(
Icons.chevron_right_outlined,
color: colorScheme.strokeBase,
),
singleBorderRadius: 4,
alignCaptionedTextToLeft: true,
onTap: () async {
_onPlatformRestrictedPaymentDetailsClick();
},
),
),
);
}
}
widgets.add(
SubFaqWidget(isOnboarding: widget.isOnboarding),
);
if (!widget.isOnboarding) {
widgets.add(
Padding(
padding: const EdgeInsets.fromLTRB(16, 2, 16, 2),
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(() {});
}
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(
GestureDetector(
onTap: () async {
if (widget.isOnboarding && plan.id == freeProductID) {
Bus.instance.fire(SubscriptionPurchasedEvent());
// ignore: unawaited_futures
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (BuildContext context) {
return const HomeWidget();
},
),
(route) => false,
);
unawaited(
_billingService.verifySubscription(
freeProductID,
"",
paymentProvider: "ente",
),
);
} else {
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,
isPopular: _isPopularPlan(plan),
isOnboarding: widget.isOnboarding,
),
),
);
}
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(
GestureDetector(
onTap: () {
if (_currentSubscription!.isFreePlan() && widget.isOnboarding) {
Bus.instance.fire(SubscriptionPurchasedEvent());
// ignore: unawaited_futures
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (BuildContext context) {
return const HomeWidget();
},
),
(route) => false,
);
unawaited(
_billingService.verifySubscription(
freeProductID,
"",
paymentProvider: "ente",
),
);
}
},
child: SubscriptionPlanWidget(
storage: _freePlan.storage,
price: "",
period: S.of(context).freeTrial,
isActive: true,
isOnboarding: widget.isOnboarding,
),
),
);
}
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(
GestureDetector(
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,
isPopular: _isPopularPlan(plan),
isOnboarding: widget.isOnboarding,
),
),
);
}
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,
GestureDetector(
onTap: () {
if (_currentSubscription!.isFreePlan() & widget.isOnboarding) {
Bus.instance.fire(SubscriptionPurchasedEvent());
// ignore: unawaited_futures
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (BuildContext context) {
return const HomeWidget();
},
),
(route) => false,
);
unawaited(
_billingService.verifySubscription(
freeProductID,
"",
paymentProvider: "ente",
),
);
}
},
child: SubscriptionPlanWidget(
storage: _currentSubscription!.storage,
price: _currentSubscription!.price,
period: _currentSubscription!.period,
isActive: true,
isOnboarding: widget.isOnboarding,
),
),
);
}
bool _isPopularPlan(BillingPlan plan) {
return popularProductIDs.contains(plan.id);
}
}

View File

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

View File

@@ -150,10 +150,6 @@ class _ItemsWidgetState extends State<ItemsWidget> {
return '한국어';
case 'ar':
return 'العربية';
case 'uk':
return 'Українська';
case 'vi':
return 'Tiếng Việt';
default:
return locale.languageCode;
}

View File

@@ -41,8 +41,8 @@ class FileDetailsWidget extends StatefulWidget {
const FileDetailsWidget(
this.file, {
super.key,
});
Key? key,
}) : super(key: key);
@override
State<FileDetailsWidget> createState() => _FileDetailsWidgetState();

View File

@@ -1,3 +1,4 @@
import "package:flutter/foundation.dart";
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:photos/models/file/file.dart';
@@ -22,8 +23,8 @@ class FileWidget extends StatelessWidget {
this.tagPrefix,
this.backgroundDecoration,
required this.isFromMemories,
super.key,
});
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
@@ -53,7 +54,6 @@ class FileWidget extends StatelessWidget {
file,
tagPrefix: tagPrefix,
playbackCallback: playbackCallback,
key: key ?? ValueKey(fileKey),
);
} else {
Logger('FileWidget').severe('unsupported file type ${file.fileType}');

View File

@@ -17,7 +17,7 @@ import "package:photos/utils/date_time_util.dart";
import "package:photos/utils/debouncer.dart";
class SearchWidget extends StatefulWidget {
const SearchWidget({super.key});
const SearchWidget({Key? key}) : super(key: key);
@override
State<SearchWidget> createState() => SearchWidgetState();
@@ -143,7 +143,6 @@ class SearchWidgetState extends State<SearchWidget> {
controller: textController,
focusNode: focusNode,
style: Theme.of(context).textTheme.titleMedium,
textAlignVertical: const TextAlignVertical(y: 0),
// Below parameters are to disable auto-suggestion
// Above parameters are to disable auto-suggestion
decoration: InputDecoration(
@@ -156,6 +155,21 @@ class SearchWidgetState extends State<SearchWidget> {
focusedBorder: const UnderlineInputBorder(
borderSide: BorderSide.none,
),
prefixIconConstraints: const BoxConstraints(
maxHeight: 44,
maxWidth: 44,
minHeight: 44,
minWidth: 44,
),
suffixIconConstraints: const BoxConstraints(
maxHeight: 44,
maxWidth: 44,
minHeight: 44,
minWidth: 44,
),
contentPadding: const EdgeInsets.symmetric(
vertical: 8,
),
prefixIcon: Hero(
tag: "search_icon",
child: Icon(

View File

@@ -26,7 +26,7 @@ import "package:photos/ui/viewer/search_tab/moments_section.dart";
import "package:photos/ui/viewer/search_tab/people_section.dart";
class SearchTab extends StatefulWidget {
const SearchTab({super.key});
const SearchTab({Key? key}) : super(key: key);
@override
State<SearchTab> createState() => _SearchTabState();
@@ -105,10 +105,7 @@ class _AllSearchSectionsState extends State<AllSearchSections> {
return Padding(
padding: const EdgeInsets.only(bottom: 72),
child: Text(
S.of(context).searchSectionsLengthMismatch(
snapshot.data!.length,
searchTypes.length,
),
S.of(context).searchSectionsLengthMismatch(snapshot.data!.length, searchTypes.length),
),
);
}

View File

@@ -22,7 +22,6 @@ Future<void> onTapCollectEventPhotos(BuildContext context) async {
alwaysShowSuccessState: false,
initialValue: currentDate,
textCapitalization: TextCapitalization.words,
popnavAfterSubmission: false,
onSubmit: (String text) async {
// indicates user cancelled the rename request
if (text.trim() == "") {
@@ -32,14 +31,14 @@ Future<void> onTapCollectEventPhotos(BuildContext context) async {
try {
final Collection c =
await CollectionsService.instance.createAlbum(text);
await routeToPage(
// ignore: unawaited_futures
routeToPage(
context,
CollectionPage(
isFromCollectPhotos: true,
CollectionWithThumbnail(c, null),
),
);
Navigator.of(context).pop();
} catch (e, s) {
Logger("Collect event photos from CollectPhotosCardWidget")
.severe("Failed to rename album", e, s);

View File

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

View File

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

View File

@@ -631,54 +631,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.9.3+3"
firebase_core:
dependency: "direct main"
description:
name: firebase_core
sha256: "51dfe2fbf3a984787a2e7b8592f2f05c986bfedd6fdacea3f9e0a7beb334de96"
url: "https://pub.dev"
source: hosted
version: "3.6.0"
firebase_core_platform_interface:
dependency: transitive
description:
name: firebase_core_platform_interface
sha256: e30da58198a6d4b49d5bce4e852f985c32cb10db329ebef9473db2b9f09ce810
url: "https://pub.dev"
source: hosted
version: "5.3.0"
firebase_core_web:
dependency: transitive
description:
name: firebase_core_web
sha256: f967a7138f5d2ffb1ce15950e2a382924239eaa521150a8f144af34e68b3b3e5
url: "https://pub.dev"
source: hosted
version: "2.18.1"
firebase_messaging:
dependency: "direct main"
description:
name: firebase_messaging
sha256: eb6e28a3a35deda61fe8634967c84215efc19133ba58d8e0fc6c9a2af2cba05e
url: "https://pub.dev"
source: hosted
version: "15.1.3"
firebase_messaging_platform_interface:
dependency: transitive
description:
name: firebase_messaging_platform_interface
sha256: b316c4ee10d93d32c033644207afc282d9b2b4372f3cf9c6022f3558b3873d2d
url: "https://pub.dev"
source: hosted
version: "4.5.46"
firebase_messaging_web:
dependency: transitive
description:
name: firebase_messaging_web
sha256: d7f0147a1a9fe4313168e20154a01fd5cf332898de1527d3930ff77b8c7f5387
url: "https://pub.dev"
source: hosted
version: "3.9.2"
fixnum:
dependency: transitive
description:
@@ -1253,38 +1205,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.1+1"
in_app_purchase:
dependency: "direct main"
description:
name: in_app_purchase
sha256: "960f26a08d9351fb8f89f08901f8a829d41b04d45a694b8f776121d9e41dcad6"
url: "https://pub.dev"
source: hosted
version: "3.2.0"
in_app_purchase_android:
dependency: transitive
description:
name: in_app_purchase_android
sha256: bee60266e443d4ae0e4809bae7f9cd4d6be75ab5ec6bb3d2ca737774a274a3bd
url: "https://pub.dev"
source: hosted
version: "0.3.6+9"
in_app_purchase_platform_interface:
dependency: transitive
description:
name: in_app_purchase_platform_interface
sha256: "1d353d38251da5b9fea6635c0ebfc6bb17a2d28d0e86ea5e083bf64244f1fb4c"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
in_app_purchase_storekit:
dependency: transitive
description:
name: in_app_purchase_storekit
sha256: e9a408da11d055f9af9849859e1251b2b0a2f84f256fa3bf1285c5614a397079
url: "https://pub.dev"
source: hosted
version: "0.3.18+1"
integration_test:
dependency: "direct dev"
description: flutter

View File

@@ -66,8 +66,7 @@ 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: ^3.6.0
firebase_messaging: ^15.1.3
fk_user_agent: ^2.0.1
flutter:
sdk: flutter
flutter_animate: ^4.1.0
@@ -100,7 +99,6 @@ dependencies:
image: ^4.0.17
image_editor: ^1.3.0
image_picker: ^1.1.1
in_app_purchase: ^3.0.7
intl: ^0.19.0
json_annotation: ^4.8.0
latlong2: ^0.9.0
@@ -228,7 +226,7 @@ flutter_icons:
android: "launcher_icon"
adaptive_icon_foreground: "assets/launcher_icon/ente-icon-foreground.png"
adaptive_icon_background: "#ffffff"
ios: true
ios: false # F-Droid
image_path: "assets/icon-light.png"
flutter_native_splash:

1
mobile/thirdparty/flutter vendored Submodule

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
/build

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
/build

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -255,7 +255,6 @@ func main() {
UsageCtrl: usageController,
CollectionRepo: collectionRepo,
TaskLockingRepo: taskLockingRepo,
DiscordController: discordController,
QueueRepo: queueRepo,
ObjectCleanupCtrl: objectCleanupController,
LockController: lockController,

View File

@@ -317,10 +317,6 @@ internal:
# List of user IDs that can use the admin API endpoints.
# If this is not set, as a fallback, the first user is considered an admin.
admins: []
# In case there is a single admin, it can be alternatively specified by as
# the "admin" instead of "admins". This can be useful e.g. when wishing to
# pass the admin as an environment variable.
admin:
# Replication config
#

View File

@@ -296,7 +296,7 @@ func (h *AdminHandler) UpdateEmailMFA(c *gin.Context) {
}
go h.DiscordController.NotifyAdminAction(
fmt.Sprintf("Admin (%d) updating email mfa (%v) for account %d", auth.GetUserID(c.Request.Header), *request.EmailMFA, request.UserID))
fmt.Sprintf("Admin (%d) updating email mfa (%v) for account %d", auth.GetUserID(c.Request.Header), request.EmailMFA, request.UserID))
logger := logrus.WithFields(logrus.Fields{
"user_id": request.UserID,
"admin_id": auth.GetUserID(c.Request.Header),

View File

@@ -6,8 +6,6 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/ente-io/museum/pkg/controller/discord"
"github.com/ente-io/museum/pkg/utils/network"
"runtime/debug"
"strconv"
"strings"
@@ -45,7 +43,6 @@ type FileController struct {
ObjectCleanupCtrl *ObjectCleanupController
LockController *lock.LockController
EmailNotificationCtrl *email.EmailNotificationController
DiscordController *discord.DiscordController
HostName string
cleanupCronRunning bool
}
@@ -105,7 +102,7 @@ func (c *FileController) validateFileCreateOrUpdateReq(userID int64, file ente.F
}
// Create adds an entry for a file in the respective tables
func (c *FileController) Create(ctx *gin.Context, userID int64, file ente.File, userAgent string, app ente.App) (ente.File, error) {
func (c *FileController) Create(ctx context.Context, userID int64, file ente.File, userAgent string, app ente.App) (ente.File, error) {
err := c.validateFileCreateOrUpdateReq(userID, file)
if err != nil {
return file, stacktrace.Propagate(err, "")
@@ -159,7 +156,7 @@ func (c *FileController) Create(ctx *gin.Context, userID int64, file ente.File,
if err != nil {
return file, stacktrace.Propagate(err, "")
}
file, err = c.onDuplicateObjectDetected(ctx, file, existing, hotDC)
file, err = c.onDuplicateObjectDetected(file, existing, hotDC)
if err != nil {
return file, stacktrace.Propagate(err, "")
}
@@ -790,7 +787,7 @@ func (c *FileController) sizeOf(objectKey string) (int64, error) {
return *head.ContentLength, nil
}
func (c *FileController) onDuplicateObjectDetected(ctx *gin.Context, file ente.File, existing ente.File, hotDC string) (ente.File, error) {
func (c *FileController) onDuplicateObjectDetected(file ente.File, existing ente.File, hotDC string) (ente.File, error) {
newJSON, _ := json.Marshal(file)
existingJSON, _ := json.Marshal(existing)
log.Info("Comparing " + string(newJSON) + " against " + string(existingJSON))
@@ -808,60 +805,27 @@ func (c *FileController) onDuplicateObjectDetected(ctx *gin.Context, file ente.F
return file, nil
} else {
// Overwrote an existing file or thumbnail
go c.onExistingObjectsReplaced(ctx, file, hotDC)
go c.onExistingObjectsReplaced(file, hotDC)
return ente.File{}, ente.ErrBadRequest
}
}
func (c *FileController) safeAlert(msg string) {
func (c *FileController) onExistingObjectsReplaced(file ente.File, hotDC string) {
defer func() {
if r := recover(); r != nil {
log.Errorf("Panic caught: %s, stack: %s", r, string(debug.Stack()))
}
}()
c.DiscordController.Notify(msg)
}
func (c *FileController) onExistingObjectsReplaced(ctx *gin.Context, file ente.File, hotDC string) {
defer func() {
if r := recover(); r != nil {
log.Errorf("Panic caught: %s, stack: %s", r, string(debug.Stack()))
}
}()
client := network.GetClientInfo(ctx)
reqId := requestid.Get(ctx)
revertErr := false
go c.safeAlert(fmt.Sprintf(`Client %s replaced an existing object req_id %s for (file: %s, thum %s)`, client, reqId, file.File.ObjectKey, file.Thumbnail.ObjectKey))
log.Error("Replaced existing object, reverting", file)
logger := log.WithFields(log.Fields{
"req_id": reqId,
"owner_id": file.OwnerID,
"file_obj": file.File.ObjectKey,
"fileSize": file.File.Size,
"thumb_obj": file.Thumbnail.ObjectKey,
"thumbSize": file.Thumbnail.Size,
})
err := c.rollbackObject(file.File.ObjectKey)
if err != nil {
logger.Error("Error rolling back latest file from hot storage", err)
revertErr = true
log.Error("Error rolling back latest file from hot storage", err)
}
err = c.rollbackObject(file.Thumbnail.ObjectKey)
if err != nil {
log.Error("Error rolling back latest thumbnail from hot storage", err)
revertErr = true
}
resetErr := c.FileRepo.ResetNeedsReplication(file, hotDC)
if resetErr != nil {
log.Error("Error resetting needs replication", resetErr)
revertErr = true
}
if revertErr {
go c.safeAlert(fmt.Sprintf(`☠️ Client %s replaced an existing object req_id %s for user %d, failed to revert`, client, reqId, file.OwnerID))
} else {
go c.safeAlert(fmt.Sprintf(`🔄 Client %s replaced an existing object req_id %s for user %d, reverted`, client, reqId, file.OwnerID))
}
c.FileRepo.ResetNeedsReplication(file, hotDC)
}
func (c *FileController) rollbackObject(objectKey string) error {

View File

@@ -27,9 +27,8 @@ import (
)
const (
slowUploadThreshold = 2 * time.Second
slowSpeedThreshold = 0.5 // MB/s
replicationDelayForStaleObjects = 30
slowUploadThreshold = 2 * time.Second
slowSpeedThreshold = 0.5 // MB/s
)
// ReplicationController3 oversees version 3 of our object replication.
@@ -251,16 +250,6 @@ func (c *ReplicationController3) tryReplicate() error {
logger.Error(err)
}
if strings.Contains(err.Error(), "size of the uploaded file") {
delayErr := c.ObjectCopiesRepo.DelayNextAttemptByDays(context.Background(), objectKey, replicationDelayForStaleObjects)
if delayErr != nil {
logger.WithError(delayErr).Error("Failed to delay next attempt")
} else {
discordAlert := fmt.Sprintf("🔥 Size mismatch for object %s, deferred next attemp for %d days", objectKey, replicationDelayForStaleObjects)
c.notifyDiscord(discordAlert)
}
}
if err == nil {
logger.Info("Replication attempt succeeded")
} else {

View File

@@ -91,17 +91,8 @@ func (m *AuthMiddleware) AdminAuthMiddleware() gin.HandlerFunc {
return
}
}
// The config allows alternatively specifying a singular admin ID to
// workaround Viper issues in passing env vars for an int slice.
admin := viper.GetInt("internal.admin")
if len(admins) == 0 && admin != 0 {
if int64(admin) == userID {
c.Next()
return
}
}
// if no admins are set, then check if the user is first user in the system
if len(admins) == 0 && admin == 0 {
if len(admins) == 0 {
id, err := m.UserAuthRepo.GetMinUserID()
if err != nil && id == userID {
c.Next()

View File

@@ -111,15 +111,6 @@ func (repo *ObjectCopiesRepository) RegisterReplicationAttempt(tx *sql.Tx, ctx c
return stacktrace.Propagate(err, "")
}
func (repo *ObjectCopiesRepository) DelayNextAttemptByDays(ctx context.Context, objectKey string, days int) error {
_, err := repo.DB.ExecContext(ctx, `
UPDATE object_copies
SET last_attempt = last_attempt + ($2 * 24::BIGINT * 60 * 60 * 1000 * 1000)
WHERE object_key = $1
`, objectKey, days)
return stacktrace.Propagate(err, "")
}
// ResetNeedsB2Replication modifies the db to indicate that objectKey should be
// re-replicated to Backblaze even if it has already been replicated there.
func (repo *ObjectCopiesRepository) ResetNeedsB2Replication(objectKey string) error {

View File

@@ -11,6 +11,7 @@ import {
logStartupBanner,
logUnhandledErrorsAndRejections,
} from "@/base/log-web";
import { MessageContainer } from "@ente/shared/components/MessageContainer";
import { useLocalState } from "@ente/shared/hooks/useLocalState";
import HTTPService from "@ente/shared/network/HTTPService";
import {
@@ -35,6 +36,9 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
const router = useRouter();
const [isI18nReady, setIsI18nReady] = useState<boolean>(false);
const [loading, setLoading] = useState(false);
const [offline, setOffline] = useState(
typeof window !== "undefined" && !window.navigator.onLine,
);
const [showNavbar, setShowNavBar] = useState(false);
const { showMiniDialog, miniDialogProps } = useAttributedMiniDialog();
@@ -53,6 +57,9 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
return () => logUnhandledErrorsAndRejections(false);
}, []);
const setUserOnline = () => setOffline(false);
const setUserOffline = () => setOffline(true);
useEffect(() => {
router.events.on("routeChangeStart", (url: string) => {
const newPathname = url.split("?")[0];
@@ -64,6 +71,14 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
router.events.on("routeChangeComplete", () => {
setLoading(false);
});
window.addEventListener("online", setUserOnline);
window.addEventListener("offline", setUserOffline);
return () => {
window.removeEventListener("online", setUserOnline);
window.removeEventListener("offline", setUserOffline);
};
}, [router]);
const logout = useCallback(() => {
@@ -90,6 +105,9 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
<ThemeProvider theme={getTheme(themeColor, "auth")}>
<CssBaseline enableColorScheme />
{showNavbar && <AppNavbar />}
<MessageContainer>
{isI18nReady && offline && t("OFFLINE_MSG")}
</MessageContainer>
<AttributedMiniDialog {...miniDialogProps} />

View File

@@ -10,10 +10,8 @@ import {
HorizontalFlex,
VerticallyCentered,
} from "@ente/shared/components/Container";
import {
OverflowMenu,
OverflowMenuOption,
} from "@ente/shared/components/OverflowMenu";
import OverflowMenu from "@ente/shared/components/OverflowMenu/menu";
import { OverflowMenuOption } from "@ente/shared/components/OverflowMenu/option";
import { AUTH_PAGES as PAGES } from "@ente/shared/constants/pages";
import LogoutOutlined from "@mui/icons-material/LogoutOutlined";
import MoreHoriz from "@mui/icons-material/MoreHoriz";

View File

@@ -0,0 +1,44 @@
import { VerticallyCenteredFlex } from "@ente/shared/components/Container";
import { Typography, type ButtonProps } from "@mui/material";
interface Iprops {
mainText: string;
subText?: string;
subIcon?: React.ReactNode;
color?: ButtonProps["color"];
}
const getSubTextColor = (color: ButtonProps["color"]) => {
switch (color) {
case "critical":
return "critical.main";
default:
return "text.faint";
}
};
export const CaptionedText = (props: Iprops) => {
return (
<VerticallyCenteredFlex gap={"4px"}>
<Typography> {props.mainText}</Typography>
<Typography variant="small" color={getSubTextColor(props.color)}>
{"•"}
</Typography>
{props.subText ? (
<Typography
variant="small"
color={getSubTextColor(props.color)}
>
{props.subText}
</Typography>
) : (
<Typography
variant="small"
color={getSubTextColor(props.color)}
>
{props.subIcon}
</Typography>
)}
</VerticallyCenteredFlex>
);
};

View File

@@ -23,11 +23,10 @@ import {
} from "@/new/photos/services/magic-metadata";
import { useAppContext } from "@/new/photos/types/context";
import { HorizontalFlex } from "@ente/shared/components/Container";
import {
OverflowMenu,
OverflowMenuOption,
import OverflowMenu, {
StyledMenu,
} from "@ente/shared/components/OverflowMenu";
} from "@ente/shared/components/OverflowMenu/menu";
import { OverflowMenuOption } from "@ente/shared/components/OverflowMenu/option";
import ArchiveOutlined from "@mui/icons-material/ArchiveOutlined";
import DeleteOutlinedIcon from "@mui/icons-material/DeleteOutlined";
import EditIcon from "@mui/icons-material/Edit";

View File

@@ -1,24 +1,18 @@
import { isDesktop } from "@/base/app";
import { EnteSwitch } from "@/base/components/EnteSwitch";
import type { ButtonishProps } from "@/base/components/mui";
import type { ModalVisibilityProps } from "@/base/components/utils/modal";
import { ensureElectron } from "@/base/electron";
import log from "@/base/log";
import { EnteFile } from "@/media/file";
import { DialogCloseIconButton } from "@/new/photos/components/mui/Dialog";
import { useAppContext } from "@/new/photos/types/context";
import ChangeDirectoryOption from "@ente/shared/components/ChangeDirectoryOption";
import {
SpaceBetweenFlex,
VerticallyCenteredFlex,
} from "@ente/shared/components/Container";
import LinkButton from "@ente/shared/components/LinkButton";
import {
OverflowMenu,
OverflowMenuOption,
} from "@ente/shared/components/OverflowMenu";
import { CustomError } from "@ente/shared/error";
import FolderIcon from "@mui/icons-material/Folder";
import MoreHoriz from "@mui/icons-material/MoreHoriz";
import {
Box,
Button,
@@ -268,18 +262,6 @@ const DirectoryPathContainer = styled(LinkButton)(
`,
);
const ChangeDirectoryOption: React.FC<ButtonishProps> = ({ onClick }) => (
<OverflowMenu
triggerButtonProps={{ sx: { ml: 1 } }}
ariaControls={"export-option"}
triggerButtonIcon={<MoreHoriz />}
>
<OverflowMenuOption onClick={onClick} startIcon={<FolderIcon />}>
{t("CHANGE_FOLDER")}
</OverflowMenuOption>
</OverflowMenu>
);
function ContinuousExport({ continuousExport, toggleContinuousExport }) {
return (
<SpaceBetweenFlex minHeight={"48px"}>

View File

@@ -207,25 +207,52 @@ const PhotoFrame = ({
return <div />;
}
// Return a (curried) function which will return true if the URL was updated
// (for the given params), and false otherwise.
const updateThumbURL =
// Return a function which will return true if the URL was updated (for the
// given params), and false otherwise.
const updateURL =
(index: number) => (id: number, url: string, forceUpdate?: boolean) => {
const file = displayFiles[index];
// This is to prevent outdated call from updating the wrong file.
// this is to prevent outdated updateURL call from updating the wrong file
if (file.id !== id) {
log.info(
`Ignoring stale updateThumbURL for display file at index ${index} (file ID ${file.id}, expected ${id})`,
`[${id}]PhotoSwipe: updateURL: file id mismatch: ${file.id} !== ${id}`,
);
throw Error("Update URL file id mismatch");
throw Error("update url file id mismatch");
}
if (file.msrc && !forceUpdate) {
return false;
}
updateDisplayFileThumbnail(file, url);
updateFileMsrcProps(file, url);
return true;
};
// Return true if the URL was updated (for the given params), and false
// otherwise.
const updateSrcURL = async (
index: number,
id: number,
srcURLs: SourceURLs,
forceUpdate?: boolean,
) => {
const file = displayFiles[index];
// this is to prevent outdate updateSrcURL call from updating the wrong file
if (file.id !== id) {
log.info(
`[${id}]PhotoSwipe: updateSrcURL: file id mismatch: ${file.id}`,
);
throw Error("update url file id mismatch");
}
if (file.isSourceLoaded && !forceUpdate) {
return false;
} else if (file.conversionFailed) {
log.info(`[${id}]PhotoSwipe: updateSrcURL: conversion failed`);
throw Error("file conversion failed");
}
await updateFileSrcProps(file, srcURLs, enableDownload);
return true;
};
const handleClose = (needUpdate) => {
setOpen(false);
needUpdate && syncWithRemote();
@@ -288,7 +315,7 @@ const PhotoFrame = ({
<PreviewCard
key={`tile-${item.id}-selected-${selected[item.id] ?? false}`}
file={item}
updateURL={updateThumbURL(index)}
updateURL={updateURL(index)}
onClick={onThumbnailClick(index)}
selectable={selectable}
onSelect={handleSelect(
@@ -339,7 +366,7 @@ const PhotoFrame = ({
// URL will always be defined (unless an error is thrown) since
// we are not passing the `cachedOnly` option.
const url = await downloadManager.renderableThumbnailURL(item)!;
updateThumbnail(instance, index, item, url, false);
updateThumb(instance, index, item, url, false);
} catch (e) {
log.error("getSlideData failed get msrc url failed", e);
thumbFetching[item.id] = false;
@@ -372,7 +399,22 @@ const PhotoFrame = ({
url: imageURL,
type: "normal",
};
updateSource(instance, index, item, dummyImgSrcUrl, false);
try {
if (await updateSrcURL(index, item.id, dummyImgSrcUrl)) {
log.info(
`[${item.id}] calling invalidateCurrItems for live photo imgSrc, source loaded: ${item.isSourceLoaded}`,
);
instance.invalidateCurrItems();
if ((instance as any).isOpen()) {
instance.updateSize(true);
}
}
} catch (e) {
log.error(
"updating photoswipe after for live photo imgSrc update failed",
e,
);
}
if (!imageURL) {
// no image url, no need to load video
return;
@@ -383,15 +425,30 @@ const PhotoFrame = ({
url: { video: videoURL, image: imageURL },
type: "livePhoto",
};
updateSource(
instance,
index,
item,
loadedLivePhotoSrcURL,
true,
);
try {
const updated = await updateSrcURL(
index,
item.id,
loadedLivePhotoSrcURL,
true,
);
if (updated) {
log.info(
`[${item.id}] calling invalidateCurrItems for live photo complete, source loaded: ${item.isSourceLoaded}`,
);
instance.invalidateCurrItems();
if ((instance as any).isOpen()) {
instance.updateSize(true);
}
}
} catch (e) {
log.error(
"updating photoswipe for live photo complete update failed",
e,
);
}
} else {
updateSource(instance, index, item, srcURLs, false);
await updateOrig(instance, index, item, srcURLs, false);
}
} catch (e) {
log.error("getSlideData failed get src url failed", e);
@@ -400,7 +457,7 @@ const PhotoFrame = ({
}
};
const updateThumbnail = (
const updateThumb = (
instance: PhotoSwipe<PhotoSwipe.Options>,
index: number,
item: DisplayFile,
@@ -408,7 +465,7 @@ const PhotoFrame = ({
forceUpdate?: boolean,
) => {
try {
if (updateThumbURL(index)(item.id, url, forceUpdate)) {
if (updateURL(index)(item.id, url, forceUpdate)) {
log.info(
`[${item.id}] calling invalidateCurrItems for thumbnail msrc: ${!!item.msrc}`,
);
@@ -423,28 +480,26 @@ const PhotoFrame = ({
}
};
const updateSource = (
const updateOrig = async (
instance: PhotoSwipe<PhotoSwipe.Options>,
index: number,
item: DisplayFile,
srcURL: SourceURLs,
overwrite: boolean,
forceUpdate?: boolean,
) => {
const file = displayFiles[index];
// This is to prevent outdated call from updating the wrong file.
if (file.id !== item.id) {
log.info(
`Ignoring stale updateSourceURL for display file at index ${index} (file ID ${file.id}, expected ${item.id})`,
);
throw new Error("Update URL file id mismatch");
}
if (file.isSourceLoaded && !overwrite) return;
if (file.conversionFailed) throw new Error("File conversion failed");
updateDisplayFileSource(file, srcURL, enableDownload);
instance.invalidateCurrItems();
if ((instance as any).isOpen()) {
instance.updateSize(true);
try {
if (await updateSrcURL(index, item.id, srcURL, forceUpdate)) {
log.info(
`[${item.id}] calling invalidateCurrItems for src, source loaded: ${item.isSourceLoaded}`,
);
instance.invalidateCurrItems();
if ((instance as any).isOpen()) {
instance.updateSize(true);
}
}
} catch (e) {
log.error("updating photoswipe after src url update failed", e);
throw e;
}
};
@@ -453,7 +508,7 @@ const PhotoFrame = ({
index: number,
item: DisplayFile,
) => {
updateThumbnail(instance, index, item, item.msrc, true);
updateThumb(instance, index, item, item.msrc, true);
try {
log.info(
@@ -465,7 +520,7 @@ const PhotoFrame = ({
forceConvert: true,
});
updateSource(instance, index, item, srcURL, true);
await updateOrig(instance, index, item, srcURL, true);
} catch (e) {
log.error("getConvertedVideo failed get src url failed", e);
fetching[item.id] = false;
@@ -526,7 +581,7 @@ const PhotoFrame = ({
export default PhotoFrame;
const updateDisplayFileThumbnail = (file: DisplayFile, url: string) => {
function updateFileMsrcProps(file: DisplayFile, url: string) {
file.w = window.innerWidth;
file.h = window.innerHeight;
file.msrc = url;
@@ -542,13 +597,13 @@ const updateDisplayFileThumbnail = (file: DisplayFile, url: string) => {
</div>
`;
}
};
}
const updateDisplayFileSource = (
async function updateFileSrcProps(
file: DisplayFile,
srcURLs: SourceURLs,
enableDownload: boolean,
) => {
) {
const { url } = srcURLs;
const isRenderable = !!url;
file.w = window.innerWidth;
@@ -601,4 +656,4 @@ const updateDisplayFileSource = (
log.error(`unknown file type - ${file.metadata.fileType}`);
file.src = url as string;
}
};
}

View File

@@ -6,7 +6,7 @@ import type { CollectionMapping, Electron, ZipItem } from "@/base/types/ipc";
import type { Collection } from "@/media/collection";
import type { EnteFile } from "@/media/file";
import { UploaderNameInput } from "@/new/albums/components/UploaderNameInput";
import { CollectionMappingChoice } from "@/new/photos/components/CollectionMappingChoice";
import { CollectionMappingChoiceDialog } from "@/new/photos/components/CollectionMappingChoiceDialog";
import type { CollectionSelectorAttributes } from "@/new/photos/components/CollectionSelector";
import { downloadAppDialogAttributes } from "@/new/photos/components/utils/download";
import { exportMetadataDirectoryName } from "@/new/photos/services/export";
@@ -134,8 +134,7 @@ export default function Uploader({
const [percentComplete, setPercentComplete] = useState(0);
const [hasLivePhotos, setHasLivePhotos] = useState(false);
const [openCollectionMappingChoice, setOpenCollectionMappingChoice] =
useState(false);
const [openChoiceDialog, setOpenChoiceDialog] = useState(false);
const [importSuggestion, setImportSuggestion] = useState<ImportSuggestion>(
DEFAULT_IMPORT_SUGGESTION,
);
@@ -222,8 +221,8 @@ export default function Uploader({
const closeUploadProgress = () => setUploadProgressView(false);
const handleCollectionMappingChoiceClose = () => {
setOpenCollectionMappingChoice(false);
const handleChoiceModalClose = () => {
setOpenChoiceDialog(false);
uploadRunning.current = false;
};
@@ -466,7 +465,7 @@ export default function Uploader({
let showNextModal = () => {};
if (importSuggestion.hasNestedFolders) {
showNextModal = () => setOpenCollectionMappingChoice(true);
showNextModal = () => setOpenChoiceDialog(true);
} else {
showNextModal = () =>
showCollectionCreateModal(importSuggestion.rootFolderName);
@@ -741,7 +740,7 @@ export default function Uploader({
}
};
const handleCollectionMappingSelect = (mapping: CollectionMapping) => {
const didSelectCollectionMapping = (mapping: CollectionMapping) => {
switch (mapping) {
case "root":
uploadToSingleNewCollection(
@@ -782,10 +781,10 @@ export default function Uploader({
return (
<>
<CollectionMappingChoice
open={openCollectionMappingChoice}
onClose={handleCollectionMappingChoiceClose}
onSelect={handleCollectionMappingSelect}
<CollectionMappingChoiceDialog
open={openChoiceDialog}
onClose={handleChoiceModalClose}
didSelect={didSelectCollectionMapping}
/>
<UploadTypeSelector
open={props.uploadTypeSelectorView}

View File

@@ -6,7 +6,7 @@ import {
import { ensureElectron } from "@/base/electron";
import { basename, dirname } from "@/base/file-name";
import type { CollectionMapping, FolderWatch } from "@/base/types/ipc";
import { CollectionMappingChoice } from "@/new/photos/components/CollectionMappingChoice";
import { CollectionMappingChoiceDialog } from "@/new/photos/components/CollectionMappingChoiceDialog";
import { DialogCloseIconButton } from "@/new/photos/components/mui/Dialog";
import { AppContext, useAppContext } from "@/new/photos/types/context";
import {
@@ -15,10 +15,8 @@ import {
SpaceBetweenFlex,
VerticallyCentered,
} from "@ente/shared/components/Container";
import {
OverflowMenu,
OverflowMenuOption,
} from "@ente/shared/components/OverflowMenu";
import OverflowMenu from "@ente/shared/components/OverflowMenu/menu";
import { OverflowMenuOption } from "@ente/shared/components/OverflowMenu/option";
import CheckIcon from "@mui/icons-material/Check";
import DoNotDisturbOutlinedIcon from "@mui/icons-material/DoNotDisturbOutlined";
import FolderCopyOutlinedIcon from "@mui/icons-material/FolderCopyOutlined";
@@ -143,9 +141,9 @@ export const WatchFolder: React.FC<ModalVisibilityProps> = ({
</Stack>
</DialogContent>
</Dialog>
<CollectionMappingChoice
<CollectionMappingChoiceDialog
{...mappingChoiceVisibilityProps}
onSelect={handleCollectionMappingSelect}
didSelect={handleCollectionMappingSelect}
/>
</>
);

View File

@@ -19,13 +19,13 @@ import {
updateAvailableForDownloadDialogAttributes,
updateReadyToInstallDialogAttributes,
} from "@/new/photos/components/utils/download";
import { useIsOffline } from "@/new/photos/components/utils/use-is-offline";
import { useLoadingBar } from "@/new/photos/components/utils/use-loading-bar";
import { photosDialogZIndex } from "@/new/photos/components/utils/z-index";
import { runMigrations } from "@/new/photos/services/migrations";
import { initML, isMLSupported } from "@/new/photos/services/ml";
import { getFamilyPortalRedirectURL } from "@/new/photos/services/user-details";
import { AppContext } from "@/new/photos/types/context";
import { MessageContainer } from "@ente/shared/components/MessageContainer";
import { useLocalState } from "@ente/shared/hooks/useLocalState";
import HTTPService from "@ente/shared/network/HTTPService";
import {
@@ -37,7 +37,7 @@ import { getTheme } from "@ente/shared/themes";
import { THEME_COLOR } from "@ente/shared/themes/constants";
import type { User } from "@ente/shared/user/types";
import ArrowForward from "@mui/icons-material/ArrowForward";
import { CssBaseline, styled } from "@mui/material";
import { CssBaseline } from "@mui/material";
import { ThemeProvider } from "@mui/material/styles";
import Notification from "components/Notification";
import { t } from "i18next";
@@ -55,6 +55,9 @@ export default function App({ Component, pageProps }: AppProps) {
const router = useRouter();
const [isI18nReady, setIsI18nReady] = useState<boolean>(false);
const [loading, setLoading] = useState(false);
const [offline, setOffline] = useState(
typeof window !== "undefined" && !window.navigator.onLine,
);
const [showNavbar, setShowNavBar] = useState(false);
const [watchFolderView, setWatchFolderView] = useState(false);
const [watchFolderFiles, setWatchFolderFiles] = useState<FileList>(null);
@@ -63,7 +66,6 @@ export default function App({ Component, pageProps }: AppProps) {
const [notificationAttributes, setNotificationAttributes] =
useState<NotificationAttributes>(null);
const isOffline = useIsOffline();
const { showMiniDialog, miniDialogProps } = useAttributedMiniDialog();
const { loadingBarRef, showLoadingBar, hideLoadingBar } = useLoadingBar();
const [themeColor, setThemeColor] = useLocalState(
@@ -126,6 +128,9 @@ export default function App({ Component, pageProps }: AppProps) {
if (isDesktop) void resumeExportsIfNeeded();
}, []);
const setUserOnline = () => setOffline(false);
const setUserOffline = () => setOffline(true);
useEffect(() => {
const query = new URLSearchParams(window.location.search);
const needsFamilyRedirect = query.get("redirect") == "families";
@@ -150,6 +155,14 @@ export default function App({ Component, pageProps }: AppProps) {
router.events.on("routeChangeComplete", () => {
setLoading(false);
});
window.addEventListener("online", setUserOnline);
window.addEventListener("offline", setUserOffline);
return () => {
window.removeEventListener("online", setUserOnline);
window.removeEventListener("offline", setUserOffline);
};
}, []);
useEffect(() => {
@@ -197,9 +210,9 @@ export default function App({ Component, pageProps }: AppProps) {
<ThemeProvider theme={getTheme(themeColor, "photos")}>
<CssBaseline enableColorScheme />
{showNavbar && <AppNavbar />}
<OfflineMessageContainer>
{isI18nReady && isOffline && t("OFFLINE_MSG")}
</OfflineMessageContainer>
<MessageContainer>
{isI18nReady && offline && t("OFFLINE_MSG")}
</MessageContainer>
<LoadingBar color="#51cd7c" ref={loadingBarRef} />
<AttributedMiniDialog
@@ -236,14 +249,6 @@ export default function App({ Component, pageProps }: AppProps) {
);
}
const OfflineMessageContainer = styled("div")`
background-color: #111;
padding: 0;
font-size: 14px;
text-align: center;
line-height: 32px;
`;
const redirectToFamilyPortal = () =>
void getFamilyPortalRedirectURL().then((url) => {
window.location.href = url;

View File

@@ -8,13 +8,11 @@ import {
useIsTouchscreen,
} from "@/base/components/utils/hooks";
import { sharedCryptoWorker } from "@/base/crypto";
import { isHTTP401Error } from "@/base/http";
import log from "@/base/log";
import { downloadManager } from "@/gallery/services/download";
import { updateShouldDisableCFUploadProxy } from "@/gallery/services/upload";
import type { Collection } from "@/media/collection";
import { type EnteFile, mergeMetadata } from "@/media/file";
import { verifyPublicAlbumPassword } from "@/new/albums/services/publicCollection";
import {
GalleryItemsHeaderAdapter,
GalleryItemsSummary,
@@ -30,10 +28,8 @@ import {
FluidContainer,
VerticallyCentered,
} from "@ente/shared/components/Container";
import {
OverflowMenu,
OverflowMenuOption,
} from "@ente/shared/components/OverflowMenu";
import OverflowMenu from "@ente/shared/components/OverflowMenu/menu";
import { OverflowMenuOption } from "@ente/shared/components/OverflowMenu/option";
import SingleInputForm, {
type SingleInputFormProps,
} from "@ente/shared/components/SingleInputForm";
@@ -74,6 +70,7 @@ import {
removePublicFiles,
savePublicCollectionPassword,
syncPublicFiles,
verifyPublicCollectionPassword,
} from "services/publicCollectionService";
import uploadManager from "services/upload/uploadManager";
import {
@@ -243,9 +240,10 @@ export default function PublicCollectionGallery() {
? await cryptoWorker.toB64(bs58.decode(ck))
: await cryptoWorker.fromHex(ck);
token.current = t;
downloadManager.setPublicAlbumsCredentials({
accessToken: token.current,
});
downloadManager.setPublicAlbumsCredentials(
token.current,
undefined,
);
await updateShouldDisableCFUploadProxy();
collectionKey.current = dck;
url.current = window.location.href;
@@ -269,10 +267,10 @@ export default function PublicCollectionGallery() {
setPublicFiles(localPublicFiles);
passwordJWTToken.current =
await getLocalPublicCollectionPassword(collectionUID);
downloadManager.setPublicAlbumsCredentials({
accessToken: token.current,
accessTokenJWT: passwordJWTToken.current,
});
downloadManager.setPublicAlbumsCredentials(
token.current,
passwordJWTToken.current,
);
}
await syncWithRemote();
} finally {
@@ -399,29 +397,47 @@ export default function PublicCollectionGallery() {
setFieldError,
) => {
try {
const jwtToken = await verifyPublicAlbumPassword(
publicCollection.publicURLs[0]!,
password,
token.current,
);
passwordJWTToken.current = jwtToken;
downloadManager.setPublicAlbumsCredentials({
accessToken: token.current,
accessTokenJWT: passwordJWTToken.current,
});
const collectionUID = getPublicCollectionUID(token.current);
await savePublicCollectionPassword(collectionUID, jwtToken);
} catch (e) {
log.error("Failed to verifyLinkPassword", e);
if (isHTTP401Error(e)) {
setFieldError(t("INCORRECT_PASSPHRASE"));
} else {
setFieldError(t("generic_error_retry"));
const cryptoWorker = await sharedCryptoWorker();
let hashedPassword: string = null;
try {
const publicUrl = publicCollection.publicURLs[0];
hashedPassword = await cryptoWorker.deriveKey(
password,
publicUrl.nonce,
publicUrl.opsLimit,
publicUrl.memLimit,
);
} catch (e) {
log.error("failed to derive key for verifyLinkPassword", e);
setFieldError(`${t("generic_error_retry")} ${e.message}`);
return;
}
return;
const collectionUID = getPublicCollectionUID(token.current);
try {
const jwtToken = await verifyPublicCollectionPassword(
token.current,
hashedPassword,
);
passwordJWTToken.current = jwtToken;
downloadManager.setPublicAlbumsCredentials(
token.current,
passwordJWTToken.current,
);
await savePublicCollectionPassword(collectionUID, jwtToken);
} catch (e) {
const parsedError = parseSharingErrorCodes(e);
if (parsedError.message === CustomError.TOKEN_EXPIRED) {
setFieldError(t("INCORRECT_PASSPHRASE"));
return;
}
throw e;
}
await syncWithRemote();
hideLoadingBar();
} catch (e) {
log.error("failed to verifyLinkPassword", e);
setFieldError(`${t("generic_error_retry")} ${e.message}`);
}
await syncWithRemote();
};
if (loading) {

View File

@@ -10,7 +10,6 @@ import { logoutSearch } from "@/new/photos/services/search";
import { logoutSettings } from "@/new/photos/services/settings";
import { logoutUserDetails } from "@/new/photos/services/user-details";
import exportService from "./export";
import uploadManager from "./upload/uploadManager";
/**
* Logout sequence for the photos app.
@@ -60,12 +59,6 @@ export const photosLogout = async () => {
ignoreError("Upload", e);
}
try {
uploadManager.logout();
} catch (e) {
ignoreError("Upload", e);
}
try {
downloadManager.logout();
} catch (e) {

View File

@@ -362,6 +362,25 @@ export const getPublicCollection = async (
}
};
export const verifyPublicCollectionPassword = async (
token: string,
passwordHash: string,
): Promise<string> => {
try {
const resp = await HTTPService.post(
await apiURL("/public-collection/verify-password"),
{ passHash: passwordHash },
null,
{ "X-Auth-Access-Token": token },
);
const jwtToken = resp.data.jwtToken;
return jwtToken;
} catch (e) {
log.error("failed to verify public collection password", e);
throw e;
}
};
export const removePublicCollectionWithFiles = async (
collectionUID: string,
collectionKey: string,

View File

@@ -1,33 +1,16 @@
import {
authenticatedPublicAlbumsRequestHeaders,
ensureOk,
type PublicAlbumsCredentials,
} from "@/base/http";
import log from "@/base/log";
import { apiURL } from "@/base/origins";
import { EnteFile } from "@/media/file";
import { retryAsyncOperation } from "@/utils/promise";
import { CustomError, handleUploadError } from "@ente/shared/error";
import HTTPService from "@ente/shared/network/HTTPService";
import { z } from "zod";
import {
MultipartUploadURLs,
UploadFile,
type UploadURL,
} from "./upload-service";
import { MultipartUploadURLs, UploadFile, UploadURL } from "./upload-service";
/**
* Zod schema for {@link UploadURL}.
*
* TODO: Duplicated with uploadHttpClient, can be removed after we refactor this
* code.
*/
const UploadURL = z.object({
objectKey: z.string(),
url: z.string(),
});
const MAX_URL_REQUESTS = 50;
class PublicUploadHttpClient {
private uploadURLFetchInProgress = null;
async uploadFile(
uploadFile: UploadFile,
token: string,
@@ -55,29 +38,43 @@ class PublicUploadHttpClient {
}
}
/**
* Sibling of {@link fetchUploadURLs} for public albums.
*/
async fetchUploadURLs(
countHint: number,
credentials: PublicAlbumsCredentials,
) {
const count = Math.min(50, countHint * 2).toString();
const params = new URLSearchParams({ count });
const url = await apiURL("/public-collection/upload-urls");
const res = await fetch(`${url}?${params.toString()}`, {
// TODO: Use authenticatedPublicAlbumsRequestHeaders after the public
// albums refactor branch is merged.
// headers: await authenticatedRequestHeaders(),
headers: authenticatedPublicAlbumsRequestHeaders(credentials),
});
ensureOk(res);
return (
// TODO: The as cast will not be needed when tsc strict mode is
// enabled for this code.
z.object({ urls: UploadURL.array() }).parse(await res.json())
.urls as UploadURL[]
);
count: number,
urlStore: UploadURL[],
token: string,
passwordToken: string,
): Promise<void> {
try {
if (!this.uploadURLFetchInProgress) {
try {
if (!token) {
throw Error(CustomError.TOKEN_MISSING);
}
this.uploadURLFetchInProgress = HTTPService.get(
await apiURL("/public-collection/upload-urls"),
{
count: Math.min(MAX_URL_REQUESTS, count * 2),
},
{
"X-Auth-Access-Token": token,
...(passwordToken && {
"X-Auth-Access-Token-JWT": passwordToken,
}),
},
);
const response = await this.uploadURLFetchInProgress;
for (const url of response.data.urls) {
urlStore.push(url);
}
} finally {
this.uploadURLFetchInProgress = null;
}
}
return this.uploadURLFetchInProgress;
} catch (e) {
log.error("fetch public upload-url failed ", e);
throw e;
}
}
async fetchMultipartUploadURLs(

View File

@@ -111,19 +111,11 @@ class UploadService {
private uploadURLs: UploadURL[] = [];
private pendingUploadCount: number = 0;
private publicUploadProps: PublicUploadProps = undefined;
private activeUploadURLRefill: Promise<void> | undefined;
init(publicUploadProps: PublicUploadProps) {
this.publicUploadProps = publicUploadProps;
}
logout() {
this.uploadURLs = [];
this.pendingUploadCount = 0;
this.publicUploadProps = undefined;
this.activeUploadURLRefill = undefined;
}
async setFileCount(fileCount: number) {
this.pendingUploadCount = fileCount;
await this.preFetchUploadURLs();
@@ -135,21 +127,19 @@ class UploadService {
async getUploadURL() {
if (this.uploadURLs.length === 0 && this.pendingUploadCount) {
await this.refillUploadURLs();
this.ensureUniqueUploadURLs();
await this.fetchUploadURLs();
}
return this.uploadURLs.pop();
}
private async preFetchUploadURLs() {
try {
await this.refillUploadURLs();
await this.fetchUploadURLs();
// checking for any subscription related errors
} catch (e) {
log.error("prefetch uploadURL failed", e);
handleUploadError(e);
}
this.ensureUniqueUploadURLs();
}
async uploadFile(uploadFile: UploadFile) {
@@ -164,47 +154,20 @@ class UploadService {
}
}
private async refillUploadURLs() {
try {
if (!this.activeUploadURLRefill) {
this.activeUploadURLRefill = this._refillUploadURLs();
}
await this.activeUploadURLRefill;
} finally {
this.activeUploadURLRefill = undefined;
}
}
private ensureUniqueUploadURLs() {
// TODO: Sanity check added on new implementation Nov 2024, remove after
// a while (tag: Migration).
if (
this.uploadURLs.length !=
new Set(this.uploadURLs.map((u) => u.url)).size
) {
throw new Error("Duplicate upload URLs detected");
}
}
private async _refillUploadURLs() {
let urls: UploadURL[];
private async fetchUploadURLs() {
if (this.publicUploadProps.accessedThroughSharedURL) {
if (!this.publicUploadProps.token) {
throw Error(CustomError.TOKEN_MISSING);
}
urls = await publicUploadHttpClient.fetchUploadURLs(
await publicUploadHttpClient.fetchUploadURLs(
this.pendingUploadCount,
{
accessToken: this.publicUploadProps.token,
accessTokenJWT: this.publicUploadProps.passwordToken,
},
this.uploadURLs,
this.publicUploadProps.token,
this.publicUploadProps.passwordToken,
);
} else {
urls = await UploadHttpClient.fetchUploadURLs(
await UploadHttpClient.fetchUploadURLs(
this.pendingUploadCount,
this.uploadURLs,
);
}
urls.forEach((u) => this.uploadURLs.push(u));
}
async fetchMultipartUploadURLs(count: number) {
@@ -328,13 +291,8 @@ export interface MultipartUploadURLs {
completeURL: string;
}
/**
* A pre-signed URL alongwith the associated object key.
*/
export interface UploadURL {
/** A pre-signed URL that can be used to upload data to S3. */
url: string;
/** The objectKey with which remote will refer to this object. */
objectKey: string;
}

Some files were not shown because too many files have changed in this diff Show More