mirror of
https://github.com/oxen-io/session-android.git
synced 2025-07-02 03:38:29 +00:00
Add QR group link share.
This commit is contained in:
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.ColorInt;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.google.zxing.BarcodeFormat;
|
import com.google.zxing.BarcodeFormat;
|
||||||
import com.google.zxing.WriterException;
|
import com.google.zxing.WriterException;
|
||||||
import com.google.zxing.common.BitMatrix;
|
import com.google.zxing.common.BitMatrix;
|
||||||
import com.google.zxing.qrcode.QRCodeWriter;
|
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) {
|
private QrCode() {
|
||||||
return create(data, Color.BLACK);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NonNull Bitmap create(String data, @ColorInt int foregroundColor) {
|
public static final String TAG = Log.tag(QrCode.class);
|
||||||
try {
|
|
||||||
BitMatrix result = new QRCodeWriter().encode(data, BarcodeFormat.QR_CODE, 512, 512);
|
|
||||||
Bitmap bitmap = Bitmap.createBitmap(result.getWidth(), result.getHeight(), Bitmap.Config.ARGB_8888);
|
|
||||||
|
|
||||||
for (int y = 0; y < result.getHeight(); y++) {
|
public static @NonNull Bitmap create(@Nullable String data) {
|
||||||
for (int x = 0; x < result.getWidth(); x++) {
|
return create(data, Color.BLACK, Color.TRANSPARENT);
|
||||||
if (result.get(x, y)) {
|
}
|
||||||
bitmap.setPixel(x, y, foregroundColor);
|
|
||||||
}
|
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;
|
return bitmap;
|
||||||
} catch (WriterException e) {
|
} catch (WriterException e) {
|
||||||
|
@ -18,6 +18,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
|||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.groups.GroupId;
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
import org.thoughtcrime.securesms.groups.LiveGroup;
|
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.BottomSheetUtil;
|
||||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
@ -77,8 +78,10 @@ public final class GroupLinkBottomSheetDialogFragment extends BottomSheetDialogF
|
|||||||
dismiss();
|
dismiss();
|
||||||
});
|
});
|
||||||
|
|
||||||
viewQrButton.setOnClickListener(v -> dismiss()); // Todo [Alan] GV2 Add share QR within signal
|
viewQrButton.setOnClickListener(v -> {
|
||||||
viewQrButton.setVisibility(View.GONE);
|
GroupLinkShareQrDialogFragment.show(requireFragmentManager(), groupId);
|
||||||
|
dismiss();
|
||||||
|
});
|
||||||
|
|
||||||
shareBySystemButton.setOnClickListener(v -> {
|
shareBySystemButton.setOnClickListener(v -> {
|
||||||
ShareCompat.IntentBuilder.from(requireActivity())
|
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" />
|
<attr name="background_tint" format="color" />
|
||||||
</declare-styleable>
|
</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">
|
<declare-styleable name="WebRtcAudioOutputToggleButtonState">
|
||||||
<attr name="state_speaker_on" format="boolean" />
|
<attr name="state_speaker_on" format="boolean" />
|
||||||
<attr name="state_speaker_off" format="boolean" />
|
<attr name="state_speaker_off" format="boolean" />
|
||||||
|
Loading…
x
Reference in New Issue
Block a user