mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-19 06:09:55 +01:00
Merge branch '1438-send-image-attachments-ui' into 'master'
Implement UX for sending image attachments See merge request briar/briar!1004
This commit is contained in:
@@ -105,6 +105,7 @@ dependencies {
|
|||||||
implementation "com.android.support:cardview-v7:$supportVersion"
|
implementation "com.android.support:cardview-v7:$supportVersion"
|
||||||
implementation "com.android.support:support-annotations:$supportVersion"
|
implementation "com.android.support:support-annotations:$supportVersion"
|
||||||
implementation "com.android.support:exifinterface:$supportVersion"
|
implementation "com.android.support:exifinterface:$supportVersion"
|
||||||
|
implementation "com.android.support:palette-v7:$supportVersion"
|
||||||
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
|
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
|
||||||
implementation "android.arch.lifecycle:extensions:1.1.1"
|
implementation "android.arch.lifecycle:extensions:1.1.1"
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import org.briarproject.briar.BriarCoreModule;
|
|||||||
import org.briarproject.briar.android.conversation.glide.BriarModelLoader;
|
import org.briarproject.briar.android.conversation.glide.BriarModelLoader;
|
||||||
import org.briarproject.briar.android.login.SignInReminderReceiver;
|
import org.briarproject.briar.android.login.SignInReminderReceiver;
|
||||||
import org.briarproject.briar.android.reporting.BriarReportSender;
|
import org.briarproject.briar.android.reporting.BriarReportSender;
|
||||||
import org.briarproject.briar.android.view.TextInputView;
|
import org.briarproject.briar.android.view.EmojiTextInputView;
|
||||||
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
||||||
import org.briarproject.briar.api.android.DozeWatchdog;
|
import org.briarproject.briar.api.android.DozeWatchdog;
|
||||||
import org.briarproject.briar.api.android.LockManager;
|
import org.briarproject.briar.api.android.LockManager;
|
||||||
@@ -169,7 +169,7 @@ public interface AndroidComponent
|
|||||||
|
|
||||||
void inject(NotificationCleanupService notificationCleanupService);
|
void inject(NotificationCleanupService notificationCleanupService);
|
||||||
|
|
||||||
void inject(TextInputView textInputView);
|
void inject(EmojiTextInputView textInputView);
|
||||||
|
|
||||||
void inject(BriarModelLoader briarModelLoader);
|
void inject(BriarModelLoader briarModelLoader);
|
||||||
|
|
||||||
|
|||||||
@@ -30,4 +30,10 @@ public interface TestingConstants {
|
|||||||
long EXPIRY_DATE = IS_DEBUG_BUILD || IS_BETA_BUILD ?
|
long EXPIRY_DATE = IS_DEBUG_BUILD || IS_BETA_BUILD ?
|
||||||
BuildConfig.BuildTimestamp + 90 * 24 * 60 * 60 * 1000L :
|
BuildConfig.BuildTimestamp + 90 * 24 * 60 * 60 * 1000L :
|
||||||
Long.MAX_VALUE;
|
Long.MAX_VALUE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feature flag for enabling image attachments.
|
||||||
|
*/
|
||||||
|
boolean FEATURE_FLAG_IMAGE_ATTACHMENTS = IS_DEBUG_BUILD;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,5 +14,6 @@ public interface RequestCodes {
|
|||||||
int REQUEST_BLUETOOTH_DISCOVERABLE = 10;
|
int REQUEST_BLUETOOTH_DISCOVERABLE = 10;
|
||||||
int REQUEST_UNLOCK = 11;
|
int REQUEST_UNLOCK = 11;
|
||||||
int REQUEST_KEYGUARD_UNLOCK = 12;
|
int REQUEST_KEYGUARD_UNLOCK = 12;
|
||||||
|
int REQUEST_ATTACH_IMAGE = 13;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.briarproject.briar.android.blog;
|
package org.briarproject.briar.android.blog;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@@ -18,7 +19,10 @@ import org.briarproject.briar.android.controller.handler.UiExceptionHandler;
|
|||||||
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
|
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
|
||||||
import org.briarproject.briar.android.fragment.BaseFragment;
|
import org.briarproject.briar.android.fragment.BaseFragment;
|
||||||
import org.briarproject.briar.android.view.TextInputView;
|
import org.briarproject.briar.android.view.TextInputView;
|
||||||
import org.briarproject.briar.android.view.TextInputView.TextInputListener;
|
import org.briarproject.briar.android.view.TextSendController;
|
||||||
|
import org.briarproject.briar.android.view.TextSendController.SendListener;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
@@ -29,10 +33,11 @@ import static android.view.View.INVISIBLE;
|
|||||||
import static android.view.View.VISIBLE;
|
import static android.view.View.VISIBLE;
|
||||||
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
|
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
|
||||||
import static org.briarproject.briar.android.blog.BasePostFragment.POST_ID;
|
import static org.briarproject.briar.android.blog.BasePostFragment.POST_ID;
|
||||||
|
import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH;
|
||||||
|
|
||||||
@MethodsNotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
public class ReblogFragment extends BaseFragment implements TextInputListener {
|
public class ReblogFragment extends BaseFragment implements SendListener {
|
||||||
|
|
||||||
public static final String TAG = ReblogFragment.class.getName();
|
public static final String TAG = ReblogFragment.class.getName();
|
||||||
|
|
||||||
@@ -77,7 +82,11 @@ public class ReblogFragment extends BaseFragment implements TextInputListener {
|
|||||||
View v = inflater.inflate(R.layout.fragment_reblog, container, false);
|
View v = inflater.inflate(R.layout.fragment_reblog, container, false);
|
||||||
ui = new ViewHolder(v);
|
ui = new ViewHolder(v);
|
||||||
ui.post.setTransitionName(postId);
|
ui.post.setTransitionName(postId);
|
||||||
ui.input.setSendButtonEnabled(false);
|
TextSendController sendController =
|
||||||
|
new TextSendController(ui.input, this, true);
|
||||||
|
ui.input.setSendController(sendController);
|
||||||
|
ui.input.setEnabled(false);
|
||||||
|
ui.input.setMaxTextLength(MAX_BLOG_POST_TEXT_LENGTH);
|
||||||
showProgressBar();
|
showProgressBar();
|
||||||
|
|
||||||
return v;
|
return v;
|
||||||
@@ -112,16 +121,14 @@ public class ReblogFragment extends BaseFragment implements TextInputListener {
|
|||||||
ui.post.bindItem(item);
|
ui.post.bindItem(item);
|
||||||
ui.post.hideReblogButton();
|
ui.post.hideReblogButton();
|
||||||
|
|
||||||
ui.input.setListener(this);
|
ui.input.setEnabled(true);
|
||||||
ui.input.setSendButtonEnabled(true);
|
|
||||||
ui.scrollView.post(() -> ui.scrollView.fullScroll(FOCUS_DOWN));
|
ui.scrollView.post(() -> ui.scrollView.fullScroll(FOCUS_DOWN));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSendClick(String text) {
|
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
|
||||||
ui.input.hideSoftKeyboard();
|
ui.input.hideSoftKeyboard();
|
||||||
String comment = getComment();
|
feedController.repeatPost(item, text,
|
||||||
feedController.repeatPost(item, comment,
|
|
||||||
new UiExceptionHandler<DbException>(this) {
|
new UiExceptionHandler<DbException>(this) {
|
||||||
@Override
|
@Override
|
||||||
public void onExceptionUi(DbException exception) {
|
public void onExceptionUi(DbException exception) {
|
||||||
@@ -131,12 +138,6 @@ public class ReblogFragment extends BaseFragment implements TextInputListener {
|
|||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private String getComment() {
|
|
||||||
if (ui.input.getText().length() == 0) return null;
|
|
||||||
return ui.input.getText().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showProgressBar() {
|
private void showProgressBar() {
|
||||||
ui.progressBar.setVisibility(VISIBLE);
|
ui.progressBar.setVisibility(VISIBLE);
|
||||||
ui.input.setVisibility(GONE);
|
ui.input.setVisibility(GONE);
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package org.briarproject.briar.android.blog;
|
package org.briarproject.briar.android.blog;
|
||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.Editable;
|
import android.support.annotation.Nullable;
|
||||||
import android.text.TextWatcher;
|
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
@@ -14,19 +14,22 @@ import org.briarproject.bramble.api.FormatException;
|
|||||||
import org.briarproject.bramble.api.db.DbException;
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
import org.briarproject.bramble.api.identity.IdentityManager;
|
import org.briarproject.bramble.api.identity.IdentityManager;
|
||||||
import org.briarproject.bramble.api.identity.LocalAuthor;
|
import org.briarproject.bramble.api.identity.LocalAuthor;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.sync.GroupId;
|
import org.briarproject.bramble.api.sync.GroupId;
|
||||||
import org.briarproject.bramble.util.StringUtils;
|
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||||
import org.briarproject.briar.android.activity.BriarActivity;
|
import org.briarproject.briar.android.activity.BriarActivity;
|
||||||
import org.briarproject.briar.android.view.TextInputView;
|
import org.briarproject.briar.android.view.TextInputView;
|
||||||
import org.briarproject.briar.android.view.TextInputView.TextInputListener;
|
import org.briarproject.briar.android.view.TextSendController;
|
||||||
|
import org.briarproject.briar.android.view.TextSendController.SendListener;
|
||||||
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
||||||
import org.briarproject.briar.api.blog.BlogManager;
|
import org.briarproject.briar.api.blog.BlogManager;
|
||||||
import org.briarproject.briar.api.blog.BlogPost;
|
import org.briarproject.briar.api.blog.BlogPost;
|
||||||
import org.briarproject.briar.api.blog.BlogPostFactory;
|
import org.briarproject.briar.api.blog.BlogPostFactory;
|
||||||
|
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.util.List;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
@@ -35,10 +38,13 @@ import static android.view.View.GONE;
|
|||||||
import static android.view.View.VISIBLE;
|
import static android.view.View.VISIBLE;
|
||||||
import static java.util.logging.Level.WARNING;
|
import static java.util.logging.Level.WARNING;
|
||||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||||
|
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
|
||||||
import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH;
|
import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH;
|
||||||
|
|
||||||
|
@MethodsNotNullByDefault
|
||||||
|
@ParametersNotNullByDefault
|
||||||
public class WriteBlogPostActivity extends BriarActivity
|
public class WriteBlogPostActivity extends BriarActivity
|
||||||
implements OnEditorActionListener, TextInputListener {
|
implements OnEditorActionListener, SendListener {
|
||||||
|
|
||||||
private static final Logger LOG =
|
private static final Logger LOG =
|
||||||
Logger.getLogger(WriteBlogPostActivity.class.getName());
|
Logger.getLogger(WriteBlogPostActivity.class.getName());
|
||||||
@@ -58,9 +64,8 @@ public class WriteBlogPostActivity extends BriarActivity
|
|||||||
@Inject
|
@Inject
|
||||||
volatile BlogManager blogManager;
|
volatile BlogManager blogManager;
|
||||||
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle state) {
|
public void onCreate(@Nullable Bundle state) {
|
||||||
super.onCreate(state);
|
super.onCreate(state);
|
||||||
|
|
||||||
Intent i = getIntent();
|
Intent i = getIntent();
|
||||||
@@ -71,24 +76,10 @@ public class WriteBlogPostActivity extends BriarActivity
|
|||||||
setContentView(R.layout.activity_write_blog_post);
|
setContentView(R.layout.activity_write_blog_post);
|
||||||
|
|
||||||
input = findViewById(R.id.textInput);
|
input = findViewById(R.id.textInput);
|
||||||
input.setSendButtonEnabled(false);
|
TextSendController sendController =
|
||||||
input.addTextChangedListener(new TextWatcher() {
|
new TextSendController(input, this, false);
|
||||||
@Override
|
input.setSendController(sendController);
|
||||||
public void beforeTextChanged(CharSequence s, int start, int count,
|
input.setMaxTextLength(MAX_BLOG_POST_TEXT_LENGTH);
|
||||||
int after) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTextChanged(CharSequence s, int start, int before,
|
|
||||||
int count) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void afterTextChanged(Editable s) {
|
|
||||||
enableOrDisablePublishButton();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
input.setListener(this);
|
|
||||||
|
|
||||||
progressBar = findViewById(R.id.progressBar);
|
progressBar = findViewById(R.id.progressBar);
|
||||||
}
|
}
|
||||||
@@ -127,18 +118,15 @@ public class WriteBlogPostActivity extends BriarActivity
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void enableOrDisablePublishButton() {
|
|
||||||
input.setSendButtonEnabled(input.getText().length() > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSendClick(String text) {
|
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
|
||||||
|
if (isNullOrEmpty(text)) throw new AssertionError();
|
||||||
|
|
||||||
// hide publish button, show progress bar
|
// hide publish button, show progress bar
|
||||||
input.hideSoftKeyboard();
|
input.hideSoftKeyboard();
|
||||||
input.setVisibility(GONE);
|
input.setVisibility(GONE);
|
||||||
progressBar.setVisibility(VISIBLE);
|
progressBar.setVisibility(VISIBLE);
|
||||||
|
|
||||||
text = StringUtils.truncateUtf8(text, MAX_BLOG_POST_TEXT_LENGTH);
|
|
||||||
storePost(text);
|
storePost(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.arch.lifecycle.ViewModelProvider;
|
|||||||
import android.arch.lifecycle.ViewModelProviders;
|
import android.arch.lifecycle.ViewModelProviders;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.annotation.UiThread;
|
import android.support.annotation.UiThread;
|
||||||
@@ -52,7 +53,6 @@ import org.briarproject.bramble.api.sync.Message;
|
|||||||
import org.briarproject.bramble.api.sync.MessageId;
|
import org.briarproject.bramble.api.sync.MessageId;
|
||||||
import org.briarproject.bramble.api.sync.event.MessagesAckedEvent;
|
import org.briarproject.bramble.api.sync.event.MessagesAckedEvent;
|
||||||
import org.briarproject.bramble.api.sync.event.MessagesSentEvent;
|
import org.briarproject.bramble.api.sync.event.MessagesSentEvent;
|
||||||
import org.briarproject.bramble.util.StringUtils;
|
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||||
import org.briarproject.briar.android.activity.BriarActivity;
|
import org.briarproject.briar.android.activity.BriarActivity;
|
||||||
@@ -63,8 +63,12 @@ import org.briarproject.briar.android.forum.ForumActivity;
|
|||||||
import org.briarproject.briar.android.introduction.IntroductionActivity;
|
import org.briarproject.briar.android.introduction.IntroductionActivity;
|
||||||
import org.briarproject.briar.android.privategroup.conversation.GroupActivity;
|
import org.briarproject.briar.android.privategroup.conversation.GroupActivity;
|
||||||
import org.briarproject.briar.android.view.BriarRecyclerView;
|
import org.briarproject.briar.android.view.BriarRecyclerView;
|
||||||
|
import org.briarproject.briar.android.view.ImagePreview;
|
||||||
|
import org.briarproject.briar.android.view.TextAttachmentController;
|
||||||
|
import org.briarproject.briar.android.view.TextAttachmentController.AttachImageListener;
|
||||||
import org.briarproject.briar.android.view.TextInputView;
|
import org.briarproject.briar.android.view.TextInputView;
|
||||||
import org.briarproject.briar.android.view.TextInputView.TextInputListener;
|
import org.briarproject.briar.android.view.TextSendController;
|
||||||
|
import org.briarproject.briar.android.view.TextSendController.SendListener;
|
||||||
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
||||||
import org.briarproject.briar.api.blog.BlogSharingManager;
|
import org.briarproject.briar.api.blog.BlogSharingManager;
|
||||||
import org.briarproject.briar.api.client.ProtocolStateException;
|
import org.briarproject.briar.api.client.ProtocolStateException;
|
||||||
@@ -106,6 +110,7 @@ import static android.support.v4.app.ActivityOptionsCompat.makeSceneTransitionAn
|
|||||||
import static android.support.v4.view.ViewCompat.setTransitionName;
|
import static android.support.v4.view.ViewCompat.setTransitionName;
|
||||||
import static android.support.v7.util.SortedList.INVALID_POSITION;
|
import static android.support.v7.util.SortedList.INVALID_POSITION;
|
||||||
import static android.view.Gravity.RIGHT;
|
import static android.view.Gravity.RIGHT;
|
||||||
|
import static android.widget.Toast.LENGTH_LONG;
|
||||||
import static android.widget.Toast.LENGTH_SHORT;
|
import static android.widget.Toast.LENGTH_SHORT;
|
||||||
import static java.util.Collections.emptyList;
|
import static java.util.Collections.emptyList;
|
||||||
import static java.util.Collections.sort;
|
import static java.util.Collections.sort;
|
||||||
@@ -115,6 +120,9 @@ import static java.util.logging.Level.WARNING;
|
|||||||
import static org.briarproject.bramble.util.LogUtils.logDuration;
|
import static org.briarproject.bramble.util.LogUtils.logDuration;
|
||||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||||
import static org.briarproject.bramble.util.LogUtils.now;
|
import static org.briarproject.bramble.util.LogUtils.now;
|
||||||
|
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
|
||||||
|
import static org.briarproject.briar.android.TestingConstants.FEATURE_FLAG_IMAGE_ATTACHMENTS;
|
||||||
|
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_ATTACH_IMAGE;
|
||||||
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_INTRODUCTION;
|
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_INTRODUCTION;
|
||||||
import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENT;
|
import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENT;
|
||||||
import static org.briarproject.briar.android.conversation.ImageActivity.DATE;
|
import static org.briarproject.briar.android.conversation.ImageActivity.DATE;
|
||||||
@@ -130,8 +138,8 @@ import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.S
|
|||||||
@MethodsNotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
public class ConversationActivity extends BriarActivity
|
public class ConversationActivity extends BriarActivity
|
||||||
implements EventListener, ConversationListener, TextInputListener,
|
implements EventListener, ConversationListener, SendListener,
|
||||||
TextCache, AttachmentCache {
|
TextCache, AttachmentCache, AttachImageListener {
|
||||||
|
|
||||||
public static final String CONTACT_ID = "briar.CONTACT_ID";
|
public static final String CONTACT_ID = "briar.CONTACT_ID";
|
||||||
|
|
||||||
@@ -161,6 +169,7 @@ public class ConversationActivity extends BriarActivity
|
|||||||
private BriarRecyclerView list;
|
private BriarRecyclerView list;
|
||||||
private LinearLayoutManager layoutManager;
|
private LinearLayoutManager layoutManager;
|
||||||
private TextInputView textInputView;
|
private TextInputView textInputView;
|
||||||
|
private TextSendController sendController;
|
||||||
|
|
||||||
// Fields that are accessed from background threads must be volatile
|
// Fields that are accessed from background threads must be volatile
|
||||||
@Inject
|
@Inject
|
||||||
@@ -250,7 +259,16 @@ public class ConversationActivity extends BriarActivity
|
|||||||
list.setEmptyText(getString(R.string.no_private_messages));
|
list.setEmptyText(getString(R.string.no_private_messages));
|
||||||
|
|
||||||
textInputView = findViewById(R.id.text_input_container);
|
textInputView = findViewById(R.id.text_input_container);
|
||||||
textInputView.setListener(this);
|
if (FEATURE_FLAG_IMAGE_ATTACHMENTS) {
|
||||||
|
ImagePreview imagePreview = findViewById(R.id.imagePreview);
|
||||||
|
sendController = new TextAttachmentController(textInputView,
|
||||||
|
imagePreview, this, this);
|
||||||
|
} else {
|
||||||
|
sendController = new TextSendController(textInputView, this, false);
|
||||||
|
}
|
||||||
|
textInputView.setSendController(sendController);
|
||||||
|
textInputView.setMaxTextLength(MAX_PRIVATE_MESSAGE_TEXT_LENGTH);
|
||||||
|
textInputView.setEnabled(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -267,6 +285,9 @@ public class ConversationActivity extends BriarActivity
|
|||||||
Snackbar.LENGTH_SHORT);
|
Snackbar.LENGTH_SHORT);
|
||||||
snackbar.getView().setBackgroundResource(R.color.briar_primary);
|
snackbar.getView().setBackgroundResource(R.color.briar_primary);
|
||||||
snackbar.show();
|
snackbar.show();
|
||||||
|
} else if (request == REQUEST_ATTACH_IMAGE && result == RESULT_OK) {
|
||||||
|
// remove cast when removing FEATURE_FLAG_IMAGE_ATTACHMENTS
|
||||||
|
((TextAttachmentController) sendController).onImageReceived(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,7 +426,7 @@ public class ConversationActivity extends BriarActivity
|
|||||||
runOnUiThreadUnlessDestroyed(() -> {
|
runOnUiThreadUnlessDestroyed(() -> {
|
||||||
if (revision == adapter.getRevision()) {
|
if (revision == adapter.getRevision()) {
|
||||||
adapter.incrementRevision();
|
adapter.incrementRevision();
|
||||||
textInputView.setSendButtonEnabled(true);
|
textInputView.setEnabled(true);
|
||||||
List<ConversationItem> items = createItems(headers);
|
List<ConversationItem> items = createItems(headers);
|
||||||
adapter.addAll(items);
|
adapter.addAll(items);
|
||||||
list.showData();
|
list.showData();
|
||||||
@@ -573,14 +594,23 @@ public class ConversationActivity extends BriarActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSendClick(String text) {
|
public void onAttachImage(Intent intent) {
|
||||||
if (text.isEmpty()) return;
|
startActivityForResult(intent, REQUEST_ATTACH_IMAGE);
|
||||||
text = StringUtils.truncateUtf8(text, MAX_PRIVATE_MESSAGE_TEXT_LENGTH);
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
|
||||||
|
if (!imageUris.isEmpty()) {
|
||||||
|
Toast.makeText(this, "Not yet implemented.", LENGTH_LONG).show();
|
||||||
|
textInputView.clearText();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isNullOrEmpty(text)) throw new AssertionError();
|
||||||
long timestamp = System.currentTimeMillis();
|
long timestamp = System.currentTimeMillis();
|
||||||
timestamp = Math.max(timestamp, getMinTimestampForNewMessage());
|
timestamp = Math.max(timestamp, getMinTimestampForNewMessage());
|
||||||
if (messagingGroupId == null) loadGroupId(text, timestamp);
|
if (messagingGroupId == null) loadGroupId(text, timestamp);
|
||||||
else createMessage(text, timestamp);
|
else createMessage(text, timestamp);
|
||||||
textInputView.setText("");
|
textInputView.clearText();
|
||||||
}
|
}
|
||||||
|
|
||||||
private long getMinTimestampForNewMessage() {
|
private long getMinTimestampForNewMessage() {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package org.briarproject.briar.android.introduction;
|
package org.briarproject.briar.android.introduction;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v7.app.ActionBar;
|
import android.support.v7.app.ActionBar;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
@@ -17,14 +17,17 @@ import org.briarproject.bramble.api.contact.Contact;
|
|||||||
import org.briarproject.bramble.api.contact.ContactId;
|
import org.briarproject.bramble.api.contact.ContactId;
|
||||||
import org.briarproject.bramble.api.contact.ContactManager;
|
import org.briarproject.bramble.api.contact.ContactManager;
|
||||||
import org.briarproject.bramble.api.db.DbException;
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
import org.briarproject.bramble.util.StringUtils;
|
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||||
import org.briarproject.briar.android.fragment.BaseFragment;
|
import org.briarproject.briar.android.fragment.BaseFragment;
|
||||||
import org.briarproject.briar.android.view.TextInputView;
|
import org.briarproject.briar.android.view.TextInputView;
|
||||||
import org.briarproject.briar.android.view.TextInputView.TextInputListener;
|
import org.briarproject.briar.android.view.TextSendController;
|
||||||
|
import org.briarproject.briar.android.view.TextSendController.SendListener;
|
||||||
import org.briarproject.briar.api.introduction.IntroductionManager;
|
import org.briarproject.briar.api.introduction.IntroductionManager;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
@@ -41,8 +44,10 @@ import static org.briarproject.bramble.util.LogUtils.logException;
|
|||||||
import static org.briarproject.briar.android.util.UiUtils.getContactDisplayName;
|
import static org.briarproject.briar.android.util.UiUtils.getContactDisplayName;
|
||||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_INTRODUCTION_TEXT_LENGTH;
|
import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_INTRODUCTION_TEXT_LENGTH;
|
||||||
|
|
||||||
|
@MethodsNotNullByDefault
|
||||||
|
@ParametersNotNullByDefault
|
||||||
public class IntroductionMessageFragment extends BaseFragment
|
public class IntroductionMessageFragment extends BaseFragment
|
||||||
implements TextInputListener {
|
implements SendListener {
|
||||||
|
|
||||||
public static final String TAG =
|
public static final String TAG =
|
||||||
IntroductionMessageFragment.class.getName();
|
IntroductionMessageFragment.class.getName();
|
||||||
@@ -84,8 +89,9 @@ public class IntroductionMessageFragment extends BaseFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater,
|
public View onCreateView(LayoutInflater inflater,
|
||||||
ViewGroup container, Bundle savedInstanceState) {
|
@Nullable ViewGroup container,
|
||||||
|
@Nullable Bundle savedInstanceState) {
|
||||||
|
|
||||||
// change toolbar text
|
// change toolbar text
|
||||||
ActionBar actionBar = introductionActivity.getSupportActionBar();
|
ActionBar actionBar = introductionActivity.getSupportActionBar();
|
||||||
@@ -97,7 +103,11 @@ public class IntroductionMessageFragment extends BaseFragment
|
|||||||
View v = inflater.inflate(R.layout.introduction_message, container,
|
View v = inflater.inflate(R.layout.introduction_message, container,
|
||||||
false);
|
false);
|
||||||
ui = new ViewHolder(v);
|
ui = new ViewHolder(v);
|
||||||
ui.message.setSendButtonEnabled(false);
|
TextSendController sendController =
|
||||||
|
new TextSendController(ui.message, this, true);
|
||||||
|
ui.message.setSendController(sendController);
|
||||||
|
ui.message.setMaxTextLength(MAX_INTRODUCTION_TEXT_LENGTH);
|
||||||
|
ui.message.setEnabled(false);
|
||||||
|
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
@@ -156,13 +166,10 @@ public class IntroductionMessageFragment extends BaseFragment
|
|||||||
ui.progressBar.setVisibility(GONE);
|
ui.progressBar.setVisibility(GONE);
|
||||||
|
|
||||||
if (possible) {
|
if (possible) {
|
||||||
// set button action
|
|
||||||
ui.message.setListener(IntroductionMessageFragment.this);
|
|
||||||
|
|
||||||
// show views
|
// show views
|
||||||
ui.notPossible.setVisibility(GONE);
|
ui.notPossible.setVisibility(GONE);
|
||||||
ui.message.setVisibility(VISIBLE);
|
ui.message.setVisibility(VISIBLE);
|
||||||
ui.message.setSendButtonEnabled(true);
|
ui.message.setEnabled(true);
|
||||||
ui.message.showSoftKeyboard();
|
ui.message.showSoftKeyboard();
|
||||||
} else {
|
} else {
|
||||||
ui.notPossible.setVisibility(VISIBLE);
|
ui.notPossible.setVisibility(VISIBLE);
|
||||||
@@ -184,14 +191,11 @@ public class IntroductionMessageFragment extends BaseFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSendClick(@NonNull String text) {
|
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
|
||||||
// disable button to prevent accidental double invitations
|
// disable button to prevent accidental double invitations
|
||||||
ui.message.setSendButtonEnabled(false);
|
ui.message.setEnabled(false);
|
||||||
|
|
||||||
String txt = ui.message.getText().toString();
|
makeIntroduction(contact1, contact2, text);
|
||||||
if (txt.isEmpty()) txt = null;
|
|
||||||
else txt = StringUtils.truncateUtf8(txt, MAX_INTRODUCTION_TEXT_LENGTH);
|
|
||||||
makeIntroduction(contact1, contact2, txt);
|
|
||||||
|
|
||||||
// don't wait for the introduction to be made before finishing activity
|
// don't wait for the introduction to be made before finishing activity
|
||||||
introductionActivity.hideSoftKeyboard(ui.message);
|
introductionActivity.hideSoftKeyboard(ui.message);
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ public class GroupActivity extends
|
|||||||
|
|
||||||
private void setGroupEnabled(boolean enabled) {
|
private void setGroupEnabled(boolean enabled) {
|
||||||
isDissolved = !enabled;
|
isDissolved = !enabled;
|
||||||
textInput.setSendButtonEnabled(enabled);
|
textInput.setEnabled(enabled);
|
||||||
list.getRecyclerView().setAlpha(enabled ? 1f : 0.5f);
|
list.getRecyclerView().setAlpha(enabled ? 1f : 0.5f);
|
||||||
|
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package org.briarproject.briar.android.privategroup.creation;
|
package org.briarproject.briar.android.privategroup.creation;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
import org.briarproject.bramble.api.contact.ContactId;
|
import org.briarproject.bramble.api.contact.ContactId;
|
||||||
import org.briarproject.bramble.api.db.DbException;
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
@@ -18,6 +20,7 @@ public interface CreateGroupController
|
|||||||
ResultExceptionHandler<GroupId, DbException> result);
|
ResultExceptionHandler<GroupId, DbException> result);
|
||||||
|
|
||||||
void sendInvitation(GroupId g, Collection<ContactId> contacts,
|
void sendInvitation(GroupId g, Collection<ContactId> contacts,
|
||||||
String text, ResultExceptionHandler<Void, DbException> result);
|
@Nullable String text,
|
||||||
|
ResultExceptionHandler<Void, DbException> result);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package org.briarproject.briar.android.privategroup.creation;
|
package org.briarproject.briar.android.privategroup.creation;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
import org.briarproject.bramble.api.contact.Contact;
|
import org.briarproject.bramble.api.contact.Contact;
|
||||||
import org.briarproject.bramble.api.contact.ContactId;
|
import org.briarproject.bramble.api.contact.ContactId;
|
||||||
import org.briarproject.bramble.api.contact.ContactManager;
|
import org.briarproject.bramble.api.contact.ContactManager;
|
||||||
@@ -123,7 +125,8 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sendInvitation(GroupId g, Collection<ContactId> contactIds,
|
public void sendInvitation(GroupId g, Collection<ContactId> contactIds,
|
||||||
String text, ResultExceptionHandler<Void, DbException> handler) {
|
@Nullable String text,
|
||||||
|
ResultExceptionHandler<Void, DbException> handler) {
|
||||||
runOnDbThread(() -> {
|
runOnDbThread(() -> {
|
||||||
try {
|
try {
|
||||||
LocalAuthor localAuthor = identityManager.getLocalAuthor();
|
LocalAuthor localAuthor = identityManager.getLocalAuthor();
|
||||||
@@ -144,7 +147,7 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void signInvitations(GroupId g, LocalAuthor localAuthor,
|
private void signInvitations(GroupId g, LocalAuthor localAuthor,
|
||||||
Collection<Contact> contacts, String text,
|
Collection<Contact> contacts, @Nullable String text,
|
||||||
ResultExceptionHandler<Void, DbException> handler) {
|
ResultExceptionHandler<Void, DbException> handler) {
|
||||||
cryptoExecutor.execute(() -> {
|
cryptoExecutor.execute(() -> {
|
||||||
long timestamp = clock.currentTimeMillis();
|
long timestamp = clock.currentTimeMillis();
|
||||||
@@ -160,15 +163,14 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void sendInvitations(GroupId g,
|
private void sendInvitations(GroupId g,
|
||||||
Collection<InvitationContext> contexts, String text,
|
Collection<InvitationContext> contexts, @Nullable String text,
|
||||||
ResultExceptionHandler<Void, DbException> handler) {
|
ResultExceptionHandler<Void, DbException> handler) {
|
||||||
runOnDbThread(() -> {
|
runOnDbThread(() -> {
|
||||||
try {
|
try {
|
||||||
String txt = text.isEmpty() ? null : text;
|
|
||||||
for (InvitationContext context : contexts) {
|
for (InvitationContext context : contexts) {
|
||||||
try {
|
try {
|
||||||
groupInvitationManager.sendInvitation(g,
|
groupInvitationManager.sendInvitation(g,
|
||||||
context.contactId, txt, context.timestamp,
|
context.contactId, text, context.timestamp,
|
||||||
context.signature);
|
context.signature);
|
||||||
} catch (NoSuchContactException e) {
|
} catch (NoSuchContactException e) {
|
||||||
// Continue
|
// Continue
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ public class GroupInviteActivity extends ContactSelectorActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onButtonClick(String text) {
|
public void onButtonClick(@Nullable String text) {
|
||||||
if (groupId == null)
|
if (groupId == null)
|
||||||
throw new IllegalStateException("GroupId was not initialized");
|
throw new IllegalStateException("GroupId was not initialized");
|
||||||
controller.sendInvitation(groupId, contacts, text,
|
controller.sendInvitation(groupId, contacts, text,
|
||||||
@@ -72,7 +72,6 @@ public class GroupInviteActivity extends ContactSelectorActivity
|
|||||||
handleDbException(exception);
|
handleDbException(exception);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,28 +1,31 @@
|
|||||||
package org.briarproject.briar.android.sharing;
|
package org.briarproject.briar.android.sharing;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.support.annotation.StringRes;
|
import android.support.annotation.StringRes;
|
||||||
import android.support.annotation.UiThread;
|
import android.support.annotation.UiThread;
|
||||||
import android.support.design.widget.Snackbar;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
import org.briarproject.briar.android.fragment.BaseFragment;
|
import org.briarproject.briar.android.fragment.BaseFragment;
|
||||||
import org.briarproject.briar.android.view.LargeTextInputView;
|
import org.briarproject.briar.android.view.LargeTextInputView;
|
||||||
import org.briarproject.briar.android.view.TextInputView.TextInputListener;
|
import org.briarproject.briar.android.view.TextSendController;
|
||||||
|
import org.briarproject.briar.android.view.TextSendController.SendListener;
|
||||||
|
|
||||||
import static android.support.design.widget.Snackbar.LENGTH_SHORT;
|
import java.util.List;
|
||||||
import static org.briarproject.bramble.util.StringUtils.truncateUtf8;
|
|
||||||
import static org.briarproject.bramble.util.StringUtils.utf8IsTooLong;
|
|
||||||
import static org.briarproject.briar.api.sharing.SharingConstants.MAX_INVITATION_TEXT_LENGTH;
|
|
||||||
|
|
||||||
|
@MethodsNotNullByDefault
|
||||||
|
@ParametersNotNullByDefault
|
||||||
public abstract class BaseMessageFragment extends BaseFragment
|
public abstract class BaseMessageFragment extends BaseFragment
|
||||||
implements TextInputListener {
|
implements SendListener {
|
||||||
|
|
||||||
protected LargeTextInputView message;
|
protected LargeTextInputView message;
|
||||||
private MessageFragmentListener listener;
|
private MessageFragmentListener listener;
|
||||||
@@ -34,16 +37,20 @@ public abstract class BaseMessageFragment extends BaseFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
public View onCreateView(@Nullable LayoutInflater inflater,
|
||||||
Bundle savedInstanceState) {
|
@Nullable ViewGroup container,
|
||||||
|
@Nullable Bundle savedInstanceState) {
|
||||||
|
|
||||||
// inflate view
|
// inflate view
|
||||||
View v = inflater.inflate(R.layout.fragment_message, container,
|
View v = inflater.inflate(R.layout.fragment_message, container,
|
||||||
false);
|
false);
|
||||||
message = v.findViewById(R.id.messageView);
|
message = v.findViewById(R.id.messageView);
|
||||||
|
TextSendController sendController =
|
||||||
|
new TextSendController(message, this, true);
|
||||||
|
message.setSendController(sendController);
|
||||||
|
message.setMaxTextLength(listener.getMaximumTextLength());
|
||||||
message.setButtonText(getString(getButtonText()));
|
message.setButtonText(getString(getButtonText()));
|
||||||
message.setHint(getHintText());
|
message.setHint(getHintText());
|
||||||
message.setListener(this);
|
|
||||||
|
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
@@ -76,21 +83,12 @@ public abstract class BaseMessageFragment extends BaseFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSendClick(String text) {
|
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
|
||||||
if (utf8IsTooLong(text, listener.getMaximumTextLength())) {
|
|
||||||
Snackbar.make(message, R.string.text_too_long, LENGTH_SHORT).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// disable button to prevent accidental double actions
|
// disable button to prevent accidental double actions
|
||||||
message.setSendButtonEnabled(false);
|
message.setEnabled(false);
|
||||||
message.hideSoftKeyboard();
|
message.hideSoftKeyboard();
|
||||||
|
|
||||||
text = truncateUtf8(text, MAX_INVITATION_TEXT_LENGTH);
|
listener.onButtonClick(text);
|
||||||
if(!listener.onButtonClick(text)) {
|
|
||||||
message.setSendButtonEnabled(true);
|
|
||||||
message.showSoftKeyboard();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
@@ -101,8 +99,7 @@ public abstract class BaseMessageFragment extends BaseFragment
|
|||||||
|
|
||||||
void setTitle(@StringRes int titleRes);
|
void setTitle(@StringRes int titleRes);
|
||||||
|
|
||||||
/** Returns true when the button click has been consumed. */
|
void onButtonClick(@Nullable String text);
|
||||||
boolean onButtonClick(String text);
|
|
||||||
|
|
||||||
int getMaximumTextLength();
|
int getMaximumTextLength();
|
||||||
|
|
||||||
|
|||||||
@@ -41,13 +41,12 @@ public abstract class ShareActivity extends ContactSelectorActivity
|
|||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
@Override
|
@Override
|
||||||
public boolean onButtonClick(String text) {
|
public void onButtonClick(@Nullable String text) {
|
||||||
share(contacts, text);
|
share(contacts, text);
|
||||||
setResult(RESULT_OK);
|
setResult(RESULT_OK);
|
||||||
supportFinishAfterTransition();
|
supportFinishAfterTransition();
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract void share(Collection<ContactId> contacts, String text);
|
abstract void share(Collection<ContactId> contacts, @Nullable String text);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public class ShareBlogActivity extends ShareActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
void share(Collection<ContactId> contacts, String text) {
|
void share(Collection<ContactId> contacts, @Nullable String text) {
|
||||||
controller.share(groupId, contacts, text,
|
controller.share(groupId, contacts, text,
|
||||||
new UiExceptionHandler<DbException>(this) {
|
new UiExceptionHandler<DbException>(this) {
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ import org.briarproject.briar.android.controller.handler.ExceptionHandler;
|
|||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
public interface ShareBlogController
|
public interface ShareBlogController
|
||||||
extends ContactSelectorController<SelectableContactItem> {
|
extends ContactSelectorController<SelectableContactItem> {
|
||||||
|
|
||||||
void share(GroupId g, Collection<ContactId> contacts, String text,
|
void share(GroupId g, Collection<ContactId> contacts, @Nullable String text,
|
||||||
ExceptionHandler<DbException> handler);
|
ExceptionHandler<DbException> handler);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ import java.util.Collection;
|
|||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import javax.annotation.concurrent.Immutable;
|
import javax.annotation.concurrent.Immutable;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import static java.util.logging.Level.WARNING;
|
import static java.util.logging.Level.WARNING;
|
||||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||||
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
|
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
@@ -56,17 +56,16 @@ class ShareBlogControllerImpl extends ContactSelectorControllerImpl
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void share(GroupId g, Collection<ContactId> contacts, String text,
|
public void share(GroupId g, Collection<ContactId> contacts, @Nullable
|
||||||
ExceptionHandler<DbException> handler) {
|
String text, ExceptionHandler<DbException> handler) {
|
||||||
runOnDbThread(() -> {
|
runOnDbThread(() -> {
|
||||||
try {
|
try {
|
||||||
String txt = isNullOrEmpty(text) ? null : text;
|
|
||||||
for (ContactId c : contacts) {
|
for (ContactId c : contacts) {
|
||||||
try {
|
try {
|
||||||
long time = Math.max(clock.currentTimeMillis(),
|
long time = Math.max(clock.currentTimeMillis(),
|
||||||
conversationManager.getGroupCount(c)
|
conversationManager.getGroupCount(c)
|
||||||
.getLatestMsgTime() + 1);
|
.getLatestMsgTime() + 1);
|
||||||
blogSharingManager.sendInvitation(g, c, txt, time);
|
blogSharingManager.sendInvitation(g, c, text, time);
|
||||||
} catch (NoSuchContactException | NoSuchGroupException e) {
|
} catch (NoSuchContactException | NoSuchGroupException e) {
|
||||||
logException(LOG, WARNING, e);
|
logException(LOG, WARNING, e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public class ShareForumActivity extends ShareActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
void share(Collection<ContactId> contacts, String text) {
|
void share(Collection<ContactId> contacts, @Nullable String text) {
|
||||||
controller.share(groupId, contacts, text,
|
controller.share(groupId, contacts, text,
|
||||||
new UiExceptionHandler<DbException>(this) {
|
new UiExceptionHandler<DbException>(this) {
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ import org.briarproject.briar.android.controller.handler.ExceptionHandler;
|
|||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
public interface ShareForumController
|
public interface ShareForumController
|
||||||
extends ContactSelectorController<SelectableContactItem> {
|
extends ContactSelectorController<SelectableContactItem> {
|
||||||
|
|
||||||
void share(GroupId g, Collection<ContactId> contacts, String text,
|
void share(GroupId g, Collection<ContactId> contacts, @Nullable String text,
|
||||||
ExceptionHandler<DbException> handler);
|
ExceptionHandler<DbException> handler);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,19 +13,19 @@ import org.briarproject.bramble.api.sync.GroupId;
|
|||||||
import org.briarproject.bramble.api.system.Clock;
|
import org.briarproject.bramble.api.system.Clock;
|
||||||
import org.briarproject.briar.android.contactselection.ContactSelectorControllerImpl;
|
import org.briarproject.briar.android.contactselection.ContactSelectorControllerImpl;
|
||||||
import org.briarproject.briar.android.controller.handler.ExceptionHandler;
|
import org.briarproject.briar.android.controller.handler.ExceptionHandler;
|
||||||
import org.briarproject.briar.api.forum.ForumSharingManager;
|
|
||||||
import org.briarproject.briar.api.conversation.ConversationManager;
|
import org.briarproject.briar.api.conversation.ConversationManager;
|
||||||
|
import org.briarproject.briar.api.forum.ForumSharingManager;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import javax.annotation.concurrent.Immutable;
|
import javax.annotation.concurrent.Immutable;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import static java.util.logging.Level.WARNING;
|
import static java.util.logging.Level.WARNING;
|
||||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||||
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
|
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
@@ -57,16 +57,15 @@ class ShareForumControllerImpl extends ContactSelectorControllerImpl
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void share(GroupId g, Collection<ContactId> contacts,
|
public void share(GroupId g, Collection<ContactId> contacts,
|
||||||
String text, ExceptionHandler<DbException> handler) {
|
@Nullable String text, ExceptionHandler<DbException> handler) {
|
||||||
runOnDbThread(() -> {
|
runOnDbThread(() -> {
|
||||||
try {
|
try {
|
||||||
String txt = isNullOrEmpty(text) ? null : text;
|
|
||||||
for (ContactId c : contacts) {
|
for (ContactId c : contacts) {
|
||||||
try {
|
try {
|
||||||
long time = Math.max(clock.currentTimeMillis(),
|
long time = Math.max(clock.currentTimeMillis(),
|
||||||
conversationManager.getGroupCount(c)
|
conversationManager.getGroupCount(c)
|
||||||
.getLatestMsgTime() + 1);
|
.getLatestMsgTime() + 1);
|
||||||
forumSharingManager.sendInvitation(g, c, txt, time);
|
forumSharingManager.sendInvitation(g, c, text, time);
|
||||||
} catch (NoSuchContactException | NoSuchGroupException e) {
|
} catch (NoSuchContactException | NoSuchGroupException e) {
|
||||||
logException(LOG, WARNING, e);
|
logException(LOG, WARNING, e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.briarproject.briar.android.threaded;
|
package org.briarproject.briar.android.threaded;
|
||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.CallSuper;
|
import android.support.annotation.CallSuper;
|
||||||
import android.support.annotation.StringRes;
|
import android.support.annotation.StringRes;
|
||||||
@@ -26,13 +27,15 @@ import org.briarproject.briar.android.threaded.ThreadItemAdapter.ThreadItemListe
|
|||||||
import org.briarproject.briar.android.threaded.ThreadListController.ThreadListDataSource;
|
import org.briarproject.briar.android.threaded.ThreadListController.ThreadListDataSource;
|
||||||
import org.briarproject.briar.android.threaded.ThreadListController.ThreadListListener;
|
import org.briarproject.briar.android.threaded.ThreadListController.ThreadListListener;
|
||||||
import org.briarproject.briar.android.view.BriarRecyclerView;
|
import org.briarproject.briar.android.view.BriarRecyclerView;
|
||||||
|
import org.briarproject.briar.android.view.KeyboardAwareLinearLayout;
|
||||||
import org.briarproject.briar.android.view.TextInputView;
|
import org.briarproject.briar.android.view.TextInputView;
|
||||||
import org.briarproject.briar.android.view.TextInputView.TextInputListener;
|
import org.briarproject.briar.android.view.TextSendController;
|
||||||
|
import org.briarproject.briar.android.view.TextSendController.SendListener;
|
||||||
import org.briarproject.briar.android.view.UnreadMessageButton;
|
import org.briarproject.briar.android.view.UnreadMessageButton;
|
||||||
import org.briarproject.briar.api.client.NamedGroup;
|
import org.briarproject.briar.api.client.NamedGroup;
|
||||||
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
@@ -42,14 +45,14 @@ import static android.support.design.widget.Snackbar.make;
|
|||||||
import static android.support.v7.widget.RecyclerView.NO_POSITION;
|
import static android.support.v7.widget.RecyclerView.NO_POSITION;
|
||||||
import static android.support.v7.widget.RecyclerView.SCROLL_STATE_IDLE;
|
import static android.support.v7.widget.RecyclerView.SCROLL_STATE_IDLE;
|
||||||
import static java.util.logging.Level.INFO;
|
import static java.util.logging.Level.INFO;
|
||||||
import static org.briarproject.bramble.util.StringUtils.utf8IsTooLong;
|
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
|
||||||
import static org.briarproject.briar.android.threaded.ThreadItemAdapter.UnreadCount;
|
import static org.briarproject.briar.android.threaded.ThreadItemAdapter.UnreadCount;
|
||||||
|
|
||||||
@MethodsNotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadItem, A extends ThreadItemAdapter<I>>
|
public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadItem, A extends ThreadItemAdapter<I>>
|
||||||
extends BriarActivity
|
extends BriarActivity
|
||||||
implements ThreadListListener<I>, TextInputListener, SharingListener,
|
implements ThreadListListener<I>, SendListener, SharingListener,
|
||||||
ThreadItemListener<I>, ThreadListDataSource {
|
ThreadItemListener<I>, ThreadListDataSource {
|
||||||
|
|
||||||
protected static final String KEY_REPLY_ID = "replyId";
|
protected static final String KEY_REPLY_ID = "replyId";
|
||||||
@@ -86,7 +89,10 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
|
|||||||
getController().setGroupId(groupId);
|
getController().setGroupId(groupId);
|
||||||
|
|
||||||
textInput = findViewById(R.id.text_input_container);
|
textInput = findViewById(R.id.text_input_container);
|
||||||
textInput.setListener(this);
|
TextSendController sendController =
|
||||||
|
new TextSendController(textInput, this, false);
|
||||||
|
textInput.setSendController(sendController);
|
||||||
|
textInput.setMaxTextLength(getMaxTextLength());
|
||||||
list = findViewById(R.id.list);
|
list = findViewById(R.id.list);
|
||||||
layoutManager = new LinearLayoutManager(this);
|
layoutManager = new LinearLayoutManager(this);
|
||||||
// FIXME pre-fetching messes with read state, find better solution #1289
|
// FIXME pre-fetching messes with read state, find better solution #1289
|
||||||
@@ -266,7 +272,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
|
|||||||
@Override
|
@Override
|
||||||
public void onBackPressed() {
|
public void onBackPressed() {
|
||||||
if (adapter.getHighlightedItem() != null) {
|
if (adapter.getHighlightedItem() != null) {
|
||||||
textInput.setText("");
|
textInput.clearText();
|
||||||
replyId = null;
|
replyId = null;
|
||||||
updateTextInput();
|
updateTextInput();
|
||||||
} else {
|
} else {
|
||||||
@@ -348,13 +354,9 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSendClick(String text) {
|
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
|
||||||
if (text.trim().length() == 0)
|
if (isNullOrEmpty(text)) throw new AssertionError();
|
||||||
return;
|
|
||||||
if (utf8IsTooLong(text, getMaxTextLength())) {
|
|
||||||
displaySnackbar(R.string.text_too_long);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
I replyItem = adapter.getHighlightedItem();
|
I replyItem = adapter.getHighlightedItem();
|
||||||
UiResultExceptionHandler<I, DbException> handler =
|
UiResultExceptionHandler<I, DbException> handler =
|
||||||
new UiResultExceptionHandler<I, DbException>(this) {
|
new UiResultExceptionHandler<I, DbException>(this) {
|
||||||
@@ -370,7 +372,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
|
|||||||
};
|
};
|
||||||
getController().createAndStoreMessage(text, replyItem, handler);
|
getController().createAndStoreMessage(text, replyItem, handler);
|
||||||
textInput.hideSoftKeyboard();
|
textInput.hideSoftKeyboard();
|
||||||
textInput.setText("");
|
textInput.clearText();
|
||||||
replyId = null;
|
replyId = null;
|
||||||
updateTextInput();
|
updateTextInput();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,239 @@
|
|||||||
|
package org.briarproject.briar.android.view;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.TypedArray;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.annotation.StringRes;
|
||||||
|
import android.support.v7.widget.AppCompatImageButton;
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.text.TextWatcher;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.inputmethod.InputMethodManager;
|
||||||
|
import android.widget.EditText;
|
||||||
|
|
||||||
|
import com.vanniktech.emoji.EmojiEditText;
|
||||||
|
import com.vanniktech.emoji.EmojiPopup;
|
||||||
|
import com.vanniktech.emoji.RecentEmoji;
|
||||||
|
|
||||||
|
import org.briarproject.briar.R;
|
||||||
|
import org.briarproject.briar.android.BriarApplication;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import static android.content.Context.INPUT_METHOD_SERVICE;
|
||||||
|
import static android.content.Context.LAYOUT_INFLATER_SERVICE;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_ENTER;
|
||||||
|
import static android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT;
|
||||||
|
import static java.util.Objects.requireNonNull;
|
||||||
|
import static org.briarproject.bramble.util.StringUtils.utf8IsTooLong;
|
||||||
|
|
||||||
|
public class EmojiTextInputView extends KeyboardAwareLinearLayout implements
|
||||||
|
TextWatcher {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
RecentEmoji recentEmoji;
|
||||||
|
|
||||||
|
private final AppCompatImageButton emojiToggle;
|
||||||
|
private final EmojiPopup emojiPopup;
|
||||||
|
private final EditText editText;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private TextInputListener listener;
|
||||||
|
private int maxLength = Integer.MAX_VALUE;
|
||||||
|
private boolean emptyTextAllowed = false;
|
||||||
|
private boolean isEmpty = true;
|
||||||
|
|
||||||
|
public EmojiTextInputView(Context context) {
|
||||||
|
this(context, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EmojiTextInputView(Context context, @Nullable AttributeSet attrs) {
|
||||||
|
this(context, attrs, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EmojiTextInputView(Context context, @Nullable AttributeSet attrs,
|
||||||
|
int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
|
||||||
|
// inflate layout
|
||||||
|
LayoutInflater inflater = (LayoutInflater) requireNonNull(
|
||||||
|
context.getSystemService(LAYOUT_INFLATER_SERVICE));
|
||||||
|
inflater.inflate(R.layout.emoji_text_input_view, this, true);
|
||||||
|
|
||||||
|
// get attributes
|
||||||
|
TypedArray a = context.obtainStyledAttributes(attrs,
|
||||||
|
R.styleable.EmojiTextInputView);
|
||||||
|
int paddingBottom = a.getDimensionPixelSize(
|
||||||
|
R.styleable.EmojiTextInputView_textPaddingBottom, 0);
|
||||||
|
int paddingEnd = a.getDimensionPixelSize(
|
||||||
|
R.styleable.EmojiTextInputView_textPaddingEnd, 0);
|
||||||
|
int maxLines =
|
||||||
|
a.getInteger(R.styleable.EmojiTextInputView_maxTextLines, 0);
|
||||||
|
a.recycle();
|
||||||
|
|
||||||
|
// apply attributes to editText
|
||||||
|
editText = findViewById(R.id.input_text);
|
||||||
|
editText.setPadding(0, 0, paddingEnd, paddingBottom);
|
||||||
|
if (maxLines > 0) editText.setMaxLines(maxLines);
|
||||||
|
editText.setOnClickListener(v -> showSoftKeyboard());
|
||||||
|
editText.addTextChangedListener(this);
|
||||||
|
// support sending with Ctrl+Enter
|
||||||
|
editText.setOnKeyListener((v, keyCode, event) -> {
|
||||||
|
if (listener != null && keyCode == KEYCODE_ENTER &&
|
||||||
|
event.isCtrlPressed()) {
|
||||||
|
listener.onSendEvent();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
emojiToggle = findViewById(R.id.emoji_toggle);
|
||||||
|
|
||||||
|
// stuff we can't do in edit mode goes below
|
||||||
|
if (isInEditMode()) {
|
||||||
|
emojiPopup = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BriarApplication app =
|
||||||
|
(BriarApplication) context.getApplicationContext();
|
||||||
|
app.getApplicationComponent().inject(this);
|
||||||
|
emojiPopup = EmojiPopup.Builder
|
||||||
|
.fromRootView(this)
|
||||||
|
.setRecentEmoji(recentEmoji)
|
||||||
|
.setOnEmojiPopupShownListener(this::showKeyboardIcon)
|
||||||
|
.setOnEmojiPopupDismissListener(this::showEmojiIcon)
|
||||||
|
.build((EmojiEditText) editText);
|
||||||
|
emojiToggle.setOnClickListener(v -> emojiPopup.toggle());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeTextChanged(CharSequence s, int start, int count,
|
||||||
|
int after) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(CharSequence s, int start, int before,
|
||||||
|
int count) {
|
||||||
|
// Need to start at position 0 to change empty
|
||||||
|
if (start != 0 || emptyTextAllowed || listener == null) return;
|
||||||
|
if (s.length() == 0) {
|
||||||
|
if (!isEmpty) {
|
||||||
|
isEmpty = true;
|
||||||
|
listener.onTextIsEmptyChanged(true);
|
||||||
|
}
|
||||||
|
} else if (isEmpty) {
|
||||||
|
isEmpty = false;
|
||||||
|
listener.onTextIsEmptyChanged(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable s) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setEnabled(boolean enabled) {
|
||||||
|
super.setEnabled(enabled);
|
||||||
|
editText.setEnabled(enabled);
|
||||||
|
emojiToggle.setEnabled(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setGravity(int gravity) {
|
||||||
|
editText.setGravity(gravity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
|
||||||
|
return editText.requestFocus(direction, previouslyFocusedRect);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDetachedFromWindow() {
|
||||||
|
super.onDetachedFromWindow();
|
||||||
|
if (emojiPopup.isShowing()) emojiPopup.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setTextInputListener(@Nullable TextInputListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setAllowEmptyText(boolean emptyTextAllowed) {
|
||||||
|
this.emptyTextAllowed = emptyTextAllowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setMaxLength(int maxLength) {
|
||||||
|
this.maxLength = maxLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setMaxLines(int maxLines) {
|
||||||
|
editText.setMaxLines(maxLines);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current text or {@code null},
|
||||||
|
* if it is empty or only consists of white-spaces.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
String getText() {
|
||||||
|
Editable editable = editText.getText();
|
||||||
|
String str = editable == null ? null : editable.toString().trim();
|
||||||
|
if (str == null || str.length() == 0) return null;
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearText() {
|
||||||
|
editText.setText(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isEmpty() {
|
||||||
|
return getText() == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isTooLong() {
|
||||||
|
return editText.getText() != null &&
|
||||||
|
utf8IsTooLong(editText.getText().toString().trim(), maxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
CharSequence getHint() {
|
||||||
|
return editText.getHint();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setHint(@StringRes int res) {
|
||||||
|
setHint(getContext().getString(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setHint(CharSequence hint) {
|
||||||
|
editText.setHint(hint);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showEmojiIcon() {
|
||||||
|
emojiToggle.setImageResource(R.drawable.ic_emoji_toggle);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showKeyboardIcon() {
|
||||||
|
emojiToggle.setImageResource(R.drawable.ic_keyboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
void showSoftKeyboard() {
|
||||||
|
Object o = getContext().getSystemService(INPUT_METHOD_SERVICE);
|
||||||
|
InputMethodManager imm = (InputMethodManager) requireNonNull(o);
|
||||||
|
imm.showSoftInput(editText, SHOW_IMPLICIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
void hideSoftKeyboard() {
|
||||||
|
if (emojiPopup.isShowing()) emojiPopup.dismiss();
|
||||||
|
IBinder token = editText.getWindowToken();
|
||||||
|
Object o = getContext().getSystemService(INPUT_METHOD_SERVICE);
|
||||||
|
InputMethodManager imm = (InputMethodManager) requireNonNull(o);
|
||||||
|
imm.hideSoftInputFromWindow(token, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TextInputListener {
|
||||||
|
void onTextIsEmptyChanged(boolean isEmpty);
|
||||||
|
void onSendEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package org.briarproject.briar.android.view;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.constraint.ConstraintLayout;
|
||||||
|
import android.support.v7.graphics.Palette;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.bumptech.glide.load.DataSource;
|
||||||
|
import com.bumptech.glide.load.engine.GlideException;
|
||||||
|
import com.bumptech.glide.request.RequestListener;
|
||||||
|
import com.bumptech.glide.request.target.Target;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.briar.R;
|
||||||
|
import org.briarproject.briar.android.conversation.glide.GlideApp;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static android.content.Context.LAYOUT_INFLATER_SERVICE;
|
||||||
|
import static android.graphics.Color.BLACK;
|
||||||
|
import static android.graphics.Color.WHITE;
|
||||||
|
import static android.support.v7.app.AppCompatDelegate.MODE_NIGHT_YES;
|
||||||
|
import static android.support.v7.app.AppCompatDelegate.getDefaultNightMode;
|
||||||
|
import static android.widget.Toast.LENGTH_LONG;
|
||||||
|
import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE;
|
||||||
|
import static com.bumptech.glide.load.resource.bitmap.DownsampleStrategy.FIT_CENTER;
|
||||||
|
import static java.util.Objects.requireNonNull;
|
||||||
|
|
||||||
|
@NotNullByDefault
|
||||||
|
public class ImagePreview extends ConstraintLayout {
|
||||||
|
|
||||||
|
private final ImageView imageView;
|
||||||
|
private final int backgroundColor =
|
||||||
|
getDefaultNightMode() == MODE_NIGHT_YES ? BLACK : WHITE;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private ImagePreviewListener listener;
|
||||||
|
|
||||||
|
public ImagePreview(Context context) {
|
||||||
|
this(context, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImagePreview(Context context, @Nullable AttributeSet attrs) {
|
||||||
|
this(context, attrs, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImagePreview(Context context, @Nullable AttributeSet attrs,
|
||||||
|
int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
|
||||||
|
// inflate layout
|
||||||
|
LayoutInflater inflater = (LayoutInflater) requireNonNull(
|
||||||
|
context.getSystemService(LAYOUT_INFLATER_SERVICE));
|
||||||
|
inflater.inflate(R.layout.image_preview, this, true);
|
||||||
|
|
||||||
|
// find image view and set background color
|
||||||
|
imageView = findViewById(R.id.imageView);
|
||||||
|
imageView.setBackgroundColor(backgroundColor);
|
||||||
|
|
||||||
|
// set cancel listener
|
||||||
|
findViewById(R.id.imageCancelButton).setOnClickListener(view -> {
|
||||||
|
if (listener != null) listener.onCancel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void setImagePreviewListener(ImagePreviewListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
void showPreview(List<Uri> imageUris) {
|
||||||
|
setVisibility(VISIBLE);
|
||||||
|
GlideApp.with(imageView)
|
||||||
|
.asBitmap()
|
||||||
|
.load(imageUris.get(0)) // TODO show more than the first
|
||||||
|
.diskCacheStrategy(NONE)
|
||||||
|
.downsample(FIT_CENTER)
|
||||||
|
.addListener(new RequestListener<Bitmap>() {
|
||||||
|
@Override
|
||||||
|
public boolean onLoadFailed(@Nullable GlideException e,
|
||||||
|
Object model, Target<Bitmap> target,
|
||||||
|
boolean isFirstResource) {
|
||||||
|
if (listener != null) listener.onCancel();
|
||||||
|
Toast.makeText(imageView.getContext(),
|
||||||
|
R.string.image_attach_error, LENGTH_LONG)
|
||||||
|
.show();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onResourceReady(Bitmap resource,
|
||||||
|
Object model, Target<Bitmap> target,
|
||||||
|
DataSource dataSource, boolean isFirstResource) {
|
||||||
|
Palette.from(resource).generate(
|
||||||
|
ImagePreview.this::onPaletteGenerated);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into(imageView);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onPaletteGenerated(@Nullable Palette palette) {
|
||||||
|
if (palette == null) return;
|
||||||
|
int color = getDefaultNightMode() == MODE_NIGHT_YES ?
|
||||||
|
palette.getDarkMutedColor(backgroundColor) :
|
||||||
|
palette.getLightMutedColor(backgroundColor);
|
||||||
|
imageView.setBackgroundColor(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImagePreviewListener {
|
||||||
|
void onCancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
package org.thoughtcrime.securesms.components;
|
/*
|
||||||
|
Taken from Signal, licences under GPLv3
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.briarproject.briar.android.view;
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@@ -24,6 +28,7 @@ import javax.annotation.Nullable;
|
|||||||
import static android.content.Context.WINDOW_SERVICE;
|
import static android.content.Context.WINDOW_SERVICE;
|
||||||
import static android.view.Surface.ROTATION_270;
|
import static android.view.Surface.ROTATION_270;
|
||||||
import static android.view.Surface.ROTATION_90;
|
import static android.view.Surface.ROTATION_90;
|
||||||
|
import static java.util.Objects.requireNonNull;
|
||||||
import static java.util.logging.Level.INFO;
|
import static java.util.logging.Level.INFO;
|
||||||
import static java.util.logging.Level.WARNING;
|
import static java.util.logging.Level.WARNING;
|
||||||
|
|
||||||
@@ -38,8 +43,6 @@ public class KeyboardAwareLinearLayout extends LinearLayout {
|
|||||||
Logger.getLogger(KeyboardAwareLinearLayout.class.getName());
|
Logger.getLogger(KeyboardAwareLinearLayout.class.getName());
|
||||||
|
|
||||||
private final Rect rect = new Rect();
|
private final Rect rect = new Rect();
|
||||||
private final Set<OnKeyboardHiddenListener> hiddenListeners =
|
|
||||||
new HashSet<>();
|
|
||||||
private final Set<OnKeyboardShownListener> shownListeners = new HashSet<>();
|
private final Set<OnKeyboardShownListener> shownListeners = new HashSet<>();
|
||||||
private final int minKeyboardSize;
|
private final int minKeyboardSize;
|
||||||
private final int minCustomKeyboardSize;
|
private final int minCustomKeyboardSize;
|
||||||
@@ -153,7 +156,6 @@ public class KeyboardAwareLinearLayout extends LinearLayout {
|
|||||||
protected void onKeyboardClose() {
|
protected void onKeyboardClose() {
|
||||||
LOG.info("onKeyboardClose()");
|
LOG.info("onKeyboardClose()");
|
||||||
keyboardOpen = false;
|
keyboardOpen = false;
|
||||||
notifyHiddenListeners();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isKeyboardOpen() {
|
public boolean isKeyboardOpen() {
|
||||||
@@ -173,7 +175,7 @@ public class KeyboardAwareLinearLayout extends LinearLayout {
|
|||||||
private int getDeviceRotation() {
|
private int getDeviceRotation() {
|
||||||
WindowManager windowManager =
|
WindowManager windowManager =
|
||||||
(WindowManager) getContext().getSystemService(WINDOW_SERVICE);
|
(WindowManager) getContext().getSystemService(WINDOW_SERVICE);
|
||||||
return windowManager.getDefaultDisplay().getRotation();
|
return requireNonNull(windowManager).getDefaultDisplay().getRotation();
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getKeyboardLandscapeHeight() {
|
private int getKeyboardLandscapeHeight() {
|
||||||
@@ -199,43 +201,6 @@ public class KeyboardAwareLinearLayout extends LinearLayout {
|
|||||||
prefs.edit().putInt("keyboard_height_portrait", height).apply();
|
prefs.edit().putInt("keyboard_height_portrait", height).apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void postOnKeyboardClose(Runnable runnable) {
|
|
||||||
if (keyboardOpen) {
|
|
||||||
addOnKeyboardHiddenListener(new OnKeyboardHiddenListener() {
|
|
||||||
@Override
|
|
||||||
public void onKeyboardHidden() {
|
|
||||||
removeOnKeyboardHiddenListener(this);
|
|
||||||
runnable.run();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
runnable.run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void postOnKeyboardOpen(Runnable runnable) {
|
|
||||||
if (!keyboardOpen) {
|
|
||||||
addOnKeyboardShownListener(new OnKeyboardShownListener() {
|
|
||||||
@Override
|
|
||||||
public void onKeyboardShown() {
|
|
||||||
removeOnKeyboardShownListener(this);
|
|
||||||
runnable.run();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
runnable.run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addOnKeyboardHiddenListener(OnKeyboardHiddenListener listener) {
|
|
||||||
hiddenListeners.add(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeOnKeyboardHiddenListener(
|
|
||||||
OnKeyboardHiddenListener listener) {
|
|
||||||
hiddenListeners.remove(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addOnKeyboardShownListener(OnKeyboardShownListener listener) {
|
public void addOnKeyboardShownListener(OnKeyboardShownListener listener) {
|
||||||
shownListeners.add(listener);
|
shownListeners.add(listener);
|
||||||
}
|
}
|
||||||
@@ -245,15 +210,6 @@ public class KeyboardAwareLinearLayout extends LinearLayout {
|
|||||||
shownListeners.remove(listener);
|
shownListeners.remove(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notifyHiddenListeners() {
|
|
||||||
// Make a copy as listeners may remove themselves when called
|
|
||||||
Set<OnKeyboardHiddenListener> listeners =
|
|
||||||
new HashSet<>(hiddenListeners);
|
|
||||||
for (OnKeyboardHiddenListener listener : listeners) {
|
|
||||||
listener.onKeyboardHidden();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void notifyShownListeners() {
|
private void notifyShownListeners() {
|
||||||
// Make a copy as listeners may remove themselves when called
|
// Make a copy as listeners may remove themselves when called
|
||||||
Set<OnKeyboardShownListener> listeners = new HashSet<>(shownListeners);
|
Set<OnKeyboardShownListener> listeners = new HashSet<>(shownListeners);
|
||||||
@@ -262,11 +218,8 @@ public class KeyboardAwareLinearLayout extends LinearLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface OnKeyboardHiddenListener {
|
|
||||||
void onKeyboardHidden();
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface OnKeyboardShownListener {
|
public interface OnKeyboardShownListener {
|
||||||
void onKeyboardShown();
|
void onKeyboardShown();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,6 @@ import android.content.Context;
|
|||||||
import android.content.res.TypedArray;
|
import android.content.res.TypedArray;
|
||||||
import android.support.annotation.UiThread;
|
import android.support.annotation.UiThread;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
|
|
||||||
@@ -14,6 +13,7 @@ import org.briarproject.briar.R;
|
|||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import static android.view.Gravity.BOTTOM;
|
||||||
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
|
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
|
||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
@@ -32,18 +32,6 @@ public class LargeTextInputView extends TextInputView {
|
|||||||
public LargeTextInputView(Context context, @Nullable AttributeSet attrs,
|
public LargeTextInputView(Context context, @Nullable AttributeSet attrs,
|
||||||
int defStyleAttr) {
|
int defStyleAttr) {
|
||||||
super(context, attrs, defStyleAttr);
|
super(context, attrs, defStyleAttr);
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void inflateLayout(Context context) {
|
|
||||||
LayoutInflater inflater = (LayoutInflater) context
|
|
||||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
|
||||||
inflater.inflate(R.layout.text_input_view_large, this, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void setUpViews(Context context, @Nullable AttributeSet attrs) {
|
|
||||||
super.setUpViews(context, attrs);
|
|
||||||
|
|
||||||
// get attributes
|
// get attributes
|
||||||
TypedArray attributes = context.obtainStyledAttributes(attrs,
|
TypedArray attributes = context.obtainStyledAttributes(attrs,
|
||||||
@@ -57,21 +45,27 @@ public class LargeTextInputView extends TextInputView {
|
|||||||
attributes.recycle();
|
attributes.recycle();
|
||||||
|
|
||||||
if (buttonText != null) setButtonText(buttonText);
|
if (buttonText != null) setButtonText(buttonText);
|
||||||
if (maxLines > 0) editText.setMaxLines(maxLines);
|
if (maxLines > 0) textInput.setMaxLines(maxLines);
|
||||||
if (fillHeight) {
|
if (fillHeight) {
|
||||||
ViewGroup layout = findViewById(R.id.input_layout);
|
ViewGroup layout = findViewById(R.id.input_layout);
|
||||||
LayoutParams params = (LayoutParams) layout.getLayoutParams();
|
LayoutParams params = (LayoutParams) layout.getLayoutParams();
|
||||||
params.height = 0;
|
params.height = 0;
|
||||||
params.weight = 1;
|
params.weight = 1;
|
||||||
layout.setLayoutParams(params);
|
layout.setLayoutParams(params);
|
||||||
ViewGroup.LayoutParams editParams = editText.getLayoutParams();
|
ViewGroup.LayoutParams inputParams = textInput.getLayoutParams();
|
||||||
editParams.height = MATCH_PARENT;
|
inputParams.height = MATCH_PARENT;
|
||||||
editText.setLayoutParams(editParams);
|
textInput.setLayoutParams(inputParams);
|
||||||
}
|
}
|
||||||
|
textInput.setGravity(BOTTOM);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getLayout() {
|
||||||
|
return R.layout.text_input_view_large;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setButtonText(String text) {
|
public void setButtonText(String text) {
|
||||||
((Button) sendButton).setText(text);
|
((Button) findViewById(R.id.btn_send)).setText(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
package org.briarproject.briar.android.view;
|
||||||
|
|
||||||
|
import android.content.ClipData;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.annotation.UiThread;
|
||||||
|
import android.support.v4.view.AbsSavedState;
|
||||||
|
import android.support.v7.widget.AppCompatImageButton;
|
||||||
|
|
||||||
|
import org.briarproject.briar.R;
|
||||||
|
import org.briarproject.briar.android.view.ImagePreview.ImagePreviewListener;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static android.content.Intent.ACTION_GET_CONTENT;
|
||||||
|
import static android.content.Intent.ACTION_OPEN_DOCUMENT;
|
||||||
|
import static android.content.Intent.CATEGORY_OPENABLE;
|
||||||
|
import static android.content.Intent.EXTRA_ALLOW_MULTIPLE;
|
||||||
|
import static android.os.Build.VERSION.SDK_INT;
|
||||||
|
import static android.support.v4.view.AbsSavedState.EMPTY_STATE;
|
||||||
|
import static android.view.View.GONE;
|
||||||
|
import static android.view.View.INVISIBLE;
|
||||||
|
import static android.view.View.VISIBLE;
|
||||||
|
import static java.util.Collections.emptyList;
|
||||||
|
import static java.util.Collections.singletonList;
|
||||||
|
import static java.util.Objects.requireNonNull;
|
||||||
|
|
||||||
|
@UiThread
|
||||||
|
public class TextAttachmentController extends TextSendController
|
||||||
|
implements ImagePreviewListener {
|
||||||
|
|
||||||
|
private final AppCompatImageButton imageButton;
|
||||||
|
private final ImagePreview imagePreview;
|
||||||
|
|
||||||
|
private final AttachImageListener imageListener;
|
||||||
|
|
||||||
|
private CharSequence textHint;
|
||||||
|
private List<Uri> imageUris = emptyList();
|
||||||
|
|
||||||
|
public TextAttachmentController(TextInputView v, ImagePreview imagePreview,
|
||||||
|
SendListener listener, AttachImageListener imageListener) {
|
||||||
|
super(v, listener, false);
|
||||||
|
this.imageListener = imageListener;
|
||||||
|
this.imagePreview = imagePreview;
|
||||||
|
this.imagePreview.setImagePreviewListener(this);
|
||||||
|
|
||||||
|
imageButton = v.findViewById(R.id.imageButton);
|
||||||
|
imageButton.setOnClickListener(view -> onImageButtonClicked());
|
||||||
|
|
||||||
|
textHint = textInput.getHint();
|
||||||
|
|
||||||
|
// show image button
|
||||||
|
showImageButton(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextIsEmptyChanged(boolean isEmpty) {
|
||||||
|
if (imageUris.isEmpty()) showImageButton(isEmpty);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSendEvent() {
|
||||||
|
if (canSend()) {
|
||||||
|
listener.onSendClick(textInput.getText(), imageUris);
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean canSendEmptyText() {
|
||||||
|
return !imageUris.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onImageButtonClicked() {
|
||||||
|
Intent intent = new Intent(SDK_INT >= 19 ?
|
||||||
|
ACTION_OPEN_DOCUMENT : ACTION_GET_CONTENT);
|
||||||
|
intent.addCategory(CATEGORY_OPENABLE);
|
||||||
|
intent.setType("image/*");
|
||||||
|
if (SDK_INT >= 18) // TODO set true to allow attaching multiple images
|
||||||
|
intent.putExtra(EXTRA_ALLOW_MULTIPLE, false);
|
||||||
|
requireNonNull(imageListener).onAttachImage(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onImageReceived(@Nullable Intent resultData) {
|
||||||
|
if (resultData == null) return;
|
||||||
|
if (resultData.getData() != null) {
|
||||||
|
imageUris = singletonList(resultData.getData());
|
||||||
|
onNewUris();
|
||||||
|
} else if (SDK_INT >= 18 && resultData.getClipData() != null) {
|
||||||
|
ClipData clipData = resultData.getClipData();
|
||||||
|
imageUris = new ArrayList<>(clipData.getItemCount());
|
||||||
|
for (int i = 0; i < clipData.getItemCount(); i++) {
|
||||||
|
imageUris.add(clipData.getItemAt(i).getUri());
|
||||||
|
}
|
||||||
|
onNewUris();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onNewUris() {
|
||||||
|
if (imageUris.isEmpty()) return;
|
||||||
|
showImageButton(false);
|
||||||
|
textInput.setHint(R.string.image_caption_hint);
|
||||||
|
imagePreview.showPreview(imageUris);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showImageButton(boolean showImageButton) {
|
||||||
|
if (showImageButton) {
|
||||||
|
imageButton.setVisibility(VISIBLE);
|
||||||
|
sendButton.setEnabled(false);
|
||||||
|
if (SDK_INT <= 15) {
|
||||||
|
sendButton.setVisibility(INVISIBLE);
|
||||||
|
imageButton.setEnabled(true);
|
||||||
|
} else {
|
||||||
|
sendButton.clearAnimation();
|
||||||
|
sendButton.animate().alpha(0f).withEndAction(() -> {
|
||||||
|
sendButton.setVisibility(INVISIBLE);
|
||||||
|
imageButton.setEnabled(true);
|
||||||
|
}).start();
|
||||||
|
imageButton.clearAnimation();
|
||||||
|
imageButton.animate().alpha(1f).start();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sendButton.setVisibility(VISIBLE);
|
||||||
|
// enable/disable buttons right away to allow fast sending
|
||||||
|
sendButton.setEnabled(enabled);
|
||||||
|
imageButton.setEnabled(false);
|
||||||
|
if (SDK_INT <= 15) {
|
||||||
|
imageButton.setVisibility(INVISIBLE);
|
||||||
|
} else {
|
||||||
|
sendButton.clearAnimation();
|
||||||
|
sendButton.animate().alpha(1f).start();
|
||||||
|
imageButton.clearAnimation();
|
||||||
|
imageButton.animate().alpha(0f).withEndAction(() ->
|
||||||
|
imageButton.setVisibility(INVISIBLE)
|
||||||
|
).start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reset() {
|
||||||
|
// restore hint
|
||||||
|
textInput.setHint(textHint);
|
||||||
|
// hide image layout
|
||||||
|
imagePreview.setVisibility(GONE);
|
||||||
|
// reset image URIs
|
||||||
|
imageUris = emptyList();
|
||||||
|
// show the image button again, so images can get attached
|
||||||
|
showImageButton(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Parcelable onSaveInstanceState(@Nullable Parcelable superState) {
|
||||||
|
SavedState state =
|
||||||
|
new SavedState(superState == null ? EMPTY_STATE : superState);
|
||||||
|
state.imageUris = imageUris;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public Parcelable onRestoreInstanceState(@NonNull Parcelable inState) {
|
||||||
|
SavedState state = (SavedState) inState;
|
||||||
|
imageUris = state.imageUris;
|
||||||
|
onNewUris();
|
||||||
|
return state.getSuperState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCancel() {
|
||||||
|
textInput.clearText();
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class SavedState extends AbsSavedState {
|
||||||
|
private List<Uri> imageUris;
|
||||||
|
|
||||||
|
private SavedState(Parcelable superState) {
|
||||||
|
super(superState);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SavedState(Parcel in) {
|
||||||
|
super(in);
|
||||||
|
//noinspection unchecked
|
||||||
|
imageUris = in.readArrayList(Uri.class.getClassLoader());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(Parcel out, int flags) {
|
||||||
|
super.writeToParcel(out, flags);
|
||||||
|
out.writeList(imageUris);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Parcelable.Creator<SavedState> CREATOR
|
||||||
|
= new Parcelable.Creator<SavedState>() {
|
||||||
|
public SavedState createFromParcel(Parcel in) {
|
||||||
|
return new SavedState(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SavedState[] newArray(int size) {
|
||||||
|
return new SavedState[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface AttachImageListener {
|
||||||
|
void onAttachImage(Intent intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -4,51 +4,31 @@ import android.animation.LayoutTransition;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.TypedArray;
|
import android.content.res.TypedArray;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.os.IBinder;
|
import android.os.Parcelable;
|
||||||
import android.support.annotation.CallSuper;
|
import android.support.annotation.LayoutRes;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.support.annotation.StringRes;
|
import android.support.annotation.StringRes;
|
||||||
import android.support.annotation.UiThread;
|
import android.support.annotation.UiThread;
|
||||||
import android.support.v7.widget.AppCompatImageButton;
|
|
||||||
import android.text.Editable;
|
|
||||||
import android.text.TextWatcher;
|
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.widget.LinearLayout;
|
||||||
import android.view.inputmethod.InputMethodManager;
|
|
||||||
|
|
||||||
import com.vanniktech.emoji.EmojiEditText;
|
|
||||||
import com.vanniktech.emoji.EmojiPopup;
|
|
||||||
import com.vanniktech.emoji.RecentEmoji;
|
|
||||||
|
|
||||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
import org.briarproject.briar.android.BriarApplication;
|
import org.briarproject.briar.android.view.KeyboardAwareLinearLayout.OnKeyboardShownListener;
|
||||||
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
|
|
||||||
import static android.content.Context.INPUT_METHOD_SERVICE;
|
|
||||||
import static android.content.Context.LAYOUT_INFLATER_SERVICE;
|
import static android.content.Context.LAYOUT_INFLATER_SERVICE;
|
||||||
import static android.view.KeyEvent.KEYCODE_ENTER;
|
import static java.util.Objects.requireNonNull;
|
||||||
import static android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT;
|
|
||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
@MethodsNotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
public class TextInputView extends KeyboardAwareLinearLayout {
|
public class TextInputView extends LinearLayout {
|
||||||
|
|
||||||
@Inject
|
|
||||||
RecentEmoji recentEmoji;
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
TextInputListener listener;
|
TextSendController textSendController;
|
||||||
|
final EmojiTextInputView textInput;
|
||||||
AppCompatImageButton emojiToggle;
|
|
||||||
EmojiEditText editText;
|
|
||||||
EmojiPopup emojiPopup;
|
|
||||||
View sendButton;
|
|
||||||
|
|
||||||
public TextInputView(Context context) {
|
public TextInputView(Context context) {
|
||||||
this(context, null);
|
this(context, null);
|
||||||
@@ -61,118 +41,109 @@ public class TextInputView extends KeyboardAwareLinearLayout {
|
|||||||
public TextInputView(Context context, @Nullable AttributeSet attrs,
|
public TextInputView(Context context, @Nullable AttributeSet attrs,
|
||||||
int defStyleAttr) {
|
int defStyleAttr) {
|
||||||
super(context, attrs, defStyleAttr);
|
super(context, attrs, defStyleAttr);
|
||||||
if (!isInEditMode()) {
|
setSaveEnabled(true);
|
||||||
BriarApplication app =
|
|
||||||
(BriarApplication) context.getApplicationContext();
|
|
||||||
app.getApplicationComponent().inject(this);
|
|
||||||
}
|
|
||||||
setOrientation(VERTICAL);
|
setOrientation(VERTICAL);
|
||||||
setLayoutTransition(new LayoutTransition());
|
setLayoutTransition(new LayoutTransition());
|
||||||
inflateLayout(context);
|
|
||||||
setUpViews(context, attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void inflateLayout(Context context) {
|
// inflate layout
|
||||||
LayoutInflater inflater = (LayoutInflater) context
|
LayoutInflater inflater = (LayoutInflater) requireNonNull(
|
||||||
.getSystemService(LAYOUT_INFLATER_SERVICE);
|
context.getSystemService(LAYOUT_INFLATER_SERVICE));
|
||||||
inflater.inflate(R.layout.text_input_view, this, true);
|
inflater.inflate(getLayout(), this, true);
|
||||||
}
|
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
protected void setUpViews(Context context, @Nullable AttributeSet attrs) {
|
|
||||||
emojiToggle = findViewById(R.id.emoji_toggle);
|
|
||||||
editText = findViewById(R.id.input_text);
|
|
||||||
emojiPopup = EmojiPopup.Builder
|
|
||||||
.fromRootView(this)
|
|
||||||
.setRecentEmoji(recentEmoji)
|
|
||||||
.setOnEmojiPopupShownListener(this::showKeyboardIcon)
|
|
||||||
.setOnEmojiPopupDismissListener(this::showEmojiIcon)
|
|
||||||
.build(editText);
|
|
||||||
sendButton = findViewById(R.id.btn_send);
|
|
||||||
|
|
||||||
// get attributes
|
// get attributes
|
||||||
TypedArray attributes = context.obtainStyledAttributes(attrs,
|
TypedArray attributes = context.obtainStyledAttributes(attrs,
|
||||||
R.styleable.TextInputView);
|
R.styleable.TextInputView);
|
||||||
String hint = attributes.getString(R.styleable.TextInputView_hint);
|
String hint = attributes.getString(R.styleable.TextInputView_hint);
|
||||||
|
boolean allowEmptyText = attributes
|
||||||
|
.getBoolean(R.styleable.TextInputView_allowEmptyText, false);
|
||||||
attributes.recycle();
|
attributes.recycle();
|
||||||
|
|
||||||
if (hint != null) editText.setHint(hint);
|
textInput = findViewById(R.id.emojiTextInput);
|
||||||
|
textInput.setAllowEmptyText(allowEmptyText);
|
||||||
emojiToggle.setOnClickListener(v -> emojiPopup.toggle());
|
if (hint != null) textInput.setHint(hint);
|
||||||
editText.setOnClickListener(v -> showSoftKeyboard());
|
|
||||||
editText.setOnKeyListener((v, keyCode, event) -> {
|
|
||||||
if (keyCode == KEYCODE_ENTER && event.isCtrlPressed()) {
|
|
||||||
trySendMessage();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
sendButton.setOnClickListener(v -> trySendMessage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showEmojiIcon() {
|
@LayoutRes
|
||||||
emojiToggle.setImageResource(R.drawable.ic_emoji_toggle);
|
protected int getLayout() {
|
||||||
|
return R.layout.text_input_view;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showKeyboardIcon() {
|
@Nullable
|
||||||
emojiToggle.setImageResource(R.drawable.ic_keyboard);
|
@Override
|
||||||
}
|
protected Parcelable onSaveInstanceState() {
|
||||||
|
Parcelable superState = super.onSaveInstanceState();
|
||||||
private void trySendMessage() {
|
if (textSendController != null) {
|
||||||
if (listener != null) {
|
superState = textSendController.onSaveInstanceState(superState);
|
||||||
listener.onSendClick(editText.getText().toString());
|
|
||||||
}
|
}
|
||||||
|
return superState;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onRestoreInstanceState(Parcelable state) {
|
||||||
|
if (textSendController != null) {
|
||||||
|
Parcelable outState =
|
||||||
|
textSendController.onRestoreInstanceState(state);
|
||||||
|
super.onRestoreInstanceState(outState);
|
||||||
|
} else {
|
||||||
|
super.onRestoreInstanceState(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call this in onCreate() before any other methods of this class.
|
||||||
|
*/
|
||||||
|
public <T extends TextSendController> void setSendController(T controller) {
|
||||||
|
textSendController = controller;
|
||||||
|
textInput.setTextInputListener(textSendController);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setEnabled(boolean enabled) {
|
||||||
|
super.setEnabled(enabled);
|
||||||
|
textInput.setEnabled(enabled);
|
||||||
|
requireNonNull(textSendController).setEnabled(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
|
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
|
||||||
return editText.requestFocus(direction, previouslyFocusedRect);
|
return textInput.requestFocus(direction, previouslyFocusedRect);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
EmojiTextInputView getEmojiTextInputView() {
|
||||||
public void onDetachedFromWindow() {
|
return textInput;
|
||||||
super.onDetachedFromWindow();
|
|
||||||
if (emojiPopup.isShowing()) emojiPopup.dismiss();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setText(String text) {
|
public void clearText() {
|
||||||
editText.setText(text);
|
textInput.clearText();
|
||||||
}
|
|
||||||
|
|
||||||
public Editable getText() {
|
|
||||||
return editText.getText();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setHint(@StringRes int res) {
|
public void setHint(@StringRes int res) {
|
||||||
editText.setHint(res);
|
textInput.setHint(getContext().getString(res));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSendButtonEnabled(boolean enabled) {
|
public void setMaxTextLength(int maxLength) {
|
||||||
sendButton.setEnabled(enabled);
|
textInput.setMaxLength(maxLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addTextChangedListener(TextWatcher watcher) {
|
public boolean isKeyboardOpen() {
|
||||||
editText.addTextChangedListener(watcher);
|
return textInput.isKeyboardOpen();
|
||||||
}
|
|
||||||
|
|
||||||
public void setListener(TextInputListener listener) {
|
|
||||||
this.listener = listener;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showSoftKeyboard() {
|
public void showSoftKeyboard() {
|
||||||
Object o = getContext().getSystemService(INPUT_METHOD_SERVICE);
|
textInput.showSoftKeyboard();
|
||||||
((InputMethodManager) o).showSoftInput(editText, SHOW_IMPLICIT);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void hideSoftKeyboard() {
|
public void hideSoftKeyboard() {
|
||||||
if (emojiPopup.isShowing()) emojiPopup.dismiss();
|
textInput.hideSoftKeyboard();
|
||||||
IBinder token = editText.getWindowToken();
|
|
||||||
Object o = getContext().getSystemService(INPUT_METHOD_SERVICE);
|
|
||||||
((InputMethodManager) o).hideSoftInputFromWindow(token, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface TextInputListener {
|
public void addOnKeyboardShownListener(OnKeyboardShownListener listener) {
|
||||||
void onSendClick(String text);
|
textInput.addOnKeyboardShownListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeOnKeyboardShownListener(
|
||||||
|
OnKeyboardShownListener listener) {
|
||||||
|
textInput.removeOnKeyboardShownListener(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package org.briarproject.briar.android.view;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
import android.support.annotation.CallSuper;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.annotation.UiThread;
|
||||||
|
import android.support.design.widget.Snackbar;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.briar.R;
|
||||||
|
import org.briarproject.briar.android.view.EmojiTextInputView.TextInputListener;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static android.support.design.widget.Snackbar.LENGTH_SHORT;
|
||||||
|
import static java.util.Collections.emptyList;
|
||||||
|
|
||||||
|
@UiThread
|
||||||
|
@NotNullByDefault
|
||||||
|
public class TextSendController implements TextInputListener {
|
||||||
|
|
||||||
|
protected final EmojiTextInputView textInput;
|
||||||
|
protected final View sendButton;
|
||||||
|
protected final SendListener listener;
|
||||||
|
protected boolean enabled = true;
|
||||||
|
protected final boolean allowEmptyText;
|
||||||
|
|
||||||
|
private boolean wasEmpty = true;
|
||||||
|
|
||||||
|
public TextSendController(TextInputView v, SendListener listener,
|
||||||
|
boolean allowEmptyText) {
|
||||||
|
this.sendButton = v.findViewById(R.id.btn_send);
|
||||||
|
this.sendButton.setOnClickListener(view -> onSendEvent());
|
||||||
|
this.sendButton.setEnabled(allowEmptyText);
|
||||||
|
this.listener = listener;
|
||||||
|
this.textInput = v.getEmojiTextInputView();
|
||||||
|
this.allowEmptyText = allowEmptyText;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextIsEmptyChanged(boolean isEmpty) {
|
||||||
|
sendButton.setEnabled(enabled && (!isEmpty || canSendEmptyText()));
|
||||||
|
wasEmpty = isEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSendEvent() {
|
||||||
|
if (canSend()) {
|
||||||
|
listener.onSendClick(textInput.getText(), emptyList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final boolean canSend() {
|
||||||
|
if (textInput.isTooLong()) {
|
||||||
|
Snackbar.make(sendButton, R.string.text_too_long, LENGTH_SHORT)
|
||||||
|
.show();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return enabled && (canSendEmptyText() || !textInput.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean canSendEmptyText() {
|
||||||
|
return allowEmptyText;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public Parcelable onSaveInstanceState(@Nullable Parcelable superState) {
|
||||||
|
return superState;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public Parcelable onRestoreInstanceState(Parcelable state) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
public void setEnabled(boolean enabled) {
|
||||||
|
sendButton.setEnabled(enabled && (!wasEmpty || canSendEmptyText()));
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface SendListener {
|
||||||
|
void onSendClick(@Nullable String text, List<Uri> imageUris);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,621 +0,0 @@
|
|||||||
GNU GENERAL PUBLIC LICENSE
|
|
||||||
Version 3, 29 June 2007
|
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
|
||||||
of this license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Preamble
|
|
||||||
|
|
||||||
The GNU General Public License is a free, copyleft license for
|
|
||||||
software and other kinds of works.
|
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
|
||||||
to take away your freedom to share and change the works. By contrast,
|
|
||||||
the GNU General Public License is intended to guarantee your freedom to
|
|
||||||
share and change all versions of a program--to make sure it remains free
|
|
||||||
software for all its users. We, the Free Software Foundation, use the
|
|
||||||
GNU General Public License for most of our software; it applies also to
|
|
||||||
any other work released this way by its authors. You can apply it to
|
|
||||||
your programs, too.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
|
||||||
have the freedom to distribute copies of free software (and charge for
|
|
||||||
them if you wish), that you receive source code or can get it if you
|
|
||||||
want it, that you can change the software or use pieces of it in new
|
|
||||||
free programs, and that you know you can do these things.
|
|
||||||
|
|
||||||
To protect your rights, we need to prevent others from denying you
|
|
||||||
these rights or asking you to surrender the rights. Therefore, you have
|
|
||||||
certain responsibilities if you distribute copies of the software, or if
|
|
||||||
you modify it: responsibilities to respect the freedom of others.
|
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
|
||||||
gratis or for a fee, you must pass on to the recipients the same
|
|
||||||
freedoms that you received. You must make sure that they, too, receive
|
|
||||||
or can get the source code. And you must show them these terms so they
|
|
||||||
know their rights.
|
|
||||||
|
|
||||||
Developers that use the GNU GPL protect your rights with two steps:
|
|
||||||
(1) assert copyright on the software, and (2) offer you this License
|
|
||||||
giving you legal permission to copy, distribute and/or modify it.
|
|
||||||
|
|
||||||
For the developers' and authors' protection, the GPL clearly explains
|
|
||||||
that there is no warranty for this free software. For both users' and
|
|
||||||
authors' sake, the GPL requires that modified versions be marked as
|
|
||||||
changed, so that their problems will not be attributed erroneously to
|
|
||||||
authors of previous versions.
|
|
||||||
|
|
||||||
Some devices are designed to deny users access to install or run
|
|
||||||
modified versions of the software inside them, although the manufacturer
|
|
||||||
can do so. This is fundamentally incompatible with the aim of
|
|
||||||
protecting users' freedom to change the software. The systematic
|
|
||||||
pattern of such abuse occurs in the area of products for individuals to
|
|
||||||
use, which is precisely where it is most unacceptable. Therefore, we
|
|
||||||
have designed this version of the GPL to prohibit the practice for those
|
|
||||||
products. If such problems arise substantially in other domains, we
|
|
||||||
stand ready to extend this provision to those domains in future versions
|
|
||||||
of the GPL, as needed to protect the freedom of users.
|
|
||||||
|
|
||||||
Finally, every program is threatened constantly by software patents.
|
|
||||||
States should not allow patents to restrict development and use of
|
|
||||||
software on general-purpose computers, but in those that do, we wish to
|
|
||||||
avoid the special danger that patents applied to a free program could
|
|
||||||
make it effectively proprietary. To prevent this, the GPL assures that
|
|
||||||
patents cannot be used to render the program non-free.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow.
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
0. Definitions.
|
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU General Public License.
|
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
|
||||||
works, such as semiconductor masks.
|
|
||||||
|
|
||||||
"The Program" refers to any copyrightable work licensed under this
|
|
||||||
License. Each licensee is addressed as "you". "Licensees" and
|
|
||||||
"recipients" may be individuals or organizations.
|
|
||||||
|
|
||||||
To "modify" a work means to copy from or adapt all or part of the work
|
|
||||||
in a fashion requiring copyright permission, other than the making of an
|
|
||||||
exact copy. The resulting work is called a "modified version" of the
|
|
||||||
earlier work or a work "based on" the earlier work.
|
|
||||||
|
|
||||||
A "covered work" means either the unmodified Program or a work based
|
|
||||||
on the Program.
|
|
||||||
|
|
||||||
To "propagate" a work means to do anything with it that, without
|
|
||||||
permission, would make you directly or secondarily liable for
|
|
||||||
infringement under applicable copyright law, except executing it on a
|
|
||||||
computer or modifying a private copy. Propagation includes copying,
|
|
||||||
distribution (with or without modification), making available to the
|
|
||||||
public, and in some countries other activities as well.
|
|
||||||
|
|
||||||
To "convey" a work means any kind of propagation that enables other
|
|
||||||
parties to make or receive copies. Mere interaction with a user through
|
|
||||||
a computer network, with no transfer of a copy, is not conveying.
|
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices"
|
|
||||||
to the extent that it includes a convenient and prominently visible
|
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
|
||||||
tells the user that there is no warranty for the work (except to the
|
|
||||||
extent that warranties are provided), that licensees may convey the
|
|
||||||
work under this License, and how to view a copy of this License. If
|
|
||||||
the interface presents a list of user commands or options, such as a
|
|
||||||
menu, a prominent item in the list meets this criterion.
|
|
||||||
|
|
||||||
1. Source Code.
|
|
||||||
|
|
||||||
The "source code" for a work means the preferred form of the work
|
|
||||||
for making modifications to it. "Object code" means any non-source
|
|
||||||
form of a work.
|
|
||||||
|
|
||||||
A "Standard Interface" means an interface that either is an official
|
|
||||||
standard defined by a recognized standards body, or, in the case of
|
|
||||||
interfaces specified for a particular programming language, one that
|
|
||||||
is widely used among developers working in that language.
|
|
||||||
|
|
||||||
The "System Libraries" of an executable work include anything, other
|
|
||||||
than the work as a whole, that (a) is included in the normal form of
|
|
||||||
packaging a Major Component, but which is not part of that Major
|
|
||||||
Component, and (b) serves only to enable use of the work with that
|
|
||||||
Major Component, or to implement a Standard Interface for which an
|
|
||||||
implementation is available to the public in source code form. A
|
|
||||||
"Major Component", in this context, means a major essential component
|
|
||||||
(kernel, window system, and so on) of the specific operating system
|
|
||||||
(if any) on which the executable work runs, or a compiler used to
|
|
||||||
produce the work, or an object code interpreter used to run it.
|
|
||||||
|
|
||||||
The "Corresponding Source" for a work in object code form means all
|
|
||||||
the source code needed to generate, install, and (for an executable
|
|
||||||
work) run the object code and to modify the work, including scripts to
|
|
||||||
control those activities. However, it does not include the work's
|
|
||||||
System Libraries, or general-purpose tools or generally available free
|
|
||||||
programs which are used unmodified in performing those activities but
|
|
||||||
which are not part of the work. For example, Corresponding Source
|
|
||||||
includes interface definition files associated with source files for
|
|
||||||
the work, and the source code for shared libraries and dynamically
|
|
||||||
linked subprograms that the work is specifically designed to require,
|
|
||||||
such as by intimate data communication or control flow between those
|
|
||||||
subprograms and other parts of the work.
|
|
||||||
|
|
||||||
The Corresponding Source need not include anything that users
|
|
||||||
can regenerate automatically from other parts of the Corresponding
|
|
||||||
Source.
|
|
||||||
|
|
||||||
The Corresponding Source for a work in source code form is that
|
|
||||||
same work.
|
|
||||||
|
|
||||||
2. Basic Permissions.
|
|
||||||
|
|
||||||
All rights granted under this License are granted for the term of
|
|
||||||
copyright on the Program, and are irrevocable provided the stated
|
|
||||||
conditions are met. This License explicitly affirms your unlimited
|
|
||||||
permission to run the unmodified Program. The output from running a
|
|
||||||
covered work is covered by this License only if the output, given its
|
|
||||||
content, constitutes a covered work. This License acknowledges your
|
|
||||||
rights of fair use or other equivalent, as provided by copyright law.
|
|
||||||
|
|
||||||
You may make, run and propagate covered works that you do not
|
|
||||||
convey, without conditions so long as your license otherwise remains
|
|
||||||
in force. You may convey covered works to others for the sole purpose
|
|
||||||
of having them make modifications exclusively for you, or provide you
|
|
||||||
with facilities for running those works, provided that you comply with
|
|
||||||
the terms of this License in conveying all material for which you do
|
|
||||||
not control copyright. Those thus making or running the covered works
|
|
||||||
for you must do so exclusively on your behalf, under your direction
|
|
||||||
and control, on terms that prohibit them from making any copies of
|
|
||||||
your copyrighted material outside their relationship with you.
|
|
||||||
|
|
||||||
Conveying under any other circumstances is permitted solely under
|
|
||||||
the conditions stated below. Sublicensing is not allowed; section 10
|
|
||||||
makes it unnecessary.
|
|
||||||
|
|
||||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
|
||||||
|
|
||||||
No covered work shall be deemed part of an effective technological
|
|
||||||
measure under any applicable law fulfilling obligations under article
|
|
||||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
|
||||||
similar laws prohibiting or restricting circumvention of such
|
|
||||||
measures.
|
|
||||||
|
|
||||||
When you convey a covered work, you waive any legal power to forbid
|
|
||||||
circumvention of technological measures to the extent such circumvention
|
|
||||||
is effected by exercising rights under this License with respect to
|
|
||||||
the covered work, and you disclaim any intention to limit operation or
|
|
||||||
modification of the work as a means of enforcing, against the work's
|
|
||||||
users, your or third parties' legal rights to forbid circumvention of
|
|
||||||
technological measures.
|
|
||||||
|
|
||||||
4. Conveying Verbatim Copies.
|
|
||||||
|
|
||||||
You may convey verbatim copies of the Program's source code as you
|
|
||||||
receive it, in any medium, provided that you conspicuously and
|
|
||||||
appropriately publish on each copy an appropriate copyright notice;
|
|
||||||
keep intact all notices stating that this License and any
|
|
||||||
non-permissive terms added in accord with section 7 apply to the code;
|
|
||||||
keep intact all notices of the absence of any warranty; and give all
|
|
||||||
recipients a copy of this License along with the Program.
|
|
||||||
|
|
||||||
You may charge any price or no price for each copy that you convey,
|
|
||||||
and you may offer support or warranty protection for a fee.
|
|
||||||
|
|
||||||
5. Conveying Modified Source Versions.
|
|
||||||
|
|
||||||
You may convey a work based on the Program, or the modifications to
|
|
||||||
produce it from the Program, in the form of source code under the
|
|
||||||
terms of section 4, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
a) The work must carry prominent notices stating that you modified
|
|
||||||
it, and giving a relevant date.
|
|
||||||
|
|
||||||
b) The work must carry prominent notices stating that it is
|
|
||||||
released under this License and any conditions added under section
|
|
||||||
7. This requirement modifies the requirement in section 4 to
|
|
||||||
"keep intact all notices".
|
|
||||||
|
|
||||||
c) You must license the entire work, as a whole, under this
|
|
||||||
License to anyone who comes into possession of a copy. This
|
|
||||||
License will therefore apply, along with any applicable section 7
|
|
||||||
additional terms, to the whole of the work, and all its parts,
|
|
||||||
regardless of how they are packaged. This License gives no
|
|
||||||
permission to license the work in any other way, but it does not
|
|
||||||
invalidate such permission if you have separately received it.
|
|
||||||
|
|
||||||
d) If the work has interactive user interfaces, each must display
|
|
||||||
Appropriate Legal Notices; however, if the Program has interactive
|
|
||||||
interfaces that do not display Appropriate Legal Notices, your
|
|
||||||
work need not make them do so.
|
|
||||||
|
|
||||||
A compilation of a covered work with other separate and independent
|
|
||||||
works, which are not by their nature extensions of the covered work,
|
|
||||||
and which are not combined with it such as to form a larger program,
|
|
||||||
in or on a volume of a storage or distribution medium, is called an
|
|
||||||
"aggregate" if the compilation and its resulting copyright are not
|
|
||||||
used to limit the access or legal rights of the compilation's users
|
|
||||||
beyond what the individual works permit. Inclusion of a covered work
|
|
||||||
in an aggregate does not cause this License to apply to the other
|
|
||||||
parts of the aggregate.
|
|
||||||
|
|
||||||
6. Conveying Non-Source Forms.
|
|
||||||
|
|
||||||
You may convey a covered work in object code form under the terms
|
|
||||||
of sections 4 and 5, provided that you also convey the
|
|
||||||
machine-readable Corresponding Source under the terms of this License,
|
|
||||||
in one of these ways:
|
|
||||||
|
|
||||||
a) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by the
|
|
||||||
Corresponding Source fixed on a durable physical medium
|
|
||||||
customarily used for software interchange.
|
|
||||||
|
|
||||||
b) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by a
|
|
||||||
written offer, valid for at least three years and valid for as
|
|
||||||
long as you offer spare parts or customer support for that product
|
|
||||||
model, to give anyone who possesses the object code either (1) a
|
|
||||||
copy of the Corresponding Source for all the software in the
|
|
||||||
product that is covered by this License, on a durable physical
|
|
||||||
medium customarily used for software interchange, for a price no
|
|
||||||
more than your reasonable cost of physically performing this
|
|
||||||
conveying of source, or (2) access to copy the
|
|
||||||
Corresponding Source from a network server at no charge.
|
|
||||||
|
|
||||||
c) Convey individual copies of the object code with a copy of the
|
|
||||||
written offer to provide the Corresponding Source. This
|
|
||||||
alternative is allowed only occasionally and noncommercially, and
|
|
||||||
only if you received the object code with such an offer, in accord
|
|
||||||
with subsection 6b.
|
|
||||||
|
|
||||||
d) Convey the object code by offering access from a designated
|
|
||||||
place (gratis or for a charge), and offer equivalent access to the
|
|
||||||
Corresponding Source in the same way through the same place at no
|
|
||||||
further charge. You need not require recipients to copy the
|
|
||||||
Corresponding Source along with the object code. If the place to
|
|
||||||
copy the object code is a network server, the Corresponding Source
|
|
||||||
may be on a different server (operated by you or a third party)
|
|
||||||
that supports equivalent copying facilities, provided you maintain
|
|
||||||
clear directions next to the object code saying where to find the
|
|
||||||
Corresponding Source. Regardless of what server hosts the
|
|
||||||
Corresponding Source, you remain obligated to ensure that it is
|
|
||||||
available for as long as needed to satisfy these requirements.
|
|
||||||
|
|
||||||
e) Convey the object code using peer-to-peer transmission, provided
|
|
||||||
you inform other peers where the object code and Corresponding
|
|
||||||
Source of the work are being offered to the general public at no
|
|
||||||
charge under subsection 6d.
|
|
||||||
|
|
||||||
A separable portion of the object code, whose source code is excluded
|
|
||||||
from the Corresponding Source as a System Library, need not be
|
|
||||||
included in conveying the object code work.
|
|
||||||
|
|
||||||
A "User Product" is either (1) a "consumer product", which means any
|
|
||||||
tangible personal property which is normally used for personal, family,
|
|
||||||
or household purposes, or (2) anything designed or sold for incorporation
|
|
||||||
into a dwelling. In determining whether a product is a consumer product,
|
|
||||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
|
||||||
product received by a particular user, "normally used" refers to a
|
|
||||||
typical or common use of that class of product, regardless of the status
|
|
||||||
of the particular user or of the way in which the particular user
|
|
||||||
actually uses, or expects or is expected to use, the product. A product
|
|
||||||
is a consumer product regardless of whether the product has substantial
|
|
||||||
commercial, industrial or non-consumer uses, unless such uses represent
|
|
||||||
the only significant mode of use of the product.
|
|
||||||
|
|
||||||
"Installation Information" for a User Product means any methods,
|
|
||||||
procedures, authorization keys, or other information required to install
|
|
||||||
and execute modified versions of a covered work in that User Product from
|
|
||||||
a modified version of its Corresponding Source. The information must
|
|
||||||
suffice to ensure that the continued functioning of the modified object
|
|
||||||
code is in no case prevented or interfered with solely because
|
|
||||||
modification has been made.
|
|
||||||
|
|
||||||
If you convey an object code work under this section in, or with, or
|
|
||||||
specifically for use in, a User Product, and the conveying occurs as
|
|
||||||
part of a transaction in which the right of possession and use of the
|
|
||||||
User Product is transferred to the recipient in perpetuity or for a
|
|
||||||
fixed term (regardless of how the transaction is characterized), the
|
|
||||||
Corresponding Source conveyed under this section must be accompanied
|
|
||||||
by the Installation Information. But this requirement does not apply
|
|
||||||
if neither you nor any third party retains the ability to install
|
|
||||||
modified object code on the User Product (for example, the work has
|
|
||||||
been installed in ROM).
|
|
||||||
|
|
||||||
The requirement to provide Installation Information does not include a
|
|
||||||
requirement to continue to provide support service, warranty, or updates
|
|
||||||
for a work that has been modified or installed by the recipient, or for
|
|
||||||
the User Product in which it has been modified or installed. Access to a
|
|
||||||
network may be denied when the modification itself materially and
|
|
||||||
adversely affects the operation of the network or violates the rules and
|
|
||||||
protocols for communication across the network.
|
|
||||||
|
|
||||||
Corresponding Source conveyed, and Installation Information provided,
|
|
||||||
in accord with this section must be in a format that is publicly
|
|
||||||
documented (and with an implementation available to the public in
|
|
||||||
source code form), and must require no special password or key for
|
|
||||||
unpacking, reading or copying.
|
|
||||||
|
|
||||||
7. Additional Terms.
|
|
||||||
|
|
||||||
"Additional permissions" are terms that supplement the terms of this
|
|
||||||
License by making exceptions from one or more of its conditions.
|
|
||||||
Additional permissions that are applicable to the entire Program shall
|
|
||||||
be treated as though they were included in this License, to the extent
|
|
||||||
that they are valid under applicable law. If additional permissions
|
|
||||||
apply only to part of the Program, that part may be used separately
|
|
||||||
under those permissions, but the entire Program remains governed by
|
|
||||||
this License without regard to the additional permissions.
|
|
||||||
|
|
||||||
When you convey a copy of a covered work, you may at your option
|
|
||||||
remove any additional permissions from that copy, or from any part of
|
|
||||||
it. (Additional permissions may be written to require their own
|
|
||||||
removal in certain cases when you modify the work.) You may place
|
|
||||||
additional permissions on material, added by you to a covered work,
|
|
||||||
for which you have or can give appropriate copyright permission.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, for material you
|
|
||||||
add to a covered work, you may (if authorized by the copyright holders of
|
|
||||||
that material) supplement the terms of this License with terms:
|
|
||||||
|
|
||||||
a) Disclaiming warranty or limiting liability differently from the
|
|
||||||
terms of sections 15 and 16 of this License; or
|
|
||||||
|
|
||||||
b) Requiring preservation of specified reasonable legal notices or
|
|
||||||
author attributions in that material or in the Appropriate Legal
|
|
||||||
Notices displayed by works containing it; or
|
|
||||||
|
|
||||||
c) Prohibiting misrepresentation of the origin of that material, or
|
|
||||||
requiring that modified versions of such material be marked in
|
|
||||||
reasonable ways as different from the original version; or
|
|
||||||
|
|
||||||
d) Limiting the use for publicity purposes of names of licensors or
|
|
||||||
authors of the material; or
|
|
||||||
|
|
||||||
e) Declining to grant rights under trademark law for use of some
|
|
||||||
trade names, trademarks, or service marks; or
|
|
||||||
|
|
||||||
f) Requiring indemnification of licensors and authors of that
|
|
||||||
material by anyone who conveys the material (or modified versions of
|
|
||||||
it) with contractual assumptions of liability to the recipient, for
|
|
||||||
any liability that these contractual assumptions directly impose on
|
|
||||||
those licensors and authors.
|
|
||||||
|
|
||||||
All other non-permissive additional terms are considered "further
|
|
||||||
restrictions" within the meaning of section 10. If the Program as you
|
|
||||||
received it, or any part of it, contains a notice stating that it is
|
|
||||||
governed by this License along with a term that is a further
|
|
||||||
restriction, you may remove that term. If a license document contains
|
|
||||||
a further restriction but permits relicensing or conveying under this
|
|
||||||
License, you may add to a covered work material governed by the terms
|
|
||||||
of that license document, provided that the further restriction does
|
|
||||||
not survive such relicensing or conveying.
|
|
||||||
|
|
||||||
If you add terms to a covered work in accord with this section, you
|
|
||||||
must place, in the relevant source files, a statement of the
|
|
||||||
additional terms that apply to those files, or a notice indicating
|
|
||||||
where to find the applicable terms.
|
|
||||||
|
|
||||||
Additional terms, permissive or non-permissive, may be stated in the
|
|
||||||
form of a separately written license, or stated as exceptions;
|
|
||||||
the above requirements apply either way.
|
|
||||||
|
|
||||||
8. Termination.
|
|
||||||
|
|
||||||
You may not propagate or modify a covered work except as expressly
|
|
||||||
provided under this License. Any attempt otherwise to propagate or
|
|
||||||
modify it is void, and will automatically terminate your rights under
|
|
||||||
this License (including any patent licenses granted under the third
|
|
||||||
paragraph of section 11).
|
|
||||||
|
|
||||||
However, if you cease all violation of this License, then your
|
|
||||||
license from a particular copyright holder is reinstated (a)
|
|
||||||
provisionally, unless and until the copyright holder explicitly and
|
|
||||||
finally terminates your license, and (b) permanently, if the copyright
|
|
||||||
holder fails to notify you of the violation by some reasonable means
|
|
||||||
prior to 60 days after the cessation.
|
|
||||||
|
|
||||||
Moreover, your license from a particular copyright holder is
|
|
||||||
reinstated permanently if the copyright holder notifies you of the
|
|
||||||
violation by some reasonable means, this is the first time you have
|
|
||||||
received notice of violation of this License (for any work) from that
|
|
||||||
copyright holder, and you cure the violation prior to 30 days after
|
|
||||||
your receipt of the notice.
|
|
||||||
|
|
||||||
Termination of your rights under this section does not terminate the
|
|
||||||
licenses of parties who have received copies or rights from you under
|
|
||||||
this License. If your rights have been terminated and not permanently
|
|
||||||
reinstated, you do not qualify to receive new licenses for the same
|
|
||||||
material under section 10.
|
|
||||||
|
|
||||||
9. Acceptance Not Required for Having Copies.
|
|
||||||
|
|
||||||
You are not required to accept this License in order to receive or
|
|
||||||
run a copy of the Program. Ancillary propagation of a covered work
|
|
||||||
occurring solely as a consequence of using peer-to-peer transmission
|
|
||||||
to receive a copy likewise does not require acceptance. However,
|
|
||||||
nothing other than this License grants you permission to propagate or
|
|
||||||
modify any covered work. These actions infringe copyright if you do
|
|
||||||
not accept this License. Therefore, by modifying or propagating a
|
|
||||||
covered work, you indicate your acceptance of this License to do so.
|
|
||||||
|
|
||||||
10. Automatic Licensing of Downstream Recipients.
|
|
||||||
|
|
||||||
Each time you convey a covered work, the recipient automatically
|
|
||||||
receives a license from the original licensors, to run, modify and
|
|
||||||
propagate that work, subject to this License. You are not responsible
|
|
||||||
for enforcing compliance by third parties with this License.
|
|
||||||
|
|
||||||
An "entity transaction" is a transaction transferring control of an
|
|
||||||
organization, or substantially all assets of one, or subdividing an
|
|
||||||
organization, or merging organizations. If propagation of a covered
|
|
||||||
work results from an entity transaction, each party to that
|
|
||||||
transaction who receives a copy of the work also receives whatever
|
|
||||||
licenses to the work the party's predecessor in interest had or could
|
|
||||||
give under the previous paragraph, plus a right to possession of the
|
|
||||||
Corresponding Source of the work from the predecessor in interest, if
|
|
||||||
the predecessor has it or can get it with reasonable efforts.
|
|
||||||
|
|
||||||
You may not impose any further restrictions on the exercise of the
|
|
||||||
rights granted or affirmed under this License. For example, you may
|
|
||||||
not impose a license fee, royalty, or other charge for exercise of
|
|
||||||
rights granted under this License, and you may not initiate litigation
|
|
||||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
|
||||||
any patent claim is infringed by making, using, selling, offering for
|
|
||||||
sale, or importing the Program or any portion of it.
|
|
||||||
|
|
||||||
11. Patents.
|
|
||||||
|
|
||||||
A "contributor" is a copyright holder who authorizes use under this
|
|
||||||
License of the Program or a work on which the Program is based. The
|
|
||||||
work thus licensed is called the contributor's "contributor version".
|
|
||||||
|
|
||||||
A contributor's "essential patent claims" are all patent claims
|
|
||||||
owned or controlled by the contributor, whether already acquired or
|
|
||||||
hereafter acquired, that would be infringed by some manner, permitted
|
|
||||||
by this License, of making, using, or selling its contributor version,
|
|
||||||
but do not include claims that would be infringed only as a
|
|
||||||
consequence of further modification of the contributor version. For
|
|
||||||
purposes of this definition, "control" includes the right to grant
|
|
||||||
patent sublicenses in a manner consistent with the requirements of
|
|
||||||
this License.
|
|
||||||
|
|
||||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
|
||||||
patent license under the contributor's essential patent claims, to
|
|
||||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
|
||||||
propagate the contents of its contributor version.
|
|
||||||
|
|
||||||
In the following three paragraphs, a "patent license" is any express
|
|
||||||
agreement or commitment, however denominated, not to enforce a patent
|
|
||||||
(such as an express permission to practice a patent or covenant not to
|
|
||||||
sue for patent infringement). To "grant" such a patent license to a
|
|
||||||
party means to make such an agreement or commitment not to enforce a
|
|
||||||
patent against the party.
|
|
||||||
|
|
||||||
If you convey a covered work, knowingly relying on a patent license,
|
|
||||||
and the Corresponding Source of the work is not available for anyone
|
|
||||||
to copy, free of charge and under the terms of this License, through a
|
|
||||||
publicly available network server or other readily accessible means,
|
|
||||||
then you must either (1) cause the Corresponding Source to be so
|
|
||||||
available, or (2) arrange to deprive yourself of the benefit of the
|
|
||||||
patent license for this particular work, or (3) arrange, in a manner
|
|
||||||
consistent with the requirements of this License, to extend the patent
|
|
||||||
license to downstream recipients. "Knowingly relying" means you have
|
|
||||||
actual knowledge that, but for the patent license, your conveying the
|
|
||||||
covered work in a country, or your recipient's use of the covered work
|
|
||||||
in a country, would infringe one or more identifiable patents in that
|
|
||||||
country that you have reason to believe are valid.
|
|
||||||
|
|
||||||
If, pursuant to or in connection with a single transaction or
|
|
||||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
|
||||||
covered work, and grant a patent license to some of the parties
|
|
||||||
receiving the covered work authorizing them to use, propagate, modify
|
|
||||||
or convey a specific copy of the covered work, then the patent license
|
|
||||||
you grant is automatically extended to all recipients of the covered
|
|
||||||
work and works based on it.
|
|
||||||
|
|
||||||
A patent license is "discriminatory" if it does not include within
|
|
||||||
the scope of its coverage, prohibits the exercise of, or is
|
|
||||||
conditioned on the non-exercise of one or more of the rights that are
|
|
||||||
specifically granted under this License. You may not convey a covered
|
|
||||||
work if you are a party to an arrangement with a third party that is
|
|
||||||
in the business of distributing software, under which you make payment
|
|
||||||
to the third party based on the extent of your activity of conveying
|
|
||||||
the work, and under which the third party grants, to any of the
|
|
||||||
parties who would receive the covered work from you, a discriminatory
|
|
||||||
patent license (a) in connection with copies of the covered work
|
|
||||||
conveyed by you (or copies made from those copies), or (b) primarily
|
|
||||||
for and in connection with specific products or compilations that
|
|
||||||
contain the covered work, unless you entered into that arrangement,
|
|
||||||
or that patent license was granted, prior to 28 March 2007.
|
|
||||||
|
|
||||||
Nothing in this License shall be construed as excluding or limiting
|
|
||||||
any implied license or other defenses to infringement that may
|
|
||||||
otherwise be available to you under applicable patent law.
|
|
||||||
|
|
||||||
12. No Surrender of Others' Freedom.
|
|
||||||
|
|
||||||
If conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot convey a
|
|
||||||
covered work so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you may
|
|
||||||
not convey it at all. For example, if you agree to terms that obligate you
|
|
||||||
to collect a royalty for further conveying from those to whom you convey
|
|
||||||
the Program, the only way you could satisfy both those terms and this
|
|
||||||
License would be to refrain entirely from conveying the Program.
|
|
||||||
|
|
||||||
13. Use with the GNU Affero General Public License.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
|
||||||
permission to link or combine any covered work with a work licensed
|
|
||||||
under version 3 of the GNU Affero General Public License into a single
|
|
||||||
combined work, and to convey the resulting work. The terms of this
|
|
||||||
License will continue to apply to the part which is the covered work,
|
|
||||||
but the special requirements of the GNU Affero General Public License,
|
|
||||||
section 13, concerning interaction through a network will apply to the
|
|
||||||
combination as such.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
|
||||||
the GNU General Public License from time to time. Such new versions will
|
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
|
||||||
address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
|
||||||
Program specifies that a certain numbered version of the GNU General
|
|
||||||
Public License "or any later version" applies to it, you have the
|
|
||||||
option of following the terms and conditions either of that numbered
|
|
||||||
version or of any later version published by the Free Software
|
|
||||||
Foundation. If the Program does not specify a version number of the
|
|
||||||
GNU General Public License, you may choose any version ever published
|
|
||||||
by the Free Software Foundation.
|
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
|
||||||
versions of the GNU General Public License can be used, that proxy's
|
|
||||||
public statement of acceptance of a version permanently authorizes you
|
|
||||||
to choose that version for the Program.
|
|
||||||
|
|
||||||
Later license versions may give you additional or different
|
|
||||||
permissions. However, no additional obligations are imposed on any
|
|
||||||
author or copyright holder as a result of your choosing to follow a
|
|
||||||
later version.
|
|
||||||
|
|
||||||
15. Disclaimer of Warranty.
|
|
||||||
|
|
||||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
|
||||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
|
||||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
|
||||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
||||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
|
||||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
|
||||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
16. Limitation of Liability.
|
|
||||||
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
|
||||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
|
||||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
|
||||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
|
||||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
|
||||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
|
||||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
|
||||||
SUCH DAMAGES.
|
|
||||||
|
|
||||||
17. Interpretation of Sections 15 and 16.
|
|
||||||
|
|
||||||
If the disclaimer of warranty and limitation of liability provided
|
|
||||||
above cannot be given local legal effect according to their terms,
|
|
||||||
reviewing courts shall apply local law that most closely approximates
|
|
||||||
an absolute waiver of all civil liability in connection with the
|
|
||||||
Program, unless a warranty or assumption of liability accompanies a
|
|
||||||
copy of the Program in return for a fee.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
10
briar-android/src/main/res/drawable/ic_image.xml
Normal file
10
briar-android/src/main/res/drawable/ic_image.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<vector
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportHeight="24.0"
|
||||||
|
android:viewportWidth="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
|
||||||
|
</vector>
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:animateLayoutChanges="true"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
tools:context=".android.conversation.ConversationActivity">
|
tools:context=".android.conversation.ConversationActivity">
|
||||||
|
|
||||||
@@ -32,9 +33,9 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_marginLeft="@dimen/margin_medium"
|
android:layout_marginLeft="@dimen/margin_medium"
|
||||||
android:layout_marginStart="@dimen/margin_medium"
|
android:layout_marginStart="@dimen/margin_medium"
|
||||||
|
android:ellipsize="end"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:ellipsize="end"
|
|
||||||
android:textColor="@color/action_bar_text"
|
android:textColor="@color/action_bar_text"
|
||||||
tools:text="Contact Name of someone who chose a long name"/>
|
tools:text="Contact Name of someone who chose a long name"/>
|
||||||
|
|
||||||
@@ -48,7 +49,15 @@
|
|||||||
android:id="@+id/conversationView"
|
android:id="@+id/conversationView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1"/>
|
android:layout_weight="2"/>
|
||||||
|
|
||||||
|
<org.briarproject.briar.android.view.ImagePreview
|
||||||
|
android:id="@+id/imagePreview"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible"/>
|
||||||
|
|
||||||
<org.briarproject.briar.android.view.TextInputView
|
<org.briarproject.briar.android.view.TextInputView
|
||||||
android:id="@+id/text_input_container"
|
android:id="@+id/text_input_container"
|
||||||
|
|||||||
34
briar-android/src/main/res/layout/emoji_text_input_view.xml
Normal file
34
briar-android/src/main/res/layout/emoji_text_input_view.xml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<merge
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
tools:parentTag="org.briarproject.briar.android.view.KeyboardAwareLinearLayout"
|
||||||
|
tools:showIn="@layout/fragment_reblog">
|
||||||
|
|
||||||
|
<android.support.v7.widget.AppCompatImageButton
|
||||||
|
android:id="@+id/emoji_toggle"
|
||||||
|
android:layout_width="@dimen/text_input_height"
|
||||||
|
android:layout_height="@dimen/text_input_height"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:padding="@dimen/margin_small"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:src="@drawable/ic_emoji_toggle"
|
||||||
|
app:tint="?attr/colorControlNormal"/>
|
||||||
|
|
||||||
|
<com.vanniktech.emoji.EmojiEditText
|
||||||
|
android:id="@+id/input_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:inputType="textMultiLine|textCapSentences|textAutoCorrect"
|
||||||
|
android:minHeight="@dimen/text_input_height"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
tools:text="Line 1\nLine 2\nLine 3"/>
|
||||||
|
|
||||||
|
</merge>
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
app:allowEmptyText="true"
|
||||||
app:buttonText="@string/forum_share_button"
|
app:buttonText="@string/forum_share_button"
|
||||||
app:fillHeight="true"
|
app:fillHeight="true"
|
||||||
app:hint="@string/forum_share_message"/>
|
app:hint="@string/forum_share_message"/>
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="bottom"
|
android:gravity="bottom"
|
||||||
|
app:allowEmptyText="true"
|
||||||
app:buttonText="@string/blogs_reblog_button"
|
app:buttonText="@string/blogs_reblog_button"
|
||||||
app:hint="@string/blogs_reblog_comment_hint"
|
app:hint="@string/blogs_reblog_comment_hint"
|
||||||
app:maxLines="5"/>
|
app:maxLines="5"/>
|
||||||
|
|||||||
49
briar-android/src/main/res/layout/image_preview.xml
Normal file
49
briar-android/src/main/res/layout/image_preview.xml
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<merge
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:animateLayoutChanges="true"
|
||||||
|
tools:parentTag="android.support.constraint.ConstraintLayout"
|
||||||
|
tools:showIn="@layout/activity_conversation">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/divider"
|
||||||
|
style="@style/Divider.Horizontal"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/imageView"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"/>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/imageView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/divider"
|
||||||
|
tools:ignore="ContentDescription"
|
||||||
|
tools:srcCompat="@tools:sample/avatars"/>
|
||||||
|
|
||||||
|
<android.support.design.widget.FloatingActionButton
|
||||||
|
android:id="@+id/imageCancelButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:src="@drawable/ic_close"
|
||||||
|
app:backgroundTint="@color/briar_red"
|
||||||
|
app:fabCustomSize="26dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_bias="0"
|
||||||
|
app:maxImageSize="18dp"/>
|
||||||
|
|
||||||
|
</merge>
|
||||||
@@ -118,6 +118,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="@dimen/margin_large"
|
android:layout_marginTop="@dimen/margin_large"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
|
app:allowEmptyText="true"
|
||||||
app:buttonText="@string/introduction_button"
|
app:buttonText="@string/introduction_button"
|
||||||
app:hint="@string/introduction_message_hint"
|
app:hint="@string/introduction_message_hint"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:animateLayoutChanges="true"
|
||||||
tools:showIn="@layout/activity_conversation">
|
tools:showIn="@layout/activity_conversation">
|
||||||
|
|
||||||
<View
|
<View
|
||||||
@@ -16,46 +17,48 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="@color/card_background">
|
android:background="@color/card_background">
|
||||||
|
|
||||||
<android.support.v7.widget.AppCompatImageButton
|
<org.briarproject.briar.android.view.EmojiTextInputView
|
||||||
android:id="@+id/emoji_toggle"
|
android:id="@+id/emojiTextInput"
|
||||||
android:layout_width="@dimen/text_input_height"
|
|
||||||
android:layout_height="@dimen/text_input_height"
|
|
||||||
android:layout_gravity="bottom"
|
|
||||||
android:background="?attr/selectableItemBackground"
|
|
||||||
android:padding="@dimen/margin_small"
|
|
||||||
android:scaleType="center"
|
|
||||||
android:src="@drawable/ic_emoji_toggle"
|
|
||||||
app:tint="?attr/colorControlNormal"/>
|
|
||||||
|
|
||||||
<com.vanniktech.emoji.EmojiEditText
|
|
||||||
android:id="@+id/input_text"
|
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:background="@android:color/transparent"
|
app:maxTextLines="4"/>
|
||||||
android:inputType="textMultiLine|textCapSentences"
|
|
||||||
android:maxLines="4"
|
|
||||||
android:minHeight="@dimen/text_input_height"
|
|
||||||
android:paddingLeft="2dp"
|
|
||||||
android:paddingStart="2dp"
|
|
||||||
android:textColor="?android:attr/textColorPrimary"
|
|
||||||
tools:ignore="RtlSymmetry"
|
|
||||||
tools:text="Line 1\nLine 2\nLine 3"/>
|
|
||||||
|
|
||||||
<android.support.v7.widget.AppCompatImageButton
|
<FrameLayout
|
||||||
android:id="@+id/btn_send"
|
|
||||||
android:layout_width="@dimen/text_input_height"
|
android:layout_width="@dimen/text_input_height"
|
||||||
android:layout_height="@dimen/text_input_height"
|
android:layout_height="@dimen/text_input_height"
|
||||||
android:layout_gravity="bottom"
|
android:layout_gravity="bottom">
|
||||||
android:background="?attr/selectableItemBackground"
|
|
||||||
android:clickable="true"
|
<android.support.v7.widget.AppCompatImageButton
|
||||||
android:contentDescription="@string/send"
|
android:id="@+id/imageButton"
|
||||||
android:enabled="false"
|
android:layout_width="@dimen/text_input_height"
|
||||||
android:focusable="true"
|
android:layout_height="@dimen/text_input_height"
|
||||||
android:padding="4dp"
|
android:background="?attr/selectableItemBackground"
|
||||||
android:scaleType="center"
|
android:clickable="true"
|
||||||
android:src="@drawable/social_send_now_white"
|
android:contentDescription="@string/image_attach"
|
||||||
app:tint="@color/briar_accent"/>
|
android:enabled="false"
|
||||||
|
android:focusable="true"
|
||||||
|
android:padding="4dp"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:src="@drawable/ic_image"
|
||||||
|
android:visibility="invisible"
|
||||||
|
app:tint="?attr/colorControlNormal"/>
|
||||||
|
|
||||||
|
<android.support.v7.widget.AppCompatImageButton
|
||||||
|
android:id="@+id/btn_send"
|
||||||
|
android:layout_width="@dimen/text_input_height"
|
||||||
|
android:layout_height="@dimen/text_input_height"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:clickable="true"
|
||||||
|
android:contentDescription="@string/send"
|
||||||
|
android:enabled="false"
|
||||||
|
android:focusable="true"
|
||||||
|
android:padding="4dp"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:src="@drawable/social_send_now_white"
|
||||||
|
app:tint="@color/briar_accent"/>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -22,39 +22,12 @@
|
|||||||
app:cardCornerRadius="0dp"
|
app:cardCornerRadius="0dp"
|
||||||
app:cardUseCompatPadding="false">
|
app:cardUseCompatPadding="false">
|
||||||
|
|
||||||
<LinearLayout
|
<org.briarproject.briar.android.view.EmojiTextInputView
|
||||||
|
android:id="@+id/emojiTextInput"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="horizontal">
|
app:textPaddingBottom="10dp"
|
||||||
|
app:textPaddingEnd="@dimen/margin_medium"/>
|
||||||
<android.support.v7.widget.AppCompatImageButton
|
|
||||||
android:id="@+id/emoji_toggle"
|
|
||||||
android:layout_width="@dimen/text_input_height"
|
|
||||||
android:layout_height="@dimen/text_input_height"
|
|
||||||
android:layout_gravity="bottom"
|
|
||||||
android:background="?attr/selectableItemBackground"
|
|
||||||
android:padding="@dimen/margin_small"
|
|
||||||
android:scaleType="center"
|
|
||||||
android:src="@drawable/ic_emoji_toggle"
|
|
||||||
app:tint="?attr/colorControlNormal"/>
|
|
||||||
|
|
||||||
<com.vanniktech.emoji.EmojiEditText
|
|
||||||
android:id="@+id/input_text"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="@android:color/transparent"
|
|
||||||
android:gravity="bottom"
|
|
||||||
android:inputType="textMultiLine|textLongMessage|textCapSentences|textAutoCorrect"
|
|
||||||
android:minHeight="@dimen/text_input_height"
|
|
||||||
android:paddingBottom="10dp"
|
|
||||||
android:paddingEnd="@dimen/margin_small"
|
|
||||||
android:paddingRight="@dimen/margin_small"
|
|
||||||
android:paddingTop="@dimen/margin_small"
|
|
||||||
android:textColor="?android:attr/textColorPrimary"
|
|
||||||
tools:ignore="RtlSymmetry"
|
|
||||||
tools:text="Line 1\nLine 2\nLine 3"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</android.support.v7.widget.CardView>
|
</android.support.v7.widget.CardView>
|
||||||
|
|
||||||
@@ -64,6 +37,7 @@
|
|||||||
android:layout_marginEnd="@dimen/margin_small"
|
android:layout_marginEnd="@dimen/margin_small"
|
||||||
android:layout_marginLeft="@dimen/margin_small"
|
android:layout_marginLeft="@dimen/margin_small"
|
||||||
android:layout_marginRight="@dimen/margin_small"
|
android:layout_marginRight="@dimen/margin_small"
|
||||||
android:layout_marginStart="@dimen/margin_small"/>
|
android:layout_marginStart="@dimen/margin_small"
|
||||||
|
tools:text="@string/send"/>
|
||||||
|
|
||||||
</merge>
|
</merge>
|
||||||
|
|||||||
@@ -19,8 +19,15 @@
|
|||||||
</attr>
|
</attr>
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
|
|
||||||
|
<declare-styleable name="EmojiTextInputView">
|
||||||
|
<attr name="textPaddingBottom" format="dimension"/>
|
||||||
|
<attr name="textPaddingEnd" format="dimension"/>
|
||||||
|
<attr name="maxTextLines" format="integer"/>
|
||||||
|
</declare-styleable>
|
||||||
|
|
||||||
<declare-styleable name="TextInputView">
|
<declare-styleable name="TextInputView">
|
||||||
<attr name="hint" format="string"/>
|
<attr name="hint" format="string"/>
|
||||||
|
<attr name="allowEmptyText" format="boolean"/>
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
|
|
||||||
<declare-styleable name="LargeTextInputView">
|
<declare-styleable name="LargeTextInputView">
|
||||||
|
|||||||
@@ -127,6 +127,9 @@
|
|||||||
<string name="date_no_private_messages">No messages.</string>
|
<string name="date_no_private_messages">No messages.</string>
|
||||||
<string name="no_private_messages">No messages to show</string>
|
<string name="no_private_messages">No messages to show</string>
|
||||||
<string name="message_hint">Type message</string>
|
<string name="message_hint">Type message</string>
|
||||||
|
<string name="image_caption_hint">Add a caption (optional)</string>
|
||||||
|
<string name="image_attach">Attach image</string>
|
||||||
|
<string name="image_attach_error">Could not attach image</string>
|
||||||
<string name="set_contact_alias">Change contact name</string>
|
<string name="set_contact_alias">Change contact name</string>
|
||||||
<string name="set_contact_alias_hint">Contact name</string>
|
<string name="set_contact_alias_hint">Contact name</string>
|
||||||
<string name="set_alias_button">Change</string>
|
<string name="set_alias_button">Change</string>
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ dependencyVerification {
|
|||||||
'com.android.support:interpolator:28.0.0:interpolator-28.0.0.aar:7bc7ee86a0db39a4b51956f3e89842d2bd962118d57d779eb6ed6b34ba0677ea',
|
'com.android.support:interpolator:28.0.0:interpolator-28.0.0.aar:7bc7ee86a0db39a4b51956f3e89842d2bd962118d57d779eb6ed6b34ba0677ea',
|
||||||
'com.android.support:loader:28.0.0:loader-28.0.0.aar:920b85efd72dc33e915b0f88a883fe73b88483c6df8751a741e17611f2460341',
|
'com.android.support:loader:28.0.0:loader-28.0.0.aar:920b85efd72dc33e915b0f88a883fe73b88483c6df8751a741e17611f2460341',
|
||||||
'com.android.support:localbroadcastmanager:28.0.0:localbroadcastmanager-28.0.0.aar:d287c823af5fdde72c099fcfc5f630efe9687af7a914343ae6fd92de32c8a806',
|
'com.android.support:localbroadcastmanager:28.0.0:localbroadcastmanager-28.0.0.aar:d287c823af5fdde72c099fcfc5f630efe9687af7a914343ae6fd92de32c8a806',
|
||||||
|
'com.android.support:palette-v7:28.0.0:palette-v7-28.0.0.aar:317202dddb953d152d0677dbd8bb3b9d8ef8dcd0bdee0da4f40c98826e4960e6',
|
||||||
'com.android.support:preference-v14:28.0.0:preference-v14-28.0.0.aar:8133c6e19233fa51e036a341e6d3f4adeead3375cebf777efced0fe154c3267e',
|
'com.android.support:preference-v14:28.0.0:preference-v14-28.0.0.aar:8133c6e19233fa51e036a341e6d3f4adeead3375cebf777efced0fe154c3267e',
|
||||||
'com.android.support:preference-v7:28.0.0:preference-v7-28.0.0.aar:75eabe936d1fc3b178450a554c4d433466036f2be6d6dccdf971eac9590fdbf5',
|
'com.android.support:preference-v7:28.0.0:preference-v7-28.0.0.aar:75eabe936d1fc3b178450a554c4d433466036f2be6d6dccdf971eac9590fdbf5',
|
||||||
'com.android.support:print:28.0.0:print-28.0.0.aar:4be8a812d73e4a80e35b91ceae127def3f0bb9726bf3bc439aa0cc81503f5728',
|
'com.android.support:print:28.0.0:print-28.0.0.aar:4be8a812d73e4a80e35b91ceae127def3f0bb9726bf3bc439aa0cc81503f5728',
|
||||||
|
|||||||
Reference in New Issue
Block a user