draft to mime is working

fixed save draft
This commit is contained in:
Mariotaku Lee 2016-12-08 11:07:13 +08:00
parent bd03437c71
commit faf8e980be
27 changed files with 414 additions and 165 deletions

View File

@ -8,7 +8,7 @@ buildscript {
}
dependencies {
classpath 'com.github.ben-manes:gradle-versions-plugin:0.13.0'
classpath 'com.android.tools.build:gradle:2.2.2'
classpath 'com.android.tools.build:gradle:2.2.3'
classpath 'com.google.gms:google-services:3.0.0'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
classpath('fr.avianey.androidsvgdrawable:gradle-plugin:3.0.0') {

View File

@ -2,6 +2,9 @@ package org.mariotaku.twidere.annotation;
import android.support.annotation.StringDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Created by mariotaku on 16/1/28.
*/
@ -15,6 +18,7 @@ import android.support.annotation.StringDef;
CustomTabType.SEARCH_STATUSES,
CustomTabType.LIST_TIMELINE,
})
@Retention(RetentionPolicy.SOURCE)
public @interface CustomTabType {
String HOME_TIMELINE = "home_timeline";
String NOTIFICATIONS_TIMELINE = "notifications_timeline";

View File

@ -30,7 +30,7 @@ import com.hannesdorfmann.parcelableplease.annotation.ParcelableThisPlease;
import org.mariotaku.commons.objectcursor.LoganSquareCursorFieldConverter;
import org.mariotaku.library.objectcursor.annotation.CursorField;
import org.mariotaku.library.objectcursor.annotation.CursorObject;
import org.mariotaku.twidere.model.draft.ActionExtra;
import org.mariotaku.twidere.model.draft.ActionExtras;
import org.mariotaku.twidere.model.util.DraftExtrasConverter;
import org.mariotaku.twidere.model.util.UserKeysCursorFieldConverter;
import org.mariotaku.twidere.provider.TwidereDataStore;
@ -69,7 +69,7 @@ public class Draft implements Parcelable {
@Nullable
@ParcelableThisPlease
@CursorField(value = Drafts.ACTION_EXTRAS, converter = DraftExtrasConverter.class)
public ActionExtra action_extras;
public ActionExtras action_extras;
public Draft() {

View File

@ -23,6 +23,7 @@ import android.content.ContentValues;
import android.database.Cursor;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.bluelinelabs.logansquare.annotation.JsonField;
@ -85,7 +86,7 @@ public class ParcelableLocation implements Parcelable {
}
@Nullable
public static ParcelableLocation valueOf(@Nullable final String locationString) {
public static ParcelableLocation valueOf(@NonNull final String locationString) {
if (locationString == null) return null;
final String[] longlat = locationString.split(",");
if (longlat.length != 2) {
@ -106,7 +107,9 @@ public class ParcelableLocation implements Parcelable {
public static class Converter implements CursorFieldConverter<ParcelableLocation> {
@Override
public ParcelableLocation parseField(Cursor cursor, int columnIndex, ParameterizedType fieldType) {
return valueOf(cursor.getString(columnIndex));
final String locationString = cursor.getString(columnIndex);
if (locationString == null) return null;
return valueOf(locationString);
}
@Override

View File

@ -5,5 +5,5 @@ import android.os.Parcelable;
/**
* Created by mariotaku on 16/2/21.
*/
public interface ActionExtra extends Parcelable {
public interface ActionExtras extends Parcelable {
}

View File

@ -1,51 +0,0 @@
package org.mariotaku.twidere.model.draft;
import android.os.Parcel;
import com.bluelinelabs.logansquare.annotation.JsonField;
import com.bluelinelabs.logansquare.annotation.JsonObject;
import com.hannesdorfmann.parcelableplease.annotation.ParcelablePlease;
import com.hannesdorfmann.parcelableplease.annotation.ParcelableThisPlease;
/**
* Created by mariotaku on 16/2/21.
*/
@ParcelablePlease
@JsonObject
public class SendDirectMessageActionExtra implements ActionExtra {
@ParcelableThisPlease
@JsonField(name = "recipient_id")
String recipientId;
public String getRecipientId() {
return recipientId;
}
public void setRecipientId(String recipientId) {
this.recipientId = recipientId;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
SendDirectMessageActionExtraParcelablePlease.writeToParcel(this, dest, flags);
}
public static final Creator<SendDirectMessageActionExtra> CREATOR = new Creator<SendDirectMessageActionExtra>() {
@Override
public SendDirectMessageActionExtra createFromParcel(Parcel source) {
SendDirectMessageActionExtra target = new SendDirectMessageActionExtra();
SendDirectMessageActionExtraParcelablePlease.readFromParcel(target, source);
return target;
}
@Override
public SendDirectMessageActionExtra[] newArray(int size) {
return new SendDirectMessageActionExtra[size];
}
};
}

View File

@ -0,0 +1,67 @@
package org.mariotaku.twidere.model.draft;
import android.os.Parcel;
import com.bluelinelabs.logansquare.annotation.JsonField;
import com.bluelinelabs.logansquare.annotation.JsonObject;
import com.hannesdorfmann.parcelableplease.annotation.ParcelablePlease;
import com.hannesdorfmann.parcelableplease.annotation.ParcelableThisPlease;
/**
* Created by mariotaku on 16/2/21.
*/
@ParcelablePlease
@JsonObject
public class SendDirectMessageActionExtras implements ActionExtras {
@ParcelableThisPlease
@JsonField(name = "recipient_id")
String recipientId;
public String getRecipientId() {
return recipientId;
}
public void setRecipientId(String recipientId) {
this.recipientId = recipientId;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
SendDirectMessageActionExtrasParcelablePlease.writeToParcel(this, dest, flags);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SendDirectMessageActionExtras that = (SendDirectMessageActionExtras) o;
return recipientId != null ? recipientId.equals(that.recipientId) : that.recipientId == null;
}
@Override
public int hashCode() {
return recipientId != null ? recipientId.hashCode() : 0;
}
public static final Creator<SendDirectMessageActionExtras> CREATOR = new Creator<SendDirectMessageActionExtras>() {
@Override
public SendDirectMessageActionExtras createFromParcel(Parcel source) {
SendDirectMessageActionExtras target = new SendDirectMessageActionExtras();
SendDirectMessageActionExtrasParcelablePlease.readFromParcel(target, source);
return target;
}
@Override
public SendDirectMessageActionExtras[] newArray(int size) {
return new SendDirectMessageActionExtras[size];
}
};
}

View File

@ -14,7 +14,7 @@ import org.mariotaku.twidere.model.ParcelableStatus;
*/
@ParcelablePlease
@JsonObject
public class UpdateStatusActionExtra implements ActionExtra {
public class UpdateStatusActionExtras implements ActionExtras {
@ParcelableThisPlease
@JsonField(name = "in_reply_to_status")
ParcelableStatus inReplyToStatus;
@ -78,20 +78,47 @@ public class UpdateStatusActionExtra implements ActionExtra {
@Override
public void writeToParcel(Parcel dest, int flags) {
UpdateStatusActionExtraParcelablePlease.writeToParcel(this, dest, flags);
UpdateStatusActionExtrasParcelablePlease.writeToParcel(this, dest, flags);
}
public static final Creator<UpdateStatusActionExtra> CREATOR = new Creator<UpdateStatusActionExtra>() {
@Override
public UpdateStatusActionExtra createFromParcel(Parcel source) {
UpdateStatusActionExtra target = new UpdateStatusActionExtra();
UpdateStatusActionExtraParcelablePlease.readFromParcel(target, source);
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UpdateStatusActionExtras that = (UpdateStatusActionExtras) o;
if (possiblySensitive != that.possiblySensitive) return false;
if (displayCoordinates != that.displayCoordinates) return false;
if (inReplyToStatus != null ? !inReplyToStatus.equals(that.inReplyToStatus) : that.inReplyToStatus != null)
return false;
if (repostStatusId != null ? !repostStatusId.equals(that.repostStatusId) : that.repostStatusId != null)
return false;
return attachmentUrl != null ? attachmentUrl.equals(that.attachmentUrl) : that.attachmentUrl == null;
}
@Override
public int hashCode() {
int result = inReplyToStatus != null ? inReplyToStatus.hashCode() : 0;
result = 31 * result + (possiblySensitive ? 1 : 0);
result = 31 * result + (repostStatusId != null ? repostStatusId.hashCode() : 0);
result = 31 * result + (displayCoordinates ? 1 : 0);
result = 31 * result + (attachmentUrl != null ? attachmentUrl.hashCode() : 0);
return result;
}
public static final Creator<UpdateStatusActionExtras> CREATOR = new Creator<UpdateStatusActionExtras>() {
@Override
public UpdateStatusActionExtras createFromParcel(Parcel source) {
UpdateStatusActionExtras target = new UpdateStatusActionExtras();
UpdateStatusActionExtrasParcelablePlease.readFromParcel(target, source);
return target;
}
@Override
public UpdateStatusActionExtra[] newArray(int size) {
return new UpdateStatusActionExtra[size];
public UpdateStatusActionExtras[] newArray(int size) {
return new UpdateStatusActionExtras[size];
}
};
}

View File

@ -8,9 +8,9 @@ import com.bluelinelabs.logansquare.LoganSquare;
import org.mariotaku.library.objectcursor.converter.CursorFieldConverter;
import org.mariotaku.twidere.model.Draft;
import org.mariotaku.twidere.model.draft.ActionExtra;
import org.mariotaku.twidere.model.draft.SendDirectMessageActionExtra;
import org.mariotaku.twidere.model.draft.UpdateStatusActionExtra;
import org.mariotaku.twidere.model.draft.ActionExtras;
import org.mariotaku.twidere.model.draft.SendDirectMessageActionExtras;
import org.mariotaku.twidere.model.draft.UpdateStatusActionExtras;
import org.mariotaku.twidere.provider.TwidereDataStore.Drafts;
import java.io.IOException;
@ -19,33 +19,30 @@ import java.lang.reflect.ParameterizedType;
/**
* Created by mariotaku on 16/2/20.
*/
public class DraftExtrasConverter implements CursorFieldConverter<ActionExtra> {
public class DraftExtrasConverter implements CursorFieldConverter<ActionExtras> {
@Override
public ActionExtra parseField(Cursor cursor, int columnIndex, ParameterizedType fieldType) throws IOException {
public ActionExtras parseField(Cursor cursor, int columnIndex, ParameterizedType fieldType) throws IOException {
final String actionType = cursor.getString(cursor.getColumnIndex(Drafts.ACTION_TYPE));
if (TextUtils.isEmpty(actionType)) return null;
final String json = cursor.getString(columnIndex);
if (TextUtils.isEmpty(actionType) || TextUtils.isEmpty(json)) return null;
switch (actionType) {
case "0":
case "1":
case Draft.Action.UPDATE_STATUS:
case Draft.Action.REPLY:
case Draft.Action.QUOTE: {
final String string = cursor.getString(columnIndex);
if (TextUtils.isEmpty(string)) return null;
return LoganSquare.parse(string, UpdateStatusActionExtra.class);
return LoganSquare.parse(json, UpdateStatusActionExtras.class);
}
case "2":
case Draft.Action.SEND_DIRECT_MESSAGE: {
final String string = cursor.getString(columnIndex);
if (TextUtils.isEmpty(string)) return null;
return LoganSquare.parse(string, SendDirectMessageActionExtra.class);
return LoganSquare.parse(json, SendDirectMessageActionExtras.class);
}
}
return null;
}
@Override
public void writeField(ContentValues values, ActionExtra object, String columnName, ParameterizedType fieldType) throws IOException {
public void writeField(ContentValues values, ActionExtras object, String columnName, ParameterizedType fieldType) throws IOException {
if (object == null) return;
values.put(columnName, LoganSquare.serialize(object));
}

View File

@ -125,6 +125,7 @@ dependencies {
androidTestCompile "com.android.support:support-annotations:$android_support_lib_version"
androidTestCompile 'com.android.support.test:runner:0.5'
androidTestCompile 'com.android.support.test:rules:0.5'
androidTestCompile 'commons-io:commons-io:2.5'
compile 'com.android.support:multidex:1.0.1'
compile "com.android.support:support-v4:$android_support_lib_version"

View File

@ -1,16 +1,15 @@
package org.mariotaku.twidere
import android.net.Uri
import android.support.test.InstrumentationRegistry
import android.support.test.runner.AndroidJUnit4
import org.apache.commons.io.IOUtils
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
import org.mariotaku.twidere.extension.readMimeMessageFrom
import org.mariotaku.twidere.extension.writeMimeMessageTo
import org.mariotaku.twidere.model.Draft
import org.mariotaku.twidere.model.ParcelableMedia
import org.mariotaku.twidere.model.ParcelableMediaUpdate
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.*
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.util.concurrent.TimeUnit
@ -24,16 +23,24 @@ class DraftExtensionsTest {
fun testMimeMessageProcessing() {
val context = InstrumentationRegistry.getTargetContext()
val draft = Draft()
draft.action_type = Draft.Action.UPDATE_STATUS
draft.timestamp = System.currentTimeMillis()
draft.account_keys = arrayOf(UserKey("user1", "twitter.com"), UserKey("user2", "twitter.com"))
draft.text = "Hello world"
draft.media = arrayOf(ParcelableMediaUpdate().apply {
this.uri = "file:///system/media/audio/ringtones/Atria.ogg"
draft.text = "Hello world 测试"
draft.location = ParcelableLocation(-11.956, 99.625) // Randomly generated
draft.media = arrayOf(
"file:///system/media/audio/ringtones/Atria.ogg",
"file:///system/media/audio/ringtones/Callisto.ogg",
"file:///system/media/audio/ringtones/Dione.ogg"
).map { uri ->
ParcelableMediaUpdate().apply {
this.uri = uri
this.type = ParcelableMedia.Type.VIDEO
this.alt_text = String(CharArray(420).apply {
fill('A')
})
})
}
}.toTypedArray()
val output = ByteArrayOutputStream()
draft.writeMimeMessageTo(context, output)
val input = ByteArrayInputStream(output.toByteArray())
@ -44,10 +51,16 @@ class DraftExtensionsTest {
Assert.assertArrayEquals(draft.account_keys?.sortedArray(), newDraft.account_keys?.sortedArray())
Assert.assertEquals(TimeUnit.MILLISECONDS.toSeconds(draft.timestamp), TimeUnit.MILLISECONDS.toSeconds(newDraft.timestamp))
Assert.assertEquals(draft.text, newDraft.text)
Assert.assertEquals(draft.location, newDraft.location)
Assert.assertEquals(draft.action_type, newDraft.action_type)
Assert.assertEquals(draft.action_extras, newDraft.action_extras)
draft.media?.forEachIndexed { idx, expected ->
val actual = newDraft.media!![idx]
Assert.assertEquals(expected.alt_text, actual.alt_text)
Assert.assertEquals(expected.type, actual.type)
val stl = context.contentResolver.openInputStream(Uri.parse(expected.uri))
val str = context.contentResolver.openInputStream(Uri.parse(actual.uri))
Assert.assertTrue(IOUtils.contentEquals(stl, str))
}
}
}

View File

@ -29,6 +29,9 @@
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<activity
android:name="org.mariotaku.twidere.activity.DropboxAuthStarterActivity"
android:theme="@style/Theme.Twidere.NoDisplay"/>
<activity
android:name="com.dropbox.core.android.AuthActivity"
android:configChanges="orientation|keyboard"
@ -42,5 +45,7 @@
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<service android:name="org.mariotaku.twidere.service.DropboxDataSyncService"/>
</application>
</manifest>

View File

@ -0,0 +1,16 @@
package org.mariotaku.twidere.activity
import android.os.Bundle
import com.dropbox.core.android.Auth
import org.mariotaku.twidere.Constants.DROPBOX_APP_KEY
/**
* Created by mariotaku on 2016/12/7.
*/
class DropboxAuthStarterActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Auth.startOAuth2Authentication(this, DROPBOX_APP_KEY)
finish()
}
}

View File

@ -0,0 +1,56 @@
package org.mariotaku.twidere.service
import android.app.Service
import android.content.Intent
import android.content.SyncResult
import android.os.Bundle
import android.os.IBinder
import org.mariotaku.twidere.IDataSyncService
import org.mariotaku.twidere.activity.DropboxAuthStarterActivity
import org.mariotaku.twidere.model.SyncAuthInfo
import java.lang.ref.WeakReference
/**
* Created by mariotaku on 2016/12/7.
*/
class DropboxDataSyncService : Service() {
private val serviceInterface: ServiceInterface
init {
serviceInterface = ServiceInterface(WeakReference(this))
}
override fun onBind(intent: Intent?): IBinder {
return serviceInterface.asBinder()
}
private fun getAuthInfo(): SyncAuthInfo? {
throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates.
}
private fun getAuthRequestIntent(info: SyncAuthInfo?): Intent {
return Intent(this, DropboxAuthStarterActivity::class.java)
}
private fun onPerformSync(info: SyncAuthInfo, extras: Bundle?, syncResult: SyncResult) {
throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates.
}
internal class ServiceInterface(val service: WeakReference<DropboxDataSyncService>) : IDataSyncService.Stub() {
override fun getAuthInfo(): SyncAuthInfo? {
return service.get().getAuthInfo()
}
override fun getAuthRequestIntent(info: SyncAuthInfo?): Intent {
return service.get().getAuthRequestIntent(info)
}
override fun onPerformSync(info: SyncAuthInfo, extras: Bundle?, syncResult: SyncResult) {
service.get().onPerformSync(info, extras, syncResult)
}
}
}

View File

@ -6,7 +6,7 @@ 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.UpdateStatusActionExtra;
import org.mariotaku.twidere.model.draft.UpdateStatusActionExtras;
/**
* Created by mariotaku on 16/2/12.
@ -25,8 +25,8 @@ public class ParcelableStatusUpdateUtils {
statusUpdate.text = draft.text;
statusUpdate.location = draft.location;
statusUpdate.media = draft.media;
if (draft.action_extras instanceof UpdateStatusActionExtra) {
final UpdateStatusActionExtra extra = (UpdateStatusActionExtra) draft.action_extras;
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();

View File

@ -42,7 +42,7 @@ import org.mariotaku.twidere.model.ParcelableUser;
import org.mariotaku.twidere.model.ParcelableUserMention;
import org.mariotaku.twidere.model.ParcelableUserValuesCreator;
import org.mariotaku.twidere.model.UserKey;
import org.mariotaku.twidere.model.draft.SendDirectMessageActionExtra;
import org.mariotaku.twidere.model.draft.SendDirectMessageActionExtras;
import org.mariotaku.twidere.model.util.ParcelableActivityExtensionsKt;
import org.mariotaku.twidere.model.util.ParcelableDirectMessageUtils;
import org.mariotaku.twidere.model.util.ParcelableStatusUtils;
@ -121,7 +121,7 @@ public final class ContentValuesCreator implements TwidereConstants {
values.put(Drafts.MEDIA, JsonSerializer.serialize(Arrays.asList(mediaArray),
ParcelableMediaUpdate.class));
}
final SendDirectMessageActionExtra extra = new SendDirectMessageActionExtra();
final SendDirectMessageActionExtras extra = new SendDirectMessageActionExtras();
extra.setRecipientId(recipientId);
values.put(Drafts.ACTION_EXTRAS, JsonSerializer.serialize(extra));
return values;

View File

@ -31,7 +31,6 @@ import android.os.RemoteException;
import android.util.Log;
import org.mariotaku.twidere.BuildConfig;
import org.mariotaku.twidere.Constants;
import org.mariotaku.twidere.IStatusShortener;
import org.mariotaku.twidere.model.ParcelableStatus;
import org.mariotaku.twidere.model.ParcelableStatusUpdate;
@ -40,8 +39,10 @@ import org.mariotaku.twidere.model.UserKey;
import java.util.List;
public final class StatusShortenerInterface extends AbsServiceInterface<IStatusShortener>
implements Constants {
import static org.mariotaku.twidere.TwidereConstants.LOGTAG;
import static org.mariotaku.twidere.constant.IntentConstants.INTENT_ACTION_EXTENSION_SHORTEN_STATUS;
public final class StatusShortenerInterface extends AbsServiceInterface<IStatusShortener> {
protected StatusShortenerInterface(Context context, String shortenerName, Bundle metaData) {
super(context, shortenerName, metaData);

View File

@ -0,0 +1,21 @@
package org.mariotaku.twidere.util.io;
import java.io.IOException;
import java.io.OutputStream;
/**
* Created by mariotaku on 2016/12/8.
*/
public final class CountOnlyOutputStream extends OutputStream {
private int count;
@Override
public void write(int i) throws IOException {
count++;
}
public int getCount() {
return count;
}
}

View File

@ -77,7 +77,7 @@ import org.mariotaku.twidere.constant.KeyboardShortcutConstants
import org.mariotaku.twidere.fragment.BaseDialogFragment
import org.mariotaku.twidere.fragment.ProgressDialogFragment
import org.mariotaku.twidere.model.*
import org.mariotaku.twidere.model.draft.UpdateStatusActionExtra
import org.mariotaku.twidere.model.draft.UpdateStatusActionExtras
import org.mariotaku.twidere.model.util.AccountUtils
import org.mariotaku.twidere.model.util.ParcelableLocationUtils
import org.mariotaku.twidere.preference.ServicePickerPreference
@ -178,8 +178,11 @@ class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener
override fun onBackPressed() {
if (currentTask != null && currentTask!!.status == AsyncTask.Status.RUNNING) return
if (hasComposingStatus()) {
shouldSkipDraft = false
if (!shouldSkipDraft && hasComposingStatus() ) {
saveToDrafts()
Toast.makeText(this, R.string.status_saved_to_draft, Toast.LENGTH_SHORT).show()
shouldSkipDraft = true
finish()
} else {
shouldSkipDraft = true
discardTweet()
@ -429,7 +432,8 @@ class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener
draft.text = text
draft.media = media
draft.location = recentLocation
draft.action_extras = UpdateStatusActionExtra().apply {
draft.timestamp = System.currentTimeMillis()
draft.action_extras = UpdateStatusActionExtras().apply {
this.inReplyToStatus = this@ComposeActivity.inReplyToStatus
this.isPossiblySensitive = this@ComposeActivity.possiblySensitive
}
@ -837,8 +841,8 @@ class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener
addMedia(Arrays.asList(*draft.media))
}
recentLocation = draft.location
if (draft.action_extras is UpdateStatusActionExtra) {
val extra = draft.action_extras as UpdateStatusActionExtra?
if (draft.action_extras is UpdateStatusActionExtras) {
val extra = draft.action_extras as UpdateStatusActionExtras?
possiblySensitive = extra!!.isPossiblySensitive
inReplyToStatus = extra.inReplyToStatus
}
@ -871,16 +875,16 @@ class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener
}
when (draft.action_type) {
Draft.Action.REPLY -> {
if (draft.action_extras is UpdateStatusActionExtra) {
showReplyLabel((draft.action_extras as UpdateStatusActionExtra).inReplyToStatus)
if (draft.action_extras is UpdateStatusActionExtras) {
showReplyLabel((draft.action_extras as UpdateStatusActionExtras).inReplyToStatus)
} else {
hideLabel()
return false
}
}
Draft.Action.QUOTE -> {
if (draft.action_extras is UpdateStatusActionExtra) {
showQuoteLabel((draft.action_extras as UpdateStatusActionExtra).inReplyToStatus)
if (draft.action_extras is UpdateStatusActionExtras) {
showQuoteLabel((draft.action_extras as UpdateStatusActionExtras).inReplyToStatus)
} else {
hideLabel()
return false
@ -1280,7 +1284,7 @@ class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener
update.media = media
update.in_reply_to_status = inReplyToStatus
update.is_possibly_sensitive = isPossiblySensitive
update.attachment_url = (draft?.action_extras as? UpdateStatusActionExtra)?.attachmentUrl
update.attachment_url = (draft?.action_extras as? UpdateStatusActionExtras)?.attachmentUrl
BackgroundOperationService.updateStatusesAsync(this, action, update)
if (preferences.getBoolean(KEY_NO_CLOSE_AFTER_TWEET_SENT, false) && inReplyToStatus == null) {
possiblySensitive = false

View File

@ -48,7 +48,7 @@ class DraftsAdapter(context: Context) : SimpleCursorAdapter(context, R.layout.li
private val mediaPreviewStyle: Int
private var mTextSize: Float = 0.toFloat()
private var mIndices: DraftCursorIndices? = null
private var indices: DraftCursorIndices? = null
init {
GeneralComponentHelper.build(context).inject(this)
@ -58,7 +58,7 @@ class DraftsAdapter(context: Context) : SimpleCursorAdapter(context, R.layout.li
override fun bindView(view: View, context: Context, cursor: Cursor) {
val holder = view.tag as DraftViewHolder
val indices = mIndices!!
val indices = indices!!
val accountKeys = UserKey.arrayOf(cursor.getString(indices.account_keys))
val text = cursor.getString(indices.text)
val mediaUpdates = JsonSerializer.parseArray(cursor.getString(indices.media), ParcelableMediaUpdate::class.java)
@ -115,11 +115,16 @@ class DraftsAdapter(context: Context) : SimpleCursorAdapter(context, R.layout.li
override fun swapCursor(c: Cursor?): Cursor? {
val old = super.swapCursor(c)
if (c != null) {
mIndices = DraftCursorIndices(c)
indices = DraftCursorIndices(c)
}
return old
}
fun getDraft(position: Int): Draft {
cursor.moveToPosition(position)
return indices!!.newObject(cursor)
}
private fun getActionName(context: Context, actionType: String): String? {
if (TextUtils.isEmpty(actionType)) return context.getString(R.string.update_status)
when (actionType) {

View File

@ -2,27 +2,30 @@ package org.mariotaku.twidere.extension
import android.content.Context
import android.net.Uri
import com.bluelinelabs.logansquare.LoganSquare
import com.nostra13.universalimageloader.utils.IoUtils
import org.apache.james.mime4j.dom.Header
import org.apache.james.mime4j.dom.MessageServiceFactory
import org.apache.james.mime4j.dom.address.Mailbox
import org.apache.james.mime4j.dom.field.*
import org.apache.james.mime4j.message.AbstractMessage
import org.apache.james.mime4j.message.BodyPart
import org.apache.james.mime4j.message.MultipartImpl
import org.apache.james.mime4j.message.SimpleContentHandler
import org.apache.james.mime4j.message.*
import org.apache.james.mime4j.parser.MimeStreamParser
import org.apache.james.mime4j.storage.StorageBodyFactory
import org.apache.james.mime4j.stream.BodyDescriptor
import org.apache.james.mime4j.stream.MimeConfig
import org.apache.james.mime4j.stream.RawField
import org.apache.james.mime4j.util.MimeUtil
import org.mariotaku.ktextension.convert
import org.mariotaku.ktextension.toInt
import org.mariotaku.ktextension.toString
import org.mariotaku.twidere.extension.model.getMimeType
import org.mariotaku.twidere.model.Draft
import org.mariotaku.twidere.model.ParcelableMedia
import org.mariotaku.twidere.model.ParcelableMediaUpdate
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.*
import org.mariotaku.twidere.model.Draft.Action
import org.mariotaku.twidere.model.draft.SendDirectMessageActionExtras
import org.mariotaku.twidere.model.draft.UpdateStatusActionExtras
import org.mariotaku.twidere.util.collection.NonEmptyHashMap
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import java.io.OutputStream
import java.nio.charset.Charset
@ -45,44 +48,64 @@ fun Draft.writeMimeMessageTo(context: Context, st: OutputStream) {
message.date = Date(this.timestamp)
message.setFrom(this.account_keys?.map { Mailbox(it.id, it.host) })
if (message.header == null) {
message.header = HeaderImpl()
}
this.location?.let { location ->
message.header.addField(RawField("X-GeoLocation", location.toString()))
}
this.action_type?.let { type ->
message.header.addField(RawField("X-Action-Type", type))
}
val multipart = MultipartImpl("mixed")
multipart.addBodyPart(BodyPart().apply {
setText(bodyFactory.textBody(this@writeMimeMessageTo.text))
setText(bodyFactory.textBody(this@writeMimeMessageTo.text, Charsets.UTF_8.name()))
})
this.action_extras?.let { extras ->
multipart.addBodyPart(BodyPart().apply {
setText(bodyFactory.textBody(LoganSquare.serialize(extras)), "json")
this.filename = "twidere.action.extras.json"
})
}
this.media?.forEach { mediaItem ->
multipart.addBodyPart(BodyPart().apply {
val uri = Uri.parse(mediaItem.uri)
val storage = storageProvider.store(contentResolver.openInputStream(uri))
val mimeType = mediaItem.getMimeType(contentResolver) ?: "application/octet-stream"
val parameters = NonEmptyHashMap<String, String?>()
parameters["alt_text"] = mediaItem.alt_text
parameters["media_type"] = mediaItem.type.toString()
this.setBody(bodyFactory.binaryBody(storage), mimeType, parameters)
val storage = storageProvider.store(contentResolver.openInputStream(uri))
this.filename = uri.lastPathSegment
this.contentTransferEncoding = MimeUtil.ENC_BASE64
this.setBody(bodyFactory.binaryBody(storage), mimeType, parameters)
})
}
message.setMultipart(multipart)
writer.writeMessage(message, st)
st.flush()
}
fun Draft.readMimeMessageFrom(context: Context, st: InputStream) {
val config = MimeConfig()
val parser = MimeStreamParser(config)
parser.setContentHandler(DraftContentHandler(this))
parser.isContentDecoding = true
parser.setContentHandler(DraftContentHandler(context, this))
parser.parse(st)
}
private class DraftContentHandler(private val draft: Draft) : SimpleContentHandler() {
private class DraftContentHandler(private val context: Context, private val draft: Draft) : SimpleContentHandler() {
private val processingStack = Stack<SimpleContentHandler>()
private val mediaList: MutableList<ParcelableMediaUpdate> = ArrayList()
override fun headers(header: Header) {
if (processingStack.isEmpty()) {
draft.timestamp = header.getField("Date").convert {
draft.timestamp = header.getField("Date")?.convert {
(it as DateTimeField).date.time
}
draft.account_keys = header.getField("From").convert { field ->
} ?: 0
draft.account_keys = header.getField("From")?.convert { field ->
when (field) {
is MailboxField -> {
return@convert arrayOf(field.mailbox.convert { UserKey(it.localPart, it.domain) })
@ -95,6 +118,8 @@ private class DraftContentHandler(private val draft: Draft) : SimpleContentHandl
}
}
}
draft.location = header.getField("X-GeoLocation")?.body?.convert(ParcelableLocation::valueOf)
draft.action_type = header.getField("X-Action-Type")?.body
} else {
processingStack.peek().headers(header)
}
@ -108,7 +133,7 @@ private class DraftContentHandler(private val draft: Draft) : SimpleContentHandl
}
override fun startBodyPart() {
processingStack.push(BodyPartHandler(draft))
processingStack.push(BodyPartHandler(context, draft))
}
override fun body(bd: BodyDescriptor?, `is`: InputStream?) {
@ -131,7 +156,7 @@ private class DraftContentHandler(private val draft: Draft) : SimpleContentHandl
}
}
private class BodyPartHandler(private val draft: Draft) : SimpleContentHandler() {
private class BodyPartHandler(private val context: Context, private val draft: Draft) : SimpleContentHandler() {
internal lateinit var header: Header
internal var media: ParcelableMediaUpdate? = null
@ -147,11 +172,32 @@ private class BodyPartHandler(private val draft: Draft) : SimpleContentHandler()
val contentDisposition = header.getField("Content-Disposition") as? ContentDispositionField
if (contentDisposition != null && contentDisposition.isAttachment) {
when (contentDisposition.filename) {
"twidere.action.extras.json" -> {
draft.action_extras = when (draft.action_type) {
"0", "1", Action.UPDATE_STATUS, Action.REPLY, Action.QUOTE -> {
LoganSquare.parse(st, UpdateStatusActionExtras::class.java)
}
"2", Action.SEND_DIRECT_MESSAGE -> {
LoganSquare.parse(st, SendDirectMessageActionExtras::class.java)
}
else -> {
null
}
}
}
else -> {
val contentType = header.getField("Content-Type") as? ContentTypeField
val filename = contentDisposition.filename ?: return
val mediaFile = File(context.filesDir, filename)
media = ParcelableMediaUpdate().apply {
bd.transferEncoding
this.type = contentType?.getParameter("media_type").toInt(ParcelableMedia.Type.UNKNOWN)
this.alt_text = contentType?.getParameter("alt_text")
FileOutputStream(mediaFile).use {
IoUtils.copyStream(st, it, null)
it.flush()
}
this.uri = Uri.fromFile(mediaFile).toString()
}
}
}

View File

@ -27,9 +27,11 @@ import android.content.DialogInterface
import android.content.DialogInterface.OnClickListener
import android.content.Intent
import android.database.Cursor
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.AsyncTask
import android.os.Bundle
import android.os.Environment
import android.os.Handler
import android.support.v4.app.DialogFragment
import android.support.v4.app.FragmentActivity
@ -43,7 +45,10 @@ import android.widget.AbsListView.MultiChoiceModeListener
import android.widget.AdapterView
import android.widget.AdapterView.OnItemClickListener
import android.widget.ListView
import android.widget.Toast
import kotlinx.android.synthetic.main.fragment_drafts.*
import nl.komponents.kovenant.task
import nl.komponents.kovenant.ui.successUi
import org.mariotaku.sqliteqb.library.Columns.Column
import org.mariotaku.sqliteqb.library.Expression
import org.mariotaku.sqliteqb.library.RawItemArray
@ -52,10 +57,10 @@ import org.mariotaku.twidere.TwidereConstants.*
import org.mariotaku.twidere.activity.iface.IExtendedActivity
import org.mariotaku.twidere.adapter.DraftsAdapter
import org.mariotaku.twidere.constant.IntentConstants
import org.mariotaku.twidere.extension.writeMimeMessageTo
import org.mariotaku.twidere.model.Draft
import org.mariotaku.twidere.model.DraftCursorIndices
import org.mariotaku.twidere.model.ParcelableMediaUpdate
import org.mariotaku.twidere.model.draft.SendDirectMessageActionExtra
import org.mariotaku.twidere.model.draft.SendDirectMessageActionExtras
import org.mariotaku.twidere.model.util.ParcelableStatusUpdateUtils
import org.mariotaku.twidere.provider.TwidereDataStore.Drafts
import org.mariotaku.twidere.service.BackgroundOperationService
@ -63,6 +68,7 @@ import org.mariotaku.twidere.util.AsyncTaskUtils
import org.mariotaku.twidere.util.JsonSerializer
import org.mariotaku.twidere.util.Utils.getDefaultTextSize
import java.io.File
import java.io.FileOutputStream
import java.util.*
class DraftsFragment : BaseSupportFragment(), LoaderCallbacks<Cursor?>, OnItemClickListener, MultiChoiceModeListener {
@ -85,27 +91,52 @@ class DraftsFragment : BaseSupportFragment(), LoaderCallbacks<Cursor?>, OnItemCl
R.id.delete -> {
val f = DeleteDraftsConfirmDialogFragment()
val args = Bundle()
args.putLongArray(IntentConstants.EXTRA_IDS, listView!!.checkedItemIds)
args.putLongArray(IntentConstants.EXTRA_IDS, listView.checkedItemIds)
f.arguments = args
f.show(childFragmentManager, "delete_drafts_confirm")
}
R.id.send -> {
val c = adapter!!.cursor
if (c == null || c.isClosed) return false
val checked = listView!!.checkedItemPositions
val checked = listView.checkedItemPositions
val list = ArrayList<Draft>()
val indices = DraftCursorIndices(c)
for (i in 0 until checked.size()) {
if (checked.valueAt(i) && c.moveToPosition(checked.keyAt(i))) {
list.add(indices.newObject(c))
val position = checked.keyAt(i)
if (checked.valueAt(i)) {
list.add(adapter!!.getDraft(position))
}
}
if (sendDrafts(list)) {
val where = Expression.`in`(Column(Drafts._ID),
RawItemArray(listView!!.checkedItemIds))
RawItemArray(listView.checkedItemIds))
contentResolver.delete(Drafts.CONTENT_URI, where.sql, null)
}
}
R.id.save -> {
val checked = listView.checkedItemPositions
val drafts = ArrayList<Draft>()
for (i in 0 until checked.size()) {
val position = checked.keyAt(i)
if (checked.valueAt(i)) {
drafts.add(adapter!!.getDraft(position))
}
}
task {
val pubDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS)
val paths = drafts.map { draft ->
val name = "${draft.timestamp}.eml"
val destFile = File(pubDir, name)
FileOutputStream(destFile).use {
draft.writeMimeMessageTo(context, it)
it.flush()
}
return@map destFile.absolutePath
}
MediaScannerConnection.scanFile(context, paths.toTypedArray(), null, null)
}.successUi {
Toast.makeText(context, R.string.draft_saved, Toast.LENGTH_SHORT).show()
}.fail { ex ->
Log.w(LOGTAG, ex)
}
}
else -> {
return false
}
@ -140,9 +171,7 @@ class DraftsFragment : BaseSupportFragment(), LoaderCallbacks<Cursor?>, OnItemCl
}
override fun onItemClick(view: AdapterView<*>, child: View, position: Int, id: Long) {
val c = adapter!!.cursor
if (c == null || c.isClosed || !c.moveToPosition(position)) return
val item = DraftCursorIndices.fromCursor(c)
val item = adapter!!.getDraft(position)
if (TextUtils.isEmpty(item.action_type)) {
editDraft(item)
return
@ -162,13 +191,13 @@ class DraftsFragment : BaseSupportFragment(), LoaderCallbacks<Cursor?>, OnItemCl
super.onActivityCreated(savedInstanceState)
adapter = DraftsAdapter(activity)
adapter!!.setTextSize(preferences.getInt(KEY_TEXT_SIZE, getDefaultTextSize(activity)).toFloat())
listView!!.adapter = adapter
listView!!.emptyView = emptyView
listView!!.onItemClickListener = this
listView!!.choiceMode = ListView.CHOICE_MODE_MULTIPLE_MODAL
listView!!.setMultiChoiceModeListener(this)
emptyIcon!!.setImageResource(R.drawable.ic_info_draft)
emptyText!!.setText(R.string.drafts_hint_messages)
listView.adapter = adapter
listView.emptyView = emptyView
listView.onItemClickListener = this
listView.choiceMode = ListView.CHOICE_MODE_MULTIPLE_MODAL
listView.setMultiChoiceModeListener(this)
emptyIcon.setImageResource(R.drawable.ic_info_draft)
emptyText.setText(R.string.drafts_hint_messages)
loaderManager.initLoader(0, null, this)
setListShown(false)
}
@ -207,8 +236,8 @@ class DraftsFragment : BaseSupportFragment(), LoaderCallbacks<Cursor?>, OnItemCl
}
Draft.Action.SEND_DIRECT_MESSAGE_COMPAT, Draft.Action.SEND_DIRECT_MESSAGE -> {
var recipientId: String? = null
if (item.action_extras is SendDirectMessageActionExtra) {
recipientId = (item.action_extras as SendDirectMessageActionExtra).recipientId
if (item.action_extras is SendDirectMessageActionExtras) {
recipientId = (item.action_extras as SendDirectMessageActionExtras).recipientId
}
if (item.account_keys?.isEmpty() ?: true || recipientId == null) {
continue@loop
@ -224,7 +253,7 @@ class DraftsFragment : BaseSupportFragment(), LoaderCallbacks<Cursor?>, OnItemCl
private fun updateTitle(mode: ActionMode?) {
if (listView == null || mode == null) return
val count = listView!!.checkedItemCount
val count = listView.checkedItemCount
mode.title = resources.getQuantityString(R.plurals.Nitems_selected, count, count)
}

View File

@ -199,7 +199,7 @@ object ParcelableStatusUtils {
private fun getPlaceFullName(status: Status): String? {
val place = status.place
if (place != null) return place.fullName
val location = status.location
val location = status.location ?: return null
if (ParcelableLocation.valueOf(location) == null) {
return location
}
@ -211,7 +211,7 @@ object ParcelableStatusUtils {
if (geoLocation != null) {
return ParcelableLocationUtils.fromGeoLocation(geoLocation)
}
val locationString = status.location
val locationString = status.location ?: return null
val location = ParcelableLocation.valueOf(locationString)
if (location != null) {
return location

View File

@ -31,7 +31,6 @@ class AccountAuthenticatorService : Service() {
internal class TwidereAccountAuthenticator(val context: Context) : AbstractAccountAuthenticator(context) {
// TODO: Make SignInActivity comply with AccountAuthenticatorActivity
override fun addAccount(response: AccountAuthenticatorResponse, accountType: String,
authTokenType: String?, requiredFeatures: Array<String>?,
options: Bundle?): Bundle {

View File

@ -62,7 +62,7 @@ import org.mariotaku.twidere.TwidereConstants.*
import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.extension.newMicroBlogInstance
import org.mariotaku.twidere.model.*
import org.mariotaku.twidere.model.draft.SendDirectMessageActionExtra
import org.mariotaku.twidere.model.draft.SendDirectMessageActionExtras
import org.mariotaku.twidere.model.util.AccountUtils
import org.mariotaku.twidere.model.util.ParcelableDirectMessageUtils
import org.mariotaku.twidere.model.util.ParcelableStatusUpdateUtils
@ -173,7 +173,7 @@ class BackgroundOperationService : IntentService("background_operation"), Consta
updateStatuses(item.action_type, ParcelableStatusUpdateUtils.fromDraftItem(this, item))
}
Draft.Action.SEND_DIRECT_MESSAGE_COMPAT, Draft.Action.SEND_DIRECT_MESSAGE -> {
val recipientId = (item.action_extras as? SendDirectMessageActionExtra)?.recipientId ?: return
val recipientId = (item.action_extras as? SendDirectMessageActionExtras)?.recipientId ?: return
if (item.account_keys?.isEmpty() ?: true) {
return
}

View File

@ -32,7 +32,7 @@ import org.mariotaku.twidere.TwidereConstants.*
import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.app.TwidereApplication
import org.mariotaku.twidere.model.*
import org.mariotaku.twidere.model.draft.UpdateStatusActionExtra
import org.mariotaku.twidere.model.draft.UpdateStatusActionExtras
import org.mariotaku.twidere.model.util.ParcelableLocationUtils
import org.mariotaku.twidere.model.util.ParcelableStatusUtils
import org.mariotaku.twidere.preference.ServicePickerPreference
@ -519,7 +519,8 @@ class UpdateStatusTask(
draft.text = statusUpdate.text
draft.location = statusUpdate.location
draft.media = statusUpdate.media
draft.action_extras = UpdateStatusActionExtra().apply {
draft.timestamp = System.currentTimeMillis()
draft.action_extras = UpdateStatusActionExtras().apply {
inReplyToStatus = statusUpdate.in_reply_to_status
isPossiblySensitive = statusUpdate.is_possibly_sensitive
isRepostStatusId = statusUpdate.repost_status_id

View File

@ -5,12 +5,17 @@
<item
android:id="@id/delete"
android:icon="@drawable/ic_action_delete"
app:showAsAction="ifRoom|withText"
android:title="@string/delete"/>
android:title="@string/delete"
app:showAsAction="ifRoom|withText"/>
<item
android:id="@id/send"
android:icon="@drawable/ic_action_send"
app:showAsAction="ifRoom|withText"
android:title="@string/send"/>
android:title="@string/send"
app:showAsAction="ifRoom|withText"/>
<item
android:id="@id/save"
android:enabled="@bool/debug"
android:icon="@drawable/ic_action_save"
android:title="@string/save"
android:visible="@bool/debug"/>
</menu>