Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
834bee3f46 |
3
.gitmodules
vendored
@@ -9,6 +9,3 @@
|
||||
[submodule "auth/assets/simple-icons"]
|
||||
path = mobile/apps/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
|
||||
|
||||
@@ -349,5 +349,52 @@
|
||||
"mastodon": "Mastodon",
|
||||
"matrix": "Matrix",
|
||||
"discord": "Discord",
|
||||
"reddit": "Reddit"
|
||||
"reddit": "Reddit",
|
||||
"information": "Information",
|
||||
"saveInformation": "Save information",
|
||||
"informationDescription": "Save important information that can be shared and passed down to loved ones.",
|
||||
"personalNote": "Personal note",
|
||||
"personalNoteDescription": "Save important notes or thoughts",
|
||||
"physicalRecords": "Physical records",
|
||||
"physicalRecordsDescription": "Save the real-world locations of important items",
|
||||
"accountCredentials": "Account credentials",
|
||||
"accountCredentialsDescription": "Securely store login details for important accounts",
|
||||
"emergencyContact": "Emergency contact",
|
||||
"emergencyContactDescription": "Save details of people to contact in emergencies",
|
||||
"noteName": "Title",
|
||||
"noteNameHint": "Give your note a meaningful title",
|
||||
"noteContent": "Content",
|
||||
"noteContentHint": "Write down important thoughts, instructions, or memories you want to preserve",
|
||||
"recordName": "Record name",
|
||||
"recordNameHint": "Name of the real-world item",
|
||||
"recordLocation": "Location",
|
||||
"recordLocationHint": "Where can this item be found? (e.g., 'Safety deposit box at First Bank, Box #123')",
|
||||
"recordNotes": "Notes",
|
||||
"recordNotesHint": "Any additional details about accessing or understanding this record",
|
||||
"credentialName": "Account name",
|
||||
"credentialNameHint": "Name of the service or account",
|
||||
"username": "Username",
|
||||
"usernameHint": "Login username or email address",
|
||||
"password": "Password",
|
||||
"passwordHint": "Account password",
|
||||
"credentialNotes": "Additional notes",
|
||||
"credentialNotesHint": "Recovery methods, security questions, or other important details",
|
||||
"contactName": "Contact name",
|
||||
"contactNameHint": "Full name of the emergency contact",
|
||||
"contactDetails": "Contact details",
|
||||
"contactDetailsHint": "Phone number, email, or other contact information",
|
||||
"contactNotes": "Message for contact",
|
||||
"contactNotesHint": "Important information to share with this person when they are contacted",
|
||||
"saveRecord": "Save",
|
||||
"recordSavedSuccessfully": "Record saved successfully",
|
||||
"failedToSaveRecord": "Failed to save record",
|
||||
"pleaseEnterNoteName": "Please enter a title",
|
||||
"pleaseEnterNoteContent": "Please enter content",
|
||||
"pleaseEnterRecordName": "Please enter a record name",
|
||||
"pleaseEnterLocation": "Please enter a location",
|
||||
"pleaseEnterAccountName": "Please enter an account name",
|
||||
"pleaseEnterUsername": "Please enter a username",
|
||||
"pleaseEnterPassword": "Please enter a password",
|
||||
"pleaseEnterContactName": "Please enter a contact name",
|
||||
"pleaseEnterContactDetails": "Please enter contact details"
|
||||
}
|
||||
|
||||
@@ -1017,6 +1017,288 @@ abstract class AppLocalizations {
|
||||
/// In en, this message translates to:
|
||||
/// **'Reddit'**
|
||||
String get reddit;
|
||||
|
||||
/// No description provided for @information.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Information'**
|
||||
String get information;
|
||||
|
||||
/// No description provided for @saveInformation.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Save information'**
|
||||
String get saveInformation;
|
||||
|
||||
/// No description provided for @informationDescription.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Save important information that can be shared and passed down to loved ones.'**
|
||||
String get informationDescription;
|
||||
|
||||
/// No description provided for @personalNote.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Personal note'**
|
||||
String get personalNote;
|
||||
|
||||
/// No description provided for @personalNoteDescription.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Save important notes or thoughts'**
|
||||
String get personalNoteDescription;
|
||||
|
||||
/// No description provided for @physicalRecords.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Physical records'**
|
||||
String get physicalRecords;
|
||||
|
||||
/// No description provided for @physicalRecordsDescription.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Save the real-world locations of important items'**
|
||||
String get physicalRecordsDescription;
|
||||
|
||||
/// No description provided for @accountCredentials.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Account credentials'**
|
||||
String get accountCredentials;
|
||||
|
||||
/// No description provided for @accountCredentialsDescription.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Securely store login details for important accounts'**
|
||||
String get accountCredentialsDescription;
|
||||
|
||||
/// No description provided for @emergencyContact.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Emergency contact'**
|
||||
String get emergencyContact;
|
||||
|
||||
/// No description provided for @emergencyContactDescription.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Save details of people to contact in emergencies'**
|
||||
String get emergencyContactDescription;
|
||||
|
||||
/// No description provided for @noteName.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Title'**
|
||||
String get noteName;
|
||||
|
||||
/// No description provided for @noteNameHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Give your note a meaningful title'**
|
||||
String get noteNameHint;
|
||||
|
||||
/// No description provided for @noteContent.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Content'**
|
||||
String get noteContent;
|
||||
|
||||
/// No description provided for @noteContentHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Write down important thoughts, instructions, or memories you want to preserve'**
|
||||
String get noteContentHint;
|
||||
|
||||
/// No description provided for @recordName.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Record name'**
|
||||
String get recordName;
|
||||
|
||||
/// No description provided for @recordNameHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Name of the real-world item'**
|
||||
String get recordNameHint;
|
||||
|
||||
/// No description provided for @recordLocation.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Location'**
|
||||
String get recordLocation;
|
||||
|
||||
/// No description provided for @recordLocationHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Where can this item be found? (e.g., \'Safety deposit box at First Bank, Box #123\')'**
|
||||
String get recordLocationHint;
|
||||
|
||||
/// No description provided for @recordNotes.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Notes'**
|
||||
String get recordNotes;
|
||||
|
||||
/// No description provided for @recordNotesHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Any additional details about accessing or understanding this record'**
|
||||
String get recordNotesHint;
|
||||
|
||||
/// No description provided for @credentialName.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Account name'**
|
||||
String get credentialName;
|
||||
|
||||
/// No description provided for @credentialNameHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Name of the service or account'**
|
||||
String get credentialNameHint;
|
||||
|
||||
/// No description provided for @username.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Username'**
|
||||
String get username;
|
||||
|
||||
/// No description provided for @usernameHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Login username or email address'**
|
||||
String get usernameHint;
|
||||
|
||||
/// No description provided for @password.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Password'**
|
||||
String get password;
|
||||
|
||||
/// No description provided for @passwordHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Account password'**
|
||||
String get passwordHint;
|
||||
|
||||
/// No description provided for @credentialNotes.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Additional notes'**
|
||||
String get credentialNotes;
|
||||
|
||||
/// No description provided for @credentialNotesHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Recovery methods, security questions, or other important details'**
|
||||
String get credentialNotesHint;
|
||||
|
||||
/// No description provided for @contactName.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Contact name'**
|
||||
String get contactName;
|
||||
|
||||
/// No description provided for @contactNameHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Full name of the emergency contact'**
|
||||
String get contactNameHint;
|
||||
|
||||
/// No description provided for @contactDetails.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Contact details'**
|
||||
String get contactDetails;
|
||||
|
||||
/// No description provided for @contactDetailsHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Phone number, email, or other contact information'**
|
||||
String get contactDetailsHint;
|
||||
|
||||
/// No description provided for @contactNotes.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Message for contact'**
|
||||
String get contactNotes;
|
||||
|
||||
/// No description provided for @contactNotesHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Important information to share with this person when they are contacted'**
|
||||
String get contactNotesHint;
|
||||
|
||||
/// No description provided for @saveRecord.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Save'**
|
||||
String get saveRecord;
|
||||
|
||||
/// No description provided for @recordSavedSuccessfully.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Record saved successfully'**
|
||||
String get recordSavedSuccessfully;
|
||||
|
||||
/// No description provided for @failedToSaveRecord.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Failed to save record'**
|
||||
String get failedToSaveRecord;
|
||||
|
||||
/// No description provided for @pleaseEnterNoteName.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Please enter a title'**
|
||||
String get pleaseEnterNoteName;
|
||||
|
||||
/// No description provided for @pleaseEnterNoteContent.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Please enter content'**
|
||||
String get pleaseEnterNoteContent;
|
||||
|
||||
/// No description provided for @pleaseEnterRecordName.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Please enter a record name'**
|
||||
String get pleaseEnterRecordName;
|
||||
|
||||
/// No description provided for @pleaseEnterLocation.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Please enter a location'**
|
||||
String get pleaseEnterLocation;
|
||||
|
||||
/// No description provided for @pleaseEnterAccountName.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Please enter an account name'**
|
||||
String get pleaseEnterAccountName;
|
||||
|
||||
/// No description provided for @pleaseEnterUsername.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Please enter a username'**
|
||||
String get pleaseEnterUsername;
|
||||
|
||||
/// No description provided for @pleaseEnterPassword.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Please enter a password'**
|
||||
String get pleaseEnterPassword;
|
||||
|
||||
/// No description provided for @pleaseEnterContactName.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Please enter a contact name'**
|
||||
String get pleaseEnterContactName;
|
||||
|
||||
/// No description provided for @pleaseEnterContactDetails.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Please enter contact details'**
|
||||
String get pleaseEnterContactDetails;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
||||
@@ -534,4 +534,155 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get reddit => 'Reddit';
|
||||
|
||||
@override
|
||||
String get information => 'Information';
|
||||
|
||||
@override
|
||||
String get saveInformation => 'Save information';
|
||||
|
||||
@override
|
||||
String get informationDescription =>
|
||||
'Save important information that can be shared and passed down to loved ones.';
|
||||
|
||||
@override
|
||||
String get personalNote => 'Personal note';
|
||||
|
||||
@override
|
||||
String get personalNoteDescription => 'Save important notes or thoughts';
|
||||
|
||||
@override
|
||||
String get physicalRecords => 'Physical records';
|
||||
|
||||
@override
|
||||
String get physicalRecordsDescription =>
|
||||
'Save the real-world locations of important items';
|
||||
|
||||
@override
|
||||
String get accountCredentials => 'Account credentials';
|
||||
|
||||
@override
|
||||
String get accountCredentialsDescription =>
|
||||
'Securely store login details for important accounts';
|
||||
|
||||
@override
|
||||
String get emergencyContact => 'Emergency contact';
|
||||
|
||||
@override
|
||||
String get emergencyContactDescription =>
|
||||
'Save details of people to contact in emergencies';
|
||||
|
||||
@override
|
||||
String get noteName => 'Title';
|
||||
|
||||
@override
|
||||
String get noteNameHint => 'Give your note a meaningful title';
|
||||
|
||||
@override
|
||||
String get noteContent => 'Content';
|
||||
|
||||
@override
|
||||
String get noteContentHint =>
|
||||
'Write down important thoughts, instructions, or memories you want to preserve';
|
||||
|
||||
@override
|
||||
String get recordName => 'Record name';
|
||||
|
||||
@override
|
||||
String get recordNameHint => 'Name of the real-world item';
|
||||
|
||||
@override
|
||||
String get recordLocation => 'Location';
|
||||
|
||||
@override
|
||||
String get recordLocationHint =>
|
||||
'Where can this item be found? (e.g., \'Safety deposit box at First Bank, Box #123\')';
|
||||
|
||||
@override
|
||||
String get recordNotes => 'Notes';
|
||||
|
||||
@override
|
||||
String get recordNotesHint =>
|
||||
'Any additional details about accessing or understanding this record';
|
||||
|
||||
@override
|
||||
String get credentialName => 'Account name';
|
||||
|
||||
@override
|
||||
String get credentialNameHint => 'Name of the service or account';
|
||||
|
||||
@override
|
||||
String get username => 'Username';
|
||||
|
||||
@override
|
||||
String get usernameHint => 'Login username or email address';
|
||||
|
||||
@override
|
||||
String get password => 'Password';
|
||||
|
||||
@override
|
||||
String get passwordHint => 'Account password';
|
||||
|
||||
@override
|
||||
String get credentialNotes => 'Additional notes';
|
||||
|
||||
@override
|
||||
String get credentialNotesHint =>
|
||||
'Recovery methods, security questions, or other important details';
|
||||
|
||||
@override
|
||||
String get contactName => 'Contact name';
|
||||
|
||||
@override
|
||||
String get contactNameHint => 'Full name of the emergency contact';
|
||||
|
||||
@override
|
||||
String get contactDetails => 'Contact details';
|
||||
|
||||
@override
|
||||
String get contactDetailsHint =>
|
||||
'Phone number, email, or other contact information';
|
||||
|
||||
@override
|
||||
String get contactNotes => 'Message for contact';
|
||||
|
||||
@override
|
||||
String get contactNotesHint =>
|
||||
'Important information to share with this person when they are contacted';
|
||||
|
||||
@override
|
||||
String get saveRecord => 'Save';
|
||||
|
||||
@override
|
||||
String get recordSavedSuccessfully => 'Record saved successfully';
|
||||
|
||||
@override
|
||||
String get failedToSaveRecord => 'Failed to save record';
|
||||
|
||||
@override
|
||||
String get pleaseEnterNoteName => 'Please enter a title';
|
||||
|
||||
@override
|
||||
String get pleaseEnterNoteContent => 'Please enter content';
|
||||
|
||||
@override
|
||||
String get pleaseEnterRecordName => 'Please enter a record name';
|
||||
|
||||
@override
|
||||
String get pleaseEnterLocation => 'Please enter a location';
|
||||
|
||||
@override
|
||||
String get pleaseEnterAccountName => 'Please enter an account name';
|
||||
|
||||
@override
|
||||
String get pleaseEnterUsername => 'Please enter a username';
|
||||
|
||||
@override
|
||||
String get pleaseEnterPassword => 'Please enter a password';
|
||||
|
||||
@override
|
||||
String get pleaseEnterContactName => 'Please enter a contact name';
|
||||
|
||||
@override
|
||||
String get pleaseEnterContactDetails => 'Please enter contact details';
|
||||
}
|
||||
|
||||
54
mobile/apps/locker/lib/models/file_type.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
enum FileType {
|
||||
image,
|
||||
video,
|
||||
livePhoto,
|
||||
other,
|
||||
info, // New type for information files
|
||||
}
|
||||
|
||||
int getInt(FileType fileType) {
|
||||
switch (fileType) {
|
||||
case FileType.image:
|
||||
return 0;
|
||||
case FileType.video:
|
||||
return 1;
|
||||
case FileType.livePhoto:
|
||||
return 2;
|
||||
case FileType.other:
|
||||
return 3;
|
||||
case FileType.info:
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
FileType getFileType(int fileType) {
|
||||
switch (fileType) {
|
||||
case 0:
|
||||
return FileType.image;
|
||||
case 1:
|
||||
return FileType.video;
|
||||
case 2:
|
||||
return FileType.livePhoto;
|
||||
case 3:
|
||||
return FileType.other;
|
||||
case 4:
|
||||
return FileType.info;
|
||||
default:
|
||||
return FileType.other;
|
||||
}
|
||||
}
|
||||
|
||||
String getHumanReadableString(FileType fileType) {
|
||||
switch (fileType) {
|
||||
case FileType.image:
|
||||
return 'Images';
|
||||
case FileType.video:
|
||||
return 'Videos';
|
||||
case FileType.livePhoto:
|
||||
return 'Live Photos';
|
||||
case FileType.other:
|
||||
return 'Other Files';
|
||||
case FileType.info:
|
||||
return 'Information';
|
||||
}
|
||||
}
|
||||
244
mobile/apps/locker/lib/models/info/info_item.dart
Normal file
@@ -0,0 +1,244 @@
|
||||
import 'dart:convert';
|
||||
|
||||
// Enum for different information types
|
||||
enum InfoType {
|
||||
note,
|
||||
physicalRecord,
|
||||
accountCredential,
|
||||
emergencyContact,
|
||||
}
|
||||
|
||||
// Extension to convert enum to string and vice versa
|
||||
extension InfoTypeExtension on InfoType {
|
||||
String get value {
|
||||
switch (this) {
|
||||
case InfoType.note:
|
||||
return 'note';
|
||||
case InfoType.physicalRecord:
|
||||
return 'physical-record';
|
||||
case InfoType.accountCredential:
|
||||
return 'account-credential';
|
||||
case InfoType.emergencyContact:
|
||||
return 'emergency-contact';
|
||||
}
|
||||
}
|
||||
|
||||
static InfoType fromString(String value) {
|
||||
switch (value) {
|
||||
case 'note':
|
||||
return InfoType.note;
|
||||
case 'physical-record':
|
||||
return InfoType.physicalRecord;
|
||||
case 'account-credential':
|
||||
return InfoType.accountCredential;
|
||||
case 'emergency-contact':
|
||||
return InfoType.emergencyContact;
|
||||
default:
|
||||
throw ArgumentError('Unknown InfoType: $value');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Base class for all information data
|
||||
abstract class InfoData {
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
static InfoData fromJson(InfoType type, Map<String, dynamic> json) {
|
||||
switch (type) {
|
||||
case InfoType.note:
|
||||
return PersonalNoteData.fromJson(json);
|
||||
case InfoType.physicalRecord:
|
||||
return PhysicalRecordData.fromJson(json);
|
||||
case InfoType.accountCredential:
|
||||
return AccountCredentialData.fromJson(json);
|
||||
case InfoType.emergencyContact:
|
||||
return EmergencyContactData.fromJson(json);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Personal Note Data Model
|
||||
class PersonalNoteData extends InfoData {
|
||||
final String title;
|
||||
final String content;
|
||||
|
||||
PersonalNoteData({
|
||||
required this.title,
|
||||
required this.content,
|
||||
});
|
||||
|
||||
factory PersonalNoteData.fromJson(Map<String, dynamic> json) {
|
||||
return PersonalNoteData(
|
||||
title: json['title'] ?? '',
|
||||
content: json['content'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'title': title,
|
||||
'content': content,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Physical Record Data Model
|
||||
class PhysicalRecordData extends InfoData {
|
||||
final String name;
|
||||
final String location;
|
||||
final String? notes;
|
||||
|
||||
PhysicalRecordData({
|
||||
required this.name,
|
||||
required this.location,
|
||||
this.notes,
|
||||
});
|
||||
|
||||
factory PhysicalRecordData.fromJson(Map<String, dynamic> json) {
|
||||
return PhysicalRecordData(
|
||||
name: json['name'] ?? '',
|
||||
location: json['location'] ?? '',
|
||||
notes: json['notes'],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
'location': location,
|
||||
if (notes != null && notes!.isNotEmpty) 'notes': notes,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Account Credential Data Model
|
||||
class AccountCredentialData extends InfoData {
|
||||
final String name;
|
||||
final String username;
|
||||
final String password;
|
||||
final String? notes;
|
||||
|
||||
AccountCredentialData({
|
||||
required this.name,
|
||||
required this.username,
|
||||
required this.password,
|
||||
this.notes,
|
||||
});
|
||||
|
||||
factory AccountCredentialData.fromJson(Map<String, dynamic> json) {
|
||||
return AccountCredentialData(
|
||||
name: json['name'] ?? '',
|
||||
username: json['username'] ?? '',
|
||||
password: json['password'] ?? '',
|
||||
notes: json['notes'],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
'username': username,
|
||||
'password': password,
|
||||
if (notes != null && notes!.isNotEmpty) 'notes': notes,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Emergency Contact Data Model
|
||||
class EmergencyContactData extends InfoData {
|
||||
final String name;
|
||||
final String contactDetails;
|
||||
final String? notes;
|
||||
|
||||
EmergencyContactData({
|
||||
required this.name,
|
||||
required this.contactDetails,
|
||||
this.notes,
|
||||
});
|
||||
|
||||
factory EmergencyContactData.fromJson(Map<String, dynamic> json) {
|
||||
return EmergencyContactData(
|
||||
name: json['name'] ?? '',
|
||||
contactDetails: json['contactDetails'] ?? '',
|
||||
notes: json['notes'],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
'contactDetails': contactDetails,
|
||||
if (notes != null && notes!.isNotEmpty) 'notes': notes,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Main Information Item wrapper
|
||||
class InfoItem {
|
||||
final InfoType type;
|
||||
final InfoData data;
|
||||
final DateTime createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
InfoItem({
|
||||
required this.type,
|
||||
required this.data,
|
||||
required this.createdAt,
|
||||
this.updatedAt,
|
||||
});
|
||||
|
||||
factory InfoItem.fromJson(Map<String, dynamic> json) {
|
||||
final type = InfoTypeExtension.fromString(json['type']);
|
||||
final data = InfoData.fromJson(type, json['data']);
|
||||
|
||||
return InfoItem(
|
||||
type: type,
|
||||
data: data,
|
||||
createdAt: DateTime.parse(json['createdAt']),
|
||||
updatedAt:
|
||||
json['updatedAt'] != null ? DateTime.parse(json['updatedAt']) : null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'type': type.value,
|
||||
'data': data.toJson(),
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
if (updatedAt != null) 'updatedAt': updatedAt!.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
String toJsonString() => jsonEncode(toJson());
|
||||
|
||||
static InfoItem fromJsonString(String jsonString) {
|
||||
return InfoItem.fromJson(jsonDecode(jsonString));
|
||||
}
|
||||
|
||||
// Create a copy with updated data
|
||||
InfoItem copyWith({
|
||||
InfoType? type,
|
||||
InfoData? data,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return InfoItem(
|
||||
type: type ?? this.type,
|
||||
data: data ?? this.data,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
// Update with new data and timestamp
|
||||
InfoItem update(InfoData newData) {
|
||||
return copyWith(
|
||||
data: newData,
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:locker/models/file_type.dart';
|
||||
import 'package:locker/services/files/download/file_url.dart';
|
||||
import 'package:locker/services/files/sync/models/file_magic.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
@@ -23,6 +24,7 @@ class EnteFile {
|
||||
String? thumbnailDecryptionHeader;
|
||||
String? metadataDecryptionHeader;
|
||||
int? fileSize;
|
||||
FileType? fileType;
|
||||
|
||||
String? mMdEncodedJson;
|
||||
int mMdVersion = 0;
|
||||
|
||||
@@ -17,6 +17,7 @@ const motionVideoIndexKey = "mvi";
|
||||
const noThumbKey = "noThumb";
|
||||
const dateTimeKey = 'dateTime';
|
||||
const offsetTimeKey = 'offsetTime';
|
||||
const infoKey = 'info';
|
||||
|
||||
class MagicMetadata {
|
||||
// 0 -> visible
|
||||
@@ -74,6 +75,11 @@ class PubMagicMetadata {
|
||||
// 1 -> panorama
|
||||
int? mediaType;
|
||||
|
||||
// JSON containing information data for info files
|
||||
// Contains type (note, physical-record, account-credential, emergency-contact)
|
||||
// and data (the actual information content)
|
||||
Map<String, dynamic>? info;
|
||||
|
||||
PubMagicMetadata({
|
||||
this.editedTime,
|
||||
this.editedName,
|
||||
@@ -89,6 +95,7 @@ class PubMagicMetadata {
|
||||
this.dateTime,
|
||||
this.offsetTime,
|
||||
this.sv,
|
||||
this.info,
|
||||
});
|
||||
|
||||
factory PubMagicMetadata.fromEncodedJson(String encodedJson) =>
|
||||
@@ -114,6 +121,7 @@ class PubMagicMetadata {
|
||||
dateTime: map[dateTimeKey],
|
||||
offsetTime: map[offsetTimeKey],
|
||||
sv: safeParseInt(map[streamVersionKey], streamVersionKey),
|
||||
info: map[infoKey],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -97,6 +97,66 @@ class FileUploader {
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
/// Special upload method for info files that contain only metadata
|
||||
Future<EnteFile> uploadInfoFile(
|
||||
EnteFile infoFile,
|
||||
Collection collection,
|
||||
) async {
|
||||
try {
|
||||
_logger.info('Starting upload of info file: ${infoFile.title}');
|
||||
|
||||
// Generate a file key for encryption
|
||||
final fileKey = CryptoUtil.generateKey();
|
||||
|
||||
// Create metadata for the info file
|
||||
final Map<String, dynamic> metadata = infoFile.metadata;
|
||||
final encryptedMetadataResult = await CryptoUtil.encryptData(
|
||||
utf8.encode(jsonEncode(metadata)),
|
||||
fileKey,
|
||||
);
|
||||
|
||||
final encryptedMetadata = CryptoUtil.bin2base64(
|
||||
encryptedMetadataResult.encryptedData!,
|
||||
);
|
||||
final metadataDecryptionHeader = CryptoUtil.bin2base64(
|
||||
encryptedMetadataResult.header!,
|
||||
);
|
||||
|
||||
// Encrypt the file key with collection key
|
||||
final encryptedFileKeyData = CryptoUtil.encryptSync(
|
||||
fileKey,
|
||||
CryptoHelper.instance.getCollectionKey(collection),
|
||||
);
|
||||
final encryptedKey =
|
||||
CryptoUtil.bin2base64(encryptedFileKeyData.encryptedData!);
|
||||
final keyDecryptionNonce =
|
||||
CryptoUtil.bin2base64(encryptedFileKeyData.nonce!);
|
||||
|
||||
final pubMetadataRequest = await getPubMetadataRequest(
|
||||
infoFile,
|
||||
{'info': infoFile.pubMagicMetadata.info},
|
||||
fileKey,
|
||||
);
|
||||
|
||||
// Upload as metadata-only file (no file content or thumbnail)
|
||||
final uploadedFile = await _uploadInfoFileMetadata(
|
||||
infoFile,
|
||||
collection.id,
|
||||
encryptedKey,
|
||||
keyDecryptionNonce,
|
||||
encryptedMetadata,
|
||||
metadataDecryptionHeader,
|
||||
pubMetadataRequest,
|
||||
);
|
||||
|
||||
_logger.info('Successfully uploaded info file: ${uploadedFile.title}');
|
||||
return uploadedFile;
|
||||
} catch (e, s) {
|
||||
_logger.severe('Failed to upload info file: ${infoFile.title}', e, s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
int getCurrentSessionUploadCount() {
|
||||
return _totalCountInUploadSession;
|
||||
}
|
||||
@@ -660,6 +720,43 @@ class FileUploader {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Upload method specifically for info files that don't require file content or thumbnails
|
||||
Future<EnteFile> _uploadInfoFileMetadata(
|
||||
EnteFile file,
|
||||
int collectionID,
|
||||
String encryptedKey,
|
||||
String keyDecryptionNonce,
|
||||
String encryptedMetadata,
|
||||
String metadataDecryptionHeader,
|
||||
MetadataRequest pubMetadata,
|
||||
) async {
|
||||
final request = {
|
||||
"collectionID": collectionID,
|
||||
"encryptedKey": encryptedKey,
|
||||
"keyDecryptionNonce": keyDecryptionNonce,
|
||||
"metadata": {
|
||||
"encryptedData": encryptedMetadata,
|
||||
"decryptionHeader": metadataDecryptionHeader,
|
||||
},
|
||||
"pubMagicMetadata": pubMetadata,
|
||||
};
|
||||
try {
|
||||
final response = await _enteDio.post("/files/meta", data: request);
|
||||
final data = response.data;
|
||||
file.uploadedFileID = data["id"];
|
||||
file.collectionID = collectionID;
|
||||
file.updationTime = data["updationTime"];
|
||||
file.ownerID = data["ownerID"];
|
||||
file.encryptedKey = encryptedKey;
|
||||
file.keyDecryptionNonce = keyDecryptionNonce;
|
||||
file.metadataDecryptionHeader = metadataDecryptionHeader;
|
||||
return file;
|
||||
} catch (e, s) {
|
||||
_logger.severe("Info file upload failed", e, s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FileUploadItem {
|
||||
|
||||
170
mobile/apps/locker/lib/services/info_file_service.dart
Normal file
@@ -0,0 +1,170 @@
|
||||
import 'package:locker/models/file_type.dart';
|
||||
import 'package:locker/models/info/info_item.dart';
|
||||
import 'package:locker/services/collections/models/collection.dart';
|
||||
import 'package:locker/services/files/sync/models/file.dart';
|
||||
import 'package:locker/services/files/sync/models/file_magic.dart';
|
||||
import 'package:locker/services/files/upload/file_upload_service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class InfoFileService {
|
||||
static final InfoFileService instance = InfoFileService._privateConstructor();
|
||||
InfoFileService._privateConstructor();
|
||||
|
||||
final _logger = Logger('InfoFileService');
|
||||
|
||||
/// Creates and uploads an info file
|
||||
Future<EnteFile> createAndUploadInfoFile({
|
||||
required InfoItem infoItem,
|
||||
required Collection collection,
|
||||
}) async {
|
||||
try {
|
||||
// Create EnteFile object directly without a physical file
|
||||
final enteFile = EnteFile();
|
||||
enteFile.fileType = FileType.info;
|
||||
enteFile.collectionID = collection.id;
|
||||
|
||||
// Set the title based on info type and data
|
||||
enteFile.title = _getInfoFileTitle(infoItem);
|
||||
|
||||
// Set creation and modification times
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
enteFile.creationTime = now;
|
||||
enteFile.modificationTime = now;
|
||||
|
||||
// Create public magic metadata with info data
|
||||
final pubMagicMetadata = PubMagicMetadata(
|
||||
info: {
|
||||
'type': infoItem.type.name,
|
||||
'data': infoItem.data.toJson(),
|
||||
},
|
||||
noThumb: true, // No thumbnail for info files
|
||||
);
|
||||
enteFile.pubMagicMetadata = pubMagicMetadata;
|
||||
|
||||
// Upload the file using the special info file upload method
|
||||
final uploadedFile = await _uploadInfoFile(enteFile, collection);
|
||||
|
||||
_logger.info('Successfully uploaded info file: ${uploadedFile.title}');
|
||||
return uploadedFile;
|
||||
} catch (e, s) {
|
||||
_logger.severe('Failed to create and upload info file', e, s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates an existing info file with new data
|
||||
Future<EnteFile> updateInfoFile({
|
||||
required EnteFile existingFile,
|
||||
required InfoItem updatedInfoItem,
|
||||
}) async {
|
||||
try {
|
||||
// Update the public magic metadata
|
||||
final updatedPubMagicMetadata = existingFile.pubMagicMetadata;
|
||||
updatedPubMagicMetadata.info = {
|
||||
'type': updatedInfoItem.type.name,
|
||||
'data': updatedInfoItem.data.toJson(),
|
||||
};
|
||||
|
||||
// Update the title
|
||||
final updatedTitle = _getInfoFileTitle(updatedInfoItem);
|
||||
updatedPubMagicMetadata.editedName = updatedTitle;
|
||||
updatedPubMagicMetadata.editedTime =
|
||||
DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
existingFile.pubMagicMetadata = updatedPubMagicMetadata;
|
||||
|
||||
// Update metadata on server
|
||||
// This would call the metadata update service
|
||||
// TODO: Implement metadata update and sync
|
||||
|
||||
_logger.info('Successfully updated info file: $updatedTitle');
|
||||
return existingFile;
|
||||
} catch (e, s) {
|
||||
_logger.severe('Failed to update info file', e, s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts info data from a file
|
||||
InfoItem? extractInfoFromFile(EnteFile file) {
|
||||
try {
|
||||
if (file.fileType != FileType.info ||
|
||||
file.pubMagicMetadata.info == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final infoData = file.pubMagicMetadata.info!;
|
||||
final typeString = infoData['type'] as String?;
|
||||
final data = infoData['data'] as Map<String, dynamic>?;
|
||||
|
||||
if (typeString == null || data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final infoType = InfoType.values.firstWhere(
|
||||
(type) => type.name == typeString,
|
||||
orElse: () => InfoType.note,
|
||||
);
|
||||
|
||||
InfoData infoDataObj;
|
||||
switch (infoType) {
|
||||
case InfoType.note:
|
||||
infoDataObj = PersonalNoteData.fromJson(data);
|
||||
break;
|
||||
case InfoType.physicalRecord:
|
||||
infoDataObj = PhysicalRecordData.fromJson(data);
|
||||
break;
|
||||
case InfoType.accountCredential:
|
||||
infoDataObj = AccountCredentialData.fromJson(data);
|
||||
break;
|
||||
case InfoType.emergencyContact:
|
||||
infoDataObj = EmergencyContactData.fromJson(data);
|
||||
break;
|
||||
}
|
||||
|
||||
return InfoItem(
|
||||
type: infoType,
|
||||
data: infoDataObj,
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
} catch (e, s) {
|
||||
_logger.severe('Failed to extract info from file', e, s);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if a file is an info file
|
||||
bool isInfoFile(EnteFile file) {
|
||||
return file.fileType == FileType.info && file.pubMagicMetadata.info != null;
|
||||
}
|
||||
|
||||
String _getInfoFileTitle(InfoItem infoItem) {
|
||||
switch (infoItem.type) {
|
||||
case InfoType.note:
|
||||
final noteData = infoItem.data as PersonalNoteData;
|
||||
return noteData.title.isNotEmpty ? noteData.title : 'Personal Note';
|
||||
case InfoType.physicalRecord:
|
||||
final recordData = infoItem.data as PhysicalRecordData;
|
||||
return recordData.name.isNotEmpty ? recordData.name : 'Physical Record';
|
||||
case InfoType.accountCredential:
|
||||
final credData = infoItem.data as AccountCredentialData;
|
||||
return credData.name.isNotEmpty
|
||||
? '${credData.name} Account'
|
||||
: 'Account Credential';
|
||||
case InfoType.emergencyContact:
|
||||
final contactData = infoItem.data as EmergencyContactData;
|
||||
return contactData.name.isNotEmpty
|
||||
? '${contactData.name} (Emergency Contact)'
|
||||
: 'Emergency Contact';
|
||||
}
|
||||
}
|
||||
|
||||
/// Special upload method for info files that don't require physical file content
|
||||
Future<EnteFile> _uploadInfoFile(
|
||||
EnteFile enteFile,
|
||||
Collection collection,
|
||||
) async {
|
||||
// Use the FileUploader's special method for info files
|
||||
return await FileUploader.instance.uploadInfoFile(enteFile, collection);
|
||||
}
|
||||
}
|
||||
224
mobile/apps/locker/lib/ui/components/form_text_input_widget.dart
Normal file
@@ -0,0 +1,224 @@
|
||||
import 'package:ente_ui/components/text_input_widget.dart';
|
||||
import 'package:ente_ui/theme/ente_theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// A form-compatible wrapper that uses Ente UI TextInputWidget when possible,
|
||||
/// or falls back to custom implementation for advanced features
|
||||
class FormTextInputWidget extends StatefulWidget {
|
||||
final TextEditingController controller;
|
||||
final String labelText;
|
||||
final String? hintText;
|
||||
final String? Function(String?)? validator;
|
||||
final bool obscureText;
|
||||
final Widget? suffixIcon;
|
||||
final int? maxLines;
|
||||
final TextCapitalization textCapitalization;
|
||||
final TextInputType? keyboardType;
|
||||
final bool enabled;
|
||||
final bool autofocus;
|
||||
final int? maxLength;
|
||||
final bool showValidationErrors;
|
||||
|
||||
const FormTextInputWidget({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.labelText,
|
||||
this.hintText,
|
||||
this.validator,
|
||||
this.obscureText = false,
|
||||
this.suffixIcon,
|
||||
this.maxLines = 1,
|
||||
this.textCapitalization = TextCapitalization.none,
|
||||
this.keyboardType,
|
||||
this.enabled = true,
|
||||
this.autofocus = false,
|
||||
this.maxLength,
|
||||
this.showValidationErrors = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<FormTextInputWidget> createState() => _FormTextInputWidgetState();
|
||||
}
|
||||
|
||||
class _FormTextInputWidgetState extends State<FormTextInputWidget> {
|
||||
final GlobalKey<FormFieldState> _formFieldKey = GlobalKey<FormFieldState>();
|
||||
String? _errorText;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.controller.addListener(_onTextChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller.removeListener(_onTextChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onTextChanged() {
|
||||
if (_errorText != null) {
|
||||
setState(() {
|
||||
_errorText = null;
|
||||
});
|
||||
}
|
||||
// Only validate if we should show validation errors
|
||||
if (widget.showValidationErrors) {
|
||||
_formFieldKey.currentState?.validate();
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we can use the UI package's TextInputWidget
|
||||
bool get _canUseTextInputWidget {
|
||||
return widget.suffixIcon == null &&
|
||||
(widget.maxLines ?? 1) == 1 &&
|
||||
widget.enabled;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
final textTheme = getEnteTextTheme(context);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (_canUseTextInputWidget) ...[
|
||||
// Use the UI package's TextInputWidget for simple cases
|
||||
TextInputWidget(
|
||||
label: widget.labelText,
|
||||
hintText: widget.hintText,
|
||||
initialValue: widget.controller.text,
|
||||
isPasswordInput: widget.obscureText,
|
||||
textCapitalization: widget.textCapitalization,
|
||||
autoFocus: widget.autofocus,
|
||||
maxLength: widget.maxLength,
|
||||
shouldSurfaceExecutionStates: false,
|
||||
onChange: (value) {
|
||||
if (widget.controller.text != value) {
|
||||
widget.controller.text = value;
|
||||
}
|
||||
},
|
||||
),
|
||||
] else ...[
|
||||
// Custom implementation for advanced features
|
||||
if (widget.labelText.isNotEmpty) ...[
|
||||
Text(widget.labelText),
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: TextFormField(
|
||||
controller: widget.controller,
|
||||
validator: (value) => null, // Handled separately
|
||||
obscureText: widget.obscureText,
|
||||
maxLines: widget.obscureText ? 1 : widget.maxLines,
|
||||
textCapitalization: widget.textCapitalization,
|
||||
keyboardType: widget.keyboardType,
|
||||
enabled: widget.enabled,
|
||||
autofocus: widget.autofocus,
|
||||
inputFormatters: widget.maxLength != null
|
||||
? [LengthLimitingTextInputFormatter(widget.maxLength!)]
|
||||
: null,
|
||||
style: textTheme.body,
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.hintText,
|
||||
hintStyle:
|
||||
textTheme.body.copyWith(color: colorScheme.textMuted),
|
||||
filled: true,
|
||||
fillColor: colorScheme.fillFaint,
|
||||
contentPadding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
|
||||
border: const UnderlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: colorScheme.strokeFaint,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: colorScheme.primary500,
|
||||
width: 2,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: colorScheme.warning500,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: colorScheme.warning500,
|
||||
width: 2,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
suffixIcon: widget.suffixIcon != null
|
||||
? Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: widget.suffixIcon,
|
||||
)
|
||||
: null,
|
||||
suffixIconConstraints: const BoxConstraints(
|
||||
maxHeight: 24,
|
||||
maxWidth: 48,
|
||||
minHeight: 24,
|
||||
minWidth: 48,
|
||||
),
|
||||
errorStyle: const TextStyle(fontSize: 0, height: 0),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// Custom validation error display (for both cases)
|
||||
if (_errorText != null && widget.showValidationErrors) ...[
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text(
|
||||
_errorText!,
|
||||
style: textTheme.mini.copyWith(
|
||||
color: colorScheme.warning500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// Invisible FormField for validation integration
|
||||
SizedBox(
|
||||
height: 0,
|
||||
child: FormField<String>(
|
||||
key: _formFieldKey,
|
||||
validator: (value) {
|
||||
final error = widget.validator?.call(widget.controller.text);
|
||||
if (mounted && widget.showValidationErrors) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_errorText = error;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return error;
|
||||
},
|
||||
builder: (FormFieldState<String> field) {
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
357
mobile/apps/locker/lib/ui/pages/account_credentials_page.dart
Normal file
@@ -0,0 +1,357 @@
|
||||
import 'package:ente_ui/components/buttons/gradient_button.dart';
|
||||
import 'package:ente_ui/theme/ente_theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:locker/l10n/l10n.dart';
|
||||
import 'package:locker/models/info/info_item.dart';
|
||||
import 'package:locker/services/collections/collections_service.dart';
|
||||
import 'package:locker/services/collections/models/collection.dart';
|
||||
import 'package:locker/services/info_file_service.dart';
|
||||
import 'package:locker/ui/components/collection_selection_widget.dart';
|
||||
import 'package:locker/ui/components/form_text_input_widget.dart';
|
||||
|
||||
class AccountCredentialsPage extends StatefulWidget {
|
||||
const AccountCredentialsPage({super.key});
|
||||
|
||||
@override
|
||||
State<AccountCredentialsPage> createState() => _AccountCredentialsPageState();
|
||||
}
|
||||
|
||||
class _AccountCredentialsPageState extends State<AccountCredentialsPage> {
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
final TextEditingController _usernameController = TextEditingController();
|
||||
final TextEditingController _passwordController = TextEditingController();
|
||||
final TextEditingController _notesController = TextEditingController();
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
bool _isLoading = false;
|
||||
bool _passwordVisible = false;
|
||||
bool _showValidationErrors = false;
|
||||
final _passwordFocusNode = FocusNode();
|
||||
bool _passwordInFocus = false;
|
||||
|
||||
// Collection selection state
|
||||
List<Collection> _availableCollections = [];
|
||||
Set<int> _selectedCollectionIds = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_passwordFocusNode.addListener(() {
|
||||
setState(() {
|
||||
_passwordInFocus = _passwordFocusNode.hasFocus;
|
||||
});
|
||||
});
|
||||
_loadCollections();
|
||||
}
|
||||
|
||||
Future<void> _loadCollections() async {
|
||||
try {
|
||||
final collections = await CollectionService.instance.getCollections();
|
||||
setState(() {
|
||||
_availableCollections = collections;
|
||||
// Pre-select a default collection if available
|
||||
if (collections.isNotEmpty) {
|
||||
final defaultCollection = collections.firstWhere(
|
||||
(c) => c.name == 'Information',
|
||||
orElse: () => collections.first,
|
||||
);
|
||||
_selectedCollectionIds = {defaultCollection.id};
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
// Handle error silently or show a message
|
||||
}
|
||||
}
|
||||
|
||||
void _onToggleCollection(int collectionId) {
|
||||
setState(() {
|
||||
if (_selectedCollectionIds.contains(collectionId)) {
|
||||
_selectedCollectionIds.remove(collectionId);
|
||||
} else {
|
||||
// Allow multiple selections
|
||||
_selectedCollectionIds.add(collectionId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _onCollectionsUpdated(List<Collection> updatedCollections) {
|
||||
setState(() {
|
||||
_availableCollections = updatedCollections;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_usernameController.dispose();
|
||||
_passwordController.dispose();
|
||||
_notesController.dispose();
|
||||
_passwordFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
context.l10n.accountCredentials,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
elevation: 0,
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
),
|
||||
body: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.accountCredentialsDescription,
|
||||
style: getEnteTextTheme(context).body.copyWith(
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
FormTextInputWidget(
|
||||
controller: _nameController,
|
||||
labelText: context.l10n.credentialName,
|
||||
hintText: context.l10n.credentialNameHint,
|
||||
showValidationErrors: _showValidationErrors,
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return context.l10n.pleaseEnterAccountName;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FormTextInputWidget(
|
||||
controller: _usernameController,
|
||||
labelText: context.l10n.username,
|
||||
hintText: context.l10n.usernameHint,
|
||||
showValidationErrors: _showValidationErrors,
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return context.l10n.pleaseEnterUsername;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(context.l10n.password),
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 0, 0, 0),
|
||||
child: TextFormField(
|
||||
controller: _passwordController,
|
||||
focusNode: _passwordFocusNode,
|
||||
obscureText: !_passwordVisible,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
style: getEnteTextTheme(context).body,
|
||||
decoration: InputDecoration(
|
||||
fillColor: getEnteColorScheme(context).fillFaint,
|
||||
filled: true,
|
||||
hintText: context.l10n.passwordHint,
|
||||
hintStyle: getEnteTextTheme(context).body.copyWith(
|
||||
color: getEnteColorScheme(context).textMuted,
|
||||
),
|
||||
contentPadding:
|
||||
const EdgeInsets.fromLTRB(16, 16, 16, 16),
|
||||
border: const UnderlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: getEnteColorScheme(context).strokeFaint,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: getEnteColorScheme(context).primary500,
|
||||
width: 2,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: getEnteColorScheme(context).warning500,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: getEnteColorScheme(context).warning500,
|
||||
width: 2,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
suffixIcon: _passwordInFocus
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
_passwordVisible
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_passwordVisible = !_passwordVisible;
|
||||
});
|
||||
},
|
||||
)
|
||||
: null,
|
||||
suffixIconConstraints: const BoxConstraints(
|
||||
maxHeight: 24,
|
||||
maxWidth: 48,
|
||||
minHeight: 24,
|
||||
minWidth: 48,
|
||||
),
|
||||
),
|
||||
validator: _showValidationErrors
|
||||
? (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return context.l10n.pleaseEnterPassword;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
: null,
|
||||
onChanged: (value) {
|
||||
if (_showValidationErrors) {
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FormTextInputWidget(
|
||||
controller: _notesController,
|
||||
labelText: context.l10n.credentialNotes,
|
||||
hintText: context.l10n.credentialNotesHint,
|
||||
maxLines: 5,
|
||||
showValidationErrors: _showValidationErrors,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
CollectionSelectionWidget(
|
||||
collections: _availableCollections,
|
||||
selectedCollectionIds: _selectedCollectionIds,
|
||||
onToggleCollection: _onToggleCollection,
|
||||
onCollectionsUpdated: _onCollectionsUpdated,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: GradientButton(
|
||||
onTap: _isLoading ? null : _saveRecord,
|
||||
text: context.l10n.saveRecord,
|
||||
paddingValue: 16.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _saveRecord() async {
|
||||
setState(() {
|
||||
_showValidationErrors = true;
|
||||
});
|
||||
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_selectedCollectionIds.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Please select at least one collection'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
// Create InfoItem for account credentials
|
||||
final credentialData = AccountCredentialData(
|
||||
name: _nameController.text.trim(),
|
||||
username: _usernameController.text.trim(),
|
||||
password: _passwordController.text.trim(),
|
||||
notes: _notesController.text.trim().isNotEmpty
|
||||
? _notesController.text.trim()
|
||||
: null,
|
||||
);
|
||||
|
||||
final infoItem = InfoItem(
|
||||
type: InfoType.accountCredential,
|
||||
data: credentialData,
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
|
||||
// Upload to all selected collections
|
||||
final selectedCollections = _availableCollections
|
||||
.where((c) => _selectedCollectionIds.contains(c.id))
|
||||
.toList();
|
||||
|
||||
// Create and upload the info file to each selected collection
|
||||
for (final collection in selectedCollections) {
|
||||
await InfoFileService.instance.createAndUploadInfoFile(
|
||||
infoItem: infoItem,
|
||||
collection: collection,
|
||||
);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop(); // Go back to information page
|
||||
|
||||
// Show success message
|
||||
final collectionCount = selectedCollections.length;
|
||||
final message = collectionCount == 1
|
||||
? context.l10n.recordSavedSuccessfully
|
||||
: 'Record saved to $collectionCount collections successfully';
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('${context.l10n.failedToSaveRecord}: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
258
mobile/apps/locker/lib/ui/pages/emergency_contact_page.dart
Normal file
@@ -0,0 +1,258 @@
|
||||
import 'package:ente_ui/components/buttons/gradient_button.dart';
|
||||
import 'package:ente_ui/theme/ente_theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:locker/l10n/l10n.dart';
|
||||
import 'package:locker/models/info/info_item.dart';
|
||||
import 'package:locker/services/collections/collections_service.dart';
|
||||
import 'package:locker/services/collections/models/collection.dart';
|
||||
import 'package:locker/services/info_file_service.dart';
|
||||
import 'package:locker/ui/components/collection_selection_widget.dart';
|
||||
import 'package:locker/ui/components/form_text_input_widget.dart';
|
||||
|
||||
class EmergencyContactPage extends StatefulWidget {
|
||||
const EmergencyContactPage({super.key});
|
||||
|
||||
@override
|
||||
State<EmergencyContactPage> createState() => _EmergencyContactPageState();
|
||||
}
|
||||
|
||||
class _EmergencyContactPageState extends State<EmergencyContactPage> {
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
final TextEditingController _contactDetailsController =
|
||||
TextEditingController();
|
||||
final TextEditingController _notesController = TextEditingController();
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
bool _isLoading = false;
|
||||
bool _showValidationErrors = false;
|
||||
|
||||
// Collection selection state
|
||||
List<Collection> _availableCollections = [];
|
||||
Set<int> _selectedCollectionIds = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadCollections();
|
||||
}
|
||||
|
||||
Future<void> _loadCollections() async {
|
||||
try {
|
||||
final collections = await CollectionService.instance.getCollections();
|
||||
setState(() {
|
||||
_availableCollections = collections;
|
||||
// Pre-select a default collection if available
|
||||
if (collections.isNotEmpty) {
|
||||
final defaultCollection = collections.firstWhere(
|
||||
(c) => c.name == 'Information',
|
||||
orElse: () => collections.first,
|
||||
);
|
||||
_selectedCollectionIds = {defaultCollection.id};
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
// Handle error silently or show a message
|
||||
}
|
||||
}
|
||||
|
||||
void _onToggleCollection(int collectionId) {
|
||||
setState(() {
|
||||
if (_selectedCollectionIds.contains(collectionId)) {
|
||||
_selectedCollectionIds.remove(collectionId);
|
||||
} else {
|
||||
// Allow multiple selections
|
||||
_selectedCollectionIds.add(collectionId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _onCollectionsUpdated(List<Collection> updatedCollections) {
|
||||
setState(() {
|
||||
_availableCollections = updatedCollections;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_contactDetailsController.dispose();
|
||||
_notesController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
context.l10n.emergencyContact,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
elevation: 0,
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
),
|
||||
body: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.emergencyContactDescription,
|
||||
style: getEnteTextTheme(context).body.copyWith(
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
FormTextInputWidget(
|
||||
controller: _nameController,
|
||||
labelText: context.l10n.contactName,
|
||||
hintText: context.l10n.contactNameHint,
|
||||
showValidationErrors: _showValidationErrors,
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return context.l10n.pleaseEnterContactName;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FormTextInputWidget(
|
||||
controller: _contactDetailsController,
|
||||
labelText: context.l10n.contactDetails,
|
||||
hintText: context.l10n.contactDetailsHint,
|
||||
maxLines: 3,
|
||||
showValidationErrors: _showValidationErrors,
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return context.l10n.pleaseEnterContactDetails;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FormTextInputWidget(
|
||||
controller: _notesController,
|
||||
labelText: context.l10n.contactNotes,
|
||||
hintText: context.l10n.contactNotesHint,
|
||||
maxLines: 4,
|
||||
showValidationErrors: _showValidationErrors,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
CollectionSelectionWidget(
|
||||
collections: _availableCollections,
|
||||
selectedCollectionIds: _selectedCollectionIds,
|
||||
onToggleCollection: _onToggleCollection,
|
||||
onCollectionsUpdated: _onCollectionsUpdated,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: GradientButton(
|
||||
onTap: _isLoading ? null : _saveRecord,
|
||||
text: context.l10n.saveRecord,
|
||||
paddingValue: 16.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _saveRecord() async {
|
||||
setState(() {
|
||||
_showValidationErrors = true;
|
||||
});
|
||||
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_selectedCollectionIds.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Please select at least one collection'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
// Create InfoItem for emergency contact
|
||||
final contactData = EmergencyContactData(
|
||||
name: _nameController.text.trim(),
|
||||
contactDetails: _contactDetailsController.text.trim(),
|
||||
notes: _notesController.text.trim().isNotEmpty
|
||||
? _notesController.text.trim()
|
||||
: null,
|
||||
);
|
||||
|
||||
final infoItem = InfoItem(
|
||||
type: InfoType.emergencyContact,
|
||||
data: contactData,
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
|
||||
// Upload to all selected collections
|
||||
final selectedCollections = _availableCollections
|
||||
.where((c) => _selectedCollectionIds.contains(c.id))
|
||||
.toList();
|
||||
|
||||
// Create and upload the info file to each selected collection
|
||||
for (final collection in selectedCollections) {
|
||||
await InfoFileService.instance.createAndUploadInfoFile(
|
||||
infoItem: infoItem,
|
||||
collection: collection,
|
||||
);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop(); // Go back to information page
|
||||
|
||||
// Show success message
|
||||
final collectionCount = selectedCollections.length;
|
||||
final message = collectionCount == 1
|
||||
? context.l10n.recordSavedSuccessfully
|
||||
: 'Record saved to $collectionCount collections successfully';
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('${context.l10n.failedToSaveRecord}: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import 'package:locker/ui/components/search_result_view.dart';
|
||||
import 'package:locker/ui/mixins/search_mixin.dart';
|
||||
import 'package:locker/ui/pages/all_collections_page.dart';
|
||||
import 'package:locker/ui/pages/collection_page.dart';
|
||||
import 'package:locker/ui/pages/information_page.dart';
|
||||
import "package:locker/ui/pages/settings_page.dart";
|
||||
import 'package:locker/ui/pages/uploader_page.dart';
|
||||
import 'package:locker/utils/collection_actions.dart';
|
||||
@@ -686,34 +687,41 @@ class _HomePageState extends UploaderPageState<HomePage>
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: getEnteColorScheme(context).fillBase,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Text(
|
||||
context.l10n.createCollectionTooltip,
|
||||
style: getEnteTextTheme(context).small.copyWith(
|
||||
color: getEnteColorScheme(context)
|
||||
.backgroundBase,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
_toggleFab();
|
||||
_showInformationDialog();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: getEnteColorScheme(context).fillBase,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Text(
|
||||
context.l10n.saveInformation,
|
||||
style:
|
||||
getEnteTextTheme(context).small.copyWith(
|
||||
color: getEnteColorScheme(context)
|
||||
.backgroundBase,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
FloatingActionButton(
|
||||
heroTag: "createCollection",
|
||||
heroTag: "information",
|
||||
mini: true,
|
||||
onPressed: () {
|
||||
_toggleFab();
|
||||
_createCollection();
|
||||
_showInformationDialog();
|
||||
},
|
||||
backgroundColor:
|
||||
getEnteColorScheme(context).fillBase,
|
||||
child: const Icon(Icons.create_new_folder),
|
||||
child: const Icon(Icons.edit_document),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -727,22 +735,29 @@ class _HomePageState extends UploaderPageState<HomePage>
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: getEnteColorScheme(context).fillBase,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Text(
|
||||
context.l10n.uploadDocumentTooltip,
|
||||
style:
|
||||
getEnteTextTheme(context).small.copyWith(
|
||||
color: getEnteColorScheme(context)
|
||||
.backgroundBase,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
_toggleFab();
|
||||
addFile();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: getEnteColorScheme(context).fillBase,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Text(
|
||||
context.l10n.uploadDocumentTooltip,
|
||||
style: getEnteTextTheme(context)
|
||||
.small
|
||||
.copyWith(
|
||||
color: getEnteColorScheme(context)
|
||||
.backgroundBase,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
@@ -808,4 +823,12 @@ class _HomePageState extends UploaderPageState<HomePage>
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _showInformationDialog() {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const InformationPage(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
157
mobile/apps/locker/lib/ui/pages/information_page.dart
Normal file
@@ -0,0 +1,157 @@
|
||||
import 'package:ente_ui/theme/ente_theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:locker/l10n/l10n.dart';
|
||||
import 'package:locker/ui/pages/account_credentials_page.dart';
|
||||
import 'package:locker/ui/pages/emergency_contact_page.dart';
|
||||
import 'package:locker/ui/pages/personal_note_page.dart';
|
||||
import 'package:locker/ui/pages/physical_records_page.dart';
|
||||
|
||||
enum InformationType {
|
||||
note,
|
||||
physicalRecord,
|
||||
credentials,
|
||||
emergencyContact,
|
||||
}
|
||||
|
||||
class InformationPage extends StatelessWidget {
|
||||
const InformationPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
context.l10n.saveInformation,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
elevation: 0,
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.informationDescription,
|
||||
style: getEnteTextTheme(context).body.copyWith(
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
_buildInformationOption(
|
||||
context,
|
||||
icon: Icons.notes,
|
||||
title: context.l10n.personalNote,
|
||||
description: context.l10n.personalNoteDescription,
|
||||
type: InformationType.note,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildInformationOption(
|
||||
context,
|
||||
icon: Icons.folder_outlined,
|
||||
title: context.l10n.physicalRecords,
|
||||
description: context.l10n.physicalRecordsDescription,
|
||||
type: InformationType.physicalRecord,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildInformationOption(
|
||||
context,
|
||||
icon: Icons.lock,
|
||||
title: context.l10n.accountCredentials,
|
||||
description: context.l10n.accountCredentialsDescription,
|
||||
type: InformationType.credentials,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildInformationOption(
|
||||
context,
|
||||
icon: Icons.contact_phone,
|
||||
title: context.l10n.emergencyContact,
|
||||
description: context.l10n.emergencyContactDescription,
|
||||
type: InformationType.emergencyContact,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInformationOption(
|
||||
BuildContext context, {
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required String description,
|
||||
required InformationType type,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
_showInformationForm(context, type);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: getEnteColorScheme(context).fillFaint,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 24,
|
||||
color: getEnteColorScheme(context).primary500,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: getEnteTextTheme(context).body.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
description,
|
||||
style: getEnteTextTheme(context).small.copyWith(
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showInformationForm(BuildContext context, InformationType type) {
|
||||
Widget page;
|
||||
switch (type) {
|
||||
case InformationType.note:
|
||||
page = const PersonalNotePage();
|
||||
break;
|
||||
case InformationType.physicalRecord:
|
||||
page = const PhysicalRecordsPage();
|
||||
break;
|
||||
case InformationType.credentials:
|
||||
page = const AccountCredentialsPage();
|
||||
break;
|
||||
case InformationType.emergencyContact:
|
||||
page = const EmergencyContactPage();
|
||||
break;
|
||||
}
|
||||
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => page),
|
||||
);
|
||||
}
|
||||
}
|
||||
245
mobile/apps/locker/lib/ui/pages/personal_note_page.dart
Normal file
@@ -0,0 +1,245 @@
|
||||
import 'package:ente_ui/components/buttons/gradient_button.dart';
|
||||
import 'package:ente_ui/theme/ente_theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:locker/l10n/l10n.dart';
|
||||
import 'package:locker/models/info/info_item.dart';
|
||||
import 'package:locker/services/collections/collections_service.dart';
|
||||
import 'package:locker/services/collections/models/collection.dart';
|
||||
import 'package:locker/services/info_file_service.dart';
|
||||
import 'package:locker/ui/components/collection_selection_widget.dart';
|
||||
import 'package:locker/ui/components/form_text_input_widget.dart';
|
||||
|
||||
class PersonalNotePage extends StatefulWidget {
|
||||
const PersonalNotePage({super.key});
|
||||
|
||||
@override
|
||||
State<PersonalNotePage> createState() => _PersonalNotePageState();
|
||||
}
|
||||
|
||||
class _PersonalNotePageState extends State<PersonalNotePage> {
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
final TextEditingController _contentController = TextEditingController();
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
bool _isLoading = false;
|
||||
bool _showValidationErrors = false;
|
||||
|
||||
// Collection selection state
|
||||
List<Collection> _availableCollections = [];
|
||||
Set<int> _selectedCollectionIds = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadCollections();
|
||||
}
|
||||
|
||||
Future<void> _loadCollections() async {
|
||||
try {
|
||||
final collections = await CollectionService.instance.getCollections();
|
||||
setState(() {
|
||||
_availableCollections = collections;
|
||||
// Pre-select a default collection if available
|
||||
if (collections.isNotEmpty) {
|
||||
final defaultCollection = collections.firstWhere(
|
||||
(c) => c.name == 'Information',
|
||||
orElse: () => collections.first,
|
||||
);
|
||||
_selectedCollectionIds = {defaultCollection.id};
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
// Handle error silently or show a message
|
||||
}
|
||||
}
|
||||
|
||||
void _onToggleCollection(int collectionId) {
|
||||
setState(() {
|
||||
if (_selectedCollectionIds.contains(collectionId)) {
|
||||
_selectedCollectionIds.remove(collectionId);
|
||||
} else {
|
||||
// Allow multiple selections
|
||||
_selectedCollectionIds.add(collectionId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _onCollectionsUpdated(List<Collection> updatedCollections) {
|
||||
setState(() {
|
||||
_availableCollections = updatedCollections;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_contentController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
context.l10n.personalNote,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
elevation: 0,
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
),
|
||||
body: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.personalNoteDescription,
|
||||
style: getEnteTextTheme(context).body.copyWith(
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
FormTextInputWidget(
|
||||
controller: _nameController,
|
||||
labelText: context.l10n.noteName,
|
||||
hintText: context.l10n.noteNameHint,
|
||||
showValidationErrors: _showValidationErrors,
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return context.l10n.pleaseEnterNoteName;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FormTextInputWidget(
|
||||
controller: _contentController,
|
||||
labelText: context.l10n.noteContent,
|
||||
hintText: context.l10n.noteContentHint,
|
||||
maxLines: 6,
|
||||
showValidationErrors: _showValidationErrors,
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return context.l10n.pleaseEnterNoteContent;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
CollectionSelectionWidget(
|
||||
collections: _availableCollections,
|
||||
selectedCollectionIds: _selectedCollectionIds,
|
||||
onToggleCollection: _onToggleCollection,
|
||||
onCollectionsUpdated: _onCollectionsUpdated,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: GradientButton(
|
||||
onTap: _isLoading ? null : _saveRecord,
|
||||
text: context.l10n.saveRecord,
|
||||
paddingValue: 16.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _saveRecord() async {
|
||||
setState(() {
|
||||
_showValidationErrors = true;
|
||||
});
|
||||
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_selectedCollectionIds.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Please select at least one collection'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
// Create InfoItem for personal note
|
||||
final noteData = PersonalNoteData(
|
||||
title: _nameController.text.trim(),
|
||||
content: _contentController.text.trim(),
|
||||
);
|
||||
|
||||
final infoItem = InfoItem(
|
||||
type: InfoType.note,
|
||||
data: noteData,
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
|
||||
// Upload to all selected collections
|
||||
final selectedCollections = _availableCollections
|
||||
.where((c) => _selectedCollectionIds.contains(c.id))
|
||||
.toList();
|
||||
|
||||
// Create and upload the info file to each selected collection
|
||||
for (final collection in selectedCollections) {
|
||||
await InfoFileService.instance.createAndUploadInfoFile(
|
||||
infoItem: infoItem,
|
||||
collection: collection,
|
||||
);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
Navigator.of(context)
|
||||
.pop(); // Close this page and return to information page
|
||||
|
||||
// Show success message
|
||||
final collectionCount = selectedCollections.length;
|
||||
final message = collectionCount == 1
|
||||
? context.l10n.recordSavedSuccessfully
|
||||
: 'Record saved to $collectionCount collections successfully';
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('${context.l10n.failedToSaveRecord}: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
257
mobile/apps/locker/lib/ui/pages/physical_records_page.dart
Normal file
@@ -0,0 +1,257 @@
|
||||
import 'package:ente_ui/components/buttons/gradient_button.dart';
|
||||
import 'package:ente_ui/theme/ente_theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:locker/l10n/l10n.dart';
|
||||
import 'package:locker/models/info/info_item.dart';
|
||||
import 'package:locker/services/collections/collections_service.dart';
|
||||
import 'package:locker/services/collections/models/collection.dart';
|
||||
import 'package:locker/services/info_file_service.dart';
|
||||
import 'package:locker/ui/components/collection_selection_widget.dart';
|
||||
import 'package:locker/ui/components/form_text_input_widget.dart';
|
||||
|
||||
class PhysicalRecordsPage extends StatefulWidget {
|
||||
const PhysicalRecordsPage({super.key});
|
||||
|
||||
@override
|
||||
State<PhysicalRecordsPage> createState() => _PhysicalRecordsPageState();
|
||||
}
|
||||
|
||||
class _PhysicalRecordsPageState extends State<PhysicalRecordsPage> {
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
final TextEditingController _locationController = TextEditingController();
|
||||
final TextEditingController _notesController = TextEditingController();
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
bool _isLoading = false;
|
||||
bool _showValidationErrors = false;
|
||||
|
||||
// Collection selection state
|
||||
List<Collection> _availableCollections = [];
|
||||
Set<int> _selectedCollectionIds = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadCollections();
|
||||
}
|
||||
|
||||
Future<void> _loadCollections() async {
|
||||
try {
|
||||
final collections = await CollectionService.instance.getCollections();
|
||||
setState(() {
|
||||
_availableCollections = collections;
|
||||
// Pre-select a default collection if available
|
||||
if (collections.isNotEmpty) {
|
||||
final defaultCollection = collections.firstWhere(
|
||||
(c) => c.name == 'Information',
|
||||
orElse: () => collections.first,
|
||||
);
|
||||
_selectedCollectionIds = {defaultCollection.id};
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
// Handle error silently or show a message
|
||||
}
|
||||
}
|
||||
|
||||
void _onToggleCollection(int collectionId) {
|
||||
setState(() {
|
||||
if (_selectedCollectionIds.contains(collectionId)) {
|
||||
_selectedCollectionIds.remove(collectionId);
|
||||
} else {
|
||||
// Allow multiple selections
|
||||
_selectedCollectionIds.add(collectionId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _onCollectionsUpdated(List<Collection> updatedCollections) {
|
||||
setState(() {
|
||||
_availableCollections = updatedCollections;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_locationController.dispose();
|
||||
_notesController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
context.l10n.physicalRecords,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
elevation: 0,
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
),
|
||||
body: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.physicalRecordsDescription,
|
||||
style: getEnteTextTheme(context).body.copyWith(
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
FormTextInputWidget(
|
||||
controller: _nameController,
|
||||
labelText: context.l10n.recordName,
|
||||
hintText: context.l10n.recordNameHint,
|
||||
showValidationErrors: _showValidationErrors,
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return context.l10n.pleaseEnterRecordName;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FormTextInputWidget(
|
||||
controller: _locationController,
|
||||
labelText: context.l10n.recordLocation,
|
||||
hintText: context.l10n.recordLocationHint,
|
||||
maxLines: 3,
|
||||
showValidationErrors: _showValidationErrors,
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return context.l10n.pleaseEnterLocation;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FormTextInputWidget(
|
||||
controller: _notesController,
|
||||
labelText: context.l10n.recordNotes,
|
||||
hintText: context.l10n.recordNotesHint,
|
||||
maxLines: 5,
|
||||
showValidationErrors: _showValidationErrors,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
CollectionSelectionWidget(
|
||||
collections: _availableCollections,
|
||||
selectedCollectionIds: _selectedCollectionIds,
|
||||
onToggleCollection: _onToggleCollection,
|
||||
onCollectionsUpdated: _onCollectionsUpdated,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: GradientButton(
|
||||
onTap: _isLoading ? null : _saveRecord,
|
||||
text: context.l10n.saveRecord,
|
||||
paddingValue: 16.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _saveRecord() async {
|
||||
setState(() {
|
||||
_showValidationErrors = true;
|
||||
});
|
||||
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_selectedCollectionIds.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Please select at least one collection'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
// Create InfoItem for physical record
|
||||
final recordData = PhysicalRecordData(
|
||||
name: _nameController.text.trim(),
|
||||
location: _locationController.text.trim(),
|
||||
notes: _notesController.text.trim().isNotEmpty
|
||||
? _notesController.text.trim()
|
||||
: null,
|
||||
);
|
||||
|
||||
final infoItem = InfoItem(
|
||||
type: InfoType.physicalRecord,
|
||||
data: recordData,
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
|
||||
// Upload to all selected collections
|
||||
final selectedCollections = _availableCollections
|
||||
.where((c) => _selectedCollectionIds.contains(c.id))
|
||||
.toList();
|
||||
|
||||
// Create and upload the info file to each selected collection
|
||||
for (final collection in selectedCollections) {
|
||||
await InfoFileService.instance.createAndUploadInfoFile(
|
||||
infoItem: infoItem,
|
||||
collection: collection,
|
||||
);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop(); // Go back to information page
|
||||
|
||||
// Show success message
|
||||
final collectionCount = selectedCollections.length;
|
||||
final message = collectionCount == 1
|
||||
? context.l10n.recordSavedSuccessfully
|
||||
: 'Record saved to $collectionCount collections successfully';
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('${context.l10n.failedToSaveRecord}: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="io.ente.photos">
|
||||
<application
|
||||
tools:replace="android:label"
|
||||
android:name="${applicationName}"
|
||||
<application android:name="${applicationName}"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/icon_green"
|
||||
android:usesCleartextTraffic="true"
|
||||
|
||||
@@ -1,49 +1,36 @@
|
||||
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.
|
||||
Ente is a simple app to backup and share your photos and videos.
|
||||
|
||||
Why Ente Photos?
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
Features:
|
||||
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.
|
||||
|
||||
END-TO-END ENCRYPTED STORAGE: Your photos and videos are encrypted on your device, and then automatically backed up to the cloud.
|
||||
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.
|
||||
|
||||
SHARE AND COLLABORATE: Let your family or friends add photos and videos to your albums. Everything, end-to-end encrypted.
|
||||
We are here to make the safest photos app ever, come join our journey!
|
||||
|
||||
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.
|
||||
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!
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 584 KiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 522 KiB After Width: | Height: | Size: 690 KiB |
|
Before Width: | Height: | Size: 662 KiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 195 KiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 521 KiB After Width: | Height: | Size: 853 KiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 76 KiB |
@@ -1 +1 @@
|
||||
Backup, Organise, Share - Private photo storage with end-to-end encryption
|
||||
Ente Photos is an open source photos app, that provides end-to-end encrypted backups for your photos and videos.
|
||||
|
||||
@@ -1 +1 @@
|
||||
Ente Photos - Encrypted photo storage
|
||||
Ente Photos - Open source, end-to-end encrypted alternative to Google Photos
|
||||
@@ -8,10 +8,10 @@ allprojects {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
mavenLocal() // for FDroid
|
||||
// maven {
|
||||
// url "${project(':ffmpeg_kit_flutter').projectDir}/libs"
|
||||
// }
|
||||
// mavenLocal() // for FDroid
|
||||
maven {
|
||||
url "${project(':ffmpeg_kit_flutter').projectDir}/libs"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,49 +1,36 @@
|
||||
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.
|
||||
ente is a simple app to backup and share your photos and videos.
|
||||
|
||||
Why Ente Photos?
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
Features:
|
||||
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.
|
||||
|
||||
END-TO-END ENCRYPTED STORAGE: Your photos and videos are encrypted on your device, and then automatically backed up to the cloud.
|
||||
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.
|
||||
|
||||
SHARE AND COLLABORATE: Let your family or friends add photos and videos to your albums. Everything, end-to-end encrypted.
|
||||
We are here to make the safest photos app ever, come join our journey!
|
||||
|
||||
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.
|
||||
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!
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -1 +1 @@
|
||||
Backup, Organise, Share - Private photo storage with end-to-end encryption
|
||||
ente is an end-to-end encrypted photo storage app
|
||||
@@ -1 +1 @@
|
||||
Ente Photos - Encrypted photo storage
|
||||
ente - encrypted photo storage
|
||||
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 584 KiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 522 KiB After Width: | Height: | Size: 690 KiB |
|
Before Width: | Height: | Size: 662 KiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 195 KiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 521 KiB After Width: | Height: | Size: 853 KiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 76 KiB |
@@ -1,10 +1,10 @@
|
||||
import 'dart:async';
|
||||
import "dart:core";
|
||||
import 'dart:io';
|
||||
|
||||
import "package:adaptive_theme/adaptive_theme.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";
|
||||
@@ -36,6 +36,7 @@ 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/search_service.dart';
|
||||
import 'package:photos/services/sync/local_sync_service.dart';
|
||||
import 'package:photos/services/sync/remote_sync_service.dart';
|
||||
@@ -272,12 +273,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,
|
||||
);
|
||||
}).ignore();
|
||||
}
|
||||
_logger.info("PushService/HomeWidget done $tlog");
|
||||
unawaited(SemanticSearchService.instance.init());
|
||||
@@ -402,6 +402,31 @@ Future<bool> _isRunningInForeground() async {
|
||||
(currentTime - kFGTaskDeathTimeoutInMicroseconds);
|
||||
}
|
||||
|
||||
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||
final bool isRunningInFG = await _isRunningInForeground(); // hb
|
||||
final bool isInForeground = AppLifecycleService.instance.isForeground;
|
||||
if (await _isRunningInForeground()) {
|
||||
_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
|
||||
runWithLogs(
|
||||
() async {
|
||||
_logger.info("Background push received");
|
||||
await _init(true, via: 'firebasePush');
|
||||
if (PushService.shouldSync(message)) {
|
||||
await _sync('firebaseBgSyncNoActiveProcess');
|
||||
}
|
||||
},
|
||||
prefix: "[fbg]",
|
||||
).ignore();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _logFGHeartBeatInfo(SharedPreferences prefs) async {
|
||||
final bool isRunningInFG = await _isRunningInForeground();
|
||||
await prefs.reload();
|
||||
|
||||
@@ -4,6 +4,7 @@ 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";
|
||||
@@ -28,7 +29,6 @@ class BillingService {
|
||||
late final _logger = Logger("BillingService");
|
||||
final Dio _enteDio;
|
||||
|
||||
// ignore: unused_field
|
||||
bool _isOnSubscriptionPage = false;
|
||||
|
||||
Future<BillingPlans>? _future;
|
||||
@@ -42,6 +42,23 @@ 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,6 +1,24 @@
|
||||
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}) {
|
||||
return StripeSubscriptionPage(isOnboarding: isOnBoarding);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -9,14 +9,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "85.0.0"
|
||||
_flutterfire_internals:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _flutterfire_internals
|
||||
sha256: a5788040810bd84400bc209913fbc40f388cded7cdf95ee2f5d2bff7e38d5241
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.58"
|
||||
adaptive_theme:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: adaptive_theme
|
||||
sha256: "41b8af1bb5f3fb87db1aed8c8a2dc3eab79116d20793b5c9b6d2e8809402de9a"
|
||||
sha256: caa49b4c73b681bf12a641dff77aa1383262a00cf38b9d1a25b180e275ba5ab9
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.7.1+2"
|
||||
version: "3.7.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -29,10 +37,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: android_intent_plus
|
||||
sha256: "2329378af63f49b985cb2e110ac784d08374f1e2b1984be77ba9325b1c8cce11"
|
||||
sha256: dfc1fd3a577205ae8f11e990fb4ece8c90cceabbee56fcf48e463ecf0bd6aae3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.3.1"
|
||||
version: "5.3.0"
|
||||
animated_list_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -61,10 +69,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: app_links
|
||||
sha256: "5f88447519add627fe1cbcab4fd1da3d4fed15b9baf29f28b22535c95ecee3e8"
|
||||
sha256: "85ed8fc1d25a76475914fff28cc994653bd900bc2c26e4b57a49e097febb54ba"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.4.1"
|
||||
version: "6.4.0"
|
||||
app_links_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -158,10 +166,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build
|
||||
sha256: ce76b1d48875e3233fde17717c23d1f60a91cc631597e49a400c89b475395b1d
|
||||
sha256: "7d95cbbb1526ab5ae977df9b4cc660963b9b27f6d1075c0b34653868911385e4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "3.0.0"
|
||||
build_cli_annotations:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -174,10 +182,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_config
|
||||
sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187"
|
||||
sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.1.2"
|
||||
build_daemon:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -190,26 +198,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_resolvers
|
||||
sha256: d1d57f7807debd7349b4726a19fd32ec8bc177c71ad0febf91a20f84cd2d4b46
|
||||
sha256: "38c9c339333a09b090a638849a4c56e70a404c6bdd3b511493addfbc113b60c2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
version: "3.0.0"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: b24597fceb695969d47025c958f3837f9f0122e237c6a22cb082a5ac66c3ca30
|
||||
sha256: b971d4a1c789eba7be3e6fe6ce5e5b50fd3719e3cb485b3fad6d04358304351d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.1"
|
||||
version: "2.6.0"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_runner_core
|
||||
sha256: "066dda7f73d8eb48ba630a55acb50c4a84a2e6b453b1cb4567f581729e794f7b"
|
||||
sha256: c04e612ca801cd0928ccdb891c263a2b1391cb27940a5ea5afcf9ba894de5d62
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.3.1"
|
||||
version: "9.2.0"
|
||||
built_collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -222,10 +230,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_value
|
||||
sha256: a30f0a0e38671e89a492c44d005b5545b830a961575bbd8336d42869ff71066d
|
||||
sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.12.0"
|
||||
version: "8.10.1"
|
||||
cached_network_image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -271,10 +279,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: checked_yaml
|
||||
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
|
||||
sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.4"
|
||||
version: "2.0.3"
|
||||
chewie:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -337,10 +345,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: connectivity_plus
|
||||
sha256: b5e72753cf63becce2c61fd04dfe0f1c430cc5278b53a1342dc5ad839eab29ec
|
||||
sha256: "051849e2bd7c7b3bc5844ea0d096609ddc3a859890ec3a9ac4a65a2620cc1f99"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.5"
|
||||
version: "6.1.4"
|
||||
connectivity_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -361,18 +369,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: coverage
|
||||
sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d"
|
||||
sha256: aa07dbe5f2294c827b7edb9a87bba44a9c15a3cc81bc8da2ca19b37322d30080
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
version: "1.14.1"
|
||||
cronet_http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cronet_http
|
||||
sha256: "1b99ad5ae81aa9d2f12900e5f17d3681f3828629bb7f7fe7ad88076a34209840"
|
||||
sha256: "5ed075c59b2d4bd43af4e73d906b8082e98ecd2af9c625327370ef28361bf635"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
version: "1.3.4"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -401,10 +409,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cupertino_http
|
||||
sha256: "72187f715837290a63479a5b0ae709f4fedad0ed6bd0441c275eceaa02d5abae"
|
||||
sha256: "8fb9e2c36d0732d9d96abd76683406b57e78a2514e27c962e0c603dbe6f2e3f8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
version: "2.2.0"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -425,10 +433,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb"
|
||||
sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
version: "3.1.0"
|
||||
dart_ui_isolate:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -473,10 +481,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9
|
||||
sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.9.0"
|
||||
version: "5.8.0+1"
|
||||
dio_web_adapter:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -663,7 +671,7 @@ packages:
|
||||
description:
|
||||
path: "flutter/flutter"
|
||||
ref: remove-event-sub
|
||||
resolved-ref: b7aac7903f70dce6d71506e221066af1a53ec7fc
|
||||
resolved-ref: "2c0f34797df830ef61e6ed479583b368c342cb37"
|
||||
url: "https://github.com/ente-io/ffmpeg-kit"
|
||||
source: git
|
||||
version: "6.0.3"
|
||||
@@ -672,7 +680,7 @@ packages:
|
||||
description:
|
||||
path: "flutter/flutter_platform_interface"
|
||||
ref: remove-event-sub
|
||||
resolved-ref: b7aac7903f70dce6d71506e221066af1a53ec7fc
|
||||
resolved-ref: "2c0f34797df830ef61e6ed479583b368c342cb37"
|
||||
url: "https://github.com/ente-io/ffmpeg-kit"
|
||||
source: git
|
||||
version: "0.2.1"
|
||||
@@ -700,38 +708,54 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.14"
|
||||
file_selector_linux:
|
||||
dependency: transitive
|
||||
firebase_core:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_selector_linux
|
||||
sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33"
|
||||
name: firebase_core
|
||||
sha256: c6e8a6bf883d8ddd0dec39be90872daca65beaa6f4cff0051ed3b16c56b82e9f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.3+2"
|
||||
file_selector_macos:
|
||||
version: "3.15.1"
|
||||
firebase_core_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_macos
|
||||
sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c"
|
||||
name: firebase_core_platform_interface
|
||||
sha256: "5dbc900677dcbe5873d22ad7fbd64b047750124f1f9b7ebe2a33b9ddccc838eb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.4+4"
|
||||
file_selector_platform_interface:
|
||||
version: "6.0.0"
|
||||
firebase_core_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_platform_interface
|
||||
sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b
|
||||
name: firebase_core_web
|
||||
sha256: "0ed0dc292e8f9ac50992e2394e9d336a0275b6ae400d64163fdf0a8a8b556c37"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.2"
|
||||
file_selector_windows:
|
||||
version: "2.24.1"
|
||||
firebase_messaging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_messaging
|
||||
sha256: "0f3363f97672eb9f65609fa00ed2f62cc8ec93e7e2d4def99726f9165d3d8a73"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.2.9"
|
||||
firebase_messaging_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_windows
|
||||
sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b"
|
||||
name: firebase_messaging_platform_interface
|
||||
sha256: "7a05ef119a14c5f6a9440d1e0223bcba20c8daf555450e119c4c477bf2c3baa9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.3+4"
|
||||
version: "4.6.9"
|
||||
firebase_messaging_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_web
|
||||
sha256: a4547f76da2a905190f899eb4d0150e1d0fd52206fce469d9f05ae15bb68b2c5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.10.9"
|
||||
fixnum:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -930,10 +954,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_local_notifications
|
||||
sha256: a9966c850de5e445331b854fa42df96a8020066d67f125a5964cbc6556643f68
|
||||
sha256: edae0c34573233ab03f5ba1f07465e55c384743893042cb19e010b4ee8541c12
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "19.4.1"
|
||||
version: "19.3.0"
|
||||
flutter_local_notifications_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -954,10 +978,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_windows
|
||||
sha256: ed46d7ae4ec9d19e4c8fa2badac5fe27ba87a3fe387343ce726f927af074ec98
|
||||
sha256: f8fc0652a601f83419d623c85723a3e82ad81f92b33eaa9bcc21ea1b94773e6e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
version: "1.0.0"
|
||||
flutter_localizations:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@@ -1007,10 +1031,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
sha256: b0694b7fb1689b0e6cc193b3f1fcac6423c4f93c74fb20b806c6b6f196db0c31
|
||||
sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.30"
|
||||
version: "2.0.28"
|
||||
flutter_rust_bridge:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1088,10 +1112,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_spinkit
|
||||
sha256: "77850df57c00dc218bfe96071d576a8babec24cf58b2ed121c83cca4a2fdce7f"
|
||||
sha256: d2696eed13732831414595b98863260e33e8882fc069ee80ec35d4ac9ddb0472
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.2.2"
|
||||
version: "5.2.1"
|
||||
flutter_staggered_grid_view:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1104,10 +1128,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_svg
|
||||
sha256: b9c2ad5872518a27507ab432d1fb97e8813b05f0fc693f9d40fad06d073e0678
|
||||
sha256: cd57f7969b4679317c17af6fd16ee233c1e60a82ed209d8a475c54fd6fd6f845
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
version: "2.2.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@@ -1231,10 +1255,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
|
||||
sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
version: "1.4.0"
|
||||
http_client_helper:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1275,102 +1299,38 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.4"
|
||||
image_editor:
|
||||
in_app_purchase:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image_editor
|
||||
sha256: "38070067264fd9fea4328ca630d2ff7bd65ebe6aa4ed375d983b732d2ae7146b"
|
||||
name: in_app_purchase
|
||||
sha256: "5cddd7f463f3bddb1d37a72b95066e840d5822d66291331d7f8f05ce32c24b6c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
image_editor_common:
|
||||
version: "3.2.3"
|
||||
in_app_purchase_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_editor_common
|
||||
sha256: "93d2f5c8b636f862775dd62a9ec20d09c8272598daa02f935955a4640e1844ee"
|
||||
name: in_app_purchase_android
|
||||
sha256: fd76e5612da6facadcfe8a3477da092908227260a9f6ec7db9a66dd989c69b02
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
image_editor_ohos:
|
||||
version: "0.4.0+2"
|
||||
in_app_purchase_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_editor_ohos
|
||||
sha256: "06756859586d5acefec6e3b4f356f9b1ce05ef09213bcb9a0ce1680ecea2d054"
|
||||
name: in_app_purchase_platform_interface
|
||||
sha256: "1d353d38251da5b9fea6635c0ebfc6bb17a2d28d0e86ea5e083bf64244f1fb4c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.9"
|
||||
image_editor_platform_interface:
|
||||
version: "1.4.0"
|
||||
in_app_purchase_storekit:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_editor_platform_interface
|
||||
sha256: "474517efc770464f7d99942472d8cfb369a3c378e95466ec17f74d2b80bd40de"
|
||||
name: in_app_purchase_storekit
|
||||
sha256: ceddd5a70d268f762d29993ed470054bc2baf8793e41800bc82cde05110260d0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
image_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image_picker
|
||||
sha256: "736eb56a911cf24d1859315ad09ddec0b66104bc41a7f8c5b96b4e2620cf5041"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
image_picker_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_android
|
||||
sha256: "28f3987ca0ec702d346eae1d90eda59603a2101b52f1e234ded62cff1d5cfa6e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.13+1"
|
||||
image_picker_for_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_for_web
|
||||
sha256: "40c2a6a0da15556dc0f8e38a3246064a971a9f512386c3339b89f76db87269b6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
image_picker_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_ios
|
||||
sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.13"
|
||||
image_picker_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_linux
|
||||
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.2"
|
||||
image_picker_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_macos
|
||||
sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.2"
|
||||
image_picker_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_platform_interface
|
||||
sha256: "9f143b0dba3e459553209e20cc425c9801af48e6dfa4f01a0fcf927be3f41665"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.11.0"
|
||||
image_picker_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_windows
|
||||
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.2"
|
||||
version: "0.4.2"
|
||||
integration_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@@ -1388,10 +1348,10 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: intl_utils
|
||||
sha256: da67ac187b521445d745f7c68e7254c2090f6c3c9081c93cd515480af9e59569
|
||||
sha256: "3b2655259dbebd26e3b1466d0839f389dc59a275337e1b760ac645bc7814d2d3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.8.11"
|
||||
version: "2.8.10"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1412,10 +1372,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni
|
||||
sha256: d2c361082d554d4593c3012e26f6b188f902acd291330f13d6427641a92b3da1
|
||||
sha256: "459727a9daf91bdfb39b014cf3c186cf77f0136124a274ac83c186e12262ac4e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.14.2"
|
||||
version: "0.12.2"
|
||||
js:
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
@@ -1436,10 +1396,10 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: json_serializable
|
||||
sha256: "33a040668b31b320aafa4822b7b1e177e163fc3c1e835c6750319d4ab23aa6fe"
|
||||
sha256: ce2cf974ccdee13be2a510832d7fba0b94b364e0b0395dee42abaa51b855be27
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.11.1"
|
||||
version: "6.10.0"
|
||||
latlong2:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1516,18 +1476,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: local_auth_android
|
||||
sha256: "48924f4a8b3cc45994ad5993e2e232d3b00788a305c1bf1c7db32cef281ce9a3"
|
||||
sha256: "63ad7ca6396290626dc0cb34725a939e4cfe965d80d36112f08d49cf13a8136e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.52"
|
||||
version: "1.0.49"
|
||||
local_auth_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: local_auth_darwin
|
||||
sha256: "0e9706a8543a4a2eee60346294d6a633dd7c3ee60fae6b752570457c4ff32055"
|
||||
sha256: "25163ce60a5a6c468cf7a0e3dc8a165f824cabc2aa9e39a5e9fc5c2311b7686f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
version: "1.5.0"
|
||||
local_auth_ios:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1556,10 +1516,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logger
|
||||
sha256: "55d6c23a6c15db14920e037fe7e0dc32e7cdaf3b64b4b25df2d541b5b6b81c0c"
|
||||
sha256: "2621da01aabaf223f8f961e751f2c943dbb374dc3559b982f200ccedadaa6999"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.1"
|
||||
version: "2.6.0"
|
||||
logging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1789,10 +1749,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: native_dio_adapter
|
||||
sha256: "1c51bd42027861d27ccad462ba0903f5e3197461cc6d59a0bb8658cb5ad7bd01"
|
||||
sha256: "7420bc9517b2abe09810199a19924617b45690a44ecfb0616ac9babc11875c03"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
version: "1.4.0"
|
||||
native_video_player:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1878,18 +1838,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968"
|
||||
sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.3.1"
|
||||
version: "8.3.0"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_platform_interface
|
||||
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
|
||||
sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
version: "3.2.0"
|
||||
panorama:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1943,18 +1903,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "993381400e94d18469750e5b9dcb8206f15bc09f9da86b9e44a9b0092a0066db"
|
||||
sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.18"
|
||||
version: "2.2.17"
|
||||
path_provider_foundation:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd"
|
||||
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
version: "2.4.1"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2031,10 +1991,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
|
||||
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
version: "6.1.0"
|
||||
photo_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -2063,10 +2023,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: pinput
|
||||
sha256: c41f42ee301505ae2375ec32871c985d3717bf8aee845620465b286e0140aad2
|
||||
sha256: "8a73be426a91fefec90a7f130763ca39772d547e92f19a827cf4aa02e323d35a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.2"
|
||||
version: "5.0.1"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2160,10 +2120,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: provider
|
||||
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
|
||||
sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.5+1"
|
||||
version: "6.1.5"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2248,10 +2208,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: screen_brightness_android
|
||||
sha256: d34f5321abd03bc3474f4c381f53d189117eba0b039eac1916aa92cca5fd0a96
|
||||
sha256: fb5fa43cb89d0c9b8534556c427db1e97e46594ac5d66ebdcf16063b773d54ed
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
version: "2.1.2"
|
||||
screen_brightness_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2296,18 +2256,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: share_plus
|
||||
sha256: d7dc0630a923883c6328ca31b89aa682bacbf2f8304162d29f7c6aaff03a27a1
|
||||
sha256: b2961506569e28948d75ec346c28775bb111986bb69dc6a20754a457e3d97fa0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.1.0"
|
||||
version: "11.0.0"
|
||||
share_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: share_plus_platform_interface
|
||||
sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a"
|
||||
sha256: "1032d392bc5d2095a77447a805aa3f804d2ae6a4d5eef5e6ebb3bd94c1bc19ef"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
version: "6.0.0"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -2320,10 +2280,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: a2608114b1ffdcbc9c120eb71a0e207c71da56202852d4aab8a5e30a82269e74
|
||||
sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.12"
|
||||
version: "2.4.10"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2413,10 +2373,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_helper
|
||||
sha256: "6a3c6cc82073a8797f8c4dc4572146114a39652851c157db37e964d9c7038723"
|
||||
sha256: a447acb083d3a5ef17f983dd36201aeea33fedadb3228fa831f2f0c92f0f3aca
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.8"
|
||||
version: "1.3.7"
|
||||
source_map_stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2469,10 +2429,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_common
|
||||
sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6"
|
||||
sha256: "84731e8bfd8303a3389903e01fb2141b6e59b5973cacbb0929021df08dddbe8b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.6"
|
||||
version: "2.5.5"
|
||||
sqflite_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2501,34 +2461,34 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlite3
|
||||
sha256: f393d92c71bdcc118d6203d07c991b9be0f84b1a6f89dd4f7eed348131329924
|
||||
sha256: c0503c69b44d5714e6abbf4c1f51a3c3cc42b75ce785f44404765e4635481d38
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.9.0"
|
||||
version: "2.7.6"
|
||||
sqlite3_flutter_libs:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqlite3_flutter_libs
|
||||
sha256: "2b03273e71867a8a4d030861fc21706200debe5c5858a4b9e58f4a1c129586a4"
|
||||
sha256: e07232b998755fe795655c56d1f5426e0190c9c435e1752d39e7b1cd33699c71
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.39"
|
||||
version: "0.5.34"
|
||||
sqlite3_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlite3_web
|
||||
sha256: "0f6ebcb4992d1892ac5c8b5ecd22a458ab9c5eb6428b11ae5ecb5d63545844da"
|
||||
sha256: "967e076442f7e1233bd7241ca61f3efe4c7fc168dac0f38411bdb3bdf471eb3c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.2"
|
||||
version: "0.3.1"
|
||||
sqlite_async:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqlite_async
|
||||
sha256: "4da3b035bcce83d10beae879c6539e75230826bba95ed440565b6afbdd8b1550"
|
||||
sha256: "9332aedd311a19dd215dcb55729bc68dc587dc7655b569ab8819b68ee0be0082"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.11.8"
|
||||
version: "0.11.7"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2605,10 +2565,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: synchronized
|
||||
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
|
||||
sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.0"
|
||||
version: "3.3.1"
|
||||
system_info_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -2741,26 +2701,26 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
|
||||
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.2"
|
||||
version: "6.3.1"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "69ee86740f2847b9a4ba6cffa74ed12ce500bbe2b07f3dc1e643439da60637b7"
|
||||
sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.18"
|
||||
version: "6.3.16"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7
|
||||
sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.4"
|
||||
version: "6.3.3"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2773,10 +2733,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f
|
||||
sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.3"
|
||||
version: "3.2.2"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2829,10 +2789,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics_compiler
|
||||
sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc
|
||||
sha256: "557a315b7d2a6dbb0aaaff84d857967ce6bdc96a63dc6ee2a57ce5a6ee5d3331"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.19"
|
||||
version: "1.1.17"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2879,18 +2839,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_android
|
||||
sha256: "59e5a457ddcc1688f39e9aef0efb62aa845cf0cbbac47e44ac9730dc079a2385"
|
||||
sha256: "4a5135754a62dbc827a64a42ef1f8ed72c962e191c97e2d48744225c2b9ebb73"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.8.13"
|
||||
version: "2.8.7"
|
||||
video_player_avfoundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_avfoundation
|
||||
sha256: f9a780aac57802b2892f93787e5ea53b5f43cc57dc107bee9436458365be71cd
|
||||
sha256: "0d47db6cbf72db61d86369219efd35c7f9d93515e1319da941ece81b1f21c49c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.8.4"
|
||||
version: "2.7.2"
|
||||
video_player_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2903,10 +2863,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_web
|
||||
sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb"
|
||||
sha256: e8bba2e5d1e159d5048c9a491bb2a7b29c535c612bb7d10c1e21107f5bd365ba
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
version: "2.3.5"
|
||||
video_thumbnail:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -2959,10 +2919,10 @@ packages:
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
name: watcher
|
||||
sha256: "5bf046f41320ac97a469d506261797f35254fa61c641741ef32dacda98b7d39c"
|
||||
sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.3"
|
||||
version: "1.1.2"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -3007,18 +2967,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: wechat_assets_picker
|
||||
sha256: c307e50394c1e6dfcd5c4701e84efb549fce71444fedcf2e671c50d809b3e2a1
|
||||
sha256: cafe3d32564ed3cacf9822f251941f7b44fe9885c17c8de4fca7e939a459e1ef
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.8.0"
|
||||
version: "9.5.1"
|
||||
wechat_picker_library:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wechat_picker_library
|
||||
sha256: "5cb61b9aa935b60da5b043f8446fbb9c5077419f20ccc4856bf444aec4f44bc1"
|
||||
sha256: a42e09cb85b15fc9410f6a69671371cc60aa99c4a1f7967f6593a7f665f6f47a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.7"
|
||||
version: "1.0.5"
|
||||
widgets_to_image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -3071,10 +3031,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: xml
|
||||
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.6.1"
|
||||
version: "6.5.0"
|
||||
xmlstream:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -77,6 +77,8 @@ dependencies:
|
||||
ref: remove-event-sub
|
||||
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
|
||||
@@ -113,8 +115,7 @@ dependencies:
|
||||
html_unescape: ^2.0.0
|
||||
http: ^1.1.0
|
||||
image: ^4.0.17
|
||||
image_editor: ^1.6.0
|
||||
image_picker: ^1.1.1
|
||||
in_app_purchase: ^3.0.7
|
||||
intl: ^0.19.0
|
||||
latlong2: ^0.9.0
|
||||
launcher_icon_switcher: ^0.0.2
|
||||
|
||||
1
mobile/thirdparty/flutter
vendored
@@ -1,19 +0,0 @@
|
||||
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.
|
||||
@@ -1,22 +0,0 @@
|
||||
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.
|
||||
@@ -1,28 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,9 +0,0 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/workspace.xml
|
||||
/.idea/libraries
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
@@ -1 +0,0 @@
|
||||
/build
|
||||
@@ -1,28 +0,0 @@
|
||||
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'])
|
||||
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
# 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
|
||||
@@ -1,27 +0,0 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<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>
|
||||
@@ -1,34 +0,0 @@
|
||||
<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>
|
||||
@@ -1,170 +0,0 @@
|
||||
<?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>
|
||||
@@ -1,5 +0,0 @@
|
||||
<?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>
|
||||
@@ -1,5 +0,0 @@
|
||||
<?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>
|
||||
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorPrimary">#3F51B5</color>
|
||||
<color name="colorPrimaryDark">#303F9F</color>
|
||||
<color name="colorAccent">#FF4081</color>
|
||||
</resources>
|
||||
@@ -1,3 +0,0 @@
|
||||
<resources>
|
||||
<string name="app_name">BackgroundFetch</string>
|
||||
</resources>
|
||||
@@ -1,11 +0,0 @@
|
||||
<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>
|
||||
@@ -1,17 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
// 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"
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
# 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
|
||||
@@ -1,6 +0,0 @@
|
||||
#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
|
||||
@@ -1,160 +0,0 @@
|
||||
#!/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 "$@"
|
||||
@@ -1,90 +0,0 @@
|
||||
@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 +0,0 @@
|
||||
include ':app', ':tsbackgroundfetch'
|
||||
@@ -1 +0,0 @@
|
||||
/build
|
||||
@@ -1,152 +0,0 @@
|
||||
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')
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
# 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
|
||||
@@ -1,26 +0,0 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
<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>
|
||||
@@ -1,291 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,300 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,362 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
<resources>
|
||||
<string name="app_name">TSBackgroundFetch</string>
|
||||
</resources>
|
||||
@@ -1,17 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||