Account settings migration 13 to 14 (bitfireAT/davx5#262)

* AccountSettings: prevent redundant calls, move migrations to separate class

* Don't create AccountSettings while migrating

* Double thread sleep time to wait for sync framework to get disabled, and update logging

* Remove manual testing helper

* Fix tests

---------

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
This commit is contained in:
Sunik Kupfer 2023-05-12 10:23:14 +02:00 committed by Ricki Hirner
parent 8ac8971293
commit 63f51c4b42
4 changed files with 435 additions and 383 deletions

View file

@ -84,14 +84,14 @@ class PeriodicSyncWorkerTest {
@Test
fun enable_enqueuesPeriodicWorker() {
PeriodicSyncWorker.enable(context, account, CalendarContract.AUTHORITY, 60)
PeriodicSyncWorker.enable(context, account, CalendarContract.AUTHORITY, 60, false)
val workerName = PeriodicSyncWorker.workerName(account, CalendarContract.AUTHORITY)
assertTrue(workScheduledOrRunning(context, workerName))
}
@Test
fun disable_removesPeriodicWorker() {
PeriodicSyncWorker.enable(context, account, CalendarContract.AUTHORITY, 60)
PeriodicSyncWorker.enable(context, account, CalendarContract.AUTHORITY, 60, false)
PeriodicSyncWorker.disable(context, account, CalendarContract.AUTHORITY)
val workerName = PeriodicSyncWorker.workerName(account, CalendarContract.AUTHORITY)
assertFalse(workScheduledOrRunning(context, workerName))

View file

@ -5,51 +5,24 @@ package at.bitfire.davdroid.settings
import android.accounts.Account
import android.accounts.AccountManager
import android.annotation.SuppressLint
import android.content.*
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.Parcel
import android.os.RemoteException
import android.provider.CalendarContract
import android.provider.CalendarContract.ExtendedProperties
import android.provider.ContactsContract
import android.util.Base64
import androidx.annotation.WorkerThread
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import at.bitfire.davdroid.InvalidAccountException
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.Credentials
import at.bitfire.davdroid.db.Service
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.resource.LocalTask
import at.bitfire.davdroid.resource.TaskUtils
import at.bitfire.davdroid.syncadapter.PeriodicSyncWorker
import at.bitfire.davdroid.syncadapter.SyncUtils
import at.bitfire.davdroid.util.closeCompat
import at.bitfire.ical4android.AndroidCalendar
import at.bitfire.ical4android.AndroidEvent
import at.bitfire.ical4android.TaskProvider
import at.bitfire.ical4android.TaskProvider.ProviderName.OpenTasks
import at.bitfire.ical4android.UnknownProperty
import at.bitfire.vcard4android.ContactsStorageException
import at.bitfire.vcard4android.GroupMethod
import at.techbee.jtx.JtxContract.asSyncAdapter
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent
import net.fortuna.ical4j.model.Property
import net.fortuna.ical4j.model.property.Url
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.apache.commons.lang3.StringUtils
import org.dmfs.tasks.contract.TaskContract
import java.io.ByteArrayInputStream
import java.io.ObjectInputStream
import java.util.logging.Level
/**
@ -131,6 +104,10 @@ class AccountSettings(
const val SYNC_INTERVAL_MANUALLY = -1L
/** Static property to indicate whether AccountSettings migration is currently running.
* **Access must be `synchronized` with `AccountSettings::class.java`.** */
@Volatile
var currentlyUpdating = false
fun initialUserData(credentials: Credentials?): Bundle {
val bundle = Bundle(2)
@ -175,11 +152,20 @@ class AccountSettings(
try {
version = Integer.parseInt(versionStr)
} catch (e: NumberFormatException) {
Logger.log.log(Level.SEVERE, "Invalid account version: $versionStr", e)
}
Logger.log.fine("Account ${account.name} has version $version, current version: $CURRENT_VERSION")
if (version < CURRENT_VERSION)
update(version)
if (version < CURRENT_VERSION) {
if (currentlyUpdating) {
Logger.log.severe("Redundant call: migration created AccountSettings(). This must never happen.")
throw IllegalStateException("Redundant call: migration created AccountSettings()")
} else {
currentlyUpdating = true
update(version)
currentlyUpdating = false
}
}
}
}
@ -249,7 +235,7 @@ class AccountSettings(
PeriodicSyncWorker.disable(context, account, authority)
} else {
Logger.log.fine("Setting periodic sync of $account/$authority to $seconds seconds")
PeriodicSyncWorker.enable(context, account, authority, seconds)
PeriodicSyncWorker.enable(context, account, authority, seconds, getSyncWifiOnly())
}.result.get() // On operation (enable/disable) failure exception is thrown
} catch (e: Exception) {
Logger.log.log(Level.SEVERE, "Failed to set sync interval of $account/$authority to $seconds seconds", e)
@ -451,8 +437,16 @@ class AccountSettings(
val fromVersion = toVersion-1
Logger.log.info("Updating account ${account.name} from version $fromVersion to $toVersion")
try {
val updateProc = this::class.java.getDeclaredMethod("update_${fromVersion}_$toVersion")
updateProc.invoke(this)
val migrations = AccountSettingsMigrations(
context = context,
db = db,
settings = settings,
account = account,
accountManager = accountManager,
accountSettings = this
)
val updateProc = AccountSettingsMigrations::class.java.getDeclaredMethod("update_${fromVersion}_$toVersion")
updateProc.invoke(migrations)
Logger.log.info("Account version update successful")
accountManager.setUserData(account, KEY_SETTINGS_VERSION, toVersion.toString())
@ -462,351 +456,4 @@ class AccountSettings(
}
}
/**
* Disables all sync adapter periodic syncs for every authority. Then enables
* corresponding PeriodicSyncWorkers
*/
@Suppress("unused","FunctionName")
fun update_13_14() {
val authorities = listOf(
context.getString(R.string.address_books_authority),
CalendarContract.AUTHORITY,
TaskProvider.ProviderName.JtxBoard.authority,
TaskProvider.ProviderName.OpenTasks.authority,
TaskProvider.ProviderName.TasksOrg.authority
)
for (authority in authorities) {
// Enable PeriodicSyncWorker (WorkManager), with known intervals
v14_enableWorkManager(authority)
// Disable periodic syncs (sync adapter framework)
v14_disableSyncFramework(authority)
}
}
private fun v14_enableWorkManager(authority: String) {
getSyncInterval(authority)?.let { syncInterval ->
if (!setSyncInterval(authority, syncInterval))
Logger.log.severe("Failed to enable PeriodicSyncWorker for $authority")
}
}
private fun v14_disableSyncFramework(authority: String) {
// Cancel potentially running sync
ContentResolver.cancelSync(account, authority)
// Disable periodic syncs (sync adapter framework)
val disable: () -> Boolean = {
/* Ugly hack: because there is no callback for when the sync status/interval has been
updated, we need to make this call blocking. */
val syncs = ContentResolver.getPeriodicSyncs(account, authority)
for (sync in syncs) {
Logger.log.fine("Disabling sync framework periodic syncs of $account/$authority")
ContentResolver.removePeriodicSync(sync.account, sync.authority, sync.extras)
}
/* return */
!syncs.all { sync ->
ContentResolver.getPeriodicSyncs(sync.account, sync.authority).isEmpty()
}
}
// try up to 10 times with 100 ms pause
var success = false
for (idxTry in 0 until 10) {
success = disable()
if (success)
break
Thread.sleep(100)
}
if (!success)
Logger.log.severe("Failed to disable sync framework periodic syncs for $authority")
}
@Suppress("unused","FunctionName")
/**
* Not a per-account migration, but not a database migration, too, so it fits best there.
* Best future solution would be that SettingsManager manages versions and migrations.
*
* Updates proxy settings from override_proxy_* to proxy_type, proxy_host, proxy_port.
*/
private fun update_12_13() {
// proxy settings are managed by SharedPreferencesProvider
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
// old setting names
val overrideProxy = "override_proxy"
val overrideProxyHost = "override_proxy_host"
val overrideProxyPort = "override_proxy_port"
val edit = preferences.edit()
if (preferences.contains(overrideProxy)) {
if (preferences.getBoolean(overrideProxy, false))
// override_proxy set, migrate to proxy_type = HTTP
edit.putInt(Settings.PROXY_TYPE, Settings.PROXY_TYPE_HTTP)
edit.remove(overrideProxy)
}
if (preferences.contains(overrideProxyHost)) {
preferences.getString(overrideProxyHost, null)?.let { host ->
edit.putString(Settings.PROXY_HOST, host)
}
edit.remove(overrideProxyHost)
}
if (preferences.contains(overrideProxyPort)) {
val port = preferences.getInt(overrideProxyPort, 0)
if (port != 0)
edit.putInt(Settings.PROXY_PORT, port)
edit.remove(overrideProxyPort)
}
edit.apply()
}
@Suppress("unused","FunctionName")
/**
* Store event URLs as URL (extended property) instead of unknown property. At the same time,
* convert legacy unknown properties to the current format.
*/
private fun update_11_12() {
if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.WRITE_CALENDAR) == PackageManager.PERMISSION_GRANTED) {
val provider = context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)
if (provider != null)
try {
// Attention: CalendarProvider does NOT limit the results of the ExtendedProperties query
// to the given account! So all extended properties will be processed number-of-accounts times.
val extUri = ExtendedProperties.CONTENT_URI.asSyncAdapter(account)
provider.query(extUri, arrayOf(
ExtendedProperties._ID, // idx 0
ExtendedProperties.NAME, // idx 1
ExtendedProperties.VALUE // idx 2
), null, null, null)?.use { cursor ->
while (cursor.moveToNext()) {
val id = cursor.getLong(0)
val rawValue = cursor.getString(2)
val uri by lazy {
ContentUris.withAppendedId(ExtendedProperties.CONTENT_URI, id).asSyncAdapter(account)
}
when (cursor.getString(1)) {
UnknownProperty.CONTENT_ITEM_TYPE -> {
// unknown property; check whether it's a URL
try {
val property = UnknownProperty.fromJsonString(rawValue)
if (property is Url) { // rewrite to MIMETYPE_URL
val newValues = ContentValues(2)
newValues.put(ExtendedProperties.NAME, AndroidEvent.MIMETYPE_URL)
newValues.put(ExtendedProperties.VALUE, property.value)
provider.update(uri, newValues, null, null)
}
} catch (e: Exception) {
Logger.log.log(Level.WARNING, "Couldn't rewrite URL from unknown property to ${AndroidEvent.MIMETYPE_URL}", e)
}
}
"unknown-property" -> {
// unknown property (deprecated format); convert to current format
try {
val stream = ByteArrayInputStream(Base64.decode(rawValue, Base64.NO_WRAP))
ObjectInputStream(stream).use {
(it.readObject() as? Property)?.let { property ->
// rewrite to current format
val newValues = ContentValues(2)
newValues.put(ExtendedProperties.NAME, UnknownProperty.CONTENT_ITEM_TYPE)
newValues.put(ExtendedProperties.VALUE, UnknownProperty.toJsonString(property))
provider.update(uri, newValues, null, null)
}
}
} catch(e: Exception) {
Logger.log.log(Level.WARNING, "Couldn't rewrite deprecated unknown property to current format", e)
}
}
"unknown-property.v2" -> {
// unknown property (deprecated MIME type); rewrite to current MIME type
val newValues = ContentValues(1)
newValues.put(ExtendedProperties.NAME, UnknownProperty.CONTENT_ITEM_TYPE)
provider.update(uri, newValues, null, null)
}
}
}
}
} finally {
provider.closeCompat()
}
}
}
@Suppress("unused","FunctionName")
/**
* The tasks sync interval should be stored in account settings. It's used to set the sync interval
* again when the tasks provider is switched.
*/
private fun update_10_11() {
TaskUtils.currentProvider(context)?.let { provider ->
val interval = getSyncInterval(provider.authority)
if (interval != null)
accountManager.setUserData(account, KEY_SYNC_INTERVAL_TASKS, interval.toString())
}
}
@Suppress("unused","FunctionName")
/**
* Task synchronization now handles alarms, categories, relations and unknown properties.
* Setting task ETags to null will cause them to be downloaded (and parsed) again.
*
* Also update the allowed reminder types for calendars.
**/
private fun update_9_10() {
TaskProvider.acquire(context, OpenTasks)?.use { provider ->
val tasksUri = provider.tasksUri().asSyncAdapter(account)
val emptyETag = ContentValues(1)
emptyETag.putNull(LocalTask.COLUMN_ETAG)
provider.client.update(tasksUri, emptyETag, "${TaskContract.Tasks._DIRTY}=0 AND ${TaskContract.Tasks._DELETED}=0", null)
}
@SuppressLint("Recycle")
if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.WRITE_CALENDAR) == PackageManager.PERMISSION_GRANTED)
context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)?.let { provider ->
provider.update(CalendarContract.Calendars.CONTENT_URI.asSyncAdapter(account),
AndroidCalendar.calendarBaseValues, null, null)
provider.closeCompat()
}
}
@Suppress("unused","FunctionName")
/**
* It seems that somehow some non-CalDAV accounts got OpenTasks syncable, which caused battery problems.
* Disable it on those accounts for the future.
*/
private fun update_8_9() {
val hasCalDAV = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV) != null
if (!hasCalDAV && ContentResolver.getIsSyncable(account, OpenTasks.authority) != 0) {
Logger.log.info("Disabling OpenTasks sync for $account")
ContentResolver.setIsSyncable(account, OpenTasks.authority, 0)
}
}
@Suppress("unused","FunctionName")
@SuppressLint("Recycle")
/**
* There is a mistake in this method. [TaskContract.Tasks.SYNC_VERSION] is used to store the
* SEQUENCE and should not be used for the eTag.
*/
private fun update_7_8() {
TaskProvider.acquire(context, OpenTasks)?.use { provider ->
// ETag is now in sync_version instead of sync1
// UID is now in _uid instead of sync2
provider.client.query(provider.tasksUri().asSyncAdapter(account),
arrayOf(TaskContract.Tasks._ID, TaskContract.Tasks.SYNC1, TaskContract.Tasks.SYNC2),
"${TaskContract.Tasks.ACCOUNT_TYPE}=? AND ${TaskContract.Tasks.ACCOUNT_NAME}=?",
arrayOf(account.type, account.name), null)!!.use { cursor ->
while (cursor.moveToNext()) {
val id = cursor.getLong(0)
val eTag = cursor.getString(1)
val uid = cursor.getString(2)
val values = ContentValues(4)
values.put(TaskContract.Tasks._UID, uid)
values.put(TaskContract.Tasks.SYNC_VERSION, eTag)
values.putNull(TaskContract.Tasks.SYNC1)
values.putNull(TaskContract.Tasks.SYNC2)
Logger.log.log(Level.FINER, "Updating task $id", values)
provider.client.update(
ContentUris.withAppendedId(provider.tasksUri(), id).asSyncAdapter(account),
values, null, null)
}
}
}
}
@Suppress("unused")
@SuppressLint("Recycle")
private fun update_6_7() {
// add calendar colors
context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)?.let { provider ->
try {
AndroidCalendar.insertColors(provider, account)
} finally {
provider.closeCompat()
}
}
// update allowed WiFi settings key
val onlySSID = accountManager.getUserData(account, "wifi_only_ssid")
accountManager.setUserData(account, KEY_WIFI_ONLY_SSIDS, onlySSID)
accountManager.setUserData(account, "wifi_only_ssid", null)
}
@Suppress("unused")
@SuppressLint("Recycle", "ParcelClassLoader")
private fun update_5_6() {
context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)?.let { provider ->
val parcel = Parcel.obtain()
try {
// don't run syncs during the migration
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 0)
ContentResolver.setIsSyncable(account, context.getString(R.string.address_books_authority), 0)
ContentResolver.cancelSync(account, null)
// get previous address book settings (including URL)
val raw = ContactsContract.SyncState.get(provider, account)
if (raw == null)
Logger.log.info("No contacts sync state, ignoring account")
else {
parcel.unmarshall(raw, 0, raw.size)
parcel.setDataPosition(0)
val params = parcel.readBundle()!!
val url = params.getString("url")?.toHttpUrlOrNull()
if (url == null)
Logger.log.info("No address book URL, ignoring account")
else {
// create new address book
val info = Collection(url = url, type = Collection.TYPE_ADDRESSBOOK, displayName = account.name)
Logger.log.log(Level.INFO, "Creating new address book account", url)
val addressBookAccount = Account(LocalAddressBook.accountName(account, info), context.getString(R.string.account_type_address_book))
if (!accountManager.addAccountExplicitly(addressBookAccount, null, LocalAddressBook.initialUserData(account, info.url.toString())))
throw ContactsStorageException("Couldn't create address book account")
// move contacts to new address book
Logger.log.info("Moving contacts from $account to $addressBookAccount")
val newAccount = ContentValues(2)
newAccount.put(ContactsContract.RawContacts.ACCOUNT_NAME, addressBookAccount.name)
newAccount.put(ContactsContract.RawContacts.ACCOUNT_TYPE, addressBookAccount.type)
val affected = provider.update(ContactsContract.RawContacts.CONTENT_URI.buildUpon()
.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, account.name)
.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, account.type)
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build(),
newAccount,
"${ContactsContract.RawContacts.ACCOUNT_NAME}=? AND ${ContactsContract.RawContacts.ACCOUNT_TYPE}=?",
arrayOf(account.name, account.type))
Logger.log.info("$affected contacts moved to new address book")
}
ContactsContract.SyncState.set(provider, account, null)
}
} catch(e: RemoteException) {
throw ContactsStorageException("Couldn't migrate contacts to new address book", e)
} finally {
parcel.recycle()
provider.closeCompat()
}
}
// update version number so that further syncs don't repeat the migration
accountManager.setUserData(account, KEY_SETTINGS_VERSION, "6")
// request sync of new address book account
ContentResolver.setIsSyncable(account, context.getString(R.string.address_books_authority), 1)
setSyncInterval(context.getString(R.string.address_books_authority), 4*3600)
}
/* Android 7.1.1 OpenTasks fix */
@Suppress("unused")
private fun update_4_5() {
// call PackageChangedReceiver which then enables/disables OpenTasks sync when it's (not) available
SyncUtils.updateTaskSync(context)
}
@Suppress("unused")
private fun update_3_4() {
setGroupMethod(GroupMethod.CATEGORIES)
}
// updates from AccountSettings version 2 and below are not supported anymore
}

View file

@ -0,0 +1,405 @@
package at.bitfire.davdroid.settings
import android.accounts.Account
import android.accounts.AccountManager
import android.annotation.SuppressLint
import android.content.ContentResolver
import android.content.ContentUris
import android.content.ContentValues
import android.content.Context
import android.content.pm.PackageManager
import android.os.Parcel
import android.os.RemoteException
import android.provider.CalendarContract
import android.provider.ContactsContract
import android.util.Base64
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.Service
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.resource.LocalTask
import at.bitfire.davdroid.resource.TaskUtils
import at.bitfire.davdroid.syncadapter.SyncUtils
import at.bitfire.davdroid.util.closeCompat
import at.bitfire.ical4android.AndroidCalendar
import at.bitfire.ical4android.AndroidEvent
import at.bitfire.ical4android.TaskProvider
import at.bitfire.ical4android.UnknownProperty
import at.bitfire.vcard4android.ContactsStorageException
import at.bitfire.vcard4android.GroupMethod
import at.techbee.jtx.JtxContract.asSyncAdapter
import net.fortuna.ical4j.model.Property
import net.fortuna.ical4j.model.property.Url
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.dmfs.tasks.contract.TaskContract
import java.io.ByteArrayInputStream
import java.io.ObjectInputStream
import java.util.logging.Level
class AccountSettingsMigrations(
val context: Context,
val db: AppDatabase,
val settings: SettingsManager,
val account: Account,
val accountManager: AccountManager,
val accountSettings: AccountSettings
) {
/**
* Disables all sync adapter periodic syncs for every authority. Then enables
* corresponding PeriodicSyncWorkers
*/
@Suppress("unused","FunctionName")
fun update_13_14() {
// Cancel any potentially running syncs for this account (sync framework)
ContentResolver.cancelSync(account, null)
val authorities = listOf(
context.getString(R.string.address_books_authority),
CalendarContract.AUTHORITY,
TaskProvider.ProviderName.JtxBoard.authority,
TaskProvider.ProviderName.OpenTasks.authority,
TaskProvider.ProviderName.TasksOrg.authority
)
for (authority in authorities) {
// Enable PeriodicSyncWorker (WorkManager), with known intervals
v14_enableWorkManager(authority)
// Disable periodic syncs (sync adapter framework)
v14_disableSyncFramework(authority)
}
}
private fun v14_enableWorkManager(authority: String) {
val enabled = accountSettings.getSyncInterval(authority)?.let { syncInterval ->
accountSettings.setSyncInterval(authority, syncInterval)
} ?: false
Logger.log.info("PeriodicSyncWorker for $account/$authority enabled=$enabled")
}
private fun v14_disableSyncFramework(authority: String) {
// Disable periodic syncs (sync adapter framework)
val disable: () -> Boolean = {
/* Ugly hack: because there is no callback for when the sync status/interval has been
updated, we need to make this call blocking. */
for (sync in ContentResolver.getPeriodicSyncs(account, authority))
ContentResolver.removePeriodicSync(sync.account, sync.authority, sync.extras)
// check whether syncs are really disabled
var result = true
for (sync in ContentResolver.getPeriodicSyncs(account, authority)) {
Logger.log.info("Sync framework still has a periodic sync for $account/$authority: $sync")
result = false
}
result
}
// try up to 10 times with 100 ms pause
var success = false
for (idxTry in 0 until 10) {
success = disable()
if (success)
break
Thread.sleep(200)
}
Logger.log.info("Sync framework periodic syncs for $account/$authority disabled=$success")
}
@Suppress("unused","FunctionName")
/**
* Not a per-account migration, but not a database migration, too, so it fits best there.
* Best future solution would be that SettingsManager manages versions and migrations.
*
* Updates proxy settings from override_proxy_* to proxy_type, proxy_host, proxy_port.
*/
private fun update_12_13() {
// proxy settings are managed by SharedPreferencesProvider
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
// old setting names
val overrideProxy = "override_proxy"
val overrideProxyHost = "override_proxy_host"
val overrideProxyPort = "override_proxy_port"
val edit = preferences.edit()
if (preferences.contains(overrideProxy)) {
if (preferences.getBoolean(overrideProxy, false))
// override_proxy set, migrate to proxy_type = HTTP
edit.putInt(Settings.PROXY_TYPE, Settings.PROXY_TYPE_HTTP)
edit.remove(overrideProxy)
}
if (preferences.contains(overrideProxyHost)) {
preferences.getString(overrideProxyHost, null)?.let { host ->
edit.putString(Settings.PROXY_HOST, host)
}
edit.remove(overrideProxyHost)
}
if (preferences.contains(overrideProxyPort)) {
val port = preferences.getInt(overrideProxyPort, 0)
if (port != 0)
edit.putInt(Settings.PROXY_PORT, port)
edit.remove(overrideProxyPort)
}
edit.apply()
}
@Suppress("unused","FunctionName")
/**
* Store event URLs as URL (extended property) instead of unknown property. At the same time,
* convert legacy unknown properties to the current format.
*/
private fun update_11_12() {
if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.WRITE_CALENDAR) == PackageManager.PERMISSION_GRANTED) {
val provider = context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)
if (provider != null)
try {
// Attention: CalendarProvider does NOT limit the results of the ExtendedProperties query
// to the given account! So all extended properties will be processed number-of-accounts times.
val extUri = CalendarContract.ExtendedProperties.CONTENT_URI.asSyncAdapter(account)
provider.query(extUri, arrayOf(
CalendarContract.ExtendedProperties._ID, // idx 0
CalendarContract.ExtendedProperties.NAME, // idx 1
CalendarContract.ExtendedProperties.VALUE // idx 2
), null, null, null)?.use { cursor ->
while (cursor.moveToNext()) {
val id = cursor.getLong(0)
val rawValue = cursor.getString(2)
val uri by lazy {
ContentUris.withAppendedId(CalendarContract.ExtendedProperties.CONTENT_URI, id).asSyncAdapter(account)
}
when (cursor.getString(1)) {
UnknownProperty.CONTENT_ITEM_TYPE -> {
// unknown property; check whether it's a URL
try {
val property = UnknownProperty.fromJsonString(rawValue)
if (property is Url) { // rewrite to MIMETYPE_URL
val newValues = ContentValues(2)
newValues.put(CalendarContract.ExtendedProperties.NAME, AndroidEvent.MIMETYPE_URL)
newValues.put(CalendarContract.ExtendedProperties.VALUE, property.value)
provider.update(uri, newValues, null, null)
}
} catch (e: Exception) {
Logger.log.log(Level.WARNING, "Couldn't rewrite URL from unknown property to ${AndroidEvent.MIMETYPE_URL}", e)
}
}
"unknown-property" -> {
// unknown property (deprecated format); convert to current format
try {
val stream = ByteArrayInputStream(Base64.decode(rawValue, Base64.NO_WRAP))
ObjectInputStream(stream).use {
(it.readObject() as? Property)?.let { property ->
// rewrite to current format
val newValues = ContentValues(2)
newValues.put(CalendarContract.ExtendedProperties.NAME, UnknownProperty.CONTENT_ITEM_TYPE)
newValues.put(CalendarContract.ExtendedProperties.VALUE, UnknownProperty.toJsonString(property))
provider.update(uri, newValues, null, null)
}
}
} catch(e: Exception) {
Logger.log.log(Level.WARNING, "Couldn't rewrite deprecated unknown property to current format", e)
}
}
"unknown-property.v2" -> {
// unknown property (deprecated MIME type); rewrite to current MIME type
val newValues = ContentValues(1)
newValues.put(CalendarContract.ExtendedProperties.NAME, UnknownProperty.CONTENT_ITEM_TYPE)
provider.update(uri, newValues, null, null)
}
}
}
}
} finally {
provider.closeCompat()
}
}
}
@Suppress("unused","FunctionName")
/**
* The tasks sync interval should be stored in account settings. It's used to set the sync interval
* again when the tasks provider is switched.
*/
private fun update_10_11() {
TaskUtils.currentProvider(context)?.let { provider ->
val interval = accountSettings.getSyncInterval(provider.authority)
if (interval != null)
accountManager.setUserData(account,
AccountSettings.KEY_SYNC_INTERVAL_TASKS, interval.toString())
}
}
@Suppress("unused","FunctionName")
/**
* Task synchronization now handles alarms, categories, relations and unknown properties.
* Setting task ETags to null will cause them to be downloaded (and parsed) again.
*
* Also update the allowed reminder types for calendars.
**/
private fun update_9_10() {
TaskProvider.acquire(context, TaskProvider.ProviderName.OpenTasks)?.use { provider ->
val tasksUri = provider.tasksUri().asSyncAdapter(account)
val emptyETag = ContentValues(1)
emptyETag.putNull(LocalTask.COLUMN_ETAG)
provider.client.update(tasksUri, emptyETag, "${TaskContract.Tasks._DIRTY}=0 AND ${TaskContract.Tasks._DELETED}=0", null)
}
@SuppressLint("Recycle")
if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.WRITE_CALENDAR) == PackageManager.PERMISSION_GRANTED)
context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)?.let { provider ->
provider.update(
CalendarContract.Calendars.CONTENT_URI.asSyncAdapter(account),
AndroidCalendar.calendarBaseValues, null, null)
provider.closeCompat()
}
}
@Suppress("unused","FunctionName")
/**
* It seems that somehow some non-CalDAV accounts got OpenTasks syncable, which caused battery problems.
* Disable it on those accounts for the future.
*/
private fun update_8_9() {
val hasCalDAV = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV) != null
if (!hasCalDAV && ContentResolver.getIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority) != 0) {
Logger.log.info("Disabling OpenTasks sync for $account")
ContentResolver.setIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority, 0)
}
}
@Suppress("unused","FunctionName")
@SuppressLint("Recycle")
/**
* There is a mistake in this method. [TaskContract.Tasks.SYNC_VERSION] is used to store the
* SEQUENCE and should not be used for the eTag.
*/
private fun update_7_8() {
TaskProvider.acquire(context, TaskProvider.ProviderName.OpenTasks)?.use { provider ->
// ETag is now in sync_version instead of sync1
// UID is now in _uid instead of sync2
provider.client.query(provider.tasksUri().asSyncAdapter(account),
arrayOf(TaskContract.Tasks._ID, TaskContract.Tasks.SYNC1, TaskContract.Tasks.SYNC2),
"${TaskContract.Tasks.ACCOUNT_TYPE}=? AND ${TaskContract.Tasks.ACCOUNT_NAME}=?",
arrayOf(account.type, account.name), null)!!.use { cursor ->
while (cursor.moveToNext()) {
val id = cursor.getLong(0)
val eTag = cursor.getString(1)
val uid = cursor.getString(2)
val values = ContentValues(4)
values.put(TaskContract.Tasks._UID, uid)
values.put(TaskContract.Tasks.SYNC_VERSION, eTag)
values.putNull(TaskContract.Tasks.SYNC1)
values.putNull(TaskContract.Tasks.SYNC2)
Logger.log.log(Level.FINER, "Updating task $id", values)
provider.client.update(
ContentUris.withAppendedId(provider.tasksUri(), id).asSyncAdapter(account),
values, null, null)
}
}
}
}
@Suppress("unused")
@SuppressLint("Recycle")
private fun update_6_7() {
// add calendar colors
context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)?.let { provider ->
try {
AndroidCalendar.insertColors(provider, account)
} finally {
provider.closeCompat()
}
}
// update allowed WiFi settings key
val onlySSID = accountManager.getUserData(account, "wifi_only_ssid")
accountManager.setUserData(account, AccountSettings.KEY_WIFI_ONLY_SSIDS, onlySSID)
accountManager.setUserData(account, "wifi_only_ssid", null)
}
@Suppress("unused")
@SuppressLint("Recycle", "ParcelClassLoader")
private fun update_5_6() {
context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)?.let { provider ->
val parcel = Parcel.obtain()
try {
// don't run syncs during the migration
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 0)
ContentResolver.setIsSyncable(account, context.getString(R.string.address_books_authority), 0)
ContentResolver.cancelSync(account, null)
// get previous address book settings (including URL)
val raw = ContactsContract.SyncState.get(provider, account)
if (raw == null)
Logger.log.info("No contacts sync state, ignoring account")
else {
parcel.unmarshall(raw, 0, raw.size)
parcel.setDataPosition(0)
val params = parcel.readBundle()!!
val url = params.getString("url")?.toHttpUrlOrNull()
if (url == null)
Logger.log.info("No address book URL, ignoring account")
else {
// create new address book
val info = Collection(url = url, type = Collection.TYPE_ADDRESSBOOK, displayName = account.name)
Logger.log.log(Level.INFO, "Creating new address book account", url)
val addressBookAccount = Account(
LocalAddressBook.accountName(account, info), context.getString(
R.string.account_type_address_book))
if (!accountManager.addAccountExplicitly(addressBookAccount, null, LocalAddressBook.initialUserData(account, info.url.toString())))
throw ContactsStorageException("Couldn't create address book account")
// move contacts to new address book
Logger.log.info("Moving contacts from $account to $addressBookAccount")
val newAccount = ContentValues(2)
newAccount.put(ContactsContract.RawContacts.ACCOUNT_NAME, addressBookAccount.name)
newAccount.put(ContactsContract.RawContacts.ACCOUNT_TYPE, addressBookAccount.type)
val affected = provider.update(
ContactsContract.RawContacts.CONTENT_URI.buildUpon()
.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, account.name)
.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, account.type)
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build(),
newAccount,
"${ContactsContract.RawContacts.ACCOUNT_NAME}=? AND ${ContactsContract.RawContacts.ACCOUNT_TYPE}=?",
arrayOf(account.name, account.type))
Logger.log.info("$affected contacts moved to new address book")
}
ContactsContract.SyncState.set(provider, account, null)
}
} catch(e: RemoteException) {
throw ContactsStorageException("Couldn't migrate contacts to new address book", e)
} finally {
parcel.recycle()
provider.closeCompat()
}
}
// update version number so that further syncs don't repeat the migration
accountManager.setUserData(account, AccountSettings.KEY_SETTINGS_VERSION, "6")
// request sync of new address book account
ContentResolver.setIsSyncable(account, context.getString(R.string.address_books_authority), 1)
accountSettings.setSyncInterval(context.getString(R.string.address_books_authority), 4*3600)
}
/* Android 7.1.1 OpenTasks fix */
@Suppress("unused")
private fun update_4_5() {
// call PackageChangedReceiver which then enables/disables OpenTasks sync when it's (not) available
SyncUtils.updateTaskSync(context)
}
@Suppress("unused")
private fun update_3_4() {
accountSettings.setGroupMethod(GroupMethod.CATEGORIES)
}
// updates from AccountSettings version 2 and below are not supported anymore
}

View file

@ -55,7 +55,7 @@ class PeriodicSyncWorker @AssistedInject constructor(
* @param interval interval between recurring syncs in seconds
* @return operation object to check when and whether activation was successful
*/
fun enable(context: Context, account: Account, authority: String, interval: Long): Operation {
fun enable(context: Context, account: Account, authority: String, interval: Long, syncWifiOnly: Boolean): Operation {
val arguments = Data.Builder()
.putString(ARG_AUTHORITY, authority)
.putString(ARG_ACCOUNT_NAME, account.name)
@ -63,7 +63,7 @@ class PeriodicSyncWorker @AssistedInject constructor(
.build()
val constraints = Constraints.Builder()
.setRequiredNetworkType(
if (AccountSettings(context, account).getSyncWifiOnly())
if (syncWifiOnly)
NetworkType.UNMETERED
else
NetworkType.CONNECTED