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 { dependencies {
classpath 'com.github.ben-manes:gradle-versions-plugin:0.13.0' 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.google.gms:google-services:3.0.0'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
classpath('fr.avianey.androidsvgdrawable:gradle-plugin:3.0.0') { 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 android.support.annotation.StringDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/** /**
* Created by mariotaku on 16/1/28. * Created by mariotaku on 16/1/28.
*/ */
@ -15,6 +18,7 @@ import android.support.annotation.StringDef;
CustomTabType.SEARCH_STATUSES, CustomTabType.SEARCH_STATUSES,
CustomTabType.LIST_TIMELINE, CustomTabType.LIST_TIMELINE,
}) })
@Retention(RetentionPolicy.SOURCE)
public @interface CustomTabType { public @interface CustomTabType {
String HOME_TIMELINE = "home_timeline"; String HOME_TIMELINE = "home_timeline";
String NOTIFICATIONS_TIMELINE = "notifications_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.commons.objectcursor.LoganSquareCursorFieldConverter;
import org.mariotaku.library.objectcursor.annotation.CursorField; import org.mariotaku.library.objectcursor.annotation.CursorField;
import org.mariotaku.library.objectcursor.annotation.CursorObject; 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.DraftExtrasConverter;
import org.mariotaku.twidere.model.util.UserKeysCursorFieldConverter; import org.mariotaku.twidere.model.util.UserKeysCursorFieldConverter;
import org.mariotaku.twidere.provider.TwidereDataStore; import org.mariotaku.twidere.provider.TwidereDataStore;
@ -69,7 +69,7 @@ public class Draft implements Parcelable {
@Nullable @Nullable
@ParcelableThisPlease @ParcelableThisPlease
@CursorField(value = Drafts.ACTION_EXTRAS, converter = DraftExtrasConverter.class) @CursorField(value = Drafts.ACTION_EXTRAS, converter = DraftExtrasConverter.class)
public ActionExtra action_extras; public ActionExtras action_extras;
public Draft() { public Draft() {

View File

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

View File

@ -5,5 +5,5 @@ import android.os.Parcelable;
/** /**
* Created by mariotaku on 16/2/21. * 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 @ParcelablePlease
@JsonObject @JsonObject
public class UpdateStatusActionExtra implements ActionExtra { public class UpdateStatusActionExtras implements ActionExtras {
@ParcelableThisPlease @ParcelableThisPlease
@JsonField(name = "in_reply_to_status") @JsonField(name = "in_reply_to_status")
ParcelableStatus inReplyToStatus; ParcelableStatus inReplyToStatus;
@ -78,20 +78,47 @@ public class UpdateStatusActionExtra implements ActionExtra {
@Override @Override
public void writeToParcel(Parcel dest, int flags) { 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 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 @Override
public UpdateStatusActionExtra createFromParcel(Parcel source) { public UpdateStatusActionExtras createFromParcel(Parcel source) {
UpdateStatusActionExtra target = new UpdateStatusActionExtra(); UpdateStatusActionExtras target = new UpdateStatusActionExtras();
UpdateStatusActionExtraParcelablePlease.readFromParcel(target, source); UpdateStatusActionExtrasParcelablePlease.readFromParcel(target, source);
return target; return target;
} }
@Override @Override
public UpdateStatusActionExtra[] newArray(int size) { public UpdateStatusActionExtras[] newArray(int size) {
return new UpdateStatusActionExtra[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.library.objectcursor.converter.CursorFieldConverter;
import org.mariotaku.twidere.model.Draft; import org.mariotaku.twidere.model.Draft;
import org.mariotaku.twidere.model.draft.ActionExtra; import org.mariotaku.twidere.model.draft.ActionExtras;
import org.mariotaku.twidere.model.draft.SendDirectMessageActionExtra; import org.mariotaku.twidere.model.draft.SendDirectMessageActionExtras;
import org.mariotaku.twidere.model.draft.UpdateStatusActionExtra; import org.mariotaku.twidere.model.draft.UpdateStatusActionExtras;
import org.mariotaku.twidere.provider.TwidereDataStore.Drafts; import org.mariotaku.twidere.provider.TwidereDataStore.Drafts;
import java.io.IOException; import java.io.IOException;
@ -19,33 +19,30 @@ import java.lang.reflect.ParameterizedType;
/** /**
* Created by mariotaku on 16/2/20. * Created by mariotaku on 16/2/20.
*/ */
public class DraftExtrasConverter implements CursorFieldConverter<ActionExtra> { public class DraftExtrasConverter implements CursorFieldConverter<ActionExtras> {
@Override @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)); 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) { switch (actionType) {
case "0": case "0":
case "1": case "1":
case Draft.Action.UPDATE_STATUS: case Draft.Action.UPDATE_STATUS:
case Draft.Action.REPLY: case Draft.Action.REPLY:
case Draft.Action.QUOTE: { case Draft.Action.QUOTE: {
final String string = cursor.getString(columnIndex); return LoganSquare.parse(json, UpdateStatusActionExtras.class);
if (TextUtils.isEmpty(string)) return null;
return LoganSquare.parse(string, UpdateStatusActionExtra.class);
} }
case "2": case "2":
case Draft.Action.SEND_DIRECT_MESSAGE: { case Draft.Action.SEND_DIRECT_MESSAGE: {
final String string = cursor.getString(columnIndex); return LoganSquare.parse(json, SendDirectMessageActionExtras.class);
if (TextUtils.isEmpty(string)) return null;
return LoganSquare.parse(string, SendDirectMessageActionExtra.class);
} }
} }
return null; return null;
} }
@Override @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; if (object == null) return;
values.put(columnName, LoganSquare.serialize(object)); 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:support-annotations:$android_support_lib_version"
androidTestCompile 'com.android.support.test:runner:0.5' androidTestCompile 'com.android.support.test:runner:0.5'
androidTestCompile 'com.android.support.test:rules: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:multidex:1.0.1'
compile "com.android.support:support-v4:$android_support_lib_version" compile "com.android.support:support-v4:$android_support_lib_version"

View File

@ -1,16 +1,15 @@
package org.mariotaku.twidere package org.mariotaku.twidere
import android.net.Uri
import android.support.test.InstrumentationRegistry import android.support.test.InstrumentationRegistry
import android.support.test.runner.AndroidJUnit4 import android.support.test.runner.AndroidJUnit4
import org.apache.commons.io.IOUtils
import org.junit.Assert import org.junit.Assert
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mariotaku.twidere.extension.readMimeMessageFrom import org.mariotaku.twidere.extension.readMimeMessageFrom
import org.mariotaku.twidere.extension.writeMimeMessageTo import org.mariotaku.twidere.extension.writeMimeMessageTo
import org.mariotaku.twidere.model.Draft import org.mariotaku.twidere.model.*
import org.mariotaku.twidere.model.ParcelableMedia
import org.mariotaku.twidere.model.ParcelableMediaUpdate
import org.mariotaku.twidere.model.UserKey
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -24,16 +23,24 @@ class DraftExtensionsTest {
fun testMimeMessageProcessing() { fun testMimeMessageProcessing() {
val context = InstrumentationRegistry.getTargetContext() val context = InstrumentationRegistry.getTargetContext()
val draft = Draft() val draft = Draft()
draft.action_type = Draft.Action.UPDATE_STATUS
draft.timestamp = System.currentTimeMillis() draft.timestamp = System.currentTimeMillis()
draft.account_keys = arrayOf(UserKey("user1", "twitter.com"), UserKey("user2", "twitter.com")) draft.account_keys = arrayOf(UserKey("user1", "twitter.com"), UserKey("user2", "twitter.com"))
draft.text = "Hello world" draft.text = "Hello world 测试"
draft.media = arrayOf(ParcelableMediaUpdate().apply { draft.location = ParcelableLocation(-11.956, 99.625) // Randomly generated
this.uri = "file:///system/media/audio/ringtones/Atria.ogg" draft.media = arrayOf(
this.type = ParcelableMedia.Type.VIDEO "file:///system/media/audio/ringtones/Atria.ogg",
this.alt_text = String(CharArray(420).apply { "file:///system/media/audio/ringtones/Callisto.ogg",
fill('A') "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() val output = ByteArrayOutputStream()
draft.writeMimeMessageTo(context, output) draft.writeMimeMessageTo(context, output)
val input = ByteArrayInputStream(output.toByteArray()) val input = ByteArrayInputStream(output.toByteArray())
@ -44,10 +51,16 @@ class DraftExtensionsTest {
Assert.assertArrayEquals(draft.account_keys?.sortedArray(), newDraft.account_keys?.sortedArray()) Assert.assertArrayEquals(draft.account_keys?.sortedArray(), newDraft.account_keys?.sortedArray())
Assert.assertEquals(TimeUnit.MILLISECONDS.toSeconds(draft.timestamp), TimeUnit.MILLISECONDS.toSeconds(newDraft.timestamp)) Assert.assertEquals(TimeUnit.MILLISECONDS.toSeconds(draft.timestamp), TimeUnit.MILLISECONDS.toSeconds(newDraft.timestamp))
Assert.assertEquals(draft.text, newDraft.text) 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 -> draft.media?.forEachIndexed { idx, expected ->
val actual = newDraft.media!![idx] val actual = newDraft.media!![idx]
Assert.assertEquals(expected.alt_text, actual.alt_text) Assert.assertEquals(expected.alt_text, actual.alt_text)
Assert.assertEquals(expected.type, actual.type) 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"/> <category android:name="android.intent.category.DEFAULT"/>
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name="org.mariotaku.twidere.activity.DropboxAuthStarterActivity"
android:theme="@style/Theme.Twidere.NoDisplay"/>
<activity <activity
android:name="com.dropbox.core.android.AuthActivity" android:name="com.dropbox.core.android.AuthActivity"
android:configChanges="orientation|keyboard" android:configChanges="orientation|keyboard"
@ -42,5 +45,7 @@
<category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.DEFAULT"/>
</intent-filter> </intent-filter>
</activity> </activity>
<service android:name="org.mariotaku.twidere.service.DropboxDataSyncService"/>
</application> </application>
</manifest> </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.AccountDetails;
import org.mariotaku.twidere.model.Draft; import org.mariotaku.twidere.model.Draft;
import org.mariotaku.twidere.model.ParcelableStatusUpdate; 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. * Created by mariotaku on 16/2/12.
@ -25,8 +25,8 @@ public class ParcelableStatusUpdateUtils {
statusUpdate.text = draft.text; statusUpdate.text = draft.text;
statusUpdate.location = draft.location; statusUpdate.location = draft.location;
statusUpdate.media = draft.media; statusUpdate.media = draft.media;
if (draft.action_extras instanceof UpdateStatusActionExtra) { if (draft.action_extras instanceof UpdateStatusActionExtras) {
final UpdateStatusActionExtra extra = (UpdateStatusActionExtra) draft.action_extras; final UpdateStatusActionExtras extra = (UpdateStatusActionExtras) draft.action_extras;
statusUpdate.in_reply_to_status = extra.getInReplyToStatus(); statusUpdate.in_reply_to_status = extra.getInReplyToStatus();
statusUpdate.is_possibly_sensitive = extra.isPossiblySensitive(); statusUpdate.is_possibly_sensitive = extra.isPossiblySensitive();
statusUpdate.display_coordinates = extra.getDisplayCoordinates(); 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.ParcelableUserMention;
import org.mariotaku.twidere.model.ParcelableUserValuesCreator; import org.mariotaku.twidere.model.ParcelableUserValuesCreator;
import org.mariotaku.twidere.model.UserKey; 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.ParcelableActivityExtensionsKt;
import org.mariotaku.twidere.model.util.ParcelableDirectMessageUtils; import org.mariotaku.twidere.model.util.ParcelableDirectMessageUtils;
import org.mariotaku.twidere.model.util.ParcelableStatusUtils; 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), values.put(Drafts.MEDIA, JsonSerializer.serialize(Arrays.asList(mediaArray),
ParcelableMediaUpdate.class)); ParcelableMediaUpdate.class));
} }
final SendDirectMessageActionExtra extra = new SendDirectMessageActionExtra(); final SendDirectMessageActionExtras extra = new SendDirectMessageActionExtras();
extra.setRecipientId(recipientId); extra.setRecipientId(recipientId);
values.put(Drafts.ACTION_EXTRAS, JsonSerializer.serialize(extra)); values.put(Drafts.ACTION_EXTRAS, JsonSerializer.serialize(extra));
return values; return values;

View File

@ -31,7 +31,6 @@ import android.os.RemoteException;
import android.util.Log; import android.util.Log;
import org.mariotaku.twidere.BuildConfig; import org.mariotaku.twidere.BuildConfig;
import org.mariotaku.twidere.Constants;
import org.mariotaku.twidere.IStatusShortener; import org.mariotaku.twidere.IStatusShortener;
import org.mariotaku.twidere.model.ParcelableStatus; import org.mariotaku.twidere.model.ParcelableStatus;
import org.mariotaku.twidere.model.ParcelableStatusUpdate; import org.mariotaku.twidere.model.ParcelableStatusUpdate;
@ -40,8 +39,10 @@ import org.mariotaku.twidere.model.UserKey;
import java.util.List; import java.util.List;
public final class StatusShortenerInterface extends AbsServiceInterface<IStatusShortener> import static org.mariotaku.twidere.TwidereConstants.LOGTAG;
implements Constants { 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) { protected StatusShortenerInterface(Context context, String shortenerName, Bundle metaData) {
super(context, shortenerName, 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.BaseDialogFragment
import org.mariotaku.twidere.fragment.ProgressDialogFragment import org.mariotaku.twidere.fragment.ProgressDialogFragment
import org.mariotaku.twidere.model.* 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.AccountUtils
import org.mariotaku.twidere.model.util.ParcelableLocationUtils import org.mariotaku.twidere.model.util.ParcelableLocationUtils
import org.mariotaku.twidere.preference.ServicePickerPreference import org.mariotaku.twidere.preference.ServicePickerPreference
@ -178,8 +178,11 @@ class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener
override fun onBackPressed() { override fun onBackPressed() {
if (currentTask != null && currentTask!!.status == AsyncTask.Status.RUNNING) return if (currentTask != null && currentTask!!.status == AsyncTask.Status.RUNNING) return
if (hasComposingStatus()) { if (!shouldSkipDraft && hasComposingStatus() ) {
shouldSkipDraft = false saveToDrafts()
Toast.makeText(this, R.string.status_saved_to_draft, Toast.LENGTH_SHORT).show()
shouldSkipDraft = true
finish()
} else { } else {
shouldSkipDraft = true shouldSkipDraft = true
discardTweet() discardTweet()
@ -429,7 +432,8 @@ class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener
draft.text = text draft.text = text
draft.media = media draft.media = media
draft.location = recentLocation draft.location = recentLocation
draft.action_extras = UpdateStatusActionExtra().apply { draft.timestamp = System.currentTimeMillis()
draft.action_extras = UpdateStatusActionExtras().apply {
this.inReplyToStatus = this@ComposeActivity.inReplyToStatus this.inReplyToStatus = this@ComposeActivity.inReplyToStatus
this.isPossiblySensitive = this@ComposeActivity.possiblySensitive this.isPossiblySensitive = this@ComposeActivity.possiblySensitive
} }
@ -837,8 +841,8 @@ class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener
addMedia(Arrays.asList(*draft.media)) addMedia(Arrays.asList(*draft.media))
} }
recentLocation = draft.location recentLocation = draft.location
if (draft.action_extras is UpdateStatusActionExtra) { if (draft.action_extras is UpdateStatusActionExtras) {
val extra = draft.action_extras as UpdateStatusActionExtra? val extra = draft.action_extras as UpdateStatusActionExtras?
possiblySensitive = extra!!.isPossiblySensitive possiblySensitive = extra!!.isPossiblySensitive
inReplyToStatus = extra.inReplyToStatus inReplyToStatus = extra.inReplyToStatus
} }
@ -871,16 +875,16 @@ class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener
} }
when (draft.action_type) { when (draft.action_type) {
Draft.Action.REPLY -> { Draft.Action.REPLY -> {
if (draft.action_extras is UpdateStatusActionExtra) { if (draft.action_extras is UpdateStatusActionExtras) {
showReplyLabel((draft.action_extras as UpdateStatusActionExtra).inReplyToStatus) showReplyLabel((draft.action_extras as UpdateStatusActionExtras).inReplyToStatus)
} else { } else {
hideLabel() hideLabel()
return false return false
} }
} }
Draft.Action.QUOTE -> { Draft.Action.QUOTE -> {
if (draft.action_extras is UpdateStatusActionExtra) { if (draft.action_extras is UpdateStatusActionExtras) {
showQuoteLabel((draft.action_extras as UpdateStatusActionExtra).inReplyToStatus) showQuoteLabel((draft.action_extras as UpdateStatusActionExtras).inReplyToStatus)
} else { } else {
hideLabel() hideLabel()
return false return false
@ -1280,7 +1284,7 @@ class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener
update.media = media update.media = media
update.in_reply_to_status = inReplyToStatus update.in_reply_to_status = inReplyToStatus
update.is_possibly_sensitive = isPossiblySensitive 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) BackgroundOperationService.updateStatusesAsync(this, action, update)
if (preferences.getBoolean(KEY_NO_CLOSE_AFTER_TWEET_SENT, false) && inReplyToStatus == null) { if (preferences.getBoolean(KEY_NO_CLOSE_AFTER_TWEET_SENT, false) && inReplyToStatus == null) {
possiblySensitive = false possiblySensitive = false

View File

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

View File

@ -2,27 +2,30 @@ package org.mariotaku.twidere.extension
import android.content.Context import android.content.Context
import android.net.Uri 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.Header
import org.apache.james.mime4j.dom.MessageServiceFactory import org.apache.james.mime4j.dom.MessageServiceFactory
import org.apache.james.mime4j.dom.address.Mailbox import org.apache.james.mime4j.dom.address.Mailbox
import org.apache.james.mime4j.dom.field.* import org.apache.james.mime4j.dom.field.*
import org.apache.james.mime4j.message.AbstractMessage import org.apache.james.mime4j.message.*
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.parser.MimeStreamParser import org.apache.james.mime4j.parser.MimeStreamParser
import org.apache.james.mime4j.storage.StorageBodyFactory import org.apache.james.mime4j.storage.StorageBodyFactory
import org.apache.james.mime4j.stream.BodyDescriptor import org.apache.james.mime4j.stream.BodyDescriptor
import org.apache.james.mime4j.stream.MimeConfig 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.convert
import org.mariotaku.ktextension.toInt import org.mariotaku.ktextension.toInt
import org.mariotaku.ktextension.toString import org.mariotaku.ktextension.toString
import org.mariotaku.twidere.extension.model.getMimeType import org.mariotaku.twidere.extension.model.getMimeType
import org.mariotaku.twidere.model.Draft import org.mariotaku.twidere.model.*
import org.mariotaku.twidere.model.ParcelableMedia import org.mariotaku.twidere.model.Draft.Action
import org.mariotaku.twidere.model.ParcelableMediaUpdate import org.mariotaku.twidere.model.draft.SendDirectMessageActionExtras
import org.mariotaku.twidere.model.UserKey import org.mariotaku.twidere.model.draft.UpdateStatusActionExtras
import org.mariotaku.twidere.util.collection.NonEmptyHashMap import org.mariotaku.twidere.util.collection.NonEmptyHashMap
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.nio.charset.Charset import java.nio.charset.Charset
@ -45,44 +48,64 @@ fun Draft.writeMimeMessageTo(context: Context, st: OutputStream) {
message.date = Date(this.timestamp) message.date = Date(this.timestamp)
message.setFrom(this.account_keys?.map { Mailbox(it.id, it.host) }) 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") val multipart = MultipartImpl("mixed")
multipart.addBodyPart(BodyPart().apply { 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 -> this.media?.forEach { mediaItem ->
multipart.addBodyPart(BodyPart().apply { multipart.addBodyPart(BodyPart().apply {
val uri = Uri.parse(mediaItem.uri) val uri = Uri.parse(mediaItem.uri)
val storage = storageProvider.store(contentResolver.openInputStream(uri))
val mimeType = mediaItem.getMimeType(contentResolver) ?: "application/octet-stream" val mimeType = mediaItem.getMimeType(contentResolver) ?: "application/octet-stream"
val parameters = NonEmptyHashMap<String, String?>() val parameters = NonEmptyHashMap<String, String?>()
parameters["alt_text"] = mediaItem.alt_text parameters["alt_text"] = mediaItem.alt_text
parameters["media_type"] = mediaItem.type.toString() 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.filename = uri.lastPathSegment
this.contentTransferEncoding = MimeUtil.ENC_BASE64
this.setBody(bodyFactory.binaryBody(storage), mimeType, parameters)
}) })
} }
message.setMultipart(multipart) message.setMultipart(multipart)
writer.writeMessage(message, st) writer.writeMessage(message, st)
st.flush()
} }
fun Draft.readMimeMessageFrom(context: Context, st: InputStream) { fun Draft.readMimeMessageFrom(context: Context, st: InputStream) {
val config = MimeConfig() val config = MimeConfig()
val parser = MimeStreamParser(config) val parser = MimeStreamParser(config)
parser.setContentHandler(DraftContentHandler(this)) parser.isContentDecoding = true
parser.setContentHandler(DraftContentHandler(context, this))
parser.parse(st) 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 processingStack = Stack<SimpleContentHandler>()
private val mediaList: MutableList<ParcelableMediaUpdate> = ArrayList() private val mediaList: MutableList<ParcelableMediaUpdate> = ArrayList()
override fun headers(header: Header) { override fun headers(header: Header) {
if (processingStack.isEmpty()) { if (processingStack.isEmpty()) {
draft.timestamp = header.getField("Date").convert { draft.timestamp = header.getField("Date")?.convert {
(it as DateTimeField).date.time (it as DateTimeField).date.time
} } ?: 0
draft.account_keys = header.getField("From").convert { field -> draft.account_keys = header.getField("From")?.convert { field ->
when (field) { when (field) {
is MailboxField -> { is MailboxField -> {
return@convert arrayOf(field.mailbox.convert { UserKey(it.localPart, it.domain) }) 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 { } else {
processingStack.peek().headers(header) processingStack.peek().headers(header)
} }
@ -108,7 +133,7 @@ private class DraftContentHandler(private val draft: Draft) : SimpleContentHandl
} }
override fun startBodyPart() { override fun startBodyPart() {
processingStack.push(BodyPartHandler(draft)) processingStack.push(BodyPartHandler(context, draft))
} }
override fun body(bd: BodyDescriptor?, `is`: InputStream?) { 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 lateinit var header: Header
internal var media: ParcelableMediaUpdate? = null 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 val contentDisposition = header.getField("Content-Disposition") as? ContentDispositionField
if (contentDisposition != null && contentDisposition.isAttachment) { if (contentDisposition != null && contentDisposition.isAttachment) {
when (contentDisposition.filename) { 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 -> { else -> {
val contentType = header.getField("Content-Type") as? ContentTypeField val contentType = header.getField("Content-Type") as? ContentTypeField
val filename = contentDisposition.filename ?: return
val mediaFile = File(context.filesDir, filename)
media = ParcelableMediaUpdate().apply { media = ParcelableMediaUpdate().apply {
bd.transferEncoding
this.type = contentType?.getParameter("media_type").toInt(ParcelableMedia.Type.UNKNOWN) this.type = contentType?.getParameter("media_type").toInt(ParcelableMedia.Type.UNKNOWN)
this.alt_text = contentType?.getParameter("alt_text") 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.DialogInterface.OnClickListener
import android.content.Intent import android.content.Intent
import android.database.Cursor import android.database.Cursor
import android.media.MediaScannerConnection
import android.net.Uri import android.net.Uri
import android.os.AsyncTask import android.os.AsyncTask
import android.os.Bundle import android.os.Bundle
import android.os.Environment
import android.os.Handler import android.os.Handler
import android.support.v4.app.DialogFragment import android.support.v4.app.DialogFragment
import android.support.v4.app.FragmentActivity import android.support.v4.app.FragmentActivity
@ -43,7 +45,10 @@ import android.widget.AbsListView.MultiChoiceModeListener
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.AdapterView.OnItemClickListener import android.widget.AdapterView.OnItemClickListener
import android.widget.ListView import android.widget.ListView
import android.widget.Toast
import kotlinx.android.synthetic.main.fragment_drafts.* 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.Columns.Column
import org.mariotaku.sqliteqb.library.Expression import org.mariotaku.sqliteqb.library.Expression
import org.mariotaku.sqliteqb.library.RawItemArray 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.activity.iface.IExtendedActivity
import org.mariotaku.twidere.adapter.DraftsAdapter import org.mariotaku.twidere.adapter.DraftsAdapter
import org.mariotaku.twidere.constant.IntentConstants import org.mariotaku.twidere.constant.IntentConstants
import org.mariotaku.twidere.extension.writeMimeMessageTo
import org.mariotaku.twidere.model.Draft import org.mariotaku.twidere.model.Draft
import org.mariotaku.twidere.model.DraftCursorIndices
import org.mariotaku.twidere.model.ParcelableMediaUpdate 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.model.util.ParcelableStatusUpdateUtils
import org.mariotaku.twidere.provider.TwidereDataStore.Drafts import org.mariotaku.twidere.provider.TwidereDataStore.Drafts
import org.mariotaku.twidere.service.BackgroundOperationService 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.JsonSerializer
import org.mariotaku.twidere.util.Utils.getDefaultTextSize import org.mariotaku.twidere.util.Utils.getDefaultTextSize
import java.io.File import java.io.File
import java.io.FileOutputStream
import java.util.* import java.util.*
class DraftsFragment : BaseSupportFragment(), LoaderCallbacks<Cursor?>, OnItemClickListener, MultiChoiceModeListener { class DraftsFragment : BaseSupportFragment(), LoaderCallbacks<Cursor?>, OnItemClickListener, MultiChoiceModeListener {
@ -85,27 +91,52 @@ class DraftsFragment : BaseSupportFragment(), LoaderCallbacks<Cursor?>, OnItemCl
R.id.delete -> { R.id.delete -> {
val f = DeleteDraftsConfirmDialogFragment() val f = DeleteDraftsConfirmDialogFragment()
val args = Bundle() val args = Bundle()
args.putLongArray(IntentConstants.EXTRA_IDS, listView!!.checkedItemIds) args.putLongArray(IntentConstants.EXTRA_IDS, listView.checkedItemIds)
f.arguments = args f.arguments = args
f.show(childFragmentManager, "delete_drafts_confirm") f.show(childFragmentManager, "delete_drafts_confirm")
} }
R.id.send -> { R.id.send -> {
val c = adapter!!.cursor val checked = listView.checkedItemPositions
if (c == null || c.isClosed) return false
val checked = listView!!.checkedItemPositions
val list = ArrayList<Draft>() val list = ArrayList<Draft>()
val indices = DraftCursorIndices(c)
for (i in 0 until checked.size()) { for (i in 0 until checked.size()) {
if (checked.valueAt(i) && c.moveToPosition(checked.keyAt(i))) { val position = checked.keyAt(i)
list.add(indices.newObject(c)) if (checked.valueAt(i)) {
list.add(adapter!!.getDraft(position))
} }
} }
if (sendDrafts(list)) { if (sendDrafts(list)) {
val where = Expression.`in`(Column(Drafts._ID), val where = Expression.`in`(Column(Drafts._ID),
RawItemArray(listView!!.checkedItemIds)) RawItemArray(listView.checkedItemIds))
contentResolver.delete(Drafts.CONTENT_URI, where.sql, null) 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 -> { else -> {
return false return false
} }
@ -140,9 +171,7 @@ class DraftsFragment : BaseSupportFragment(), LoaderCallbacks<Cursor?>, OnItemCl
} }
override fun onItemClick(view: AdapterView<*>, child: View, position: Int, id: Long) { override fun onItemClick(view: AdapterView<*>, child: View, position: Int, id: Long) {
val c = adapter!!.cursor val item = adapter!!.getDraft(position)
if (c == null || c.isClosed || !c.moveToPosition(position)) return
val item = DraftCursorIndices.fromCursor(c)
if (TextUtils.isEmpty(item.action_type)) { if (TextUtils.isEmpty(item.action_type)) {
editDraft(item) editDraft(item)
return return
@ -162,13 +191,13 @@ class DraftsFragment : BaseSupportFragment(), LoaderCallbacks<Cursor?>, OnItemCl
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
adapter = DraftsAdapter(activity) adapter = DraftsAdapter(activity)
adapter!!.setTextSize(preferences.getInt(KEY_TEXT_SIZE, getDefaultTextSize(activity)).toFloat()) adapter!!.setTextSize(preferences.getInt(KEY_TEXT_SIZE, getDefaultTextSize(activity)).toFloat())
listView!!.adapter = adapter listView.adapter = adapter
listView!!.emptyView = emptyView listView.emptyView = emptyView
listView!!.onItemClickListener = this listView.onItemClickListener = this
listView!!.choiceMode = ListView.CHOICE_MODE_MULTIPLE_MODAL listView.choiceMode = ListView.CHOICE_MODE_MULTIPLE_MODAL
listView!!.setMultiChoiceModeListener(this) listView.setMultiChoiceModeListener(this)
emptyIcon!!.setImageResource(R.drawable.ic_info_draft) emptyIcon.setImageResource(R.drawable.ic_info_draft)
emptyText!!.setText(R.string.drafts_hint_messages) emptyText.setText(R.string.drafts_hint_messages)
loaderManager.initLoader(0, null, this) loaderManager.initLoader(0, null, this)
setListShown(false) setListShown(false)
} }
@ -207,8 +236,8 @@ class DraftsFragment : BaseSupportFragment(), LoaderCallbacks<Cursor?>, OnItemCl
} }
Draft.Action.SEND_DIRECT_MESSAGE_COMPAT, Draft.Action.SEND_DIRECT_MESSAGE -> { Draft.Action.SEND_DIRECT_MESSAGE_COMPAT, Draft.Action.SEND_DIRECT_MESSAGE -> {
var recipientId: String? = null var recipientId: String? = null
if (item.action_extras is SendDirectMessageActionExtra) { if (item.action_extras is SendDirectMessageActionExtras) {
recipientId = (item.action_extras as SendDirectMessageActionExtra).recipientId recipientId = (item.action_extras as SendDirectMessageActionExtras).recipientId
} }
if (item.account_keys?.isEmpty() ?: true || recipientId == null) { if (item.account_keys?.isEmpty() ?: true || recipientId == null) {
continue@loop continue@loop
@ -224,7 +253,7 @@ class DraftsFragment : BaseSupportFragment(), LoaderCallbacks<Cursor?>, OnItemCl
private fun updateTitle(mode: ActionMode?) { private fun updateTitle(mode: ActionMode?) {
if (listView == null || mode == null) return if (listView == null || mode == null) return
val count = listView!!.checkedItemCount val count = listView.checkedItemCount
mode.title = resources.getQuantityString(R.plurals.Nitems_selected, count, count) mode.title = resources.getQuantityString(R.plurals.Nitems_selected, count, count)
} }

View File

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

View File

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

View File

@ -62,7 +62,7 @@ import org.mariotaku.twidere.TwidereConstants.*
import org.mariotaku.twidere.annotation.AccountType import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.extension.newMicroBlogInstance import org.mariotaku.twidere.extension.newMicroBlogInstance
import org.mariotaku.twidere.model.* 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.AccountUtils
import org.mariotaku.twidere.model.util.ParcelableDirectMessageUtils import org.mariotaku.twidere.model.util.ParcelableDirectMessageUtils
import org.mariotaku.twidere.model.util.ParcelableStatusUpdateUtils 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)) updateStatuses(item.action_type, ParcelableStatusUpdateUtils.fromDraftItem(this, item))
} }
Draft.Action.SEND_DIRECT_MESSAGE_COMPAT, Draft.Action.SEND_DIRECT_MESSAGE -> { 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) { if (item.account_keys?.isEmpty() ?: true) {
return return
} }

View File

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

View File

@ -5,12 +5,17 @@
<item <item
android:id="@id/delete" android:id="@id/delete"
android:icon="@drawable/ic_action_delete" android:icon="@drawable/ic_action_delete"
app:showAsAction="ifRoom|withText" android:title="@string/delete"
android:title="@string/delete"/> app:showAsAction="ifRoom|withText"/>
<item <item
android:id="@id/send" android:id="@id/send"
android:icon="@drawable/ic_action_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> </menu>