feat: fallback to zxing-based lib to scan QrCodes

Uses a zxing-based library as a fallback when scanning a Qr-Code and GMS
is not available.

Closes https://github.com/mastodon/mastodon-android/issues/825
This commit is contained in:
FineFindus 2024-07-08 21:34:01 +02:00
parent 72df72228f
commit f5517f3d49
No known key found for this signature in database
GPG Key ID: 64873EE210FF8E6B
8 changed files with 160 additions and 21 deletions

View File

@ -96,6 +96,7 @@ dependencies {
implementation 'com.squareup:otto:1.3.8'
implementation 'de.psdev:async-otto:1.0.3'
implementation 'com.google.zxing:core:3.5.3'
implementation 'com.github.markusfisch:BarcodeScannerView:1.6.0'
implementation 'org.microg:safe-parcel:1.5.0'
implementation 'org.parceler:parceler-api:1.1.12'
annotationProcessor 'org.parceler:parceler:1.1.12'

View File

@ -9,6 +9,9 @@
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE"/>
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<permission android:name="${applicationId}.permission.C2D_MESSAGE" android:protectionLevel="signature"/>
@ -79,6 +82,7 @@
<data android:mimeType="*/*"/>
</intent-filter>
</activity>
<activity android:name=".QrCodeScanActivity"/>
<service android:name=".AudioPlayerService" android:foregroundServiceType="mediaPlayback"/>
<service android:name=".NotificationActionHandlerService" android:exported="false"/>

View File

@ -0,0 +1,98 @@
package org.joinmastodon.android;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.provider.Settings;
import android.view.HapticFeedbackConstants;
import android.view.View;
import androidx.annotation.NonNull;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import de.markusfisch.android.barcodescannerview.widget.BarcodeScannerView;
public class QrCodeScanActivity extends Activity{
private static final int PERMISSION_RESULT=65537;
private BarcodeScannerView scannerView;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
UiUtils.setUserPreferredTheme(this);
setContentView(R.layout.activity_qr_scan);
if(this.checkSelfPermission(Manifest.permission.CAMERA)!=PackageManager.PERMISSION_GRANTED){
requestPermissions(new String[]{Manifest.permission.CAMERA}, PERMISSION_RESULT);
}
findViewById(R.id.dismiss).setOnClickListener(view -> finish());
scannerView=findViewById(R.id.scanner);
scannerView.setCropRatio(.75f);
scannerView.setOnBarcodeListener(barcode -> {
vibrate(scannerView);
Intent result=new Intent();
result.putExtra("barcode", barcode.getText());
setResult(RESULT_OK, result);
finish();
return false;
});
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults){
if(requestCode==PERMISSION_RESULT){
if(grantResults[0]==PackageManager.PERMISSION_GRANTED){
scannerView.openAsync();
}else if(!this.shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)){
new M3AlertDialogBuilder(this)
.setTitle(R.string.permission_required)
.setMessage(R.string.camera_permission_to_scan)
.setPositiveButton(R.string.open_settings, (dialog, which)->this.startActivity(new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", this.getPackageName(), null))))
.setNegativeButton(R.string.cancel, (dialogInterface, i) -> finish())
.setOnCancelListener(dialogInterface -> finish())
.show();
}
}
}
private static void vibrate(View v) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
v.performHapticFeedback(HapticFeedbackConstants.CONFIRM);
return;
}
Vibrator vibrator=v.getContext().getSystemService(Vibrator.class);
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
vibrator.vibrate(VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK));
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
VibrationEffect effect=VibrationEffect.createOneShot(75L, 128);
vibrator.vibrate(effect);
} else {
vibrator.vibrate(75L);
}
}
@Override
public void onResume() {
super.onResume();
scannerView.openAsync();
}
@Override
public void onPause() {
super.onPause();
scannerView.close();
}
}

View File

@ -53,7 +53,6 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import android.window.OnBackInvokedCallback;
import android.window.OnBackInvokedDispatcher;
import com.google.zxing.BarcodeFormat;
@ -64,6 +63,7 @@ import com.google.zxing.qrcode.QRCodeWriter;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.QrCodeScanActivity;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.session.AccountSessionManager;
@ -127,7 +127,10 @@ public class ProfileQrCodeFragment extends AppKitFragment{
accountID=getArguments().getString("account");
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
setCancelable(false);
scannerIntent=BarcodeScanner.createIntent(Barcode.FORMAT_QR_CODE, false, true);
scannerIntent=GmsClient.isGooglePlayServicesAvailable(getActivity())
? BarcodeScanner.createIntent(Barcode.FORMAT_QR_CODE, false, true)
: new Intent(getActivity(), QrCodeScanActivity.class);
}
@Override
@ -265,11 +268,9 @@ public class ProfileQrCodeFragment extends AppKitFragment{
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
if(GmsClient.isGooglePlayServicesAvailable(getActivity())){
MenuItem item=menu.add(0, 0, 0, R.string.scan_qr_code);
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
item.setIcon(R.drawable.ic_qr_code_scanner_24px);
}
MenuItem item=menu.add(0, 0, 0, R.string.scan_qr_code);
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
item.setIcon(R.drawable.ic_qr_code_scanner_24px);
}
@Override
@ -330,11 +331,15 @@ public class ProfileQrCodeFragment extends AppKitFragment{
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data){
if(requestCode==SCAN_RESULT && resultCode==Activity.RESULT_OK && BarcodeScanner.isValidResult(data)){
Barcode code=BarcodeScanner.getResult(data);
if(requestCode==SCAN_RESULT && resultCode==Activity.RESULT_OK){
String code=data.getStringExtra("barcode");
if(BarcodeScanner.isValidResult(data)){
Barcode barcode=BarcodeScanner.getResult(data);
code=barcode.rawValue;
}
if(code!=null){
if(code.rawValue.startsWith("https:") || code.rawValue.startsWith("http:")){
((MainActivity)getActivity()).handleURL(Uri.parse(code.rawValue), accountID);
if(code.startsWith("https:") || code.startsWith("http:")){
((MainActivity)getActivity()).handleURL(Uri.parse(code), accountID);
dismiss();
}else{
Toast.makeText(themeWrapper, R.string.link_not_supported, Toast.LENGTH_SHORT).show();

View File

@ -17,6 +17,7 @@ import android.widget.TextView;
import android.widget.Toast;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.QrCodeScanActivity;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.ScrollableToTop;
import org.joinmastodon.android.googleservices.GmsClient;
@ -70,7 +71,9 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop{
setRetainInstance(true);
accountID=getArguments().getString("account");
scannerIntent=BarcodeScanner.createIntent(Barcode.FORMAT_QR_CODE, false, true);
scannerIntent=GmsClient.isGooglePlayServicesAvailable(getActivity())
? BarcodeScanner.createIntent(Barcode.FORMAT_QR_CODE, false, true)
: new Intent(getActivity(), QrCodeScanActivity.class);
}
@Nullable
@ -186,11 +189,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop{
searchView.setVisibility(View.VISIBLE);
}
searchScanQR=view.findViewById(R.id.search_scan_qr);
if(!GmsClient.isGooglePlayServicesAvailable(getActivity())){
searchScanQR.setVisibility(View.GONE);
}else{
searchScanQR.setOnClickListener(v->openQrScanner());
}
searchScanQR.setOnClickListener(v->openQrScanner());
View searchWrap=view.findViewById(R.id.search_wrap);
searchWrap.setOutlineProvider(OutlineProviders.roundedRect(28));
@ -280,11 +279,15 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop{
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data){
if(requestCode==SCAN_RESULT && resultCode==Activity.RESULT_OK && BarcodeScanner.isValidResult(data)){
Barcode code=BarcodeScanner.getResult(data);
if(requestCode==SCAN_RESULT && resultCode==Activity.RESULT_OK){
String code=data.getStringExtra("barcode");
if(BarcodeScanner.isValidResult(data)){
Barcode barcode=BarcodeScanner.getResult(data);
code=barcode.rawValue;
}
if(code!=null){
if(code.rawValue.startsWith("https:") || code.rawValue.startsWith("http:")){
((MainActivity)getActivity()).handleURL(Uri.parse(code.rawValue), accountID);
if(code.startsWith("https:") || code.startsWith("http:")){
((MainActivity)getActivity()).handleURL(Uri.parse(code), accountID);
}else{
Toast.makeText(getActivity(), R.string.link_not_supported, Toast.LENGTH_SHORT).show();
}

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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"
android:clipChildren="false"
android:clipToPadding="false">
<de.markusfisch.android.barcodescannerview.widget.BarcodeScannerView
android:id="@+id/scanner"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<ImageButton
android:id="@+id/dismiss"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|top"
android:background="?android:selectableItemBackground"
android:contentDescription="@string/back"
android:padding="16dp"
android:src="@drawable/ic_arrow_back"
android:tint="@android:color/white"
android:layout_margin="16dp"/>
</FrameLayout>

View File

@ -233,6 +233,7 @@
<string name="download">Download</string>
<string name="permission_required">Permission required</string>
<string name="storage_permission_to_download">The app needs access to your storage to save this file.</string>
<string name="camera_permission_to_scan">The app needs access to your camera to scan.</string>
<string name="open_settings">Open settings</string>
<string name="error_saving_file">Error saving file</string>
<string name="file_saved">File saved</string>

View File

@ -4,6 +4,7 @@ dependencyResolutionManagement {
google()
mavenCentral()
mavenLocal()
maven { url 'https://jitpack.io' }
}
}
rootProject.name = "Mastodon"