Commit 228df712 authored by Steppschuh's avatar Steppschuh
Browse files

Release 1.6.6

parent 17187669
# Release 1.6.6
- Improved accessibility features
- Clearer consent screens and disclosures
- Miscellaneous bug fixes and improvements
We integrated the TalkBack screen reader to help blind and visually impaired users interact with the luca app.
Our consent dialogs have been updated to clarify under which conditions data is being processed. It is now indicated for which feature the data is required and which section of our privacy policy or the GDPR this relates to.
# Release 1.6.0
- Automatic check-out
......
pipeline {
agent {
label('docker')
}
environment {
CONTAINER = 'mingc/android-build-box:1.20.0'
}
stages {
stage('Docker pull') {
steps {
sh("docker pull ${CONTAINER}")
}
}
stage('Unit Tests') {
steps {
sh("cd Luca && docker run --rm -v `pwd`:/Luca ${CONTAINER} bash -c 'cd /Luca; ./gradlew :app:test'")
}
}
stage('Build') {
steps {
sh("cd Luca && docker run --rm -v `pwd`:/Luca ${CONTAINER} bash -c 'cd /Luca; ./gradlew :app:assembleDebug'")
}
}
stage('Archive') {
steps {
archiveArtifacts artifacts: 'Luca/app/build/outputs/apk/**/*.apk', excludes: 'Luca/app/build/outputs/apk/**/*-androidTest.apk', fingerprint: true
}
}
}
}
......@@ -7,16 +7,16 @@ android {
applicationId "de.culture4life.luca"
minSdkVersion 21
targetSdkVersion 30
versionCode 52
versionName "1.6.1"
versionCode 57
versionName "1.6.6"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
signingConfigs {
culture4life {
keyAlias project.property("C4L_SIGNING_KEY_ALIAS")
keyPassword project.property("C4L_SIGNING_KEY_PASSWORD")
storeFile file(project.property("C4L_SIGNING_STORE_FILE"))
storePassword project.property("C4L_SIGNING_STORE_PASSWORD")
keyAlias project.getProperties().getOrDefault("C4L_SIGNING_KEY_ALIAS", '"<signing key alias>"')
keyPassword project.getProperties().getOrDefault("C4L_SIGNING_KEY_PASSWORD", '"<signing key password>"')
storeFile file(project.getProperties().getOrDefault("C4L_SIGNING_STORE_FILE", '"<signing store file>"'))
storePassword project.getProperties().getOrDefault("C4L_SIGNING_STORE_PASSWORD", '"<signing store password>"')
}
}
buildTypes {
......@@ -26,18 +26,25 @@ android {
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.culture4life
buildConfigField "String", "STAGING_API_USERNAME", '""'
buildConfigField "String", "STAGING_API_PASSWORD", '""'
}
debug {
debuggable true
minifyEnabled false
versionNameSuffix " Debug"
applicationIdSuffix ".debug"
buildConfigField "String", "STAGING_API_USERNAME", project.getProperties().getOrDefault("STAGING_API_USERNAME", '"<staging username>"')
buildConfigField "String", "STAGING_API_PASSWORD", project.getProperties().getOrDefault("STAGING_API_PASSWORD", '"<staging password>"')
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
testOptions {
unitTests.includeAndroidResources true
}
}
dependencies {
......@@ -78,10 +85,11 @@ dependencies {
implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.1.0'
implementation 'com.uber.rxdogtag2:rxdogtag:2.0.1'
implementation 'commons-codec:commons-codec:1.15'
implementation 'io.reactivex.rxjava3:rxjava:3.0.6'
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
implementation 'net.grandcentrix.tray:tray:0.12.0'
implementation 'org.bouncycastle:bcpkix-jdk15on:1.56'
implementation 'org.bouncycastle:bcpkix-jdk15on:1.65'
testImplementation 'androidx.test:core:1.3.0'
......@@ -95,7 +103,7 @@ dependencies {
testImplementation 'org.powermock:powermock-module-junit4:2.0.0-beta.5'
testImplementation 'org.powermock:powermock-module-junit4-common:2.0.0-beta.5'
testImplementation 'org.powermock:powermock-module-junit4-rule:2.0.0-beta.5'
testImplementation 'org.robolectric:robolectric:4.2'
testImplementation 'org.robolectric:robolectric:4.5.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
......
......@@ -8,13 +8,16 @@ import de.culture4life.luca.LucaApplication;
import net.lachlanmckee.timberjunit.TimberTestRule;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import androidx.annotation.NonNull;
import androidx.test.platform.app.InstrumentationRegistry;
......@@ -112,6 +115,15 @@ public class CryptoManagerTest {
.assertResult("AOU\nÄÖÜ");
}
@Test
public void testGenerateRecentStartOfDayTimestamps() {
List<Long> test = cryptoManager.generateRecentStartOfDayTimestamps(14).toList().blockingGet();
for (int i = 0; i < test.size() - 1; i++) {
long diff = test.get(i) - test.get(i + 1);
Assert.assertEquals(diff, TimeUnit.DAYS.toMillis(1));
}
}
public static String encodeSecret(@NonNull byte[] secret) {
return RxBase64.encode(secret, Base64.NO_WRAP)
.blockingGet();
......
package de.culture4life.luca.util;
import de.culture4life.luca.LucaApplication;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import androidx.test.platform.app.InstrumentationRegistry;
public class AccessibilityServiceUtilTest {
private LucaApplication application;
@Before
public void setup() {
application = (LucaApplication) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext();
}
@Test
@Ignore("Enable only temporarily when your device has talkBack enabled")
public void testIsGoogleTalkbackActive() {
Assert.assertTrue(AccessibilityServiceUtil.isGoogleTalkbackActive(application));
}
}
\ No newline at end of file
package de.culture4life.luca.util;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
public class Z85Test {
private static final String BASE64_ENCODED_DATA = "AgAAgKt0X2dGsPr48Gs80B49tT8Zlk3SRLrTmiboPwTFAXJkEJuNV3/CMsQSc9fOrX4qBC2oYAMK+Zy+Gv1/10C/8/U5W8ZDoXoGrsRkHF/rM44eZ0fK9qjKDnx9+lgz";
private static final String Z85_ENCODED_DATA = "0SSjJT8}YYmZh::[m$ek9Zc[}8j11Bm7Q<ocG)U%-q$0&5sXUXF5i$V5{8WIT:R-5eVr4V3I*ja8Vtfqk!({-iA%G<P)u{C-af&{(OBLxxgHbrSlbP$EFN54";
@Test
public void encodeAndDecode_validInput_correctOutput() {
byte[] input = "AOU\nÄÖÜ".getBytes(StandardCharsets.UTF_8);
String encoded = Z85.encode(input);
byte[] decoded = Z85.decode(encoded);
assertArrayEquals(input, decoded);
}
@Test
public void encode_validInput_correctOutput() {
byte[] data = SerializationUtil.deserializeFromBase64(BASE64_ENCODED_DATA).blockingGet();
String actual = Z85.encode(data);
System.out.println(actual);
assertEquals(Z85_ENCODED_DATA, actual);
}
@Test
public void decode_validInput_correctOutput() {
byte[] expected = SerializationUtil.deserializeFromBase64(BASE64_ENCODED_DATA).blockingGet();
byte[] actual = Z85.decode(Z85_ENCODED_DATA);
assertArrayEquals(expected, actual);
}
}
\ No newline at end of file
......@@ -19,6 +19,7 @@ import de.culture4life.luca.location.GeofenceManager;
import de.culture4life.luca.location.LocationManager;
import de.culture4life.luca.meeting.MeetingManager;
import de.culture4life.luca.network.NetworkManager;
import de.culture4life.luca.network.endpoints.LucaEndpointsV3;
import de.culture4life.luca.notification.LucaNotificationManager;
import de.culture4life.luca.preference.PreferencesManager;
import de.culture4life.luca.registration.RegistrationManager;
......@@ -306,8 +307,8 @@ public class LucaApplication extends MultiDexApplication {
}
public Single<Boolean> isUpdateRequired() {
return networkManager.getLucaEndpoints()
.getSupportedVersionNumber()
return networkManager.getLucaEndpointsV3()
.flatMap(LucaEndpointsV3::getSupportedVersionNumber)
.map(jsonObject -> jsonObject.get("minimumVersion").getAsInt())
.doOnSuccess(versionNumber -> Timber.d("Minimum supported app version number: %d", versionNumber))
.map(minimumVersionNumber -> BuildConfig.VERSION_CODE < minimumVersionNumber);
......
......@@ -55,6 +55,7 @@ import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import timber.log.Timber;
......@@ -100,6 +101,9 @@ public class CheckInManager extends Manager {
@Nullable
private GeofencingRequest autoCheckoutGeofenceRequest;
@Nullable
private Disposable automaticCheckoutDisposable;
public CheckInManager(@NonNull PreferencesManager preferencesManager, @NonNull NetworkManager networkManager, @NonNull GeofenceManager geofenceManager, @NonNull LocationManager locationManager, @NonNull HistoryManager historyManager, @NonNull CryptoManager cryptoManager, @NonNull LucaNotificationManager notificationManager) {
this.preferencesManager = preferencesManager;
this.networkManager = networkManager;
......@@ -402,20 +406,33 @@ public class CheckInManager extends Manager {
@RequiresPermission("android.permission.ACCESS_FINE_LOCATION")
public Completable enableAutomaticCheckOut() {
return createAutoCheckoutGeofenceRequest()
.flatMapObservable(geofenceManager::getGeofenceEvents)
.firstElement()
.ignoreElement()
.andThen(performAutomaticCheckout()
.doOnError(throwable -> Timber.w("Unable to perform automatic check-out: %s", throwable.toString()))
.retryWhen(errors -> errors
.doOnNext(throwable -> Timber.v("Retrying automatic check-out in %d seconds", TimeUnit.MILLISECONDS.toSeconds(AUTOMATIC_CHECK_OUT_RETRY_DELAY)))
.delay(AUTOMATIC_CHECK_OUT_RETRY_DELAY, TimeUnit.MILLISECONDS, Schedulers.io())));
return Completable.fromAction(() -> {
if (automaticCheckoutDisposable != null && !automaticCheckoutDisposable.isDisposed()) {
automaticCheckoutDisposable.dispose();
}
automaticCheckoutDisposable = createAutoCheckoutGeofenceRequest()
.flatMapObservable(geofenceManager::getGeofenceEvents)
.firstElement()
.ignoreElement()
.andThen(performAutomaticCheckout()
.doOnError(throwable -> Timber.w("Unable to perform automatic check-out: %s", throwable.toString()))
.retryWhen(errors -> errors
.doOnNext(throwable -> Timber.v("Retrying automatic check-out in %d seconds", TimeUnit.MILLISECONDS.toSeconds(AUTOMATIC_CHECK_OUT_RETRY_DELAY)))
.delay(AUTOMATIC_CHECK_OUT_RETRY_DELAY, TimeUnit.MILLISECONDS, Schedulers.io())))
.subscribeOn(Schedulers.io())
.subscribe();
managerDisposable.add(automaticCheckoutDisposable);
});
}
public Completable disableAutomaticCheckOut() {
return Maybe.fromCallable(() -> autoCheckoutGeofenceRequest)
.flatMapCompletable(geofenceManager::removeGeofences)
.andThen(Completable.fromAction(() -> {
if (automaticCheckoutDisposable != null && !automaticCheckoutDisposable.isDisposed()) {
automaticCheckoutDisposable.dispose();
}
}))
.doOnComplete(() -> autoCheckoutGeofenceRequest = null);
}
......
......@@ -167,10 +167,10 @@ public class CryptoManager extends Manager {
}
/**
* In app versions before 1.4.8, the daily key pair public key was named "backend master key". To
* avoid the impression that there would be an all mighty "master key", it has been renamed. In
* app versions before 1.6.1, the daily key pair public key was named "rotating backend public
* key". To match with the security concept of luca, it has been renamed.
* In app versions before 1.4.8, the daily key pair public key was named "backend master key".
* To avoid the impression that there would be an all mighty "master key", it has been renamed.
* In app versions before 1.6.1, the daily key pair public key was named "rotating backend
* public key". To match with the security concept of luca, it has been renamed.
*/
private Completable migrateDailyKeyPairPublicKey() {
Maybe<Integer> restoreId = preferencesManager.restoreIfAvailable(OLD_ROTATING_BACKEND_PUBLIC_KEY_ID_KEY, Integer.class);
......@@ -528,10 +528,10 @@ public class CryptoManager extends Manager {
.map(secret -> new Pair<>(startOfDayTimestamp, secret)));
}
private Observable<Long> generateRecentStartOfDayTimestamps(int days) {
public Observable<Long> generateRecentStartOfDayTimestamps(int days) {
return TimeUtil.getStartOfDayTimestamp()
.flatMapObservable(firstStartOfDayTimestamp -> Observable.range(0, days)
.map(dayIndex -> firstStartOfDayTimestamp - (dayIndex * TimeUnit.DAYS.toMillis(dayIndex))));
.map(dayIndex -> firstStartOfDayTimestamp - TimeUnit.DAYS.toMillis(dayIndex)));
}
/**
......
......@@ -20,6 +20,7 @@ import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.schedulers.Schedulers;
import okhttp3.CertificatePinner;
import okhttp3.Credentials;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
......@@ -39,7 +40,6 @@ public class NetworkManager extends Manager {
private Gson gson;
private Retrofit retrofit;
private OkHttpClient okHttpClient;
private LucaEndpointsV3 lucaEndpointsV3;
private ConnectivityManager connectivityManager;
......@@ -61,30 +61,7 @@ public class NetworkManager extends Manager {
.setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
.create();
Interceptor userAgentInterceptor = chain -> chain.proceed(chain.request()
.newBuilder()
.header("User-Agent", USER_AGENT)
.build());
CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add("**.luca-app.de", "sha256/wjD2X9ht0iXPN2sSXiXd2aF6ar5cxHOmXZnnkAiwVpU=") // CN=*.luca-app.de,O=neXenio GmbH,L=Berlin,ST=Berlin,C=DE,2.5.4.5=#130c43534d303233353532353339
.build();
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.callTimeout(10, TimeUnit.SECONDS)
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.addInterceptor(userAgentInterceptor)
.certificatePinner(certificatePinner);
if (BuildConfig.DEBUG) {
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
builder.addInterceptor(loggingInterceptor);
}
okHttpClient = builder.build();
OkHttpClient okHttpClient = createOkHttpClient();
retrofit = new Retrofit.Builder()
.baseUrl(API_BASE_URL)
......@@ -97,6 +74,40 @@ public class NetworkManager extends Manager {
});
}
@NonNull
private OkHttpClient createOkHttpClient() {
Interceptor userAgentInterceptor = chain -> chain.proceed(chain.request()
.newBuilder()
.header("User-Agent", USER_AGENT)
.build());
CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add("**.luca-app.de", "sha256/wjD2X9ht0iXPN2sSXiXd2aF6ar5cxHOmXZnnkAiwVpU=") // CN=*.luca-app.de,O=neXenio GmbH,L=Berlin,ST=Berlin,C=DE,2.5.4.5=#130c43534d303233353532353339
.build();
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.callTimeout(10, TimeUnit.SECONDS)
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.addInterceptor(userAgentInterceptor)
.certificatePinner(certificatePinner);
if (BuildConfig.DEBUG) {
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
builder.addInterceptor(loggingInterceptor);
builder.authenticator((route, response) -> {
String credential = Credentials.basic(BuildConfig.STAGING_API_USERNAME, BuildConfig.STAGING_API_PASSWORD);
return response.request().newBuilder()
.header("Authorization", credential)
.build();
});
}
return builder.build();
}
@Deprecated
public LucaEndpointsV3 getLucaEndpoints() {
return lucaEndpointsV3;
......
......@@ -8,6 +8,8 @@ import android.content.Context;
import android.content.ContextWrapper;
import android.content.DialogInterface;
import android.os.Bundle;
import android.text.method.LinkMovementMethod;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
......@@ -27,6 +29,13 @@ public class BaseDialogFragment extends DialogFragment {
this.builder = builder;
}
@Override
public void onStart() {
super.onStart();
TextView messageTextView = (TextView) getDialog().findViewById(android.R.id.message);
messageTextView.setMovementMethod(LinkMovementMethod.getInstance());
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return builder.create();
......
......@@ -114,7 +114,7 @@ public class HistoryFragment extends BaseFragment<HistoryViewModel> {
private void showShareHistoryConfirmationDialog() {
new BaseDialogFragment(new MaterialAlertDialogBuilder(getContext())
.setTitle(getString(R.string.history_share_confirmation_title))
.setMessage(getString(R.string.history_share_confirmation_description))
.setMessage(R.string.history_share_confirmation_description)
.setPositiveButton(R.string.history_share_confirmation_action, (dialogInterface, i) -> viewModel.onShareHistoryRequested())
.setNegativeButton(R.string.action_cancel, (dialogInterface, i) -> dialogInterface.cancel()))
.show();
......
......@@ -8,12 +8,14 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.ncorti.slidetoact.SlideToActView;
import de.culture4life.luca.R;
import de.culture4life.luca.ui.BaseFragment;
import de.culture4life.luca.ui.dialog.BaseDialogFragment;
import de.culture4life.luca.util.AccessibilityServiceUtil;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
......@@ -62,6 +64,7 @@ public class MeetingFragment extends BaseFragment<MeetingViewModel> {
if (!isHostingMeeting) {
navigationController.navigate(R.id.action_meetingFragment_to_qrCodeFragment);
}
AccessibilityServiceUtil.speak(getContext(), getString(R.string.meeting_was_ended_hint));
});
observe(viewModel.getQrCode(), value -> qrCodeImageView.setImageBitmap(value));
observe(viewModel.getIsLoading(), loading -> loadingView.setVisibility(loading ? View.VISIBLE : View.GONE));
......@@ -70,6 +73,13 @@ public class MeetingFragment extends BaseFragment<MeetingViewModel> {
meetingDescriptionInfoImageView.setOnClickListener(v -> showMeetingDescriptionInfo());
meetingMembersInfoImageView.setOnClickListener(v -> showMeetingMembersInfo());
slideToActView.setOnSlideCompleteListener(view -> viewModel.onMeetingEndRequested());
slideToActView.setOnSlideUserFailedListener((view, isOutside) -> {
if (AccessibilityServiceUtil.isGoogleTalkbackActive(getContext())) {
viewModel.onMeetingEndRequested();
} else {
Toast.makeText(getContext(), R.string.venue_slider_clicked, Toast.LENGTH_SHORT).show();
}
});
slideToActView.setReversed(true);
observe(viewModel.getIsLoading(), loading -> {
......@@ -77,7 +87,6 @@ public class MeetingFragment extends BaseFragment<MeetingViewModel> {
slideToActView.resetSlider();
}
});
}));
}
......
......@@ -5,6 +5,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.common.util.concurrent.ListenableFuture;
import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Size;
......@@ -17,6 +18,7 @@ import android.widget.TextView;
import de.culture4life.luca.BuildConfig;
import de.culture4life.luca.R;
import de.culture4life.luca.ui.BaseFragment;
import de.culture4life.luca.ui.ViewError;
import de.culture4life.luca.ui.dialog.BaseDialogFragment;
import de.culture4life.luca.ui.registration.RegistrationActivity;
......@@ -25,7 +27,6 @@ import java.util.concurrent.Executors;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.camera.core.Camera;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.Preview;
......@@ -45,6 +46,8 @@ public class QrCodeFragment extends BaseFragment<QrCodeViewModel> {
private PreviewView cameraPreviewView;
private View loadingView;
private TextView qrCodeCaptionTextView;
private TextView headingTextView;
private TextView subHeadingTextView;
private TextView nameTextView;
private TextView addressTextView;
......@@ -64,6 +67,8 @@ public class QrCodeFragment extends BaseFragment<QrCodeViewModel> {
qrCodeImageView = view.findViewById(R.id.qrCodeImageView);
cameraPreviewView = view.findViewById(R.id.cameraPreviewView);
loadingView = view.findViewById(R.id.loadingLayout);
headingTextView = view.findViewById(R.id.headingTextView);
subHeadingTextView = view.findViewById(R.id.subHeadingTextView);
qrCodeCaptionTextView = view.findViewById(R.id.qrCodeCaptionTextView);
......@@ -174,6 +179,30 @@ public class QrCodeFragment extends BaseFragment<QrCodeViewModel> {
dialogFragment.show();
}
private void showCameraDialog() {
showCameraDialog(false);
}
private void showCameraDialog(boolean directToSettings) {
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getContext())
.setTitle(R.string.camera_access_title)
.setMessage(R.string.camera_access_description)
.setNegativeButton(R.string.action_cancel, (dialog, which) -> dialog.cancel());
if (directToSettings) {
builder = builder.setPositiveButton(R.string.action_settings, (dialog, which) -> {
application.openAppSettings();
dialog.dismiss();
});
} else {
builder = builder.setPositiveButton(R.string.action_enable, (dialog, which) -> {
showCameraPreview();
dialog.dismiss();
});
}
new BaseDialogFragment(builder).show();
}
private void showJoinPrivateMeetingDialog(@NonNull String privateMeetingUrl) {
BaseDialogFragment dialogFragment = new BaseDialogFragment(new MaterialAlertDialogBuilder(getContext())
.setTitle(R.string.meeting_join_heading)
......@@ -195,7 +224,7 @@ public class QrCodeFragment extends BaseFragment<QrCodeViewModel> {
private void toggleCameraPreview() {
if (cameraPreviewDisposable == null) {
showCameraPreview();
showCameraDialog();
} else {
hideCameraPreview();
}
......@@ -206,7 +235,9 @@ public class QrCodeFragment extends BaseFragment<QrCodeViewModel> {
.doOnComplete(() -> {
cameraPreviewView.setVisibility(View.VISIBLE);
qrCodeImageView.setVisibility(View.GONE);
cameraToggleButton.setText(R.string.show_qr_code);
cameraToggleButton.setText(R.string.close_qr_code_scanner);
headingTextView.setText(R.string.scan_qr_code);
subHeadingTextView.setText(R.string.scan_qr_code_description);