From e0826633bbcbad70b35349f8c5a8fd9f7be41901 Mon Sep 17 00:00:00 2001 From: Mariotaku Lee Date: Sun, 1 Jan 2017 01:13:21 +0800 Subject: [PATCH] file based drafts/filters sync now working --- .../mariotaku/twidere/TwidereConstants.java | 6 +- .../twidere/constant/IntentConstants.java | 2 + .../org/mariotaku/twidere/model/Draft.java | 14 +- .../mariotaku/twidere/model/FiltersData.java | 39 ++++ .../twidere/model/ParcelableStatusUpdate.java | 3 + .../twidere/provider/TwidereDataStore.java | 2 + twidere/build.gradle | 2 +- .../twidere/service/DropboxDataSyncService.kt | 160 +++++++------- .../support/v4/app/ListFragmentAccessor.java | 9 + .../edu/tsinghua/hotmobi/model/BaseEvent.java | 1 + .../java/org/mariotaku/twidere/Constants.java | 3 +- .../util/ParcelableStatusUpdateUtils.java | 38 ---- .../model/util/ParcelableStatusUpdateUtils.kt | 38 ++++ .../twidere/util/ActivityTracker.java | 3 +- .../org/mariotaku/twidere/util/Utils.java | 5 +- .../ktextension/CollectionExtensions.kt | 8 +- .../ktextension/SparseArrayExtensions.kt | 5 + .../twidere/activity/ComposeActivity.kt | 23 +- .../twidere/activity/LinkHandlerActivity.kt | 8 + .../extension/model/DraftExtensions.kt | 17 +- .../extension/model/FiltersDataExtensions.kt | 61 ++--- .../twidere/fragment/BaseListFragment.kt | 17 +- .../filter/BaseFiltersImportFragment.kt | 3 +- .../fragment/filter/FilteredUsersFragment.kt | 4 +- .../filter/FiltersSubscriptionsFragment.kt | 14 ++ .../service/BackgroundOperationService.kt | 8 +- .../twidere/task/twitter/UpdateStatusTask.kt | 1 + .../util/sync/FileBasedDraftsSyncHelper.kt | 209 ++++++++++++++++++ .../sync/FileBasedFiltersDataSyncHelper.kt | 93 ++++++++ .../twidere/util/sync/SyncHelperCommons.kt | 17 ++ twidere/src/main/res/values/strings.xml | 3 +- .../src/main/res/xml/preferences_filters.xml | 9 +- 32 files changed, 656 insertions(+), 169 deletions(-) create mode 100644 twidere/src/main/java/android/support/v4/app/ListFragmentAccessor.java delete mode 100644 twidere/src/main/java/org/mariotaku/twidere/model/util/ParcelableStatusUpdateUtils.java create mode 100644 twidere/src/main/java/org/mariotaku/twidere/model/util/ParcelableStatusUpdateUtils.kt create mode 100644 twidere/src/main/kotlin/org/mariotaku/twidere/fragment/filter/FiltersSubscriptionsFragment.kt create mode 100644 twidere/src/main/kotlin/org/mariotaku/twidere/util/sync/FileBasedDraftsSyncHelper.kt create mode 100644 twidere/src/main/kotlin/org/mariotaku/twidere/util/sync/FileBasedFiltersDataSyncHelper.kt create mode 100644 twidere/src/main/kotlin/org/mariotaku/twidere/util/sync/SyncHelperCommons.kt diff --git a/twidere.component.common/src/main/java/org/mariotaku/twidere/TwidereConstants.java b/twidere.component.common/src/main/java/org/mariotaku/twidere/TwidereConstants.java index fcc4ac0b5..64c080aa5 100644 --- a/twidere.component.common/src/main/java/org/mariotaku/twidere/TwidereConstants.java +++ b/twidere.component.common/src/main/java/org/mariotaku/twidere/TwidereConstants.java @@ -120,10 +120,12 @@ public interface TwidereConstants extends SharedPreferenceConstants, IntentConst String AUTHORITY_ACCOUNTS = "accounts"; String AUTHORITY_DRAFTS = "drafts"; String AUTHORITY_FILTERS = "filters"; - String AUTHORITY_FILTERS_IMPORT_BLOCKS = "filters/import/blocks"; - String AUTHORITY_FILTERS_IMPORT_MUTES = "filters/import/mutes"; String AUTHORITY_PROFILE_EDITOR = "profile_editor"; + String PATH_FILTERS_IMPORT_BLOCKS = "import/blocks"; + String PATH_FILTERS_IMPORT_MUTES = "import/mutes"; + String PATH_FILTERS_SUBSCRIPTIONS = "subscriptions"; + String QUERY_PARAM_ACCOUNT_KEY = "account_key"; String QUERY_PARAM_ACCOUNT_HOST = "account_host"; String QUERY_PARAM_ACCOUNT_NAME = "account_name"; diff --git a/twidere.component.common/src/main/java/org/mariotaku/twidere/constant/IntentConstants.java b/twidere.component.common/src/main/java/org/mariotaku/twidere/constant/IntentConstants.java index f5fe0e880..582006ee2 100644 --- a/twidere.component.common/src/main/java/org/mariotaku/twidere/constant/IntentConstants.java +++ b/twidere.component.common/src/main/java/org/mariotaku/twidere/constant/IntentConstants.java @@ -212,4 +212,6 @@ public interface IntentConstants { String EXTRA_OBJECT = "object"; String EXTRA_SIMPLE_LAYOUT = "simple_layout"; String EXTRA_API_CONFIG = "api_config"; + String EXTRA_COUNT = "count"; + } diff --git a/twidere.component.common/src/main/java/org/mariotaku/twidere/model/Draft.java b/twidere.component.common/src/main/java/org/mariotaku/twidere/model/Draft.java index 3c324e9ca..bec8685d2 100644 --- a/twidere.component.common/src/main/java/org/mariotaku/twidere/model/Draft.java +++ b/twidere.component.common/src/main/java/org/mariotaku/twidere/model/Draft.java @@ -28,6 +28,7 @@ import com.hannesdorfmann.parcelableplease.annotation.ParcelablePlease; import com.hannesdorfmann.parcelableplease.annotation.ParcelableThisPlease; import org.mariotaku.commons.objectcursor.LoganSquareCursorFieldConverter; +import org.mariotaku.library.objectcursor.annotation.AfterCursorObjectCreated; import org.mariotaku.library.objectcursor.annotation.CursorField; import org.mariotaku.library.objectcursor.annotation.CursorObject; import org.mariotaku.twidere.model.draft.ActionExtras; @@ -38,6 +39,7 @@ import org.mariotaku.twidere.provider.TwidereDataStore.Drafts; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.UUID; @ParcelablePlease @CursorObject(valuesCreator = true, tableInfo = true) @@ -70,7 +72,10 @@ public class Draft implements Parcelable { @ParcelableThisPlease @CursorField(value = Drafts.ACTION_EXTRAS, converter = DraftExtrasConverter.class) public ActionExtras action_extras; - + @Nullable + @ParcelableThisPlease + @CursorField(value = Drafts.UNIQUE_ID) + public String unique_id; public Draft() { @@ -86,6 +91,13 @@ public class Draft implements Parcelable { DraftParcelablePlease.writeToParcel(this, dest, flags); } + @AfterCursorObjectCreated + void afterCursorObjectCreated() { + if (unique_id == null) { + unique_id = UUID.nameUUIDFromBytes((_id + ":" + timestamp).getBytes()).toString(); + } + } + public static final Creator CREATOR = new Creator() { @Override public Draft createFromParcel(Parcel source) { diff --git a/twidere.component.common/src/main/java/org/mariotaku/twidere/model/FiltersData.java b/twidere.component.common/src/main/java/org/mariotaku/twidere/model/FiltersData.java index d626f64c1..58332108f 100644 --- a/twidere.component.common/src/main/java/org/mariotaku/twidere/model/FiltersData.java +++ b/twidere.component.common/src/main/java/org/mariotaku/twidere/model/FiltersData.java @@ -10,6 +10,7 @@ import org.mariotaku.twidere.model.util.UserKeyCursorFieldConverter; import org.mariotaku.twidere.provider.TwidereDataStore; import org.mariotaku.twidere.provider.TwidereDataStore.Filters; +import java.util.Collection; import java.util.List; /** @@ -69,6 +70,44 @@ public class FiltersData { '}'; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FiltersData that = (FiltersData) o; + + if (!contentEquals(users, that.users)) return false; + if (!contentEquals(keywords, that.keywords)) + return false; + if (!contentEquals(sources, that.sources)) return false; + if (!contentEquals(links, that.links)) return false; + return true; + + } + + private static boolean contentEquals(Collection collection1, Collection collection2) { + if (collection1 == collection2) return true; + if (collection1 == null || collection2 == null) return false; + if (collection1.size() != collection2.size()) return false; + for (E item1 : collection1) { + if (!collection2.contains(item1)) return false; + } + for (E item2 : collection2) { + if (!collection1.contains(item2)) return false; + } + return true; + } + + @Override + public int hashCode() { + int result = users != null ? users.hashCode() : 0; + result = 31 * result + (keywords != null ? keywords.hashCode() : 0); + result = 31 * result + (sources != null ? sources.hashCode() : 0); + result = 31 * result + (links != null ? links.hashCode() : 0); + return result; + } + @JsonObject @CursorObject(valuesCreator = true, tableInfo = true) public static class UserItem { diff --git a/twidere.component.common/src/main/java/org/mariotaku/twidere/model/ParcelableStatusUpdate.java b/twidere.component.common/src/main/java/org/mariotaku/twidere/model/ParcelableStatusUpdate.java index 5c8043859..5b060a498 100644 --- a/twidere.component.common/src/main/java/org/mariotaku/twidere/model/ParcelableStatusUpdate.java +++ b/twidere.component.common/src/main/java/org/mariotaku/twidere/model/ParcelableStatusUpdate.java @@ -63,6 +63,9 @@ public class ParcelableStatusUpdate implements Parcelable { @JsonField(name = "attachment_url") @ParcelableThisPlease public String attachment_url; + @JsonField(name = "draft_unique_id") + @ParcelableThisPlease + public String draft_unique_id; public ParcelableStatusUpdate() { } diff --git a/twidere.component.common/src/main/java/org/mariotaku/twidere/provider/TwidereDataStore.java b/twidere.component.common/src/main/java/org/mariotaku/twidere/provider/TwidereDataStore.java index 650f0122e..52ead7891 100644 --- a/twidere.component.common/src/main/java/org/mariotaku/twidere/provider/TwidereDataStore.java +++ b/twidere.component.common/src/main/java/org/mariotaku/twidere/provider/TwidereDataStore.java @@ -553,6 +553,8 @@ public interface TwidereDataStore { String ACTION_EXTRAS = "action_extras"; + String UNIQUE_ID = "unique_id"; + String[] COLUMNS = DraftTableInfo.COLUMNS; String[] TYPES = DraftTableInfo.TYPES; diff --git a/twidere/build.gradle b/twidere/build.gradle index 4e260a19f..8c3b0f3e9 100644 --- a/twidere/build.gradle +++ b/twidere/build.gradle @@ -171,7 +171,7 @@ dependencies { compile "com.github.mariotaku.CommonsLibrary:text:$mariotaku_commons_library_version" compile "com.github.mariotaku.CommonsLibrary:text-kotlin:$mariotaku_commons_library_version" compile 'com.github.mariotaku:KPreferences:0.9.5' - compile 'com.github.mariotaku:Chameleon:0.9.3' + compile 'com.github.mariotaku:Chameleon:0.9.4' compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" compile 'nl.komponents.kovenant:kovenant:3.3.0' compile 'nl.komponents.kovenant:kovenant-android:3.3.0' diff --git a/twidere/src/google/kotlin/org/mariotaku/twidere/service/DropboxDataSyncService.kt b/twidere/src/google/kotlin/org/mariotaku/twidere/service/DropboxDataSyncService.kt index a5b6f1578..1a110a9d9 100644 --- a/twidere/src/google/kotlin/org/mariotaku/twidere/service/DropboxDataSyncService.kt +++ b/twidere/src/google/kotlin/org/mariotaku/twidere/service/DropboxDataSyncService.kt @@ -4,20 +4,26 @@ import android.app.NotificationManager import android.content.Context import android.content.Intent import android.support.v7.app.NotificationCompat +import android.util.Log import android.util.Xml import com.dropbox.core.DbxRequestConfig +import com.dropbox.core.NetworkIOException import com.dropbox.core.v2.DbxClientV2 +import com.dropbox.core.v2.files.DeleteArg +import com.dropbox.core.v2.files.FileMetadata +import com.dropbox.core.v2.files.ListFolderResult import com.dropbox.core.v2.files.WriteMode import org.mariotaku.kpreferences.get -import org.mariotaku.ktextension.map import org.mariotaku.twidere.BuildConfig import org.mariotaku.twidere.R import org.mariotaku.twidere.dropboxAuthTokenKey import org.mariotaku.twidere.extension.model.* -import org.mariotaku.twidere.model.DraftCursorIndices +import org.mariotaku.twidere.model.Draft import org.mariotaku.twidere.model.FiltersData -import org.mariotaku.twidere.provider.TwidereDataStore.Drafts -import java.io.* +import org.mariotaku.twidere.util.sync.FileBasedDraftsSyncHelper +import org.mariotaku.twidere.util.sync.FileBasedFiltersDataSyncHelper +import java.io.IOException +import java.util.* /** * Created by mariotaku on 2016/12/7. @@ -44,98 +50,102 @@ class DropboxDataSyncService : BaseIntentService("dropbox_data_sync") { } private fun uploadDrafts(client: DbxClientV2) { - val cur = contentResolver.query(Drafts.CONTENT_URI, Drafts.COLUMNS, null, null, null) ?: return - cur.map(DraftCursorIndices(cur)).forEach { draft -> - client.newUploader("/Drafts/${draft.timestamp}.eml").use { - draft.writeMimeMessageTo(this, it.outputStream) - it.finish() - } + val helper = DropboxDraftsSyncHelper(this, client) + try { + helper.performSync() + } catch (e: IOException) { + Log.w(LOGTAG, e) } - cur.close() } @Throws(IOException::class) private fun syncFilters(client: DbxClientV2) { val helper = DropboxFiltersDataSyncHelper(this, client) - helper.sync() + try { + helper.performSync() + } catch (e: IOException) { + Log.w(LOGTAG, e) + } } - abstract class FileBasedFiltersDataSyncHelper(val context: Context) { + class DropboxDraftsSyncHelper(context: Context, val client: DbxClientV2) : FileBasedDraftsSyncHelper(context) { @Throws(IOException::class) - protected abstract fun loadFromRemote(): FiltersData - - @Throws(IOException::class) - protected abstract fun saveToRemote(data: FiltersData) - - fun sync() { - val remoteFilters: FiltersData = loadFromRemote() - - val filters: FiltersData = FiltersData().apply { - read(context.contentResolver) - initFields() - } - - val syncDataDir: File = context.syncDataDir.apply { - if (!exists()) { - mkdirs() - } - } - val snapshotFile = File(syncDataDir, "filters.xml") - val deletedFilters: FiltersData? = try { - FileReader(snapshotFile).use { - val result = FiltersData() - val parser = Xml.newPullParser() - parser.setInput(it) - result.parse(parser) - result.removeAll(filters) - return@use result - } - } catch (e: FileNotFoundException) { - null - } - - filters.addAll(remoteFilters, true) - - if (deletedFilters != null) { - filters.removeAll(deletedFilters) - } - - filters.write(context.contentResolver) - - saveToRemote(filters) + override fun Draft.saveToRemote(): FileMetadata { try { - FileWriter(snapshotFile).use { - val serializer = Xml.newSerializer() - serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true) - serializer.setOutput(it) - filters.serialize(serializer) + client.newUploader("/Drafts/$filename", this.timestamp).use { + this.writeMimeMessageTo(context, it.outputStream) + return it.finish() } - } catch (e: FileNotFoundException) { - // Ignore + } catch (e: NetworkIOException) { + throw IOException(e) } } - private val Context.syncDataDir: File - get() = File(filesDir, "sync_data") + @Throws(IOException::class) + override fun Draft.loadFromRemote(info: FileMetadata): Boolean { + try { + client.files().download(info.pathLower).use { + val parsed = this.readMimeMessageFrom(context, it.inputStream) + if (parsed) { + this.timestamp = info.draftTimestamp + this.unique_id = info.draftFileName.substringBeforeLast(".eml") + } + return parsed + } + } catch (e: NetworkIOException) { + throw IOException(e) + } + } + + override fun removeDrafts(list: List): Boolean { + return client.files().deleteBatch(list.map { DeleteArg(it.pathLower) }) != null + } + + override fun removeDraft(info: FileMetadata): Boolean { + return client.files().delete(info.pathLower) != null + } + + override val FileMetadata.draftTimestamp: Long get() = this.clientModified.time + + override val FileMetadata.draftFileName: String get() = this.name + + override fun listRemoteDrafts(): List { + val result = ArrayList() + var listResult: ListFolderResult = client.files().listFolder("/Drafts/") + while (true) { + // Do something with files + listResult.entries.mapNotNullTo(result) { it as? FileMetadata } + if (!listResult.hasMore) break + listResult = client.files().listFolderContinue(listResult.cursor) + } + return result + } + } class DropboxFiltersDataSyncHelper(context: Context, val client: DbxClientV2) : FileBasedFiltersDataSyncHelper(context) { - override fun loadFromRemote(): FiltersData = client.newDownloader("/Common/filters.xml").use { downloader -> - val result = FiltersData() - val parser = Xml.newPullParser() - parser.setInput(downloader.inputStream, "UTF-8") - result.parse(parser) - result.initFields() - return@use result + override fun FiltersData.loadFromRemote(snapshotModifiedMillis: Long): Boolean { + client.newDownloader("/Common/filters.xml").use { downloader -> + // Local file is the same with remote version + if (Math.abs(downloader.result.clientModified.time - snapshotModifiedMillis) < 1000) { + return false + } + val parser = Xml.newPullParser() + parser.setInput(downloader.inputStream, "UTF-8") + this.parse(parser) + this.initFields() + return true + } } - override fun saveToRemote(data: FiltersData) { - client.newUploader("/Common/filters.xml").use { uploader -> + override fun FiltersData.saveToRemote(localModifiedTime: Long): Boolean { + client.newUploader("/Common/filters.xml", localModifiedTime).use { uploader -> val serializer = Xml.newSerializer() serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true) serializer.setOutput(uploader.outputStream, "UTF-8") - data.serialize(serializer) + this.serialize(serializer) uploader.finish() + return true } } @@ -143,6 +153,8 @@ class DropboxDataSyncService : BaseIntentService("dropbox_data_sync") { } +private const val LOGTAG = "Twidere.DBSync" +private fun DbxClientV2.newUploader(path: String, clientModified: Long) = files().uploadBuilder(path) + .withMode(WriteMode.OVERWRITE).withMute(true).withClientModified(Date(clientModified)).start() -private fun DbxClientV2.newUploader(path: String) = files().uploadBuilder(path).withMode(WriteMode.OVERWRITE).withMute(true).start() private fun DbxClientV2.newDownloader(path: String) = files().downloadBuilder(path).start() diff --git a/twidere/src/main/java/android/support/v4/app/ListFragmentAccessor.java b/twidere/src/main/java/android/support/v4/app/ListFragmentAccessor.java new file mode 100644 index 000000000..07067e20a --- /dev/null +++ b/twidere/src/main/java/android/support/v4/app/ListFragmentAccessor.java @@ -0,0 +1,9 @@ +package android.support.v4.app; + +/** + * Created by mariotaku on 2016/12/31. + */ + +public class ListFragmentAccessor { + public static final int INTERNAL_PROGRESS_CONTAINER_ID = ListFragment.INTERNAL_PROGRESS_CONTAINER_ID; +} diff --git a/twidere/src/main/java/edu/tsinghua/hotmobi/model/BaseEvent.java b/twidere/src/main/java/edu/tsinghua/hotmobi/model/BaseEvent.java index ef2b6a86b..4035b914e 100644 --- a/twidere/src/main/java/edu/tsinghua/hotmobi/model/BaseEvent.java +++ b/twidere/src/main/java/edu/tsinghua/hotmobi/model/BaseEvent.java @@ -94,6 +94,7 @@ public abstract class BaseEvent implements Parcelable, LogModel { } public void markStart(@NonNull Context context) { + if (!BuildConfig.HOTMOBI_LOG_ENABLED) return; setStartTime(System.currentTimeMillis()); setTimeOffset(TimeZone.getDefault().getOffset(startTime)); setLocation(LocationUtils.getCachedLatLng(context)); diff --git a/twidere/src/main/java/org/mariotaku/twidere/Constants.java b/twidere/src/main/java/org/mariotaku/twidere/Constants.java index c6dd3094d..de1181a5c 100644 --- a/twidere/src/main/java/org/mariotaku/twidere/Constants.java +++ b/twidere/src/main/java/org/mariotaku/twidere/Constants.java @@ -34,7 +34,7 @@ import static org.mariotaku.twidere.annotation.PreferenceType.STRING; public interface Constants extends TwidereConstants { String DATABASES_NAME = "twidere.sqlite"; - int DATABASES_VERSION = 156; + int DATABASES_VERSION = 157; int MENU_GROUP_STATUS_EXTENSION = 10; int MENU_GROUP_COMPOSE_EXTENSION = 11; @@ -78,6 +78,7 @@ public interface Constants extends TwidereConstants { int LINK_ID_FILTERS = 110; int LINK_ID_FILTERS_IMPORT_BLOCKS = 111; int LINK_ID_FILTERS_IMPORT_MUTES = 112; + int LINK_ID_FILTERS_SUBSCRIPTIONS = 113; int LINK_ID_PROFILE_EDITOR = 121; String TWIDERE_PREVIEW_NICKNAME = "Twidere"; diff --git a/twidere/src/main/java/org/mariotaku/twidere/model/util/ParcelableStatusUpdateUtils.java b/twidere/src/main/java/org/mariotaku/twidere/model/util/ParcelableStatusUpdateUtils.java deleted file mode 100644 index c7be16e99..000000000 --- a/twidere/src/main/java/org/mariotaku/twidere/model/util/ParcelableStatusUpdateUtils.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.mariotaku.twidere.model.util; - -import android.accounts.AccountManager; -import android.content.Context; - -import org.mariotaku.twidere.model.AccountDetails; -import org.mariotaku.twidere.model.Draft; -import org.mariotaku.twidere.model.ParcelableStatusUpdate; -import org.mariotaku.twidere.model.draft.UpdateStatusActionExtras; - -/** - * Created by mariotaku on 16/2/12. - */ -public class ParcelableStatusUpdateUtils { - private ParcelableStatusUpdateUtils() { - } - - public static ParcelableStatusUpdate fromDraftItem(final Context context, final Draft draft) { - ParcelableStatusUpdate statusUpdate = new ParcelableStatusUpdate(); - if (draft.account_keys != null) { - statusUpdate.accounts = AccountUtils.getAllAccountDetails(AccountManager.get(context), draft.account_keys, true); - } else { - statusUpdate.accounts = new AccountDetails[0]; - } - statusUpdate.text = draft.text; - statusUpdate.location = draft.location; - statusUpdate.media = draft.media; - if (draft.action_extras instanceof UpdateStatusActionExtras) { - final UpdateStatusActionExtras extra = (UpdateStatusActionExtras) draft.action_extras; - statusUpdate.in_reply_to_status = extra.getInReplyToStatus(); - statusUpdate.is_possibly_sensitive = extra.isPossiblySensitive(); - statusUpdate.display_coordinates = extra.getDisplayCoordinates(); - statusUpdate.attachment_url = extra.getAttachmentUrl(); - } - return statusUpdate; - } - -} diff --git a/twidere/src/main/java/org/mariotaku/twidere/model/util/ParcelableStatusUpdateUtils.kt b/twidere/src/main/java/org/mariotaku/twidere/model/util/ParcelableStatusUpdateUtils.kt new file mode 100644 index 000000000..3e4fdae0b --- /dev/null +++ b/twidere/src/main/java/org/mariotaku/twidere/model/util/ParcelableStatusUpdateUtils.kt @@ -0,0 +1,38 @@ +package org.mariotaku.twidere.model.util + +import android.accounts.AccountManager +import android.content.Context +import org.mariotaku.twidere.extension.model.unique_id_non_null + +import org.mariotaku.twidere.model.AccountDetails +import org.mariotaku.twidere.model.Draft +import org.mariotaku.twidere.model.ParcelableStatusUpdate +import org.mariotaku.twidere.model.draft.UpdateStatusActionExtras + +/** + * Created by mariotaku on 16/2/12. + */ +object ParcelableStatusUpdateUtils { + + fun fromDraftItem(context: Context, draft: Draft): ParcelableStatusUpdate { + val statusUpdate = ParcelableStatusUpdate() + if (draft.account_keys != null) { + statusUpdate.accounts = AccountUtils.getAllAccountDetails(AccountManager.get(context), draft.account_keys!!, true) + } else { + statusUpdate.accounts = arrayOfNulls(0) + } + statusUpdate.text = draft.text + statusUpdate.location = draft.location + statusUpdate.media = draft.media + if (draft.action_extras is UpdateStatusActionExtras) { + val extra = draft.action_extras as UpdateStatusActionExtras? + statusUpdate.in_reply_to_status = extra!!.inReplyToStatus + statusUpdate.is_possibly_sensitive = extra.isPossiblySensitive + statusUpdate.display_coordinates = extra.displayCoordinates + statusUpdate.attachment_url = extra.attachmentUrl + } + statusUpdate.draft_unique_id = draft.unique_id_non_null + return statusUpdate + } + +} diff --git a/twidere/src/main/java/org/mariotaku/twidere/util/ActivityTracker.java b/twidere/src/main/java/org/mariotaku/twidere/util/ActivityTracker.java index e002c06b7..e9c1f9a9a 100644 --- a/twidere/src/main/java/org/mariotaku/twidere/util/ActivityTracker.java +++ b/twidere/src/main/java/org/mariotaku/twidere/util/ActivityTracker.java @@ -27,6 +27,7 @@ import android.os.Bundle; import org.apache.commons.collections.primitives.ArrayIntList; import org.apache.commons.collections.primitives.IntList; +import org.mariotaku.twidere.BuildConfig; import org.mariotaku.twidere.activity.HomeActivity; import edu.tsinghua.hotmobi.HotMobiLogger; @@ -66,7 +67,7 @@ public class ActivityTracker implements Application.ActivityLifecycleCallbacks { mHomeActivityStarted = true; } // BEGIN HotMobi - if (mSessionEvent == null) { + if (mSessionEvent == null && BuildConfig.HOTMOBI_LOG_ENABLED) { mSessionEvent = SessionEvent.create(activity); } // END HotMobi diff --git a/twidere/src/main/java/org/mariotaku/twidere/util/Utils.java b/twidere/src/main/java/org/mariotaku/twidere/util/Utils.java index 918d0c73c..c32dc1e1c 100644 --- a/twidere/src/main/java/org/mariotaku/twidere/util/Utils.java +++ b/twidere/src/main/java/org/mariotaku/twidere/util/Utils.java @@ -196,8 +196,9 @@ public final class Utils implements Constants { LINK_HANDLER_URI_MATCHER.addURI(AUTHORITY_ACCOUNTS, null, LINK_ID_ACCOUNTS); LINK_HANDLER_URI_MATCHER.addURI(AUTHORITY_DRAFTS, null, LINK_ID_DRAFTS); LINK_HANDLER_URI_MATCHER.addURI(AUTHORITY_FILTERS, null, LINK_ID_FILTERS); - LINK_HANDLER_URI_MATCHER.addURI(AUTHORITY_FILTERS_IMPORT_BLOCKS, null, LINK_ID_FILTERS_IMPORT_BLOCKS); - LINK_HANDLER_URI_MATCHER.addURI(AUTHORITY_FILTERS_IMPORT_MUTES, null, LINK_ID_FILTERS_IMPORT_MUTES); + LINK_HANDLER_URI_MATCHER.addURI(AUTHORITY_FILTERS, PATH_FILTERS_IMPORT_BLOCKS, LINK_ID_FILTERS_IMPORT_BLOCKS); + LINK_HANDLER_URI_MATCHER.addURI(AUTHORITY_FILTERS, PATH_FILTERS_IMPORT_MUTES, LINK_ID_FILTERS_IMPORT_MUTES); + LINK_HANDLER_URI_MATCHER.addURI(AUTHORITY_FILTERS, PATH_FILTERS_SUBSCRIPTIONS, LINK_ID_FILTERS_SUBSCRIPTIONS); LINK_HANDLER_URI_MATCHER.addURI(AUTHORITY_PROFILE_EDITOR, null, LINK_ID_PROFILE_EDITOR); HOME_TABS_URI_MATCHER.addURI(CustomTabType.HOME_TIMELINE, null, TAB_CODE_HOME_TIMELINE); diff --git a/twidere/src/main/kotlin/org/mariotaku/ktextension/CollectionExtensions.kt b/twidere/src/main/kotlin/org/mariotaku/ktextension/CollectionExtensions.kt index 9b25515ad..48d97d7dd 100644 --- a/twidere/src/main/kotlin/org/mariotaku/ktextension/CollectionExtensions.kt +++ b/twidere/src/main/kotlin/org/mariotaku/ktextension/CollectionExtensions.kt @@ -12,6 +12,10 @@ fun Collection<*>?.isNullOrEmpty(): Boolean { return this == null || this.isEmpty() } -fun MutableCollection.addAllIgnoreDuplicates(collection: Collection) { - addAll(collection.filter { it !in this }) +fun MutableCollection.addAllEnhanced(collection: Collection, ignoreDuplicates: Boolean): Boolean { + if (ignoreDuplicates) { + return addAll(collection.filter { it !in this }) + } else { + return addAll(collection) + } } \ No newline at end of file diff --git a/twidere/src/main/kotlin/org/mariotaku/ktextension/SparseArrayExtensions.kt b/twidere/src/main/kotlin/org/mariotaku/ktextension/SparseArrayExtensions.kt index ac5f9e730..5d2c698dc 100644 --- a/twidere/src/main/kotlin/org/mariotaku/ktextension/SparseArrayExtensions.kt +++ b/twidere/src/main/kotlin/org/mariotaku/ktextension/SparseArrayExtensions.kt @@ -1,5 +1,6 @@ package org.mariotaku.ktextension +import android.support.v4.util.LongSparseArray import android.util.SparseBooleanArray /** @@ -7,4 +8,8 @@ import android.util.SparseBooleanArray */ operator fun SparseBooleanArray.set(key: Int, value: Boolean) { put(key, value) +} + +operator fun LongSparseArray.set(key: Long, value: E) { + put(key, value) } \ No newline at end of file diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/activity/ComposeActivity.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/activity/ComposeActivity.kt index 044549f01..0c9b8b962 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/activity/ComposeActivity.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/activity/ComposeActivity.kt @@ -68,10 +68,12 @@ import org.mariotaku.ktextension.toTypedArray import org.mariotaku.twidere.BuildConfig import org.mariotaku.twidere.Constants.* import org.mariotaku.twidere.R +import org.mariotaku.twidere.TwidereConstants import org.mariotaku.twidere.adapter.ArrayRecyclerAdapter import org.mariotaku.twidere.adapter.BaseRecyclerViewAdapter import org.mariotaku.twidere.constant.* import org.mariotaku.twidere.extension.model.getAccountUser +import org.mariotaku.twidere.extension.model.unique_id_non_null import org.mariotaku.twidere.fragment.BaseDialogFragment import org.mariotaku.twidere.fragment.PermissionRequestDialog import org.mariotaku.twidere.fragment.PermissionRequestDialog.PermissionRequestCancelCallback @@ -134,6 +136,7 @@ class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener private var composeKeyMetaState: Int = 0 private var draft: Draft? = null private var nameFirst: Boolean = false + private var draftUniqueId: String? = null private var shouldSkipDraft: Boolean = false // Listeners @@ -220,6 +223,7 @@ class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener outState.putParcelable(EXTRA_DRAFT, draft) outState.putBoolean(EXTRA_SHOULD_SAVE_ACCOUNTS, shouldSaveAccounts) outState.putString(EXTRA_ORIGINAL_TEXT, originalText) + outState.putString(EXTRA_DRAFT_UNIQUE_ID, draftUniqueId) super.onSaveInstanceState(outState) } @@ -436,7 +440,7 @@ class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener fun saveToDrafts(): Uri { val text = editText.text.toString() val draft = Draft() - + draft.unique_id = this.draftUniqueId ?: UUID.randomUUID().toString() draft.action_type = getDraftAction(intent.action) draft.account_keys = accountsAdapter.selectedAccountKeys draft.text = text @@ -575,11 +579,12 @@ class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener if (mediaList != null) { addMedia(mediaList) } - inReplyToStatus = savedInstanceState.getParcelable(EXTRA_STATUS) - mentionUser = savedInstanceState.getParcelable(EXTRA_USER) - draft = savedInstanceState.getParcelable(EXTRA_DRAFT) + inReplyToStatus = savedInstanceState.getParcelable(EXTRA_STATUS) + mentionUser = savedInstanceState.getParcelable(EXTRA_USER) + draft = savedInstanceState.getParcelable(EXTRA_DRAFT) shouldSaveAccounts = savedInstanceState.getBoolean(EXTRA_SHOULD_SAVE_ACCOUNTS) originalText = savedInstanceState.getString(EXTRA_ORIGINAL_TEXT) + draftUniqueId = savedInstanceState.getString(EXTRA_DRAFT_UNIQUE_ID) setLabel(intent) } else { // The context was first created @@ -842,6 +847,7 @@ class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener private fun handleEditDraftIntent(draft: Draft?): Boolean { if (draft == null) return false + draftUniqueId = draft.unique_id_non_null editText.setText(draft.text) val selectionEnd = editText.length() editText.setSelection(selectionEnd) @@ -854,6 +860,8 @@ class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener possiblySensitive = it.isPossiblySensitive inReplyToStatus = it.inReplyToStatus } + val tag = Uri.withAppendedPath(Drafts.CONTENT_URI, draft._id.toString()).toString() + notificationManager.cancel(tag, TwidereConstants.NOTIFICATION_ID_DRAFTS) return true } @@ -1908,9 +1916,10 @@ class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener companion object { // Constants - private val EXTRA_SHOULD_SAVE_ACCOUNTS = "should_save_accounts" - private val EXTRA_ORIGINAL_TEXT = "original_text" - private val DISCARD_STATUS_DIALOG_FRAGMENT_TAG = "discard_status" + private const val EXTRA_SHOULD_SAVE_ACCOUNTS = "should_save_accounts" + private const val EXTRA_ORIGINAL_TEXT = "original_text" + private const val EXTRA_DRAFT_UNIQUE_ID = "draft_unique_id" + private const val DISCARD_STATUS_DIALOG_FRAGMENT_TAG = "discard_status" val LOCATION_VALUE_PLACE = "place" val LOCATION_VALUE_COORDINATE = "coordinate" diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/activity/LinkHandlerActivity.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/activity/LinkHandlerActivity.kt index 0228a17d7..ccc97c5de 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/activity/LinkHandlerActivity.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/activity/LinkHandlerActivity.kt @@ -51,6 +51,7 @@ import org.mariotaku.twidere.fragment.* import org.mariotaku.twidere.fragment.filter.FiltersFragment import org.mariotaku.twidere.fragment.filter.FiltersImportBlocksFragment import org.mariotaku.twidere.fragment.filter.FiltersImportMutesFragment +import org.mariotaku.twidere.fragment.filter.FiltersSubscriptionsFragment import org.mariotaku.twidere.fragment.iface.IBaseFragment import org.mariotaku.twidere.fragment.iface.IBaseFragment.SystemWindowsInsetsCallback import org.mariotaku.twidere.fragment.iface.IToolBarSupportFragment @@ -387,6 +388,9 @@ class LinkHandlerActivity : BaseActivity(), SystemWindowsInsetsCallback, IContro LINK_ID_FILTERS_IMPORT_MUTES -> { title = getString(R.string.title_select_users) } + LINK_ID_FILTERS_SUBSCRIPTIONS -> { + title = getString(R.string.title_manage_filter_subscriptions) + } else -> { title = getString(R.string.app_name) } @@ -728,6 +732,10 @@ class LinkHandlerActivity : BaseActivity(), SystemWindowsInsetsCallback, IContro LINK_ID_FILTERS_IMPORT_MUTES -> { fragment = FiltersImportMutesFragment() } + LINK_ID_FILTERS_SUBSCRIPTIONS -> { + fragment = FiltersSubscriptionsFragment() + isAccountIdRequired = false + } else -> { return null } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/DraftExtensions.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/DraftExtensions.kt index f3e6b843e..0edfde1fe 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/DraftExtensions.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/DraftExtensions.kt @@ -92,12 +92,14 @@ fun Draft.writeMimeMessageTo(context: Context, st: OutputStream) { st.flush() } -fun Draft.readMimeMessageFrom(context: Context, st: InputStream) { +fun Draft.readMimeMessageFrom(context: Context, st: InputStream): Boolean { val config = MimeConfig() val parser = MimeStreamParser(config) parser.isContentDecoding = true - parser.setContentHandler(DraftContentHandler(context, this)) + val handler = DraftContentHandler(context, this) + parser.setContentHandler(handler) parser.parse(st) + return !handler.malformedData } fun Draft.getActionName(context: Context): String? { @@ -119,9 +121,16 @@ fun Draft.getActionName(context: Context): String? { return null } +val Draft.filename: String get() = "$unique_id_non_null.eml" + +val Draft.unique_id_non_null: String + get() = unique_id ?: UUID.nameUUIDFromBytes(("$_id:$timestamp").toByteArray()).toString() + private class DraftContentHandler(private val context: Context, private val draft: Draft) : SimpleContentHandler() { private val processingStack = Stack() private val mediaList: MutableList = ArrayList() + + internal var malformedData: Boolean = false override fun headers(header: Header) { if (processingStack.isEmpty()) { draft.timestamp = header.getField("Date")?.convert { @@ -159,6 +168,10 @@ private class DraftContentHandler(private val context: Context, private val draf } override fun body(bd: BodyDescriptor?, `is`: InputStream?) { + if (processingStack.isEmpty()) { + malformedData = true + return + } processingStack.peek().body(bd, `is`) } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/FiltersDataExtensions.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/FiltersDataExtensions.kt index bbb60c5bc..a444ec602 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/FiltersDataExtensions.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/FiltersDataExtensions.kt @@ -2,7 +2,7 @@ package org.mariotaku.twidere.extension.model import android.content.ContentResolver import android.net.Uri -import org.mariotaku.ktextension.addAllIgnoreDuplicates +import org.mariotaku.ktextension.addAllEnhanced import org.mariotaku.ktextension.convert import org.mariotaku.ktextension.map import org.mariotaku.sqliteqb.library.Expression @@ -150,31 +150,42 @@ fun FiltersData.parse(parser: XmlPullParser) { } -fun FiltersData.addAll(data: FiltersData, ignoreDuplicates: Boolean = false) { - this.users = mergeList(this.users, data.users, ignoreDuplicates = ignoreDuplicates) - this.keywords = mergeList(this.keywords, data.keywords, ignoreDuplicates = ignoreDuplicates) - this.sources = mergeList(this.sources, data.sources, ignoreDuplicates = ignoreDuplicates) - this.links = mergeList(this.links, data.links, ignoreDuplicates = ignoreDuplicates) -} - -fun FiltersData.removeAll(data: FiltersData) { - data.users?.let { this.users?.removeAll(it) } - data.keywords?.let { this.keywords?.removeAll(it) } - data.sources?.let { this.sources?.removeAll(it) } - data.links?.let { this.links?.removeAll(it) } -} - -private fun mergeList(vararg lists: List?, ignoreDuplicates: Boolean): List { - val result = ArrayList() - lists.forEach { - if (it == null) return@forEach - if (ignoreDuplicates) { - result.addAllIgnoreDuplicates(it) - } else { - result.addAll(it) - } +fun FiltersData.addAll(data: FiltersData, ignoreDuplicates: Boolean = false): Boolean { + var changed: Boolean = false + if (this.users != null) { + changed = changed or this.users.addAllEnhanced(collection = data.users, ignoreDuplicates = ignoreDuplicates) + } else { + this.users = data.users + changed = true } - return result + if (this.keywords != null) { + changed = changed or this.keywords.addAllEnhanced(collection = data.keywords, ignoreDuplicates = ignoreDuplicates) + } else { + this.keywords = data.keywords + changed = true + } + if (this.sources != null) { + changed = changed or this.sources.addAllEnhanced(collection = data.sources, ignoreDuplicates = ignoreDuplicates) + } else { + this.sources = data.sources + changed = true + } + if (this.links != null) { + changed = changed or this.links.addAllEnhanced(collection = data.links, ignoreDuplicates = ignoreDuplicates) + } else { + this.links = data.links + changed = true + } + return changed +} + +fun FiltersData.removeAll(data: FiltersData): Boolean { + var changed: Boolean = false + changed = changed or (data.users?.let { this.users?.removeAll(it) } ?: false) + changed = changed or (data.keywords?.let { this.keywords?.removeAll(it) } ?: false) + changed = changed or (data.sources?.let { this.sources?.removeAll(it) } ?: false) + changed = changed or (data.links?.let { this.links?.removeAll(it) } ?: false) + return changed } fun FiltersData.initFields() { diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/BaseListFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/BaseListFragment.kt index 5a29a9b74..776276a9a 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/BaseListFragment.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/BaseListFragment.kt @@ -24,8 +24,15 @@ import android.content.Context import android.content.SharedPreferences import android.os.Bundle import android.support.v4.app.ListFragment +import android.support.v4.app.ListFragmentAccessor +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup import android.widget.AbsListView import android.widget.AbsListView.OnScrollListener +import android.widget.ProgressBar +import org.mariotaku.chameleon.Chameleon +import org.mariotaku.chameleon.view.ChameleonProgressBar import org.mariotaku.twidere.app.TwidereApplication import org.mariotaku.twidere.constant.IntentConstants.EXTRA_TAB_POSITION import org.mariotaku.twidere.fragment.iface.RefreshScrollTopInterface @@ -103,7 +110,14 @@ open class BaseListFragment : ListFragment(), OnScrollListener, RefreshScrollTop activityFirstCreated = true } - fun onPostStart() { + override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view = super.onCreateView(inflater, container, savedInstanceState)!! + ((view.findViewById(ListFragmentAccessor.INTERNAL_PROGRESS_CONTAINER_ID) as ViewGroup).getChildAt(0) as ProgressBar).apply { + val appearance = ChameleonProgressBar.Appearance() + appearance.progressColor = Chameleon.getOverrideTheme(activity, activity).colorPrimary + ChameleonProgressBar.Appearance.apply(this, appearance) + } + return view } override fun onScroll(view: AbsListView, firstVisibleItem: Int, visibleItemCount: Int, @@ -129,7 +143,6 @@ open class BaseListFragment : ListFragment(), OnScrollListener, RefreshScrollTop override fun onStart() { super.onStart() - onPostStart() } override fun onStop() { diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/filter/BaseFiltersImportFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/filter/BaseFiltersImportFragment.kt index 17de84018..fce00d98c 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/filter/BaseFiltersImportFragment.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/filter/BaseFiltersImportFragment.kt @@ -26,6 +26,7 @@ import org.mariotaku.twidere.adapter.iface.IContentCardAdapter import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter.IndicatorPosition import org.mariotaku.twidere.constant.* +import org.mariotaku.twidere.constant.IntentConstants.EXTRA_COUNT import org.mariotaku.twidere.fragment.AbsContentListRecyclerViewFragment import org.mariotaku.twidere.fragment.BaseDialogFragment import org.mariotaku.twidere.fragment.MessageDialogFragment @@ -44,12 +45,10 @@ import java.lang.ref.WeakReference /** * Created by mariotaku on 2016/12/26. */ -private const val EXTRA_COUNT = "count" abstract class BaseFiltersImportFragment : AbsContentListRecyclerViewFragment(), LoaderManager.LoaderCallbacks?> { - protected var nextCursor: Long = -1 private set protected var prevCursor: Long = -1 diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/filter/FilteredUsersFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/filter/FilteredUsersFragment.kt index a5924a06f..731f0eba4 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/filter/FilteredUsersFragment.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/filter/FilteredUsersFragment.kt @@ -68,14 +68,14 @@ class FilteredUsersFragment : BaseFiltersFragment() { REQUEST_IMPORT_BLOCKS_SELECT_ACCOUNT -> { if (resultCode != FragmentActivity.RESULT_OK) return val intent = Intent(context, LinkHandlerActivity::class.java) - intent.data = Uri.Builder().scheme(SCHEME_TWIDERE).authority(AUTHORITY_FILTERS_IMPORT_BLOCKS).build() + intent.data = Uri.Builder().scheme(SCHEME_TWIDERE).authority(AUTHORITY_FILTERS).path(PATH_FILTERS_IMPORT_BLOCKS).build() intent.putExtra(EXTRA_ACCOUNT_KEY, data!!.getParcelableExtra(EXTRA_ACCOUNT_KEY)) startActivity(intent) } REQUEST_IMPORT_MUTES_SELECT_ACCOUNT -> { if (resultCode != FragmentActivity.RESULT_OK) return val intent = Intent(context, LinkHandlerActivity::class.java) - intent.data = Uri.Builder().scheme(SCHEME_TWIDERE).authority(AUTHORITY_FILTERS_IMPORT_MUTES).build() + intent.data = Uri.Builder().scheme(SCHEME_TWIDERE).authority(AUTHORITY_FILTERS).path(PATH_FILTERS_IMPORT_MUTES).build() intent.putExtra(EXTRA_ACCOUNT_KEY, data!!.getParcelableExtra(EXTRA_ACCOUNT_KEY)) startActivity(intent) } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/filter/FiltersSubscriptionsFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/filter/FiltersSubscriptionsFragment.kt new file mode 100644 index 000000000..879016719 --- /dev/null +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/filter/FiltersSubscriptionsFragment.kt @@ -0,0 +1,14 @@ +package org.mariotaku.twidere.fragment.filter + +import android.os.Bundle +import org.mariotaku.twidere.fragment.BaseListFragment + +/** + * Created by mariotaku on 2016/12/31. + */ +class FiltersSubscriptionsFragment : BaseListFragment() { + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + } +} \ No newline at end of file diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/service/BackgroundOperationService.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/service/BackgroundOperationService.kt index 39c61a905..3f5767136 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/service/BackgroundOperationService.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/service/BackgroundOperationService.kt @@ -130,11 +130,11 @@ class BackgroundOperationService : BaseIntentService("background_operation") { val where = Expression.equals(Drafts._ID, draftId) val cr = contentResolver val c = cr.query(Drafts.CONTENT_URI, Drafts.COLUMNS, where.sql, null, null) ?: return - val i = DraftCursorIndices(c) - val item: Draft - try { + @Suppress("ConvertTryFinallyToUseCall") + val item: Draft = try { + val i = DraftCursorIndices(c) if (!c.moveToFirst()) return - item = i.newObject(c) + i.newObject(c) } finally { c.close() } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/UpdateStatusTask.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/UpdateStatusTask.kt index bd764f5ce..c7abf0daf 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/UpdateStatusTask.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/UpdateStatusTask.kt @@ -516,6 +516,7 @@ class UpdateStatusTask( private fun saveDraft(@Draft.Action draftAction: String?, statusUpdate: ParcelableStatusUpdate): Long { val draft = Draft() + draft.unique_id = statusUpdate.draft_unique_id ?: UUID.randomUUID().toString() draft.account_keys = statusUpdate.accounts.map { it.key }.toTypedArray() draft.action_type = draftAction ?: Draft.Action.UPDATE_STATUS draft.text = statusUpdate.text diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/util/sync/FileBasedDraftsSyncHelper.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/util/sync/FileBasedDraftsSyncHelper.kt new file mode 100644 index 000000000..f5a500f99 --- /dev/null +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/util/sync/FileBasedDraftsSyncHelper.kt @@ -0,0 +1,209 @@ +package org.mariotaku.twidere.util.sync + +import android.content.Context +import android.support.v4.util.LongSparseArray +import android.util.Log +import org.mariotaku.ktextension.map +import org.mariotaku.ktextension.set +import org.mariotaku.sqliteqb.library.Expression +import org.mariotaku.twidere.BuildConfig +import org.mariotaku.twidere.extension.model.filename +import org.mariotaku.twidere.extension.model.unique_id_non_null +import org.mariotaku.twidere.model.Draft +import org.mariotaku.twidere.model.DraftCursorIndices +import org.mariotaku.twidere.model.DraftValuesCreator +import org.mariotaku.twidere.provider.TwidereDataStore.Drafts +import org.mariotaku.twidere.util.content.ContentResolverUtils +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException +import java.util.* + +/** + * Created by mariotaku on 2016/12/31. + */ + +abstract class FileBasedDraftsSyncHelper(val context: Context) { + fun performSync(): Boolean { + val syncDataDir: File = context.syncDataDir.mkdirIfNotExists() ?: return false + val snapshotsListFile = File(syncDataDir, "draft_ids.list") + + // Read last synced id + val snapshotIds: List = try { + snapshotsListFile.readLines() + } catch (e: FileNotFoundException) { + emptyList() + } + + val localDrafts = run { + val cur = context.contentResolver.query(Drafts.CONTENT_URI, Drafts.COLUMNS, null, null, null)!! + try { + return@run cur.map(DraftCursorIndices(cur)) + } finally { + cur.close() + } + } + + // Ids of draft removed locally, we will delete these from remote storage + val localRemovedIds = snapshotIds.filter { id -> localDrafts.none { draft -> draft.unique_id_non_null == id } } + + + val remoteDrafts = listRemoteDrafts() + // Download remote items + val downloadRemoteInfoList = ArrayList() + // Remote remote items: snapshot has but database doesn't have + val removeRemoteInfoList = ArrayList() + // Update local items using remote + val updateLocalInfoList = LongSparseArray() + // Remove local items: snapshot has but remote doesn't have + val removeLocalIdsList = snapshotIds.filter { snapshotId -> + remoteDrafts.none { it.draftFileName == "$snapshotId.eml" } + } + + val uploadLocalList = ArrayList() + + remoteDrafts.forEach { remoteDraft -> + val localDraft = localDrafts.find { it.filename == remoteDraft.draftFileName } + if (remoteDraft.draftFileName.substringBefore(".eml") in localRemovedIds) { + // Local removed, remove remote + removeRemoteInfoList.add(remoteDraft) + } else if (localDraft == null) { + // Local doesn't exist, download remote + downloadRemoteInfoList.add(remoteDraft) + } else if (remoteDraft.draftTimestamp - localDraft.timestamp > 1000) { + // Local is older, update from remote + updateLocalInfoList[localDraft._id] = remoteDraft + } else if (localDraft.timestamp - remoteDraft.draftTimestamp > 1000) { + // Local is newer, upload local + uploadLocalList.add(localDraft) + } + } + + // Deal with local drafts that remote doesn't have + localDrafts.filterTo(uploadLocalList) { localDraft -> + if (remoteDrafts.any { it.draftFileName == localDraft.filename }) { + return@filterTo false + } + if (downloadRemoteInfoList.any { it.draftFileName == localDraft.filename }) { + return@filterTo false + } + if (removeRemoteInfoList.any { it.draftFileName == localDraft.filename }) { + return@filterTo false + } + if (localDraft.unique_id_non_null in removeLocalIdsList) { + return@filterTo false + } + if ((0 until updateLocalInfoList.size()).any { updateLocalInfoList.valueAt(it).draftFileName == localDraft.filename }) { + return@filterTo false + } + return@filterTo true + } + + + // Upload local items + if (BuildConfig.DEBUG) { + val fileList = uploadLocalList.joinToString(",") { it.filename } + Log.d(LOGTAG, "Uploading local drafts $fileList") + } + uploadDrafts(uploadLocalList) + + // Download remote items + if (BuildConfig.DEBUG) { + val fileList = downloadRemoteInfoList.joinToString(",") { it.draftFileName } + Log.d(LOGTAG, "Downloading remote drafts $fileList") + } + ContentResolverUtils.bulkInsert(context.contentResolver, Drafts.CONTENT_URI, + downloadDrafts(downloadRemoteInfoList).map { DraftValuesCreator.create(it) }) + + // Update local items + if (BuildConfig.DEBUG) { + val fileList = (0 until updateLocalInfoList.size()).joinToString(",") { updateLocalInfoList.valueAt(it).draftFileName } + Log.d(LOGTAG, "Updating local drafts $fileList") + } + for (index in 0 until updateLocalInfoList.size()) { + val draft = Draft() + if (draft.loadFromRemote(updateLocalInfoList.valueAt(index))) { + val where = Expression.equalsArgs(Drafts._ID).sql + val whereArgs = arrayOf(updateLocalInfoList.keyAt(index).toString()) + context.contentResolver.update(Drafts.CONTENT_URI, DraftValuesCreator.create(draft), where, whereArgs) + } + } + + // Remove local items + if (BuildConfig.DEBUG) { + val fileList = removeLocalIdsList.joinToString(",") { "$it.eml" } + Log.d(LOGTAG, "Removing local drafts $fileList") + } + ContentResolverUtils.bulkDelete(context.contentResolver, Drafts.CONTENT_URI, + Drafts.UNIQUE_ID, removeLocalIdsList, null) + + // Remove remote items + if (BuildConfig.DEBUG) { + val fileList = removeRemoteInfoList.joinToString(",") { it.draftFileName } + Log.d(LOGTAG, "Removing remote drafts $fileList") + } + removeDrafts(removeRemoteInfoList) + + snapshotsListFile.writer().use { writer -> + val cur = context.contentResolver.query(Drafts.CONTENT_URI, Drafts.COLUMNS, null, null, null)!! + try { + cur.map(DraftCursorIndices(cur)).map { it.unique_id_non_null }.forEach { line -> + writer.write(line) + writer.write("\n") + } + } finally { + cur.close() + } + } + + if (BuildConfig.DEBUG) { + Log.d(LOGTAG, "Drafts sync complete") + } + return true + } + + @Throws(IOException::class) + abstract fun listRemoteDrafts(): List + + @Throws(IOException::class) + open fun downloadDrafts(list: List): List { + val result = ArrayList() + list.forEach { + val draft = Draft() + if (draft.loadFromRemote(it)) { + result.add(draft) + } + } + return result + } + + @Throws(IOException::class) + open fun removeDrafts(list: List): Boolean { + var result = false + list.forEach { item -> + result = result or removeDraft(item) + } + return result + } + + @Throws(IOException::class) + open fun uploadDrafts(list: List): Boolean { + var result = false + list.forEach { item -> + result = result or (item.saveToRemote() != null) + } + return result + } + + @Throws(IOException::class) + abstract fun Draft.loadFromRemote(info: RemoteFileInfo): Boolean + + @Throws(IOException::class) + abstract fun removeDraft(info: RemoteFileInfo): Boolean + + @Throws(IOException::class) + abstract fun Draft.saveToRemote(): RemoteFileInfo? + + abstract val RemoteFileInfo.draftFileName: String + abstract val RemoteFileInfo.draftTimestamp: Long +} diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/util/sync/FileBasedFiltersDataSyncHelper.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/util/sync/FileBasedFiltersDataSyncHelper.kt new file mode 100644 index 000000000..4035e6dd5 --- /dev/null +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/util/sync/FileBasedFiltersDataSyncHelper.kt @@ -0,0 +1,93 @@ +package org.mariotaku.twidere.util.sync + +import android.content.Context +import android.util.Log +import android.util.Xml +import org.mariotaku.twidere.BuildConfig +import org.mariotaku.twidere.extension.model.* +import org.mariotaku.twidere.model.FiltersData +import java.io.* + +abstract class FileBasedFiltersDataSyncHelper(val context: Context) { + fun performSync(): Boolean { + val syncDataDir: File = context.syncDataDir.mkdirIfNotExists() ?: return false + val snapshotFile = File(syncDataDir, "filters.xml") + + val remoteFilters = FiltersData() + if (BuildConfig.DEBUG) { + Log.d(LOGTAG, "Downloading remote filters") + } + val remoteModified = remoteFilters.loadFromRemote(snapshotFile.lastModified()) + if (BuildConfig.DEBUG && !remoteModified) { + Log.d(LOGTAG, "Remote filter unchanged, skipped downloading") + } + val filters: FiltersData = FiltersData().apply { + read(context.contentResolver) + initFields() + } + + var localModified = false + + val deletedFilters: FiltersData? = try { + FileReader(snapshotFile).use { + val result = FiltersData() + val parser = Xml.newPullParser() + parser.setInput(it) + result.parse(parser) + localModified = localModified or (result != filters) + result.removeAll(filters) + return@use result + } + } catch (e: FileNotFoundException) { + localModified = true + null + } + + if (remoteModified) { + localModified = localModified or filters.addAll(remoteFilters, true) + } + + if (deletedFilters != null) { + localModified = localModified or filters.removeAll(deletedFilters) + } + + filters.write(context.contentResolver) + + val localModifiedTime = System.currentTimeMillis() + + if (localModified) { + if (BuildConfig.DEBUG) { + Log.d(LOGTAG, "Uploading filters") + } + filters.saveToRemote(localModifiedTime) + } else if (BuildConfig.DEBUG) { + Log.d(LOGTAG, "Local not modified, skip upload") + } + try { + FileWriter(snapshotFile).use { + val serializer = Xml.newSerializer() + serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true) + serializer.setOutput(it) + filters.serialize(serializer) + } + snapshotFile.setLastModified(localModifiedTime) + } catch (e: FileNotFoundException) { + // Ignore + } + + if (BuildConfig.DEBUG) { + Log.d(LOGTAG, "Filters sync complete") + } + return true + } + + /** + * Return false if remote not changed + */ + @Throws(IOException::class) + protected abstract fun FiltersData.loadFromRemote(snapshotModifiedMillis: Long): Boolean + + @Throws(IOException::class) + protected abstract fun FiltersData.saveToRemote(localModifiedTime: Long): Boolean + +} \ No newline at end of file diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/util/sync/SyncHelperCommons.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/util/sync/SyncHelperCommons.kt new file mode 100644 index 000000000..58fcabb4e --- /dev/null +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/util/sync/SyncHelperCommons.kt @@ -0,0 +1,17 @@ +package org.mariotaku.twidere.util.sync + +import android.content.Context +import java.io.File + +/** + * Created by mariotaku on 2016/12/31. + */ +internal const val LOGTAG = "Twidere.Sync" + +internal val Context.syncDataDir: File + get() = File(filesDir, "sync_data") + +internal fun File.mkdirIfNotExists(): File? { + if (exists() || mkdirs()) return this + return null +} \ No newline at end of file diff --git a/twidere/src/main/res/values/strings.xml b/twidere/src/main/res/values/strings.xml index 741c9e508..c1c6e377b 100644 --- a/twidere/src/main/res/values/strings.xml +++ b/twidere/src/main/res/values/strings.xml @@ -869,6 +869,7 @@ No user selected Advanced Filter sensitive tweets - Filter subscription + Filter subscriptions Manage + Filter subscriptions \ No newline at end of file diff --git a/twidere/src/main/res/xml/preferences_filters.xml b/twidere/src/main/res/xml/preferences_filters.xml index 9d8539152..68d5394cd 100644 --- a/twidere/src/main/res/xml/preferences_filters.xml +++ b/twidere/src/main/res/xml/preferences_filters.xml @@ -8,7 +8,12 @@ android:defaultValue="false" android:key="filter_possibility_sensitive_statuses" android:title="@string/preference_filter_possibility_sensitive_statuses"/> - - + + + + \ No newline at end of file