diff --git a/briar-android/build.gradle b/briar-android/build.gradle index c52aced57..c44c35efe 100644 --- a/briar-android/build.gradle +++ b/briar-android/build.gradle @@ -116,7 +116,7 @@ dependencies { implementation 'info.guardianproject.trustedintents:trustedintents:0.2' implementation 'de.hdodenhof:circleimageview:2.2.0' implementation 'com.google.zxing:core:3.3.3' - implementation 'uk.co.samuelwall:material-tap-target-prompt:2.12.4' + implementation 'uk.co.samuelwall:material-tap-target-prompt:2.14.0' implementation 'com.vanniktech:emoji-google:0.5.1' def glideVersion = '4.8.0' implementation("com.github.bumptech.glide:glide:$glideVersion") { @@ -134,7 +134,7 @@ dependencies { testImplementation project(path: ':bramble-core', configuration: 'testOutput') testImplementation 'org.robolectric:robolectric:4.0.1' testImplementation 'org.robolectric:shadows-support-v4:3.3.2' - testImplementation 'org.mockito:mockito-core:2.13.0' + testImplementation 'org.mockito:mockito-core:2.19.0' testImplementation 'junit:junit:4.12' testImplementation "org.jmock:jmock:2.8.2" testImplementation "org.jmock:jmock-junit4:2.8.2" diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java index 2c0c5c72f..a25460cd4 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java @@ -46,7 +46,6 @@ import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.plugin.ConnectionRegistry; import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent; import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent; -import org.briarproject.bramble.api.settings.Settings; import org.briarproject.bramble.api.settings.SettingsManager; import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.MessageId; @@ -126,7 +125,6 @@ import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHME import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENT_POSITION; import static org.briarproject.briar.android.conversation.ImageActivity.DATE; import static org.briarproject.briar.android.conversation.ImageActivity.NAME; -import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE; import static org.briarproject.briar.android.util.UiUtils.getAvatarTransitionName; import static org.briarproject.briar.android.util.UiUtils.getBulbTransitionName; import static org.briarproject.briar.android.util.UiUtils.observeOnce; @@ -144,8 +142,9 @@ public class ConversationActivity extends BriarActivity private static final Logger LOG = Logger.getLogger(ConversationActivity.class.getName()); - private static final String SHOW_ONBOARDING_INTRODUCTION = - "showOnboardingIntroduction"; + + private static final int TRANSITION_DURATION_MS = 500; + private static final int ONBOARDING_DELAY_MS = 250; @Inject AndroidNotificationManager notificationManager; @@ -154,21 +153,8 @@ public class ConversationActivity extends BriarActivity @Inject @CryptoExecutor Executor cryptoExecutor; - - private final Map textCache = new ConcurrentHashMap<>(); - private AttachmentController attachmentController; - - private ConversationViewModel viewModel; - private ConversationVisitor visitor; - private ConversationAdapter adapter; - private Toolbar toolbar; - private CircleImageView toolbarAvatar; - private ImageView toolbarStatus; - private TextView toolbarTitle; - private BriarRecyclerView list; - private LinearLayoutManager layoutManager; - private TextInputView textInputView; - private TextSendController sendController; + @Inject + ViewModelProvider.Factory viewModelFactory; // Fields that are accessed from background threads must be volatile @Inject @@ -191,24 +177,37 @@ public class ConversationActivity extends BriarActivity volatile BlogSharingManager blogSharingManager; @Inject volatile GroupInvitationManager groupInvitationManager; - @Inject - ViewModelProvider.Factory viewModelFactory; - - private volatile ContactId contactId; - @Nullable - private Parcelable layoutManagerState; + private final Map textCache = new ConcurrentHashMap<>(); private final Observer contactNameObserver = name -> { requireNonNull(name); loadMessages(); }; + private AttachmentController attachmentController; + private ConversationViewModel viewModel; + private ConversationVisitor visitor; + private ConversationAdapter adapter; + private Toolbar toolbar; + private CircleImageView toolbarAvatar; + private ImageView toolbarStatus; + private TextView toolbarTitle; + private BriarRecyclerView list; + private LinearLayoutManager layoutManager; + private TextInputView textInputView; + private TextSendController sendController; + @Nullable + private Parcelable layoutManagerState; + + private volatile ContactId contactId; + @Override public void onCreate(@Nullable Bundle state) { if (SDK_INT >= 21) { // Spurious lint warning - using END causes a crash @SuppressLint("RtlHardcoded") Transition slide = new Slide(RIGHT); + slide.setDuration(TRANSITION_DURATION_MS); setSceneTransitionAnimation(slide, null, slide); } super.onCreate(state); @@ -243,8 +242,8 @@ public class ConversationActivity extends BriarActivity requireNonNull(deleted); if (deleted) finish(); }); - viewModel.getAddedPrivateMessage() - .observe(this, this::onAddedPrivateMessage); + viewModel.getAddedPrivateMessage().observe(this, + this::onAddedPrivateMessage); setTransitionName(toolbarAvatar, getAvatarTransitionName(contactId)); setTransitionName(toolbarStatus, getBulbTransitionName(contactId)); @@ -263,6 +262,13 @@ public class ConversationActivity extends BriarActivity ImagePreview imagePreview = findViewById(R.id.imagePreview); sendController = new TextAttachmentController(textInputView, imagePreview, this, this); + observeOnce(viewModel.hasImageSupport(), this, hasSupport -> { + if (hasSupport != null && hasSupport) { + // remove cast when removing FEATURE_FLAG_IMAGE_ATTACHMENTS + ((TextAttachmentController) sendController) + .setImagesSupported(); + } + }); } else { sendController = new TextSendController(textInputView, this, false); } @@ -283,7 +289,8 @@ public class ConversationActivity extends BriarActivity } @Override - protected void onActivityResult(int request, int result, Intent data) { + protected void onActivityResult(int request, int result, + @Nullable Intent data) { super.onActivityResult(request, result, data); if (request == REQUEST_INTRODUCTION && result == RESULT_OK) { @@ -348,10 +355,18 @@ public class ConversationActivity extends BriarActivity MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.conversation_actions, menu); - enableIntroductionActionIfAvailable( - menu.findItem(R.id.action_introduction)); - enableAliasActionIfAvailable( - menu.findItem(R.id.action_set_alias)); + // enable introduction action if available + observeOnce(viewModel.showIntroductionAction(), this, enable -> { + if (enable != null && enable) { + menu.findItem(R.id.action_introduction).setEnabled(true); + // show introduction onboarding, if needed + observeOnce(viewModel.showIntroductionOnboarding(), this, + this::showIntroductionOnboarding); + } + }); + // enable alias action if available + observeOnce(viewModel.getContact(), this, contact -> + menu.findItem(R.id.action_set_alias).setEnabled(true)); return super.onCreateOptionsMenu(menu); } @@ -461,6 +476,10 @@ public class ConversationActivity extends BriarActivity if (revision == adapter.getRevision()) { adapter.incrementRevision(); textInputView.setEnabled(true); + // start observing onboarding after enabling (only once, because + // we only update this when an onboarding should be shown) + observeOnce(viewModel.showImageOnboarding(), this, + this::showImageOnboarding); List items = createItems(headers); adapter.addAll(items); list.showData(); @@ -702,74 +721,70 @@ public class ConversationActivity extends BriarActivity }); } - private void enableIntroductionActionIfAvailable(MenuItem item) { - runOnDbThread(() -> { - try { - if (contactManager.getActiveContacts().size() > 1) { - enableIntroductionAction(item); - Settings settings = - settingsManager.getSettings(SETTINGS_NAMESPACE); - if (settings.getBoolean(SHOW_ONBOARDING_INTRODUCTION, - true)) { - showIntroductionOnboarding(); - } - } - } catch (DbException e) { - logException(LOG, WARNING, e); - } - }); + private void showImageOnboarding(@Nullable Boolean show) { + if (show == null || !show) return; + if (SDK_INT >= 21) { + // show onboarding only after the enter transition has ended + // otherwise the tap target animation won't play + textInputView.postDelayed(this::showImageOnboarding, + TRANSITION_DURATION_MS + ONBOARDING_DELAY_MS); + } else { + showImageOnboarding(); + } } - private void enableAliasActionIfAvailable(MenuItem item) { - observeOnce(viewModel.getContact(), this, c -> item.setEnabled(true)); + private void showImageOnboarding() { + // remove cast when removing FEATURE_FLAG_IMAGE_ATTACHMENTS + ((TextAttachmentController) sendController) + .showImageOnboarding(this, () -> + viewModel.onImageOnboardingSeen()); } - private void enableIntroductionAction(MenuItem item) { - runOnUiThreadUnlessDestroyed(() -> item.setEnabled(true)); + private void showIntroductionOnboarding(@Nullable Boolean show) { + if (show == null || !show) return; + if (SDK_INT >= 21) { + // show onboarding only after the enter transition has ended + // otherwise the tap target animation won't play + textInputView.postDelayed(this::showIntroductionOnboarding, + TRANSITION_DURATION_MS + ONBOARDING_DELAY_MS); + } else { + showIntroductionOnboarding(); + } } private void showIntroductionOnboarding() { - runOnUiThreadUnlessDestroyed(() -> { - // find view of overflow icon - View target = null; - for (int i = 0; i < toolbar.getChildCount(); i++) { - if (toolbar.getChildAt(i) instanceof ActionMenuView) { - ActionMenuView menu = - (ActionMenuView) toolbar.getChildAt(i); - target = menu.getChildAt(menu.getChildCount() - 1); - break; - } - } - if (target == null) { - LOG.warning("No Overflow Icon found!"); - return; + // find view of overflow icon + View target = null; + for (int i = 0; i < toolbar.getChildCount(); i++) { + if (toolbar.getChildAt(i) instanceof ActionMenuView) { + ActionMenuView menu = (ActionMenuView) toolbar.getChildAt(i); + // The overflow icon should be the last child of the menu + target = menu.getChildAt(menu.getChildCount() - 1); + // If the menu hasn't been populated yet, use the menu itself + // as the target + if (target == null) target = menu; + break; } + } + if (target == null) { + LOG.warning("No Overflow Icon found!"); + return; + } - PromptStateChangeListener listener = (prompt, state) -> { - if (state == STATE_DISMISSED || state == STATE_FINISHED) { - introductionOnboardingSeen(); - } - }; - new MaterialTapTargetPrompt.Builder(ConversationActivity.this, - R.style.OnboardingDialogTheme).setTarget(target) - .setPrimaryText(R.string.introduction_onboarding_title) - .setSecondaryText(R.string.introduction_onboarding_text) - .setIcon(R.drawable.ic_more_vert_accent) - .setPromptStateChangeListener(listener) - .show(); - }); - } - - private void introductionOnboardingSeen() { - runOnDbThread(() -> { - try { - Settings settings = new Settings(); - settings.putBoolean(SHOW_ONBOARDING_INTRODUCTION, false); - settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE); - } catch (DbException e) { - logException(LOG, WARNING, e); + PromptStateChangeListener listener = (prompt, state) -> { + if (state == STATE_DISMISSED || state == STATE_FINISHED) { + viewModel.onIntroductionOnboardingSeen(); } - }); + }; + new MaterialTapTargetPrompt.Builder(ConversationActivity.this, + R.style.OnboardingDialogTheme).setTarget(target) + .setPrimaryText(R.string.introduction_onboarding_title) + .setSecondaryText(R.string.introduction_onboarding_text) + .setIcon(R.drawable.ic_more_vert_accent) + .setBackgroundColour( + ContextCompat.getColor(this, R.color.briar_primary)) + .setPromptStateChangeListener(listener) + .show(); } @Override diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java index 732c9fb7e..9c998e9eb 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java @@ -16,11 +16,14 @@ import org.briarproject.bramble.api.contact.Contact; import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.crypto.CryptoExecutor; +import org.briarproject.bramble.api.db.DatabaseComponent; import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.NoSuchContactException; import org.briarproject.bramble.api.identity.AuthorId; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.settings.Settings; +import org.briarproject.bramble.api.settings.SettingsManager; import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.Message; import org.briarproject.briar.android.util.UiUtils; @@ -34,6 +37,7 @@ import org.briarproject.briar.api.messaging.PrivateMessageHeader; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.concurrent.Executor; import java.util.logging.Logger; @@ -47,6 +51,7 @@ import static org.briarproject.bramble.util.IoUtils.tryToClose; import static org.briarproject.bramble.util.LogUtils.logDuration; import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.now; +import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE; import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce; @NotNullByDefault @@ -54,13 +59,20 @@ public class ConversationViewModel extends AndroidViewModel { private static Logger LOG = getLogger(ConversationViewModel.class.getName()); + private static final String SHOW_ONBOARDING_IMAGE = + "showOnboardingImage"; + private static final String SHOW_ONBOARDING_INTRODUCTION = + "showOnboardingIntroduction"; @DatabaseExecutor private final Executor dbExecutor; @CryptoExecutor private final Executor cryptoExecutor; + // TODO replace with TransactionManager once it exists + private final DatabaseComponent db; private final MessagingManager messagingManager; private final ContactManager contactManager; + private final SettingsManager settingsManager; private final PrivateMessageFactory privateMessageFactory; private final AttachmentController attachmentController; @@ -71,6 +83,14 @@ public class ConversationViewModel extends AndroidViewModel { Transformations.map(contact, c -> c.getAuthor().getId()); private final LiveData contactName = Transformations.map(contact, UiUtils::getContactDisplayName); + private final MutableLiveData imageSupport = + new MutableLiveData<>(); + private final MutableLiveData showImageOnboarding = + new MutableLiveData<>(); + private final MutableLiveData showIntroductionOnboarding = + new MutableLiveData<>(); + private final MutableLiveData showIntroductionAction = + new MutableLiveData<>(); private final MutableLiveData contactDeleted = new MutableLiveData<>(); private final MutableLiveData messagingGroupId = @@ -81,38 +101,46 @@ public class ConversationViewModel extends AndroidViewModel { @Inject ConversationViewModel(Application application, @DatabaseExecutor Executor dbExecutor, - @CryptoExecutor Executor cryptoExecutor, - MessagingManager messagingManager, - ContactManager contactManager, + @CryptoExecutor Executor cryptoExecutor, DatabaseComponent db, + MessagingManager messagingManager, ContactManager contactManager, + SettingsManager settingsManager, PrivateMessageFactory privateMessageFactory) { super(application); this.dbExecutor = dbExecutor; this.cryptoExecutor = cryptoExecutor; + this.db = db; this.messagingManager = messagingManager; this.contactManager = contactManager; + this.settingsManager = settingsManager; this.privateMessageFactory = privateMessageFactory; this.attachmentController = new AttachmentController(messagingManager, application.getResources()); contactDeleted.setValue(false); } + /** + * Setting the {@link ContactId} automatically triggers loading of other + * data. + */ void setContactId(ContactId contactId) { if (this.contactId == null) { this.contactId = contactId; - loadContact(); + loadContact(contactId); } else if (!contactId.equals(this.contactId)) { throw new IllegalStateException(); } } - private void loadContact() { + private void loadContact(ContactId contactId) { dbExecutor.execute(() -> { try { long start = now(); - Contact c = - contactManager.getContact(requireNonNull(contactId)); + Contact c = contactManager.getContact(contactId); contact.postValue(c); logDuration(LOG, "Loading contact", start); + start = now(); + checkFeaturesAndOnboarding(contactId); + logDuration(LOG, "Checking for image support", start); } catch (NoSuchContactException e) { contactDeleted.postValue(true); } catch (DbException e) { @@ -126,7 +154,7 @@ public class ConversationViewModel extends AndroidViewModel { try { contactManager.setContactAlias(requireNonNull(contactId), alias.isEmpty() ? null : alias); - loadContact(); + loadContact(contactId); } catch (DbException e) { logException(LOG, WARNING, e); } @@ -154,6 +182,59 @@ public class ConversationViewModel extends AndroidViewModel { }); } + @DatabaseExecutor + private void checkFeaturesAndOnboarding(ContactId c) throws DbException { + // check if images are supported + boolean imagesSupported = db.transactionWithResult(true, txn -> + messagingManager.contactSupportsImages(txn, c)); + imageSupport.postValue(imagesSupported); + + // check if introductions are supported + Collection contacts = contactManager.getActiveContacts(); + boolean introductionSupported = contacts.size() > 1; + showIntroductionAction.postValue(introductionSupported); + + Settings settings = settingsManager.getSettings(SETTINGS_NAMESPACE); + if (imagesSupported && + settings.getBoolean(SHOW_ONBOARDING_IMAGE, true)) { + // check if we should show onboarding, only if images are supported + showImageOnboarding.postValue(true); + // allow observer to stop listening for changes + showIntroductionOnboarding.postValue(false); + } else { + // allow observer to stop listening for changes + showImageOnboarding.postValue(false); + // we only show one onboarding dialog at a time + if (introductionSupported && + settings.getBoolean(SHOW_ONBOARDING_INTRODUCTION, true)) { + showIntroductionOnboarding.postValue(true); + } else { + // allow observer to stop listening for changes + showIntroductionOnboarding.postValue(false); + } + } + } + + void onImageOnboardingSeen() { + onOnboardingSeen(SHOW_ONBOARDING_IMAGE); + } + + void onIntroductionOnboardingSeen() { + onOnboardingSeen(SHOW_ONBOARDING_INTRODUCTION); + } + + private void onOnboardingSeen(String key) { + dbExecutor.execute(() -> { + try { + Settings settings = new Settings(); + settings.putBoolean(key, false); + settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } + private void storeAttachments(GroupId groupId, @Nullable String text, List uris, long timestamp) { dbExecutor.execute(() -> { @@ -262,6 +343,22 @@ public class ConversationViewModel extends AndroidViewModel { return contactName; } + LiveData hasImageSupport() { + return imageSupport; + } + + LiveData showImageOnboarding() { + return showImageOnboarding; + } + + LiveData showIntroductionOnboarding() { + return showIntroductionOnboarding; + } + + LiveData showIntroductionAction() { + return showIntroductionAction; + } + LiveData isContactDeleted() { return contactDeleted; } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/view/TextAttachmentController.java b/briar-android/src/main/java/org/briarproject/briar/android/view/TextAttachmentController.java index f7d60de2c..6248b6669 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/view/TextAttachmentController.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/view/TextAttachmentController.java @@ -1,6 +1,8 @@ package org.briarproject.briar.android.view; +import android.app.Activity; import android.content.ClipData; +import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Parcel; @@ -8,6 +10,7 @@ import android.os.Parcelable; import android.support.annotation.Nullable; import android.support.annotation.UiThread; import android.support.v4.view.AbsSavedState; +import android.support.v7.app.AlertDialog.Builder; import android.support.v7.widget.AppCompatImageButton; import android.widget.Toast; @@ -18,11 +21,15 @@ import org.briarproject.briar.android.view.ImagePreview.ImagePreviewListener; import java.util.ArrayList; import java.util.List; +import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt; +import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.PromptStateChangeListener; + 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.content.ContextCompat.getColor; import static android.support.v4.view.AbsSavedState.EMPTY_STATE; import static android.view.View.GONE; import static android.view.View.INVISIBLE; @@ -30,6 +37,9 @@ import static android.view.View.VISIBLE; import static android.widget.Toast.LENGTH_LONG; import static java.util.Collections.emptyList; import static java.util.Objects.requireNonNull; +import static org.briarproject.briar.android.util.UiUtils.resolveColorAttribute; +import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.STATE_DISMISSED; +import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.STATE_FINISHED; @UiThread @NotNullByDefault @@ -42,6 +52,7 @@ public class TextAttachmentController extends TextSendController private final AttachImageListener imageListener; private CharSequence textHint; + private boolean hasImageSupport = false; private List imageUris = emptyList(); public TextAttachmentController(TextInputView v, ImagePreview imagePreview, @@ -78,7 +89,28 @@ public class TextAttachmentController extends TextSendController return !imageUris.isEmpty(); } + /*** + * By default, image support is disabled. + * Once you know that it is supported in the current context, + * call this method to enable it. + */ + public void setImagesSupported() { + hasImageSupport = true; + imageButton.setImageResource(R.drawable.ic_image); + } + private void onImageButtonClicked() { + if (!hasImageSupport) { + Context ctx = imageButton.getContext(); + Builder builder = new Builder(ctx, R.style.OnboardingDialogTheme); + builder.setTitle( + ctx.getString(R.string.dialog_title_no_image_support)); + builder.setMessage( + ctx.getString(R.string.dialog_message_no_image_support)); + builder.setPositiveButton(R.string.got_it, null); + builder.show(); + return; + } Intent intent = new Intent(SDK_INT >= 19 ? ACTION_OPEN_DOCUMENT : ACTION_GET_CONTENT); intent.addCategory(CATEGORY_OPENABLE); @@ -187,6 +219,25 @@ public class TextAttachmentController extends TextSendController reset(); } + public void showImageOnboarding(Activity activity, + Runnable onOnboardingSeen) { + PromptStateChangeListener listener = (prompt, state) -> { + if (state == STATE_DISMISSED || state == STATE_FINISHED) { + onOnboardingSeen.run(); + } + }; + int color = resolveColorAttribute(activity, R.attr.colorControlNormal); + new MaterialTapTargetPrompt.Builder(activity, + R.style.OnboardingDialogTheme).setTarget(imageButton) + .setPrimaryText(R.string.dialog_title_image_support) + .setSecondaryText(R.string.dialog_message_image_support) + .setBackgroundColour(getColor(activity, R.color.briar_primary)) + .setIcon(R.drawable.ic_image) + .setIconDrawableColourFilter(color) + .setPromptStateChangeListener(listener) + .show(); + } + private static class SavedState extends AbsSavedState { @Nullable diff --git a/briar-android/src/main/res/drawable/ic_image_off.xml b/briar-android/src/main/res/drawable/ic_image_off.xml new file mode 100644 index 000000000..4c3cd2341 --- /dev/null +++ b/briar-android/src/main/res/drawable/ic_image_off.xml @@ -0,0 +1,11 @@ + + + diff --git a/briar-android/src/main/res/layout/text_input_view.xml b/briar-android/src/main/res/layout/text_input_view.xml index 3ca815785..474ce90c1 100644 --- a/briar-android/src/main/res/layout/text_input_view.xml +++ b/briar-android/src/main/res/layout/text_input_view.xml @@ -40,7 +40,7 @@ android:focusable="true" android:padding="4dp" android:scaleType="center" - android:src="@drawable/ic_image" + android:src="@drawable/ic_image_off" android:visibility="invisible" app:tint="?attr/colorControlNormal"/> diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml index dd4b0f96a..4e4e6c6b2 100644 --- a/briar-android/src/main/res/values/strings.xml +++ b/briar-android/src/main/res/values/strings.xml @@ -144,6 +144,10 @@ Saving this image will allow other apps to access it.\n\nAre you sure you want to save? Image was saved Could not save image + Images Unavailable + Your contact\'s Briar does not yet support image attachments. Once they upgrade you\'ll see a different icon. + You can now send images to this contact + Tap this icon to attach images. Add a Contact diff --git a/briar-android/witness.gradle b/briar-android/witness.gradle index ee7273494..4aed44ea8 100644 --- a/briar-android/witness.gradle +++ b/briar-android/witness.gradle @@ -134,8 +134,8 @@ dependencyVerification { 'junit:junit:4.12:junit-4.12.jar:59721f0805e223d84b90677887d9ff567dc534d7c502ca903c0c2b17f05c116a', 'nekohtml:nekohtml:1.9.6.2:nekohtml-1.9.6.2.jar:fdff6cfa9ed9cc911c842a5d2395f209ec621ef1239d46810e9e495809d3ae09', 'nekohtml:xercesMinimal:1.9.6.2:xercesMinimal-1.9.6.2.jar:95b8b357d19f63797dd7d67622fd3f18374d64acbc6584faba1c7759a31e8438', - 'net.bytebuddy:byte-buddy-agent:1.7.9:byte-buddy-agent-1.7.9.jar:ac1a993befb528c3271a83a9ad9c42d363d399e9deb26e0470e3c4962066c550', - 'net.bytebuddy:byte-buddy:1.7.9:byte-buddy-1.7.9.jar:2ea2ada12b790d16ac7f6e6c065cb55cbcdb6ba519355f5958851159cad3b16a', + 'net.bytebuddy:byte-buddy-agent:1.8.10:byte-buddy-agent-1.8.10.jar:f7403b1126137eb68a5cc3beaf543d965bafca87fad7d7d30082617748c19e05', + 'net.bytebuddy:byte-buddy:1.8.10:byte-buddy-1.8.10.jar:8c29e0118256acf9fbadcd75143df2d8bd9bfb07623ccf95a14646be5a92380c', 'net.sf.jopt-simple:jopt-simple:4.9:jopt-simple-4.9.jar:26c5856e954b5f864db76f13b86919b59c6eecf9fd930b96baa8884626baf2f5', 'net.sf.kxml:kxml2:2.3.0:kxml2-2.3.0.jar:f264dd9f79a1fde10ce5ecc53221eff24be4c9331c830b7d52f2f08a7b633de2', 'org.apache.ant:ant-launcher:1.9.4:ant-launcher-1.9.4.jar:7bccea20b41801ca17bcbc909a78c835d0f443f12d639c77bd6ae3d05861608d', @@ -186,7 +186,7 @@ dependencyVerification { 'org.jmock:jmock-testjar:2.8.2:jmock-testjar-2.8.2.jar:8900860f72c474e027cf97fe78dcbf154a1aa7fc62b6845c5fb4e4f3c7bc8760', 'org.jmock:jmock:2.8.2:jmock-2.8.2.jar:6c73cb4a2e6dbfb61fd99c9a768539c170ab6568e57846bd60dbf19596b65b16', 'org.jvnet.staxex:stax-ex:1.7.7:stax-ex-1.7.7.jar:a31ff7d77163c0deb09e7fee59ad35ae44c2cee2cc8552a116ccd1583d813fb4', - 'org.mockito:mockito-core:2.13.0:mockito-core-2.13.0.jar:92a746b37cf8c5730a5e7b35fd7d8cd72700089435ff92ee03ed8384d4eb3377', + 'org.mockito:mockito-core:2.19.0:mockito-core-2.19.0.jar:d6ac2e04164c5d5c89e73838dc1c8b3856ca6582d3f2daf91816fd9d7ba3c9a9', 'org.objenesis:objenesis:2.6:objenesis-2.6.jar:5e168368fbc250af3c79aa5fef0c3467a2d64e5a7bd74005f25d8399aeb0708d', 'org.ow2.asm:asm-analysis:6.0:asm-analysis-6.0.jar:2f1a6387219c3a6cc4856481f221b03bd9f2408a326d416af09af5d6f608c1f4', 'org.ow2.asm:asm-commons:6.0:asm-commons-6.0.jar:f1bce5c648a96a017bdcd01fe5d59af9845297fd7b79b81c015a6fbbd9719abf', @@ -203,6 +203,6 @@ dependencyVerification { 'org.robolectric:shadows-support-v4:3.3.2:shadows-support-v4-3.3.2.jar:6f689264738266e70fe08db7c04b7b5a75155994f4e3f7f311960d90486bf005', 'org.robolectric:utils:4.0.1:utils-4.0.1.jar:ee923ed66847271009ebeb246286b7206b160c2b6d1347fe820c00be06c280cb', 'tools.fastlane:screengrab:1.2.0:screengrab-1.2.0.aar:af4ee23bb06f94404d3ab18e2ea69db8265539fc8da29f9ee45b7e472684ba83', - 'uk.co.samuelwall:material-tap-target-prompt:2.12.4:material-tap-target-prompt-2.12.4.aar:6c0990ab3aa22de9f7d09dcb0a944e671128c31634ac8429012faa5c508202fb', + 'uk.co.samuelwall:material-tap-target-prompt:2.14.0:material-tap-target-prompt-2.14.0.aar:12ab447ba97019adbecb20e048921ca30ed7a9f72a37b83f39a4333bd759b518', ] } diff --git a/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingManager.java b/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingManager.java index 03bc693eb..a4f91af5c 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingManager.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingManager.java @@ -2,6 +2,7 @@ package org.briarproject.briar.api.messaging; import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.Transaction; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.sync.ClientId; import org.briarproject.bramble.api.sync.GroupId; @@ -60,4 +61,13 @@ public interface MessagingManager extends ConversationClient { */ Attachment getAttachment(MessageId m) throws DbException; + /** + * Returns true if the contact with the given {@link ContactId} does support + * image attachments. + * + * Added: 2019-01-01 + */ + boolean contactSupportsImages(Transaction txn, ContactId c) + throws DbException; + } diff --git a/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java index 223c980a2..4f20d13a9 100644 --- a/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java @@ -248,4 +248,13 @@ class MessagingManagerImpl extends ConversationClientImpl return new Attachment(new ByteArrayInputStream(bytes)); } + @Override + public boolean contactSupportsImages(Transaction txn, ContactId c) + throws DbException { + int minorVersion = clientVersioningManager + .getClientMinorVersion(txn, c, CLIENT_ID, 0); + // support was added in 0.1 + return minorVersion == 1; + } + }