mirror of
https://github.com/nextcloud/android.git
synced 2024-11-21 12:45:32 +03:00
Calendar backup/import
Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>
This commit is contained in:
parent
2693c103b3
commit
042c413569
57 changed files with 4261 additions and 1111 deletions
|
@ -328,6 +328,12 @@ dependencies {
|
|||
implementation "io.noties:prism4j:$prismVersion"
|
||||
kapt "io.noties:prism4j-bundler:$prismVersion"
|
||||
|
||||
implementation ('org.mnode.ical4j:ical4j:1.0.6') {
|
||||
['org.apache.commons','commons-logging'].each {
|
||||
exclude group: "$it"
|
||||
}
|
||||
}
|
||||
|
||||
// dependencies for local unit tests
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation "org.mockito:mockito-core:$mockitoVersion"
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z" /></svg>
|
Before Width: | Height: | Size: 422 B |
1
lint.xml
1
lint.xml
|
@ -43,6 +43,7 @@
|
|||
|
||||
<issue id="Typos">
|
||||
<ignore path="**/values-**/strings.xml" />
|
||||
<ignore path="**/values/setup.xml" />
|
||||
</issue>
|
||||
|
||||
<issue id="TrustAllX509TrustManager">
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
After Width: | Height: | Size: 8 KiB |
|
@ -1 +1 @@
|
|||
355
|
||||
349
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
DO NOT TOUCH; GENERATED BY DRONE
|
||||
<span class="mdl-layout-title">Lint Report: 1 error and 112 warnings</span>
|
||||
<span class="mdl-layout-title">Lint Report: 1 error and 111 warnings</span>
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
<Package name="~butterknife\..*" />
|
||||
<Package name="~de\.cotech\..*" />
|
||||
<Package name="~pl\.droidsonroids\..*" />
|
||||
<Package name="~third_parties\..*" />
|
||||
</Or>
|
||||
</Match>
|
||||
<Match>
|
||||
|
|
131
src/androidTest/assets/calendar.ics
Normal file
131
src/androidTest/assets/calendar.ics
Normal file
|
@ -0,0 +1,131 @@
|
|||
BEGIN:VCALENDAR
|
||||
PRODID:-//dummy@gmail.com//iCal Import/Export 3.18.0 Alpha1//
|
||||
EN
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
CALSCALE:GREGORIAN
|
||||
X-WR-TIMEZONE:UTC
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:Europe/Berlin
|
||||
LAST-MODIFIED:20201011T015911Z
|
||||
TZURL:http://tzurl.org/zoneinfo/Europe/Berlin
|
||||
X-LIC-LOCATION:Europe/Berlin
|
||||
X-PROLEPTIC-TZNAME:LMT
|
||||
BEGIN:STANDARD
|
||||
TZNAME:CET
|
||||
TZOFFSETFROM:+005328
|
||||
TZOFFSETTO:+0100
|
||||
DTSTART:18930401T000000
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
TZNAME:CEST
|
||||
TZOFFSETFROM:+0100
|
||||
TZOFFSETTO:+0200
|
||||
DTSTART:19160430T230000
|
||||
RDATE:19400401T020000
|
||||
RDATE:19430329T020000
|
||||
RDATE:19460414T020000
|
||||
RDATE:19470406T030000
|
||||
RDATE:19480418T020000
|
||||
RDATE:19490410T020000
|
||||
RDATE:19800406T020000
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
TZNAME:CET
|
||||
TZOFFSETFROM:+0200
|
||||
TZOFFSETTO:+0100
|
||||
DTSTART:19161001T010000
|
||||
RDATE:19421102T030000
|
||||
RDATE:19431004T030000
|
||||
RDATE:19441002T030000
|
||||
RDATE:19451118T030000
|
||||
RDATE:19461007T030000
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
TZNAME:CEST
|
||||
TZOFFSETFROM:+0100
|
||||
TZOFFSETTO:+0200
|
||||
DTSTART:19170416T020000
|
||||
RRULE:FREQ=YEARLY;UNTIL=19180415T010000Z;BYMONTH=4;BYDAY=3MO
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
TZNAME:CET
|
||||
TZOFFSETFROM:+0200
|
||||
TZOFFSETTO:+0100
|
||||
DTSTART:19170917T030000
|
||||
RRULE:FREQ=YEARLY;UNTIL=19180916T010000Z;BYMONTH=9;BYDAY=3MO
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
TZNAME:CEST
|
||||
TZOFFSETFROM:+0100
|
||||
TZOFFSETTO:+0200
|
||||
DTSTART:19440403T020000
|
||||
RRULE:FREQ=YEARLY;UNTIL=19450402T010000Z;BYMONTH=4;BYDAY=1MO
|
||||
END:DAYLIGHT
|
||||
BEGIN:DAYLIGHT
|
||||
TZNAME:CEMT
|
||||
TZOFFSETFROM:+0200
|
||||
TZOFFSETTO:+0300
|
||||
DTSTART:19450524T010000
|
||||
RDATE:19470511T020000
|
||||
END:DAYLIGHT
|
||||
BEGIN:DAYLIGHT
|
||||
TZNAME:CEST
|
||||
TZOFFSETFROM:+0300
|
||||
TZOFFSETTO:+0200
|
||||
DTSTART:19450924T030000
|
||||
RDATE:19470629T030000
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
TZNAME:CET
|
||||
TZOFFSETFROM:+0100
|
||||
TZOFFSETTO:+0100
|
||||
DTSTART:19460101T000000
|
||||
RDATE:19800101T000000
|
||||
END:STANDARD
|
||||
BEGIN:STANDARD
|
||||
TZNAME:CET
|
||||
TZOFFSETFROM:+0200
|
||||
TZOFFSETTO:+0100
|
||||
DTSTART:19471005T030000
|
||||
RRULE:FREQ=YEARLY;UNTIL=19491002T010000Z;BYMONTH=10;BYDAY=1SU
|
||||
END:STANDARD
|
||||
BEGIN:STANDARD
|
||||
TZNAME:CET
|
||||
TZOFFSETFROM:+0200
|
||||
TZOFFSETTO:+0100
|
||||
DTSTART:19800928T030000
|
||||
RRULE:FREQ=YEARLY;UNTIL=19950924T010000Z;BYMONTH=9;BYDAY=-1SU
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
TZNAME:CEST
|
||||
TZOFFSETFROM:+0100
|
||||
TZOFFSETTO:+0200
|
||||
DTSTART:19810329T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
TZNAME:CET
|
||||
TZOFFSETFROM:+0200
|
||||
TZOFFSETTO:+0100
|
||||
DTSTART:19961027T030000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20210820T083606Z
|
||||
UID:16294485666795adb8b4b08e94d3cb4445e1e3ee18fd9@nextcloud.com
|
||||
SUMMARY:Test event
|
||||
DESCRIPTION:
|
||||
ORGANIZER:mailto:dummy@gmail.com
|
||||
LOCATION:
|
||||
STATUS:CONFIRMED
|
||||
DTSTART;TZID=Europe/Berlin:20210806T090000
|
||||
DTEND:20210806T080000Z
|
||||
BEGIN:VALARM
|
||||
TRIGGER:-PT30M
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:Test event
|
||||
END:VALARM
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Tobias Kaminsky
|
||||
* Copyright (C) 2021 Tobias Kaminsky
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.owncloud.android.ui.fragment
|
||||
|
||||
import android.Manifest
|
||||
import androidx.test.espresso.intent.rule.IntentsTestRule
|
||||
import androidx.test.rule.GrantPermissionRule
|
||||
import com.owncloud.android.AbstractIT
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.ui.activity.ContactsPreferenceActivity
|
||||
import com.owncloud.android.ui.fragment.contactsbackup.BackupListFragment
|
||||
import com.owncloud.android.utils.ScreenshotTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class BackupListFragmentIT : AbstractIT() {
|
||||
@get:Rule
|
||||
val testActivityRule = IntentsTestRule(ContactsPreferenceActivity::class.java, true, false)
|
||||
|
||||
@get:Rule
|
||||
val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CALENDAR)
|
||||
|
||||
@Test
|
||||
@ScreenshotTest
|
||||
fun showLoading() {
|
||||
val sut = testActivityRule.launchActivity(null)
|
||||
val file = OCFile("/", "00000001")
|
||||
val transaction = sut.supportFragmentManager.beginTransaction()
|
||||
|
||||
transaction.replace(R.id.frame_container, BackupListFragment.newInstance(file, user))
|
||||
transaction.commit()
|
||||
|
||||
waitForIdleSync()
|
||||
screenshot(sut)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ScreenshotTest
|
||||
fun showContactList() {
|
||||
val sut = testActivityRule.launchActivity(null)
|
||||
val transaction = sut.supportFragmentManager.beginTransaction()
|
||||
val file = getFile("vcard.vcf")
|
||||
val ocFile = OCFile("/vcard.vcf", "00000002")
|
||||
ocFile.storagePath = file.absolutePath
|
||||
ocFile.mimeType = "text/vcard"
|
||||
|
||||
transaction.replace(R.id.frame_container, BackupListFragment.newInstance(ocFile, user))
|
||||
transaction.commit()
|
||||
|
||||
waitForIdleSync()
|
||||
shortSleep()
|
||||
screenshot(sut)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ScreenshotTest
|
||||
fun showCalendarList() {
|
||||
val sut = testActivityRule.launchActivity(null)
|
||||
val transaction = sut.supportFragmentManager.beginTransaction()
|
||||
val file = getFile("calendar.ics")
|
||||
val ocFile = OCFile("/Private calender_2020-09-01_10-45-20.ics.ics", "00000003")
|
||||
ocFile.storagePath = file.absolutePath
|
||||
ocFile.mimeType = "text/calendar"
|
||||
|
||||
transaction.replace(R.id.frame_container, BackupListFragment.newInstance(ocFile, user))
|
||||
transaction.commit()
|
||||
|
||||
waitForIdleSync()
|
||||
screenshot(sut)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ScreenshotTest
|
||||
fun showCalendarAndContactsList() {
|
||||
val sut = testActivityRule.launchActivity(null)
|
||||
val transaction = sut.supportFragmentManager.beginTransaction()
|
||||
|
||||
val calendarFile = getFile("calendar.ics")
|
||||
val calendarOcFile = OCFile("/Private calender_2020-09-01_10-45-20.ics", "00000003")
|
||||
calendarOcFile.storagePath = calendarFile.absolutePath
|
||||
calendarOcFile.mimeType = "text/calendar"
|
||||
|
||||
val contactFile = getFile("vcard.vcf")
|
||||
val contactOcFile = OCFile("/vcard.vcf", "00000002")
|
||||
contactOcFile.storagePath = contactFile.absolutePath
|
||||
contactOcFile.mimeType = "text/vcard"
|
||||
|
||||
val files = arrayOf(calendarOcFile, contactOcFile)
|
||||
transaction.replace(R.id.frame_container, BackupListFragment.newInstance(files, user))
|
||||
transaction.commit()
|
||||
|
||||
waitForIdleSync()
|
||||
screenshot(sut)
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
/*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Andy Scherzinger
|
||||
* Copyright (C) 2020 Andy Scherzinger
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.owncloud.android.ui.fragment
|
||||
|
||||
import androidx.test.espresso.intent.rule.IntentsTestRule
|
||||
import com.owncloud.android.AbstractIT
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.ui.activity.ContactsPreferenceActivity
|
||||
import com.owncloud.android.ui.fragment.contactsbackup.ContactListFragment
|
||||
import com.owncloud.android.utils.ScreenshotTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class ContactListFragmentIT : AbstractIT() {
|
||||
@get:Rule
|
||||
val testActivityRule = IntentsTestRule(ContactsPreferenceActivity::class.java, true, false)
|
||||
|
||||
val file = OCFile("/", "00000001")
|
||||
|
||||
@Test
|
||||
@ScreenshotTest
|
||||
fun showContactListFragmentLoading() {
|
||||
val sut = testActivityRule.launchActivity(null)
|
||||
val transaction = sut.supportFragmentManager.beginTransaction()
|
||||
transaction.replace(R.id.frame_container, ContactListFragment.newInstance(file, user))
|
||||
transaction.commit()
|
||||
|
||||
waitForIdleSync()
|
||||
screenshot(sut)
|
||||
}
|
||||
}
|
|
@ -33,6 +33,8 @@
|
|||
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
|
||||
<uses-permission android:name="android.permission.READ_CALENDAR" />
|
||||
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
|
||||
|
||||
<!-- USE_CREDENTIALS, MANAGE_ACCOUNTS and AUTHENTICATE_ACCOUNTS are needed for API <= 22.
|
||||
In API >= 23 they do not exist anymore -->
|
||||
|
|
|
@ -78,8 +78,8 @@ import com.owncloud.android.ui.fragment.FileDetailSharingFragment;
|
|||
import com.owncloud.android.ui.fragment.GalleryFragment;
|
||||
import com.owncloud.android.ui.fragment.LocalFileListFragment;
|
||||
import com.owncloud.android.ui.fragment.OCFileListFragment;
|
||||
import com.owncloud.android.ui.fragment.contactsbackup.ContactListFragment;
|
||||
import com.owncloud.android.ui.fragment.contactsbackup.ContactsBackupFragment;
|
||||
import com.owncloud.android.ui.fragment.contactsbackup.BackupFragment;
|
||||
import com.owncloud.android.ui.fragment.contactsbackup.BackupListFragment;
|
||||
import com.owncloud.android.ui.preview.PreviewImageActivity;
|
||||
import com.owncloud.android.ui.preview.PreviewImageFragment;
|
||||
import com.owncloud.android.ui.preview.PreviewMediaFragment;
|
||||
|
@ -155,13 +155,13 @@ abstract class ComponentsModule {
|
|||
abstract ChooseRichDocumentsTemplateDialogFragment chooseRichDocumentsTemplateDialogFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract ContactsBackupFragment contactsBackupFragment();
|
||||
abstract BackupFragment contactsBackupFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract PreviewImageFragment previewImageFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract ContactListFragment chooseContactListFragment();
|
||||
abstract BackupListFragment chooseContactListFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract PreviewMediaFragment previewMediaFragment();
|
||||
|
|
|
@ -87,6 +87,8 @@ class BackgroundJobFactory @Inject constructor(
|
|||
MediaFoldersDetectionWork::class -> createMediaFoldersDetectionWork(context, workerParameters)
|
||||
NotificationWork::class -> createNotificationWork(context, workerParameters)
|
||||
AccountRemovalWork::class -> createAccountRemovalWork(context, workerParameters)
|
||||
CalendarBackupWork::class -> createCalendarBackupWork(context, workerParameters)
|
||||
CalendarImportWork::class -> createCalendarImportWork(context, workerParameters)
|
||||
else -> null // caller falls back to default factory
|
||||
}
|
||||
}
|
||||
|
@ -131,6 +133,25 @@ class BackgroundJobFactory @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun createCalendarBackupWork(context: Context, params: WorkerParameters): CalendarBackupWork {
|
||||
return CalendarBackupWork(
|
||||
context,
|
||||
params,
|
||||
contentResolver,
|
||||
accountManager,
|
||||
preferences
|
||||
)
|
||||
}
|
||||
|
||||
private fun createCalendarImportWork(context: Context, params: WorkerParameters): CalendarImportWork {
|
||||
return CalendarImportWork(
|
||||
context,
|
||||
params,
|
||||
logger,
|
||||
contentResolver
|
||||
)
|
||||
}
|
||||
|
||||
private fun createFilesSyncWork(context: Context, params: WorkerParameters): FilesSyncWork {
|
||||
return FilesSyncWork(
|
||||
context = context,
|
||||
|
|
|
@ -71,6 +71,31 @@ interface BackgroundJobManager {
|
|||
*/
|
||||
fun startImmediateContactsBackup(user: User): LiveData<JobInfo?>
|
||||
|
||||
/**
|
||||
* Schedule periodic calendar backups job. Operating system will
|
||||
* decide when to start the job.
|
||||
*
|
||||
* This call is idempotent - there can be only one scheduled job
|
||||
* at any given time.
|
||||
*
|
||||
* @param user User for which job will be scheduled.
|
||||
*/
|
||||
fun schedulePeriodicCalendarBackup(user: User)
|
||||
|
||||
/**
|
||||
* Cancel periodic calendar backup. Existing tasks might finish, but no new
|
||||
* invocations will occur.
|
||||
*/
|
||||
fun cancelPeriodicCalendarBackup(user: User)
|
||||
|
||||
/**
|
||||
* Immediately start single calendar backup job.
|
||||
* This job will launch independently from periodic calendar backup.
|
||||
*
|
||||
* @return Job info with current status; status is null if job does not exist
|
||||
*/
|
||||
fun startImmediateCalendarBackup(user: User): LiveData<JobInfo?>
|
||||
|
||||
/**
|
||||
* Immediately start contacts import job. Import job will be started only once.
|
||||
* If new job is started while existing job is running - request will be ignored
|
||||
|
@ -90,6 +115,17 @@ interface BackgroundJobManager {
|
|||
selectedContacts: IntArray
|
||||
): LiveData<JobInfo?>
|
||||
|
||||
/**
|
||||
* Immediately start calendar import job. Import job will be started only once.
|
||||
* If new job is started while existing job is running - request will be ignored
|
||||
* and currently running job will continue running.
|
||||
*
|
||||
* @param calendarPaths Array of paths of calendar files to import from
|
||||
*
|
||||
* @return Job info with current status; status is null if job does not exist
|
||||
*/
|
||||
fun startImmediateCalendarImport(calendarPaths: Map<String, Int>): LiveData<JobInfo?>
|
||||
|
||||
fun schedulePeriodicFilesSyncJob()
|
||||
fun startImmediateFilesSyncJob(skipCustomFolders: Boolean = false, overridePowerSaving: Boolean = false)
|
||||
fun scheduleOfflineSync()
|
||||
|
|
|
@ -68,6 +68,8 @@ internal class BackgroundJobManagerImpl(
|
|||
const val JOB_PERIODIC_CONTACTS_BACKUP = "periodic_contacts_backup"
|
||||
const val JOB_IMMEDIATE_CONTACTS_BACKUP = "immediate_contacts_backup"
|
||||
const val JOB_IMMEDIATE_CONTACTS_IMPORT = "immediate_contacts_import"
|
||||
const val JOB_PERIODIC_CALENDAR_BACKUP = "periodic_calendar_backup"
|
||||
const val JOB_IMMEDIATE_CALENDAR_IMPORT = "immediate_calendar_import"
|
||||
const val JOB_PERIODIC_FILES_SYNC = "periodic_files_sync"
|
||||
const val JOB_IMMEDIATE_FILES_SYNC = "immediate_files_sync"
|
||||
const val JOB_PERIODIC_OFFLINE_SYNC = "periodic_offline_sync"
|
||||
|
@ -75,6 +77,7 @@ internal class BackgroundJobManagerImpl(
|
|||
const val JOB_IMMEDIATE_MEDIA_FOLDER_DETECTION = "immediate_media_folder_detection"
|
||||
const val JOB_NOTIFICATION = "notification"
|
||||
const val JOB_ACCOUNT_REMOVAL = "account_removal"
|
||||
const val JOB_IMMEDIATE_CALENDAR_BACKUP = "immediate_calendar_backup"
|
||||
|
||||
const val JOB_TEST = "test_job"
|
||||
|
||||
|
@ -85,7 +88,7 @@ internal class BackgroundJobManagerImpl(
|
|||
const val TAG_PREFIX_START_TIMESTAMP = "timestamp"
|
||||
val PREFIXES = setOf(TAG_PREFIX_NAME, TAG_PREFIX_USER, TAG_PREFIX_START_TIMESTAMP)
|
||||
const val NOT_SET_VALUE = "not set"
|
||||
const val PERIODIC_CONTACTS_BACKUP_INTERVAL_MINUTES = 24 * 60L
|
||||
const val PERIODIC_BACKUP_INTERVAL_MINUTES = 24 * 60L
|
||||
const val DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES = 15L
|
||||
const val DEFAULT_IMMEDIATE_JOB_DELAY_SEC = 3L
|
||||
|
||||
|
@ -228,7 +231,7 @@ internal class BackgroundJobManagerImpl(
|
|||
val request = periodicRequestBuilder(
|
||||
jobClass = ContactsBackupWork::class,
|
||||
jobName = JOB_PERIODIC_CONTACTS_BACKUP,
|
||||
intervalMins = PERIODIC_CONTACTS_BACKUP_INTERVAL_MINUTES,
|
||||
intervalMins = PERIODIC_BACKUP_INTERVAL_MINUTES,
|
||||
user = user
|
||||
).setInputData(data).build()
|
||||
|
||||
|
@ -267,6 +270,26 @@ internal class BackgroundJobManagerImpl(
|
|||
return workManager.getJobInfo(request.id)
|
||||
}
|
||||
|
||||
override fun startImmediateCalendarImport(calendarPaths: Map<String, Int>): LiveData<JobInfo?> {
|
||||
|
||||
val data = Data.Builder()
|
||||
.putAll(calendarPaths)
|
||||
.build()
|
||||
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiresCharging(false)
|
||||
.build()
|
||||
|
||||
val request = oneTimeRequestBuilder(CalendarImportWork::class, JOB_IMMEDIATE_CALENDAR_IMPORT)
|
||||
.setInputData(data)
|
||||
.setConstraints(constraints)
|
||||
.build()
|
||||
|
||||
workManager.enqueueUniqueWork(JOB_IMMEDIATE_CALENDAR_IMPORT, ExistingWorkPolicy.KEEP, request)
|
||||
|
||||
return workManager.getJobInfo(request.id)
|
||||
}
|
||||
|
||||
override fun startImmediateContactsBackup(user: User): LiveData<JobInfo?> {
|
||||
val data = Data.Builder()
|
||||
.putString(ContactsBackupWork.ACCOUNT, user.accountName)
|
||||
|
@ -281,6 +304,39 @@ internal class BackgroundJobManagerImpl(
|
|||
return workManager.getJobInfo(request.id)
|
||||
}
|
||||
|
||||
override fun startImmediateCalendarBackup(user: User): LiveData<JobInfo?> {
|
||||
val data = Data.Builder()
|
||||
.putString(CalendarBackupWork.ACCOUNT, user.accountName)
|
||||
.putBoolean(CalendarBackupWork.FORCE, true)
|
||||
.build()
|
||||
|
||||
val request = oneTimeRequestBuilder(CalendarBackupWork::class, JOB_IMMEDIATE_CALENDAR_BACKUP, user)
|
||||
.setInputData(data)
|
||||
.build()
|
||||
|
||||
workManager.enqueueUniqueWork(JOB_IMMEDIATE_CALENDAR_BACKUP, ExistingWorkPolicy.KEEP, request)
|
||||
return workManager.getJobInfo(request.id)
|
||||
}
|
||||
|
||||
override fun schedulePeriodicCalendarBackup(user: User) {
|
||||
val data = Data.Builder()
|
||||
.putString(CalendarBackupWork.ACCOUNT, user.accountName)
|
||||
.putBoolean(CalendarBackupWork.FORCE, true)
|
||||
.build()
|
||||
val request = periodicRequestBuilder(
|
||||
jobClass = CalendarBackupWork::class,
|
||||
jobName = JOB_PERIODIC_CALENDAR_BACKUP,
|
||||
intervalMins = PERIODIC_BACKUP_INTERVAL_MINUTES,
|
||||
user = user
|
||||
).setInputData(data).build()
|
||||
|
||||
workManager.enqueueUniquePeriodicWork(JOB_PERIODIC_CALENDAR_BACKUP, ExistingPeriodicWorkPolicy.KEEP, request)
|
||||
}
|
||||
|
||||
override fun cancelPeriodicCalendarBackup(user: User) {
|
||||
workManager.cancelJob(JOB_PERIODIC_CALENDAR_BACKUP, user)
|
||||
}
|
||||
|
||||
override fun schedulePeriodicFilesSyncJob() {
|
||||
val request = periodicRequestBuilder(
|
||||
jobClass = FilesSyncWork::class,
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Tobias Kaminsky
|
||||
* Copyright (C) 2021 Tobias Kaminsky
|
||||
* Copyright (C) 2021 Nextcloud GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.nextcloud.client.jobs
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.text.TextUtils
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.nextcloud.client.account.UserAccountManager
|
||||
import com.nextcloud.client.preferences.AppPreferences
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
import third_parties.sufficientlysecure.AndroidCalendar
|
||||
import third_parties.sufficientlysecure.SaveCalendar
|
||||
import java.util.Calendar
|
||||
|
||||
class CalendarBackupWork(
|
||||
appContext: Context,
|
||||
params: WorkerParameters,
|
||||
private val contentResolver: ContentResolver,
|
||||
private val accountManager: UserAccountManager,
|
||||
private val preferences: AppPreferences
|
||||
) : Worker(appContext, params) {
|
||||
|
||||
companion object {
|
||||
val TAG = CalendarBackupWork::class.java.simpleName
|
||||
const val ACCOUNT = "account"
|
||||
const val FORCE = "force"
|
||||
const val JOB_INTERVAL_MS: Long = 24 * 60 * 60 * 1000
|
||||
}
|
||||
|
||||
override fun doWork(): Result {
|
||||
val accountName = inputData.getString(ACCOUNT) ?: ""
|
||||
val optionalUser = accountManager.getUser(accountName)
|
||||
if (!optionalUser.isPresent || TextUtils.isEmpty(accountName)) { // no account provided
|
||||
return Result.failure()
|
||||
}
|
||||
val lastExecution = preferences.calendarLastBackup
|
||||
|
||||
val force = inputData.getBoolean(FORCE, false)
|
||||
if (force || lastExecution + JOB_INTERVAL_MS < Calendar.getInstance().timeInMillis) {
|
||||
|
||||
AndroidCalendar.loadAll(contentResolver).forEach { calendar ->
|
||||
SaveCalendar(
|
||||
applicationContext,
|
||||
calendar,
|
||||
preferences,
|
||||
accountManager.user
|
||||
).start()
|
||||
}
|
||||
|
||||
// store execution date
|
||||
preferences.calendarLastBackup = Calendar.getInstance().timeInMillis
|
||||
} else {
|
||||
Log_OC.d(TAG, "last execution less than 24h ago")
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Tobias Kaminsky
|
||||
* Copyright (C) 2017 Tobias Kaminsky
|
||||
* Copyright (C) 2017 Nextcloud GmbH.
|
||||
* Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.nextcloud.client.jobs
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.nextcloud.client.logger.Logger
|
||||
import net.fortuna.ical4j.data.CalendarBuilder
|
||||
import third_parties.sufficientlysecure.AndroidCalendar
|
||||
import third_parties.sufficientlysecure.CalendarSource
|
||||
import third_parties.sufficientlysecure.ProcessVEvent
|
||||
import java.io.File
|
||||
|
||||
class CalendarImportWork(
|
||||
private val appContext: Context,
|
||||
params: WorkerParameters,
|
||||
private val logger: Logger,
|
||||
private val contentResolver: ContentResolver
|
||||
) : Worker(appContext, params) {
|
||||
|
||||
companion object {
|
||||
const val TAG = "CalendarImportWork"
|
||||
const val SELECTED_CALENDARS = "selected_contacts_indices"
|
||||
}
|
||||
|
||||
override fun doWork(): Result {
|
||||
val calendarPaths = inputData.getStringArray(SELECTED_CALENDARS) ?: arrayOf<String>()
|
||||
val calendars = inputData.keyValueMap as Map<String, AndroidCalendar>
|
||||
|
||||
val calendarBuilder = CalendarBuilder()
|
||||
|
||||
for ((path, selectedCalendar) in calendars) {
|
||||
logger.d(TAG, "Import calendar from $path")
|
||||
|
||||
val file = File(path)
|
||||
val calendarSource = CalendarSource(
|
||||
file.toURI().toURL().toString(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
appContext
|
||||
)
|
||||
|
||||
val calendars = AndroidCalendar.loadAll(contentResolver)[0]
|
||||
|
||||
ProcessVEvent(
|
||||
appContext,
|
||||
calendarBuilder.build(calendarSource.stream),
|
||||
selectedCalendar,
|
||||
true
|
||||
).run()
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
}
|
|
@ -29,8 +29,8 @@ import android.provider.ContactsContract
|
|||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.nextcloud.client.logger.Logger
|
||||
import com.owncloud.android.ui.fragment.contactsbackup.ContactListFragment
|
||||
import com.owncloud.android.ui.fragment.contactsbackup.ContactListFragment.VCardComparator
|
||||
import com.owncloud.android.ui.fragment.contactsbackup.BackupListFragment
|
||||
import com.owncloud.android.ui.fragment.contactsbackup.VCardComparator
|
||||
import ezvcard.Ezvcard
|
||||
import ezvcard.VCard
|
||||
import third_parties.ezvcard_android.ContactOperations
|
||||
|
@ -70,7 +70,10 @@ class ContactsImportWork(
|
|||
try {
|
||||
val operations = ContactOperations(applicationContext, contactsAccountName, contactsAccountType)
|
||||
vCards.addAll(Ezvcard.parse(file).all())
|
||||
Collections.sort(vCards, VCardComparator())
|
||||
Collections.sort(
|
||||
vCards,
|
||||
VCardComparator()
|
||||
)
|
||||
cursor = contentResolver.query(
|
||||
ContactsContract.Contacts.CONTENT_URI,
|
||||
null,
|
||||
|
@ -91,7 +94,7 @@ class ContactsImportWork(
|
|||
}
|
||||
for (contactIndex in selectedContactsIndices) {
|
||||
val vCard = vCards[contactIndex]
|
||||
if (ContactListFragment.getDisplayName(vCard).isEmpty()) {
|
||||
if (BackupListFragment.getDisplayName(vCard).isEmpty()) {
|
||||
if (!ownContactMap.containsKey(vCard)) {
|
||||
operations.insertContact(vCard)
|
||||
} else {
|
||||
|
|
|
@ -361,4 +361,12 @@ public interface AppPreferences {
|
|||
void resetPinWrongAttempts();
|
||||
|
||||
int pinBruteForceDelay();
|
||||
|
||||
String getUidPid();
|
||||
|
||||
void setUidPid(String uidPid);
|
||||
|
||||
long getCalendarLastBackup();
|
||||
|
||||
void setCalendarLastBackup(long timestamp);
|
||||
}
|
||||
|
|
|
@ -90,6 +90,10 @@ public final class AppPreferencesImpl implements AppPreferences {
|
|||
private static final String PREF__PHOTO_SEARCH_TIMESTAMP = "photo_search_timestamp";
|
||||
private static final String PREF__POWER_CHECK_DISABLED = "power_check_disabled";
|
||||
private static final String PREF__PIN_BRUTE_FORCE_COUNT = "pin_brute_force_count";
|
||||
private static final String PREF__UID_PID = "uid_pid";
|
||||
|
||||
private static final String PREF__CALENDAR_AUTOMATIC_BACKUP = "calendar_automatic_backup";
|
||||
private static final String PREF__CALENDAR_LAST_BACKUP = "calendar_last_backup";
|
||||
|
||||
private final Context context;
|
||||
private final SharedPreferences preferences;
|
||||
|
@ -97,8 +101,8 @@ public final class AppPreferencesImpl implements AppPreferences {
|
|||
private final ListenerRegistry listeners;
|
||||
|
||||
/**
|
||||
* Adapter delegating raw {@link SharedPreferences.OnSharedPreferenceChangeListener} calls
|
||||
* with key-value pairs to respective {@link com.nextcloud.client.preferences.AppPreferences.Listener} method.
|
||||
* Adapter delegating raw {@link SharedPreferences.OnSharedPreferenceChangeListener} calls with key-value pairs to
|
||||
* respective {@link com.nextcloud.client.preferences.AppPreferences.Listener} method.
|
||||
*/
|
||||
static class ListenerRegistry implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private final AppPreferences preferences;
|
||||
|
@ -660,6 +664,26 @@ public final class AppPreferencesImpl implements AppPreferences {
|
|||
return computeBruteForceDelay(count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUidPid() {
|
||||
return preferences.getString(PREF__UID_PID, "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUidPid(String uidPid) {
|
||||
preferences.edit().putString(PREF__UID_PID, uidPid).apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getCalendarLastBackup() {
|
||||
return preferences.getLong(PREF__CALENDAR_LAST_BACKUP, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCalendarLastBackup(long timestamp) {
|
||||
preferences.edit().putLong(PREF__CALENDAR_LAST_BACKUP, timestamp).apply();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public int computeBruteForceDelay(int count) {
|
||||
return (int) Math.min(count / 3d, 10);
|
||||
|
|
|
@ -31,8 +31,8 @@ import com.nextcloud.client.jobs.BackgroundJobManager;
|
|||
import com.owncloud.android.R;
|
||||
import com.owncloud.android.datamodel.OCFile;
|
||||
import com.owncloud.android.ui.fragment.FileFragment;
|
||||
import com.owncloud.android.ui.fragment.contactsbackup.ContactListFragment;
|
||||
import com.owncloud.android.ui.fragment.contactsbackup.ContactsBackupFragment;
|
||||
import com.owncloud.android.ui.fragment.contactsbackup.BackupFragment;
|
||||
import com.owncloud.android.ui.fragment.contactsbackup.BackupListFragment;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
|
@ -48,7 +48,7 @@ public class ContactsPreferenceActivity extends FileActivity implements FileFrag
|
|||
public static final String EXTRA_FILE = "FILE";
|
||||
public static final String EXTRA_USER = "USER";
|
||||
/**
|
||||
* Warning: default for this extra is different between this activity and {@link ContactsBackupFragment}
|
||||
* Warning: default for this extra is different between this activity and {@link BackupFragment}
|
||||
*/
|
||||
public static final String EXTRA_SHOW_SIDEBAR = "SHOW_SIDEBAR";
|
||||
public static final String PREFERENCE_CONTACTS_AUTOMATIC_BACKUP = "PREFERENCE_CONTACTS_AUTOMATIC_BACKUP";
|
||||
|
@ -84,7 +84,7 @@ public class ContactsPreferenceActivity extends FileActivity implements FileFrag
|
|||
setupToolbar();
|
||||
|
||||
// setup drawer
|
||||
setupDrawer(R.id.nav_contacts);
|
||||
//setupDrawer(R.id.nav_contacts); // TODO needed?
|
||||
|
||||
// show sidebar?
|
||||
boolean showSidebar = getIntent().getBooleanExtra(EXTRA_SHOW_SIDEBAR, true);
|
||||
|
@ -105,12 +105,12 @@ public class ContactsPreferenceActivity extends FileActivity implements FileFrag
|
|||
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
|
||||
if (intent == null || intent.getParcelableExtra(EXTRA_FILE) == null ||
|
||||
intent.getParcelableExtra(EXTRA_USER) == null) {
|
||||
ContactsBackupFragment fragment = ContactsBackupFragment.create(showSidebar);
|
||||
BackupFragment fragment = BackupFragment.create(showSidebar);
|
||||
transaction.add(R.id.frame_container, fragment);
|
||||
} else {
|
||||
OCFile file = intent.getParcelableExtra(EXTRA_FILE);
|
||||
User user = intent.getParcelableExtra(EXTRA_USER);
|
||||
ContactListFragment contactListFragment = ContactListFragment.newInstance(file, user);
|
||||
BackupListFragment contactListFragment = BackupListFragment.newInstance(file, user);
|
||||
transaction.add(R.id.frame_container, contactListFragment);
|
||||
}
|
||||
transaction.commit();
|
||||
|
@ -139,7 +139,7 @@ public class ContactsPreferenceActivity extends FileActivity implements FileFrag
|
|||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (getSupportFragmentManager().findFragmentByTag(ContactListFragment.TAG) != null) {
|
||||
if (getSupportFragmentManager().findFragmentByTag(BackupListFragment.TAG) != null) {
|
||||
getSupportFragmentManager().popBackStack(BACKUP_TO_LIST, FragmentManager.POP_BACK_STACK_INCLUSIVE);
|
||||
} else {
|
||||
finish();
|
||||
|
|
|
@ -403,9 +403,6 @@ public abstract class DrawerActivity extends ToolbarActivity
|
|||
DrawerMenuUtil.removeMenuItem(menu, R.id.nav_community,
|
||||
!getResources().getBoolean(R.bool.participate_enabled));
|
||||
DrawerMenuUtil.removeMenuItem(menu, R.id.nav_shared, !getResources().getBoolean(R.bool.shared_enabled));
|
||||
DrawerMenuUtil.removeMenuItem(menu, R.id.nav_contacts, !getResources().getBoolean(R.bool.contacts_backup)
|
||||
|| !getResources().getBoolean(R.bool.show_drawer_contacts_backup));
|
||||
|
||||
DrawerMenuUtil.removeMenuItem(menu, R.id.nav_logout, !getResources().getBoolean(R.bool.show_drawer_logout));
|
||||
}
|
||||
|
||||
|
@ -450,8 +447,6 @@ public abstract class DrawerActivity extends ToolbarActivity
|
|||
startActivity(ActivitiesActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
} else if (itemId == R.id.nav_notifications) {
|
||||
startActivity(NotificationsActivity.class);
|
||||
} else if (itemId == R.id.nav_contacts) {
|
||||
ContactsPreferenceActivity.startActivity(this);
|
||||
} else if (itemId == R.id.nav_settings) {
|
||||
startActivity(SettingsActivity.class);
|
||||
} else if (itemId == R.id.nav_community) {
|
||||
|
|
|
@ -325,7 +325,7 @@ public class SettingsActivity extends ThemedPreferenceActivity
|
|||
|
||||
setupCalendarPreference(preferenceCategoryMore);
|
||||
|
||||
setupContactsBackupPreference(preferenceCategoryMore);
|
||||
setupBackupPreference();
|
||||
|
||||
setupE2EMnemonicPreference(preferenceCategoryMore);
|
||||
|
||||
|
@ -474,19 +474,13 @@ public class SettingsActivity extends ThemedPreferenceActivity
|
|||
}
|
||||
}
|
||||
|
||||
private void setupContactsBackupPreference(PreferenceCategory preferenceCategoryMore) {
|
||||
boolean contactsBackupEnabled = !getResources().getBoolean(R.bool.show_drawer_contacts_backup)
|
||||
&& getResources().getBoolean(R.bool.contacts_backup);
|
||||
Preference pContactsBackup = findPreference("contacts");
|
||||
private void setupBackupPreference() {
|
||||
Preference pContactsBackup = findPreference("backup");
|
||||
if (pContactsBackup != null) {
|
||||
if (contactsBackupEnabled) {
|
||||
pContactsBackup.setOnPreferenceClickListener(preference -> {
|
||||
ContactsPreferenceActivity.startActivityWithoutSidebar(this);
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
preferenceCategoryMore.removePreference(pContactsBackup);
|
||||
}
|
||||
pContactsBackup.setOnPreferenceClickListener(preference -> {
|
||||
ContactsPreferenceActivity.startActivityWithoutSidebar(this);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Tobias Kaminsky
|
||||
* Copyright (C) 2021 Tobias Kaminsky
|
||||
* Copyright (C) 2021 Nextcloud GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.owncloud.android.ui.asynctasks;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import com.owncloud.android.datamodel.OCFile;
|
||||
import com.owncloud.android.lib.common.utils.Log_OC;
|
||||
import com.owncloud.android.ui.fragment.contactsbackup.BackupListFragment;
|
||||
import com.owncloud.android.ui.fragment.contactsbackup.VCardComparator;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import ezvcard.Ezvcard;
|
||||
import ezvcard.VCard;
|
||||
|
||||
public class LoadContactsTask extends AsyncTask<Void, Void, Boolean> {
|
||||
private final WeakReference<BackupListFragment> backupListFragmentWeakReference;
|
||||
private final OCFile ocFile;
|
||||
private final List<VCard> vCards = new ArrayList<>();
|
||||
|
||||
public LoadContactsTask(BackupListFragment backupListFragment, OCFile ocFile) {
|
||||
this.backupListFragmentWeakReference = new WeakReference<>(backupListFragment);
|
||||
this.ocFile = ocFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
if (backupListFragmentWeakReference.get() != null && !backupListFragmentWeakReference.get().hasCalendarEntry()) {
|
||||
backupListFragmentWeakReference.get().showLoadingMessage(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... voids) {
|
||||
if (!isCancelled()) {
|
||||
File file = new File(ocFile.getStoragePath());
|
||||
try {
|
||||
vCards.addAll(Ezvcard.parse(file).all());
|
||||
Collections.sort(vCards, new VCardComparator());
|
||||
} catch (IOException e) {
|
||||
Log_OC.e(this, "IO Exception: " + file.getAbsolutePath());
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean bool) {
|
||||
if (!isCancelled() && bool && backupListFragmentWeakReference.get() != null) {
|
||||
backupListFragmentWeakReference.get().loadVCards(vCards);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,10 +21,8 @@
|
|||
package com.owncloud.android.ui.fragment.contactsbackup;
|
||||
|
||||
import android.Manifest;
|
||||
import android.accounts.Account;
|
||||
import android.app.DatePickerDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
|
@ -34,14 +32,14 @@ import android.view.View;
|
|||
import android.view.ViewGroup;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.DatePicker;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.nextcloud.client.account.User;
|
||||
import com.nextcloud.client.di.Injectable;
|
||||
import com.nextcloud.client.jobs.BackgroundJobManager;
|
||||
import com.nextcloud.java.util.Optional;
|
||||
import com.owncloud.android.R;
|
||||
import com.owncloud.android.databinding.ContactsBackupFragmentBinding;
|
||||
import com.owncloud.android.databinding.BackupFragmentBinding;
|
||||
import com.owncloud.android.datamodel.ArbitraryDataProvider;
|
||||
import com.owncloud.android.datamodel.FileDataStorageManager;
|
||||
import com.owncloud.android.datamodel.OCFile;
|
||||
|
@ -51,17 +49,17 @@ import com.owncloud.android.ui.activity.ContactsPreferenceActivity;
|
|||
import com.owncloud.android.ui.activity.SettingsActivity;
|
||||
import com.owncloud.android.ui.fragment.FileFragment;
|
||||
import com.owncloud.android.utils.DisplayUtils;
|
||||
import com.owncloud.android.utils.MimeTypeUtil;
|
||||
import com.owncloud.android.utils.PermissionUtil;
|
||||
import com.owncloud.android.utils.theme.ThemeButtonUtils;
|
||||
import com.owncloud.android.utils.theme.ThemeCheckableUtils;
|
||||
import com.owncloud.android.utils.theme.ThemeColorUtils;
|
||||
import com.owncloud.android.utils.theme.ThemeSnackbarUtils;
|
||||
import com.owncloud.android.utils.theme.ThemeToolbarUtils;
|
||||
import com.owncloud.android.utils.theme.ThemeUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -76,15 +74,15 @@ import third_parties.daveKoeller.AlphanumComparator;
|
|||
import static com.owncloud.android.ui.activity.ContactsPreferenceActivity.PREFERENCE_CONTACTS_AUTOMATIC_BACKUP;
|
||||
import static com.owncloud.android.ui.activity.ContactsPreferenceActivity.PREFERENCE_CONTACTS_LAST_BACKUP;
|
||||
|
||||
public class ContactsBackupFragment extends FileFragment implements DatePickerDialog.OnDateSetListener, Injectable {
|
||||
public static final String TAG = ContactsBackupFragment.class.getSimpleName();
|
||||
public class BackupFragment extends FileFragment implements DatePickerDialog.OnDateSetListener, Injectable {
|
||||
public static final String TAG = BackupFragment.class.getSimpleName();
|
||||
private static final String ARG_SHOW_SIDEBAR = "SHOW_SIDEBAR";
|
||||
private static final String KEY_CALENDAR_PICKER_OPEN = "IS_CALENDAR_PICKER_OPEN";
|
||||
private static final String KEY_CALENDAR_DAY = "CALENDAR_DAY";
|
||||
private static final String KEY_CALENDAR_MONTH = "CALENDAR_MONTH";
|
||||
private static final String KEY_CALENDAR_YEAR = "CALENDAR_YEAR";
|
||||
|
||||
private ContactsBackupFragmentBinding binding;
|
||||
private BackupFragmentBinding binding;
|
||||
|
||||
@Inject BackgroundJobManager backgroundJobManager;
|
||||
|
||||
|
@ -93,13 +91,15 @@ public class ContactsBackupFragment extends FileFragment implements DatePickerDi
|
|||
|
||||
private DatePickerDialog datePickerDialog;
|
||||
|
||||
private CompoundButton.OnCheckedChangeListener onCheckedChangeListener;
|
||||
private CompoundButton.OnCheckedChangeListener dailyBackupCheckedChangeListener;
|
||||
private CompoundButton.OnCheckedChangeListener contactsCheckedListener;
|
||||
private CompoundButton.OnCheckedChangeListener calendarCheckedListener;
|
||||
private ArbitraryDataProvider arbitraryDataProvider;
|
||||
private User user;
|
||||
private boolean showSidebar = true;
|
||||
|
||||
public static ContactsBackupFragment create(boolean showSidebar) {
|
||||
ContactsBackupFragment fragment = new ContactsBackupFragment();
|
||||
public static BackupFragment create(boolean showSidebar) {
|
||||
BackupFragment fragment = new BackupFragment();
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putBoolean(ARG_SHOW_SIDEBAR, showSidebar);
|
||||
fragment.setArguments(bundle);
|
||||
|
@ -114,7 +114,7 @@ public class ContactsBackupFragment extends FileFragment implements DatePickerDi
|
|||
getContext().getTheme().applyStyle(R.style.FallbackThemingTheme, true);
|
||||
}
|
||||
|
||||
binding = ContactsBackupFragmentBinding.inflate(inflater, container, false);
|
||||
binding = BackupFragmentBinding.inflate(inflater, container, false);
|
||||
View view = binding.getRoot();
|
||||
|
||||
setHasOptionsMenu(true);
|
||||
|
@ -129,7 +129,7 @@ public class ContactsBackupFragment extends FileFragment implements DatePickerDi
|
|||
ActionBar actionBar = contactsPreferenceActivity != null ? contactsPreferenceActivity.getSupportActionBar() : null;
|
||||
|
||||
if (actionBar != null) {
|
||||
ThemeToolbarUtils.setColoredTitle(actionBar, getString(R.string.actionbar_contacts), getContext());
|
||||
ThemeToolbarUtils.setColoredTitle(actionBar, getString(R.string.backup_title), getContext());
|
||||
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
ThemeToolbarUtils.tintBackButton(actionBar, getContext());
|
||||
|
@ -138,42 +138,84 @@ public class ContactsBackupFragment extends FileFragment implements DatePickerDi
|
|||
arbitraryDataProvider = new ArbitraryDataProvider(getContext().getContentResolver());
|
||||
|
||||
ThemeCheckableUtils.tintSwitch(
|
||||
binding.contactsAutomaticBackup, ThemeColorUtils.primaryAccentColor(getContext()));
|
||||
binding.contactsAutomaticBackup.setChecked(
|
||||
binding.contacts, ThemeColorUtils.primaryAccentColor(getContext()));
|
||||
ThemeCheckableUtils.tintSwitch(
|
||||
binding.calendar, ThemeColorUtils.primaryAccentColor(getContext()));
|
||||
ThemeCheckableUtils.tintSwitch(
|
||||
binding.dailyBackup, ThemeColorUtils.primaryAccentColor(getContext()));
|
||||
binding.dailyBackup.setChecked(
|
||||
arbitraryDataProvider.getBooleanValue(user, PREFERENCE_CONTACTS_AUTOMATIC_BACKUP));
|
||||
|
||||
onCheckedChangeListener = (buttonView, isChecked) -> {
|
||||
binding.contacts.setChecked(checkContactBackupPermission());
|
||||
binding.calendar.setChecked(checkCalendarBackupPermission());
|
||||
|
||||
dailyBackupCheckedChangeListener = (buttonView, isChecked) -> {
|
||||
if (checkAndAskForContactsReadPermission()) {
|
||||
setAutomaticBackup(isChecked);
|
||||
}
|
||||
};
|
||||
|
||||
binding.contactsAutomaticBackup.setOnCheckedChangeListener(onCheckedChangeListener);
|
||||
binding.contactsBackupNow.setOnClickListener(v -> backupContacts());
|
||||
contactsCheckedListener = (buttonView, isChecked) -> {
|
||||
if (isChecked) {
|
||||
if (checkAndAskForContactsReadPermission()) {
|
||||
binding.backupNow.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
if (!binding.calendar.isChecked()) {
|
||||
binding.backupNow.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
};
|
||||
binding.contacts.setOnCheckedChangeListener(contactsCheckedListener);
|
||||
|
||||
calendarCheckedListener = (buttonView, isChecked) -> {
|
||||
if (isChecked) {
|
||||
if (checkAndAskForCalendarReadPermission()) {
|
||||
binding.backupNow.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
if (!binding.contacts.isChecked()) {
|
||||
binding.backupNow.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
binding.calendar.setOnCheckedChangeListener(calendarCheckedListener);
|
||||
|
||||
binding.dailyBackup.setOnCheckedChangeListener(dailyBackupCheckedChangeListener);
|
||||
binding.backupNow.setOnClickListener(v -> backup());
|
||||
binding.backupNow.setEnabled(checkBackupNowPermission());
|
||||
binding.backupNow.setVisibility(checkBackupNowPermission() ? View.VISIBLE : View.GONE);
|
||||
|
||||
binding.contactsDatepicker.setOnClickListener(v -> openCleanDate());
|
||||
|
||||
// display last backup
|
||||
Long lastBackupTimestamp = arbitraryDataProvider.getLongValue(user, PREFERENCE_CONTACTS_LAST_BACKUP);
|
||||
|
||||
if (lastBackupTimestamp == -1) {
|
||||
binding.contactsLastBackupTimestamp.setText(R.string.contacts_preference_backup_never);
|
||||
binding.lastBackupWithDate.setVisibility(View.GONE);
|
||||
} else {
|
||||
binding.contactsLastBackupTimestamp.setText(
|
||||
DisplayUtils.getRelativeTimestamp(contactsPreferenceActivity, lastBackupTimestamp));
|
||||
binding.lastBackupWithDate.setText(
|
||||
String.format(getString(R.string.last_backup),
|
||||
DisplayUtils.getRelativeTimestamp(contactsPreferenceActivity, lastBackupTimestamp)));
|
||||
}
|
||||
|
||||
if (savedInstanceState != null && savedInstanceState.getBoolean(KEY_CALENDAR_PICKER_OPEN, false)) {
|
||||
if (savedInstanceState.getInt(KEY_CALENDAR_YEAR, -1) != -1 &&
|
||||
savedInstanceState.getInt(KEY_CALENDAR_MONTH, -1) != -1 &&
|
||||
savedInstanceState.getInt(KEY_CALENDAR_DAY, -1) != -1) {
|
||||
savedInstanceState.getInt(KEY_CALENDAR_MONTH, -1) != -1 &&
|
||||
savedInstanceState.getInt(KEY_CALENDAR_DAY, -1) != -1) {
|
||||
selectedDate = new Date(savedInstanceState.getInt(KEY_CALENDAR_YEAR),
|
||||
savedInstanceState.getInt(KEY_CALENDAR_MONTH), savedInstanceState.getInt(KEY_CALENDAR_DAY));
|
||||
savedInstanceState.getInt(KEY_CALENDAR_MONTH), savedInstanceState.getInt(KEY_CALENDAR_DAY));
|
||||
}
|
||||
calendarPickerOpen = true;
|
||||
}
|
||||
|
||||
ThemeButtonUtils.colorPrimaryButton(binding.contactsBackupNow, getContext());
|
||||
ThemeButtonUtils.colorPrimaryButton(binding.contactsDatepicker, getContext());
|
||||
ThemeButtonUtils.colorPrimaryButton(binding.backupNow, getContext());
|
||||
ThemeButtonUtils.themeBorderlessButton(binding.contactsDatepicker);
|
||||
|
||||
int primaryAccentColor = ThemeColorUtils.primaryAccentColor(getContext());
|
||||
binding.dataToBackUpTitle.setTextColor(primaryAccentColor);
|
||||
binding.backupSettingsTitle.setTextColor(primaryAccentColor);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
@ -281,45 +323,69 @@ public class ContactsBackupFragment extends FileFragment implements DatePickerDi
|
|||
for (int index = 0; index < permissions.length; index++) {
|
||||
if (Manifest.permission.READ_CONTACTS.equalsIgnoreCase(permissions[index])) {
|
||||
if (grantResults[index] >= 0) {
|
||||
setAutomaticBackup(true);
|
||||
} else {
|
||||
binding.contactsAutomaticBackup.setOnCheckedChangeListener(null);
|
||||
binding.contactsAutomaticBackup.setChecked(false);
|
||||
binding.contactsAutomaticBackup.setOnCheckedChangeListener(onCheckedChangeListener);
|
||||
// if approved, exit for loop
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
// if not accepted, disable again
|
||||
binding.contacts.setOnCheckedChangeListener(null);
|
||||
binding.contacts.setChecked(false);
|
||||
binding.contacts.setOnCheckedChangeListener(contactsCheckedListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (requestCode == PermissionUtil.PERMISSIONS_READ_CONTACTS_MANUALLY) {
|
||||
if (requestCode == PermissionUtil.PERMISSIONS_READ_CALENDAR_AUTOMATIC) {
|
||||
for (int index = 0; index < permissions.length; index++) {
|
||||
if (Manifest.permission.READ_CONTACTS.equalsIgnoreCase(permissions[index])) {
|
||||
if (Manifest.permission.READ_CALENDAR.equalsIgnoreCase(permissions[index])) {
|
||||
if (grantResults[index] >= 0) {
|
||||
startContactsBackupJob();
|
||||
// if approved, exit for loop
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// if not accepted, disable again
|
||||
binding.calendar.setOnCheckedChangeListener(null);
|
||||
binding.calendar.setChecked(false);
|
||||
binding.calendar.setOnCheckedChangeListener(calendarCheckedListener);
|
||||
|
||||
binding.backupNow.setVisibility(checkBackupNowPermission() ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
binding.backupNow.setVisibility(checkBackupNowPermission() ? View.VISIBLE : View.GONE);
|
||||
binding.backupNow.setEnabled(checkBackupNowPermission());
|
||||
}
|
||||
|
||||
public void backupContacts() {
|
||||
if (checkAndAskForContactsReadPermission()) {
|
||||
public void backup() {
|
||||
if (binding.contacts.isChecked() && checkAndAskForContactsReadPermission()) {
|
||||
startContactsBackupJob();
|
||||
}
|
||||
|
||||
if (binding.calendar.isChecked() && checkAndAskForCalendarReadPermission()) {
|
||||
startCalendarBackupJob();
|
||||
}
|
||||
|
||||
DisplayUtils.showSnackMessage(requireView().findViewById(R.id.contacts_linear_layout),
|
||||
R.string.contacts_preferences_backup_scheduled);
|
||||
}
|
||||
|
||||
private void startContactsBackupJob() {
|
||||
ContactsPreferenceActivity activity = (ContactsPreferenceActivity)getActivity();
|
||||
ContactsPreferenceActivity activity = (ContactsPreferenceActivity) getActivity();
|
||||
if (activity != null) {
|
||||
Optional<User> optionalUser = activity.getUser();
|
||||
if (optionalUser.isPresent()) {
|
||||
backgroundJobManager.startImmediateContactsBackup(optionalUser.get());
|
||||
DisplayUtils.showSnackMessage(getView().findViewById(R.id.contacts_linear_layout),
|
||||
R.string.contacts_preferences_backup_scheduled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void startCalendarBackupJob() {
|
||||
ContactsPreferenceActivity activity = (ContactsPreferenceActivity) getActivity();
|
||||
if (activity != null) {
|
||||
Optional<User> optionalUser = activity.getUser();
|
||||
if (optionalUser.isPresent()) {
|
||||
backgroundJobManager.startImmediateCalendarBackup(optionalUser.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -337,12 +403,15 @@ public class ContactsBackupFragment extends FileFragment implements DatePickerDi
|
|||
User user = optionalUser.get();
|
||||
if (enabled) {
|
||||
backgroundJobManager.schedulePeriodicContactsBackup(user);
|
||||
backgroundJobManager.schedulePeriodicCalendarBackup(user);
|
||||
} else {
|
||||
backgroundJobManager.cancelPeriodicContactsBackup(user);
|
||||
backgroundJobManager.cancelPeriodicCalendarBackup(user);
|
||||
}
|
||||
|
||||
arbitraryDataProvider.storeOrUpdateKeyValue(user.getAccountName(), PREFERENCE_CONTACTS_AUTOMATIC_BACKUP,
|
||||
String.valueOf(enabled));
|
||||
arbitraryDataProvider.storeOrUpdateKeyValue(user.getAccountName(),
|
||||
PREFERENCE_CONTACTS_AUTOMATIC_BACKUP,
|
||||
String.valueOf(enabled));
|
||||
}
|
||||
|
||||
private boolean checkAndAskForContactsReadPermission() {
|
||||
|
@ -352,58 +421,69 @@ public class ContactsBackupFragment extends FileFragment implements DatePickerDi
|
|||
if (PermissionUtil.checkSelfPermission(contactsPreferenceActivity, Manifest.permission.READ_CONTACTS)) {
|
||||
return true;
|
||||
} else {
|
||||
// Check if we should show an explanation
|
||||
if (PermissionUtil.shouldShowRequestPermissionRationale(contactsPreferenceActivity,
|
||||
android.Manifest.permission.READ_CONTACTS)) {
|
||||
// Show explanation to the user and then request permission
|
||||
Snackbar snackbar = DisplayUtils.createSnackbar(
|
||||
getView().findViewById(R.id.contacts_linear_layout),
|
||||
R.string.contacts_read_permission, Snackbar.LENGTH_INDEFINITE)
|
||||
.setAction(R.string.common_ok, v -> requestPermissions(
|
||||
new String[]{Manifest.permission.READ_CONTACTS},
|
||||
PermissionUtil.PERMISSIONS_READ_CONTACTS_AUTOMATIC)
|
||||
);
|
||||
|
||||
ThemeSnackbarUtils.colorSnackbar(contactsPreferenceActivity, snackbar);
|
||||
|
||||
snackbar.show();
|
||||
|
||||
return false;
|
||||
} else {
|
||||
// No explanation needed, request the permission.
|
||||
requestPermissions(new String[]{Manifest.permission.READ_CONTACTS},
|
||||
PermissionUtil.PERMISSIONS_READ_CONTACTS_AUTOMATIC);
|
||||
return false;
|
||||
}
|
||||
// No explanation needed, request the permission.
|
||||
requestPermissions(new String[]{Manifest.permission.READ_CONTACTS},
|
||||
PermissionUtil.PERMISSIONS_READ_CONTACTS_AUTOMATIC);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkAndAskForCalendarReadPermission() {
|
||||
final ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
|
||||
|
||||
// check permissions
|
||||
if (PermissionUtil.checkSelfPermission(contactsPreferenceActivity, Manifest.permission.READ_CALENDAR)) {
|
||||
return true;
|
||||
} else {
|
||||
// No explanation needed, request the permission.
|
||||
requestPermissions(new String[]{Manifest.permission.READ_CALENDAR},
|
||||
PermissionUtil.PERMISSIONS_READ_CALENDAR_AUTOMATIC);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkBackupNowPermission() {
|
||||
return (checkCalendarBackupPermission() && binding.calendar.isChecked()) ||
|
||||
(checkContactBackupPermission() && binding.contacts.isChecked());
|
||||
}
|
||||
|
||||
private boolean checkCalendarBackupPermission() {
|
||||
return PermissionUtil.checkSelfPermission(getContext(), Manifest.permission.READ_CALENDAR);
|
||||
}
|
||||
|
||||
private boolean checkContactBackupPermission() {
|
||||
return PermissionUtil.checkSelfPermission(getContext(), Manifest.permission.READ_CONTACTS);
|
||||
}
|
||||
|
||||
public void openCleanDate() {
|
||||
openDate(null);
|
||||
if (checkAndAskForCalendarReadPermission() && checkAndAskForContactsReadPermission()) {
|
||||
openDate(null);
|
||||
}
|
||||
}
|
||||
|
||||
public void openDate(@Nullable Date savedDate) {
|
||||
final ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
|
||||
|
||||
String backupFolderString = getResources().getString(R.string.contacts_backup_folder) + OCFile.PATH_SEPARATOR;
|
||||
OCFile backupFolder = contactsPreferenceActivity.getStorageManager().getFileByPath(backupFolderString);
|
||||
if (contactsPreferenceActivity == null) {
|
||||
Toast.makeText(getContext(), getString(R.string.error_choosing_date), Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
List<OCFile> backupFiles = contactsPreferenceActivity.getStorageManager().getFolderContent(backupFolder,
|
||||
false);
|
||||
String contactsBackupFolderString =
|
||||
getResources().getString(R.string.contacts_backup_folder) + OCFile.PATH_SEPARATOR;
|
||||
String calendarBackupFolderString =
|
||||
getResources().getString(R.string.calendar_backup_folder) + OCFile.PATH_SEPARATOR;
|
||||
|
||||
Collections.sort(backupFiles, new Comparator<OCFile>() {
|
||||
@Override
|
||||
public int compare(OCFile o1, OCFile o2) {
|
||||
if (o1.getModificationTimestamp() == o2.getModificationTimestamp()) {
|
||||
return 0;
|
||||
}
|
||||
FileDataStorageManager storageManager = contactsPreferenceActivity.getStorageManager();
|
||||
|
||||
if (o1.getModificationTimestamp() > o2.getModificationTimestamp()) {
|
||||
return 1;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
OCFile contactsBackupFolder = storageManager.getFileByDecryptedRemotePath(contactsBackupFolderString);
|
||||
OCFile calendarBackupFolder = storageManager.getFileByDecryptedRemotePath(calendarBackupFolderString);
|
||||
|
||||
List<OCFile> backupFiles = storageManager.getFolderContent(contactsBackupFolder, false);
|
||||
backupFiles.addAll(storageManager.getFolderContent(calendarBackupFolder, false));
|
||||
|
||||
Collections.sort(backupFiles, (o1, o2) -> {
|
||||
return Long.compare(o1.getModificationTimestamp(), o2.getModificationTimestamp());
|
||||
});
|
||||
|
||||
Calendar cal = Calendar.getInstance();
|
||||
|
@ -427,12 +507,7 @@ public class ContactsBackupFragment extends FileFragment implements DatePickerDi
|
|||
.getModificationTimestamp());
|
||||
datePickerDialog.getDatePicker().setMinDate(backupFiles.get(0).getModificationTimestamp());
|
||||
|
||||
datePickerDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
|
||||
@Override
|
||||
public void onDismiss(DialogInterface dialog) {
|
||||
selectedDate = null;
|
||||
}
|
||||
});
|
||||
datePickerDialog.setOnDismissListener(dialog -> selectedDate = null);
|
||||
|
||||
datePickerDialog.setTitle("");
|
||||
datePickerDialog.show();
|
||||
|
@ -480,12 +555,26 @@ public class ContactsBackupFragment extends FileFragment implements DatePickerDi
|
|||
@Override
|
||||
public void onDateSet(DatePicker view, int year, int month, int dayOfMonth) {
|
||||
final ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
|
||||
|
||||
if (contactsPreferenceActivity == null) {
|
||||
Toast.makeText(getContext(), getString(R.string.error_choosing_date), Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
selectedDate = new Date(year, month, dayOfMonth);
|
||||
|
||||
String backupFolderString = getResources().getString(R.string.contacts_backup_folder) + OCFile.PATH_SEPARATOR;
|
||||
OCFile backupFolder = contactsPreferenceActivity.getStorageManager().getFileByPath(backupFolderString);
|
||||
List<OCFile> backupFiles = contactsPreferenceActivity.getStorageManager().getFolderContent(
|
||||
backupFolder, false);
|
||||
String contactsBackupFolderString =
|
||||
getResources().getString(R.string.contacts_backup_folder) + OCFile.PATH_SEPARATOR;
|
||||
String calendarBackupFolderString =
|
||||
getResources().getString(R.string.calendar_backup_folder) + OCFile.PATH_SEPARATOR;
|
||||
|
||||
FileDataStorageManager storageManager = contactsPreferenceActivity.getStorageManager();
|
||||
|
||||
OCFile contactsBackupFolder = storageManager.getFileByDecryptedRemotePath(contactsBackupFolderString);
|
||||
OCFile calendarBackupFolder = storageManager.getFileByDecryptedRemotePath(calendarBackupFolderString);
|
||||
|
||||
List<OCFile> backupFiles = storageManager.getFolderContent(contactsBackupFolder, false);
|
||||
backupFiles.addAll(storageManager.getFolderContent(calendarBackupFolder, false));
|
||||
|
||||
// find file with modification with date and time between 00:00 and 23:59
|
||||
// if more than one file exists, take oldest
|
||||
|
@ -498,38 +587,57 @@ public class ContactsBackupFragment extends FileFragment implements DatePickerDi
|
|||
date.set(Calendar.SECOND, 1);
|
||||
date.set(Calendar.MILLISECOND, 0);
|
||||
date.set(Calendar.AM_PM, Calendar.AM);
|
||||
Long start = date.getTimeInMillis();
|
||||
long start = date.getTimeInMillis();
|
||||
|
||||
// end
|
||||
date.set(Calendar.HOUR, 23);
|
||||
date.set(Calendar.MINUTE, 59);
|
||||
date.set(Calendar.SECOND, 59);
|
||||
Long end = date.getTimeInMillis();
|
||||
long end = date.getTimeInMillis();
|
||||
|
||||
OCFile backupToRestore = null;
|
||||
OCFile contactsBackupToRestore = null;
|
||||
List<OCFile> calendarBackupsToRestore = new ArrayList<>();
|
||||
|
||||
for (OCFile file : backupFiles) {
|
||||
if (start < file.getModificationTimestamp() && end > file.getModificationTimestamp()) {
|
||||
if (backupToRestore == null) {
|
||||
backupToRestore = file;
|
||||
} else if (backupToRestore.getModificationTimestamp() < file.getModificationTimestamp()) {
|
||||
backupToRestore = file;
|
||||
// contact
|
||||
if (MimeTypeUtil.isVCard(file)) {
|
||||
if (contactsBackupToRestore == null) {
|
||||
contactsBackupToRestore = file;
|
||||
} else if (contactsBackupToRestore.getModificationTimestamp() < file.getModificationTimestamp()) {
|
||||
contactsBackupToRestore = file;
|
||||
}
|
||||
}
|
||||
|
||||
// calendars
|
||||
if (MimeTypeUtil.isCalendar(file)) {
|
||||
calendarBackupsToRestore.add(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (backupToRestore != null) {
|
||||
List<OCFile> backupToRestore = new ArrayList<>();
|
||||
|
||||
if (contactsBackupToRestore != null) {
|
||||
backupToRestore.add(contactsBackupToRestore);
|
||||
}
|
||||
|
||||
backupToRestore.addAll(calendarBackupsToRestore);
|
||||
|
||||
|
||||
if (backupToRestore.isEmpty()) {
|
||||
DisplayUtils.showSnackMessage(getView().findViewById(R.id.contacts_linear_layout),
|
||||
R.string.contacts_preferences_no_file_found);
|
||||
} else {
|
||||
final User user = contactsPreferenceActivity.getUser().orElseThrow(RuntimeException::new);
|
||||
Fragment contactListFragment = ContactListFragment.newInstance(backupToRestore, user);
|
||||
OCFile[] files = new OCFile[backupToRestore.size()];
|
||||
Fragment contactListFragment = BackupListFragment.newInstance(backupToRestore.toArray(files), user);
|
||||
|
||||
contactsPreferenceActivity.getSupportFragmentManager().
|
||||
beginTransaction()
|
||||
.replace(R.id.frame_container, contactListFragment, ContactListFragment.TAG)
|
||||
.addToBackStack(ContactsPreferenceActivity.BACKUP_TO_LIST)
|
||||
.commit();
|
||||
} else {
|
||||
DisplayUtils.showSnackMessage(getView().findViewById(R.id.contacts_linear_layout),
|
||||
R.string.contacts_preferences_no_file_found);
|
||||
beginTransaction()
|
||||
.replace(R.id.frame_container, contactListFragment, BackupListFragment.TAG)
|
||||
.addToBackStack(ContactsPreferenceActivity.BACKUP_TO_LIST)
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,403 @@
|
|||
/*
|
||||
*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Tobias Kaminsky
|
||||
* Copyright (C) 2021 Tobias Kaminsky
|
||||
* Copyright (C) 2021 Nextcloud GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.owncloud.android.ui.fragment.contactsbackup
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.database.Cursor
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.provider.ContactsContract
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.CheckedTextView
|
||||
import android.widget.ImageView
|
||||
import com.afollestad.sectionedrecyclerview.SectionedRecyclerViewAdapter
|
||||
import com.afollestad.sectionedrecyclerview.SectionedViewHolder
|
||||
import com.bumptech.glide.request.animation.GlideAnimation
|
||||
import com.bumptech.glide.request.target.SimpleTarget
|
||||
import com.nextcloud.client.account.UserAccountManager
|
||||
import com.nextcloud.client.network.ClientFactory
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.databinding.BackupListItemHeaderBinding
|
||||
import com.owncloud.android.databinding.CalendarlistListItemBinding
|
||||
import com.owncloud.android.databinding.ContactlistListItemBinding
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
import com.owncloud.android.ui.TextDrawable
|
||||
import com.owncloud.android.ui.fragment.contactsbackup.BackupListFragment.getDisplayName
|
||||
import com.owncloud.android.utils.BitmapUtils
|
||||
import com.owncloud.android.utils.DisplayUtils
|
||||
import com.owncloud.android.utils.theme.ThemeColorUtils
|
||||
import ezvcard.VCard
|
||||
import ezvcard.property.Photo
|
||||
import third_parties.sufficientlysecure.AndroidCalendar
|
||||
|
||||
@Suppress("LongParameterList", "TooManyFunctions")
|
||||
class BackupListAdapter(
|
||||
val accountManager: UserAccountManager,
|
||||
val clientFactory: ClientFactory,
|
||||
private val checkedVCards: HashSet<Int> = HashSet(),
|
||||
private val checkedCalendars: HashMap<String, Int> = HashMap(),
|
||||
val backupListFragment: BackupListFragment,
|
||||
val context: Context
|
||||
) : SectionedRecyclerViewAdapter<SectionedViewHolder>() {
|
||||
private val calendarFiles = arrayListOf<OCFile>()
|
||||
private val contacts = arrayListOf<VCard>()
|
||||
private var availableContactAccounts = listOf<ContactsAccount>()
|
||||
|
||||
companion object {
|
||||
const val SECTION_CALENDAR = 0
|
||||
const val SECTION_CONTACTS = 1
|
||||
|
||||
const val VIEW_TYPE_CALENDAR = 2
|
||||
const val VIEW_TYPE_CONTACTS = 3
|
||||
|
||||
const val SINGLE_SELECTION = 1
|
||||
|
||||
const val SINGLE_ACCOUNT = 1
|
||||
}
|
||||
|
||||
init {
|
||||
shouldShowHeadersForEmptySections(false)
|
||||
shouldShowFooters(false)
|
||||
availableContactAccounts = getAccountForImport()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SectionedViewHolder {
|
||||
return when (viewType) {
|
||||
VIEW_TYPE_HEADER -> {
|
||||
BackupListHeaderViewHolder(
|
||||
BackupListItemHeaderBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
),
|
||||
context
|
||||
)
|
||||
}
|
||||
VIEW_TYPE_CONTACTS -> {
|
||||
ContactItemViewHolder(
|
||||
ContactlistListItemBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
)
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
CalendarItemViewHolder(
|
||||
CalendarlistListItemBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
),
|
||||
context
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: SectionedViewHolder?,
|
||||
section: Int,
|
||||
relativePosition: Int,
|
||||
absolutePosition: Int
|
||||
) {
|
||||
if (section == SECTION_CALENDAR) {
|
||||
bindCalendarViewHolder(holder as CalendarItemViewHolder, relativePosition)
|
||||
}
|
||||
|
||||
if (section == SECTION_CONTACTS) {
|
||||
bindContactViewHolder(holder as ContactItemViewHolder, relativePosition)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(section: Int): Int {
|
||||
return if (section == SECTION_CALENDAR) {
|
||||
calendarFiles.size
|
||||
} else {
|
||||
contacts.size
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSectionCount(): Int {
|
||||
return 2
|
||||
}
|
||||
|
||||
override fun getItemViewType(section: Int, relativePosition: Int, absolutePosition: Int): Int {
|
||||
return if (section == SECTION_CALENDAR) {
|
||||
VIEW_TYPE_CALENDAR
|
||||
} else {
|
||||
VIEW_TYPE_CONTACTS
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindHeaderViewHolder(holder: SectionedViewHolder?, section: Int, expanded: Boolean) {
|
||||
val headerViewHolder = holder as BackupListHeaderViewHolder
|
||||
|
||||
headerViewHolder.binding.name.setTextColor(ThemeColorUtils.primaryColor(context))
|
||||
|
||||
if (section == SECTION_CALENDAR) {
|
||||
headerViewHolder.binding.name.text = context.resources.getString(R.string.calendars)
|
||||
headerViewHolder.binding.spinner.visibility = View.GONE
|
||||
} else {
|
||||
headerViewHolder.binding.name.text = context.resources.getString(R.string.contacts)
|
||||
if (checkedVCards.isNotEmpty()) {
|
||||
headerViewHolder.binding.spinner.visibility = View.VISIBLE
|
||||
|
||||
holder.binding.spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||
backupListFragment.setSelectedAccount(availableContactAccounts[position])
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {
|
||||
backupListFragment.setSelectedAccount(null)
|
||||
}
|
||||
}
|
||||
|
||||
headerViewHolder.setContactsAccount(availableContactAccounts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindFooterViewHolder(holder: SectionedViewHolder?, section: Int) {
|
||||
// not needed
|
||||
}
|
||||
|
||||
fun addCalendar(file: OCFile) {
|
||||
calendarFiles.add(file)
|
||||
notifyItemInserted(calendarFiles.size - 1)
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun replaceVcards(vCards: MutableList<VCard>) {
|
||||
contacts.clear()
|
||||
contacts.addAll(vCards)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun bindContactViewHolder(holder: ContactItemViewHolder, position: Int) {
|
||||
val vCard = contacts[position]
|
||||
|
||||
setChecked(checkedVCards.contains(position), holder.binding.name)
|
||||
|
||||
holder.binding.name.text = getDisplayName(vCard)
|
||||
|
||||
// photo
|
||||
if (vCard.photos.size > 0) {
|
||||
setPhoto(holder.binding.icon, vCard.photos[0])
|
||||
} else {
|
||||
try {
|
||||
holder.binding.icon.setImageDrawable(
|
||||
TextDrawable.createNamedAvatar(
|
||||
holder.binding.name.text.toString(),
|
||||
context.resources.getDimension(R.dimen.list_item_avatar_icon_radius)
|
||||
)
|
||||
)
|
||||
} catch (e: Resources.NotFoundException) {
|
||||
holder.binding.icon.setImageResource(R.drawable.ic_user)
|
||||
}
|
||||
}
|
||||
|
||||
holder.setVCardListener { toggleVCard(holder, position) }
|
||||
}
|
||||
|
||||
private fun setChecked(checked: Boolean, checkedTextView: CheckedTextView) {
|
||||
checkedTextView.isChecked = checked
|
||||
if (checked) {
|
||||
checkedTextView.checkMarkDrawable
|
||||
.setColorFilter(ThemeColorUtils.primaryColor(context), PorterDuff.Mode.SRC_ATOP)
|
||||
} else {
|
||||
checkedTextView.checkMarkDrawable.clearColorFilter()
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleVCard(holder: ContactItemViewHolder, position: Int) {
|
||||
holder.binding.name.isChecked = !holder.binding.name.isChecked
|
||||
if (holder.binding.name.isChecked) {
|
||||
holder.binding.name.checkMarkDrawable.setColorFilter(
|
||||
ThemeColorUtils.primaryColor(context),
|
||||
PorterDuff.Mode.SRC_ATOP
|
||||
)
|
||||
checkedVCards.add(position)
|
||||
} else {
|
||||
holder.binding.name.checkMarkDrawable.clearColorFilter()
|
||||
checkedVCards.remove(position)
|
||||
}
|
||||
|
||||
showRestoreButton()
|
||||
notifySectionChanged(SECTION_CONTACTS)
|
||||
}
|
||||
|
||||
private fun setPhoto(imageView: ImageView, firstPhoto: Photo) {
|
||||
val url = firstPhoto.url
|
||||
val data = firstPhoto.data
|
||||
if (data != null && data.isNotEmpty()) {
|
||||
val thumbnail = BitmapFactory.decodeByteArray(data, 0, data.size)
|
||||
val drawable = BitmapUtils.bitmapToCircularBitmapDrawable(
|
||||
context.resources,
|
||||
thumbnail
|
||||
)
|
||||
imageView.setImageDrawable(drawable)
|
||||
} else if (url != null) {
|
||||
val target = object : SimpleTarget<Drawable>() {
|
||||
override fun onResourceReady(resource: Drawable?, glideAnimation: GlideAnimation<in Drawable>?) {
|
||||
imageView.setImageDrawable(resource)
|
||||
}
|
||||
|
||||
override fun onLoadFailed(e: java.lang.Exception?, errorDrawable: Drawable?) {
|
||||
super.onLoadFailed(e, errorDrawable)
|
||||
imageView.setImageDrawable(errorDrawable)
|
||||
}
|
||||
}
|
||||
|
||||
DisplayUtils.downloadIcon(
|
||||
accountManager,
|
||||
clientFactory,
|
||||
context,
|
||||
url,
|
||||
target,
|
||||
R.drawable.ic_user,
|
||||
imageView.width,
|
||||
imageView.height
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindCalendarViewHolder(holder: CalendarItemViewHolder, position: Int) {
|
||||
val ocFile: OCFile = calendarFiles[position]
|
||||
|
||||
setChecked(checkedCalendars.containsValue(position), holder.binding.name)
|
||||
val name = ocFile.fileName
|
||||
val calendarName = name.substring(0, name.indexOf("_"))
|
||||
val date = name.substring(name.lastIndexOf("_") + 1).replace(".ics", "").replace("-", ":")
|
||||
holder.binding.name.text = context.resources.getString(R.string.calendar_name_linewrap, calendarName, date)
|
||||
holder.setCalendars(ArrayList(AndroidCalendar.loadAll(context.contentResolver)))
|
||||
holder.binding.spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, calendarPosition: Int, id: Long) {
|
||||
checkedCalendars[calendarFiles[position].storagePath] = calendarPosition
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {
|
||||
checkedCalendars[calendarFiles[position].storagePath] = -1
|
||||
}
|
||||
}
|
||||
|
||||
holder.setListener { toggleCalendar(holder, position) }
|
||||
}
|
||||
|
||||
private fun toggleCalendar(holder: CalendarItemViewHolder, position: Int) {
|
||||
val checkedTextView = holder.binding.name
|
||||
checkedTextView.isChecked = !checkedTextView.isChecked
|
||||
if (checkedTextView.isChecked) {
|
||||
checkedTextView.checkMarkDrawable.setColorFilter(
|
||||
ThemeColorUtils.primaryColor(context),
|
||||
PorterDuff.Mode.SRC_ATOP
|
||||
)
|
||||
holder.showCalendars(true)
|
||||
checkedCalendars[calendarFiles[position].storagePath] = 0
|
||||
} else {
|
||||
checkedTextView.checkMarkDrawable.clearColorFilter()
|
||||
checkedCalendars.remove(calendarFiles[position].storagePath)
|
||||
holder.showCalendars(false)
|
||||
}
|
||||
|
||||
showRestoreButton()
|
||||
}
|
||||
|
||||
private fun showRestoreButton() {
|
||||
val checkedEmpty = checkedCalendars.isEmpty() && checkedVCards.isEmpty()
|
||||
val noCalendarAvailable =
|
||||
checkedCalendars.isNotEmpty() && AndroidCalendar.loadAll(context.contentResolver).isEmpty()
|
||||
|
||||
if (checkedEmpty || noCalendarAvailable) {
|
||||
backupListFragment.showRestoreButton(false)
|
||||
} else {
|
||||
backupListFragment.showRestoreButton(true)
|
||||
}
|
||||
}
|
||||
|
||||
fun getCheckedCalendarStringArray(): Array<String> {
|
||||
return checkedCalendars.keys.toTypedArray()
|
||||
}
|
||||
|
||||
fun getCheckedContactsIntArray(): IntArray {
|
||||
return checkedVCards.toIntArray()
|
||||
}
|
||||
|
||||
fun selectAll(selectAll: Boolean) {
|
||||
if (selectAll) {
|
||||
contacts.forEachIndexed { index, _ -> checkedVCards.add(index) }
|
||||
} else {
|
||||
checkedVCards.clear()
|
||||
checkedCalendars.clear()
|
||||
}
|
||||
|
||||
showRestoreButton()
|
||||
}
|
||||
|
||||
fun getCheckedCalendarPathsArray(): Map<String, Int> {
|
||||
return checkedCalendars
|
||||
}
|
||||
|
||||
fun hasCalendarEntry(): Boolean {
|
||||
return calendarFiles.isNotEmpty()
|
||||
}
|
||||
|
||||
@Suppress("NestedBlockDepth", "TooGenericExceptionCaught")
|
||||
private fun getAccountForImport(): List<ContactsAccount> {
|
||||
val contactsAccounts = ArrayList<ContactsAccount>()
|
||||
|
||||
// add local one
|
||||
contactsAccounts.add(ContactsAccount("Local contacts", null, null))
|
||||
|
||||
var cursor: Cursor? = null
|
||||
try {
|
||||
cursor = context.contentResolver.query(
|
||||
ContactsContract.RawContacts.CONTENT_URI,
|
||||
arrayOf(
|
||||
ContactsContract.RawContacts.ACCOUNT_NAME,
|
||||
ContactsContract.RawContacts.ACCOUNT_TYPE
|
||||
),
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
if (cursor != null && cursor.count > 0) {
|
||||
while (cursor.moveToNext()) {
|
||||
val name = cursor.getString(cursor.getColumnIndex(ContactsContract.RawContacts.ACCOUNT_NAME))
|
||||
val type = cursor.getString(cursor.getColumnIndex(ContactsContract.RawContacts.ACCOUNT_TYPE))
|
||||
val account = ContactsAccount(name, name, type)
|
||||
if (!contactsAccounts.contains(account)) {
|
||||
contactsAccounts.add(account)
|
||||
}
|
||||
}
|
||||
cursor.close()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log_OC.d(BackupListFragment.TAG, e.message)
|
||||
} finally {
|
||||
cursor?.close()
|
||||
}
|
||||
|
||||
return contactsAccounts
|
||||
}
|
||||
}
|
|
@ -0,0 +1,490 @@
|
|||
/*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Tobias Kaminsky
|
||||
* Copyright (C) 2017 Tobias Kaminsky
|
||||
* Copyright (C) 2017 Nextcloud GmbH.
|
||||
* Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* <p>
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* at your option) any later version.
|
||||
* <p>
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* <p>
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.owncloud.android.ui.fragment.contactsbackup;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.nextcloud.client.account.User;
|
||||
import com.nextcloud.client.account.UserAccountManager;
|
||||
import com.nextcloud.client.di.Injectable;
|
||||
import com.nextcloud.client.files.downloader.DownloadRequest;
|
||||
import com.nextcloud.client.files.downloader.Request;
|
||||
import com.nextcloud.client.files.downloader.Transfer;
|
||||
import com.nextcloud.client.files.downloader.TransferManagerConnection;
|
||||
import com.nextcloud.client.files.downloader.TransferState;
|
||||
import com.nextcloud.client.jobs.BackgroundJobManager;
|
||||
import com.nextcloud.client.network.ClientFactory;
|
||||
import com.owncloud.android.R;
|
||||
import com.owncloud.android.databinding.BackuplistFragmentBinding;
|
||||
import com.owncloud.android.datamodel.OCFile;
|
||||
import com.owncloud.android.ui.activity.ContactsPreferenceActivity;
|
||||
import com.owncloud.android.ui.asynctasks.LoadContactsTask;
|
||||
import com.owncloud.android.ui.events.VCardToggleEvent;
|
||||
import com.owncloud.android.ui.fragment.FileFragment;
|
||||
import com.owncloud.android.utils.MimeTypeUtil;
|
||||
import com.owncloud.android.utils.PermissionUtil;
|
||||
import com.owncloud.android.utils.theme.ThemeColorUtils;
|
||||
import com.owncloud.android.utils.theme.ThemeToolbarUtils;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import ezvcard.VCard;
|
||||
import kotlin.Unit;
|
||||
|
||||
/**
|
||||
* This fragment shows all contacts or calendars from files and allows to import them.
|
||||
*/
|
||||
public class BackupListFragment extends FileFragment implements Injectable {
|
||||
public static final String TAG = BackupListFragment.class.getSimpleName();
|
||||
|
||||
public static final String FILE_NAMES = "FILE_NAMES";
|
||||
public static final String FILE_NAME = "FILE_NAME";
|
||||
public static final String USER = "USER";
|
||||
public static final String CHECKED_CALENDAR_ITEMS_ARRAY_KEY = "CALENDAR_CHECKED_ITEMS";
|
||||
public static final String CHECKED_CONTACTS_ITEMS_ARRAY_KEY = "CONTACTS_CHECKED_ITEMS";
|
||||
|
||||
private BackuplistFragmentBinding binding;
|
||||
|
||||
private BackupListAdapter listAdapter;
|
||||
private final List<VCard> vCards = new ArrayList<>();
|
||||
private final List<OCFile> ocFiles = new ArrayList<>();
|
||||
@Inject UserAccountManager accountManager;
|
||||
@Inject ClientFactory clientFactory;
|
||||
@Inject BackgroundJobManager backgroundJobManager;
|
||||
private TransferManagerConnection fileDownloader;
|
||||
private LoadContactsTask loadContactsTask = null;
|
||||
private ContactsAccount selectedAccount;
|
||||
|
||||
public static BackupListFragment newInstance(OCFile file, User user) {
|
||||
BackupListFragment frag = new BackupListFragment();
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putParcelable(FILE_NAME, file);
|
||||
arguments.putParcelable(USER, user);
|
||||
frag.setArguments(arguments);
|
||||
|
||||
return frag;
|
||||
}
|
||||
|
||||
public static BackupListFragment newInstance(OCFile[] files, User user) {
|
||||
BackupListFragment frag = new BackupListFragment();
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putParcelableArray(FILE_NAMES, files);
|
||||
arguments.putParcelable(USER, user);
|
||||
frag.setArguments(arguments);
|
||||
|
||||
return frag;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
inflater.inflate(R.menu.fragment_contact_list, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
|
||||
binding = BackuplistFragmentBinding.inflate(inflater, container, false);
|
||||
View view = binding.getRoot();
|
||||
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
|
||||
|
||||
if (contactsPreferenceActivity != null) {
|
||||
ActionBar actionBar = contactsPreferenceActivity.getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
ThemeToolbarUtils.setColoredTitle(actionBar, R.string.actionbar_calendar_contacts_restore, getContext());
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
contactsPreferenceActivity.setDrawerIndicatorEnabled(false);
|
||||
}
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
listAdapter = new BackupListAdapter(accountManager,
|
||||
clientFactory,
|
||||
new HashSet<>(),
|
||||
new HashMap<>(),
|
||||
this,
|
||||
requireContext());
|
||||
} else {
|
||||
HashMap<String, Integer> checkedCalendarItems = new HashMap<>();
|
||||
String[] checkedCalendarItemsArray = savedInstanceState.getStringArray(CHECKED_CALENDAR_ITEMS_ARRAY_KEY);
|
||||
if (checkedCalendarItemsArray != null) {
|
||||
for (String checkedItem : checkedCalendarItemsArray) {
|
||||
checkedCalendarItems.put(checkedItem, -1);
|
||||
}
|
||||
}
|
||||
if (checkedCalendarItems.size() > 0) {
|
||||
showRestoreButton(true);
|
||||
}
|
||||
|
||||
HashSet<Integer> checkedContactsItems = new HashSet<>();
|
||||
int[] checkedContactsItemsArray = savedInstanceState.getIntArray(CHECKED_CONTACTS_ITEMS_ARRAY_KEY);
|
||||
if (checkedContactsItemsArray != null) {
|
||||
for (int checkedItem : checkedContactsItemsArray) {
|
||||
checkedContactsItems.add(checkedItem);
|
||||
}
|
||||
}
|
||||
if (checkedContactsItems.size() > 0) {
|
||||
showRestoreButton(true);
|
||||
}
|
||||
|
||||
listAdapter = new BackupListAdapter(accountManager,
|
||||
clientFactory,
|
||||
checkedContactsItems,
|
||||
checkedCalendarItems,
|
||||
this,
|
||||
requireContext());
|
||||
}
|
||||
|
||||
binding.list.setAdapter(listAdapter);
|
||||
binding.list.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
|
||||
Bundle arguments = getArguments();
|
||||
if (arguments == null) {
|
||||
return view;
|
||||
}
|
||||
|
||||
if (arguments.getParcelable(FILE_NAME) != null) {
|
||||
ocFiles.add(arguments.getParcelable(FILE_NAME));
|
||||
} else if (arguments.getParcelableArray(FILE_NAMES) != null) {
|
||||
for (Parcelable file : arguments.getParcelableArray(FILE_NAMES)) {
|
||||
ocFiles.add((OCFile) file);
|
||||
}
|
||||
} else {
|
||||
return view;
|
||||
}
|
||||
|
||||
User user = getArguments().getParcelable(USER);
|
||||
fileDownloader = new TransferManagerConnection(getActivity(), user);
|
||||
fileDownloader.registerTransferListener(this::onDownloadUpdate);
|
||||
fileDownloader.bind();
|
||||
|
||||
for (OCFile file : ocFiles) {
|
||||
if (!file.isDown()) {
|
||||
Request request = new DownloadRequest(user, file);
|
||||
fileDownloader.enqueue(request);
|
||||
}
|
||||
|
||||
if (MimeTypeUtil.isVCard(file) && file.isDown()) {
|
||||
setFile(file);
|
||||
loadContactsTask = new LoadContactsTask(this, file);
|
||||
loadContactsTask.execute();
|
||||
}
|
||||
|
||||
if (MimeTypeUtil.isCalendar(file) && file.isDown()) {
|
||||
showLoadingMessage(false);
|
||||
listAdapter.addCalendar(file);
|
||||
}
|
||||
}
|
||||
|
||||
binding.restoreSelected.setOnClickListener(v -> {
|
||||
if (checkAndAskForCalendarWritePermission()) {
|
||||
importCalendar();
|
||||
}
|
||||
|
||||
if (listAdapter.getCheckedContactsIntArray().length > 0 && checkAndAskForContactsWritePermission()) {
|
||||
importContacts(selectedAccount);
|
||||
return;
|
||||
}
|
||||
|
||||
Snackbar
|
||||
.make(
|
||||
binding.list,
|
||||
R.string.contacts_preferences_import_scheduled,
|
||||
Snackbar.LENGTH_LONG
|
||||
)
|
||||
.show();
|
||||
|
||||
closeFragment();
|
||||
});
|
||||
|
||||
binding.restoreSelected.setTextColor(ThemeColorUtils.primaryAccentColor(getContext()));
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
super.onDetach();
|
||||
if (fileDownloader != null) {
|
||||
fileDownloader.unbind();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putStringArray(CHECKED_CALENDAR_ITEMS_ARRAY_KEY, listAdapter.getCheckedCalendarStringArray());
|
||||
outState.putIntArray(CHECKED_CONTACTS_ITEMS_ARRAY_KEY, listAdapter.getCheckedContactsIntArray());
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
public void onMessageEvent(VCardToggleEvent event) {
|
||||
if (event.showRestoreButton) {
|
||||
binding.contactlistRestoreSelectedContainer.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.contactlistRestoreSelectedContainer.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
public void showRestoreButton(boolean show) {
|
||||
binding.contactlistRestoreSelectedContainer.setVisibility(show ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
|
||||
contactsPreferenceActivity.setDrawerIndicatorEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
|
||||
contactsPreferenceActivity.setDrawerIndicatorEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
EventBus.getDefault().register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
EventBus.getDefault().unregister(this);
|
||||
if (loadContactsTask != null) {
|
||||
loadContactsTask.cancel(true);
|
||||
}
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
boolean retval;
|
||||
int itemId = item.getItemId();
|
||||
|
||||
if (itemId == android.R.id.home) {
|
||||
closeFragment();
|
||||
retval = true;
|
||||
} else if (itemId == R.id.action_select_all) {
|
||||
item.setChecked(!item.isChecked());
|
||||
setSelectAllMenuItem(item, item.isChecked());
|
||||
listAdapter.selectAll(item.isChecked());
|
||||
retval = true;
|
||||
} else {
|
||||
retval = super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
public void showLoadingMessage(boolean showIt) {
|
||||
binding.loadingListContainer.setVisibility(showIt ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
private void setSelectAllMenuItem(MenuItem selectAll, boolean checked) {
|
||||
selectAll.setChecked(checked);
|
||||
if (checked) {
|
||||
selectAll.setIcon(R.drawable.ic_select_none);
|
||||
} else {
|
||||
selectAll.setIcon(R.drawable.ic_select_all);
|
||||
}
|
||||
}
|
||||
|
||||
private void importContacts(ContactsAccount account) {
|
||||
backgroundJobManager.startImmediateContactsImport(account.getName(),
|
||||
account.getType(),
|
||||
getFile().getStoragePath(),
|
||||
listAdapter.getCheckedContactsIntArray());
|
||||
|
||||
Snackbar
|
||||
.make(
|
||||
binding.list,
|
||||
R.string.contacts_preferences_import_scheduled,
|
||||
Snackbar.LENGTH_LONG
|
||||
)
|
||||
.show();
|
||||
|
||||
closeFragment();
|
||||
}
|
||||
|
||||
private void importCalendar() {
|
||||
backgroundJobManager.startImmediateCalendarImport(listAdapter.getCheckedCalendarPathsArray());
|
||||
|
||||
Snackbar
|
||||
.make(
|
||||
binding.list,
|
||||
R.string.contacts_preferences_import_scheduled,
|
||||
Snackbar.LENGTH_LONG
|
||||
)
|
||||
.show();
|
||||
|
||||
closeFragment();
|
||||
}
|
||||
|
||||
private void closeFragment() {
|
||||
ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
|
||||
if (contactsPreferenceActivity != null) {
|
||||
contactsPreferenceActivity.onBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkAndAskForContactsWritePermission() {
|
||||
// check permissions
|
||||
if (!PermissionUtil.checkSelfPermission(getContext(), Manifest.permission.WRITE_CONTACTS)) {
|
||||
requestPermissions(new String[]{Manifest.permission.WRITE_CONTACTS},
|
||||
PermissionUtil.PERMISSIONS_WRITE_CONTACTS);
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkAndAskForCalendarWritePermission() {
|
||||
// check permissions
|
||||
if (!PermissionUtil.checkSelfPermission(getContext(), Manifest.permission.WRITE_CALENDAR)) {
|
||||
requestPermissions(new String[]{Manifest.permission.WRITE_CALENDAR},
|
||||
PermissionUtil.PERMISSIONS_WRITE_CALENDAR);
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
|
||||
if (requestCode == PermissionUtil.PERMISSIONS_WRITE_CONTACTS) {
|
||||
for (int index = 0; index < permissions.length; index++) {
|
||||
if (Manifest.permission.WRITE_CONTACTS.equalsIgnoreCase(permissions[index])) {
|
||||
if (grantResults[index] >= 0) {
|
||||
importContacts(selectedAccount);
|
||||
} else {
|
||||
if (getView() != null) {
|
||||
Snackbar.make(getView(), R.string.contactlist_no_permission, Snackbar.LENGTH_LONG)
|
||||
.show();
|
||||
} else {
|
||||
Toast.makeText(getContext(), R.string.contactlist_no_permission, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (requestCode == PermissionUtil.PERMISSIONS_WRITE_CALENDAR) {
|
||||
for (int index = 0; index < permissions.length; index++) {
|
||||
if (Manifest.permission.WRITE_CALENDAR.equalsIgnoreCase(permissions[index])) {
|
||||
if (grantResults[index] >= 0) {
|
||||
importContacts(selectedAccount);
|
||||
} else {
|
||||
if (getView() != null) {
|
||||
Snackbar.make(getView(), R.string.contactlist_no_permission, Snackbar.LENGTH_LONG)
|
||||
.show();
|
||||
} else {
|
||||
Toast.makeText(getContext(), R.string.contactlist_no_permission, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Unit onDownloadUpdate(Transfer download) {
|
||||
final Activity activity = getActivity();
|
||||
if (download.getState() == TransferState.COMPLETED && activity != null) {
|
||||
OCFile ocFile = download.getFile();
|
||||
|
||||
if (MimeTypeUtil.isVCard(ocFile)) {
|
||||
setFile(ocFile);
|
||||
loadContactsTask = new LoadContactsTask(this, ocFile);
|
||||
loadContactsTask.execute();
|
||||
}
|
||||
}
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
|
||||
public void loadVCards(List<VCard> cards) {
|
||||
showLoadingMessage(false);
|
||||
vCards.clear();
|
||||
vCards.addAll(cards);
|
||||
listAdapter.replaceVcards(vCards);
|
||||
}
|
||||
|
||||
public static String getDisplayName(VCard vCard) {
|
||||
if (vCard.getFormattedName() != null) {
|
||||
return vCard.getFormattedName().getValue();
|
||||
} else if (vCard.getTelephoneNumbers() != null && vCard.getTelephoneNumbers().size() > 0) {
|
||||
return vCard.getTelephoneNumbers().get(0).getText();
|
||||
} else if (vCard.getEmails() != null && vCard.getEmails().size() > 0) {
|
||||
return vCard.getEmails().get(0).getValue();
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
public boolean hasCalendarEntry() {
|
||||
return listAdapter.hasCalendarEntry();
|
||||
}
|
||||
|
||||
public void setSelectedAccount(ContactsAccount account) {
|
||||
selectedAccount = account;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Tobias Kaminsky
|
||||
* Copyright (C) 2021 Tobias Kaminsky
|
||||
* Copyright (C) 2021 Nextcloud GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.owncloud.android.ui.fragment.contactsbackup
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.ArrayAdapter
|
||||
import com.afollestad.sectionedrecyclerview.SectionedViewHolder
|
||||
import com.owncloud.android.databinding.BackupListItemHeaderBinding
|
||||
import java.util.ArrayList
|
||||
|
||||
class BackupListHeaderViewHolder(
|
||||
val binding: BackupListItemHeaderBinding,
|
||||
val context: Context
|
||||
) : SectionedViewHolder(binding.root) {
|
||||
val adapter = ArrayAdapter<ContactsAccount?>(
|
||||
context,
|
||||
android.R.layout.simple_spinner_dropdown_item,
|
||||
ArrayList()
|
||||
)
|
||||
|
||||
init {
|
||||
binding.spinner.adapter = adapter
|
||||
}
|
||||
|
||||
fun setContactsAccount(accounts: List<ContactsAccount>) {
|
||||
adapter.clear()
|
||||
adapter.addAll(accounts)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Tobias Kaminsky
|
||||
* Copyright (C) 2021 Tobias Kaminsky
|
||||
* Copyright (C) 2021 Nextcloud GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.owncloud.android.ui.fragment.contactsbackup
|
||||
|
||||
import com.afollestad.sectionedrecyclerview.SectionedViewHolder
|
||||
import com.owncloud.android.databinding.BackupListItemBinding
|
||||
|
||||
class BackupListItemViewHolder(val binding: BackupListItemBinding) : SectionedViewHolder(binding.root)
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Tobias Kaminsky
|
||||
* Copyright (C) 2021 Tobias Kaminsky
|
||||
* Copyright (C) 2021 Nextcloud GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.owncloud.android.ui.fragment.contactsbackup;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.afollestad.sectionedrecyclerview.SectionedViewHolder;
|
||||
import com.owncloud.android.R;
|
||||
import com.owncloud.android.databinding.CalendarlistListItemBinding;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import third_parties.sufficientlysecure.AndroidCalendar;
|
||||
|
||||
class CalendarItemViewHolder extends SectionedViewHolder {
|
||||
public CalendarlistListItemBinding binding;
|
||||
private final ArrayAdapter<AndroidCalendar> adapter;
|
||||
private final Context context;
|
||||
|
||||
CalendarItemViewHolder(CalendarlistListItemBinding binding, Context context) {
|
||||
super(binding.getRoot());
|
||||
|
||||
this.binding = binding;
|
||||
this.context = context;
|
||||
|
||||
adapter = new ArrayAdapter<>(context,
|
||||
android.R.layout.simple_spinner_dropdown_item,
|
||||
new ArrayList<>());
|
||||
|
||||
binding.spinner.setAdapter(adapter);
|
||||
}
|
||||
|
||||
public void setCalendars(ArrayList<AndroidCalendar> calendars) {
|
||||
adapter.clear();
|
||||
adapter.addAll(calendars);
|
||||
}
|
||||
|
||||
public void setListener(View.OnClickListener onClickListener) {
|
||||
itemView.setOnClickListener(onClickListener);
|
||||
}
|
||||
|
||||
public void showCalendars(boolean show) {
|
||||
if (show) {
|
||||
if (adapter.isEmpty()) {
|
||||
Toast.makeText(context,
|
||||
context.getResources().getString(R.string.no_calendar_exists),
|
||||
Toast.LENGTH_LONG)
|
||||
.show();
|
||||
} else {
|
||||
binding.spinner.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
binding.spinner.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Tobias Kaminsky
|
||||
* Copyright (C) 2021 Tobias Kaminsky
|
||||
* Copyright (C) 2021 Nextcloud GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.owncloud.android.ui.fragment.contactsbackup;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import com.afollestad.sectionedrecyclerview.SectionedViewHolder;
|
||||
import com.owncloud.android.databinding.ContactlistListItemBinding;
|
||||
|
||||
public class ContactItemViewHolder extends SectionedViewHolder {
|
||||
public ContactlistListItemBinding binding;
|
||||
|
||||
ContactItemViewHolder(ContactlistListItemBinding binding) {
|
||||
super(binding.getRoot());
|
||||
|
||||
this.binding = binding;
|
||||
binding.getRoot().setTag(this);
|
||||
}
|
||||
|
||||
public void setVCardListener(View.OnClickListener onClickListener) {
|
||||
itemView.setOnClickListener(onClickListener);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,250 @@
|
|||
/*
|
||||
*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Tobias Kaminsky
|
||||
* Copyright (C) 2021 Tobias Kaminsky
|
||||
* Copyright (C) 2021 Nextcloud GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.owncloud.android.ui.fragment.contactsbackup;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckedTextView;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.bumptech.glide.request.animation.GlideAnimation;
|
||||
import com.bumptech.glide.request.target.SimpleTarget;
|
||||
import com.nextcloud.client.account.UserAccountManager;
|
||||
import com.nextcloud.client.network.ClientFactory;
|
||||
import com.owncloud.android.R;
|
||||
import com.owncloud.android.databinding.ContactlistListItemBinding;
|
||||
import com.owncloud.android.ui.TextDrawable;
|
||||
import com.owncloud.android.ui.events.VCardToggleEvent;
|
||||
import com.owncloud.android.utils.BitmapUtils;
|
||||
import com.owncloud.android.utils.DisplayUtils;
|
||||
import com.owncloud.android.utils.theme.ThemeColorUtils;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.graphics.drawable.RoundedBitmapDrawable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import ezvcard.VCard;
|
||||
import ezvcard.property.Photo;
|
||||
|
||||
import static com.owncloud.android.ui.fragment.contactsbackup.BackupListFragment.getDisplayName;
|
||||
|
||||
class ContactListAdapter extends RecyclerView.Adapter<ContactItemViewHolder> {
|
||||
private static final int SINGLE_SELECTION = 1;
|
||||
|
||||
private List<VCard> vCards;
|
||||
private Set<Integer> checkedVCards;
|
||||
|
||||
private Context context;
|
||||
|
||||
private UserAccountManager accountManager;
|
||||
private ClientFactory clientFactory;
|
||||
|
||||
ContactListAdapter(UserAccountManager accountManager, ClientFactory clientFactory, Context context,
|
||||
List<VCard> vCards) {
|
||||
this.vCards = vCards;
|
||||
this.context = context;
|
||||
this.checkedVCards = new HashSet<>();
|
||||
this.accountManager = accountManager;
|
||||
this.clientFactory = clientFactory;
|
||||
}
|
||||
|
||||
ContactListAdapter(UserAccountManager accountManager,
|
||||
Context context,
|
||||
List<VCard> vCards,
|
||||
Set<Integer> checkedVCards) {
|
||||
this.vCards = vCards;
|
||||
this.context = context;
|
||||
this.checkedVCards = checkedVCards;
|
||||
this.accountManager = accountManager;
|
||||
}
|
||||
|
||||
public int getCheckedCount() {
|
||||
if (checkedVCards != null) {
|
||||
return checkedVCards.size();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void replaceVCards(List<VCard> vCards) {
|
||||
this.vCards = vCards;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public int[] getCheckedIntArray() {
|
||||
int[] intArray;
|
||||
if (checkedVCards != null && checkedVCards.size() > 0) {
|
||||
intArray = new int[checkedVCards.size()];
|
||||
int i = 0;
|
||||
for (int position : checkedVCards) {
|
||||
intArray[i] = position;
|
||||
i++;
|
||||
}
|
||||
return intArray;
|
||||
} else {
|
||||
return new int[0];
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ContactItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new ContactItemViewHolder(ContactlistListItemBinding.inflate(LayoutInflater.from(parent.getContext()),
|
||||
parent,
|
||||
false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull final ContactItemViewHolder holder, final int position) {
|
||||
final int verifiedPosition = holder.getAdapterPosition();
|
||||
final VCard vcard = vCards.get(verifiedPosition);
|
||||
|
||||
if (vcard != null) {
|
||||
|
||||
setChecked(checkedVCards.contains(position), holder.binding.name);
|
||||
|
||||
holder.binding.name.setText(getDisplayName(vcard));
|
||||
|
||||
// photo
|
||||
if (vcard.getPhotos().size() > 0) {
|
||||
setPhoto(holder.binding.icon, vcard.getPhotos().get(0));
|
||||
} else {
|
||||
try {
|
||||
holder.binding.icon.setImageDrawable(
|
||||
TextDrawable.createNamedAvatar(
|
||||
holder.binding.name.getText().toString(),
|
||||
context.getResources().getDimension(R.dimen.list_item_avatar_icon_radius)
|
||||
)
|
||||
);
|
||||
} catch (Exception e) {
|
||||
holder.binding.icon.setImageResource(R.drawable.ic_user);
|
||||
}
|
||||
}
|
||||
|
||||
holder.setVCardListener(v -> toggleVCard(holder, verifiedPosition));
|
||||
}
|
||||
}
|
||||
|
||||
private void setPhoto(ImageView imageView, Photo firstPhoto) {
|
||||
String url = firstPhoto.getUrl();
|
||||
byte[] data = firstPhoto.getData();
|
||||
|
||||
if (data != null && data.length > 0) {
|
||||
Bitmap thumbnail = BitmapFactory.decodeByteArray(data, 0, data.length);
|
||||
RoundedBitmapDrawable drawable = BitmapUtils.bitmapToCircularBitmapDrawable(context.getResources(),
|
||||
thumbnail);
|
||||
|
||||
imageView.setImageDrawable(drawable);
|
||||
} else if (url != null) {
|
||||
SimpleTarget target = new SimpleTarget<Drawable>() {
|
||||
@Override
|
||||
public void onResourceReady(Drawable resource, GlideAnimation glideAnimation) {
|
||||
imageView.setImageDrawable(resource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFailed(Exception e, Drawable errorDrawable) {
|
||||
super.onLoadFailed(e, errorDrawable);
|
||||
imageView.setImageDrawable(errorDrawable);
|
||||
}
|
||||
};
|
||||
DisplayUtils.downloadIcon(accountManager,
|
||||
clientFactory,
|
||||
context,
|
||||
url,
|
||||
target,
|
||||
R.drawable.ic_user,
|
||||
imageView.getWidth(),
|
||||
imageView.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
private void setChecked(boolean checked, CheckedTextView checkedTextView) {
|
||||
checkedTextView.setChecked(checked);
|
||||
|
||||
if (checked) {
|
||||
checkedTextView.getCheckMarkDrawable()
|
||||
.setColorFilter(ThemeColorUtils.primaryColor(context), PorterDuff.Mode.SRC_ATOP);
|
||||
} else {
|
||||
checkedTextView.getCheckMarkDrawable().clearColorFilter();
|
||||
}
|
||||
}
|
||||
|
||||
private void toggleVCard(ContactItemViewHolder holder, int verifiedPosition) {
|
||||
holder.binding.name.setChecked(!holder.binding.name.isChecked());
|
||||
|
||||
if (holder.binding.name.isChecked()) {
|
||||
holder.binding.name.getCheckMarkDrawable().setColorFilter(ThemeColorUtils.primaryColor(context),
|
||||
PorterDuff.Mode.SRC_ATOP);
|
||||
|
||||
checkedVCards.add(verifiedPosition);
|
||||
if (checkedVCards.size() == SINGLE_SELECTION) {
|
||||
EventBus.getDefault().post(new VCardToggleEvent(true));
|
||||
}
|
||||
} else {
|
||||
holder.binding.name.getCheckMarkDrawable().clearColorFilter();
|
||||
|
||||
checkedVCards.remove(verifiedPosition);
|
||||
|
||||
if (checkedVCards.isEmpty()) {
|
||||
EventBus.getDefault().post(new VCardToggleEvent(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return vCards.size();
|
||||
}
|
||||
|
||||
public void selectAllFiles(boolean select) {
|
||||
checkedVCards = new HashSet<>();
|
||||
if (select) {
|
||||
for (int i = 0; i < vCards.size(); i++) {
|
||||
checkedVCards.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (checkedVCards.size() > 0) {
|
||||
EventBus.getDefault().post(new VCardToggleEvent(true));
|
||||
} else {
|
||||
EventBus.getDefault().post(new VCardToggleEvent(false));
|
||||
}
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return getItemCount() == 0;
|
||||
}
|
||||
}
|
|
@ -1,735 +0,0 @@
|
|||
/*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Tobias Kaminsky
|
||||
* Copyright (C) 2017 Tobias Kaminsky
|
||||
* Copyright (C) 2017 Nextcloud GmbH.
|
||||
* Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* <p>
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* at your option) any later version.
|
||||
* <p>
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* <p>
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.owncloud.android.ui.fragment.contactsbackup;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.provider.ContactsContract;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.CheckedTextView;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.bumptech.glide.request.animation.GlideAnimation;
|
||||
import com.bumptech.glide.request.target.SimpleTarget;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.nextcloud.client.account.User;
|
||||
import com.nextcloud.client.account.UserAccountManager;
|
||||
import com.nextcloud.client.di.Injectable;
|
||||
import com.nextcloud.client.files.downloader.Direction;
|
||||
import com.nextcloud.client.files.downloader.DownloadRequest;
|
||||
import com.nextcloud.client.files.downloader.Request;
|
||||
import com.nextcloud.client.files.downloader.Transfer;
|
||||
import com.nextcloud.client.files.downloader.TransferManagerConnection;
|
||||
import com.nextcloud.client.files.downloader.TransferState;
|
||||
import com.nextcloud.client.jobs.BackgroundJobManager;
|
||||
import com.nextcloud.client.network.ClientFactory;
|
||||
import com.owncloud.android.R;
|
||||
import com.owncloud.android.databinding.ContactlistFragmentBinding;
|
||||
import com.owncloud.android.datamodel.OCFile;
|
||||
import com.owncloud.android.lib.common.utils.Log_OC;
|
||||
import com.owncloud.android.ui.TextDrawable;
|
||||
import com.owncloud.android.ui.activity.ContactsPreferenceActivity;
|
||||
import com.owncloud.android.ui.events.VCardToggleEvent;
|
||||
import com.owncloud.android.ui.fragment.FileFragment;
|
||||
import com.owncloud.android.utils.BitmapUtils;
|
||||
import com.owncloud.android.utils.DisplayUtils;
|
||||
import com.owncloud.android.utils.PermissionUtil;
|
||||
import com.owncloud.android.utils.theme.ThemeColorUtils;
|
||||
import com.owncloud.android.utils.theme.ThemeToolbarUtils;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.graphics.drawable.RoundedBitmapDrawable;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import ezvcard.Ezvcard;
|
||||
import ezvcard.VCard;
|
||||
import ezvcard.property.Photo;
|
||||
import kotlin.Unit;
|
||||
|
||||
import static com.owncloud.android.ui.fragment.contactsbackup.ContactListFragment.getDisplayName;
|
||||
|
||||
/**
|
||||
* This fragment shows all contacts from a file and allows to import them.
|
||||
*/
|
||||
public class ContactListFragment extends FileFragment implements Injectable {
|
||||
public static final String TAG = ContactListFragment.class.getSimpleName();
|
||||
|
||||
public static final String FILE_NAME = "FILE_NAME";
|
||||
public static final String USER = "USER";
|
||||
public static final String CHECKED_ITEMS_ARRAY_KEY = "CHECKED_ITEMS";
|
||||
|
||||
private static final int SINGLE_ACCOUNT = 1;
|
||||
|
||||
private ContactlistFragmentBinding binding;
|
||||
|
||||
private ContactListAdapter contactListAdapter;
|
||||
private final List<VCard> vCards = new ArrayList<>();
|
||||
private OCFile ocFile;
|
||||
@Inject UserAccountManager accountManager;
|
||||
@Inject ClientFactory clientFactory;
|
||||
@Inject BackgroundJobManager backgroundJobManager;
|
||||
private TransferManagerConnection fileDownloader;
|
||||
|
||||
public static ContactListFragment newInstance(OCFile file, User user) {
|
||||
ContactListFragment frag = new ContactListFragment();
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putParcelable(FILE_NAME, file);
|
||||
arguments.putParcelable(USER, user);
|
||||
frag.setArguments(arguments);
|
||||
return frag;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
inflater.inflate(R.menu.fragment_contact_list, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
|
||||
binding = ContactlistFragmentBinding.inflate(inflater, container, false);
|
||||
View view = binding.getRoot();
|
||||
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
|
||||
|
||||
if (contactsPreferenceActivity != null) {
|
||||
ActionBar actionBar = contactsPreferenceActivity.getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
ThemeToolbarUtils.setColoredTitle(actionBar, R.string.actionbar_contacts_restore, getContext());
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
contactsPreferenceActivity.setDrawerIndicatorEnabled(false);
|
||||
}
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
contactListAdapter = new ContactListAdapter(accountManager, clientFactory, getContext(), vCards);
|
||||
} else {
|
||||
Set<Integer> checkedItems = new HashSet<>();
|
||||
int[] itemsArray = savedInstanceState.getIntArray(CHECKED_ITEMS_ARRAY_KEY);
|
||||
if (itemsArray != null) {
|
||||
for (int checkedItem : itemsArray) {
|
||||
checkedItems.add(checkedItem);
|
||||
}
|
||||
}
|
||||
if (checkedItems.size() > 0) {
|
||||
onMessageEvent(new VCardToggleEvent(true));
|
||||
}
|
||||
contactListAdapter = new ContactListAdapter(accountManager, getContext(), vCards, checkedItems);
|
||||
}
|
||||
binding.contactlistRecyclerview.setAdapter(contactListAdapter);
|
||||
binding.contactlistRecyclerview.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
|
||||
ocFile = getArguments().getParcelable(FILE_NAME);
|
||||
setFile(ocFile);
|
||||
User user = getArguments().getParcelable(USER);
|
||||
fileDownloader = new TransferManagerConnection(getActivity(), user);
|
||||
fileDownloader.registerTransferListener(this::onDownloadUpdate);
|
||||
fileDownloader.bind();
|
||||
if (!ocFile.isDown()) {
|
||||
Request request = new DownloadRequest(user, ocFile);
|
||||
fileDownloader.enqueue(request);
|
||||
} else {
|
||||
loadContactsTask.execute();
|
||||
}
|
||||
|
||||
binding.contactlistRestoreSelected.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (checkAndAskForContactsWritePermission()) {
|
||||
getAccountForImport();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
binding.contactlistRestoreSelected.setTextColor(ThemeColorUtils.primaryAccentColor(getContext()));
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
super.onDetach();
|
||||
if (fileDownloader != null) {
|
||||
fileDownloader.unbind();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putIntArray(CHECKED_ITEMS_ARRAY_KEY, contactListAdapter.getCheckedIntArray());
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
public void onMessageEvent(VCardToggleEvent event) {
|
||||
if (event.showRestoreButton) {
|
||||
binding.contactlistRestoreSelectedContainer.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.contactlistRestoreSelectedContainer.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
|
||||
contactsPreferenceActivity.setDrawerIndicatorEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
|
||||
contactsPreferenceActivity.setDrawerIndicatorEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
EventBus.getDefault().register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
EventBus.getDefault().unregister(this);
|
||||
if (loadContactsTask != null) {
|
||||
loadContactsTask.cancel(true);
|
||||
}
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
boolean retval;
|
||||
int itemId = item.getItemId();
|
||||
|
||||
if (itemId == android.R.id.home) {
|
||||
ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
|
||||
if (contactsPreferenceActivity != null) {
|
||||
contactsPreferenceActivity.onBackPressed();
|
||||
}
|
||||
retval = true;
|
||||
} else if (itemId == R.id.action_select_all) {
|
||||
item.setChecked(!item.isChecked());
|
||||
setSelectAllMenuItem(item, item.isChecked());
|
||||
contactListAdapter.selectAllFiles(item.isChecked());
|
||||
retval = true;
|
||||
} else {
|
||||
retval = super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
private void setLoadingMessage() {
|
||||
binding.loadingListContainer.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
private void setSelectAllMenuItem(MenuItem selectAll, boolean checked) {
|
||||
selectAll.setChecked(checked);
|
||||
if (checked) {
|
||||
selectAll.setIcon(R.drawable.ic_select_none);
|
||||
} else {
|
||||
selectAll.setIcon(R.drawable.ic_select_all);
|
||||
}
|
||||
}
|
||||
|
||||
static class ContactItemViewHolder extends RecyclerView.ViewHolder {
|
||||
private ImageView badge;
|
||||
private CheckedTextView name;
|
||||
|
||||
ContactItemViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
|
||||
badge = itemView.findViewById(R.id.contactlist_item_icon);
|
||||
name = itemView.findViewById(R.id.contactlist_item_name);
|
||||
|
||||
|
||||
itemView.setTag(this);
|
||||
}
|
||||
|
||||
public void setVCardListener(View.OnClickListener onClickListener) {
|
||||
itemView.setOnClickListener(onClickListener);
|
||||
}
|
||||
|
||||
public ImageView getBadge() {
|
||||
return badge;
|
||||
}
|
||||
|
||||
public void setBadge(ImageView badge) {
|
||||
this.badge = badge;
|
||||
}
|
||||
|
||||
public CheckedTextView getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(CheckedTextView name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
private void importContacts(ContactsAccount account) {
|
||||
backgroundJobManager.startImmediateContactsImport(account.name,
|
||||
account.type,
|
||||
getFile().getStoragePath(),
|
||||
contactListAdapter.getCheckedIntArray());
|
||||
|
||||
Snackbar
|
||||
.make(
|
||||
binding.contactlistRecyclerview,
|
||||
R.string.contacts_preferences_import_scheduled,
|
||||
Snackbar.LENGTH_LONG
|
||||
)
|
||||
.show();
|
||||
|
||||
Handler handler = new Handler();
|
||||
handler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (getFragmentManager().getBackStackEntryCount() > 0) {
|
||||
getFragmentManager().popBackStack();
|
||||
} else {
|
||||
getActivity().finish();
|
||||
}
|
||||
}
|
||||
}, 1750);
|
||||
}
|
||||
|
||||
private void getAccountForImport() {
|
||||
final ArrayList<ContactsAccount> contactsAccounts = new ArrayList<>();
|
||||
|
||||
// add local one
|
||||
contactsAccounts.add(new ContactsAccount("Local contacts", null, null));
|
||||
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = getContext().getContentResolver().query(ContactsContract.RawContacts.CONTENT_URI,
|
||||
new String[]{ContactsContract.RawContacts.ACCOUNT_NAME, ContactsContract.RawContacts.ACCOUNT_TYPE},
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
|
||||
if (cursor != null && cursor.getCount() > 0) {
|
||||
while (cursor.moveToNext()) {
|
||||
String name = cursor.getString(cursor.getColumnIndex(ContactsContract.RawContacts.ACCOUNT_NAME));
|
||||
String type = cursor.getString(cursor.getColumnIndex(ContactsContract.RawContacts.ACCOUNT_TYPE));
|
||||
|
||||
ContactsAccount account = new ContactsAccount(name, name, type);
|
||||
|
||||
if (!contactsAccounts.contains(account)) {
|
||||
contactsAccounts.add(account);
|
||||
}
|
||||
}
|
||||
|
||||
cursor.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log_OC.d(TAG, e.getMessage());
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
if (contactsAccounts.size() == SINGLE_ACCOUNT) {
|
||||
importContacts(contactsAccounts.get(0));
|
||||
} else {
|
||||
ArrayAdapter adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_list_item_1, contactsAccounts);
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
|
||||
builder.setTitle(R.string.contactlist_account_chooser_title)
|
||||
.setAdapter(adapter, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
importContacts(contactsAccounts.get(which));
|
||||
}
|
||||
}).show();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkAndAskForContactsWritePermission() {
|
||||
// check permissions
|
||||
if (!PermissionUtil.checkSelfPermission(getContext(), Manifest.permission.WRITE_CONTACTS)) {
|
||||
requestPermissions(new String[]{Manifest.permission.WRITE_CONTACTS},
|
||||
PermissionUtil.PERMISSIONS_WRITE_CONTACTS);
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
|
||||
if (requestCode == PermissionUtil.PERMISSIONS_WRITE_CONTACTS) {
|
||||
for (int index = 0; index < permissions.length; index++) {
|
||||
if (Manifest.permission.WRITE_CONTACTS.equalsIgnoreCase(permissions[index])) {
|
||||
if (grantResults[index] >= 0) {
|
||||
getAccountForImport();
|
||||
} else {
|
||||
if (getView() != null) {
|
||||
Snackbar.make(getView(), R.string.contactlist_no_permission, Snackbar.LENGTH_LONG)
|
||||
.show();
|
||||
} else {
|
||||
Toast.makeText(getContext(), R.string.contactlist_no_permission, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ContactsAccount {
|
||||
private String displayName;
|
||||
private String name;
|
||||
private String type;
|
||||
|
||||
ContactsAccount(String displayName, String name, String type) {
|
||||
this.displayName = displayName;
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof ContactsAccount) {
|
||||
ContactsAccount other = (ContactsAccount) obj;
|
||||
return this.name.equalsIgnoreCase(other.name) && this.type.equalsIgnoreCase(other.type);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Arrays.hashCode(new Object[]{displayName, name, type});
|
||||
}
|
||||
}
|
||||
|
||||
private Unit onDownloadUpdate(Transfer download) {
|
||||
final Activity activity = getActivity();
|
||||
if (download.getState() == TransferState.COMPLETED && activity != null) {
|
||||
ocFile = download.getFile();
|
||||
loadContactsTask.execute();
|
||||
}
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
|
||||
public static class VCardComparator implements Comparator<VCard> {
|
||||
@Override
|
||||
public int compare(VCard o1, VCard o2) {
|
||||
String contac1 = getDisplayName(o1);
|
||||
String contac2 = getDisplayName(o2);
|
||||
|
||||
return contac1.compareToIgnoreCase(contac2);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private AsyncTask<Void, Void, Boolean> loadContactsTask = new AsyncTask<Void, Void, Boolean>() {
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
setLoadingMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... voids) {
|
||||
if (!isCancelled()) {
|
||||
File file = new File(ocFile.getStoragePath());
|
||||
try {
|
||||
vCards.addAll(Ezvcard.parse(file).all());
|
||||
Collections.sort(vCards, new VCardComparator());
|
||||
} catch (IOException e) {
|
||||
Log_OC.e(TAG, "IO Exception: " + file.getAbsolutePath());
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean bool) {
|
||||
if (!isCancelled()) {
|
||||
binding.loadingListContainer.setVisibility(View.GONE);
|
||||
contactListAdapter.replaceVCards(vCards);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public static String getDisplayName(VCard vCard) {
|
||||
if (vCard.getFormattedName() != null) {
|
||||
return vCard.getFormattedName().getValue();
|
||||
} else if (vCard.getTelephoneNumbers() != null && vCard.getTelephoneNumbers().size() > 0) {
|
||||
return vCard.getTelephoneNumbers().get(0).getText();
|
||||
} else if (vCard.getEmails() != null && vCard.getEmails().size() > 0) {
|
||||
return vCard.getEmails().get(0).getValue();
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
class ContactListAdapter extends RecyclerView.Adapter<ContactListFragment.ContactItemViewHolder> {
|
||||
private static final int SINGLE_SELECTION = 1;
|
||||
|
||||
private List<VCard> vCards;
|
||||
private Set<Integer> checkedVCards;
|
||||
|
||||
private Context context;
|
||||
|
||||
private UserAccountManager accountManager;
|
||||
private ClientFactory clientFactory;
|
||||
|
||||
ContactListAdapter(UserAccountManager accountManager, ClientFactory clientFactory, Context context,
|
||||
List<VCard> vCards) {
|
||||
this.vCards = vCards;
|
||||
this.context = context;
|
||||
this.checkedVCards = new HashSet<>();
|
||||
this.accountManager = accountManager;
|
||||
this.clientFactory = clientFactory;
|
||||
}
|
||||
|
||||
ContactListAdapter(UserAccountManager accountManager,
|
||||
Context context,
|
||||
List<VCard> vCards,
|
||||
Set<Integer> checkedVCards) {
|
||||
this.vCards = vCards;
|
||||
this.context = context;
|
||||
this.checkedVCards = checkedVCards;
|
||||
this.accountManager = accountManager;
|
||||
}
|
||||
|
||||
public int getCheckedCount() {
|
||||
if (checkedVCards != null) {
|
||||
return checkedVCards.size();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void replaceVCards(List<VCard> vCards) {
|
||||
this.vCards = vCards;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public int[] getCheckedIntArray() {
|
||||
int[] intArray;
|
||||
if (checkedVCards != null && checkedVCards.size() > 0) {
|
||||
intArray = new int[checkedVCards.size()];
|
||||
int i = 0;
|
||||
for (int position : checkedVCards) {
|
||||
intArray[i] = position;
|
||||
i++;
|
||||
}
|
||||
return intArray;
|
||||
} else {
|
||||
return new int[0];
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ContactListFragment.ContactItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.contactlist_list_item, parent, false);
|
||||
|
||||
return new ContactListFragment.ContactItemViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull final ContactListFragment.ContactItemViewHolder holder, final int position) {
|
||||
final int verifiedPosition = holder.getAdapterPosition();
|
||||
final VCard vcard = vCards.get(verifiedPosition);
|
||||
|
||||
if (vcard != null) {
|
||||
|
||||
setChecked(checkedVCards.contains(position), holder.getName());
|
||||
|
||||
holder.getName().setText(getDisplayName(vcard));
|
||||
|
||||
// photo
|
||||
if (vcard.getPhotos().size() > 0) {
|
||||
setPhoto(holder.getBadge(), vcard.getPhotos().get(0));
|
||||
} else {
|
||||
try {
|
||||
holder.getBadge().setImageDrawable(
|
||||
TextDrawable.createNamedAvatar(
|
||||
holder.getName().getText().toString(),
|
||||
context.getResources().getDimension(R.dimen.list_item_avatar_icon_radius)
|
||||
)
|
||||
);
|
||||
} catch (Exception e) {
|
||||
holder.getBadge().setImageResource(R.drawable.ic_user);
|
||||
}
|
||||
}
|
||||
|
||||
holder.setVCardListener(v -> toggleVCard(holder, verifiedPosition));
|
||||
}
|
||||
}
|
||||
|
||||
private void setPhoto(ImageView imageView, Photo firstPhoto) {
|
||||
String url = firstPhoto.getUrl();
|
||||
byte[] data = firstPhoto.getData();
|
||||
|
||||
if (data != null && data.length > 0) {
|
||||
Bitmap thumbnail = BitmapFactory.decodeByteArray(data, 0, data.length);
|
||||
RoundedBitmapDrawable drawable = BitmapUtils.bitmapToCircularBitmapDrawable(context.getResources(),
|
||||
thumbnail);
|
||||
|
||||
imageView.setImageDrawable(drawable);
|
||||
} else if (url != null) {
|
||||
SimpleTarget target = new SimpleTarget<Drawable>() {
|
||||
@Override
|
||||
public void onResourceReady(Drawable resource, GlideAnimation glideAnimation) {
|
||||
imageView.setImageDrawable(resource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFailed(Exception e, Drawable errorDrawable) {
|
||||
super.onLoadFailed(e, errorDrawable);
|
||||
imageView.setImageDrawable(errorDrawable);
|
||||
}
|
||||
};
|
||||
DisplayUtils.downloadIcon(accountManager,
|
||||
clientFactory,
|
||||
context,
|
||||
url,
|
||||
target,
|
||||
R.drawable.ic_user,
|
||||
imageView.getWidth(),
|
||||
imageView.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
private void setChecked(boolean checked, CheckedTextView checkedTextView) {
|
||||
checkedTextView.setChecked(checked);
|
||||
|
||||
if (checked) {
|
||||
checkedTextView.getCheckMarkDrawable()
|
||||
.setColorFilter(ThemeColorUtils.primaryColor(context), PorterDuff.Mode.SRC_ATOP);
|
||||
} else {
|
||||
checkedTextView.getCheckMarkDrawable().clearColorFilter();
|
||||
}
|
||||
}
|
||||
|
||||
private void toggleVCard(ContactListFragment.ContactItemViewHolder holder, int verifiedPosition) {
|
||||
holder.getName().setChecked(!holder.getName().isChecked());
|
||||
|
||||
if (holder.getName().isChecked()) {
|
||||
holder.getName().getCheckMarkDrawable().setColorFilter(ThemeColorUtils.primaryColor(context),
|
||||
PorterDuff.Mode.SRC_ATOP);
|
||||
|
||||
checkedVCards.add(verifiedPosition);
|
||||
if (checkedVCards.size() == SINGLE_SELECTION) {
|
||||
EventBus.getDefault().post(new VCardToggleEvent(true));
|
||||
}
|
||||
} else {
|
||||
holder.getName().getCheckMarkDrawable().clearColorFilter();
|
||||
|
||||
checkedVCards.remove(verifiedPosition);
|
||||
|
||||
if (checkedVCards.isEmpty()) {
|
||||
EventBus.getDefault().post(new VCardToggleEvent(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return vCards.size();
|
||||
}
|
||||
|
||||
public void selectAllFiles(boolean select) {
|
||||
checkedVCards = new HashSet<>();
|
||||
if (select) {
|
||||
for (int i = 0; i < vCards.size(); i++) {
|
||||
checkedVCards.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (checkedVCards.size() > 0) {
|
||||
EventBus.getDefault().post(new VCardToggleEvent(true));
|
||||
} else {
|
||||
EventBus.getDefault().post(new VCardToggleEvent(false));
|
||||
}
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Tobias Kaminsky
|
||||
* Copyright (C) 2021 Tobias Kaminsky
|
||||
* Copyright (C) 2021 Nextcloud GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.owncloud.android.ui.fragment.contactsbackup;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class ContactsAccount {
|
||||
private final String displayName;
|
||||
private final String name;
|
||||
private final String type;
|
||||
|
||||
ContactsAccount(String displayName, String name, String type) {
|
||||
this.displayName = displayName;
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof ContactsAccount) {
|
||||
ContactsAccount other = (ContactsAccount) obj;
|
||||
return this.name.equalsIgnoreCase(other.name) && this.type.equalsIgnoreCase(other.type);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Arrays.hashCode(new Object[]{displayName, name, type});
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Tobias Kaminsky
|
||||
* Copyright (C) 2021 Tobias Kaminsky
|
||||
* Copyright (C) 2021 Nextcloud GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.owncloud.android.ui.fragment.contactsbackup;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
import ezvcard.VCard;
|
||||
|
||||
public class VCardComparator implements Comparator<VCard> {
|
||||
@Override
|
||||
public int compare(VCard o1, VCard o2) {
|
||||
String contact1 = BackupListFragment.getDisplayName(o1);
|
||||
String contact2 = BackupListFragment.getDisplayName(o2);
|
||||
|
||||
return contact1.compareToIgnoreCase(contact2);
|
||||
}
|
||||
}
|
|
@ -328,6 +328,14 @@ public final class MimeTypeUtil {
|
|||
return isVCard(file.getMimeType()) || isVCard(getMimeTypeFromPath(file.getRemotePath()));
|
||||
}
|
||||
|
||||
public static boolean isCalendar(OCFile file) {
|
||||
return isCalendar(file.getMimeType()) || isCalendar(getMimeTypeFromPath(file.getRemotePath()));
|
||||
}
|
||||
|
||||
public static boolean isCalendar(String mimeType) {
|
||||
return "text/calendar".equalsIgnoreCase(mimeType);
|
||||
}
|
||||
|
||||
public static boolean isFolder(String mimeType) {
|
||||
return MimeType.DIRECTORY.equalsIgnoreCase(mimeType);
|
||||
}
|
||||
|
|
|
@ -13,9 +13,10 @@ import androidx.core.content.ContextCompat;
|
|||
public final class PermissionUtil {
|
||||
public static final int PERMISSIONS_WRITE_EXTERNAL_STORAGE = 1;
|
||||
public static final int PERMISSIONS_READ_CONTACTS_AUTOMATIC = 2;
|
||||
public static final int PERMISSIONS_READ_CONTACTS_MANUALLY = 3;
|
||||
public static final int PERMISSIONS_WRITE_CONTACTS = 4;
|
||||
public static final int PERMISSIONS_CAMERA = 5;
|
||||
public static final int PERMISSIONS_READ_CALENDAR_AUTOMATIC = 6;
|
||||
public static final int PERMISSIONS_WRITE_CALENDAR = 7;
|
||||
|
||||
private PermissionUtil() {
|
||||
// utility class -> private constructor
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Tobias Kaminsky
|
||||
* Copyright (C) 2021 Tobias Kaminsky
|
||||
* Copyright (C) 2021 Nextcloud GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package third_parties.sufficientlysecure;
|
||||
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentResolver;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.CalendarContract.Calendars;
|
||||
import android.provider.CalendarContract.Events;
|
||||
|
||||
import com.owncloud.android.lib.common.utils.Log_OC;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
public class AndroidCalendar {
|
||||
private static final String TAG = "ICS_AndroidCalendar";
|
||||
|
||||
public long mId;
|
||||
public String mIdStr;
|
||||
public String mName;
|
||||
public String mDisplayName;
|
||||
public String mAccountName;
|
||||
public String mAccountType;
|
||||
public String mOwner;
|
||||
public boolean mIsActive;
|
||||
public String mTimezone;
|
||||
public int mNumEntries;
|
||||
|
||||
private static final String[] CAL_COLS = new String[]{
|
||||
Calendars._ID,
|
||||
Calendars.DELETED,
|
||||
Calendars.NAME,
|
||||
Calendars.CALENDAR_DISPLAY_NAME,
|
||||
Calendars.ACCOUNT_NAME,
|
||||
Calendars.ACCOUNT_TYPE,
|
||||
Calendars.OWNER_ACCOUNT,
|
||||
Calendars.VISIBLE,
|
||||
Calendars.CALENDAR_TIME_ZONE};
|
||||
|
||||
private static final String[] CAL_ID_COLS = new String[]{Events._ID};
|
||||
private static final String CAL_ID_WHERE = Events.CALENDAR_ID + "=?";
|
||||
|
||||
// Load all available calendars.
|
||||
// If an empty list is returned the caller probably needs to enable calendar
|
||||
// read permissions in App Ops/XPrivacy etc.
|
||||
public static List<AndroidCalendar> loadAll(ContentResolver resolver) {
|
||||
|
||||
if (missing(resolver, Calendars.CONTENT_URI) ||
|
||||
missing(resolver, Events.CONTENT_URI)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
Cursor cur;
|
||||
try {
|
||||
cur = resolver.query(Calendars.CONTENT_URI, CAL_COLS, null, null, null);
|
||||
} catch (Exception except) {
|
||||
Log_OC.w(TAG, "Calendar provider is missing columns, continuing anyway");
|
||||
cur = resolver.query(Calendars.CONTENT_URI, null, null, null, null);
|
||||
}
|
||||
List<AndroidCalendar> calendars = new ArrayList<>(cur.getCount());
|
||||
|
||||
while (cur.moveToNext()) {
|
||||
if (getLong(cur, Calendars.DELETED) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
AndroidCalendar calendar = new AndroidCalendar();
|
||||
calendar.mId = getLong(cur, Calendars._ID);
|
||||
if (calendar.mId == -1) {
|
||||
continue;
|
||||
}
|
||||
calendar.mIdStr = getString(cur, Calendars._ID);
|
||||
calendar.mName = getString(cur, Calendars.NAME);
|
||||
calendar.mDisplayName = getString(cur, Calendars.CALENDAR_DISPLAY_NAME);
|
||||
calendar.mAccountName = getString(cur, Calendars.ACCOUNT_NAME);
|
||||
calendar.mAccountType = getString(cur, Calendars.ACCOUNT_TYPE);
|
||||
calendar.mOwner = getString(cur, Calendars.OWNER_ACCOUNT);
|
||||
calendar.mIsActive = getLong(cur, Calendars.VISIBLE) == 1;
|
||||
calendar.mTimezone = getString(cur, Calendars.CALENDAR_TIME_ZONE);
|
||||
|
||||
final String[] args = new String[]{calendar.mIdStr};
|
||||
Cursor eventsCur = resolver.query(Events.CONTENT_URI, CAL_ID_COLS, CAL_ID_WHERE, args, null);
|
||||
calendar.mNumEntries = eventsCur.getCount();
|
||||
eventsCur.close();
|
||||
calendars.add(calendar);
|
||||
}
|
||||
cur.close();
|
||||
|
||||
return calendars;
|
||||
}
|
||||
|
||||
private static int getColumnIndex(Cursor cur, String dbName) {
|
||||
return dbName == null ? -1 : cur.getColumnIndex(dbName);
|
||||
}
|
||||
|
||||
private static long getLong(Cursor cur, String dbName) {
|
||||
int i = getColumnIndex(cur, dbName);
|
||||
return i == -1 ? -1 : cur.getLong(i);
|
||||
}
|
||||
|
||||
private static String getString(Cursor cur, String dbName) {
|
||||
int i = getColumnIndex(cur, dbName);
|
||||
return i == -1 ? null : cur.getString(i);
|
||||
}
|
||||
|
||||
private static boolean missing(ContentResolver resolver, Uri uri) {
|
||||
// Determine if a provider is missing
|
||||
ContentProviderClient provider = resolver.acquireContentProviderClient(uri);
|
||||
if (provider != null) {
|
||||
provider.release();
|
||||
}
|
||||
return provider == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return mDisplayName + " (" + mIdStr + ")";
|
||||
}
|
||||
|
||||
private boolean differ(final String lhs, final String rhs) {
|
||||
if (lhs == null) {
|
||||
return rhs != null;
|
||||
}
|
||||
return rhs == null || !lhs.equals(rhs);
|
||||
}
|
||||
|
||||
public boolean differsFrom(AndroidCalendar other) {
|
||||
return mId != other.mId ||
|
||||
mIsActive != other.mIsActive ||
|
||||
mNumEntries != other.mNumEntries ||
|
||||
differ(mName, other.mName) ||
|
||||
differ(mDisplayName, other.mDisplayName) ||
|
||||
differ(mAccountName, other.mAccountName) ||
|
||||
differ(mAccountType, other.mAccountType) ||
|
||||
differ(mOwner, other.mOwner) ||
|
||||
differ(mTimezone, other.mTimezone);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Tobias Kaminsky
|
||||
* Copyright (C) 2021 Tobias Kaminsky
|
||||
* Copyright (C) 2021 Nextcloud GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package third_parties.sufficientlysecure;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
|
||||
public class CalendarSource {
|
||||
private static final String HTTP_SEP = "://";
|
||||
|
||||
private URL mUrl = null;
|
||||
private Uri mUri = null;
|
||||
private final String mString;
|
||||
private final String mUsername;
|
||||
private final String mPassword;
|
||||
private final Context context;
|
||||
|
||||
public CalendarSource(String url,
|
||||
Uri uri,
|
||||
String username,
|
||||
String password,
|
||||
Context context) throws MalformedURLException {
|
||||
if (url != null) {
|
||||
mUrl = new URL(url);
|
||||
mString = mUrl.toString();
|
||||
} else {
|
||||
mUri = uri;
|
||||
mString = uri.toString();
|
||||
}
|
||||
mUsername = username;
|
||||
mPassword = password;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public URLConnection getConnection() throws IOException {
|
||||
if (mUsername != null) {
|
||||
String protocol = mUrl.getProtocol();
|
||||
String userPass = mUsername + ":" + mPassword;
|
||||
|
||||
if (protocol.equalsIgnoreCase("ftp") || protocol.equalsIgnoreCase("ftps")) {
|
||||
String external = mUrl.toExternalForm();
|
||||
String end = external.substring(protocol.length() + HTTP_SEP.length());
|
||||
return new URL(protocol + HTTP_SEP + userPass + "@" + end).openConnection();
|
||||
}
|
||||
|
||||
if (protocol.equalsIgnoreCase("http") || protocol.equalsIgnoreCase("https")) {
|
||||
String encoded = new String(new Base64().encode(userPass.getBytes("UTF-8")));
|
||||
URLConnection connection = mUrl.openConnection();
|
||||
connection.setRequestProperty("Authorization", "Basic " + encoded);
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
return mUrl.openConnection();
|
||||
}
|
||||
|
||||
public InputStream getStream() throws IOException {
|
||||
if (mUri != null) {
|
||||
return context.getContentResolver().openInputStream(mUri);
|
||||
}
|
||||
URLConnection c = this.getConnection();
|
||||
return c == null ? null : c.getInputStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return mString;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Tobias Kaminsky
|
||||
* Copyright (C) 2021 Tobias Kaminsky
|
||||
* Copyright (C) 2021 Nextcloud GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package third_parties.sufficientlysecure;
|
||||
|
||||
public enum DuplicateHandlingEnum {
|
||||
DUP_REPLACE,
|
||||
DUP_REPLACE_ANY,
|
||||
DUP_IGNORE,
|
||||
DUP_DONT_CHECK,
|
||||
}
|
|
@ -0,0 +1,642 @@
|
|||
/*
|
||||
* Copyright (C) 2015 Jon Griffiths (jon_p_griffiths@yahoo.com)
|
||||
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2010-2011 Lukas Aichbauer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package third_parties.sufficientlysecure;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.database.Cursor;
|
||||
import android.net.MailTo;
|
||||
import android.net.ParseException;
|
||||
import android.net.Uri;
|
||||
import android.provider.CalendarContract.Events;
|
||||
import android.provider.CalendarContract.Reminders;
|
||||
import android.text.TextUtils;
|
||||
import android.text.format.DateUtils;
|
||||
|
||||
import com.nextcloud.client.preferences.AppPreferences;
|
||||
import com.owncloud.android.R;
|
||||
import com.owncloud.android.lib.common.utils.Log_OC;
|
||||
|
||||
import net.fortuna.ical4j.model.Calendar;
|
||||
import net.fortuna.ical4j.model.ComponentList;
|
||||
import net.fortuna.ical4j.model.DateTime;
|
||||
import net.fortuna.ical4j.model.Dur;
|
||||
import net.fortuna.ical4j.model.Parameter;
|
||||
import net.fortuna.ical4j.model.Property;
|
||||
import net.fortuna.ical4j.model.component.VAlarm;
|
||||
import net.fortuna.ical4j.model.component.VEvent;
|
||||
import net.fortuna.ical4j.model.parameter.FbType;
|
||||
import net.fortuna.ical4j.model.parameter.Related;
|
||||
import net.fortuna.ical4j.model.property.Action;
|
||||
import net.fortuna.ical4j.model.property.DateProperty;
|
||||
import net.fortuna.ical4j.model.property.Duration;
|
||||
import net.fortuna.ical4j.model.property.FreeBusy;
|
||||
import net.fortuna.ical4j.model.property.Transp;
|
||||
import net.fortuna.ical4j.model.property.Trigger;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
public class ProcessVEvent {
|
||||
private static final String TAG = "ICS_ProcessVEvent";
|
||||
|
||||
private static final Duration ONE_DAY = createDuration("P1D");
|
||||
private static final Duration ZERO_SECONDS = createDuration("PT0S");
|
||||
|
||||
private static final String[] EVENT_QUERY_COLUMNS = new String[]{Events.CALENDAR_ID, Events._ID};
|
||||
private static final int EVENT_QUERY_CALENDAR_ID_COL = 0;
|
||||
private static final int EVENT_QUERY_ID_COL = 1;
|
||||
|
||||
private final Calendar mICalCalendar;
|
||||
private final boolean mIsInserter;
|
||||
private final AndroidCalendar selectedCal;
|
||||
|
||||
private Context context;
|
||||
|
||||
@Inject AppPreferences preferences;
|
||||
|
||||
// UID generation
|
||||
long mUidMs = 0;
|
||||
String mUidTail = null;
|
||||
|
||||
private final class Options {
|
||||
private final List<Integer> mDefaultReminders;
|
||||
|
||||
public Options(Context context) {
|
||||
mDefaultReminders = new ArrayList<>(); // RemindersDialog.getSavedRemindersInMinutes(this); // TODO check
|
||||
mDefaultReminders.add(0);
|
||||
mDefaultReminders.add(5);
|
||||
mDefaultReminders.add(10);
|
||||
mDefaultReminders.add(30);
|
||||
mDefaultReminders.add(60);
|
||||
}
|
||||
|
||||
public List<Integer> getReminders(List<Integer> eventReminders) {
|
||||
if (eventReminders.size() > 0 && getImportReminders()) {
|
||||
return eventReminders;
|
||||
}
|
||||
return mDefaultReminders;
|
||||
}
|
||||
|
||||
public boolean getKeepUids() {
|
||||
return true; // upstream this is a setting // TODO check if we need to also have this as a setting
|
||||
}
|
||||
|
||||
private boolean getImportReminders() {
|
||||
return true; // upstream this is a setting // TODO check if we need to also have this as a setting
|
||||
}
|
||||
|
||||
private boolean getGlobalUids() {
|
||||
return false; // upstream this is a setting // TODO check if we need to also have this as a setting
|
||||
}
|
||||
|
||||
private boolean getTestFileSupport() {
|
||||
return false; // upstream this is a setting // TODO check if we need to also have this as a setting
|
||||
}
|
||||
|
||||
public DuplicateHandlingEnum getDuplicateHandling() {
|
||||
// return DuplicateHandlingEnum.values()[getEnumInt(PREF_DUPLICATE_HANDLING, 0)];
|
||||
return DuplicateHandlingEnum.values()[0]; // TODO is option needed?
|
||||
}
|
||||
|
||||
// private int getEnumInt(final String key, final int def) {
|
||||
// return Integer.parseInt(getString(key, String.valueOf(def)));
|
||||
// }
|
||||
}
|
||||
|
||||
public ProcessVEvent(Context context, Calendar iCalCalendar, AndroidCalendar selectedCal, boolean isInserter) {
|
||||
this.context = context;
|
||||
mICalCalendar = iCalCalendar;
|
||||
this.selectedCal = selectedCal;
|
||||
mIsInserter = isInserter;
|
||||
}
|
||||
|
||||
// TODO how to run?
|
||||
public void run() throws Exception {
|
||||
final Options options = new Options(context);
|
||||
List<Integer> reminders = new ArrayList<>();
|
||||
|
||||
ComponentList events = mICalCalendar.getComponents(VEvent.VEVENT);
|
||||
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
int numDel = 0;
|
||||
int numIns = 0;
|
||||
int numDups = 0;
|
||||
|
||||
ContentValues cAlarm = new ContentValues();
|
||||
cAlarm.put(Reminders.METHOD, Reminders.METHOD_ALERT);
|
||||
|
||||
final DuplicateHandlingEnum dupes = options.getDuplicateHandling();
|
||||
|
||||
Log_OC.i(TAG, (mIsInserter ? "Insert" : "Delete") + " for id " + selectedCal.mIdStr);
|
||||
Log_OC.d(TAG, "Duplication option is " + dupes.ordinal());
|
||||
|
||||
for (Object ve : events) {
|
||||
VEvent e = (VEvent) ve;
|
||||
Log_OC.d(TAG, "source event: " + e.toString());
|
||||
|
||||
if (e.getRecurrenceId() != null) {
|
||||
// FIXME: Support these edited instances
|
||||
Log_OC.w(TAG, "Ignoring edited instance of a recurring event");
|
||||
continue;
|
||||
}
|
||||
|
||||
long insertCalendarId = selectedCal.mId; // Calendar id to insert to
|
||||
|
||||
ContentValues c = convertToDB(e, options, reminders, selectedCal.mId);
|
||||
|
||||
Cursor cur = null;
|
||||
boolean mustDelete = !mIsInserter;
|
||||
|
||||
// Determine if we need to delete a duplicate event in order to update it
|
||||
if (!mustDelete && dupes != DuplicateHandlingEnum.DUP_DONT_CHECK) {
|
||||
|
||||
cur = query(resolver, options, c);
|
||||
while (!mustDelete && cur != null && cur.moveToNext()) {
|
||||
if (dupes == DuplicateHandlingEnum.DUP_REPLACE) {
|
||||
mustDelete = cur.getLong(EVENT_QUERY_CALENDAR_ID_COL) == selectedCal.mId;
|
||||
} else {
|
||||
mustDelete = true; // Replacing all (or ignoring, handled just below)
|
||||
}
|
||||
}
|
||||
|
||||
if (mustDelete) {
|
||||
if (dupes == DuplicateHandlingEnum.DUP_IGNORE) {
|
||||
Log_OC.i(TAG, "Avoiding inserting a duplicate event");
|
||||
numDups++;
|
||||
cur.close();
|
||||
continue;
|
||||
}
|
||||
cur.moveToPosition(-1); // Rewind for use below
|
||||
}
|
||||
}
|
||||
|
||||
if (mustDelete) {
|
||||
if (cur == null) {
|
||||
cur = query(resolver, options, c);
|
||||
}
|
||||
|
||||
while (cur != null && cur.moveToNext()) {
|
||||
long rowCalendarId = cur.getLong(EVENT_QUERY_CALENDAR_ID_COL);
|
||||
|
||||
if (dupes == DuplicateHandlingEnum.DUP_REPLACE
|
||||
&& rowCalendarId != selectedCal.mId) {
|
||||
Log_OC.i(TAG, "Avoiding deleting duplicate event in calendar " + rowCalendarId);
|
||||
continue; // Not in the destination calendar
|
||||
}
|
||||
|
||||
String id = cur.getString(EVENT_QUERY_ID_COL);
|
||||
Uri eventUri = Uri.withAppendedPath(Events.CONTENT_URI, id);
|
||||
numDel += resolver.delete(eventUri, null, null);
|
||||
String where = Reminders.EVENT_ID + "=?";
|
||||
resolver.delete(Reminders.CONTENT_URI, where, new String[]{id});
|
||||
if (mIsInserter && rowCalendarId != selectedCal.mId
|
||||
&& dupes == DuplicateHandlingEnum.DUP_REPLACE_ANY) {
|
||||
// Must update this event in the calendar this row came from
|
||||
Log_OC.i(TAG, "Changing calendar: " + rowCalendarId + " to " + insertCalendarId);
|
||||
insertCalendarId = rowCalendarId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cur != null) {
|
||||
cur.close();
|
||||
}
|
||||
|
||||
if (!mIsInserter) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Events.UID_2445 != null && !c.containsKey(Events.UID_2445)) {
|
||||
// Create a UID for this event to use. We create it here so if
|
||||
// exported multiple times it will always have the same id.
|
||||
c.put(Events.UID_2445, generateUid()); // TODO use
|
||||
}
|
||||
|
||||
c.put(Events.CALENDAR_ID, insertCalendarId);
|
||||
if (options.getTestFileSupport()) {
|
||||
processEventTests(e, c, reminders);
|
||||
numIns++;
|
||||
continue;
|
||||
}
|
||||
|
||||
Uri uri = insertAndLog(resolver, Events.CONTENT_URI, c, "Event");
|
||||
if (uri == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final long id = Long.parseLong(uri.getLastPathSegment());
|
||||
|
||||
for (int time : options.getReminders(reminders)) {
|
||||
cAlarm.put(Reminders.EVENT_ID, id);
|
||||
cAlarm.put(Reminders.MINUTES, time);
|
||||
insertAndLog(resolver, Reminders.CONTENT_URI, cAlarm, "Reminder");
|
||||
}
|
||||
numIns++;
|
||||
}
|
||||
|
||||
selectedCal.mNumEntries += numIns;
|
||||
selectedCal.mNumEntries -= numDel;
|
||||
|
||||
Resources res = context.getResources();
|
||||
int n = mIsInserter ? numIns : numDel;
|
||||
String msg = res.getQuantityString(R.plurals.processed_n_entries, n, n) + "\n";
|
||||
if (mIsInserter) {
|
||||
msg += "\n";
|
||||
if (options.getDuplicateHandling() == DuplicateHandlingEnum.DUP_DONT_CHECK) {
|
||||
msg += res.getString(R.string.did_not_check_for_dupes);
|
||||
} else {
|
||||
msg += res.getQuantityString(R.plurals.found_n_duplicates, numDups, numDups);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO show failure in starting context
|
||||
// DisplayUtils.showSnackMessage(context, msg);
|
||||
}
|
||||
|
||||
// Munge a VEvent so Android won't reject it, then convert to ContentValues for inserting
|
||||
private ContentValues convertToDB(VEvent e, Options options,
|
||||
List<Integer> reminders, long calendarId) {
|
||||
reminders.clear();
|
||||
|
||||
boolean allDay = false;
|
||||
boolean startIsDate = !(e.getStartDate().getDate() instanceof DateTime);
|
||||
boolean isRecurring = hasProperty(e, Property.RRULE) || hasProperty(e, Property.RDATE);
|
||||
|
||||
if (startIsDate) {
|
||||
// If the start date is a DATE we expect the end date to be a date too and the
|
||||
// event is all-day, midnight to midnight (RFC 2445).
|
||||
allDay = true;
|
||||
}
|
||||
|
||||
if (!hasProperty(e, Property.DTEND) && !hasProperty(e, Property.DURATION)) {
|
||||
// No end date or duration given.
|
||||
// Since we added a duration above when the start date is a DATE:
|
||||
// - The start date is a DATETIME, the event lasts no time at all (RFC 2445).
|
||||
e.getProperties().add(ZERO_SECONDS);
|
||||
// Zero time events are always free (RFC 2445), so override/set TRANSP accordingly.
|
||||
removeProperty(e, Property.TRANSP);
|
||||
e.getProperties().add(Transp.TRANSPARENT);
|
||||
}
|
||||
|
||||
if (isRecurring) {
|
||||
// Recurring event. Android insists on a duration.
|
||||
if (!hasProperty(e, Property.DURATION)) {
|
||||
// Calculate duration from start to end date
|
||||
Duration d = new Duration(e.getStartDate().getDate(), e.getEndDate().getDate());
|
||||
e.getProperties().add(d);
|
||||
}
|
||||
removeProperty(e, Property.DTEND);
|
||||
} else {
|
||||
// Non-recurring event. Android insists on an end date.
|
||||
if (!hasProperty(e, Property.DTEND)) {
|
||||
// Calculate end date from duration, set it and remove the duration.
|
||||
e.getProperties().add(e.getEndDate());
|
||||
}
|
||||
removeProperty(e, Property.DURATION);
|
||||
}
|
||||
|
||||
// Now calculate the db values for the event
|
||||
ContentValues c = new ContentValues();
|
||||
|
||||
c.put(Events.CALENDAR_ID, calendarId);
|
||||
copyProperty(c, Events.TITLE, e, Property.SUMMARY);
|
||||
copyProperty(c, Events.DESCRIPTION, e, Property.DESCRIPTION);
|
||||
|
||||
if (e.getOrganizer() != null) {
|
||||
URI uri = e.getOrganizer().getCalAddress();
|
||||
try {
|
||||
MailTo mailTo = MailTo.parse(uri.toString());
|
||||
c.put(Events.ORGANIZER, mailTo.getTo());
|
||||
c.put(Events.GUESTS_CAN_MODIFY, 1); // Ensure we can edit if not the organiser
|
||||
} catch (ParseException ignored) {
|
||||
Log_OC.e(TAG, "Failed to parse Organiser URI " + uri.toString());
|
||||
}
|
||||
}
|
||||
|
||||
copyProperty(c, Events.EVENT_LOCATION, e, Property.LOCATION);
|
||||
|
||||
if (hasProperty(e, Property.STATUS)) {
|
||||
String status = e.getProperty(Property.STATUS).getValue();
|
||||
switch (status) {
|
||||
case "TENTATIVE":
|
||||
c.put(Events.STATUS, Events.STATUS_TENTATIVE);
|
||||
break;
|
||||
case "CONFIRMED":
|
||||
c.put(Events.STATUS, Events.STATUS_CONFIRMED);
|
||||
break;
|
||||
case "CANCELLED": // NOTE: In ical4j it is CANCELLED with two L
|
||||
c.put(Events.STATUS, Events.STATUS_CANCELED);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
copyProperty(c, Events.DURATION, e, Property.DURATION);
|
||||
|
||||
if (allDay) {
|
||||
c.put(Events.ALL_DAY, 1);
|
||||
}
|
||||
|
||||
copyDateProperty(c, Events.DTSTART, Events.EVENT_TIMEZONE, e.getStartDate());
|
||||
if (hasProperty(e, Property.DTEND)) {
|
||||
copyDateProperty(c, Events.DTEND, Events.EVENT_END_TIMEZONE, e.getEndDate());
|
||||
}
|
||||
|
||||
if (hasProperty(e, Property.CLASS)) {
|
||||
String access = e.getProperty(Property.CLASS).getValue();
|
||||
int accessLevel = Events.ACCESS_DEFAULT;
|
||||
switch (access) {
|
||||
case "CONFIDENTIAL":
|
||||
accessLevel = Events.ACCESS_CONFIDENTIAL;
|
||||
break;
|
||||
case "PRIVATE":
|
||||
accessLevel = Events.ACCESS_PRIVATE;
|
||||
break;
|
||||
case "PUBLIC":
|
||||
accessLevel = Events.ACCESS_PUBLIC;
|
||||
break;
|
||||
}
|
||||
|
||||
c.put(Events.ACCESS_LEVEL, accessLevel);
|
||||
}
|
||||
|
||||
// Work out availability. This is confusing as FREEBUSY and TRANSP overlap.
|
||||
if (Events.AVAILABILITY != null) {
|
||||
int availability = Events.AVAILABILITY_BUSY;
|
||||
if (hasProperty(e, Property.TRANSP)) {
|
||||
if (e.getTransparency() == Transp.TRANSPARENT) {
|
||||
availability = Events.AVAILABILITY_FREE;
|
||||
}
|
||||
|
||||
} else if (hasProperty(e, Property.FREEBUSY)) {
|
||||
FreeBusy fb = (FreeBusy) e.getProperty(Property.FREEBUSY);
|
||||
FbType fbType = (FbType) fb.getParameter(Parameter.FBTYPE);
|
||||
if (fbType != null && fbType == FbType.FREE) {
|
||||
availability = Events.AVAILABILITY_FREE;
|
||||
} else if (fbType != null && fbType == FbType.BUSY_TENTATIVE) {
|
||||
availability = Events.AVAILABILITY_TENTATIVE;
|
||||
}
|
||||
}
|
||||
c.put(Events.AVAILABILITY, availability);
|
||||
}
|
||||
|
||||
copyProperty(c, Events.RRULE, e, Property.RRULE);
|
||||
copyProperty(c, Events.RDATE, e, Property.RDATE);
|
||||
copyProperty(c, Events.EXRULE, e, Property.EXRULE);
|
||||
copyProperty(c, Events.EXDATE, e, Property.EXDATE);
|
||||
copyProperty(c, Events.CUSTOM_APP_URI, e, Property.URL);
|
||||
copyProperty(c, Events.UID_2445, e, Property.UID);
|
||||
if (c.containsKey(Events.UID_2445) && TextUtils.isEmpty(c.getAsString(Events.UID_2445))) {
|
||||
// Remove null/empty UIDs
|
||||
c.remove(Events.UID_2445);
|
||||
}
|
||||
|
||||
for (Object alarm : e.getAlarms()) {
|
||||
VAlarm a = (VAlarm) alarm;
|
||||
|
||||
if (a.getAction() != Action.AUDIO && a.getAction() != Action.DISPLAY) {
|
||||
continue; // Ignore email and procedure alarms
|
||||
}
|
||||
|
||||
Trigger t = a.getTrigger();
|
||||
final long startMs = e.getStartDate().getDate().getTime();
|
||||
long alarmStartMs = startMs;
|
||||
long alarmMs;
|
||||
|
||||
// FIXME: - Support for repeating alarms
|
||||
// - Check the calendars max number of alarms
|
||||
if (t.getDateTime() != null) {
|
||||
alarmMs = t.getDateTime().getTime(); // Absolute
|
||||
} else if (t.getDuration() != null && t.getDuration().isNegative()) {
|
||||
Related rel = (Related) t.getParameter(Parameter.RELATED);
|
||||
if (rel != null && rel == Related.END) {
|
||||
alarmStartMs = e.getEndDate().getDate().getTime();
|
||||
}
|
||||
alarmMs = alarmStartMs - durationToMs(t.getDuration()); // Relative
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
int reminder = (int) ((startMs - alarmMs) / DateUtils.MINUTE_IN_MILLIS);
|
||||
if (reminder >= 0 && !reminders.contains(reminder)) {
|
||||
reminders.add(reminder);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.getReminders(reminders).size() > 0) {
|
||||
c.put(Events.HAS_ALARM, 1);
|
||||
}
|
||||
|
||||
// FIXME: Attendees, SELF_ATTENDEE_STATUS
|
||||
return c;
|
||||
}
|
||||
|
||||
private static Duration createDuration(String value) {
|
||||
Duration d = new Duration();
|
||||
d.setValue(value);
|
||||
return d;
|
||||
}
|
||||
|
||||
private static long durationToMs(Dur d) {
|
||||
long ms = 0;
|
||||
ms += d.getSeconds() * DateUtils.SECOND_IN_MILLIS;
|
||||
ms += d.getMinutes() * DateUtils.MINUTE_IN_MILLIS;
|
||||
ms += d.getHours() * DateUtils.HOUR_IN_MILLIS;
|
||||
ms += d.getDays() * DateUtils.DAY_IN_MILLIS;
|
||||
ms += d.getWeeks() * DateUtils.WEEK_IN_MILLIS;
|
||||
return ms;
|
||||
}
|
||||
|
||||
private boolean hasProperty(VEvent e, String name) {
|
||||
return e.getProperty(name) != null;
|
||||
}
|
||||
|
||||
private void removeProperty(VEvent e, String name) {
|
||||
Property p = e.getProperty(name);
|
||||
if (p != null) {
|
||||
e.getProperties().remove(p);
|
||||
}
|
||||
}
|
||||
|
||||
private void copyProperty(ContentValues c, String dbName, VEvent e, String evName) {
|
||||
if (dbName != null) {
|
||||
Property p = e.getProperty(evName);
|
||||
if (p != null) {
|
||||
c.put(dbName, p.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void copyDateProperty(ContentValues c, String dbName, String dbTzName, DateProperty date) {
|
||||
if (dbName != null && date.getDate() != null) {
|
||||
c.put(dbName, date.getDate().getTime()); // ms since epoc in GMT
|
||||
if (dbTzName != null) {
|
||||
if (date.isUtc() || date.getTimeZone() == null) {
|
||||
c.put(dbTzName, "UTC");
|
||||
} else {
|
||||
c.put(dbTzName, date.getTimeZone().getID());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Uri insertAndLog(ContentResolver resolver, Uri uri, ContentValues c, String type) {
|
||||
Log_OC.d(TAG, "Inserting " + type + " values: " + c);
|
||||
|
||||
Uri result = resolver.insert(uri, c);
|
||||
if (result == null) {
|
||||
Log_OC.e(TAG, "failed to insert " + type);
|
||||
Log_OC.e(TAG, "failed " + type + " values: " + c); // Not already logged, dump now
|
||||
} else {
|
||||
Log_OC.d(TAG, "Insert " + type + " returned " + result.toString());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Cursor queryEvents(ContentResolver resolver, StringBuilder b, List<String> argsList) {
|
||||
final String where = b.toString();
|
||||
final String[] args = argsList.toArray(new String[argsList.size()]);
|
||||
return resolver.query(Events.CONTENT_URI, EVENT_QUERY_COLUMNS, where, args, null);
|
||||
}
|
||||
|
||||
private Cursor query(ContentResolver resolver, Options options, ContentValues c) {
|
||||
|
||||
StringBuilder b = new StringBuilder();
|
||||
List<String> argsList = new ArrayList<>();
|
||||
|
||||
if (options.getKeepUids() && Events.UID_2445 != null && c.containsKey(Events.UID_2445)) {
|
||||
// Use our UID to query, either globally or per-calendar unique
|
||||
if (!options.getGlobalUids()) {
|
||||
b.append(Events.CALENDAR_ID).append("=? AND ");
|
||||
argsList.add(c.getAsString(Events.CALENDAR_ID));
|
||||
}
|
||||
b.append(Events.UID_2445).append("=?");
|
||||
argsList.add(c.getAsString(Events.UID_2445));
|
||||
return queryEvents(resolver, b, argsList);
|
||||
}
|
||||
|
||||
// Without UIDs, the best we can do is check the start date and title within
|
||||
// the current calendar, even though this may return false duplicates.
|
||||
if (!c.containsKey(Events.CALENDAR_ID) || !c.containsKey(Events.DTSTART)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
b.append(Events.CALENDAR_ID).append("=? AND ");
|
||||
b.append(Events.DTSTART).append("=? AND ");
|
||||
b.append(Events.TITLE);
|
||||
|
||||
argsList.add(c.getAsString(Events.CALENDAR_ID));
|
||||
argsList.add(c.getAsString(Events.DTSTART));
|
||||
|
||||
if (c.containsKey(Events.TITLE)) {
|
||||
b.append("=?");
|
||||
argsList.add(c.getAsString(Events.TITLE));
|
||||
} else {
|
||||
b.append(" is null");
|
||||
}
|
||||
|
||||
return queryEvents(resolver, b, argsList);
|
||||
}
|
||||
|
||||
private void checkTestValue(VEvent e, ContentValues c, String keyValue, String testName) {
|
||||
String[] parts = keyValue.split("=");
|
||||
String key = parts[0];
|
||||
String expected = parts.length > 1 ? parts[1] : "";
|
||||
String got = c.getAsString(key);
|
||||
|
||||
if (expected.equals("<non-null>") && got != null) {
|
||||
got = "<non-null>"; // Sentinel for testing present and non-null
|
||||
}
|
||||
if (got == null) {
|
||||
got = "<null>"; // Sentinel for testing not present values
|
||||
}
|
||||
|
||||
if (!expected.equals(got)) {
|
||||
Log_OC.e(TAG, " " + keyValue + " -> FAILED");
|
||||
Log_OC.e(TAG, " values: " + c);
|
||||
String error = "Test " + testName + " FAILED, expected '" + keyValue + "', got '" + got + "'";
|
||||
throw new RuntimeException(error);
|
||||
}
|
||||
Log_OC.i(TAG, " " + keyValue + " -> PASSED");
|
||||
}
|
||||
|
||||
private void processEventTests(VEvent e, ContentValues c, List<Integer> reminders) {
|
||||
|
||||
Property testName = e.getProperty("X-TEST-NAME");
|
||||
if (testName == null) {
|
||||
return; // Not a test case
|
||||
}
|
||||
|
||||
// This is a test event. Verify it using the embedded meta data.
|
||||
Log_OC.i(TAG, "Processing test case " + testName.getValue() + "...");
|
||||
|
||||
String reminderValues = "";
|
||||
String sep = "";
|
||||
for (Integer i : reminders) {
|
||||
reminderValues += sep + i;
|
||||
sep = ",";
|
||||
}
|
||||
c.put("reminders", reminderValues);
|
||||
|
||||
for (Object o : e.getProperties()) {
|
||||
Property p = (Property) o;
|
||||
switch (p.getName()) {
|
||||
case "X-TEST-VALUE":
|
||||
checkTestValue(e, c, p.getValue(), testName.getValue());
|
||||
break;
|
||||
case "X-TEST-MIN-VERSION":
|
||||
final int ver = Integer.parseInt(p.getValue());
|
||||
if (android.os.Build.VERSION.SDK_INT < ver) {
|
||||
Log_OC.e(TAG, " -> SKIPPED (MIN-VERSION < " + ver + ")");
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO move this to some common place
|
||||
private String generateUid() {
|
||||
// Generated UIDs take the form <ms>-<uuid>@nextcloud.com.
|
||||
if (mUidTail == null) {
|
||||
String uidPid = preferences.getUidPid();
|
||||
if (uidPid.length() == 0) {
|
||||
uidPid = UUID.randomUUID().toString().replace("-", "");
|
||||
preferences.setUidPid(uidPid);
|
||||
}
|
||||
mUidTail = uidPid + "@nextcloud.com";
|
||||
}
|
||||
|
||||
mUidMs = Math.max(mUidMs, System.currentTimeMillis());
|
||||
String uid = mUidMs + mUidTail;
|
||||
mUidMs++;
|
||||
|
||||
return uid;
|
||||
}
|
||||
}
|
620
src/main/java/third_parties/sufficientlysecure/SaveCalendar.java
Normal file
620
src/main/java/third_parties/sufficientlysecure/SaveCalendar.java
Normal file
|
@ -0,0 +1,620 @@
|
|||
/*
|
||||
* Copyright (C) 2015 Jon Griffiths (jon_p_griffiths@yahoo.com)
|
||||
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2010-2011 Lukas Aichbauer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package third_parties.sufficientlysecure;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.content.res.Resources;
|
||||
import android.database.Cursor;
|
||||
import android.database.DatabaseUtils;
|
||||
import android.net.Uri;
|
||||
import android.provider.CalendarContract;
|
||||
import android.provider.CalendarContract.Events;
|
||||
import android.provider.CalendarContract.Reminders;
|
||||
import android.text.TextUtils;
|
||||
import android.text.format.DateFormat;
|
||||
import android.text.format.DateUtils;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.EditText;
|
||||
|
||||
import com.nextcloud.client.account.User;
|
||||
import com.nextcloud.client.di.Injectable;
|
||||
import com.nextcloud.client.files.downloader.PostUploadAction;
|
||||
import com.nextcloud.client.files.downloader.Request;
|
||||
import com.nextcloud.client.files.downloader.TransferManagerConnection;
|
||||
import com.nextcloud.client.files.downloader.UploadRequest;
|
||||
import com.nextcloud.client.files.downloader.UploadTrigger;
|
||||
import com.nextcloud.client.preferences.AppPreferences;
|
||||
import com.owncloud.android.R;
|
||||
import com.owncloud.android.datamodel.OCFile;
|
||||
import com.owncloud.android.files.services.NameCollisionPolicy;
|
||||
import com.owncloud.android.lib.common.utils.Log_OC;
|
||||
|
||||
import net.fortuna.ical4j.data.CalendarOutputter;
|
||||
import net.fortuna.ical4j.model.Calendar;
|
||||
import net.fortuna.ical4j.model.Date;
|
||||
import net.fortuna.ical4j.model.DateTime;
|
||||
import net.fortuna.ical4j.model.Dur;
|
||||
import net.fortuna.ical4j.model.Period;
|
||||
import net.fortuna.ical4j.model.Property;
|
||||
import net.fortuna.ical4j.model.PropertyFactoryImpl;
|
||||
import net.fortuna.ical4j.model.PropertyList;
|
||||
import net.fortuna.ical4j.model.TimeZone;
|
||||
import net.fortuna.ical4j.model.TimeZoneRegistry;
|
||||
import net.fortuna.ical4j.model.TimeZoneRegistryFactory;
|
||||
import net.fortuna.ical4j.model.component.VAlarm;
|
||||
import net.fortuna.ical4j.model.component.VEvent;
|
||||
import net.fortuna.ical4j.model.parameter.FbType;
|
||||
import net.fortuna.ical4j.model.property.Action;
|
||||
import net.fortuna.ical4j.model.property.CalScale;
|
||||
import net.fortuna.ical4j.model.property.Description;
|
||||
import net.fortuna.ical4j.model.property.DtEnd;
|
||||
import net.fortuna.ical4j.model.property.DtStamp;
|
||||
import net.fortuna.ical4j.model.property.DtStart;
|
||||
import net.fortuna.ical4j.model.property.Duration;
|
||||
import net.fortuna.ical4j.model.property.FreeBusy;
|
||||
import net.fortuna.ical4j.model.property.Method;
|
||||
import net.fortuna.ical4j.model.property.Organizer;
|
||||
import net.fortuna.ical4j.model.property.ProdId;
|
||||
import net.fortuna.ical4j.model.property.Transp;
|
||||
import net.fortuna.ical4j.model.property.Version;
|
||||
import net.fortuna.ical4j.model.property.XProperty;
|
||||
import net.fortuna.ical4j.util.CompatibilityHints;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.text.ParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
public class SaveCalendar implements Injectable {
|
||||
private static final String TAG = "ICS_SaveCalendar";
|
||||
|
||||
private final PropertyFactoryImpl mPropertyFactory = PropertyFactoryImpl.getInstance();
|
||||
private TimeZoneRegistry mTzRegistry;
|
||||
private final Set<TimeZone> mInsertedTimeZones = new HashSet<>();
|
||||
private final Set<String> mFailedOrganisers = new HashSet<>();
|
||||
boolean mAllCols;
|
||||
private final Context activity;
|
||||
private final AndroidCalendar selectedCal;
|
||||
private final AppPreferences preferences;
|
||||
private final User user;
|
||||
|
||||
// UID generation
|
||||
long mUidMs = 0;
|
||||
String mUidTail = null;
|
||||
|
||||
private static final List<String> STATUS_ENUM = Arrays.asList("TENTATIVE", "CONFIRMED", "CANCELLED");
|
||||
private static final List<String> CLASS_ENUM = Arrays.asList(null, "CONFIDENTIAL", "PRIVATE", "PUBLIC");
|
||||
private static final List<String> AVAIL_ENUM = Arrays.asList(null, "FREE", "BUSY-TENTATIVE");
|
||||
|
||||
private static final String[] EVENT_COLS = new String[]{
|
||||
Events._ID, Events.ORIGINAL_ID, Events.UID_2445, Events.TITLE, Events.DESCRIPTION,
|
||||
Events.ORGANIZER, Events.EVENT_LOCATION, Events.STATUS, Events.ALL_DAY, Events.RDATE,
|
||||
Events.RRULE, Events.DTSTART, Events.EVENT_TIMEZONE, Events.DURATION, Events.DTEND,
|
||||
Events.EVENT_END_TIMEZONE, Events.ACCESS_LEVEL, Events.AVAILABILITY, Events.EXDATE,
|
||||
Events.EXRULE, Events.CUSTOM_APP_PACKAGE, Events.CUSTOM_APP_URI, Events.HAS_ALARM
|
||||
};
|
||||
|
||||
private static final String[] REMINDER_COLS = new String[]{
|
||||
Reminders.MINUTES, Reminders.METHOD
|
||||
};
|
||||
|
||||
public SaveCalendar(Context activity, AndroidCalendar calendar, AppPreferences preferences, User user) {
|
||||
this.activity = activity; // TODO rename
|
||||
this.selectedCal = calendar;
|
||||
this.preferences = preferences;
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
public void start() throws Exception {
|
||||
mInsertedTimeZones.clear();
|
||||
mFailedOrganisers.clear();
|
||||
mAllCols = false;
|
||||
|
||||
String file = selectedCal.mDisplayName + "_" +
|
||||
DateFormat.format("yyyy-MM-dd_HH-mm-ss", java.util.Calendar.getInstance()).toString() +
|
||||
".ics";
|
||||
|
||||
File fileName = new File(activity.getCacheDir(), file);
|
||||
|
||||
Log_OC.i(TAG, "Save id " + selectedCal.mIdStr + " to file " + fileName.getAbsolutePath());
|
||||
|
||||
String name = activity.getPackageName();
|
||||
String ver;
|
||||
try {
|
||||
ver = activity.getPackageManager().getPackageInfo(name, 0).versionName;
|
||||
} catch (NameNotFoundException e) {
|
||||
ver = "Unknown Build";
|
||||
}
|
||||
|
||||
String prodId = "-//" + selectedCal.mOwner + "//iCal Import/Export " + ver + "//EN";
|
||||
Calendar cal = new Calendar();
|
||||
cal.getProperties().add(new ProdId(prodId));
|
||||
cal.getProperties().add(Version.VERSION_2_0);
|
||||
cal.getProperties().add(Method.PUBLISH);
|
||||
cal.getProperties().add(CalScale.GREGORIAN);
|
||||
|
||||
if (selectedCal.mTimezone != null) {
|
||||
// We don't write any events with floating times, but export this
|
||||
// anyway so the default timezone for new events is correct when
|
||||
// the file is imported into a system that supports it.
|
||||
cal.getProperties().add(new XProperty("X-WR-TIMEZONE", selectedCal.mTimezone));
|
||||
}
|
||||
|
||||
// query events
|
||||
ContentResolver resolver = activity.getContentResolver();
|
||||
int numberOfCreatedUids = 0;
|
||||
if (Events.UID_2445 != null) {
|
||||
numberOfCreatedUids = ensureUids(activity, resolver, selectedCal);
|
||||
}
|
||||
boolean relaxed = true; // settings.getIcal4jValidationRelaxed(); // TODO is this option needed? default true
|
||||
CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_RELAXED_VALIDATION, relaxed);
|
||||
List<VEvent> events = getEvents(resolver, selectedCal, cal);
|
||||
|
||||
for (VEvent v : events) {
|
||||
cal.getComponents().add(v);
|
||||
}
|
||||
|
||||
new CalendarOutputter().output(cal, new FileOutputStream(fileName));
|
||||
|
||||
Resources res = activity.getResources();
|
||||
String msg = res.getQuantityString(R.plurals.wrote_n_events_to, events.size(), events.size(), file);
|
||||
if (numberOfCreatedUids > 0) {
|
||||
msg += "\n" + res.getQuantityString(R.plurals.created_n_uids_to, numberOfCreatedUids, numberOfCreatedUids);
|
||||
}
|
||||
|
||||
// TODO replace DisplayUtils.showSnackMessage(activity, msg);
|
||||
|
||||
upload(fileName);
|
||||
}
|
||||
|
||||
private int ensureUids(Context activity, ContentResolver resolver, AndroidCalendar cal) {
|
||||
String[] cols = new String[]{Events._ID};
|
||||
String[] args = new String[]{cal.mIdStr};
|
||||
Map<Long, String> newUids = new HashMap<>();
|
||||
Cursor cur = resolver.query(Events.CONTENT_URI, cols,
|
||||
Events.CALENDAR_ID + " = ? AND " + Events.UID_2445 + " IS NULL", args, null);
|
||||
while (cur.moveToNext()) {
|
||||
Long id = getLong(cur, Events._ID);
|
||||
String uid = generateUid();
|
||||
newUids.put(id, uid);
|
||||
}
|
||||
for (Long id : newUids.keySet()) {
|
||||
String uid = newUids.get(id);
|
||||
Uri updateUri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id);
|
||||
ContentValues c = new ContentValues();
|
||||
c.put(Events.UID_2445, uid);
|
||||
resolver.update(updateUri, c, null, null);
|
||||
Log_OC.i(TAG, "Generated UID " + uid + " for event " + id);
|
||||
}
|
||||
return newUids.size();
|
||||
}
|
||||
|
||||
private List<VEvent> getEvents(ContentResolver resolver, AndroidCalendar cal_src, Calendar cal_dst) {
|
||||
String where = Events.CALENDAR_ID + "=?";
|
||||
String[] args = new String[]{cal_src.mIdStr};
|
||||
String sortBy = Events.CALENDAR_ID + " ASC";
|
||||
Cursor cur;
|
||||
try {
|
||||
cur = resolver.query(Events.CONTENT_URI, mAllCols ? null : EVENT_COLS,
|
||||
where, args, sortBy);
|
||||
} catch (Exception except) {
|
||||
Log_OC.w(TAG, "Calendar provider is missing columns, continuing anyway");
|
||||
int n = 0;
|
||||
for (n = 0; n < EVENT_COLS.length; ++n) {
|
||||
if (EVENT_COLS[n] == null) {
|
||||
Log_OC.e(TAG, "Invalid EVENT_COLS index " + Integer.toString(n));
|
||||
}
|
||||
}
|
||||
cur = resolver.query(Events.CONTENT_URI, null, where, args, sortBy);
|
||||
}
|
||||
|
||||
DtStamp timestamp = new DtStamp(); // Same timestamp for all events
|
||||
|
||||
// Collect up events and add them after any timezones
|
||||
List<VEvent> events = new ArrayList<>();
|
||||
while (cur.moveToNext()) {
|
||||
VEvent e = convertFromDb(cur, cal_dst, timestamp);
|
||||
if (e != null) {
|
||||
events.add(e);
|
||||
Log_OC.d(TAG, "Adding event: " + e.toString());
|
||||
}
|
||||
}
|
||||
cur.close();
|
||||
return events;
|
||||
}
|
||||
|
||||
private String calculateFileName(final String displayName) {
|
||||
// Replace all non-alnum chars with '_'
|
||||
String stripped = displayName.replaceAll("[^a-zA-Z0-9_-]", "_");
|
||||
// Replace repeated '_' with a single '_'
|
||||
return stripped.replaceAll("(_)\\1{1,}", "$1");
|
||||
}
|
||||
|
||||
private void getFileImpl(final String previousFile, final String suggestedFile,
|
||||
final String[] result) {
|
||||
|
||||
final EditText input = new EditText(activity);
|
||||
input.setHint(R.string.destination_filename);
|
||||
input.setText(previousFile);
|
||||
input.selectAll();
|
||||
|
||||
final int ok = android.R.string.ok;
|
||||
final int cancel = android.R.string.cancel;
|
||||
final int suggest = R.string.suggest;
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
AlertDialog dlg = builder.setIcon(R.mipmap.ic_launcher)
|
||||
.setTitle(R.string.enter_destination_filename)
|
||||
.setView(input)
|
||||
.setPositiveButton(ok, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface iface, int id) {
|
||||
result[0] = input.getText().toString();
|
||||
}
|
||||
})
|
||||
.setNeutralButton(suggest, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface iface, int id) {
|
||||
}
|
||||
})
|
||||
.setNegativeButton(cancel, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface iface, int id) {
|
||||
result[0] = "";
|
||||
}
|
||||
})
|
||||
.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
||||
public void onCancel(DialogInterface iface) {
|
||||
result[0] = "";
|
||||
}
|
||||
})
|
||||
.create();
|
||||
int state = WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE;
|
||||
dlg.getWindow().setSoftInputMode(state);
|
||||
dlg.show();
|
||||
// Overriding 'Suggest' here prevents it from closing the dialog
|
||||
dlg.getButton(DialogInterface.BUTTON_NEUTRAL)
|
||||
.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View onClick) {
|
||||
input.setText(suggestedFile);
|
||||
input.setSelection(input.getText().length());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private VEvent convertFromDb(Cursor cur, Calendar cal, DtStamp timestamp) {
|
||||
Log_OC.d(TAG, "cursor: " + DatabaseUtils.dumpCurrentRowToString(cur));
|
||||
|
||||
if (hasStringValue(cur, Events.ORIGINAL_ID)) {
|
||||
// FIXME: Support these edited instances
|
||||
Log_OC.w(TAG, "Ignoring edited instance of a recurring event");
|
||||
return null;
|
||||
}
|
||||
|
||||
PropertyList l = new PropertyList();
|
||||
l.add(timestamp);
|
||||
copyProperty(l, Property.UID, cur, Events.UID_2445);
|
||||
|
||||
String summary = copyProperty(l, Property.SUMMARY, cur, Events.TITLE);
|
||||
String description = copyProperty(l, Property.DESCRIPTION, cur, Events.DESCRIPTION);
|
||||
|
||||
String organizer = getString(cur, Events.ORGANIZER);
|
||||
if (!TextUtils.isEmpty(organizer)) {
|
||||
// The check for mailto: here handles early versions of this code which
|
||||
// incorrectly left it in the organizer column.
|
||||
if (!organizer.startsWith("mailto:")) {
|
||||
organizer = "mailto:" + organizer;
|
||||
}
|
||||
try {
|
||||
l.add(new Organizer(organizer));
|
||||
} catch (URISyntaxException ignored) {
|
||||
if (!mFailedOrganisers.contains(organizer)) {
|
||||
Log_OC.e(TAG, "Failed to create mailTo for organizer " + organizer);
|
||||
mFailedOrganisers.add(organizer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
copyProperty(l, Property.LOCATION, cur, Events.EVENT_LOCATION);
|
||||
copyEnumProperty(l, Property.STATUS, cur, Events.STATUS, STATUS_ENUM);
|
||||
|
||||
boolean allDay = TextUtils.equals(getString(cur, Events.ALL_DAY), "1");
|
||||
boolean isTransparent;
|
||||
DtEnd dtEnd = null;
|
||||
|
||||
if (allDay) {
|
||||
// All day event
|
||||
isTransparent = true;
|
||||
Date start = getDateTime(cur, Events.DTSTART, null, null);
|
||||
Date end = getDateTime(cur, Events.DTEND, null, null);
|
||||
l.add(new DtStart(new Date(start)));
|
||||
|
||||
if (end != null) {
|
||||
dtEnd = new DtEnd(new Date(end));
|
||||
} else {
|
||||
dtEnd = new DtEnd(utcDateFromMs(start.getTime() + DateUtils.DAY_IN_MILLIS));
|
||||
}
|
||||
|
||||
l.add(dtEnd);
|
||||
} else {
|
||||
// Regular or zero-time event. Start date must be a date-time
|
||||
Date startDate = getDateTime(cur, Events.DTSTART, Events.EVENT_TIMEZONE, cal);
|
||||
l.add(new DtStart(startDate));
|
||||
|
||||
// Use duration if we have one, otherwise end date
|
||||
if (hasStringValue(cur, Events.DURATION)) {
|
||||
isTransparent = getString(cur, Events.DURATION).equals("PT0S");
|
||||
if (!isTransparent) {
|
||||
copyProperty(l, Property.DURATION, cur, Events.DURATION);
|
||||
}
|
||||
} else {
|
||||
String endTz = Events.EVENT_END_TIMEZONE;
|
||||
if (endTz == null) {
|
||||
endTz = Events.EVENT_TIMEZONE;
|
||||
}
|
||||
Date end = getDateTime(cur, Events.DTEND, endTz, cal);
|
||||
dtEnd = new DtEnd(end);
|
||||
isTransparent = startDate.getTime() == end.getTime();
|
||||
if (!isTransparent) {
|
||||
l.add(dtEnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
copyEnumProperty(l, Property.CLASS, cur, Events.ACCESS_LEVEL, CLASS_ENUM);
|
||||
|
||||
int availability = getInt(cur, Events.AVAILABILITY);
|
||||
if (availability > Events.AVAILABILITY_TENTATIVE) {
|
||||
availability = -1; // Unknown/Invalid
|
||||
}
|
||||
|
||||
if (isTransparent) {
|
||||
// This event is ordinarily transparent. If availability shows that its
|
||||
// not free, then mark it opaque.
|
||||
if (availability >= 0 && availability != Events.AVAILABILITY_FREE) {
|
||||
l.add(Transp.OPAQUE);
|
||||
}
|
||||
|
||||
} else if (availability > Events.AVAILABILITY_BUSY) {
|
||||
// This event is ordinarily busy but differs, so output a FREEBUSY
|
||||
// period covering the time of the event
|
||||
FreeBusy fb = new FreeBusy();
|
||||
fb.getParameters().add(new FbType(AVAIL_ENUM.get(availability)));
|
||||
DateTime start = new DateTime(((DtStart) l.getProperty(Property.DTSTART)).getDate());
|
||||
|
||||
if (dtEnd != null) {
|
||||
fb.getPeriods().add(new Period(start, new DateTime(dtEnd.getDate())));
|
||||
} else {
|
||||
Duration d = (Duration) l.getProperty(Property.DURATION);
|
||||
fb.getPeriods().add(new Period(start, d.getDuration()));
|
||||
}
|
||||
l.add(fb);
|
||||
}
|
||||
|
||||
copyProperty(l, Property.RRULE, cur, Events.RRULE);
|
||||
copyProperty(l, Property.RDATE, cur, Events.RDATE);
|
||||
copyProperty(l, Property.EXRULE, cur, Events.EXRULE);
|
||||
copyProperty(l, Property.EXDATE, cur, Events.EXDATE);
|
||||
if (TextUtils.isEmpty(getString(cur, Events.CUSTOM_APP_PACKAGE))) {
|
||||
// Only copy URL if there is no app i.e. we probably imported it.
|
||||
copyProperty(l, Property.URL, cur, Events.CUSTOM_APP_URI);
|
||||
}
|
||||
|
||||
VEvent e = new VEvent(l);
|
||||
|
||||
if (getInt(cur, Events.HAS_ALARM) == 1) {
|
||||
// Add alarms
|
||||
|
||||
String s = summary == null ? (description == null ? "" : description) : summary;
|
||||
Description desc = new Description(s);
|
||||
|
||||
ContentResolver resolver = activity.getContentResolver();
|
||||
long eventId = getLong(cur, Events._ID);
|
||||
Cursor alarmCur;
|
||||
alarmCur = Reminders.query(resolver, eventId, mAllCols ? null : REMINDER_COLS);
|
||||
while (alarmCur.moveToNext()) {
|
||||
int mins = getInt(alarmCur, Reminders.MINUTES);
|
||||
if (mins == -1) {
|
||||
mins = 60; // FIXME: Get the real default
|
||||
}
|
||||
|
||||
// FIXME: We should support other types if possible
|
||||
int method = getInt(alarmCur, Reminders.METHOD);
|
||||
if (method == Reminders.METHOD_DEFAULT || method == Reminders.METHOD_ALERT) {
|
||||
VAlarm alarm = new VAlarm(new Dur(0, 0, -mins, 0));
|
||||
alarm.getProperties().add(Action.DISPLAY);
|
||||
alarm.getProperties().add(desc);
|
||||
e.getAlarms().add(alarm);
|
||||
}
|
||||
}
|
||||
alarmCur.close();
|
||||
}
|
||||
|
||||
return e;
|
||||
}
|
||||
|
||||
private int getColumnIndex(Cursor cur, String dbName) {
|
||||
return dbName == null ? -1 : cur.getColumnIndex(dbName);
|
||||
}
|
||||
|
||||
private String getString(Cursor cur, String dbName) {
|
||||
int i = getColumnIndex(cur, dbName);
|
||||
return i == -1 ? null : cur.getString(i);
|
||||
}
|
||||
|
||||
private long getLong(Cursor cur, String dbName) {
|
||||
int i = getColumnIndex(cur, dbName);
|
||||
return i == -1 ? -1 : cur.getLong(i);
|
||||
}
|
||||
|
||||
private int getInt(Cursor cur, String dbName) {
|
||||
int i = getColumnIndex(cur, dbName);
|
||||
return i == -1 ? -1 : cur.getInt(i);
|
||||
}
|
||||
|
||||
private boolean hasStringValue(Cursor cur, String dbName) {
|
||||
int i = getColumnIndex(cur, dbName);
|
||||
return i != -1 && !TextUtils.isEmpty(cur.getString(i));
|
||||
}
|
||||
|
||||
private Date utcDateFromMs(long ms) {
|
||||
// This date will be UTC provided the default false value of the iCal4j property
|
||||
// "net.fortuna.ical4j.timezone.date.floating" has not been changed.
|
||||
return new Date(ms);
|
||||
}
|
||||
|
||||
private boolean isUtcTimeZone(final String tz) {
|
||||
if (TextUtils.isEmpty(tz)) {
|
||||
return true;
|
||||
}
|
||||
final String utz = tz.toUpperCase(Locale.US);
|
||||
return utz.equals("UTC") || utz.equals("UTC-0") || utz.equals("UTC+0") || utz.endsWith("/UTC");
|
||||
}
|
||||
|
||||
private Date getDateTime(Cursor cur, String dbName, String dbTzName, Calendar cal) {
|
||||
int i = getColumnIndex(cur, dbName);
|
||||
if (i == -1 || cur.isNull(i)) {
|
||||
Log_OC.e(TAG, "No valid " + dbName + " column found, index: " + Integer.toString(i));
|
||||
return null;
|
||||
}
|
||||
|
||||
if (cal == null) {
|
||||
return utcDateFromMs(cur.getLong(i)); // Ignore timezone for date-only dates
|
||||
} else if (dbTzName == null) {
|
||||
Log_OC.e(TAG, "No valid tz " + dbName + " column given");
|
||||
}
|
||||
|
||||
String tz = getString(cur, dbTzName);
|
||||
final boolean isUtc = isUtcTimeZone(tz);
|
||||
|
||||
DateTime dt = new DateTime(isUtc);
|
||||
if (dt.isUtc() != isUtc) {
|
||||
throw new RuntimeException("UTC mismatch after construction");
|
||||
}
|
||||
dt.setTime(cur.getLong(i));
|
||||
if (dt.isUtc() != isUtc) {
|
||||
throw new RuntimeException("UTC mismatch after setTime");
|
||||
}
|
||||
|
||||
if (!isUtc) {
|
||||
if (mTzRegistry == null) {
|
||||
mTzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry();
|
||||
if (mTzRegistry == null) {
|
||||
throw new RuntimeException("Failed to create TZ registry");
|
||||
}
|
||||
}
|
||||
TimeZone t = mTzRegistry.getTimeZone(tz);
|
||||
if (t == null) {
|
||||
Log_OC.e(TAG, "Unknown TZ " + tz + ", assuming UTC");
|
||||
} else {
|
||||
dt.setTimeZone(t);
|
||||
if (!mInsertedTimeZones.contains(t)) {
|
||||
cal.getComponents().add(t.getVTimeZone());
|
||||
mInsertedTimeZones.add(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
return dt;
|
||||
}
|
||||
|
||||
private String copyProperty(PropertyList l, String evName, Cursor cur, String dbName) {
|
||||
// None of the exceptions caught below should be able to be thrown AFAICS.
|
||||
try {
|
||||
String value = getString(cur, dbName);
|
||||
if (value != null) {
|
||||
Property p = mPropertyFactory.createProperty(evName);
|
||||
p.setValue(value);
|
||||
l.add(p);
|
||||
return value;
|
||||
}
|
||||
} catch (IOException | URISyntaxException | ParseException ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void copyEnumProperty(PropertyList l, String evName, Cursor cur, String dbName,
|
||||
List<String> vals) {
|
||||
// None of the exceptions caught below should be able to be thrown AFAICS.
|
||||
try {
|
||||
int i = getColumnIndex(cur, dbName);
|
||||
if (i != -1 && !cur.isNull(i)) {
|
||||
int value = (int) cur.getLong(i);
|
||||
if (value >= 0 && value < vals.size() && vals.get(value) != null) {
|
||||
Property p = mPropertyFactory.createProperty(evName);
|
||||
p.setValue(vals.get(value));
|
||||
l.add(p);
|
||||
}
|
||||
}
|
||||
} catch (IOException | URISyntaxException | ParseException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO move this to some common place
|
||||
private String generateUid() {
|
||||
// Generated UIDs take the form <ms>-<uuid>@nextcloud.com.
|
||||
if (mUidTail == null) {
|
||||
String uidPid = preferences.getUidPid();
|
||||
if (uidPid.length() == 0) {
|
||||
uidPid = UUID.randomUUID().toString().replace("-", "");
|
||||
preferences.setUidPid(uidPid);
|
||||
}
|
||||
mUidTail = uidPid + "@nextcloud.com";
|
||||
}
|
||||
|
||||
mUidMs = Math.max(mUidMs, System.currentTimeMillis());
|
||||
String uid = mUidMs + mUidTail;
|
||||
mUidMs++;
|
||||
|
||||
return uid;
|
||||
}
|
||||
|
||||
private void upload(File file) {
|
||||
String backupFolder = activity.getResources().getString(R.string.calendar_backup_folder)
|
||||
+ OCFile.PATH_SEPARATOR;
|
||||
|
||||
Request request = new UploadRequest.Builder(user, file.getAbsolutePath(), backupFolder + file.getName())
|
||||
.setFileSize(file.length())
|
||||
.setNameConflicPolicy(NameCollisionPolicy.RENAME)
|
||||
.setCreateRemoteFolder(true)
|
||||
.setTrigger(UploadTrigger.USER)
|
||||
.setPostAction(PostUploadAction.MOVE_TO_APP)
|
||||
.setRequireWifi(false)
|
||||
.setRequireCharging(false)
|
||||
.build();
|
||||
|
||||
TransferManagerConnection connection = new TransferManagerConnection(activity, user);
|
||||
connection.enqueue(request);
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
<!--
|
||||
Nextcloud Android client application
|
||||
|
||||
Copyright (C) 2020 Nextcloud.
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
License as published by the Free Software Foundation; either
|
||||
version 3 of the License, or any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public
|
||||
License along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Icon provided by Android Material Library in Apache License 2.0
|
||||
-->
|
||||
<vector android:height="24dp" android:tint="#757575"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
|
||||
</vector>
|
143
src/main/res/layout/backup_fragment.xml
Normal file
143
src/main/res/layout/backup_fragment.xml
Normal file
|
@ -0,0 +1,143 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Nextcloud Android client application
|
||||
|
||||
Copyright (C) 2017 Tobias Kaminsky
|
||||
Copyright (C) 2017 Nextcloud.
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
License as published by the Free Software Foundation; either
|
||||
version 3 of the License, or any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public
|
||||
License along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/contacts_linear_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/standard_margin"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/data_to_back_up_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/data_to_back_up"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/contacts"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="48dp"
|
||||
android:text="@string/contacts"
|
||||
android:textColor="@color/text_color"
|
||||
android:textSize="@dimen/two_line_primary_text_size" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/calendar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="48dp"
|
||||
android:text="@string/calendar"
|
||||
android:textColor="@color/text_color"
|
||||
android:textSize="@dimen/two_line_primary_text_size" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/backup_settings_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/backup_settings"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp">
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/daily_backup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:textColor="@color/text_color"
|
||||
android:textSize="@dimen/two_line_primary_text_size" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_toStartOf="@id/daily_backup"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/daily_backup"
|
||||
android:textColor="@color/text_color"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/last_backup_with_date"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/last_backup"
|
||||
android:textSize="12sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="@dimen/standard_margin"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/backup_now"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/standard_half_margin"
|
||||
android:layout_marginEnd="@dimen/standard_half_margin"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/contacts_backup_button"
|
||||
android:theme="@style/Button.Primary"
|
||||
app:cornerRadius="@dimen/button_corner_radius" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/contacts_datepicker"
|
||||
style="@style/OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/standard_half_margin"
|
||||
android:layout_marginEnd="@dimen/standard_half_margin"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/restore_backup"
|
||||
android:visibility="gone"
|
||||
app:cornerRadius="@dimen/button_corner_radius"
|
||||
tools:visibility="visible" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
32
src/main/res/layout/backup_list_item.xml
Normal file
32
src/main/res/layout/backup_list_item.xml
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~
|
||||
~ Nextcloud Android client application
|
||||
~
|
||||
~ @author Tobias Kaminsky
|
||||
~ Copyright (C) 2021 Tobias Kaminsky
|
||||
~ Copyright (C) 2021 Nextcloud GmbH
|
||||
~
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU Affero General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU Affero General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU Affero General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
45
src/main/res/layout/backup_list_item_header.xml
Normal file
45
src/main/res/layout/backup_list_item_header.xml
Normal file
|
@ -0,0 +1,45 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~
|
||||
~ Nextcloud Android client application
|
||||
~
|
||||
~ @author Tobias Kaminsky
|
||||
~ Copyright (C) 2021 Tobias Kaminsky
|
||||
~ Copyright (C) 2021 Nextcloud GmbH
|
||||
~
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU Affero General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU Affero General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU Affero General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/standard_margin"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner"
|
||||
android:layout_marginStart="@dimen/standard_margin"
|
||||
android:layout_marginEnd="@dimen/standard_margin"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
</LinearLayout>
|
|
@ -18,64 +18,71 @@
|
|||
License along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:animateLayoutChanges="true"
|
||||
android:orientation="vertical">
|
||||
android:animateLayoutChanges="true">
|
||||
|
||||
<LinearLayout
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@android:id/list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
android:choiceMode="multipleChoice"
|
||||
android:scrollbarStyle="outsideOverlay"
|
||||
android:scrollbars="vertical"
|
||||
android:layout_above="@+id/contactlist_restore_selected_container" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/contactlist_recyclerview"
|
||||
<LinearLayout
|
||||
android:id="@+id/contactlist_restore_selected_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/bg_default"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
android:layout_alignParentBottom="true">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:choiceMode="multipleChoice"
|
||||
tools:listitem="@layout/contactlist_list_item" />
|
||||
android:layout_height="@dimen/uploader_list_separator_height"
|
||||
android:contentDescription="@null"
|
||||
android:src="@drawable/uploader_list_separator" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/contactlist_restore_selected_container"
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/restore_selected"
|
||||
style="@style/Button.Borderless"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/bg_default"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
android:text="@string/restore_selected" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/uploader_list_separator_height"
|
||||
android:src="@drawable/uploader_list_separator"
|
||||
android:contentDescription="@null"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/contactlist_restore_selected"
|
||||
style="@style/Button.Borderless"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/contaclist_restore_selected" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/loading_list_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<include layout="@layout/contactlist_list_item_shimmer" />
|
||||
|
||||
<include layout="@layout/contactlist_list_item_shimmer" />
|
||||
|
||||
<include layout="@layout/contactlist_list_item_shimmer" />
|
||||
|
||||
<include layout="@layout/contactlist_list_item_shimmer" />
|
||||
|
||||
<include layout="@layout/contactlist_list_item_shimmer" />
|
||||
|
||||
<include layout="@layout/contactlist_list_item_shimmer" />
|
||||
|
||||
<include layout="@layout/contactlist_list_item_shimmer" />
|
||||
|
||||
<include layout="@layout/contactlist_list_item_shimmer" />
|
||||
|
||||
<include layout="@layout/contactlist_list_item_shimmer" />
|
||||
|
||||
<include layout="@layout/contactlist_list_item_shimmer" />
|
||||
</LinearLayout>
|
||||
</RelativeLayout>
|
69
src/main/res/layout/calendarlist_list_item.xml
Normal file
69
src/main/res/layout/calendarlist_list_item.xml
Normal file
|
@ -0,0 +1,69 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Nextcloud Android client application
|
||||
|
||||
Copyright (C) 2021 Tobias Kaminsky
|
||||
Copyright (C) 2021 Nextcloud
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
License as published by the Free Software Foundation; either
|
||||
version 3 of the License, or any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public
|
||||
License along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon"
|
||||
android:layout_width="@dimen/contactlist_item_icon_layout_width"
|
||||
android:layout_height="@dimen/contactlist_item_icon_layout_height"
|
||||
android:layout_margin="@dimen/standard_margin"
|
||||
android:contentDescription="@string/contactlist_item_icon"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/file_calendar"
|
||||
app:srcCompat="@drawable/file_calendar"
|
||||
tools:srcCompat="@drawable/file_calendar"
|
||||
android:layout_gravity="center_vertical" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:gravity="start">
|
||||
|
||||
<CheckedTextView
|
||||
android:id="@+id/name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/standard_list_item_size"
|
||||
android:layout_marginEnd="@dimen/standard_margin"
|
||||
android:layout_weight="1"
|
||||
android:checkMark="?android:attr/listChoiceIndicatorMultiple"
|
||||
android:ellipsize="marquee"
|
||||
android:gravity="center_vertical|start"
|
||||
android:maxLines="2"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/standard_list_item_size"
|
||||
android:layout_weight="1"
|
||||
android:visibility="gone"
|
||||
android:gravity="start"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
|
@ -23,7 +23,7 @@
|
|||
android:layout_height="@dimen/standard_list_item_size">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/contactlist_item_icon"
|
||||
android:id="@+id/icon"
|
||||
android:layout_width="@dimen/contactlist_item_icon_layout_width"
|
||||
android:layout_height="@dimen/contactlist_item_icon_layout_height"
|
||||
android:layout_margin="@dimen/standard_margin"
|
||||
|
@ -32,7 +32,7 @@
|
|||
android:contentDescription="@string/contactlist_item_icon"/>
|
||||
|
||||
<CheckedTextView
|
||||
android:id="@+id/contactlist_item_name"
|
||||
android:id="@+id/name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/standard_list_item_size"
|
||||
android:layout_marginEnd="@dimen/standard_margin"
|
||||
|
|
|
@ -1,96 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Nextcloud Android client application
|
||||
|
||||
Copyright (C) 2017 Tobias Kaminsky
|
||||
Copyright (C) 2017 Nextcloud.
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
License as published by the Free Software Foundation; either
|
||||
version 3 of the License, or any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public
|
||||
License along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/contacts_linear_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/contacts_automatic_backup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/standard_margin"
|
||||
android:text="@string/contacts_automatic_backup"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textColor="@color/text_color" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/contacts_last_backup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/standard_margin"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/contacts_last_backup"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textColor="@color/text_color"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/contacts_last_backup_timestamp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/standard_margin"
|
||||
android:layout_weight="1"
|
||||
android:gravity="end"
|
||||
android:text="@string/contacts_preference_backup_never"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"/>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/contacts_backup_now"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/standard_margin"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/contacts_backup_button"
|
||||
android:theme="@style/Button.Primary"
|
||||
app:cornerRadius="@dimen/button_corner_radius" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/contacts_datepicker"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/standard_margin"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/contacts_preference_choose_date"
|
||||
android:theme="@style/Button"
|
||||
android:visibility="gone"
|
||||
app:cornerRadius="@dimen/button_corner_radius" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
|
@ -102,11 +102,6 @@
|
|||
<group
|
||||
android:id="@+id/drawer_menu_bottom"
|
||||
android:checkableBehavior="single">
|
||||
<item
|
||||
android:id="@+id/nav_contacts"
|
||||
android:icon="@drawable/nav_contacts"
|
||||
android:orderInCategory="3"
|
||||
android:title="@string/actionbar_contacts"/>
|
||||
<item
|
||||
android:id="@+id/nav_settings"
|
||||
android:icon="@drawable/nav_settings"
|
||||
|
|
|
@ -32,10 +32,10 @@
|
|||
<bool name="show_external_links">true</bool>
|
||||
<bool name="show_outdated_server_warning">true</bool>
|
||||
|
||||
<!-- Contacts backup -->
|
||||
<bool name="contacts_backup">true</bool>
|
||||
<!-- Calendar & Contacts backup -->
|
||||
<string name="contacts_backup_folder">/.Contacts-Backup</string>
|
||||
<integer name="contacts_backup_expire">-1</integer>
|
||||
<string name="calendar_backup_folder">/.Calendar-Backup</string>
|
||||
|
||||
<!-- What's new -->
|
||||
<bool name="show_whats_new">true</bool>
|
||||
|
@ -58,7 +58,6 @@
|
|||
<bool name="shared_enabled">true</bool>
|
||||
<bool name="videos_enabled">false</bool>
|
||||
<bool name="show_drawer_logout">false</bool>
|
||||
<bool name="show_drawer_contacts_backup">false</bool> <!-- if false it will shown in settings -->
|
||||
|
||||
<!-- Help, imprint and feedback, and other things -->
|
||||
<bool name="passcode_enabled">true</bool>
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
<string name="prefs_calendar_contacts_address_resolve_error">Server address for the account could not be resolved for DAVx5 (formerly known as DAVdroid)</string>
|
||||
<string name="prefs_calendar_contacts_no_store_error">Neither F-Droid nor Google Play is installed</string>
|
||||
<string name="prefs_calendar_contacts_sync_setup_successful">Calendar & contacts sync set up</string>
|
||||
<string name="prefs_daily_contacts_sync_summary">Daily backup of your contacts</string>
|
||||
<string name="prefs_daily_backup_summary">Daily backup of your calendar & contacts</string>
|
||||
<string name="prefs_sycned_folders_summary">Manage folders for auto upload</string>
|
||||
<string name="prefs_help">Help</string>
|
||||
<string name="prefs_recommend">Recommend to friend</string>
|
||||
|
@ -187,6 +187,22 @@
|
|||
<item quantity="one">Failed to copy %1$d file from the %2$s folder into</item>
|
||||
<item quantity="other">Failed to copy %1$d files from the %2$s folder into</item>
|
||||
</plurals>
|
||||
<plurals name="wrote_n_events_to">
|
||||
<item quantity="one">Wrote %1$d event to %2$s</item>
|
||||
<item quantity="other">Wrote %1$d events to %2$s</item>
|
||||
</plurals>
|
||||
<plurals name="created_n_uids_to">
|
||||
<item quantity="one">Created %1$d fresh UID</item>
|
||||
<item quantity="other">Created %1$d fresh UIDs</item>
|
||||
</plurals>
|
||||
<plurals name="processed_n_entries">
|
||||
<item quantity="one">Processed %d entry.</item>
|
||||
<item quantity="other">Processed %d entries.</item>
|
||||
</plurals>
|
||||
<plurals name="found_n_duplicates">
|
||||
<item quantity="one">Found %d duplicate entry.</item>
|
||||
<item quantity="other">Found %d duplicate entries.</item>
|
||||
</plurals>
|
||||
<string name="sync_foreign_files_forgotten_explanation">As of version 1.3.16, files uploaded from this device are copied into the local %1$s folder to prevent data loss when a single file is synced with multiple accounts.\n\nDue to this change, all files uploaded with earlier versions of this app were copied into the %2$s folder. However, an error prevented the completion of this operation during account synchronization. You may either leave the file(s) as is and delete the link to %3$s, or move the file(s) into the %1$s folder and retain the link to %4$s.\n\nListed below are the local file(s), and the remote file(s) in %5$s they were linked to.</string>
|
||||
<string name="sync_current_folder_was_removed">The folder %1$s does not exist anymore</string>
|
||||
<string name="foreign_files_move">Move all</string>
|
||||
|
@ -571,21 +587,15 @@
|
|||
<string name="activities_no_results_message">No events like additions, changes and shares yet.</string>
|
||||
<string name="prefs_category_about">About</string>
|
||||
|
||||
<string name="actionbar_contacts">Back up contacts</string>
|
||||
<string name="actionbar_contacts_restore">Restore contacts</string>
|
||||
<string name="actionbar_calendar_contacts_restore">Restore contacts & calendar</string>
|
||||
<string name="contacts_backup_button">Back up now</string>
|
||||
<string name="contacts_automatic_backup">Automatic backup</string>
|
||||
<string name="contacts_last_backup">Last backup</string>
|
||||
<string name="contacts_read_permission">Permission to read contact list needed</string>
|
||||
<string name="contaclist_restore_selected">Restore selected contacts</string>
|
||||
<string name="contactlist_account_chooser_title">Choose account to import</string>
|
||||
<string name="contactlist_no_permission">No permission given, nothing imported.</string>
|
||||
<string name="contacts_preference_choose_date">Choose date</string>
|
||||
<string name="contacts_preference_backup_never">never</string>
|
||||
<string name="restore_backup">Restore backup</string>
|
||||
<string name="contacts_preferences_no_file_found">No file found</string>
|
||||
<string name="contacts_preferences_something_strange_happened">Could not find your last backup!</string>
|
||||
<string name="contacts_preferences_backup_scheduled">Backup scheduled and will start shortly</string>
|
||||
<string name="contacts_preferences_import_scheduled">Import scheduled and will start shortly</string>
|
||||
<string name="backup_title">Contacts & calendar backup</string>
|
||||
|
||||
<string name="drawer_logout">Log out</string>
|
||||
<string name="picture_set_as_no_app">No app found to set a picture with</string>
|
||||
|
@ -961,5 +971,20 @@
|
|||
<string name="choose_template_helper_text">Please choose a template and enter a file name.</string>
|
||||
<string name="strict_mode">Strict mode: no HTTP connection allowed!</string>
|
||||
<string name="fullscreen">Fullscreen</string>
|
||||
<string name="destination_filename">Destination filename</string>
|
||||
<string name="suggest">Suggest</string>
|
||||
<string name="enter_destination_filename">Enter destination filename</string>
|
||||
<string name="did_not_check_for_dupes">Did not check for duplicates.</string>
|
||||
<string name="last_backup">Last backup: %1$s</string>
|
||||
<string name="error_choosing_date">Error choosing date</string>
|
||||
<string name="restore_selected">Restore selected</string>
|
||||
<string name="calendars">Calendars</string>
|
||||
<string name="contacts">Contacts</string>
|
||||
<string name="data_to_back_up">Data to back up</string>
|
||||
<string name="calendar">Calendar</string>
|
||||
<string name="backup_settings">Backup settings</string>
|
||||
<string name="daily_backup">Daily backup</string>
|
||||
<string name="calendar_name_linewrap" translatable="false">%1$s\n%2$s</string>
|
||||
<string name="no_calendar_exists">No calendar exists</string>
|
||||
<string name="more">More</string>
|
||||
</resources>
|
||||
|
|
|
@ -68,9 +68,9 @@
|
|||
android:key="calendar_contacts"
|
||||
android:summary="@string/prefs_calendar_contacts_summary" />
|
||||
<Preference
|
||||
android:title="@string/actionbar_contacts"
|
||||
android:key="contacts"
|
||||
android:summary="@string/prefs_daily_contacts_sync_summary"/>
|
||||
android:title="@string/backup_title"
|
||||
android:key="backup"
|
||||
android:summary="@string/prefs_daily_backup_summary" />
|
||||
<Preference
|
||||
android:title="@string/prefs_e2e_mnemonic"
|
||||
android:key="mnemonic"
|
||||
|
|
Loading…
Reference in a new issue