Add QR group link share.

master
Alan Evans 2020-08-31 12:35:38 -03:00 committed by GitHub
parent 4714895c59
commit 1a3985d709
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 340 additions and 16 deletions

View File

@ -0,0 +1,93 @@
package org.thoughtcrime.securesms.components.qr;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.util.AttributeSet;
import android.util.Log;
import androidx.annotation.ColorInt;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.SquareImageView;
import org.thoughtcrime.securesms.qr.QrCode;
import org.thoughtcrime.securesms.util.Stopwatch;
import org.thoughtcrime.securesms.util.concurrent.SerialMonoLifoExecutor;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import java.util.concurrent.Executor;
/**
* Generates a bitmap asynchronously for the supplied {@link BitMatrix} data and displays it.
*/
public class QrView extends SquareImageView {
private static final @ColorInt int DEFAULT_FOREGROUND_COLOR = Color.BLACK;
private static final @ColorInt int DEFAULT_BACKGROUND_COLOR = Color.TRANSPARENT;
private @Nullable Bitmap qrBitmap;
private @ColorInt int foregroundColor;
private @ColorInt int backgroundColor;
public QrView(Context context) {
super(context);
init(null);
}
public QrView(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs);
}
public QrView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs);
}
private void init(@Nullable AttributeSet attrs) {
if (attrs != null) {
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.QrView, 0, 0);
foregroundColor = typedArray.getColor(R.styleable.QrView_qr_foreground_color, DEFAULT_FOREGROUND_COLOR);
backgroundColor = typedArray.getColor(R.styleable.QrView_qr_background_color, DEFAULT_BACKGROUND_COLOR);
typedArray.recycle();
} else {
foregroundColor = DEFAULT_FOREGROUND_COLOR;
backgroundColor = DEFAULT_BACKGROUND_COLOR;
}
if (isInEditMode()) {
setQrText("https://signal.org");
}
}
public void setQrText(@Nullable String text) {
setQrBitmap(QrCode.create(text, foregroundColor, backgroundColor));
}
private void setQrBitmap(@Nullable Bitmap qrBitmap) {
if (this.qrBitmap == qrBitmap) {
return;
}
if (this.qrBitmap != null) {
this.qrBitmap.recycle();
}
this.qrBitmap = qrBitmap;
setImageBitmap(this.qrBitmap);
}
public @Nullable Bitmap getQrBitmap() {
return qrBitmap;
}
}

View File

@ -5,33 +5,57 @@ import android.graphics.Color;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.logging.Log;
import androidx.annotation.Nullable;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
public class QrCode {
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.Stopwatch;
public static final String TAG = QrCode.class.getSimpleName();
public final class QrCode {
public static @NonNull Bitmap create(String data) {
return create(data, Color.BLACK);
private QrCode() {
}
public static @NonNull Bitmap create(String data, @ColorInt int foregroundColor) {
try {
BitMatrix result = new QRCodeWriter().encode(data, BarcodeFormat.QR_CODE, 512, 512);
Bitmap bitmap = Bitmap.createBitmap(result.getWidth(), result.getHeight(), Bitmap.Config.ARGB_8888);
public static final String TAG = Log.tag(QrCode.class);
for (int y = 0; y < result.getHeight(); y++) {
for (int x = 0; x < result.getWidth(); x++) {
if (result.get(x, y)) {
bitmap.setPixel(x, y, foregroundColor);
}
public static @NonNull Bitmap create(@Nullable String data) {
return create(data, Color.BLACK, Color.TRANSPARENT);
}
public static @NonNull Bitmap create(@Nullable String data,
@ColorInt int foregroundColor,
@ColorInt int backgroundColor)
{
if (data == null || data.length() == 0) {
Log.w(TAG, "No data");
return Bitmap.createBitmap(512, 512, Bitmap.Config.ARGB_8888);
}
try {
Stopwatch stopwatch = new Stopwatch("QrGen");
QRCodeWriter qrCodeWriter = new QRCodeWriter();
BitMatrix qrData = qrCodeWriter.encode(data, BarcodeFormat.QR_CODE, 512, 512);
int qrWidth = qrData.getWidth();
int qrHeight = qrData.getHeight();
int[] pixels = new int[qrWidth * qrHeight];
for (int y = 0; y < qrHeight; y++) {
int offset = y * qrWidth;
for (int x = 0; x < qrWidth; x++) {
pixels[offset + x] = qrData.get(x, y) ? foregroundColor : backgroundColor;
}
}
stopwatch.split("Write pixels");
Bitmap bitmap = Bitmap.createBitmap(pixels, qrWidth, qrHeight, Bitmap.Config.ARGB_8888);
stopwatch.split("Create bitmap");
stopwatch.stop(TAG);
return bitmap;
} catch (WriterException e) {

View File

@ -18,6 +18,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.LiveGroup;
import org.thoughtcrime.securesms.recipients.ui.sharablegrouplink.qr.GroupLinkShareQrDialogFragment;
import org.thoughtcrime.securesms.util.BottomSheetUtil;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util;
@ -77,8 +78,10 @@ public final class GroupLinkBottomSheetDialogFragment extends BottomSheetDialogF
dismiss();
});
viewQrButton.setOnClickListener(v -> dismiss()); // Todo [Alan] GV2 Add share QR within signal
viewQrButton.setVisibility(View.GONE);
viewQrButton.setOnClickListener(v -> {
GroupLinkShareQrDialogFragment.show(requireFragmentManager(), groupId);
dismiss();
});
shareBySystemButton.setOnClickListener(v -> {
ShareCompat.IntentBuilder.from(requireActivity())

View File

@ -0,0 +1,92 @@
package org.thoughtcrime.securesms.recipients.ui.sharablegrouplink.qr;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelProviders;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.qr.QrView;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.BottomSheetUtil;
import org.thoughtcrime.securesms.util.ThemeUtil;
import java.util.Objects;
public class GroupLinkShareQrDialogFragment extends DialogFragment {
private static final String TAG = Log.tag(GroupLinkShareQrDialogFragment.class);
private static final String ARG_GROUP_ID = "group_id";
private GroupLinkShareQrViewModel viewModel;
private QrView qrImageView;
private View shareCodeButton;
public static void show(@NonNull FragmentManager manager, @NonNull GroupId.V2 groupId) {
DialogFragment fragment = new GroupLinkShareQrDialogFragment();
Bundle args = new Bundle();
args.putString(ARG_GROUP_ID, groupId.toString());
fragment.setArguments(args);
fragment.show(manager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(STYLE_NO_FRAME, ThemeUtil.isDarkTheme(requireActivity()) ? R.style.TextSecure_DarkTheme
: R.style.TextSecure_LightTheme);
}
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState)
{
return inflater.inflate(R.layout.group_link_share_qr_dialog_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
initializeViewModel();
initializeViews(view);
}
private void initializeViewModel() {
Bundle arguments = requireArguments();
GroupId.V2 groupId = GroupId.parseOrThrow(Objects.requireNonNull(arguments.getString(ARG_GROUP_ID))).requireV2();
GroupLinkShareQrViewModel.Factory factory = new GroupLinkShareQrViewModel.Factory(groupId);
viewModel = ViewModelProviders.of(this, factory).get(GroupLinkShareQrViewModel.class);
}
private void initializeViews(@NonNull View view) {
Toolbar toolbar = view.findViewById(R.id.group_link_share_qr_toolbar);
qrImageView = view.findViewById(R.id.group_link_share_qr_image);
shareCodeButton = view.findViewById(R.id.group_link_share_code_button);
toolbar.setNavigationOnClickListener(v -> dismissAllowingStateLoss());
viewModel.getQrUrl().observe(getViewLifecycleOwner(), this::presentUrl);
}
private void presentUrl(@Nullable String url) {
qrImageView.setQrText(url);
shareCodeButton.setOnClickListener(v -> {
// TODO [Alan] GV2 Allow qr image share
});
}
}

View File

@ -0,0 +1,41 @@
package org.thoughtcrime.securesms.recipients.ui.sharablegrouplink.qr;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.LiveGroup;
import org.thoughtcrime.securesms.groups.v2.GroupLinkUrlAndStatus;
public final class GroupLinkShareQrViewModel extends ViewModel {
private final LiveData<String> qrData;
private GroupLinkShareQrViewModel(@NonNull GroupId.V2 groupId) {
LiveGroup liveGroup = new LiveGroup(groupId);
this.qrData = Transformations.map(liveGroup.getGroupLink(), GroupLinkUrlAndStatus::getUrl);
}
LiveData<String> getQrUrl() {
return qrData;
}
public static final class Factory implements ViewModelProvider.Factory {
private final GroupId.V2 groupId;
public Factory(@NonNull GroupId.V2 groupId) {
this.groupId = groupId;
}
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new GroupLinkShareQrViewModel(groupId));
}
}
}

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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">
<androidx.appcompat.widget.Toolbar
android:id="@+id/group_link_share_qr_toolbar"
android:layout_width="0dp"
android:layout_height="?attr/actionBarSize"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navigationIcon="@drawable/ic_arrow_left_24"
app:title="@string/GroupLinkShareQrDialogFragment__qr_code" />
<org.thoughtcrime.securesms.components.qr.QrView
android:id="@+id/group_link_share_qr_image"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:scaleType="fitXY"
app:layout_constraintBottom_toTopOf="@+id/group_link_share_code_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/group_link_share_qr_toolbar"
app:layout_constraintVertical_bias="0.35"
app:layout_constraintWidth_percent="0.75"
app:qr_background_color="?android:windowBackground"
app:qr_foreground_color="?title_text_color_primary" />
<TextView
android:id="@+id/group_link_share_explain"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:labelFor="@id/group_link_share_qr_image"
android:paddingStart="32dp"
android:paddingEnd="32dp"
android:text="@string/GroupLinkShareQrDialogFragment__people_who_scan_this_code_will"
android:textAlignment="center"
app:layout_constraintBottom_toTopOf="@+id/group_link_share_code_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/group_link_share_qr_image"
app:layout_constraintVertical_bias="0" />
<Button
android:id="@+id/group_link_share_code_button"
style="@style/Button.Primary"
android:layout_width="0dp"
android:layout_height="64dp"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="16dp"
android:text="@string/GroupLinkShareQrDialogFragment__share_code"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -562,6 +562,11 @@
<attr name="background_tint" format="color" />
</declare-styleable>
<declare-styleable name="QrView">
<attr name="qr_foreground_color" format="color" />
<attr name="qr_background_color" format="color" />
</declare-styleable>
<declare-styleable name="WebRtcAudioOutputToggleButtonState">
<attr name="state_speaker_on" format="boolean" />
<attr name="state_speaker_off" format="boolean" />