file based drafts/filters sync now working
This commit is contained in:
parent
d14ccb66f4
commit
e0826633bb
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
||||
}
|
||||
|
|
|
@ -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<Draft> CREATOR = new Creator<Draft>() {
|
||||
@Override
|
||||
public Draft createFromParcel(Parcel source) {
|
||||
|
|
|
@ -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 <E> boolean contentEquals(Collection<E> collection1, Collection<E> 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 {
|
||||
|
|
|
@ -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() {
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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<FileMetadata>(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<FileMetadata>): 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<FileMetadata> {
|
||||
val result = ArrayList<FileMetadata>()
|
||||
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()
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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<AccountDetails>(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
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -12,6 +12,10 @@ fun Collection<*>?.isNullOrEmpty(): Boolean {
|
|||
return this == null || this.isEmpty()
|
||||
}
|
||||
|
||||
fun <T> MutableCollection<T>.addAllIgnoreDuplicates(collection: Collection<T>) {
|
||||
addAll(collection.filter { it !in this })
|
||||
fun <T> MutableCollection<T>.addAllEnhanced(collection: Collection<T>, ignoreDuplicates: Boolean): Boolean {
|
||||
if (ignoreDuplicates) {
|
||||
return addAll(collection.filter { it !in this })
|
||||
} else {
|
||||
return addAll(collection)
|
||||
}
|
||||
}
|
|
@ -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 <E> LongSparseArray<E>.set(key: Long, value: E) {
|
||||
put(key, value)
|
||||
}
|
|
@ -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<ParcelableStatus>(EXTRA_STATUS)
|
||||
mentionUser = savedInstanceState.getParcelable<ParcelableUser>(EXTRA_USER)
|
||||
draft = savedInstanceState.getParcelable<Draft>(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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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<SimpleContentHandler>()
|
||||
private val mediaList: MutableList<ParcelableMediaUpdate> = 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`)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 <T> mergeList(vararg lists: List<T>?, ignoreDuplicates: Boolean): List<T> {
|
||||
val result = ArrayList<T>()
|
||||
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() {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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<BaseFiltersImportFragment.SelectableUsersAdapter>(),
|
||||
LoaderManager.LoaderCallbacks<List<ParcelableUser>?> {
|
||||
|
||||
|
||||
protected var nextCursor: Long = -1
|
||||
private set
|
||||
protected var prevCursor: Long = -1
|
||||
|
|
|
@ -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<UserKey>(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<UserKey>(EXTRA_ACCOUNT_KEY))
|
||||
startActivity(intent)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<RemoteFileInfo>(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<String> = try {
|
||||
snapshotsListFile.readLines()
|
||||
} catch (e: FileNotFoundException) {
|
||||
emptyList<String>()
|
||||
}
|
||||
|
||||
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<RemoteFileInfo>()
|
||||
// Remote remote items: snapshot has but database doesn't have
|
||||
val removeRemoteInfoList = ArrayList<RemoteFileInfo>()
|
||||
// Update local items using remote
|
||||
val updateLocalInfoList = LongSparseArray<RemoteFileInfo>()
|
||||
// Remove local items: snapshot has but remote doesn't have
|
||||
val removeLocalIdsList = snapshotIds.filter { snapshotId ->
|
||||
remoteDrafts.none { it.draftFileName == "$snapshotId.eml" }
|
||||
}
|
||||
|
||||
val uploadLocalList = ArrayList<Draft>()
|
||||
|
||||
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<RemoteFileInfo>
|
||||
|
||||
@Throws(IOException::class)
|
||||
open fun downloadDrafts(list: List<RemoteFileInfo>): List<Draft> {
|
||||
val result = ArrayList<Draft>()
|
||||
list.forEach {
|
||||
val draft = Draft()
|
||||
if (draft.loadFromRemote(it)) {
|
||||
result.add(draft)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
open fun removeDrafts(list: List<RemoteFileInfo>): Boolean {
|
||||
var result = false
|
||||
list.forEach { item ->
|
||||
result = result or removeDraft(item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
open fun uploadDrafts(list: List<Draft>): 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
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -869,6 +869,7 @@
|
|||
<string name="message_no_user_selected">No user selected</string>
|
||||
<string name="preference_title_advanced">Advanced</string>
|
||||
<string name="preference_filter_possibility_sensitive_statuses">Filter sensitive tweets</string>
|
||||
<string name="preference_title_filter_subscription">Filter subscription</string>
|
||||
<string name="preference_title_filter_subscriptions">Filter subscriptions</string>
|
||||
<string name="preference_title_filter_manage_subscriptions">Manage</string>
|
||||
<string name="title_manage_filter_subscriptions">Filter subscriptions</string>
|
||||
</resources>
|
|
@ -8,7 +8,12 @@
|
|||
android:defaultValue="false"
|
||||
android:key="filter_possibility_sensitive_statuses"
|
||||
android:title="@string/preference_filter_possibility_sensitive_statuses"/>
|
||||
<PreferenceCategory android:title="@string/preference_title_filter_subscription">
|
||||
<Preference android:title="@string/preference_title_filter_manage_subscriptions"/>
|
||||
<PreferenceCategory android:title="@string/preference_title_filter_subscriptions">
|
||||
<Preference android:title="@string/preference_title_filter_manage_subscriptions">
|
||||
<intent
|
||||
android:data="twidere://filters/subscriptions"
|
||||
android:targetClass="org.mariotaku.twidere.activity.LinkHandlerActivity"
|
||||
android:targetPackage="org.mariotaku.twidere"/>
|
||||
</Preference>
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
Loading…
Reference in New Issue