Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved QRCode design #3743

Merged
merged 41 commits into from
May 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
5444269
moved some functions to qrcode tabs
Mar 4, 2020
dba05b0
added scan button in toolbar
Mar 4, 2020
80ea5f9
Added embedded scan (WIP)
Mar 5, 2020
a7c571c
scan ulitity
Mar 5, 2020
3c9c995
added permission request
Mar 6, 2020
5116c3b
scanner works
hypercubestart Mar 6, 2020
4684371
beep sound and flashlight for embedded qr camera
hypercubestart Mar 8, 2020
8904e1d
add files from merge commit
hypercubestart Mar 14, 2020
3bcdf63
moved import qrcode item to overflow for QRCodeTabs
hypercubestart Mar 14, 2020
b04c7ca
qr_code redesign
hypercubestart Mar 24, 2020
0b22c75
minor fixes
hypercubestart Mar 24, 2020
bcd27b9
hook menu item with new QRCodeTabs and fix qrcode generation
hypercubestart Mar 24, 2020
092365a
removed unused
hypercubestart Mar 24, 2020
647138a
change test name
hypercubestart Mar 25, 2020
f43aa17
fix NPE bug
hypercubestart Mar 29, 2020
5b8ecba
add analytics and minor issues
hypercubestart Mar 29, 2020
2eec648
update to viewpager2
hypercubestart Mar 31, 2020
01f9cb2
styling
hypercubestart Mar 31, 2020
d7d548a
review
hypercubestart Apr 4, 2020
f16c57e
address review comments
hypercubestart Apr 5, 2020
817ee55
update flash button to be materialbutton
hypercubestart Apr 5, 2020
9718beb
refactor into preferences.qr
hypercubestart Apr 8, 2020
cb0adfa
QrCodeFragmentAction test
pedrop30 Apr 14, 2020
29356d7
fixed test
hypercubestart Apr 15, 2020
9f3a5d5
ShowQRFragmentAction test
pedrop30 Apr 15, 2020
f341ca3
QRCodeTabsActivityPage
hypercubestart Apr 15, 2020
9658e21
fix bug share qr code doesnt work if qr code not generated yet
hypercubestart Apr 15, 2020
8c5a962
fix tests
hypercubestart Apr 28, 2020
a544327
remove copyright
hypercubestart Apr 28, 2020
6a4b27e
fix tests
hypercubestart May 6, 2020
d2c4bf0
style
hypercubestart May 6, 2020
4959cba
update qrcodeutilstest
hypercubestart May 6, 2020
4005fe2
comments
hypercubestart May 6, 2020
644f357
rule fixing
hypercubestart May 8, 2020
ee35d8a
merge
hypercubestart May 8, 2020
9fb6b6d
fix RuleChain
hypercubestart May 8, 2020
ee53a50
fix
hypercubestart May 8, 2020
b267f65
remove @rule
hypercubestart May 9, 2020
65e9dff
dark color qr code tabs
hypercubestart May 11, 2020
1810f6c
continuous qr code + handle import qrcode
hypercubestart May 11, 2020
5318fc1
fix typo
hypercubestart May 12, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions collect_app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ dependencies {

// Android Architecture Components:
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
implementation "androidx.viewpager2:viewpager2:1.0.0"

// Dagger:
implementation "com.google.dagger:dagger:${rootProject.daggerVersion}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
import org.odk.collect.android.preferences.AdminSharedPreferences;
import org.odk.collect.android.preferences.GeneralSharedPreferences;
import org.odk.collect.android.preferences.PreferenceSaver;
import org.odk.collect.android.preferences.qr.ObservableQRCodeGenerator;
import org.odk.collect.android.preferences.qr.QRCodeGenerator;
import org.odk.collect.android.utilities.QRCodeUtils;

import java.io.IOException;
Expand All @@ -52,6 +54,7 @@
public class QrCodeTest {

private final GeneralSharedPreferences preferences = GeneralSharedPreferences.getInstance();
private final QRCodeGenerator qrCodeGenerator = new ObservableQRCodeGenerator();

@Test
public void importSettingsFromQrCode() throws JSONException, IOException, WriterException, DataFormatException, ChecksumException, NotFoundException, FormatException {
Expand All @@ -73,7 +76,7 @@ public void importSettingsFromQrCode() throws JSONException, IOException, Writer

// generate QrCode
final AtomicReference<Bitmap> generatedBitmap = new AtomicReference<>();
QRCodeUtils.getQRCodeGeneratorObservable(new ArrayList<>())
qrCodeGenerator.generateQRCode(new ArrayList<>())
.subscribe(generatedBitmap::set, Timber::e);

assertNotNull(generatedBitmap.get());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package org.odk.collect.android.preferences.qr;

import android.Manifest;
import android.app.Activity;
import android.app.Instrumentation;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;

import com.google.zxing.WriterException;

import androidx.core.content.FileProvider;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.espresso.intent.Intents;
import androidx.test.espresso.intent.rule.IntentsTestRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.rule.GrantPermissionRule;
import dagger.Provides;
import io.reactivex.Observable;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.runner.RunWith;
import org.odk.collect.android.BuildConfig;
import org.odk.collect.android.R;
import org.odk.collect.android.activities.MainMenuActivity;
import org.odk.collect.android.injection.config.AppDependencyModule;
import org.odk.collect.android.storage.StoragePathProvider;
import org.odk.collect.android.storage.StorageSubdirectory;
import org.odk.collect.android.support.ResetStateRule;
import org.odk.collect.android.support.pages.MainMenuPage;
import org.odk.collect.android.utilities.FileUtils;

import java.io.File;
import java.io.IOException;
import java.util.Collection;

import static androidx.test.espresso.intent.Intents.intended;
import static androidx.test.espresso.intent.Intents.intending;
import static androidx.test.espresso.intent.matcher.BundleMatchers.hasEntry;
import static androidx.test.espresso.intent.matcher.IntentMatchers.hasAction;
import static androidx.test.espresso.intent.matcher.IntentMatchers.hasExtraWithKey;
import static androidx.test.espresso.intent.matcher.IntentMatchers.hasExtras;
import static androidx.test.espresso.intent.matcher.IntentMatchers.hasType;
import static androidx.test.espresso.intent.matcher.IntentMatchers.isInternal;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;

import static org.junit.Assert.assertTrue;


@RunWith(AndroidJUnit4.class)
public class ConfigureWithQRCodeTest {
// drawable resource that will act as "qr code" in this test
private static final int CHECKER_BACKGROUND_DRAWABLE_ID = R.drawable.checker_background;

public IntentsTestRule<MainMenuActivity> rule = new IntentsTestRule<>(MainMenuActivity.class);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've found recently that we could run into ordering problems with rules so it'd be best if this rule was also part of the RuleChain below. Just remove the @Rule and add around(rule) to the end of the chain.


@Rule
public RuleChain copyFormChain = RuleChain
.outerRule(GrantPermissionRule.grant(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_PHONE_STATE
))
.around(new ResetStateRule(new AppDependencyModule() {
@Override
@Provides
public QRCodeGenerator providesQRCodeGenerator() {
return new StubQRCodeGenerator();
}
}))
.around(rule);

@Before
public void stubAllExternalIntents() {
// By default Espresso Intents does not stub any Intents. Stubbing needs to be setup before
// every test run. In this case all external Intents will be blocked.
intending(not(isInternal())).respondWith(new Instrumentation.ActivityResult(Activity.RESULT_OK, null));
}

@Test
public void onMainMenu_clickConfigureQRCode_andClickingOnScan_opensScanner() {
new MainMenuPage(rule)
.assertOnPage()
.clickOnMenu()
.clickConfigureQR()
.clickScanFragment()
.checkIsIdDisplayed(R.id.zxing_barcode_surface);
}

@Test
public void onMainMenu_clickConfigureQRCode_andClickingOnView_showsQRCode() {
new MainMenuPage(rule)
.assertOnPage()
.clickOnMenu()
.clickConfigureQR()
.clickViewQRFragment()
.assertImageViewShowsImage(R.id.ivQRcode, BitmapFactory.decodeResource(
ApplicationProvider.getApplicationContext().getResources(),
CHECKER_BACKGROUND_DRAWABLE_ID
));
}

@Test
public void onMainMenu_clickConfigureQRCode_andClickingOnImportQRCode_startsExternalImagePickerIntent() {
new MainMenuPage(rule)
.assertOnPage()
.clickOnMenu()
.clickConfigureQR()
.clickOnMenu()
.clickOnString(R.string.import_qrcode_sd);

intended(hasAction(Intent.ACTION_PICK));
intended(hasType("image/*"));
}

@Test
public void onMainMenu_clickConfigureQRCode_andClickingOnShareQRCode_startsExternalShareIntent() {
String path = new StubQRCodeGenerator().getQrCodeFilepath();
Uri expected = FileProvider.getUriForFile(ApplicationProvider.getApplicationContext(),
BuildConfig.APPLICATION_ID + ".provider",
new File(path));

new MainMenuPage(rule)
.assertOnPage()
.clickOnMenu()
.clickConfigureQR()
.clickOnId(R.id.menu_item_share);

// should be two Intents, 1. to QRCodeTabsActivity 2. Share QR Code Intent
assertThat(Intents.getIntents().size(), equalTo(2));
Intent receivedIntent = Intents.getIntents().get(1);
assertThat(receivedIntent, hasAction(Intent.ACTION_CHOOSER));

// test title
assertThat(receivedIntent, hasExtras(hasEntry(Intent.EXTRA_TITLE,
ApplicationProvider.getApplicationContext().getString(R.string.share_qrcode))));

// test SEND intent
assertThat(receivedIntent, hasExtraWithKey(Intent.EXTRA_INTENT));
Intent sendIntent = receivedIntent.getParcelableExtra(Intent.EXTRA_INTENT);

assertThat(sendIntent, hasAction(Intent.ACTION_SEND));
assertThat(sendIntent, hasType("image/*"));
assertThat(sendIntent, hasExtras(hasEntry(Intent.EXTRA_STREAM, expected)));

// test that file stream is valid by checking that file exists
File qrcodeFile = new File(path);
assertTrue(qrcodeFile.exists());
}

// StubQRCodeGenerator is a class that is injected during this test
// to verify that the QRCode is generated and shown to user correctly
private static class StubQRCodeGenerator implements QRCodeGenerator {
@Override
public Bitmap generateQRBitMap(String data, int sideLength) throws IOException, WriterException {
// don't use this in this test, so okay to return null
return null;
}

@Override
public Observable<Bitmap> generateQRCode(Collection<String> selectedPasswordKeys) {
return Observable.create(emitter -> {
Bitmap bitmap =
BitmapFactory.decodeResource(
ApplicationProvider.getApplicationContext().getResources(),
CHECKER_BACKGROUND_DRAWABLE_ID);
emitter.onNext(bitmap);

// save bitmap to test that shareQRCode generates bitmap if file not there
FileUtils.saveBitmapToFile(bitmap, getQrCodeFilepath());
emitter.onComplete();
});
}

@Override
public String getQrCodeFilepath() {
return new StoragePathProvider().getDirPath(StorageSubdirectory.SETTINGS) + File.separator + "test-collect-settings.png";
}

@Override
public String getMd5CachePath() {
// don't use this in this test, so okay to return null
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ public AdminSettingsPage clickAdminSettings() {
return new AdminSettingsPage(rule).assertOnPage();
}

public QRCodeTabsActivityPage clickConfigureQR() {
clickOnString(R.string.configure_via_qr_code);
return new QRCodeTabsActivityPage(rule).assertOnPage();
}

public FillBlankFormPage clickFillBlankForm() {
onView(withId(R.id.enter_data)).perform(click());
return new FillBlankFormPage(rule).assertOnPage();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package org.odk.collect.android.support.pages;

import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.view.View;
import android.widget.ImageView;

import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.odk.collect.android.R;
import org.odk.collect.android.support.ActivityHelpers;

import androidx.test.espresso.Espresso;
import androidx.test.espresso.matcher.BoundedMatcher;
import androidx.test.rule.ActivityTestRule;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;

public class QRCodeTabsActivityPage extends Page<QRCodeTabsActivityPage> {
public QRCodeTabsActivityPage(ActivityTestRule rule) {
super(rule);
}

@Override
public QRCodeTabsActivityPage assertOnPage() {
checkIsStringDisplayed(R.string.configure_via_qr_code);
return this;
}

public QRCodeTabsActivityPage clickScanFragment() {
onView(withText(R.string.scan_qr_code_fragment_title)).perform(click());
return this;
}

public QRCodeTabsActivityPage clickViewQRFragment() {
onView(withText(R.string.view_qr_code_fragment_title)).perform(click());
return this;
}

public QRCodeTabsActivityPage assertImageViewShowsImage(int resourceid, Bitmap image) {
onView(withId(resourceid)).check(matches(DrawableMatcher.withBitmap(image)));
return this;
}

public QRCodeTabsActivityPage clickOnMenu() {
Espresso.openActionBarOverflowOrOptionsMenu(ActivityHelpers.getActivity());
return this;
}

// Matcher class to match the contents of a ImageView and compare with a bitmap
private static class DrawableMatcher {
private static Matcher<View> withBitmap(Bitmap match) {
return new BoundedMatcher<View, ImageView>(ImageView.class) {
@Override
public void describeTo(Description description) {
description.appendText("bitmaps did not match");
}

@Override
protected boolean matchesSafely(ImageView imageView) {
Drawable drawable = imageView.getDrawable();
if (drawable == null && match == null) {
return true;
} else if (drawable != null && match == null) {
return false;
} else if (drawable == null && match != null) {
return false;
}

Bitmap actual = ((BitmapDrawable) drawable).getBitmap();

return actual.sameAs(match);
}
};
}
}

}
4 changes: 1 addition & 3 deletions collect_app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,6 @@ the specific language governing permissions and limitations under the License.
android:screenOrientation="portrait"
android:stateNotNeeded="true"
android:windowSoftInputMode="stateAlwaysHidden" />
<activity android:name=".activities.ScanQRCodeActivity"
android:screenOrientation="portrait"
/>
<activity
android:name=".activities.FormEntryActivity"
android:windowSoftInputMode="adjustResize" />
Expand Down Expand Up @@ -130,6 +127,7 @@ the specific language governing permissions and limitations under the License.
android:configChanges="orientation|screenSize" />
<activity android:name=".activities.InstanceUploaderActivity" />
<activity android:name=".activities.AboutActivity" />
<activity android:name=".preferences.qr.QRCodeTabsActivity" />
<activity android:name=".preferences.PreferencesActivity" />
<activity android:name=".preferences.AdminPreferencesActivity" />
<activity android:name=".preferences.AndroidXPreferencesActivity" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import org.odk.collect.android.preferences.GeneralSharedPreferences;
import org.odk.collect.android.preferences.PreferenceSaver;
import org.odk.collect.android.preferences.PreferencesActivity;
import org.odk.collect.android.preferences.qr.QRCodeTabsActivity;
import org.odk.collect.android.preferences.Transport;
import org.odk.collect.android.provider.InstanceProviderAPI.InstanceColumns;
import org.odk.collect.android.storage.StorageInitializer;
Expand Down Expand Up @@ -371,7 +372,7 @@ public boolean onOptionsItemSelected(MenuItem item) {
args.putSerializable(AdminPasswordDialogFragment.ARG_ACTION, Action.SCAN_QR_CODE);
showIfNotShowing(AdminPasswordDialogFragment.class, args, getSupportFragmentManager());
} else {
startActivity(new Intent(this, ScanQRCodeActivity.class));
startActivity(new Intent(this, QRCodeTabsActivity.class));
}
return true;
case R.id.menu_about:
Expand Down Expand Up @@ -556,7 +557,7 @@ public void onCorrectAdminPassword(Action action) {

break;
case SCAN_QR_CODE:
startActivity(new Intent(this, ScanQRCodeActivity.class));
startActivity(new Intent(this, QRCodeTabsActivity.class));
break;
}
}
Expand Down
Loading