From 16c701a71a288f86dac073c0416495a9615a5688 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 17 Dec 2018 15:41:47 -0200 Subject: [PATCH] [android] only enable image feature if contact supports it Also show an onboarding the first time, the feature gets activiated --- briar-android/build.gradle | 4 +- .../conversation/ConversationActivity.java | 25 +++++++ .../conversation/ConversationViewModel.java | 68 +++++++++++++++++-- .../view/TextAttachmentController.java | 50 ++++++++++++++ .../src/main/res/drawable/ic_image_off.xml | 11 +++ .../src/main/res/layout/text_input_view.xml | 2 +- briar-android/src/main/res/values/strings.xml | 4 ++ briar-android/witness.gradle | 8 +-- 8 files changed, 161 insertions(+), 11 deletions(-) create mode 100644 briar-android/src/main/res/drawable/ic_image_off.xml 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..9fb93a657 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 @@ -263,6 +263,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); } @@ -461,6 +468,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(); @@ -728,6 +739,18 @@ public class ConversationActivity extends BriarActivity runOnUiThreadUnlessDestroyed(() -> item.setEnabled(true)); } + private void showImageOnboarding(@Nullable Boolean show) { + if (show == null || !show) return; + // show onboarding only after the enter transition has ended + // otherwise the tap target animation won't play + textInputView.postDelayed(() -> { + // remove cast when removing FEATURE_FLAG_IMAGE_ATTACHMENTS + ((TextAttachmentController) sendController) + .showImageOnboarding(this, () -> + viewModel.imageOnboardingSeen()); + }, 750); + } + private void showIntroductionOnboarding() { runOnUiThreadUnlessDestroyed(() -> { // find view of overflow icon @@ -755,6 +778,8 @@ public class ConversationActivity extends BriarActivity .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(); }); 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..7ca580db8 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,13 +16,17 @@ 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.bramble.api.versioning.ClientVersioningManager; import org.briarproject.briar.android.util.UiUtils; import org.briarproject.briar.api.messaging.Attachment; import org.briarproject.briar.api.messaging.AttachmentHeader; @@ -47,21 +51,28 @@ 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; +import static org.briarproject.briar.api.messaging.MessagingManager.CLIENT_ID; @NotNullByDefault public class ConversationViewModel extends AndroidViewModel { private static Logger LOG = getLogger(ConversationViewModel.class.getName()); + private static final String SHOW_ONBOARDING_IMAGE = + "showOnboardingImage"; @DatabaseExecutor private final Executor dbExecutor; @CryptoExecutor private final Executor cryptoExecutor; + private final DatabaseComponent db; private final MessagingManager messagingManager; private final ContactManager contactManager; + private final SettingsManager settingsManager; private final PrivateMessageFactory privateMessageFactory; + private final ClientVersioningManager clientVersioningManager; private final AttachmentController attachmentController; @Nullable @@ -71,6 +82,10 @@ 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 contactDeleted = new MutableLiveData<>(); private final MutableLiveData messagingGroupId = @@ -81,16 +96,20 @@ public class ConversationViewModel extends AndroidViewModel { @Inject ConversationViewModel(Application application, @DatabaseExecutor Executor dbExecutor, - @CryptoExecutor Executor cryptoExecutor, - MessagingManager messagingManager, - ContactManager contactManager, - PrivateMessageFactory privateMessageFactory) { + @CryptoExecutor Executor cryptoExecutor, DatabaseComponent db, + MessagingManager messagingManager, ContactManager contactManager, + SettingsManager settingsManager, + PrivateMessageFactory privateMessageFactory, + ClientVersioningManager clientVersioningManager) { super(application); this.dbExecutor = dbExecutor; this.cryptoExecutor = cryptoExecutor; + this.db = db; this.messagingManager = messagingManager; this.contactManager = contactManager; + this.settingsManager = settingsManager; this.privateMessageFactory = privateMessageFactory; + this.clientVersioningManager = clientVersioningManager; this.attachmentController = new AttachmentController(messagingManager, application.getResources()); contactDeleted.setValue(false); @@ -113,6 +132,9 @@ public class ConversationViewModel extends AndroidViewModel { contactManager.getContact(requireNonNull(contactId)); contact.postValue(c); logDuration(LOG, "Loading contact", start); + start = now(); + checkImageSupport(c.getId()); + logDuration(LOG, "Checking for image support", start); } catch (NoSuchContactException e) { contactDeleted.postValue(true); } catch (DbException e) { @@ -154,6 +176,36 @@ public class ConversationViewModel extends AndroidViewModel { }); } + @DatabaseExecutor + private void checkImageSupport(ContactId c) throws DbException { + int minorVersion = db.transactionWithResult(true, txn -> + clientVersioningManager + .getClientMinorVersion(txn, c, CLIENT_ID, 0)); + // support was added in 0.1 + boolean imagesSupported = minorVersion == 1; + imageSupport.postValue(imagesSupported); + if (!imagesSupported) return; + + // check if we should show onboarding, only if images are supported + Settings settings = + settingsManager.getSettings(SETTINGS_NAMESPACE); + if (settings.getBoolean(SHOW_ONBOARDING_IMAGE, true)) { + showImageOnboarding.postValue(true); + } + } + + void imageOnboardingSeen() { + dbExecutor.execute(() -> { + try { + Settings settings = new Settings(); + settings.putBoolean(SHOW_ONBOARDING_IMAGE, 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 +314,14 @@ public class ConversationViewModel extends AndroidViewModel { return contactName; } + LiveData hasImageSupport() { + return imageSupport; + } + + LiveData showImageOnboarding() { + return showImageOnboarding; + } + 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..79fc6eb7a 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,24 @@ 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); + MaterialTapTargetPrompt p = 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', ] }