diff --git a/.gitignore b/.gitignore index becdedd3e..1f5fa78bc 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,6 @@ UserInterfaceState.xcuserstate /app/build *apk +.project +local.properties +.tool-versions diff --git a/.travis.yml b/.travis.yml index 6deb2eee7..fa8464126 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,11 +16,12 @@ cache: - "${TRAVIS_BUILD_DIR}/gradle/wrapper/dists/" - "$HOME/.gradle/caches/" - "$HOME/.gradle/wrapper/" +before_script: + - bash scripts/prep-key.sh script: - ./gradlew assemblePlayStoreRelease - ./gradlew assembleFdroidRelease - ./gradlew testPlayStoreReleaseUnitTestCoverage after_success: - bash <(curl -s https://codecov.io/bash) - - bash scripts/prep-key.sh - bash scripts/update-apk.sh diff --git a/README.md b/README.md index 6c36126fa..6f7c6ede8 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ ![Open Event Organizer](docs/images/organizer_app_branding.png) -## Open Event Organizer App +## Open Event Organizer Android App -[![Build Status](https://img.shields.io/travis/fossasia/open-event-orga-app/development.svg)](https://travis-ci.org/fossasia/open-event-orga-app) -[![Codacy Grade](https://img.shields.io/codacy/grade/d6ae120356c94c0d94d6449ec540f520.svg)](https://www.codacy.com/app/mb/open-event-orga-app) -[![Codecov Coverage](https://img.shields.io/codecov/c/github/fossasia/open-event-orga-app/development.svg)](https://codecov.io/gh/fossasia/open-event-orga-app) +[![Build Status](https://img.shields.io/travis/fossasia/open-event-organizer-android/development.svg)](https://travis-ci.org/fossasia/open-event-organizer-android) +[![Codacy Grade](https://img.shields.io/codacy/grade/d6ae120356c94c0d94d6449ec540f520.svg)](https://www.codacy.com/app/mb/open-event-organizer-android) +[![Codecov Coverage](https://img.shields.io/codecov/c/github/fossasia/open-event-organizer-android/development.svg)](https://codecov.io/gh/fossasia/open-event-organizer-android) [![Appetize Preview](https://img.shields.io/badge/Preview-appetize.io-673AB7.svg)](https://appetize.io/app/w8v8z7pc9aewargb2uuyf108f0) [![Gitter](https://img.shields.io/badge/chat-on%20gitter-ff006f.svg)](https://gitter.im/fossasia/open-event-orga-app) [![Twitter Follow](https://img.shields.io/twitter/follow/eventyay.svg?style=social&label=Follow&maxAge=2592000?style=flat-square)](https://twitter.com/eventyay) diff --git a/app/build.gradle b/app/build.gradle index f3adaa8ff..f389bf1b4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,10 +4,13 @@ apply plugin: 'com.noveogroup.android.check' apply from: './jacoco.gradle' apply plugin: 'com.google.gms.oss.licenses.plugin' apply plugin: "net.ltgt.errorprone" +apply plugin: "com.github.b3er.local.properties" def app_name = System.getenv('app_name') ?: "Eventyay Organizer" def GOOGLE_PLACES_API_KEY = System.getenv('GOOGLE_PLACES_API_KEY') ?: "YOUR_API_KEY" +def LOCAL_KEY_PRESENT = project.hasProperty('SIGNING_KEY_FILE') && rootProject.file(SIGNING_KEY_FILE).exists() + android { lintOptions { warning 'InvalidPackage' @@ -24,8 +27,8 @@ android { applicationId "com.eventyay.organizer" minSdkVersion versions.minSdk targetSdkVersion versions.targetSdk - versionCode 13 - versionName "1.3.0" + versionCode 14 + versionName "1.4.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true manifestPlaceholders = [ @@ -33,6 +36,25 @@ android { GOOGLE_PLACES_API_KEY: GOOGLE_PLACES_API_KEY ] } + + signingConfigs { + if (TRAVIS_BUILD) { + release { + storeFile KEYSTORE_FILE + storePassword System.getenv("STORE_PASS") + keyAlias System.getenv("ALIAS") + keyPassword System.getenv("KEY_PASS") + } + } else if (LOCAL_KEY_PRESENT) { + release { + storeFile rootProject.file(SIGNING_KEY_FILE) + storePassword STORE_PASS + keyAlias ALIAS + keyPassword KEY_PASS + } + } + } + buildTypes { release { minifyEnabled true @@ -40,16 +62,18 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' buildConfigField "String", "DEFAULT_BASE_URL", '"https://api.eventyay.com/v1/"' - buildConfigField 'Boolean', 'HIDE_DRAWER_ITEMS', 'true' resValue "string", "default_base_url", '"https://api.eventyay.com/v1/"' + + if (LOCAL_KEY_PRESENT || TRAVIS_BUILD) + signingConfig signingConfigs.release } debug { testCoverageEnabled = true buildConfigField "String", "DEFAULT_BASE_URL", '"https://open-event-api-dev.herokuapp.com/v1/"' - buildConfigField 'Boolean', 'HIDE_DRAWER_ITEMS', 'false' resValue "string", "default_base_url", '"https://open-event-api-dev.herokuapp.com/v1/"' } } + dataBinding { enabled true } @@ -117,9 +141,9 @@ dependencies { // Support Lib implementation 'androidx.multidex:multidex:2.0.1' - implementation "androidx.appcompat:appcompat:1.1.0-beta01" + implementation "androidx.appcompat:appcompat:1.1.0-rc01" implementation "androidx.cardview:cardview:1.0.0" - implementation "androidx.recyclerview:recyclerview:1.1.0-alpha06" + implementation "androidx.recyclerview:recyclerview:1.1.0-beta01" implementation "com.google.android.material:material:1.1.0-alpha07" implementation "androidx.browser:browser:1.0.0" implementation "androidx.palette:palette:1.0.0" @@ -127,8 +151,8 @@ dependencies { //implementation "androidx.media:media:1.1.0-alpha04" // GradleIncompatible Workaround //implementation "androidx.legacy:legacy-support-v4:1.0.0" // GradleIncompatible Workaround annotationProcessor 'androidx.annotation:annotation:1.1.0' // Required for Glide - playStoreImplementation "com.google.android.gms:play-services-vision:${versions.play_services}" - playStoreImplementation "com.google.android.gms:play-services-places:${versions.play_services}" + playStoreImplementation "com.google.android.gms:play-services-vision:${versions.play_services_vision}" + playStoreImplementation "com.google.android.libraries.places:places:${versions.places}" implementation 'androidx.constraintlayout:constraintlayout:1.1.3' // Misc @@ -140,10 +164,10 @@ dependencies { compileOnly 'com.google.code.findbugs:findbugs-annotations:3.0.1' // Lifecycle - implementation "androidx.lifecycle:lifecycle-runtime:2.2.0-alpha01" - implementation "androidx.lifecycle:lifecycle-extensions:2.2.0-alpha01" - implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0-alpha01" - annotationProcessor "androidx.lifecycle:lifecycle-compiler:2.2.0-alpha01" + implementation "androidx.lifecycle:lifecycle-runtime:2.2.0-alpha02" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0-alpha02" + implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0-alpha02" + annotationProcessor "androidx.lifecycle:lifecycle-compiler:2.2.0-alpha02" //WorkManager implementation "android.arch.work:work-runtime:${versions.work_manager}" @@ -166,7 +190,7 @@ dependencies { implementation "com.squareup.retrofit2:retrofit:${versions.retrofit}" implementation "com.squareup.retrofit2:converter-jackson:${versions.retrofit}" implementation "com.squareup.retrofit2:adapter-rxjava2:${versions.retrofit}" - implementation 'com.squareup.okhttp3:logging-interceptor:3.14.2' + implementation 'com.squareup.okhttp3:logging-interceptor:4.0.0' implementation 'com.github.jasminb:jsonapi-converter:0.9' // RxJava @@ -175,10 +199,7 @@ dependencies { implementation 'com.f2prateek.rx.preferences2:rx-preferences:2.0.0' // Fast Adapter - implementation('com.mikepenz:fastadapter:3.0.4@aar') { - transitive = true - } - implementation 'com.mikepenz:fastadapter-commons:3.3.1@aar' + implementation "com.mikepenz:fastadapter:4.0.1" implementation 'com.timehop.stickyheadersrecyclerview:library:0.4.3' implementation 'me.xdrop:fuzzywuzzy:1.1.9' @@ -231,11 +252,14 @@ dependencies { //open-source licenses playStoreImplementation "com.google.android.gms:play-services-oss-licenses:${versions.play_services}" + // QR Code Scanner + fdroidImplementation 'com.github.blikoon:QRCodeScanner:0.1.2' + //Testing androidTestImplementation('androidx.test.espresso:espresso-core:3.2.0', { exclude group: 'com.android.support', module: 'support-annotations' }) - testImplementation("androidx.arch.core:core-testing:2.1.0-beta01", { + testImplementation("androidx.arch.core:core-testing:2.1.0-rc01", { exclude group: 'com.android.support', module: 'support-compat' exclude group: 'com.android.support', module: 'support-annotations' exclude group: 'com.android.support', module: 'support-core-utils' diff --git a/app/src/fdroid/java/com/eventyay/organizer/common/di/module/android/FlavorModule.java b/app/src/fdroid/java/com/eventyay/organizer/common/di/module/android/FlavorModule.java index ce4320f5e..12bf7a90f 100644 --- a/app/src/fdroid/java/com/eventyay/organizer/common/di/module/android/FlavorModule.java +++ b/app/src/fdroid/java/com/eventyay/organizer/common/di/module/android/FlavorModule.java @@ -1,7 +1,13 @@ package com.eventyay.organizer.common.di.module.android; +import com.eventyay.organizer.core.attendee.qrscan.ScanQRActivity; + import dagger.Module; +import dagger.android.ContributesAndroidInjector; @Module public abstract class FlavorModule { + + @ContributesAndroidInjector(modules = BarcodeFragmentBuildersModule.class) + abstract ScanQRActivity contributeScanQRActivity(); } diff --git a/app/src/fdroid/java/com/eventyay/organizer/core/attendee/ScanQRView.java b/app/src/fdroid/java/com/eventyay/organizer/core/attendee/ScanQRView.java new file mode 100644 index 000000000..456068b0c --- /dev/null +++ b/app/src/fdroid/java/com/eventyay/organizer/core/attendee/ScanQRView.java @@ -0,0 +1,26 @@ +package com.eventyay.organizer.core.attendee; + +import androidx.annotation.NonNull; + +import com.eventyay.organizer.data.attendee.Attendee; + +public interface ScanQRView { + + boolean hasCameraPermission(); + + void requestCameraPermission(); + + void showPermissionError(String error); + + void onScannedAttendee(Attendee attendee); + + void showBarcodePanel(boolean show); + + void showMessage(@NonNull int stringRes); + + void setTint(boolean matched); + + void showProgress(boolean show); + + void startScan(); +} diff --git a/app/src/fdroid/java/com/eventyay/organizer/core/attendee/ScanningDecider.java b/app/src/fdroid/java/com/eventyay/organizer/core/attendee/ScanningDecider.java index 1e24c0057..a07a7fa42 100644 --- a/app/src/fdroid/java/com/eventyay/organizer/core/attendee/ScanningDecider.java +++ b/app/src/fdroid/java/com/eventyay/organizer/core/attendee/ScanningDecider.java @@ -1,13 +1,16 @@ package com.eventyay.organizer.core.attendee; import android.content.Context; -import android.widget.Toast; +import android.content.Intent; -import com.eventyay.organizer.R; +import com.eventyay.organizer.core.main.MainActivity; public class ScanningDecider { public void openScanQRActivity(Context context, long eventId) { - Toast.makeText(context, R.string.scanning_disabled_message, Toast.LENGTH_SHORT).show(); + Intent intent = new Intent(context, com.eventyay.organizer.core.attendee.qrscan.ScanQRActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(MainActivity.EVENT_KEY, eventId); + context.startActivity(intent); } } diff --git a/app/src/fdroid/java/com/eventyay/organizer/core/attendee/qrscan/ScanQRActivity.java b/app/src/fdroid/java/com/eventyay/organizer/core/attendee/qrscan/ScanQRActivity.java new file mode 100644 index 000000000..3c5494686 --- /dev/null +++ b/app/src/fdroid/java/com/eventyay/organizer/core/attendee/qrscan/ScanQRActivity.java @@ -0,0 +1,231 @@ +package com.eventyay.organizer.core.attendee.qrscan; + +import android.Manifest; +import android.Manifest.permission; +import android.app.Activity; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.lifecycle.ViewModelProvider; +import androidx.lifecycle.ViewModelProviders; + +import android.widget.Button; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import com.blikoon.qrcodescanner.QrCodeActivity; +import com.eventyay.organizer.R; +import com.eventyay.organizer.core.attendee.ScanQRView; +import com.eventyay.organizer.core.attendee.checkin.AttendeeCheckInFragment; +import com.eventyay.organizer.core.main.MainActivity; +import com.eventyay.organizer.data.attendee.Attendee; +import com.eventyay.organizer.ui.ViewUtils; + +import javax.inject.Inject; + +import butterknife.BindView; +import butterknife.ButterKnife; +import dagger.android.AndroidInjection; +import dagger.android.AndroidInjector; +import dagger.android.DispatchingAndroidInjector; +import dagger.android.HasActivityInjector; +import dagger.android.support.DaggerAppCompatActivity; +import io.reactivex.disposables.CompositeDisposable; +import timber.log.Timber; + +import static com.eventyay.organizer.ui.ViewUtils.showView; + +@SuppressWarnings("PMD.TooManyMethods") +public class ScanQRActivity extends DaggerAppCompatActivity implements ScanQRView, HasActivityInjector { + + public static final int PERM_REQ_CODE = 123; + private static final int REQUEST_CODE_QR_SCAN = 101; + + @BindView(R.id.barcodePanel) + TextView barcodePanel; + + @BindView(R.id.progressBar) + ProgressBar progressBar; + + @BindView(R.id.startScanningAgain) + Button startScanningAgain; + + @Inject + ViewModelProvider.Factory viewModelFactory; + + @Inject + DispatchingAndroidInjector dispatchingAndroidInjector; + + private ScanQRViewModel scanQRViewModel; + + private final CompositeDisposable compositeDisposable = new CompositeDisposable(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + AndroidInjection.inject(this); + super.onCreate(savedInstanceState); + scanQRViewModel = ViewModelProviders.of(this, viewModelFactory).get(ScanQRViewModel.class); + + setContentView(R.layout.activity_scan_qr); + + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + + ButterKnife.bind(this); + + onCameraLoaded(); + + startScanningAgain.setOnClickListener(v -> startScan()); + } + + @Override + protected void onStart() { + super.onStart(); + + long eventId = getIntent().getLongExtra(MainActivity.EVENT_KEY, -1); + if (eventId == -1) { + Timber.d("No Event ID provided. Exiting ..."); + finish(); + return; + } + + scanQRViewModel.getProgress().observe(this, this::showProgress); + scanQRViewModel.getError().observe(this, this::showPermissionError); + scanQRViewModel.getMessage().observe(this, this::showMessage); + scanQRViewModel.getTint().observe(this, this::setTint); + scanQRViewModel.getShowBarcodePanelLiveData().observe(this, this::showBarcodePanel); + scanQRViewModel.getOnScannedAttendeeLiveData().observe(this, this::onScannedAttendee); + scanQRViewModel.loadAttendees(); + } + + @Override + protected void onPause() { + super.onPause(); + } + + @Override + protected void onResume() { + super.onResume(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + compositeDisposable.dispose(); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (requestCode != PERM_REQ_CODE) + return; + + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + cameraPermissionGranted(true); + } else { + cameraPermissionGranted(false); + } + } + + // View Implementation Start + + @Override + public boolean hasCameraPermission() { + return ContextCompat.checkSelfPermission(this, permission.CAMERA) == PackageManager.PERMISSION_GRANTED; + } + + @Override + public void requestCameraPermission() { + ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, PERM_REQ_CODE); + } + + @Override + public void showPermissionError(String error) { + Toast.makeText(this, error, Toast.LENGTH_SHORT).show(); + } + + @Override + public void onScannedAttendee(Attendee attendee) { + showToggleDialog(attendee.getId()); + } + + @Override + public void showBarcodePanel(boolean show) { + showView(barcodePanel, show); + } + + @Override + public void showMessage(int stringRes) { + barcodePanel.setText(getString(stringRes)); + } + + @Override + public void setTint(boolean matched) { + ViewUtils.setTint(barcodePanel, + ContextCompat.getColor(this, matched ? R.color.green_a400 : R.color.red_500) + ); + } + + @Override + public void showProgress(boolean show) { + showView(progressBar, show); + } + + public void onCameraLoaded() { + if (hasCameraPermission()) { + startScan(); + } else { + requestCameraPermission(); + } + } + + public void cameraPermissionGranted(boolean granted) { + if (granted) { + startScan(); + } else { + showProgress(false); + showPermissionError("User denied permission"); + } + } + + @Override + public void startScan() { + Intent i = new Intent(ScanQRActivity.this, QrCodeActivity.class); + startActivityForResult(i, REQUEST_CODE_QR_SCAN); + } + + public void onActivityResult(int requestCode, int resultCode, Intent intent) { + + if (requestCode == REQUEST_CODE_QR_SCAN) { + if (intent == null) + return; + + scanQRViewModel.processBarcode(intent.getStringExtra + ("com.blikoon.qrcodescanner.got_qr_scan_relult")); + + } else { + super.onActivityResult(requestCode, resultCode, intent); + } + } + + public void showToggleDialog(long attendeeId) { + AttendeeCheckInFragment bottomSheetDialogFragment = AttendeeCheckInFragment.newInstance(attendeeId); + bottomSheetDialogFragment.show(getSupportFragmentManager(), bottomSheetDialogFragment.getTag()); + bottomSheetDialogFragment.setOnCancelListener(() -> { + ViewUtils.setTint(barcodePanel, ContextCompat.getColor(this, R.color.light_blue_a400)); + showBarcodePanel(false); + startScan(); + }); + } + + @Override + public AndroidInjector activityInjector() { + return dispatchingAndroidInjector; + } +} diff --git a/app/src/main/java/com/eventyay/organizer/core/event/create/EventDetailsStepOne.java b/app/src/fdroid/java/com/eventyay/organizer/core/event/create/EventDetailsStepOne.java similarity index 100% rename from app/src/main/java/com/eventyay/organizer/core/event/create/EventDetailsStepOne.java rename to app/src/fdroid/java/com/eventyay/organizer/core/event/create/EventDetailsStepOne.java diff --git a/app/src/main/java/com/eventyay/organizer/core/event/create/UpdateEventFragment.java b/app/src/fdroid/java/com/eventyay/organizer/core/event/create/UpdateEventFragment.java similarity index 97% rename from app/src/main/java/com/eventyay/organizer/core/event/create/UpdateEventFragment.java rename to app/src/fdroid/java/com/eventyay/organizer/core/event/create/UpdateEventFragment.java index f451bd9d7..51554420b 100644 --- a/app/src/main/java/com/eventyay/organizer/core/event/create/UpdateEventFragment.java +++ b/app/src/fdroid/java/com/eventyay/organizer/core/event/create/UpdateEventFragment.java @@ -14,6 +14,8 @@ import android.net.Uri; import android.os.Bundle; import androidx.annotation.Nullable; + +import com.eventyay.organizer.core.settings.PaymentPrefsFragment; import com.google.android.material.textfield.TextInputLayout; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; @@ -122,6 +124,13 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, startActivityForResult(richEditorIntent, RICH_TEXT_REQUEST); }); + binding.form.paymentPrefs.setOnClickListener(view -> { + getFragmentManager().beginTransaction() + .replace(R.id.fragment_container, PaymentPrefsFragment.newInstance()) + .addToBackStack(null) + .commit(); + }); + setupSpinners(); attachCountryList(createEventViewModel.getCountryList()); attachCurrencyCodesList(createEventViewModel.getCurrencyCodesList()); @@ -380,7 +389,7 @@ public void setEvent(Event event) { binding.form.enableSession.setChecked(event.isSessionsSpeakersEnabled); binding.form.enableTax.setChecked(event.isTaxEnabled); binding.form.ticketingDetails.setChecked(event.isTicketingEnabled); - binding.form.organizerInfo.setChecked(event.hasOrganizerInfo); + binding.form.organizerInfo.setChecked(event.hasOwnerInfo); binding.form.onlineEvent.setChecked(event.isEventOnline); } diff --git a/app/src/fdroid/res/layout/activity_scan_qr.xml b/app/src/fdroid/res/layout/activity_scan_qr.xml new file mode 100644 index 000000000..8a86f896e --- /dev/null +++ b/app/src/fdroid/res/layout/activity_scan_qr.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + +