Compare commits
72 Commits
photos-v1.
...
fdroid-v1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e3040a36b | ||
|
|
16345fa7a1 | ||
|
|
c0d88a2cac | ||
|
|
809ce0f24a | ||
|
|
cbd22523fd | ||
|
|
85d39dc097 | ||
|
|
703f2a67f8 | ||
|
|
68c2fbfec6 | ||
|
|
fd3bcbf2a8 | ||
|
|
78077e70c6 | ||
|
|
04e2fd0262 | ||
|
|
b377217ece | ||
|
|
7242176243 | ||
|
|
b3123a6440 | ||
|
|
f4eb511beb | ||
|
|
1a689b2c19 | ||
|
|
b0c6ffdbb2 | ||
|
|
b7ccf4aaf9 | ||
|
|
e7c8265ae1 | ||
|
|
21dc35355d | ||
|
|
f86994b1d3 | ||
|
|
260a26d45c | ||
|
|
cdfa368a8c | ||
|
|
d67c6aef53 | ||
|
|
6ebb5d5bf4 | ||
|
|
224b79b648 | ||
|
|
7e0a3cdd6c | ||
|
|
f6db381e20 | ||
|
|
f0c29fef5c | ||
|
|
2a3e317725 | ||
|
|
1a1b3ebf12 | ||
|
|
f995589a02 | ||
|
|
6e0990d658 | ||
|
|
4da4261f4c | ||
|
|
0abe66ea8c | ||
|
|
193b27a186 | ||
|
|
e323096172 | ||
|
|
e41f306ac8 | ||
|
|
01d45d7c14 | ||
|
|
d55a29336f | ||
|
|
cfcbd0fbb2 | ||
|
|
21174548b5 | ||
|
|
910f13e9a8 | ||
|
|
762688db28 | ||
|
|
9df1ea0c57 | ||
|
|
e48ab71fa4 | ||
|
|
246314367a | ||
|
|
ad70bbb571 | ||
|
|
3962c55140 | ||
|
|
82e478bb12 | ||
|
|
63c8e98492 | ||
|
|
ae92d2f759 | ||
|
|
761c3e6ac2 | ||
|
|
f9a3009c60 | ||
|
|
ca0474faca | ||
|
|
b469985277 | ||
|
|
2a5dacb460 | ||
|
|
d16f98cf07 | ||
|
|
8677cbb4f8 | ||
|
|
0e33299863 | ||
|
|
93ba4e011a | ||
|
|
7977bebcaa | ||
|
|
f28f49d724 | ||
|
|
d9a93ddad6 | ||
|
|
07808d6139 | ||
|
|
1e1633bb45 | ||
|
|
c0f33de0c8 | ||
|
|
417621b17c | ||
|
|
8322540732 | ||
|
|
2d61be37bb | ||
|
|
2a10aa7d61 | ||
|
|
004eb310b3 |
3
.gitmodules
vendored
@@ -9,3 +9,6 @@
|
||||
[submodule "auth/assets/simple-icons"]
|
||||
path = auth/assets/simple-icons
|
||||
url = https://github.com/simple-icons/simple-icons.git
|
||||
[submodule "mobile/thirdparty/flutter"]
|
||||
path = mobile/thirdparty/flutter
|
||||
url = https://github.com/flutter/flutter.git
|
||||
|
||||
@@ -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/icon_green"
|
||||
android:usesCleartextTraffic="true"
|
||||
|
||||
@@ -1,36 +1,49 @@
|
||||
Ente is a simple app to backup and share your photos and videos.
|
||||
Store, share and discover your memories with Ente Photos. With end-to-end encryption, only you—and those you share with—can see your photos and videos. Ente Photos has lovingly protected over 200 million memories for people who trust us across all major platforms. Get started with 10 GB free.
|
||||
|
||||
If you've been looking for a privacy-friendly alternative to Google Photos, you've come to the right place. With Ente, they are stored end-to-end encrypted (e2ee). This means that only you can view them.
|
||||
Why Ente Photos?
|
||||
|
||||
We have open-source apps across Android, iOS, web and desktop, and your photos will seamlessly sync between all of them in an end-to-end encrypted (e2ee) manner.
|
||||
Ente Photos is designed for those who truly value their memories. With end-to-end encryption and secure backups in three locations, your photos stay truly private and safe. Powerful on-device AI helps you find faces and objects instantly, while curated stories bring cherished memories to the present. Share encrypted albums with loved ones, invite family at no extra cost, and lock sensitive images with a password. Available on mobile, desktop, and web, Ente preserves every pixel of your photos and videos.
|
||||
|
||||
Ente also makes it simple to share your albums with your loved ones, even if they aren't on Ente. You can share publicly viewable links, where they can view your album and collaborate by adding photos to it, even without an account or app.
|
||||
Features:
|
||||
|
||||
Your encrypted data is replicated to 3 different locations, including a fall-out shelter in Paris. We take posterity seriously and make it easy to ensure that your memories outlive you.
|
||||
END-TO-END ENCRYPTED STORAGE: Your photos and videos are encrypted on your device, and then automatically backed up to the cloud.
|
||||
|
||||
We are here to make the safest photos app ever, come join our journey!
|
||||
SHARE AND COLLABORATE: Let your family or friends add photos and videos to your albums. Everything, end-to-end encrypted.
|
||||
|
||||
FEATURES
|
||||
- Original quality backups, because every pixel is important
|
||||
- Family plans, so you can share storage with your family
|
||||
- Collaborative albums, so you can pool together photos after a trip
|
||||
- Shared folders, in case you want your partner to enjoy your "Camera" clicks
|
||||
- Album links, that can be protected with a password
|
||||
- Ability to free up space, by removing files that have been safely backed up
|
||||
- Human support, because you're worth it
|
||||
- Descriptions, so you can caption your memories and find them easily
|
||||
- Image editor, to add finishing touches
|
||||
- Favorite, hide and relive your memories, for they are precious
|
||||
- One-click import from Google, Apple, your hard drive and more
|
||||
- Dark theme, because your photos look good in it
|
||||
- 2FA, 3FA, biometric auth
|
||||
- and a LOT more!
|
||||
RELIVE YOUR MEMORIES: Through the stories Ente curates for you, relive your memories from previous years. Easily spread the cheer by sharing them with your loved ones or friends.
|
||||
|
||||
PERMISSIONS
|
||||
Ente requests for certain permissions to serve the purpose of a photo storage provider, which can be reviewed here: https://github.com/ente-io/ente/blob/f-droid/mobile/android/permissions.md
|
||||
SEARCH FOR ANYONE AND ANYTHING: Using on-device AI, Ente helps you find faces and key elements in a photo, so you can search through your entire library using natural language search.
|
||||
|
||||
PRICING
|
||||
We don't offer forever free plans, because it is important to us that we remain sustainable and withstand the test of time. Instead we offer affordable plans that you can freely share with your family. You can find more information at ente.io.
|
||||
INVITE YOUR FAMILY: Invite up to 5 family members to any paid plan at no extra cost. Only your storage space is shared, not your data. Each member will receive their own private space.
|
||||
|
||||
SUPPORT
|
||||
We take pride in offering human support. If you are our paid customer, you can reach out to team@ente.io and expect a response from our team within 24 hours.
|
||||
AVAILABLE EVERYWHERE: Ente Photos is available on iOS, Android, Windows, Mac, Linux and the web, so you can access your photos and videos from any device you have.
|
||||
|
||||
NEVER LOSE YOUR PHOTOS: Ente stores your encrypted backups in 3 secure locations—including an underground facility—so your photos stay safe, no matter what.
|
||||
|
||||
EASY IMPORT: Use our powerful desktop app to import data from other providers. If you need any help moving, reach out, and we'll be there.
|
||||
|
||||
ORIGINAL QUALITY BACKUPS: All photos and videos are stored in their original quality, including the metadata, without any compression or loss in quality.
|
||||
|
||||
APP LOCK: Make sure no one else can see your photos and videos using the built in App Lock. You can set a pin, or use biometrics to lock the app only for yourself.
|
||||
|
||||
HIDDEN PHOTOS: Hide your most private photos and videos to the Hidden folder, which is password protected by default.
|
||||
|
||||
FREE DEVICE SPACE: Free up your device's space by clearing files that have already been backed, in a single click.
|
||||
|
||||
COLLECT PHOTOS: Went to a party and want to collect all the photos in one place? Just share a link with your friends and ask them to upload.
|
||||
|
||||
PARTNER SHARING: Share your camera album with your partner so they can automatically see your photos on their device.
|
||||
|
||||
LEGACY: Allow trusted contacts to access your account in your absence.
|
||||
|
||||
DARK & LIGHT THEMES: Choose the mode that will make your photos pop.
|
||||
|
||||
ADDITIONAL SECURITY: Turn on two-factor authentication or set a lock-screen for the app.
|
||||
|
||||
OPEN-SOURCE AND AUDITED: Ente Photos’s code is open-source, and has been audited by third-party security experts.
|
||||
|
||||
HUMAN SUPPORT: We take pride in providing real human support. If you need help, reach out to support@ente.io, and one of us will be there to assist you.
|
||||
|
||||
Keep your memories safe and private, with Ente Photos. Get started with 10 GB free.
|
||||
|
||||
Visit ente.io to learn more.
|
||||
|
After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 584 KiB |
|
Before Width: | Height: | Size: 690 KiB After Width: | Height: | Size: 522 KiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 662 KiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 853 KiB After Width: | Height: | Size: 521 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
@@ -1 +1 @@
|
||||
Ente Photos is an open source photos app, that provides end-to-end encrypted backups for your photos and videos.
|
||||
Backup, Organise, Share - Private photo storage with end-to-end encryption
|
||||
@@ -1 +1 @@
|
||||
Ente Photos - Open source, end-to-end encrypted alternative to Google Photos
|
||||
Ente Photos - Encrypted photo storage
|
||||
@@ -8,13 +8,13 @@ allprojects {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
// mavenLocal() // for FDroid
|
||||
maven {
|
||||
url "${project(':background_fetch').projectDir}/libs"
|
||||
}
|
||||
maven {
|
||||
url "${project(':ffmpeg_kit_flutter').projectDir}/libs"
|
||||
}
|
||||
mavenLocal() // for FDroid
|
||||
// maven {
|
||||
// url "${project(':background_fetch').projectDir}/libs"
|
||||
// }
|
||||
// maven {
|
||||
// url "${project(':ffmpeg_kit_flutter').projectDir}/libs"
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,36 +1,49 @@
|
||||
ente is a simple app to backup and share your photos and videos.
|
||||
Store, share and discover your memories with Ente Photos. With end-to-end encryption, only you—and those you share with—can see your photos and videos. Ente Photos has lovingly protected over 165 million memories for people who trust us across all major platforms. Get started with 10 GB free.
|
||||
|
||||
If you've been looking for a privacy-friendly alternative to Google Photos, you've come to the right place. With ente, they are stored end-to-end encrypted (e2ee). This means that only you can view them.
|
||||
Why Ente Photos?
|
||||
|
||||
We have open-source apps across Android, iOS, web and desktop, and your photos will seamlessly sync between all of them in an end-to-end encrypted (e2ee) manner.
|
||||
Ente Photos is designed for those who truly value their memories. With end-to-end encryption and secure backups in three locations, your photos stay truly private and safe. Powerful on-device AI helps you find faces and objects instantly, while curated stories bring cherished memories to the present. Share encrypted albums with loved ones, invite family at no extra cost, and lock sensitive images with a password. Available on mobile, desktop, and web, Ente preserves every pixel of your photos and videos.
|
||||
|
||||
ente also makes it simple to share your albums with your loved ones, even if they aren't on ente. You can share publicly viewable links, where they can view your album and collaborate by adding photos to it, even without an account or app.
|
||||
Features:
|
||||
|
||||
Your encrypted data is replicated to 3 different locations, including a fall-out shelter in Paris. We take posterity seriously and make it easy to ensure that your memories outlive you.
|
||||
END-TO-END ENCRYPTED STORAGE: Your photos and videos are encrypted on your device, and then automatically backed up to the cloud.
|
||||
|
||||
We are here to make the safest photos app ever, come join our journey!
|
||||
SHARE AND COLLABORATE: Let your family or friends add photos and videos to your albums. Everything, end-to-end encrypted.
|
||||
|
||||
FEATURES
|
||||
- Original quality backups, because every pixel is important
|
||||
- Family plans, so you can share storage with your family
|
||||
- Collaborative albums, so you can pool together photos after a trip
|
||||
- Shared folders, in case you want your partner to enjoy your "Camera" clicks
|
||||
- Album links, that can be protected with a password
|
||||
- Ability to free up space, by removing files that have been safely backed up
|
||||
- Human support, because you're worth it
|
||||
- Descriptions, so you can caption your memories and find them easily
|
||||
- Image editor, to add finishing touches
|
||||
- Favorite, hide and relive your memories, for they are precious
|
||||
- One-click import from Google, Apple, your hard drive and more
|
||||
- Dark theme, because your photos look good in it
|
||||
- 2FA, 3FA, biometric auth
|
||||
- and a LOT more!
|
||||
RELIVE YOUR MEMORIES: Through the stories Ente curates for you, relive your memories from previous years. Easily spread the cheer by sharing them with your loved ones or friends.
|
||||
|
||||
PERMISSIONS
|
||||
ente requests for certain permissions to serve the purpose of a photo storage provider, which can be reviewed here: https://github.com/ente-io/ente/blob/f-droid/mobile/android/permissions.md
|
||||
SEARCH FOR ANYONE AND ANYTHING: Using on-device AI, Ente helps you find faces and key elements in a photo, so you can search through your entire library using natural language search.
|
||||
|
||||
PRICING
|
||||
We don't offer forever free plans, because it is important to us that we remain sustainable and withstand the test of time. Instead we offer affordable plans that you can freely share with your family. You can find more information at ente.io.
|
||||
INVITE YOUR FAMILY: Invite up to 5 family members to any paid plan at no extra cost. Only your storage space is shared, not your data. Each member will receive their own private space.
|
||||
|
||||
SUPPORT
|
||||
We take pride in offering human support. If you are our paid customer, you can reach out to team@ente.io and expect a response from our team within 24 hours.
|
||||
AVAILABLE EVERYWHERE: Ente Photos is available on iOS, Android, Windows, Mac, Linux and the web, so you can access your photos and videos from any device you have.
|
||||
|
||||
NEVER LOSE YOUR PHOTOS: Ente stores your encrypted backups in 3 secure locations—including an underground facility—so your photos stay safe, no matter what.
|
||||
|
||||
EASY IMPORT: Use our powerful desktop app to import data from other providers. If you need any help moving, reach out, and we'll be there.
|
||||
|
||||
ORIGINAL QUALITY BACKUPS: All photos and videos are stored in their original quality, including the metadata, without any compression or loss in quality.
|
||||
|
||||
APP LOCK: Make sure no one else can see your photos and videos using the built in App Lock. You can set a pin, or use biometrics to lock the app only for yourself.
|
||||
|
||||
HIDDEN PHOTOS: Hide your most private photos and videos to the Hidden folder, which is password protected by default.
|
||||
|
||||
FREE DEVICE SPACE: Free up your device's space by clearing files that have already been backed, in a single click.
|
||||
|
||||
COLLECT PHOTOS: Went to a party and want to collect all the photos in one place? Just share a link with your friends and ask them to upload.
|
||||
|
||||
PARTNER SHARING: Share your camera album with your partner so they can automatically see your photos on their device.
|
||||
|
||||
LEGACY: Allow trusted contacts to access your account in your absence.
|
||||
|
||||
DARK & LIGHT THEMES: Choose the mode that will make your photos pop.
|
||||
|
||||
ADDITIONAL SECURITY: Turn on two-factor authentication or set a lock-screen for the app.
|
||||
|
||||
OPEN-SOURCE AND AUDITED: Ente Photos’s code is open-source, and has been audited by third-party security experts.
|
||||
|
||||
HUMAN SUPPORT: We take pride in providing real human support. If you need help, reach out to support@ente.io, and one of us will be there to assist you.
|
||||
|
||||
Keep your memories safe and private, with Ente Photos. Get started with 10 GB free.
|
||||
|
||||
Visit ente.io to learn more.
|
||||
BIN
mobile/fastlane/metadata/android/en-US/images/featureGraphic.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 584 KiB |
|
Before Width: | Height: | Size: 690 KiB After Width: | Height: | Size: 522 KiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 662 KiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 853 KiB After Width: | Height: | Size: 521 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
@@ -1 +1 @@
|
||||
ente is an end-to-end encrypted photo storage app
|
||||
Backup, Organise, Share - Private photo storage with end-to-end encryption
|
||||
@@ -1 +1 @@
|
||||
ente - encrypted photo storage
|
||||
Ente Photos - Encrypted photo storage
|
||||
@@ -1,7 +0,0 @@
|
||||
- Location tags
|
||||
|
||||
This release includes a fresh, beautiful, privacy-friendly way to search through your photos by location!
|
||||
|
||||
Tag a photo with a location, define a radius, and ente will automatically cluster all photos clicked within that area.
|
||||
|
||||
Open a photo, and click on the Info button to get started!
|
||||
|
Before Width: | Height: | Size: 283 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 690 KiB |
|
Before Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 853 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 3.1 MiB |
|
Before Width: | Height: | Size: 3.1 MiB |
@@ -435,81 +435,81 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: 76b66b60cc809390ca1ad69bfd66b998d2387ac7
|
||||
background_fetch: 94b36ee293e82972852dba8ede1fbcd3bd3d9d57
|
||||
battery_info: 83f3aae7be2fccefab1d2bf06b8aa96f11c8bcdd
|
||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||
cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c
|
||||
dart_ui_isolate: 46f6714abe6891313267153ef6f9748d8ecfcab1
|
||||
device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89
|
||||
app_links: f3e17e4ee5e357b39d8b95290a9b2c299fca71c6
|
||||
background_fetch: 39f11371c0dce04b001c4bfd5e782bcccb0a85e2
|
||||
battery_info: b6c551049266af31556b93c9d9b9452cfec0219f
|
||||
connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
|
||||
cupertino_http: 947a233f40cfea55167a49f2facc18434ea117ba
|
||||
dart_ui_isolate: d5bcda83ca4b04f129d70eb90110b7a567aece14
|
||||
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
|
||||
ffmpeg_kit_custom: 682b4f2f1ff1f8abae5a92f6c3540f2441d5be99
|
||||
ffmpeg_kit_flutter: 915b345acc97d4142e8a9a8549d177ff10f043f5
|
||||
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
|
||||
ffmpeg_kit_flutter: 9dce4803991478c78c6fb9f972703301101095fe
|
||||
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
||||
Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf
|
||||
firebase_core: 6cbed78b4f298ed103a9fd034e6dbc846320480f
|
||||
firebase_messaging: 5e0adf2eb18b0ee59aa0c109314c091a0497ecac
|
||||
firebase_core: 6e223dfa350b2edceb729cea505eaaef59330682
|
||||
firebase_messaging: 07fde77ae28c08616a1d4d870450efc2b38cf40d
|
||||
FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d
|
||||
FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629
|
||||
FirebaseInstallations: 6c963bd2a86aca0481eef4f48f5a4df783ae5917
|
||||
FirebaseMessaging: 487b634ccdf6f7b7ff180fdcb2a9935490f764e8
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_email_sender: aa1e9772696691d02cd91fea829856c11efb8e58
|
||||
flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
|
||||
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
|
||||
flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100
|
||||
flutter_native_splash: 6cad9122ea0fad137d23137dd14b937f3e90b145
|
||||
flutter_secure_storage: 2c2ff13db9e0a5647389bff88b0ecac56e3f3418
|
||||
flutter_sodium: 7e4621538491834eba53bd524547854bcbbd6987
|
||||
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
|
||||
flutter_email_sender: e03bdda7637bcd3539bfe718fddd980e9508efaa
|
||||
flutter_image_compress_common: ec1d45c362c9d30a3f6a0426c297f47c52007e3e
|
||||
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
|
||||
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
|
||||
flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a
|
||||
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
|
||||
flutter_sodium: a00383520fc689c688b66fd3092984174712493e
|
||||
fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
|
||||
image_editor_common: 3de87e7c4804f4ae24c8f8a998362b98c105cac1
|
||||
in_app_purchase_storekit: d1a48cb0f8b29dbf5f85f782f5dd79b21b90a5e6
|
||||
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
|
||||
launcher_icon_switcher: 84c218d233505aa7d8655d8fa61a3ba802c022da
|
||||
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
|
||||
image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43
|
||||
in_app_purchase_storekit: a1ce04056e23eecc666b086040239da7619cd783
|
||||
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
|
||||
launcher_icon_switcher: 8e0ad2131a20c51c1dd939896ee32e70cd845b37
|
||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
|
||||
local_auth_ios: f7a1841beef3151d140a967c2e46f30637cdf451
|
||||
local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3
|
||||
local_auth_ios: 5046a18c018dd973247a0564496c8898dbb5adf9
|
||||
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
|
||||
maps_launcher: edf829809ba9e894d70e569bab11c16352dedb45
|
||||
media_extension: 671e2567880d96c95c65c9a82ccceed8f2e309fd
|
||||
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
|
||||
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
||||
motion_sensors: 741e702c17467b9569a92165dda8d4d88c6167f1
|
||||
motionphoto: 23e2aeb5c6380112f69468d71f970fa7438e5ed1
|
||||
move_to_background: 7e3467dd2a1d1013e98c9c1cb93fd53cd7ef9d84
|
||||
maps_launcher: 2e5b6a2d664ec6c27f82ffa81b74228d770ab203
|
||||
media_extension: 6618f07abd762cdbfaadf1b0c56a287e820f0c84
|
||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||
motion_sensors: 03f55b7c637a7e365a0b5f9697a449f9059d5d91
|
||||
motionphoto: 8b65ce50c7d7ff3c767534fc3768b2eed9ac24e4
|
||||
move_to_background: cd3091014529ec7829e342ad2d75c0a11f4378a5
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
native_video_player: e363dd14f6a498ad8a8f7e6486a0db046ad19f13
|
||||
objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
|
||||
onnxruntime: f9b296392c96c42882be020a59dbeac6310d81b2
|
||||
native_video_player: 5d36066807b680e181473e6890dde643ac85380d
|
||||
objective_c: 77e887b5ba1827970907e10e832eec1683f3431d
|
||||
onnxruntime: e7c2ae44385191eaad5ae64c935a72debaddc997
|
||||
onnxruntime-c: a909204639a1f035f575127ac406f781ac797c9c
|
||||
onnxruntime-objc: b6fab0f1787aa6f7190c2013f03037df4718bd8b
|
||||
open_mail_app: 7314a609e88eed22d53671279e189af7a0ab0f11
|
||||
open_mail_app: 70273c53f768beefdafbe310c3d9086e4da3cb02
|
||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
photo_manager: d2fbcc0f2d82458700ee6256a15018210a81d413
|
||||
privacy_screen: 3159a541f5d3a31bea916cfd4e58f9dc722b3fd4
|
||||
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
|
||||
privacy_screen: 1a131c052ceb3c3659934b003b0d397c2381a24e
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
||||
receive_sharing_intent: 79c848f5b045674ad60b9fea3bafea59962ad2c1
|
||||
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
|
||||
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
|
||||
Sentry: da60d980b197a46db0b35ea12cb8f39af48d8854
|
||||
sentry_flutter: 942017adbe00f963061cb11ec260414a990b7a42
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
sentry_flutter: 6a134f9d381e49f22ea25a67736cf0cf4d02ec9c
|
||||
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
|
||||
sqlite3_flutter_libs: 3c323550ef3b928bc0aa9513c841e45a7d242832
|
||||
system_info_plus: 555ce7047fbbf29154726db942ae785c29211740
|
||||
ua_client_hints: 92fe0d139619b73ec9fcb46cc7e079a26178f586
|
||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||
video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b
|
||||
video_thumbnail: 584ccfa55d8fd2f3d5507218b0a18d84c839c620
|
||||
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
|
||||
wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49
|
||||
sqlite3_flutter_libs: 069c435986dd4b63461aecd68f4b30be4a9e9daa
|
||||
system_info_plus: 5393c8da281d899950d751713575fbf91c7709aa
|
||||
ua_client_hints: aeabd123262c087f0ce151ef96fa3ab77bfc8b38
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
|
||||
video_thumbnail: 94ba6705afbaa120b77287080424930f23ea0c40
|
||||
volume_controller: 2e3de73d6e7e81a0067310d17fb70f2f86d71ac7
|
||||
wakelock_plus: 373cfe59b235a6dd5837d0fb88791d2f13a90d56
|
||||
|
||||
PODFILE CHECKSUM: a8ef88ad74ba499756207e7592c6071a96756d18
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import "package:adaptive_theme/adaptive_theme.dart";
|
||||
import 'package:background_fetch/background_fetch.dart';
|
||||
import "package:computer/computer.dart";
|
||||
import 'package:ente_crypto/ente_crypto.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:flutter/rendering.dart";
|
||||
@@ -38,8 +37,8 @@ import "package:photos/services/machine_learning/face_ml/person/person_service.d
|
||||
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/notification_service.dart";
|
||||
// import 'package:photos/services/push_service.dart';
|
||||
import "package:photos/services/preview_video_store.dart";
|
||||
import 'package:photos/services/push_service.dart';
|
||||
import 'package:photos/services/search_service.dart';
|
||||
import 'package:photos/services/sync/local_sync_service.dart';
|
||||
import 'package:photos/services/sync/remote_sync_service.dart';
|
||||
@@ -262,11 +261,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");
|
||||
PreviewVideoStore.instance.init(preferences);
|
||||
@@ -410,35 +409,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();
|
||||
|
||||
@@ -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";
|
||||
@@ -29,6 +28,7 @@ class BillingService {
|
||||
late final _logger = Logger("BillingService");
|
||||
final Dio _enteDio;
|
||||
|
||||
// ignore: unused_field
|
||||
bool _isOnSubscriptionPage = false;
|
||||
|
||||
Future<BillingPlans>? _future;
|
||||
@@ -42,23 +42,6 @@ class BillingService {
|
||||
// await FlutterInappPurchase.instance.initConnection;
|
||||
// FlutterInappPurchase.instance.clearTransactionIOS();
|
||||
// }
|
||||
InAppPurchase.instance.purchaseStream.listen((purchases) {
|
||||
if (_isOnSubscriptionPage) {
|
||||
return;
|
||||
}
|
||||
for (final purchase in purchases) {
|
||||
if (purchase.status == PurchaseStatus.purchased) {
|
||||
verifySubscription(
|
||||
purchase.productID,
|
||||
purchase.verificationData.serverVerificationData,
|
||||
).then((response) {
|
||||
InAppPurchase.instance.completePurchase(purchase);
|
||||
});
|
||||
} else if (Platform.isIOS && purchase.pendingCompletePurchase) {
|
||||
InAppPurchase.instance.completePurchase(purchase);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void clearCache() {
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/core/constants.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/core/network/network.dart';
|
||||
import 'package:photos/events/signed_in_event.dart';
|
||||
import 'package:photos/services/sync/sync_service.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class PushService {
|
||||
static const kFCMPushToken = "fcm_push_token";
|
||||
static const kLastFCMTokenUpdationTime = "fcm_push_token_updation_time";
|
||||
static const kFCMTokenUpdationIntervalInMicroSeconds = 30 * microSecondsInDay;
|
||||
static const kPushAction = "action";
|
||||
static const kSync = "sync";
|
||||
|
||||
static final PushService instance = PushService._privateConstructor();
|
||||
static final _logger = Logger("PushService");
|
||||
|
||||
late SharedPreferences _prefs;
|
||||
|
||||
PushService._privateConstructor();
|
||||
|
||||
Future<void> init() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
await Firebase.initializeApp();
|
||||
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
|
||||
_logger.info("Got a message whilst in the foreground!");
|
||||
_handleForegroundPushMessage(message);
|
||||
});
|
||||
try {
|
||||
if (Configuration.instance.hasConfiguredAccount()) {
|
||||
await _configurePushToken();
|
||||
} else {
|
||||
Bus.instance.on<SignedInEvent>().listen((_) async {
|
||||
// ignore: unawaited_futures
|
||||
_configurePushToken();
|
||||
});
|
||||
}
|
||||
} catch (e, s) {
|
||||
_logger.severe("Could not configure push token", e, s);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _configurePushToken() async {
|
||||
final String? fcmToken = await FirebaseMessaging.instance.getToken();
|
||||
final shouldForceRefreshServerToken =
|
||||
DateTime.now().microsecondsSinceEpoch -
|
||||
(_prefs.getInt(kLastFCMTokenUpdationTime) ?? 0) >
|
||||
kFCMTokenUpdationIntervalInMicroSeconds;
|
||||
if (fcmToken != null &&
|
||||
(_prefs.getString(kFCMPushToken) != fcmToken ||
|
||||
shouldForceRefreshServerToken)) {
|
||||
final String? apnsToken = await FirebaseMessaging.instance.getAPNSToken();
|
||||
try {
|
||||
_logger.info("Updating token on server");
|
||||
await _setPushTokenOnServer(fcmToken, apnsToken);
|
||||
await _prefs.setString(kFCMPushToken, fcmToken);
|
||||
await _prefs.setInt(
|
||||
kLastFCMTokenUpdationTime,
|
||||
DateTime.now().microsecondsSinceEpoch,
|
||||
);
|
||||
_logger.info("Push token updated on server");
|
||||
} catch (e) {
|
||||
_logger.severe("Could not set push token", e, StackTrace.current);
|
||||
}
|
||||
} else {
|
||||
_logger.info("Skipping token update");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _setPushTokenOnServer(
|
||||
String fcmToken,
|
||||
String? apnsToken,
|
||||
) async {
|
||||
await NetworkClient.instance.enteDio.post(
|
||||
"/push/token",
|
||||
data: {
|
||||
"fcmToken": fcmToken,
|
||||
"apnsToken": apnsToken,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _handleForegroundPushMessage(RemoteMessage message) {
|
||||
_logger.info("Message data: ${message.data}");
|
||||
if (message.notification != null) {
|
||||
_logger.info(
|
||||
"Message also contained a notification: ${message.notification}",
|
||||
);
|
||||
}
|
||||
if (shouldSync(message)) {
|
||||
SyncService.instance.sync();
|
||||
}
|
||||
}
|
||||
|
||||
static bool shouldSync(RemoteMessage message) {
|
||||
return message.data.containsKey(kPushAction) &&
|
||||
message.data[kPushAction] == kSync;
|
||||
}
|
||||
}
|
||||
@@ -1,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/api/billing/billing_plan.dart';
|
||||
import 'package:photos/models/api/billing/subscription.dart';
|
||||
import 'package:photos/models/user_details.dart';
|
||||
import "package:photos/service_locator.dart";
|
||||
import 'package:photos/services/account/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/notification/toast.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/dialog_util.dart';
|
||||
import "package:photos/utils/standalone/data.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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -9,14 +9,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "72.0.0"
|
||||
_flutterfire_internals:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _flutterfire_internals
|
||||
sha256: "401dd18096f5eaa140404ccbbbf346f83c850e6f27049698a7ee75a3488ddb32"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.52"
|
||||
_macros:
|
||||
dependency: transitive
|
||||
description: dart
|
||||
@@ -114,10 +106,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
|
||||
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.0"
|
||||
version: "2.7.0"
|
||||
asn1lib:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -227,10 +219,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_value
|
||||
sha256: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2"
|
||||
sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.9.3"
|
||||
version: "8.9.5"
|
||||
cached_network_image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -260,7 +252,7 @@ packages:
|
||||
description:
|
||||
path: "."
|
||||
ref: multicast_version
|
||||
resolved-ref: "1f39cd4d6efa9363e77b2439f0317bae0c92dda1"
|
||||
resolved-ref: af6378574352884beab6cddec462c7fdfc9a8c35
|
||||
url: "https://github.com/guyluz11/flutter_cast.git"
|
||||
source: git
|
||||
version: "2.0.9"
|
||||
@@ -289,6 +281,14 @@ packages:
|
||||
url: "https://github.com/ente-io/chewie.git"
|
||||
source: git
|
||||
version: "1.10.0"
|
||||
cli_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cli_config
|
||||
sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -334,10 +334,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: connectivity_plus
|
||||
sha256: "04bf81bb0b77de31557b58d052b24b3eee33f09a6e7a8c68a3e247c7df19ec27"
|
||||
sha256: "051849e2bd7c7b3bc5844ea0d096609ddc3a859890ec3a9ac4a65a2620cc1f99"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.3"
|
||||
version: "6.1.4"
|
||||
connectivity_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -358,18 +358,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: coverage
|
||||
sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43
|
||||
sha256: "9086475ef2da7102a0c0a4e37e1e30707e7fb7b6d28c209f559a9c5f8ce42016"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.1"
|
||||
version: "1.12.0"
|
||||
cronet_http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cronet_http
|
||||
sha256: "3af9c4d57bf07ef4b307e77b22be4ad61bea19ee6ff65e62184863f3a09f1415"
|
||||
sha256: "0b98ef6d6fee016915276bf1486761cdd1671a5588fe9c9e5183b31bf98ad9f5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
version: "1.3.3"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -398,10 +398,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cupertino_http
|
||||
sha256: "6fcf79586ad872ddcd6004d55c8c2aab3cdf0337436e8f99837b1b6c30665d0c"
|
||||
sha256: b4dd5cbecd25d3662ec5c64c766673e7676e80dbd45b2c46a4a873ad632b3782
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
version: "2.1.1"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -470,10 +470,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio_web_adapter
|
||||
sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a
|
||||
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.1.1"
|
||||
dots_indicator:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -679,54 +679,38 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.14"
|
||||
firebase_core:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_core
|
||||
sha256: "6a4ea0f1d533443c8afc3d809cd36a4e2b8f2e2e711f697974f55bb31d71d1b8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.12.0"
|
||||
firebase_core_platform_interface:
|
||||
file_selector_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_platform_interface
|
||||
sha256: d7253d255ff10f85cfd2adaba9ac17bae878fa3ba577462451163bd9f1d1f0bf
|
||||
name: file_selector_linux
|
||||
sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.4.0"
|
||||
firebase_core_web:
|
||||
version: "0.9.3+2"
|
||||
file_selector_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_web
|
||||
sha256: e47f5c2776de018fa19bc9f6f723df136bc75cdb164d64b65305babd715c8e41
|
||||
name: file_selector_macos
|
||||
sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.21.0"
|
||||
firebase_messaging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_messaging
|
||||
sha256: "8755a083a20bac4485e8b46d223f6f2eab34e659a76a75f8cf3cded53bc98a15"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.2.3"
|
||||
firebase_messaging_platform_interface:
|
||||
version: "0.9.4+2"
|
||||
file_selector_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_platform_interface
|
||||
sha256: "8cc771079677460de53ad8fcca5bc3074d58c5fc4f9d89b19585e5bfd9c64292"
|
||||
name: file_selector_platform_interface
|
||||
sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.6.3"
|
||||
firebase_messaging_web:
|
||||
version: "2.6.2"
|
||||
file_selector_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_web
|
||||
sha256: caa73059b0396c97f691683c4cfc3f897c8543801579b7dd4851c431d8e4e091
|
||||
name: file_selector_windows
|
||||
sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.10.3"
|
||||
version: "0.9.3+4"
|
||||
fixnum:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -994,10 +978,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
sha256: "615a505aef59b151b46bbeef55b36ce2b6ed299d160c51d84281946f0aa0ce0e"
|
||||
sha256: "1c2b787f99bdca1f3718543f81d38aa1b124817dfeb9fb196201bea85b6134bf"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.24"
|
||||
version: "2.0.26"
|
||||
flutter_secure_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1083,10 +1067,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_svg
|
||||
sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b
|
||||
sha256: d44bf546b13025ec7353091516f6881f1d4c633993cb109c3916c3a0159dadf1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.17"
|
||||
version: "2.1.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@@ -1186,10 +1170,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: html
|
||||
sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec"
|
||||
sha256: "9475be233c437f0e3637af55e7702cbbe5c23a68bd56e8a5fa2d426297b7c6c8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.15.5"
|
||||
version: "0.15.5+1"
|
||||
html_unescape:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1278,38 +1262,70 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
in_app_purchase:
|
||||
image_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: in_app_purchase
|
||||
sha256: "11a40f148eeb4f681a0572003e2b33432e110c90c1bbb4f9ef83b81ec0c4f737"
|
||||
name: image_picker
|
||||
sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
in_app_purchase_android:
|
||||
version: "1.1.2"
|
||||
image_picker_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: in_app_purchase_android
|
||||
sha256: "45ae4fe253f85b4fcc58b421fe137f6e48aca16bf8a618cd760cb0542e7f854e"
|
||||
name: image_picker_android
|
||||
sha256: "82652a75e3dd667a91187769a6a2cc81bd8c111bbead698d8e938d2b63e5e89a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.0"
|
||||
in_app_purchase_platform_interface:
|
||||
version: "0.8.12+21"
|
||||
image_picker_for_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: in_app_purchase_platform_interface
|
||||
sha256: "1d353d38251da5b9fea6635c0ebfc6bb17a2d28d0e86ea5e083bf64244f1fb4c"
|
||||
name: image_picker_for_web
|
||||
sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
in_app_purchase_storekit:
|
||||
version: "3.0.6"
|
||||
image_picker_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: in_app_purchase_storekit
|
||||
sha256: "276831961023055b55a2156c1fc043f50f6215ff49fb0f5f2273da6eeb510ecf"
|
||||
name: image_picker_ios
|
||||
sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.21"
|
||||
version: "0.8.12+2"
|
||||
image_picker_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_linux
|
||||
sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+2"
|
||||
image_picker_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_macos
|
||||
sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+2"
|
||||
image_picker_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_platform_interface
|
||||
sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.10.1"
|
||||
image_picker_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_windows
|
||||
sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+1"
|
||||
integration_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@@ -1343,10 +1359,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni
|
||||
sha256: f377c585ea9c08d48b427dc2e03780af2889d1bb094440da853c6883c1acba4b
|
||||
sha256: "459727a9daf91bdfb39b014cf3c186cf77f0136124a274ac83c186e12262ac4e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.1"
|
||||
version: "0.12.2"
|
||||
js:
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
@@ -1447,10 +1463,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: local_auth_android
|
||||
sha256: "6763aaf8965f21822624cb2fd3c03d2a8b3791037b5efb0fe4b13e110f5afc92"
|
||||
sha256: "8bba79f4f0f7bc812fce2ca20915d15618c37721246ba6c3ef2aa7a763a90cf2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.46"
|
||||
version: "1.0.47"
|
||||
local_auth_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1553,24 +1569,24 @@ packages:
|
||||
description:
|
||||
path: media_kit
|
||||
ref: HEAD
|
||||
resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80"
|
||||
resolved-ref: "6c16629c2d66555d53b13fd104d8e03f8eee3407"
|
||||
url: "https://github.com/media-kit/media-kit"
|
||||
source: git
|
||||
version: "1.1.11"
|
||||
version: "1.2.0"
|
||||
media_kit_libs_android_video:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: media_kit_libs_android_video
|
||||
sha256: "9dd8012572e4aff47516e55f2597998f0a378e3d588d0fad0ca1f11a53ae090c"
|
||||
sha256: adff9b571b8ead0867f9f91070f8df39562078c0eb3371d88b9029a2d547d7b7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.6"
|
||||
version: "1.3.7"
|
||||
media_kit_libs_ios_video:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "libs/ios/media_kit_libs_ios_video"
|
||||
ref: HEAD
|
||||
resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80"
|
||||
resolved-ref: "6c16629c2d66555d53b13fd104d8e03f8eee3407"
|
||||
url: "https://github.com/media-kit/media-kit"
|
||||
source: git
|
||||
version: "1.1.4"
|
||||
@@ -1578,10 +1594,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: media_kit_libs_linux
|
||||
sha256: e186891c31daa6bedab4d74dcdb4e8adfccc7d786bfed6ad81fe24a3b3010310
|
||||
sha256: "2b473399a49ec94452c4d4ae51cfc0f6585074398d74216092bf3d54aac37ecf"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.3"
|
||||
version: "1.2.1"
|
||||
media_kit_libs_macos_video:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1595,27 +1611,27 @@ packages:
|
||||
description:
|
||||
path: "libs/universal/media_kit_libs_video"
|
||||
ref: HEAD
|
||||
resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80"
|
||||
resolved-ref: "6c16629c2d66555d53b13fd104d8e03f8eee3407"
|
||||
url: "https://github.com/media-kit/media-kit"
|
||||
source: git
|
||||
version: "1.0.5"
|
||||
version: "1.0.6"
|
||||
media_kit_libs_windows_video:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: media_kit_libs_windows_video
|
||||
sha256: "32654572167825c42c55466f5d08eee23ea11061c84aa91b09d0e0f69bdd0887"
|
||||
sha256: dff76da2778729ab650229e6b4ec6ec111eb5151431002cbd7ea304ff1f112ab
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.10"
|
||||
version: "1.0.11"
|
||||
media_kit_video:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: media_kit_video
|
||||
ref: HEAD
|
||||
resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80"
|
||||
resolved-ref: "6c16629c2d66555d53b13fd104d8e03f8eee3407"
|
||||
url: "https://github.com/media-kit/media-kit"
|
||||
source: git
|
||||
version: "1.2.5"
|
||||
version: "1.3.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1696,10 +1712,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: multicast_dns
|
||||
sha256: "0a568c8411ab0979ab8cd4af1c29b6d316d854ab81592463ccceb92b35fde813"
|
||||
sha256: de72ada5c3db6fdd6ad4ae99452fe05fb403c4bb37c67ceb255ddd37d2b5b1eb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.2+8"
|
||||
version: "0.3.3"
|
||||
nanoid:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1720,10 +1736,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: native_video_player
|
||||
sha256: "571d2ddb9ce297a653ca69ced40e30135c6a59c5a9be9a38e3b370dd6e4bdb0e"
|
||||
sha256: "64ac4086c50f13306c7ebca70372b2c2c67c063caae25f0c486dbec16d666e9a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0-dev.4"
|
||||
version: "3.0.0"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1752,10 +1768,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: objective_c
|
||||
sha256: "62e79ab8c3ed6f6a340ea50dd48d65898f5d70425d404f0d99411f6e56e04584"
|
||||
sha256: "9f034ba1eeca53ddb339bc8f4813cb07336a849cd735559b60cdc068ecce2dc7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
version: "7.1.0"
|
||||
octo_image:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1793,26 +1809,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67"
|
||||
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.2.0"
|
||||
package_info_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: "67eae327b1b0faf761964a1d2e5d323c797f3799db0e85aa232db8d9e922bc35"
|
||||
sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.2.1"
|
||||
version: "8.3.0"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_platform_interface
|
||||
sha256: "205ec83335c2ab9107bbba3f8997f9356d72ca3c715d2f038fc773d0366b4c76"
|
||||
sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "3.2.0"
|
||||
panorama:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1922,10 +1938,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_apple
|
||||
sha256: f84a188e79a35c687c132a0a0556c254747a08561e99ab933f12f6ca71ef3c98
|
||||
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.4.6"
|
||||
version: "9.4.7"
|
||||
permission_handler_html:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2059,26 +2075,26 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: protobuf
|
||||
sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d"
|
||||
sha256: fbb0c37d435641d0b84813c1dad41e6fa61ddc880a320bce16b3063ecec35aa6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "4.0.0"
|
||||
provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: provider
|
||||
sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
|
||||
sha256: "489024f942069c2920c844ee18bb3d467c69e48955a4f32d1677f71be103e310"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.2"
|
||||
version: "6.1.4"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd"
|
||||
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
version: "2.2.0"
|
||||
pubspec_parse:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2124,10 +2140,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: screen_brightness_android
|
||||
sha256: ff9141bed547db02233e7dd88f990ab01973a0c8a8c04ddb855c7b072f33409a
|
||||
sha256: "6ba1b5812f66c64e9e4892be2d36ecd34210f4e0da8bdec6a2ea34f1aa42683e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.1.1"
|
||||
screen_brightness_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2156,18 +2172,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sentry
|
||||
sha256: "077b03f9ee44cfb1eaadbf8af58255e670de62b3f240ca154ce96a5591dc3885"
|
||||
sha256: "599701ca0693a74da361bc780b0752e1abc98226cf5095f6b069648116c896bb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.14.1"
|
||||
version: "8.14.2"
|
||||
sentry_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sentry_flutter
|
||||
sha256: a348e2a365a8ad7682dd09db54f50f19f1c87180b8278f088bc393c511aea5e0
|
||||
sha256: "5ba2cf40646a77d113b37a07bd69f61bb3ec8a73cbabe5537b05a7c89d2656f8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.14.1"
|
||||
version: "8.14.2"
|
||||
share_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -2188,18 +2204,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a"
|
||||
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.2"
|
||||
version: "2.5.3"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: a768fc8ede5f0c8e6150476e14f38e2417c0864ca36bb4582be8e21925a03c22
|
||||
sha256: "9f9f3d372d4304723e6136663bb291c0b93f5e4c8a4a6314347f481a33bda2b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.6"
|
||||
version: "2.4.7"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2377,18 +2393,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlite3
|
||||
sha256: "32b632dda27d664f85520093ed6f735ae5c49b5b75345afb8b19411bc59bb53d"
|
||||
sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.4"
|
||||
version: "2.7.5"
|
||||
sqlite3_flutter_libs:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqlite3_flutter_libs
|
||||
sha256: "57fafacd815c981735406215966ff7caaa8eab984b094f52e692accefcbd9233"
|
||||
sha256: "1a96b59227828d9eb1463191d684b37a27d66ee5ed7597fcf42eee6452c88a14"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.30"
|
||||
version: "0.5.32"
|
||||
sqlite_async:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -2617,10 +2633,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626"
|
||||
sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.2"
|
||||
version: "6.3.3"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2731,10 +2747,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_avfoundation
|
||||
sha256: "84b4752745eeccb6e75865c9aab39b3d28eb27ba5726d352d45db8297fbd75bc"
|
||||
sha256: "9ee764e5cd2fc1e10911ae8ad588e1a19db3b6aa9a6eb53c127c42d3a3c3f22f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
version: "2.7.1"
|
||||
video_player_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2747,10 +2763,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_web
|
||||
sha256: "3ef40ea6d72434edbfdba4624b90fd3a80a0740d260667d91e7ecd2d79e13476"
|
||||
sha256: e8bba2e5d1e159d5048c9a491bb2a7b29c535c612bb7d10c1e21107f5bd365ba
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.4"
|
||||
version: "2.3.5"
|
||||
video_thumbnail:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -2780,26 +2796,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: volume_controller
|
||||
sha256: "30863a51338db47fe16f92902b1a6c4ee5e15c9287b46573d7c2eb6be1f197d2"
|
||||
sha256: e82fd689bb8e1fe8e64be3fa5946ff8699058f8cf9f4c1679acdba20cda7f5bd
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.1"
|
||||
version: "3.3.3"
|
||||
wakelock_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: wakelock_plus
|
||||
sha256: "36c88af0b930121941345306d259ec4cc4ecca3b151c02e3a9e71aede83c615e"
|
||||
sha256: b6962cd9fc15e4843b573ba7b53bc46dd8a787594cf9ed5c5182581924656a58
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.10"
|
||||
version: "1.3.1"
|
||||
wakelock_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wakelock_plus_platform_interface
|
||||
sha256: "70e780bc99796e1db82fe764b1e7dcb89a86f1e5b3afb1db354de50f2e41eb7a"
|
||||
sha256: e10444072e50dbc4999d7316fd303f7ea53d31c824aa5eb05d7ccbdd98985207
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
version: "1.2.3"
|
||||
watcher:
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
@@ -2812,26 +2828,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.1.1"
|
||||
web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket
|
||||
sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83"
|
||||
sha256: bfe6f435f6ec49cb6c01da1e275ae4228719e59a6b067048c51e72d9d63bcc4b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.6"
|
||||
version: "1.0.0"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5"
|
||||
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
version: "3.0.3"
|
||||
webdriver:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -74,8 +74,6 @@ dependencies:
|
||||
ref: android-packaged
|
||||
figma_squircle: ^0.6.3
|
||||
file_saver: ^0.2.14
|
||||
firebase_core: ^3.6.0
|
||||
firebase_messaging: ^15.1.3
|
||||
fixnum: ^1.1.1
|
||||
flutter:
|
||||
sdk: flutter
|
||||
@@ -110,7 +108,7 @@ dependencies:
|
||||
http: ^1.1.0
|
||||
image: ^4.0.17
|
||||
image_editor: ^1.6.0
|
||||
in_app_purchase: ^3.0.7
|
||||
image_picker: ^1.1.1
|
||||
intl: ^0.19.0
|
||||
latlong2: ^0.9.0
|
||||
launcher_icon_switcher: ^0.0.2
|
||||
@@ -169,7 +167,7 @@ dependencies:
|
||||
git:
|
||||
url: https://github.com/eddyuan/privacy_screen.git
|
||||
ref: 855418e
|
||||
protobuf: ^3.1.0
|
||||
protobuf: ^4.0.0
|
||||
receive_sharing_intent: # pub.dev is behind
|
||||
git:
|
||||
url: https://github.com/KasemJaffer/receive_sharing_intent.git
|
||||
|
||||
1
mobile/thirdparty/flutter
vendored
Submodule
19
mobile/thirdparty/transistor-background-fetch/LICENSE
vendored
Normal 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.
|
||||
22
mobile/thirdparty/transistor-background-fetch/README.md
vendored
Normal 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.
|
||||
28
mobile/thirdparty/transistor-background-fetch/TSBackgroundFetch.podspec
vendored
Normal 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
|
||||
9
mobile/thirdparty/transistor-background-fetch/android/.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/workspace.xml
|
||||
/.idea/libraries
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
1
mobile/thirdparty/transistor-background-fetch/android/app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
28
mobile/thirdparty/transistor-background-fetch/android/app/build.gradle
vendored
Normal 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'])
|
||||
|
||||
}
|
||||
21
mobile/thirdparty/transistor-background-fetch/android/app/proguard-rules.pro
vendored
Normal 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
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
11
mobile/thirdparty/transistor-background-fetch/android/app/src/main/AndroidManifest.xml
vendored
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
BIN
mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
vendored
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
BIN
mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
vendored
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
BIN
mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
vendored
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
BIN
mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
vendored
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 15 KiB |
6
mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/values/colors.xml
vendored
Normal 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>
|
||||
3
mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/values/strings.xml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">BackgroundFetch</string>
|
||||
</resources>
|
||||
11
mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/values/styles.xml
vendored
Normal 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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
34
mobile/thirdparty/transistor-background-fetch/android/build.gradle
vendored
Normal 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"
|
||||
}
|
||||
23
mobile/thirdparty/transistor-background-fetch/android/gradle.properties
vendored
Normal 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
|
||||
BIN
mobile/thirdparty/transistor-background-fetch/android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal 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
|
||||
160
mobile/thirdparty/transistor-background-fetch/android/gradlew
vendored
Executable 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 "$@"
|
||||
90
mobile/thirdparty/transistor-background-fetch/android/gradlew.bat
vendored
Normal 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
|
||||
1
mobile/thirdparty/transistor-background-fetch/android/settings.gradle
vendored
Normal file
@@ -0,0 +1 @@
|
||||
include ':app', ':tsbackgroundfetch'
|
||||
1
mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
152
mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/build.gradle
vendored
Normal 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')
|
||||
}
|
||||
21
mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/proguard-rules.pro
vendored
Normal 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
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">TSBackgroundFetch</string>
|
||||
</resources>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||