Add QR group link share.
parent
4714895c59
commit
1a3985d709
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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" />
|
||||
|
|
Loading…
Reference in New Issue