Support for quick reply from notifications.
Fixes #483 Closes #3455 // FREEBIE
@ -112,6 +112,15 @@
|
||||
<activity android:name=".ConversationActivity"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/TextSecure.LightTheme.Popup"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
||||
|
||||
<activity android:name=".ConversationPopupActivity"
|
||||
android:windowSoftInputMode="stateVisible"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity=""
|
||||
android:excludeFromRecents="true"
|
||||
android:theme="@style/TextSecure.LightTheme.Popup"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
||||
|
||||
<activity android:name=".MessageDetailsActivity"
|
||||
|
7
res/anim/slide_from_top.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
<translate
|
||||
android:duration="250"
|
||||
android:fromYDelta="-100%"
|
||||
android:toYDelta="0%" />
|
||||
</set>
|
7
res/anim/slide_to_top.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<translate
|
||||
android:duration="250"
|
||||
android:fromYDelta="0%"
|
||||
android:toYDelta="-100%" />
|
||||
</set>
|
BIN
res/drawable-hdpi/ic_launch_white_24dp.png
Normal file
After Width: | Height: | Size: 357 B |
BIN
res/drawable-hdpi/ic_reply_white_36dp.png
Normal file
After Width: | Height: | Size: 467 B |
BIN
res/drawable-mdpi/ic_launch_white_24dp.png
Normal file
After Width: | Height: | Size: 268 B |
BIN
res/drawable-mdpi/ic_reply_white_36dp.png
Normal file
After Width: | Height: | Size: 350 B |
BIN
res/drawable-xhdpi/ic_launch_white_24dp.png
Normal file
After Width: | Height: | Size: 368 B |
BIN
res/drawable-xhdpi/ic_reply_white_36dp.png
Normal file
After Width: | Height: | Size: 567 B |
BIN
res/drawable-xxhdpi/ic_launch_white_24dp.png
Normal file
After Width: | Height: | Size: 533 B |
BIN
res/drawable-xxhdpi/ic_reply_white_36dp.png
Normal file
After Width: | Height: | Size: 814 B |
BIN
res/drawable-xxxhdpi/ic_launch_white_24dp.png
Normal file
After Width: | Height: | Size: 666 B |
BIN
res/drawable-xxxhdpi/ic_reply_white_36dp.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
9
res/menu/conversation_popup.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item android:title="@string/conversation_popup__menu_expand_popup"
|
||||
android:id="@+id/menu_expand"
|
||||
android:icon="?menu_popup_expand"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
</menu>
|
@ -95,6 +95,7 @@
|
||||
<attr name="menu_new_conversation_icon" format="reference" />
|
||||
<attr name="menu_search_icon" format="reference" />
|
||||
<attr name="menu_call_icon" format="reference" />
|
||||
<attr name="menu_popup_expand" format="reference"/>
|
||||
<attr name="menu_unlock_icon" format="reference" />
|
||||
<attr name="menu_lock_icon" format="reference" />
|
||||
<attr name="menu_lock_icon_small" format="reference" />
|
||||
|
@ -436,6 +436,9 @@
|
||||
<string name="MmsMessageRecord_bad_encrypted_mms_message">Bad encrypted MMS message...</string>
|
||||
<string name="MmsMessageRecord_mms_message_encrypted_for_non_existing_session">MMS message encrypted for non-existing session...</string>
|
||||
|
||||
<!-- MuteDialog -->
|
||||
<string name="MuteDialog_mute_notifications">Mute notifications</string>
|
||||
|
||||
<!-- ApplicationMigrationService -->
|
||||
<string name="ApplicationMigrationService_import_in_progress">Import in progress</string>
|
||||
<string name="ApplicationMigrationService_importing_text_messages">Importing text messages</string>
|
||||
@ -458,6 +461,7 @@
|
||||
<string name="MessageNotifier_mark_all_as_read">Mark all as read</string>
|
||||
<string name="MessageNotifier_mark_as_read">Mark as read</string>
|
||||
<string name="MessageNotifier_media_message">Media message</string>
|
||||
<string name="MessageNotifier_reply">Reply</string>
|
||||
|
||||
<!-- QuickResponseService -->
|
||||
<string name="QuickResponseService_quick_response_unavailable_when_TextSecure_is_locked">Quick response unavailable when TextSecure is locked!</string>
|
||||
@ -901,6 +905,9 @@
|
||||
<string name="conversation__menu_delete_thread">Delete thread</string>
|
||||
<string name="conversation__menu_view_media">All images</string>
|
||||
|
||||
<!-- conversation_popup -->
|
||||
<string name="conversation_popup__menu_expand_popup">Expand popup</string>
|
||||
|
||||
<!-- conversation_callable -->
|
||||
<string name="conversation_add_to_contacts__menu_add_to_contacts">Add to contacts</string>
|
||||
|
||||
@ -950,7 +957,6 @@
|
||||
|
||||
<!-- transport_selection_list_item -->
|
||||
<string name="transport_selection_list_item__transport_icon">Transport icon</string>
|
||||
<string name="MuteDialog_mute_notifications">Mute notifications</string>
|
||||
|
||||
|
||||
<!-- EOF -->
|
||||
|
@ -39,6 +39,23 @@
|
||||
<item name="android:windowBackground">@color/black</item>
|
||||
</style>
|
||||
|
||||
<style name="PopupAnimation" parent="@android:style/Animation">
|
||||
<item name="android:windowEnterAnimation">@anim/slide_from_top</item>
|
||||
<item name="android:windowExitAnimation">@anim/slide_to_top</item>
|
||||
</style>
|
||||
|
||||
<style name="TextSecure.LightTheme.Popup" parent="TextSecure.LightTheme">
|
||||
<item name="android:windowIsFloating">false</item>
|
||||
<item name="android:windowSoftInputMode">stateUnchanged</item>
|
||||
<item name="android:windowContentOverlay">@null</item>
|
||||
<item name="android:windowActionModeOverlay" tools:targetApi="honeycomb">true</item>
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
<item name="android:windowFrame">@null</item>
|
||||
<item name="android:windowAnimationStyle">@style/PopupAnimation</item>
|
||||
<item name="android:windowCloseOnTouchOutside" tools:targetApi="honeycomb">true</item>
|
||||
<item name="android:backgroundDimEnabled">false</item>
|
||||
</style>
|
||||
|
||||
<style name="TextSecure.LightTheme" parent="@style/Theme.AppCompat.Light.DarkActionBar">
|
||||
<item name="actionBarStyle">@style/TextSecure.LightActionBar</item>
|
||||
<item name="actionBarTabBarStyle">@style/TextSecure.LightActionBar.TabBar</item>
|
||||
@ -131,6 +148,7 @@
|
||||
<item name="menu_group_icon">@drawable/ic_group_white_24dp</item>
|
||||
<item name="menu_search_icon">@drawable/ic_search_white_24dp</item>
|
||||
<item name="menu_call_icon">@drawable/ic_call_white_24dp</item>
|
||||
<item name="menu_popup_expand">@drawable/ic_launch_white_24dp</item>
|
||||
<item name="menu_unlock_icon">@drawable/ic_unlocked_white_24dp</item>
|
||||
<item name="menu_lock_icon">@drawable/ic_lock_white_24dp</item>
|
||||
<item name="menu_lock_icon_small">@drawable/ic_lock_white_18dp</item>
|
||||
@ -254,6 +272,7 @@
|
||||
<item name="menu_group_icon">@drawable/ic_group_white_24dp</item>
|
||||
<item name="menu_search_icon">@drawable/ic_search_white_24dp</item>
|
||||
<item name="menu_call_icon">@drawable/ic_call_white_24dp</item>
|
||||
<item name="menu_popup_expand">@drawable/ic_launch_white_24dp</item>
|
||||
<item name="menu_unlock_icon">@drawable/ic_unlocked_white_24dp</item>
|
||||
<item name="menu_lock_icon">@drawable/ic_lock_white_24dp</item>
|
||||
<item name="menu_lock_icon_small">@drawable/ic_lock_white_18dp</item>
|
||||
|
@ -97,6 +97,8 @@ import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
||||
import org.whispersystems.libaxolotl.InvalidMessageException;
|
||||
import org.whispersystems.libaxolotl.util.guava.Optional;
|
||||
|
||||
@ -138,16 +140,16 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
private static final int GROUP_EDIT = 5;
|
||||
private static final int CAPTURE_PHOTO = 6;
|
||||
|
||||
private MasterSecret masterSecret;
|
||||
private ComposeText composeText;
|
||||
private AnimatingToggle buttonToggle;
|
||||
private SendButton sendButton;
|
||||
private ImageButton attachButton;
|
||||
private ConversationTitleView titleView;
|
||||
private TextView charactersLeft;
|
||||
private ConversationFragment fragment;
|
||||
private Button unblockButton;
|
||||
private View composePanel;
|
||||
private MasterSecret masterSecret;
|
||||
protected ComposeText composeText;
|
||||
private AnimatingToggle buttonToggle;
|
||||
private SendButton sendButton;
|
||||
private ImageButton attachButton;
|
||||
private ConversationTitleView titleView;
|
||||
private TextView charactersLeft;
|
||||
private ConversationFragment fragment;
|
||||
private Button unblockButton;
|
||||
private View composePanel;
|
||||
|
||||
private AttachmentTypeSelectorAdapter attachmentAdapter;
|
||||
private AttachmentManager attachmentManager;
|
||||
@ -169,7 +171,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
protected void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
dynamicLanguage.onCreate(this);
|
||||
overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -768,7 +769,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
emojiToggle.setOnClickListener(new EmojiToggleListener());
|
||||
}
|
||||
|
||||
private void initializeActionBar() {
|
||||
protected void initializeActionBar() {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setCustomView(R.layout.conversation_title_view);
|
||||
getSupportActionBar().setDisplayShowCustomEnabled(true);
|
||||
@ -950,18 +951,22 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
return drafts;
|
||||
}
|
||||
|
||||
private void saveDraft() {
|
||||
if (this.recipients == null || this.recipients.isEmpty())
|
||||
return;
|
||||
protected ListenableFuture<Long> saveDraft() {
|
||||
final SettableFuture<Long> future = new SettableFuture<>();
|
||||
|
||||
if (this.recipients == null || this.recipients.isEmpty()) {
|
||||
future.set(threadId);
|
||||
return future;
|
||||
}
|
||||
|
||||
final Drafts drafts = getDraftsForCurrentState();
|
||||
final long thisThreadId = this.threadId;
|
||||
final MasterSecret thisMasterSecret = this.masterSecret.parcelClone();
|
||||
final int thisDistributionType = this.distributionType;
|
||||
|
||||
new AsyncTask<Long, Void, Void>() {
|
||||
new AsyncTask<Long, Void, Long>() {
|
||||
@Override
|
||||
protected Void doInBackground(Long... params) {
|
||||
protected Long doInBackground(Long... params) {
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(ConversationActivity.this);
|
||||
DraftDatabase draftDatabase = DatabaseFactory.getDraftDatabase(ConversationActivity.this);
|
||||
long threadId = params[0];
|
||||
@ -974,9 +979,18 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
} else if (threadId > 0) {
|
||||
threadDatabase.update(threadId);
|
||||
}
|
||||
return null;
|
||||
|
||||
return threadId;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Long result) {
|
||||
future.set(result);
|
||||
}
|
||||
|
||||
}.execute(thisThreadId);
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
private void setBlockedUserState(Recipients recipients) {
|
||||
@ -1031,10 +1045,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
return getRecipients() != null && getRecipients().isGroupRecipient();
|
||||
}
|
||||
|
||||
private Recipients getRecipients() {
|
||||
protected Recipients getRecipients() {
|
||||
return this.recipients;
|
||||
}
|
||||
|
||||
protected long getThreadId() {
|
||||
return this.threadId;
|
||||
}
|
||||
|
||||
private String getMessage() throws InvalidMessageException {
|
||||
String rawText = composeText.getText().toString();
|
||||
|
||||
@ -1055,7 +1073,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}.execute(threadId);
|
||||
}
|
||||
|
||||
private void sendComplete(long threadId) {
|
||||
protected void sendComplete(long threadId) {
|
||||
boolean refreshFragment = (threadId != this.threadId);
|
||||
this.threadId = threadId;
|
||||
|
||||
|
@ -174,6 +174,7 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, distributionType);
|
||||
|
||||
startActivity(intent);
|
||||
overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out);
|
||||
}
|
||||
|
||||
private void handleDisplaySettings() {
|
||||
|
117
src/org/thoughtcrime/securesms/ConversationPopupActivity.java
Normal file
@ -0,0 +1,117 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.ActivityOptionsCompat;
|
||||
import android.util.Log;
|
||||
import android.view.Display;
|
||||
import android.view.Gravity;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
public class ConversationPopupActivity extends ConversationActivity {
|
||||
|
||||
private static final String TAG = ConversationPopupActivity.class.getSimpleName();
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
super.onPreCreate();
|
||||
overridePendingTransition(R.anim.slide_from_top, R.anim.slide_to_top);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle, @NonNull MasterSecret masterSecret) {
|
||||
getWindow().setFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND,
|
||||
WindowManager.LayoutParams.FLAG_DIM_BEHIND);
|
||||
|
||||
WindowManager.LayoutParams params = getWindow().getAttributes();
|
||||
params.alpha = 1.0f;
|
||||
params.dimAmount = 0.1f;
|
||||
params.gravity = Gravity.TOP;
|
||||
getWindow().setAttributes(params);
|
||||
|
||||
Display display = getWindowManager().getDefaultDisplay();
|
||||
int width = display.getWidth();
|
||||
int height = display.getHeight();
|
||||
|
||||
if (height > width) getWindow().setLayout((int) (width * .85), (int) (height * .5));
|
||||
else getWindow().setLayout((int) (width * .7), (int) (height * .75));
|
||||
|
||||
super.onCreate(bundle, masterSecret);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
composeText.requestFocus();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
if (isFinishing()) overridePendingTransition(R.anim.slide_from_top, R.anim.slide_to_top);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
MenuInflater inflater = this.getMenuInflater();
|
||||
menu.clear();
|
||||
|
||||
inflater.inflate(R.menu.conversation_popup, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_expand:
|
||||
saveDraft().addListener(new ListenableFuture.Listener<Long>() {
|
||||
@Override
|
||||
public void onSuccess(Long result) {
|
||||
ActivityOptionsCompat transition = ActivityOptionsCompat.makeScaleUpAnimation(getWindow().getDecorView(), 0, 0, getWindow().getAttributes().width, getWindow().getAttributes().height);
|
||||
Intent intent = new Intent(ConversationPopupActivity.this, ConversationActivity.class);
|
||||
intent.putExtra(ConversationActivity.RECIPIENTS_EXTRA, getRecipients().getIds());
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, result);
|
||||
|
||||
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
startActivity(intent, transition.toBundle());
|
||||
} else {
|
||||
startActivity(intent);
|
||||
overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_right);
|
||||
}
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(ExecutionException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initializeActionBar() {
|
||||
super.initializeActionBar();
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void sendComplete(long threadId) {
|
||||
super.sendComplete(threadId);
|
||||
finish();
|
||||
}
|
||||
}
|
@ -94,7 +94,6 @@ public class MessageNotifier {
|
||||
sendInThreadNotification(context, recipients);
|
||||
} else {
|
||||
Intent intent = new Intent(context, ConversationActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
intent.putExtra("recipients", recipients.getIds());
|
||||
intent.putExtra("thread_id", threadId);
|
||||
intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
|
||||
@ -221,6 +220,7 @@ public class MessageNotifier {
|
||||
context.getString(R.string.MessageNotifier_mark_as_read),
|
||||
notificationState.getMarkAsReadIntent(context, masterSecret));
|
||||
builder.addAction(markAsReadAction);
|
||||
builder.addAction(new Action(R.drawable.ic_reply_white_36dp, context.getString(R.string.MessageNotifier_reply), notifications.get(0).getReplyIntent(context)));
|
||||
builder.extend(new NotificationCompat.WearableExtender().addAction(markAsReadAction));
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ import android.support.annotation.Nullable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
|
||||
import org.thoughtcrime.securesms.ConversationActivity;
|
||||
import org.thoughtcrime.securesms.ConversationPopupActivity;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipients;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
@ -81,8 +82,7 @@ public class NotificationItem {
|
||||
}
|
||||
|
||||
public PendingIntent getPendingIntent(Context context) {
|
||||
Intent intent = new Intent(context, ConversationActivity.class);
|
||||
|
||||
Intent intent = new Intent(context, ConversationActivity.class);
|
||||
Recipients notifyRecipients = threadRecipients != null ? threadRecipients : recipients;
|
||||
if (notifyRecipients != null) intent.putExtra("recipients", notifyRecipients.getIds());
|
||||
|
||||
@ -92,4 +92,16 @@ public class NotificationItem {
|
||||
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
public PendingIntent getReplyIntent(Context context) {
|
||||
Intent intent = new Intent(context, ConversationPopupActivity.class);
|
||||
Recipients notifyRecipients = threadRecipients != null ? threadRecipients : recipients;
|
||||
if (notifyRecipients != null) intent.putExtra(ConversationActivity.RECIPIENTS_EXTRA, notifyRecipients.getIds());
|
||||
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
|
||||
intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
|
||||
|
||||
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,12 @@
|
||||
package org.thoughtcrime.securesms.util.concurrent;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
public interface ListenableFuture<T> {
|
||||
void addListener(Listener<T> listener);
|
||||
|
||||
public interface Listener<T> {
|
||||
public void onSuccess(T result);
|
||||
public void onFailure(ExecutionException e);
|
||||
}
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
package org.thoughtcrime.securesms.util.concurrent;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
public class SettableFuture<T> implements Future<T>, ListenableFuture<T> {
|
||||
|
||||
private final List<Listener<T>> listeners = new LinkedList<>();
|
||||
|
||||
private boolean completed;
|
||||
private boolean canceled;
|
||||
private volatile T result;
|
||||
private volatile Throwable exception;
|
||||
|
||||
@Override
|
||||
public synchronized boolean cancel(boolean mayInterruptIfRunning) {
|
||||
if (!completed && !canceled) {
|
||||
canceled = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized boolean isCancelled() {
|
||||
return canceled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized boolean isDone() {
|
||||
return completed;
|
||||
}
|
||||
|
||||
public boolean set(T result) {
|
||||
synchronized (this) {
|
||||
if (completed || canceled) return false;
|
||||
|
||||
this.result = result;
|
||||
this.completed = true;
|
||||
}
|
||||
|
||||
notifyAllListeners();
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean setException(Throwable throwable) {
|
||||
synchronized (this) {
|
||||
if (completed || canceled) return false;
|
||||
|
||||
this.exception = throwable;
|
||||
this.completed = true;
|
||||
}
|
||||
|
||||
notifyAllListeners();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized T get() throws InterruptedException, ExecutionException {
|
||||
while (!completed) wait();
|
||||
|
||||
if (exception != null) throw new ExecutionException(exception);
|
||||
else return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized T get(long timeout, TimeUnit unit)
|
||||
throws InterruptedException, ExecutionException, TimeoutException
|
||||
{
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
while (!completed && System.currentTimeMillis() - startTime > unit.toMillis(timeout)) {
|
||||
wait(unit.toMillis(timeout));
|
||||
}
|
||||
|
||||
if (!completed) throw new TimeoutException();
|
||||
else return get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addListener(Listener<T> listener) {
|
||||
synchronized (this) {
|
||||
listeners.add(listener);
|
||||
|
||||
if (!completed) return;
|
||||
}
|
||||
|
||||
notifyListener(listener);
|
||||
}
|
||||
|
||||
private void notifyAllListeners() {
|
||||
List<Listener<T>> localListeners;
|
||||
|
||||
synchronized (this) {
|
||||
localListeners = new LinkedList<>(listeners);
|
||||
}
|
||||
|
||||
for (Listener<T> listener : localListeners) {
|
||||
notifyListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyListener(Listener<T> listener) {
|
||||
if (exception != null) listener.onFailure(new ExecutionException(exception));
|
||||
else listener.onSuccess(result);
|
||||
}
|
||||
}
|