diff --git a/app/build.gradle b/app/build.gradle index 58ca572d3..3606adc7a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,14 +29,14 @@ apply plugin: 'com.google.firebase.crashlytics' def props = loadProperties() android { - compileSdkVersion 31 - buildToolsVersion "31.0.0" + compileSdkVersion 32 + buildToolsVersion "32.0.0" //noinspection GroovyMissingReturnStatement defaultConfig { applicationId "be.ugent.zeus.hydra" minSdkVersion 21 - targetSdkVersion 31 + targetSdkVersion 32 versionCode 30400 versionName "3.0.4" vectorDrawables.useSupportLibrary = true @@ -179,7 +179,7 @@ dependencies { implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' - implementation 'com.google.android.material:material:1.5.0' + implementation 'com.google.android.material:material:1.6.0' implementation 'androidx.browser:browser:1.4.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation "androidx.lifecycle:lifecycle-common-java8:2.4.1" @@ -199,9 +199,11 @@ dependencies { storeImplementation 'com.google.android.gms:play-services-maps:18.0.2' storeImplementation 'com.google.firebase:firebase-analytics:20.1.2' storeImplementation 'com.google.firebase:firebase-crashlytics:18.2.10' + storeImplementation 'com.google.android.gms:play-services-code-scanner:16.0.0-beta1' // Dependencies for open version. openImplementation 'org.osmdroid:osmdroid-android:6.1.11' + openImplementation 'com.journeyapps:zxing-android-embedded:4.3.0' if (props.getProperty("hydra.debug.leaks").toBoolean()) { logger.info("Leak tracking enabled...") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0ff365abd..93fbb4e7e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ + + + + + + + + implements N static final String ONCE_DRAWER = "once_drawer"; private static final String TAG = "BaseActivity"; private static final String UFORA = "com.d2l.brightspace.student.android"; - private static final int ONBOARDING_REQUEST = 5; private static final String STATE_IS_ONBOARDING_OPEN = "state_is_onboarding_open"; private static final String FRAGMENT_MENU_ID = "backStack"; @@ -196,6 +197,9 @@ public class MainActivity extends BaseActivity implements N private static final String SHORTCUT_EVENTS = "events"; private static final String SHORTCUT_LIBRARIES = "libraries"; + private static final int ONBOARDING_REQUEST = 5; + private static final int PREFERENCES_REQUEST = 5693; + private ActionBarDrawerToggle toggle; private boolean isOnboardingOpen; @@ -277,6 +281,8 @@ public void onDrawerClosed(View drawerView) { } } }); + + updateMenuVisibility(); // If the instance is null, we must initialise a fragment, otherwise android does it for us. if (savedInstanceState == null) { @@ -302,6 +308,12 @@ public void onDrawerClosed(View drawerView) { binding.drawerLayout.openDrawer(GravityCompat.START); } } + + private void updateMenuVisibility() { + // Show Zeus-mode if enabled. + MenuItem item = binding.navigationView.getMenu().findItem(R.id.drawer_zeus); + item.setVisible(EnableManager.isZeusModeEnabled(this)); + } @Override protected void onPostCreate(@Nullable Bundle savedInstanceState) { @@ -335,7 +347,15 @@ private void selectDrawerItem(@NonNull MenuItem menuItem, @NavigationSource int // First check if it are settings, then we don't update anything. if (menuItem.getItemId() == R.id.drawer_pref) { binding.drawerLayout.closeDrawer(GravityCompat.START); - PreferenceActivity.start(this, null); + Intent preferenceIntent = PreferenceActivity.startIntent(this, null); + startActivityForResult(preferenceIntent, PREFERENCES_REQUEST); + return; + } + + if (menuItem.getItemId() == R.id.drawer_zeus) { + binding.drawerLayout.closeDrawer(GravityCompat.START); + Intent intent = new Intent(this, WpiActivity.class); + startActivity(intent); return; } @@ -551,6 +571,9 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { Log.w(TAG, "Onboarding failed, stop app."); finish(); } + } else if (requestCode == PREFERENCES_REQUEST) { + // Don't care about the actual status. + updateMenuVisibility(); } // We need to call this for the fragments to work properly. super.onActivityResult(requestCode, resultCode, data); diff --git a/app/src/main/java/be/ugent/zeus/hydra/common/converter/PairJsonAdapter.java b/app/src/main/java/be/ugent/zeus/hydra/common/converter/PairJsonAdapter.java new file mode 100644 index 000000000..c44b0c1fb --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/common/converter/PairJsonAdapter.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.common.converter; + +import android.util.Pair; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.squareup.moshi.*; + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * @author Niko Strijbol + */ +public class PairJsonAdapter extends JsonAdapter> { + + private final JsonAdapter leftAdapter; + private final JsonAdapter rightAdapter; + + public PairJsonAdapter(JsonAdapter leftAdapter, JsonAdapter rightAdapter) { + this.leftAdapter = leftAdapter; + this.rightAdapter = rightAdapter; + } + + @Nullable + @Override + public Pair fromJson(JsonReader reader) throws IOException { + if (reader.peek() == JsonReader.Token.NULL) { + return null; + } + reader.beginArray(); + L lValue = leftAdapter.fromJson(reader); + R rValue = rightAdapter.fromJson(reader); + reader.endArray(); + return Pair.create(lValue, rValue); + } + + @Override + public void toJson(@NonNull JsonWriter writer, @Nullable Pair value) throws IOException { + if (value == null) { + writer.nullValue(); + return; + } + writer.beginArray(); + leftAdapter.toJson(writer, value.first); + rightAdapter.toJson(writer, value.second); + writer.endArray(); + } + + public static class Factory implements JsonAdapter.Factory { + + @Nullable + @Override + public JsonAdapter create(@NonNull Type type, @NonNull Set annotations, @NonNull Moshi moshi) { + if (!annotations.isEmpty()) { + return null; // Annotations? This factory doesn't apply. + } + + if (!(type instanceof ParameterizedType)) { + return null; // No type parameter? This factory doesn't apply. + } + + ParameterizedType parameterizedType = (ParameterizedType) type; + if (parameterizedType.getRawType() != Pair.class) { + return null; // Not a pair? This factory doesn't apply. + } + + Type leftType = parameterizedType.getActualTypeArguments()[0]; + Type rightType = parameterizedType.getActualTypeArguments()[1]; + + JsonAdapter leftAdapter = moshi.adapter(leftType); + JsonAdapter rightAdapter = moshi.adapter(rightType); + + return new PairJsonAdapter<>(leftAdapter, rightAdapter).nullSafe(); + } + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/common/network/Endpoints.java b/app/src/main/java/be/ugent/zeus/hydra/common/network/Endpoints.java index 9fb7a431b..e1ff928dc 100644 --- a/app/src/main/java/be/ugent/zeus/hydra/common/network/Endpoints.java +++ b/app/src/main/java/be/ugent/zeus/hydra/common/network/Endpoints.java @@ -33,5 +33,8 @@ public interface Endpoints { String ZEUS_V1 = "https://hydra.ugent.be/api/1.0/"; String ZEUS_V2 = "https://hydra.ugent.be/api/2.0/"; + String TAP = "https://tap.zeus.gent/"; + String TAB = "https://tab.zeus.gent/"; + String LIBRARY = "https://widgets.lib.ugent.be/"; } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/zeus/hydra/common/network/InstanceProvider.java b/app/src/main/java/be/ugent/zeus/hydra/common/network/InstanceProvider.java index eacf6a153..29bbd008f 100644 --- a/app/src/main/java/be/ugent/zeus/hydra/common/network/InstanceProvider.java +++ b/app/src/main/java/be/ugent/zeus/hydra/common/network/InstanceProvider.java @@ -27,9 +27,7 @@ import java.io.File; -import be.ugent.zeus.hydra.common.converter.BooleanJsonAdapter; -import be.ugent.zeus.hydra.common.converter.DateThreeTenAdapter; -import be.ugent.zeus.hydra.common.converter.DateTypeConverters; +import be.ugent.zeus.hydra.common.converter.*; import com.squareup.moshi.Moshi; import okhttp3.Cache; import okhttp3.OkHttpClient; @@ -76,7 +74,6 @@ public static synchronized OkHttpClient getClient(Context context) { return getClient(cacheDir); } - @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) public static synchronized Moshi getMoshi() { if (moshi == null) { moshi = new Moshi.Builder() @@ -84,6 +81,7 @@ public static synchronized Moshi getMoshi() { .add(new DateThreeTenAdapter()) .add(new DateTypeConverters.GsonOffset()) .add(new DateTypeConverters.LocalZonedDateTimeInstance()) + .add(new PairJsonAdapter.Factory()) .build(); } return moshi; diff --git a/app/src/main/java/be/ugent/zeus/hydra/common/network/InvalidFormatException.java b/app/src/main/java/be/ugent/zeus/hydra/common/network/InvalidFormatException.java index 17c8ae7ba..1726017c5 100644 --- a/app/src/main/java/be/ugent/zeus/hydra/common/network/InvalidFormatException.java +++ b/app/src/main/java/be/ugent/zeus/hydra/common/network/InvalidFormatException.java @@ -27,9 +27,9 @@ /** * @author Niko Strijbol */ -class InvalidFormatException extends RequestException { +public class InvalidFormatException extends RequestException { - InvalidFormatException(String message, Throwable cause) { + public InvalidFormatException(String message, Throwable cause) { super(message, cause); } diff --git a/app/src/main/java/be/ugent/zeus/hydra/common/network/JsonOkHttpRequest.java b/app/src/main/java/be/ugent/zeus/hydra/common/network/JsonOkHttpRequest.java index 6e6eae840..8ec447337 100644 --- a/app/src/main/java/be/ugent/zeus/hydra/common/network/JsonOkHttpRequest.java +++ b/app/src/main/java/be/ugent/zeus/hydra/common/network/JsonOkHttpRequest.java @@ -37,15 +37,10 @@ import java.util.concurrent.TimeUnit; import be.ugent.zeus.hydra.common.arch.data.BaseLiveData; -import be.ugent.zeus.hydra.common.reporting.Reporting; -import be.ugent.zeus.hydra.common.reporting.Tracker; -import be.ugent.zeus.hydra.common.request.Request; import be.ugent.zeus.hydra.common.request.Result; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonDataException; -import com.squareup.moshi.Moshi; import okhttp3.CacheControl; -import okhttp3.OkHttpClient; import okhttp3.Response; /** @@ -66,16 +61,13 @@ * @author Niko Strijbol */ @SuppressWarnings("WeakerAccess") -public abstract class JsonOkHttpRequest implements Request { +public abstract class JsonOkHttpRequest extends OkHttpRequest { private static final String TAG = "JsonOkHttpRequest"; private static final String ALLOW_STALENESS = "be.ugent.zeus.hydra.data.staleness"; - private final Moshi moshi; - private final OkHttpClient client; private final Type typeToken; - private final Tracker tracker; /** * Construct a new request. As this constructor is not type-safe, it must only be used internally. @@ -84,10 +76,8 @@ public abstract class JsonOkHttpRequest implements Request { * @param token The type token of the return type. */ JsonOkHttpRequest(@NonNull Context context, @NonNull Type token) { - this.moshi = InstanceProvider.getMoshi(); - this.client = InstanceProvider.getClient(context); + super(context); this.typeToken = token; - this.tracker = Reporting.getTracker(context); } /** @@ -187,7 +177,8 @@ protected Result executeRequest(JsonAdapter adapter, @NonNull Bundle args) protected okhttp3.Request.Builder constructRequest(@NonNull Bundle arguments) { return new okhttp3.Request.Builder() .url(getAPIUrl()) - .cacheControl(constructCacheControl(arguments)); + .cacheControl(constructCacheControl(arguments)) + .addHeader("Accept", "application/json"); } protected CacheControl constructCacheControl(@NonNull Bundle arguments) { diff --git a/app/src/main/java/be/ugent/zeus/hydra/common/network/NetworkState.java b/app/src/main/java/be/ugent/zeus/hydra/common/network/NetworkState.java new file mode 100644 index 000000000..1ac3882d0 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/common/network/NetworkState.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.common.network; + +/** + * @author Niko Strijbol + */ +public enum NetworkState { + IDLE, BUSY +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/common/network/OkHttpRequest.java b/app/src/main/java/be/ugent/zeus/hydra/common/network/OkHttpRequest.java new file mode 100644 index 000000000..668403a09 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/common/network/OkHttpRequest.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2021 The Hydra authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.common.network; + +import android.content.Context; +import android.os.Bundle; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.annotation.WorkerThread; + +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.JsonDataException; +import com.squareup.moshi.Moshi; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.net.UnknownServiceException; +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import be.ugent.zeus.hydra.common.arch.data.BaseLiveData; +import be.ugent.zeus.hydra.common.reporting.Reporting; +import be.ugent.zeus.hydra.common.reporting.Tracker; +import be.ugent.zeus.hydra.common.request.Request; +import be.ugent.zeus.hydra.common.request.Result; +import okhttp3.CacheControl; +import okhttp3.OkHttpClient; +import okhttp3.Response; + +/** + * Common implementation for requests using OkHttp. + * + * @author Niko Strijbol + */ +@SuppressWarnings("WeakerAccess") +public abstract class OkHttpRequest implements Request { + + private static final String TAG = "OkHttpRequest"; + + protected final Moshi moshi; + protected final OkHttpClient client; + protected final Tracker tracker; + + /** + * Construct a new request. As this constructor is not type-safe, it must only be used internally. + * + * @param context The context. + */ + protected OkHttpRequest(@NonNull Context context) { + this.moshi = InstanceProvider.getMoshi(); + this.client = InstanceProvider.getClient(context); + this.tracker = Reporting.getTracker(context); + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/common/scanner/BarcodeScanner.java b/app/src/main/java/be/ugent/zeus/hydra/common/scanner/BarcodeScanner.java new file mode 100644 index 000000000..eb276f373 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/common/scanner/BarcodeScanner.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.common.scanner; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import androidx.annotation.Nullable; + +import java.util.function.Consumer; + +/** + * Ask some service to scan for barcodes. + * + * TODO: this is an ugly interface. + * + * @author Niko Strijbol + */ +public interface BarcodeScanner { + /** + * If this barcode scanner needs to launch an activity or not. + */ + boolean needsActivity(); + + /** + * Get an activity to launch, which will give the barcode + * as a result. + */ + Intent getActivityIntent(Activity activity); + + /** + * @return Get the request code to use when launching an activity. + */ + int getRequestCode(); + + /** + * Get the barcode from the activity launch from the intent from + * {@link #getActivityIntent(Activity)}. + * + * @param data The result data. + * + * @return The barcode, or null. + */ + @Nullable + String interpretActivityResult(Intent data, int resultCode); + + /** + * Get a barcode without activity. + * + * Implementations should optimize, if possible, for scanning product barcodes. + * This includes EAN/UPC codes. + */ + void getBarcode(Context context, Consumer onSuccess, Consumer onError); +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/common/ui/ShrinkExtendedFabBehavior.java b/app/src/main/java/be/ugent/zeus/hydra/common/ui/ShrinkExtendedFabBehavior.java new file mode 100644 index 000000000..d4ab3d9aa --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/common/ui/ShrinkExtendedFabBehavior.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * Copyright (C) 2022 Niko Strijbol + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package be.ugent.zeus.hydra.common.ui; + + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import androidx.annotation.NonNull; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.core.view.ViewCompat; + +import com.google.android.material.behavior.HideBottomViewOnScrollBehavior; +import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton; + +/** + * Shrink the extended FAB when scrolling down and extend it when scrolling up. + * Based on {@link HideBottomViewOnScrollBehavior}. + * + * @author Niko Strijbol + */ +public class ShrinkExtendedFabBehavior extends CoordinatorLayout.Behavior { + + private static final int STATE_SHRUNK = 1; + private static final int STATE_EXTENDED = 2; + + private int currentState = STATE_EXTENDED; + + public ShrinkExtendedFabBehavior() { + } + + public ShrinkExtendedFabBehavior(Context context, AttributeSet attrs) { + super(context, attrs); + } + + private boolean isScrolledDown() { + return currentState == STATE_SHRUNK; + } + + private boolean isScrolledUp() { + return currentState == STATE_EXTENDED; + } + + @Override + public boolean onStartNestedScroll( + @NonNull CoordinatorLayout coordinatorLayout, + @NonNull ExtendedFloatingActionButton child, + @NonNull View directTargetChild, + @NonNull View target, + int nestedScrollAxes, + int type) { + return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL; + } + + @Override + public void onNestedScroll( + @NonNull CoordinatorLayout coordinatorLayout, + @NonNull ExtendedFloatingActionButton child, + @NonNull View target, + int dxConsumed, + int dyConsumed, + int dxUnconsumed, + int dyUnconsumed, + int type, + @NonNull int[] consumed) { + if (dyConsumed > 0) { + shrink(child); + } else if (dyConsumed < 0) { + extend(child); + } + } + + public void extend(@NonNull ExtendedFloatingActionButton child) { + if (isScrolledUp()) { + return; + } + currentState = STATE_EXTENDED; + child.extend(); + } + + public void shrink(@NonNull ExtendedFloatingActionButton child) { + if (isScrolledDown()) { + return; + } + currentState = STATE_SHRUNK; + child.shrink(); + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/common/ui/SimpleTextWatcher.java b/app/src/main/java/be/ugent/zeus/hydra/common/ui/SimpleTextWatcher.java new file mode 100644 index 000000000..0e096c382 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/common/ui/SimpleTextWatcher.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.common.ui; + +import android.text.Editable; +import android.text.TextWatcher; + +/** + * @author Niko Strijbol + */ +public interface SimpleTextWatcher extends TextWatcher { + + @Override + default void beforeTextChanged(CharSequence s, int start, int count, int after) { + // Do nothing. + } + + @Override + default void afterTextChanged(Editable s) { + // Do nothing. + } + + @Override + default void onTextChanged(CharSequence s, int start, int before, int count) { + onTextChanged(s.toString()); + } + + void onTextChanged(String newText); +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/common/ui/recyclerview/adapters/AdapterUpdate.java b/app/src/main/java/be/ugent/zeus/hydra/common/ui/recyclerview/adapters/AdapterUpdate.java index cdbc3ced5..ab4c51129 100644 --- a/app/src/main/java/be/ugent/zeus/hydra/common/ui/recyclerview/adapters/AdapterUpdate.java +++ b/app/src/main/java/be/ugent/zeus/hydra/common/ui/recyclerview/adapters/AdapterUpdate.java @@ -45,7 +45,7 @@ interface AdapterUpdate { List getNewData(@Nullable List existingData); /** - * Apply the update to the update callback. At this point, the underlying data of the callback is alreay + * Apply the update to the update callback. At this point, the underlying data of the callback is already * updated to reflect the new data. * * @param listUpdateCallback The callback to update. diff --git a/app/src/main/java/be/ugent/zeus/hydra/common/ui/recyclerview/adapters/EqualsItemCallback.java b/app/src/main/java/be/ugent/zeus/hydra/common/ui/recyclerview/adapters/EqualsItemCallback.java index 002dd88ed..80e3fa75c 100644 --- a/app/src/main/java/be/ugent/zeus/hydra/common/ui/recyclerview/adapters/EqualsItemCallback.java +++ b/app/src/main/java/be/ugent/zeus/hydra/common/ui/recyclerview/adapters/EqualsItemCallback.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 The Hydra authors + * Copyright (c) 2022 The Hydra authors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -26,6 +26,9 @@ import androidx.recyclerview.widget.DiffUtil; /** + * Simple diff callback for items where the equals method encapsulates + * changes to the layout. + * * @author Niko Strijbol */ public class EqualsItemCallback extends DiffUtil.ItemCallback { diff --git a/app/src/main/java/be/ugent/zeus/hydra/preferences/AboutFragment.java b/app/src/main/java/be/ugent/zeus/hydra/preferences/AboutFragment.java index 191e4f472..18c51011b 100644 --- a/app/src/main/java/be/ugent/zeus/hydra/preferences/AboutFragment.java +++ b/app/src/main/java/be/ugent/zeus/hydra/preferences/AboutFragment.java @@ -24,20 +24,27 @@ import android.content.Intent; import android.os.Bundle; +import android.widget.Toast; import androidx.appcompat.content.res.AppCompatResources; import androidx.preference.Preference; +import java.util.concurrent.atomic.AtomicInteger; + import be.ugent.zeus.hydra.BuildConfig; import be.ugent.zeus.hydra.R; import be.ugent.zeus.hydra.common.reporting.Reporting; import be.ugent.zeus.hydra.common.ui.PreferenceFragment; import be.ugent.zeus.hydra.common.ui.WebViewActivity; +import be.ugent.zeus.hydra.common.utils.NetworkUtils; +import be.ugent.zeus.hydra.wpi.EnableManager; /** * @author Niko Strijbol */ public class AboutFragment extends PreferenceFragment { + private static final int ZEUS_TIMES = 2; + @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.pref_about, rootKey); @@ -59,6 +66,29 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { return false; }); + // Ugly one-element array. + final AtomicInteger counter = new AtomicInteger(); + + // If Zeus-mode is enabled, link to the site. + // Otherwise, allow enabling it. + requirePreference("pref_about_creator_zeus").setOnPreferenceClickListener(preference -> { + if (EnableManager.isZeusModeEnabled(requireContext())) { + Toast.makeText(requireContext(), R.string.wpi_mode_enabled, Toast.LENGTH_SHORT).show(); + NetworkUtils.maybeLaunchBrowser(requireContext(), "https://zeus.ugent.be"); + } else { + int newValue = counter.incrementAndGet(); + int remaining = ZEUS_TIMES - newValue; + if (remaining == 0) { + EnableManager.setZeusModeEnabled(requireContext(), true); + Toast.makeText(requireContext(), R.string.wpi_mode_enabled, Toast.LENGTH_SHORT).show(); + } else { + String message = requireContext().getResources().getQuantityString(R.plurals.wpi_mode_press, remaining, remaining); + Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show(); + } + } + return true; + }); + requirePreference("pref_about_creator_zeus") .setIcon(AppCompatResources.getDrawable(requireContext(), R.drawable.logo_zeus)); requirePreference("pref_about_creator_dsa") diff --git a/app/src/main/java/be/ugent/zeus/hydra/preferences/PreferenceActivity.java b/app/src/main/java/be/ugent/zeus/hydra/preferences/PreferenceActivity.java index 6c10370ab..bbe36a673 100644 --- a/app/src/main/java/be/ugent/zeus/hydra/preferences/PreferenceActivity.java +++ b/app/src/main/java/be/ugent/zeus/hydra/preferences/PreferenceActivity.java @@ -51,13 +51,19 @@ public class PreferenceActivity extends BaseActivity private PreferenceEntry entry; public static void start(@NonNull Context context, @Nullable PreferenceEntry entry) { + Intent intent = startIntent(context, entry); + context.startActivity(intent); + } + + public static Intent startIntent(@NonNull Context context, @Nullable PreferenceEntry entry) { Intent intent = new Intent(context, PreferenceActivity.class); if (entry != null) { intent.putExtra(ARG_FRAGMENT, (Parcelable) entry); } - context.startActivity(intent); + return intent; } + @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/EnableManager.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/EnableManager.java new file mode 100644 index 000000000..528511955 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/EnableManager.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import androidx.core.content.pm.ShortcutInfoCompat; +import androidx.core.content.pm.ShortcutManagerCompat; +import androidx.core.graphics.drawable.IconCompat; +import androidx.preference.PreferenceManager; + +import java.util.Collections; + +import be.ugent.zeus.hydra.R; + +/** + * Utilities to check if Zeus-mode has been enabled or not. + * + * @author Niko Strijbol + */ +public class EnableManager { + private static final String PREF_ENABLE_ZEUS_MODE = "pref_enable_zeus_mode"; + private static final String ZEUS_SHORTCUT_ID = "be.ugent.zeus.hydra.shortcut.wpi"; + + private EnableManager() { + // No. + } + + public static boolean isZeusModeEnabled(Context context) { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + return preferences.getBoolean(PREF_ENABLE_ZEUS_MODE, false); + } + + public static void setZeusModeEnabled(Context context, boolean enabled) { + if (enabled) { + ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(context, ZEUS_SHORTCUT_ID) + .setShortLabel("Zeus WPI") + .setLongLabel(context.getString(R.string.drawer_title_zeus)) + .setIcon(IconCompat.createWithResource(context, R.drawable.logo_tap)) + .setIntent(new Intent(Intent.ACTION_VIEW, null, context, WpiActivity.class)) + .build(); + ShortcutManagerCompat.pushDynamicShortcut(context, shortcut); + } else { + ShortcutManagerCompat.removeDynamicShortcuts(context, Collections.singletonList(ZEUS_SHORTCUT_ID)); + } + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + preferences.edit() + .putBoolean(PREF_ENABLE_ZEUS_MODE, enabled) + .apply(); + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/WpiActivity.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/WpiActivity.java new file mode 100644 index 000000000..4634de6b7 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/WpiActivity.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.view.*; +import androidx.activity.result.ActivityResult; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; +import androidx.viewpager2.widget.ViewPager2; + +import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton; +import com.google.android.material.snackbar.Snackbar; +import com.google.android.material.tabs.TabLayoutMediator; +import com.squareup.picasso.Picasso; + +import java.text.NumberFormat; + +import be.ugent.zeus.hydra.R; +import be.ugent.zeus.hydra.common.arch.observers.PartialErrorObserver; +import be.ugent.zeus.hydra.common.arch.observers.SuccessObserver; +import be.ugent.zeus.hydra.common.ui.BaseActivity; +import be.ugent.zeus.hydra.databinding.ActivityWpiBinding; +import be.ugent.zeus.hydra.wpi.account.AccountManager; +import be.ugent.zeus.hydra.wpi.account.ApiKeyManagementActivity; +import be.ugent.zeus.hydra.wpi.account.CombinedUserViewModel; +import be.ugent.zeus.hydra.wpi.tab.create.FormActivity; +import be.ugent.zeus.hydra.wpi.tap.cart.CartActivity; + +/** + * Activity that allows you to manage your API key. + *

+ * This is a temporary solution; at some point, we'll need to implement + * a proper login solution (or do we? it is Zeus after all). + * + * @author Niko Strijbol + */ +public class WpiActivity extends BaseActivity { + + private static final String TAG = "ApiKeyManagementActivit"; + private static final int ACTIVITY_DO_REFRESH = 963; + + private CombinedUserViewModel combinedUserViewModel; + private WpiPagerAdapter pageAdapter; + + private final NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance(); + private final NumberFormat decimalFormatter = NumberFormat.getNumberInstance(); + + private final ViewPager2.OnPageChangeCallback callback = new ViewPager2.OnPageChangeCallback() { + @Override + public void onPageSelected(int position) { + if (position == 0) { + binding.tabFab.hide(); + binding.tapFab.show(); + } else if (position == 1) { + binding.tapFab.hide(); + binding.tabFab.show(); + } + } + }; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(ActivityWpiBinding::inflate); + setTitle(); + + pageAdapter = new WpiPagerAdapter(this); + ViewPager2 viewPager = binding.viewPager; + viewPager.setAdapter(pageAdapter); + + TabLayoutMediator mediator = new TabLayoutMediator(binding.tabLayout, viewPager, (tab, position) -> { + if (position == 0) { + tab.setText(R.string.wpi_tap_tab); + } else if (position == 1) { + tab.setText(R.string.wpi_tab_tab); + } + }); + mediator.attach(); + + binding.tabFab.setOnClickListener(v -> { + Intent intent = new Intent(WpiActivity.this, FormActivity.class); + startActivityForResult(intent, ACTIVITY_DO_REFRESH); + }); + binding.tapFab.setOnClickListener(v -> { + Intent intent = new Intent(WpiActivity.this, CartActivity.class); + startActivityForResult(intent, ACTIVITY_DO_REFRESH); + }); + + combinedUserViewModel = new ViewModelProvider(this).get(CombinedUserViewModel.class); + combinedUserViewModel.getData().observe(this, PartialErrorObserver.with(this::onError)); + combinedUserViewModel.getData().observe(this, SuccessObserver.with(user -> { + Picasso.get().load(user.getProfilePicture()).into(binding.profilePicture); + String balance = currencyFormatter.format(user.getBalanceDecimal()); + String orders = decimalFormatter.format(user.getOrders()); + binding.profileDescription.setText(getString(R.string.wpi_user_description, balance, orders)); + setTitle(); + })); + } + + private void setTitle() { + setTitle(AccountManager.getUsername(this)); + } + + @Override + protected void onStart() { + super.onStart(); + binding.viewPager.registerOnPageChangeCallback(callback); + } + + @Override + protected void onStop() { + super.onStop(); + binding.viewPager.unregisterOnPageChangeCallback(callback); + } + + private void onError(Throwable throwable) { + Log.e(TAG, "Error while getting data.", throwable); + // TODO: better error message. + Snackbar.make(binding.getRoot(), getString(R.string.error_network), Snackbar.LENGTH_LONG) + .setAction(getString(R.string.action_again), v -> combinedUserViewModel.onRefresh()) + .show(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.menu_wpi, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == R.id.action_manage_login) { + Intent intent = new Intent(this, ApiKeyManagementActivity.class); + startActivityForResult(intent, ACTIVITY_DO_REFRESH); + } + return super.onOptionsItemSelected(item); + } + + @SuppressLint("NotifyDataSetChanged") + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + Log.i(TAG, "onActivityResult: result"); + + if (requestCode == ACTIVITY_DO_REFRESH && resultCode == Activity.RESULT_OK) { + Log.i(TAG, "onActivityResult: refreshing for result..."); + combinedUserViewModel.onRefresh(); + + if (pageAdapter != null) { + pageAdapter.notifyDataSetChanged(); + } + } + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/WpiPagerAdapter.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/WpiPagerAdapter.java new file mode 100644 index 000000000..7ea05651c --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/WpiPagerAdapter.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.viewpager2.adapter.FragmentStateAdapter; + +import be.ugent.zeus.hydra.common.ui.AdapterOutOfBoundsException; +import be.ugent.zeus.hydra.wpi.tab.list.TransactionFragment; +import be.ugent.zeus.hydra.wpi.tap.product.ProductFragment; + +/** + * This class provides the tabs in the WPI activity. + * + * @author Niko Strijbol + */ +class WpiPagerAdapter extends FragmentStateAdapter { + + public WpiPagerAdapter(@NonNull FragmentActivity fragmentActivity) { + super(fragmentActivity); + } + + @NonNull + @Override + public Fragment createFragment(int position) { + if (position == 0) { + return new ProductFragment(); + } else if (position == 1) { + return new TransactionFragment(); + } + + throw new AdapterOutOfBoundsException(position, getItemCount()); + } + + @Override + public int getItemCount() { + return 2; + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/account/AccountManager.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/account/AccountManager.java new file mode 100644 index 000000000..67c336dbb --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/account/AccountManager.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package be.ugent.zeus.hydra.wpi.account; + +import android.content.Context; +import android.content.SharedPreferences; +import androidx.annotation.Nullable; +import androidx.preference.PreferenceManager; + +import be.ugent.zeus.hydra.R; + +/** + * @author Niko Strijbol + */ +public class AccountManager { + private static final String PREF_WPI_TAB_API_KEY = "pref_wpi_tab_api_key"; + private static final String PREF_WPI_TAP_API_KEY = "pref_wpi_tap_api_key"; + private static final String PREF_WPI_USERNAME = "pref_wpi_username"; + + private AccountManager() { + // No. + } + + public static void saveData(Context context, String tabKey, String tapKey, String username) { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + preferences.edit() + .putString(PREF_WPI_TAB_API_KEY, tabKey) + .putString(PREF_WPI_TAP_API_KEY, tapKey) + .putString(PREF_WPI_USERNAME, username) + .apply(); + } + + @Nullable + public static String getTapKey(Context context) { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + return preferences.getString(PREF_WPI_TAP_API_KEY, null); + } + + @Nullable + public static String getTabKey(Context context) { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + return preferences.getString(PREF_WPI_TAB_API_KEY, null); + } + + @Nullable + public static String getUsername(Context context) { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + return preferences.getString(PREF_WPI_USERNAME, context.getString(R.string.wpi_product_na)); + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/account/ApiKeyManagementActivity.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/account/ApiKeyManagementActivity.java new file mode 100644 index 000000000..21e32acc4 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/account/ApiKeyManagementActivity.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.account; + +import android.content.Intent; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +import android.view.MenuItem; +import androidx.annotation.Nullable; + +import java.util.Objects; + +import be.ugent.zeus.hydra.common.ui.BaseActivity; +import be.ugent.zeus.hydra.databinding.ActivityWpiApiKeyManagementBinding; + +/** + * Activity that allows you to manage your API key. + * + * This is a temporary solution; at some point, we'll need to implement + * a proper login solution (or do we? it is Zeus after all). + * + * @author Niko Strijbol + */ +public class ApiKeyManagementActivity extends BaseActivity implements TextWatcher { + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(ActivityWpiApiKeyManagementBinding::inflate); + + binding.apiTab.setText(AccountManager.getTabKey(this)); + binding.apiTap.setText(AccountManager.getTapKey(this)); + binding.apiUsername.setText(AccountManager.getUsername(this)); + } + + @Override + protected void onStart() { + super.onStart(); + binding.apiTab.addTextChangedListener(this); + binding.apiTap.addTextChangedListener(this); + binding.apiUsername.addTextChangedListener(this); + } + + @Override + protected void onStop() { + super.onStop(); + binding.apiTab.removeTextChangedListener(this); + binding.apiTap.removeTextChangedListener(this); + binding.apiUsername.removeTextChangedListener(this); + } + + @Override + public void onBackPressed() { + Intent intent = new Intent(); + setResult(RESULT_OK, intent); + super.onBackPressed(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + Intent intent = new Intent(); + setResult(RESULT_OK, intent); + } + return super.onOptionsItemSelected(item); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // Do nothing. + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + String tab = Objects.requireNonNull(binding.apiTab.getText()).toString(); + String tap = Objects.requireNonNull(binding.apiTap.getText()).toString(); + String user = Objects.requireNonNull(binding.apiUsername.getText()).toString(); + AccountManager.saveData(this, tab, tap, user); + } + + @Override + public void afterTextChanged(Editable s) { + // Do nothing. + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/account/CombinedUser.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/account/CombinedUser.java new file mode 100644 index 000000000..ce2b3ad69 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/account/CombinedUser.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.account; + +import java.math.BigDecimal; +import java.util.Objects; + +/** + * A combined user for Tab and Tap. + * + * @author Niko Strijbol + */ +public class CombinedUser { + + private int id; + private String name; + private int balance; + private String profilePicture; + protected int orders; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getBalance() { + return balance; + } + + public void setBalance(int balance) { + this.balance = balance; + } + + public String getProfilePicture() { + return profilePicture; + } + + public void setProfilePicture(String profilePicture) { + this.profilePicture = profilePicture; + } + + public int getOrders() { + return orders; + } + + public void setOrders(int orders) { + this.orders = orders; + } + + public BigDecimal getBalanceDecimal() { + return new BigDecimal(getBalance()).movePointLeft(2); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CombinedUser that = (CombinedUser) o; + return id == that.id; + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/account/CombinedUserRequest.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/account/CombinedUserRequest.java new file mode 100644 index 000000000..69bfc9af9 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/account/CombinedUserRequest.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.account; + +import android.content.Context; +import android.os.Bundle; +import android.util.Pair; +import androidx.annotation.NonNull; + +import java.util.function.Function; + +import be.ugent.zeus.hydra.common.request.Request; +import be.ugent.zeus.hydra.common.request.Result; +import be.ugent.zeus.hydra.wpi.tab.user.TabUser; +import be.ugent.zeus.hydra.wpi.tab.user.TabUserRequest; +import be.ugent.zeus.hydra.wpi.tap.user.TapUser; +import be.ugent.zeus.hydra.wpi.tap.user.TapUserRequest; + +/** + * @author Niko Strijbol + */ +public class CombinedUserRequest implements Request { + + private final Request tabUserRequest; + private final Request tapUserRequest; + + public CombinedUserRequest(Context context) { + this.tabUserRequest = new TabUserRequest(context); + this.tapUserRequest = new TapUserRequest(context); + } + + @NonNull + @Override + public Result execute(@NonNull Bundle args) { + return tabUserRequest.andThen(tapUserRequest).map(p -> { + CombinedUser user = new CombinedUser(); + user.setName(p.second.getName()); + user.setId(p.second.getId()); + user.setOrders(p.second.getOrderCount()); + user.setProfilePicture(p.second.getProfileImageUrl()); + user.setBalance(p.first.getBalance()); + return user; + }).execute(args); + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/account/CombinedUserViewModel.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/account/CombinedUserViewModel.java new file mode 100644 index 000000000..58614d9e5 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/account/CombinedUserViewModel.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.account; + +import android.app.Application; +import androidx.annotation.NonNull; + +import be.ugent.zeus.hydra.common.request.Request; +import be.ugent.zeus.hydra.common.ui.RequestViewModel; + +/** + * @author Niko Strijbol + */ +public class CombinedUserViewModel extends RequestViewModel { + + public CombinedUserViewModel(Application application) { + super(application); + } + + @NonNull + @Override + protected Request getRequest() { + return new CombinedUserRequest(getApplication()); + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/create/CreateTransactionRequest.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/create/CreateTransactionRequest.java new file mode 100644 index 000000000..f1848d320 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/create/CreateTransactionRequest.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tab.create; + +import android.content.Context; +import android.os.Bundle; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.WorkerThread; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import be.ugent.zeus.hydra.common.network.Endpoints; +import be.ugent.zeus.hydra.common.network.InvalidFormatException; +import be.ugent.zeus.hydra.common.network.OkHttpRequest; +import be.ugent.zeus.hydra.common.request.RequestException; +import be.ugent.zeus.hydra.common.request.Result; +import be.ugent.zeus.hydra.wpi.account.AccountManager; +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.JsonDataException; +import com.squareup.moshi.Types; +import okhttp3.*; + +/** + * Creates a new transaction. + * + * @author Niko Strijbol + */ +public class CreateTransactionRequest extends OkHttpRequest { + + private final TransactionForm form; + private final Context context; + + public CreateTransactionRequest(@NonNull Context context, @NonNull TransactionForm form) { + super(context); + this.context = context.getApplicationContext(); + this.form = form; + } + + @NonNull + @Override + @WorkerThread + public Result execute(@NonNull Bundle args) { + + MediaType json = MediaType.get("application/json; charset=utf-8"); + + Map> data = new HashMap<>(); + data.put("transaction", form.asApiObject(AccountManager.getUsername(context))); + Type type = Types.newParameterizedType(Map.class, String.class, Types.newParameterizedType(Map.class, String.class, Object.class)); + JsonAdapter>> adapter = moshi.adapter(type); + + String rawData = adapter.toJson(data); + + // Create a request body. + RequestBody body = RequestBody.create(rawData, json); + + // Create the request itself. + okhttp3.Request request = new Request.Builder() + .addHeader("Accept", "application/json") + .addHeader("Content-Type", json.toString()) + .addHeader("Authorization", "Bearer " + AccountManager.getTabKey(context)) + .url(Endpoints.TAB + "transactions") + .post(body) + .build(); + + try (Response response = client.newCall(request).execute()) { + if (response.isSuccessful()) { + return Result.Builder.fromData(true); + } else if (response.code() == 422) { + // If the body is null, this is unexpected. + ResponseBody responseBody = response.body(); + if (responseBody == null) { + throw new IOException("Unexpected null body for response 422"); + } + // Unprocessable entity + Type errorType = Types.newParameterizedType(List.class, String.class); + JsonAdapter> errorAdapter = moshi.adapter(errorType); + List errors = errorAdapter.fromJson(responseBody.source()); + return Result.Builder.fromException(new TabRequestException(errors)); + } else { + throw new IOException("Unexpected state in request; neither successful nor 422: got " + response.code()); + } + } catch (JsonDataException | NullPointerException e) { + // Create, log and throw exception, since this is not normal. + String message = "The server did not respond with the expected format when creating a Tab transaction."; + InvalidFormatException exception = new InvalidFormatException(message, e); + tracker.logError(exception); + return Result.Builder.fromException(exception); + } catch (IOException e) { + // This is an unknown exception, e.g. the network is gone. + RequestException exception = new RequestException(e); + tracker.logError(exception); + return Result.Builder.fromException(exception); + } + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/create/FormActivity.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/create/FormActivity.java new file mode 100644 index 000000000..7d155d2d7 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/create/FormActivity.java @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tab.create; + +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.math.BigDecimal; +import java.text.NumberFormat; +import java.util.*; +import java.util.stream.Collectors; + +import be.ugent.zeus.hydra.R; +import be.ugent.zeus.hydra.common.arch.observers.EventObserver; +import be.ugent.zeus.hydra.common.network.NetworkState; +import be.ugent.zeus.hydra.common.request.RequestException; +import be.ugent.zeus.hydra.common.ui.BaseActivity; +import be.ugent.zeus.hydra.common.ui.SimpleTextWatcher; +import be.ugent.zeus.hydra.databinding.ActivityWpiTabTransactionFormBinding; + +/** + * Form where the user can create a new transaction. + * + * @author Niko Strijbol + */ +public class FormActivity extends BaseActivity { + + private static final String TAG = "FormActivity"; + private final NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance(); + private static final String KEY_FORM_OBJECT = "wpi_tab_transaction_object"; + + private TransactionForm formObject; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(ActivityWpiTabTransactionFormBinding::inflate); + + if (savedInstanceState != null && savedInstanceState.containsKey(KEY_FORM_OBJECT)) { + formObject = savedInstanceState.getParcelable(KEY_FORM_OBJECT); + } else { + formObject = new TransactionForm(); + } + + TransactionViewModel model = new ViewModelProvider(this).get(TransactionViewModel.class); + + model.getRequestResult().observe(this, EventObserver.with(booleanResult -> { + if (booleanResult.isWithoutError()) { + int string; + if (formObject.getAmount() < 0) { + string = R.string.wpi_tab_form_done_negative; + } else { + string = R.string.wpi_tab_form_done; + } + Toast.makeText(FormActivity.this, string, Toast.LENGTH_SHORT).show(); + setResult(RESULT_OK); + finish(); + } else { + RequestException e = booleanResult.getError(); + Log.e(TAG, "error during transaction request", e); + if (e instanceof TabRequestException) { + List messages = ((TabRequestException) e).getMessages(); + handleErrorMessages(messages); + } else { + String message = e.getMessage(); + new MaterialAlertDialogBuilder(FormActivity.this) + .setTitle(android.R.string.dialog_alert_title) + .setIconAttribute(android.R.attr.alertDialogIcon) + .setMessage(getString(R.string.wpi_tab_form_error) + "\n" + message) + .setPositiveButton(android.R.string.ok, null) + .show(); + } + } + })); + + model.getNetworkState().observe(this, networkState -> { + boolean enabled = networkState == null || networkState == NetworkState.IDLE; + binding.formMember.setEnabled(enabled); + binding.formAmount.setEnabled(enabled); + binding.formMessage.setEnabled(enabled); + binding.confirmButton.setEnabled(enabled); + if (networkState == NetworkState.BUSY) { + binding.formAmountLayout.setError(null); + binding.formMemberLayout.setError(null); + } + }); + + // Set up sync between UI and object. + binding.formAmount.addTextChangedListener((SimpleTextWatcher) newText -> { + if (TextUtils.isEmpty(newText)) { + return; + } + try { + new BigDecimal(newText); + binding.formAmount.setError(null); + } catch (NumberFormatException e) { + Log.e(TAG, "onCreate: ", e); + binding.formAmount.setError("Wrong format"); + } + }); + + binding.confirmButton.setOnClickListener(v -> { + syncData(); + String message; + if (formObject.getAmount() < 0) { + message = getString(R.string.wpi_tab_form_confirm_negative, + currencyFormatter.format(formObject.getAdjustedAmount().negate()), + formObject.getDestination()); + } else { + message = getString(R.string.wpi_tab_form_confirm_positive, + currencyFormatter.format(formObject.getAdjustedAmount()), + formObject.getDestination()); + } + new MaterialAlertDialogBuilder(FormActivity.this) + .setMessage(message) + .setPositiveButton(android.R.string.ok, (dialog, which) -> model.startRequest(formObject)) + .setNegativeButton(android.R.string.cancel, null) + .show(); + }); + } + + private void syncData() { + if (binding.formMember.getText() != null) { + formObject.setDestination(binding.formMember.getText().toString()); + } + if (binding.formAmount.getText() != null) { + BigDecimal number = new BigDecimal(binding.formAmount.getText().toString()).movePointRight(2); + int cents = number.intValue(); + formObject.setAmount(cents); + } + if (binding.formMessage.getText() != null) { + formObject.setDescription(binding.formMessage.getText().toString()); + } + } + + private void handleErrorMessages(List messages) { + // TODO: clean this up. + Log.e(TAG, "handleErrorMessages: found error messages"); + + // Amount field. + List numberMessages = new ArrayList<>(); + if (messages.contains("Amount must be greater than 0")) { + numberMessages.add(getString(R.string.wpi_tab_form_error_amount_zero)); + } + List otherNumbers = messages.stream() + .filter(p -> !p.equals("Amount must be greater than 0") && p.toLowerCase(Locale.ENGLISH).contains("amount")) + .collect(Collectors.toList()); + numberMessages.addAll(otherNumbers); + String bigMessage = TextUtils.join("\n", numberMessages); + binding.formAmountLayout.setError(bigMessage); + + // Destination field. + List userMessages = new ArrayList<>(); + if (messages.contains("Creditor can't be blank") || messages.contains("Debtor can't be blank")) { + userMessages.add(getString(R.string.wpi_tab_form_error_creditor_blank)); + } + List otherUsers = messages.stream() + .filter(p -> !p.equals("Creditor can't be blank") && !p.equals("Debtor can't be blank") + && (p.toLowerCase(Locale.ENGLISH).contains("creditor") || p.toLowerCase(Locale.ENGLISH).contains("debtor"))) + .collect(Collectors.toList()); + userMessages.addAll(otherUsers); + String bigUserMessage = TextUtils.join("\n", userMessages); + binding.formMemberLayout.setError(bigUserMessage); + } + + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + outState.putParcelable(KEY_FORM_OBJECT, formObject); + super.onSaveInstanceState(outState); + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/create/TabRequestException.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/create/TabRequestException.java new file mode 100644 index 000000000..e504b8a48 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/create/TabRequestException.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tab.create; + +import android.text.TextUtils; + +import java.util.List; + +import be.ugent.zeus.hydra.common.request.RequestException; + +/** + * Error when posting a transaction results in 422. + * + * @author Niko Strijbol + */ +public class TabRequestException extends RequestException { + private final List messages; + + public TabRequestException(List messages) { + super(TextUtils.join(", ", messages)); + this.messages = messages; + } + + public List getMessages() { + return messages; + } +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/create/TransactionForm.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/create/TransactionForm.java new file mode 100644 index 000000000..a37fdf567 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/create/TransactionForm.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tab.create; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Model for creating a new transaction. + * + * @author Niko Strijbol + */ +class TransactionForm implements Parcelable { + + private String destination; + private int amount; + private String description; + + public String getDestination() { + return destination; + } + + public void setDestination(String destination) { + this.destination = destination; + } + + public int getAmount() { + return amount; + } + + public BigDecimal getAdjustedAmount() { + return new BigDecimal(getAmount()).movePointLeft(2); + } + + public void setAmount(int amount) { + this.amount = amount; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Map asApiObject(String me) { + Map m = new HashMap<>(); + m.put("debtor", me); + m.put("creditor", getDestination()); + m.put("cents", getAmount()); + m.put("message", getDescription()); + return m; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TransactionForm that = (TransactionForm) o; + return amount == that.amount && Objects.equals(destination, that.destination) && Objects.equals(description, that.description); + } + + @Override + public int hashCode() { + return Objects.hash(destination, amount, description); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(this.destination); + dest.writeInt(this.amount); + dest.writeString(this.description); + } + + public void readFromParcel(Parcel source) { + this.destination = source.readString(); + this.amount = source.readInt(); + this.description = source.readString(); + } + + public TransactionForm() { + } + + protected TransactionForm(Parcel in) { + this.destination = in.readString(); + this.amount = in.readInt(); + this.description = in.readString(); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + @Override + public TransactionForm createFromParcel(Parcel source) { + return new TransactionForm(source); + } + + @Override + public TransactionForm[] newArray(int size) { + return new TransactionForm[size]; + } + }; +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/create/TransactionViewModel.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/create/TransactionViewModel.java new file mode 100644 index 000000000..674c1093c --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/create/TransactionViewModel.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tab.create; + +import android.app.Application; +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import be.ugent.zeus.hydra.common.arch.data.Event; +import be.ugent.zeus.hydra.common.network.NetworkState; +import be.ugent.zeus.hydra.common.request.Result; +import be.ugent.zeus.hydra.common.utils.ThreadingUtils; + +/** + * Responsible for managing requests to create transactions. + * + * There are a set of methods that return LiveData. Those should be + * listened to to get the results. + * + * @author Niko Strijbol + */ +public class TransactionViewModel extends AndroidViewModel { + + private final MutableLiveData networkState; + private final MutableLiveData>> requestResult; + + public TransactionViewModel(@NonNull Application application) { + super(application); + networkState = new MutableLiveData<>(NetworkState.IDLE); + requestResult = new MutableLiveData<>(); + } + + public LiveData getNetworkState() { + return networkState; + } + + public LiveData>> getRequestResult() { + return requestResult; + } + + public void startRequest(TransactionForm transactionForm) { + CreateTransactionRequest request = new CreateTransactionRequest(getApplication(), transactionForm); + networkState.postValue(NetworkState.BUSY); + ThreadingUtils.execute(() -> { + Result result = request.execute(); + networkState.postValue(NetworkState.IDLE); + requestResult.postValue(new Event<>(result)); + }); + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/list/Transaction.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/list/Transaction.java new file mode 100644 index 000000000..bce409d9a --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/list/Transaction.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tab.list; + +import java.math.BigDecimal; +import java.time.OffsetDateTime; +import java.util.Objects; + +/** + * Transaction on Tab. + * + * @author Niko Strijbol + */ +public class Transaction { + private int id; + private String debtor; + private String creditor; + private OffsetDateTime time; + private int amount; + private String issuer; + private String message; + + public int getId() { + return id; + } + + public String getDebtor() { + return debtor; + } + + public String getCreditor() { + return creditor; + } + + public OffsetDateTime getTime() { + return time; + } + + public int getAmount() { + return amount; + } + + public String getIssuer() { + return issuer; + } + + public String getMessage() { + return message; + } + + public BigDecimal getAdjustedAmount(String fromPerspectiveOf) { + BigDecimal raw = new BigDecimal(getAmount()).movePointLeft(2); + if (fromPerspectiveOf.equals(getDebtor())) { + return raw.negate(); + } else { + return raw; + } + } + + public String getDisplayOther(String fromPerspectiveOf) { + String other = getOtherParty(fromPerspectiveOf); + if (other.equals("Tab")) { + other = "Zeus"; + } + return other; + } + + public String getOtherParty(String fromPerspectiveOf) { + String other; + if (fromPerspectiveOf.equals(getDebtor())) { + other = getCreditor(); + } else { + other = getDebtor(); + } + return other; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Transaction that = (Transaction) o; + return id == that.id; + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/list/TransactionAdapter.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/list/TransactionAdapter.java new file mode 100644 index 000000000..75403f819 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/list/TransactionAdapter.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tab.list; + +import android.view.ViewGroup; +import androidx.annotation.NonNull; + +import be.ugent.zeus.hydra.R; +import be.ugent.zeus.hydra.common.ui.recyclerview.adapters.DiffAdapter; +import be.ugent.zeus.hydra.common.utils.ViewUtils; + +/** + * @author Niko Strijbol + */ +class TransactionAdapter extends DiffAdapter { + @NonNull + @Override + public TransactionViewHolder onCreateViewHolder(@NonNull ViewGroup p, int viewType) { + return new TransactionViewHolder(ViewUtils.inflate(p, R.layout.item_transaction)); + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/list/TransactionFragment.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/list/TransactionFragment.java new file mode 100644 index 000000000..4b15c3da1 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/list/TransactionFragment.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tab.list; + +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import be.ugent.zeus.hydra.R; +import be.ugent.zeus.hydra.common.arch.observers.AdapterObserver; +import be.ugent.zeus.hydra.common.arch.observers.PartialErrorObserver; +import be.ugent.zeus.hydra.common.arch.observers.ProgressObserver; +import be.ugent.zeus.hydra.common.ui.recyclerview.SpanItemSpacingDecoration; +import be.ugent.zeus.hydra.common.utils.ColourUtils; +import be.ugent.zeus.hydra.wpi.account.CombinedUserViewModel; +import com.google.android.material.snackbar.Snackbar; + +/** + * Display Tab transactions. + * + * @author Niko Strijbol + */ +public class TransactionFragment extends Fragment { + + private static final String TAG = "TransactionFragment"; + private TransactionViewModel viewModel; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_product, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + RecyclerView recyclerView = view.findViewById(R.id.recycler_view); + recyclerView.setHasFixedSize(true); + recyclerView.addItemDecoration(new SpanItemSpacingDecoration(requireContext())); + TransactionAdapter adapter = new TransactionAdapter(); + recyclerView.setAdapter(adapter); + + SwipeRefreshLayout swipeRefreshLayout = view.findViewById(R.id.swipeRefreshLayout); + swipeRefreshLayout.setColorSchemeColors(ColourUtils.resolveColour(requireContext(), R.attr.colorSecondary)); + + viewModel = new ViewModelProvider(this).get(TransactionViewModel.class); + viewModel.getData().observe(getViewLifecycleOwner(), PartialErrorObserver.with(this::onError)); + viewModel.getData().observe(getViewLifecycleOwner(), new ProgressObserver<>(view.findViewById(R.id.progress_bar))); + viewModel.getData().observe(getViewLifecycleOwner(), new AdapterObserver<>(adapter)); + viewModel.getRefreshing().observe(getViewLifecycleOwner(), swipeRefreshLayout::setRefreshing); + + // For refreshing, we request a refresh from the parent activity. + // We then listen to the parent refresh state and refresh this fragment when the parent is refreshing. + CombinedUserViewModel activityViewModel = new ViewModelProvider(requireActivity()).get(CombinedUserViewModel.class); + swipeRefreshLayout.setOnRefreshListener(activityViewModel); + activityViewModel.getRefreshing().observe(getViewLifecycleOwner(), refreshing -> { + if (refreshing) { + viewModel.onRefresh(); + } + }); + } + + private void onError(Throwable throwable) { + Log.e(TAG, "Error while getting data.", throwable); + Snackbar.make(requireView(), getString(R.string.error_network), Snackbar.LENGTH_LONG) + .setAction(getString(R.string.action_again), v -> viewModel.requestRefresh()) + .show(); + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/list/TransactionRequest.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/list/TransactionRequest.java new file mode 100644 index 000000000..56467cc67 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/list/TransactionRequest.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2022 The Hydra authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tab.list; + +import android.content.Context; +import android.os.Bundle; +import androidx.annotation.NonNull; + +import java.time.Duration; + +import be.ugent.zeus.hydra.common.network.Endpoints; +import be.ugent.zeus.hydra.common.network.JsonArrayRequest; +import be.ugent.zeus.hydra.wpi.account.AccountManager; +import okhttp3.Request; + +/** + * This should probably be paginated at some point. + * + * @author Niko Strijbol + */ +class TransactionRequest extends JsonArrayRequest { + + private final Context context; + + TransactionRequest(Context context) { + super(context, Transaction.class); + this.context = context.getApplicationContext(); + } + + @Override + protected Request.Builder constructRequest(@NonNull Bundle arguments) { + Request.Builder builder = super.constructRequest(arguments); + builder.addHeader("Authorization", "Bearer " + AccountManager.getTabKey(context)); + return builder; + } + + @NonNull + @Override + protected String getAPIUrl() { + return Endpoints.TAB + "users/" + AccountManager.getUsername(context) + "/transactions"; + } + + @Override + public Duration getCacheDuration() { + return Duration.ZERO; + } +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/list/TransactionViewHolder.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/list/TransactionViewHolder.java new file mode 100644 index 000000000..77e6a0390 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/list/TransactionViewHolder.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tab.list; + +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import java.text.NumberFormat; +import java.util.Currency; + +import be.ugent.zeus.hydra.R; +import be.ugent.zeus.hydra.common.ui.recyclerview.viewholders.DataViewHolder; +import be.ugent.zeus.hydra.common.utils.DateUtils; +import be.ugent.zeus.hydra.wpi.account.AccountManager; + +/** + * View holder for the products in the Tab fragment. + * + * @author Niko Strijbol + */ +class TransactionViewHolder extends DataViewHolder { + + private final ImageView thumbnail; + private final TextView title; + private final TextView firstDescription; + private final TextView secondDescription; + private final TextView meta; + private final NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance(); + private final String me; + + TransactionViewHolder(View v) { + super(v); + thumbnail = v.findViewById(R.id.thumbnail); + title = v.findViewById(R.id.title); + firstDescription = v.findViewById(R.id.firstDescription); + secondDescription = v.findViewById(R.id.secondDescription); + meta = v.findViewById(R.id.meta); + currencyFormatter.setCurrency(Currency.getInstance("EUR")); + me = AccountManager.getUsername(v.getContext()); + } + + @Override + public void populate(final Transaction transaction) { + title.setText(transaction.getDisplayOther(me)); + if (transaction.getIssuer().equals("Tap")) { + thumbnail.setImageResource(R.drawable.logo_tap); + } else if (transaction.getAmount() > 0) { + thumbnail.setImageResource(R.drawable.ic_bank_transfer_out); + } else if (transaction.getAmount() < 0) { + thumbnail.setImageResource(R.drawable.ic_bank_transfer_in); + } else { + // This is probably not possible. + thumbnail.setImageResource(R.drawable.ic_receipt_long); + } + meta.setText(currencyFormatter.format(transaction.getAdjustedAmount(me))); + secondDescription.setText(transaction.getMessage()); + firstDescription.setText(DateUtils.relativeDateTimeString(transaction.getTime(), itemView.getContext())); + } + +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/list/TransactionViewModel.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/list/TransactionViewModel.java new file mode 100644 index 000000000..d86fb687f --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/list/TransactionViewModel.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tab.list; + +import android.app.Application; +import androidx.annotation.NonNull; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import be.ugent.zeus.hydra.common.request.Request; +import be.ugent.zeus.hydra.common.ui.RequestViewModel; + +/** + * @author Niko Strijbol + */ +public class TransactionViewModel extends RequestViewModel> { + + public TransactionViewModel(Application application) { + super(application); + } + + @NonNull + @Override + protected Request> getRequest() { + return new TransactionRequest(getApplication()) + .map(transactions -> transactions + .stream() + .sorted(Comparator.comparing(Transaction::getTime).reversed()) + .collect(Collectors.toList())); + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/user/TabUser.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/user/TabUser.java new file mode 100644 index 000000000..674b75189 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/user/TabUser.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tab.user; + +import java.util.Objects; + +/** + * @author Niko Strijbol + */ +public class TabUser { + private int id; + private String name; + private int balance; + + public TabUser() { + // Moshi. + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } + + public int getBalance() { + return balance; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TabUser tabUser = (TabUser) o; + return id == tabUser.id; + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/user/TabUserRequest.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/user/TabUserRequest.java new file mode 100644 index 000000000..db762c224 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/user/TabUserRequest.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tab.user; + +import android.content.Context; +import android.os.Bundle; +import androidx.annotation.NonNull; + +import java.time.Duration; + +import be.ugent.zeus.hydra.common.network.Endpoints; +import be.ugent.zeus.hydra.common.network.JsonOkHttpRequest; +import be.ugent.zeus.hydra.wpi.account.AccountManager; +import okhttp3.Request; + +/** + * Get a Tab user. + * + * @author Niko Strijbol + */ +public class TabUserRequest extends JsonOkHttpRequest { + + private final Context context; + + public TabUserRequest(Context context) { + super(context, TabUser.class); + this.context = context.getApplicationContext(); + } + + @NonNull + @Override + protected String getAPIUrl() { + return Endpoints.TAB + "users/" + AccountManager.getUsername(context); + } + + @Override + protected Request.Builder constructRequest(@NonNull Bundle arguments) { + Request.Builder builder = super.constructRequest(arguments); + builder.addHeader("Authorization", "Bearer " + AccountManager.getTabKey(context)); + return builder; + } + + @Override + public Duration getCacheDuration() { + // Do not cache this at the moment. + return Duration.ZERO; + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/barcode/Barcode.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/barcode/Barcode.java new file mode 100644 index 000000000..9c7cca4a8 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/barcode/Barcode.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tap.barcode; + +import com.squareup.moshi.Json; + +import java.util.Objects; + +/** + * A Tap barcode. + * + * @author Niko Strijbol + */ +public class Barcode { + + private int id; + @Json(name = "product_id") + private int productId; + private String code; + + public Barcode() { + // Moshi. + } + + public int getProductId() { + return productId; + } + + public int getId() { + return id; + } + + public String getCode() { + return code; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Barcode barcode = (Barcode) o; + return id == barcode.id; + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/barcode/BarcodeRequest.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/barcode/BarcodeRequest.java new file mode 100644 index 000000000..39d43b8f9 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/barcode/BarcodeRequest.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tap.barcode; + +import android.content.Context; +import android.os.Bundle; +import androidx.annotation.NonNull; + +import java.time.Duration; + +import be.ugent.zeus.hydra.common.network.Endpoints; +import be.ugent.zeus.hydra.common.network.JsonArrayRequest; +import be.ugent.zeus.hydra.wpi.account.AccountManager; +import okhttp3.Request; + +/** + * @author Niko Strijbol + */ +public class BarcodeRequest extends JsonArrayRequest { + + private final Context context; + + public BarcodeRequest(Context context) { + super(context, Barcode.class); + this.context = context.getApplicationContext(); + } + + @Override + protected Request.Builder constructRequest(@NonNull Bundle arguments) { + Request.Builder builder = super.constructRequest(arguments); + builder.addHeader("Authorization", "Bearer " + AccountManager.getTapKey(context)); + return builder; + } + + @NonNull + @Override + protected String getAPIUrl() { + return Endpoints.TAP + "barcodes"; + } + + @Override + protected Duration getCacheDuration() { + return Duration.ofDays(1); + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/Cart.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/Cart.java new file mode 100644 index 000000000..9b3456f11 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/Cart.java @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tap.cart; + +import android.util.Pair; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.math.BigDecimal; +import java.time.OffsetDateTime; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import be.ugent.zeus.hydra.wpi.tap.product.Product; + +/** + * The cart is a collection of cart products and a map of normal products. + * + * The cart products are what is in the cart, and is what should be saved to + * the parcel when saving the user's cart. The product map is to make adding + * stuff faster, so it is not necessarily needed. + * + * Because of how the diff algorithms work in the RecyclerView, the cart is + * partially read-only: this list of orders is read-only, and will result in + * a new cart you'll need to save. + * + * @author Niko Strijbol + */ +class Cart { + private final List orders; + private final Map productIdToProduct; + private final Map barcodeToProductId; + private final OffsetDateTime lastEdited; + + private Cart(List orders, Map productIdToProduct, Map barcodeToProductId) { + this(orders, productIdToProduct, barcodeToProductId, OffsetDateTime.now()); + } + + private Cart(List orders, Map productIdToProduct, Map barcodeToProductId, OffsetDateTime lastEdited) { + this.orders = orders; + this.productIdToProduct = productIdToProduct; + this.barcodeToProductId = barcodeToProductId; + this.lastEdited = lastEdited; + } + + /** + * Create a new cart based on an existing one. + * + * @param existingCart The existing cart. + * @param productIdToProduct Map of product ID's to products. + * @param barcodeToProductId Map of barcodes to product ID's. + */ + public Cart(StorageCart existingCart, Map productIdToProduct, Map barcodeToProductId) { + this(fromExisting(existingCart, productIdToProduct), productIdToProduct, barcodeToProductId, existingCart.getLastEdited()); + } + + private static List fromExisting(StorageCart cart, Map productIdToProduct) { + List orders = new ArrayList<>(); + for (Pair productIdAndAmount: cart.getProductIds()) { + Product product = productIdToProduct.get(productIdAndAmount.first); + if (product == null) { + // Skip this product, as it nog longer exists. + continue; + } + orders.add(new CartProduct(product, productIdAndAmount.second)); + } + return orders; + } + + /** + * Get a smaller class suitable for saving. It contains all data + * that cannot be found on the network. + */ + public StorageCart forStorage() { + return new StorageCart(orders.stream().map(cp -> new Pair<>(cp.getProductId(), cp.getAmount())).collect(Collectors.toList()), lastEdited); + } + + public Map>> forJson() { + List> attributes = new ArrayList<>(); + for (CartProduct cartProduct: this.getOrders()) { + Map data = new HashMap<>(); + data.put("product_id", cartProduct.getProductId()); + data.put("count", cartProduct.getAmount()); + attributes.add(data); + } + Map>> total = new HashMap<>(); + total.put("order_items_attributes", attributes); + return total; + } + + /** + * @return Unmodifiable Map of product ID's to products. + */ + public Map getProductIdToProduct() { + return Collections.unmodifiableMap(productIdToProduct); + } + + /** + * Get the product corresponding to a given barcode. + * + * @param barcode The barcode to search. + * + * @return The found product or null. + */ + @Nullable + public Product getProductFor(@NonNull String barcode) { + Integer productId = barcodeToProductId.get(barcode); + if (productId == null) { + return null; + } + return productIdToProduct.get(productId); + } + + /** + * Add a product or increment its count if already present. + * + * @param product The product to add. + * + * @return A new cart that has the added product. + */ + @NonNull + public Cart addProduct(@NonNull Product product) { + // Find the position of an existing cart product if available. + // TODO: this is probably horribly inefficient + OptionalInt index = IntStream.range(0, orders.size()) + .filter(i -> orders.get(i).getProductId() == product.getId()) + .findFirst(); + + if (index.isPresent()) { + return increment(orders.get(index.getAsInt())); + } else { + CartProduct newProduct = new CartProduct(product, 1); + List replacementList = new ArrayList<>(orders); + replacementList.add(newProduct); + return new Cart(replacementList, productIdToProduct, barcodeToProductId); + } + } + + public Cart increment(CartProduct product) { + int index = orders.indexOf(product); + CartProduct replacement = product.increment(); + List replacementList = new ArrayList<>(orders); + replacementList.set(index, replacement); + return new Cart(replacementList, productIdToProduct, barcodeToProductId); + } + + public Cart remove(CartProduct product) { + List replacementList = new ArrayList<>(orders); + replacementList.remove(product); + return new Cart(replacementList, productIdToProduct, barcodeToProductId); + } + + public Cart decrement(CartProduct product) { + if (product.getAmount() == 1) { + return remove(product); + } else { + int index = orders.indexOf(product); + CartProduct replacement = product.decrement(); + List replacementList = new ArrayList<>(orders); + replacementList.set(index, replacement); + return new Cart(replacementList, productIdToProduct, barcodeToProductId); + } + } + + public BigDecimal getTotalPrice() { + BigDecimal totalAmount = BigDecimal.ZERO; + for (CartProduct product : getOrders()) { + totalAmount = totalAmount.add(product.getPriceDecimal().multiply(BigDecimal.valueOf(product.getAmount()))); + } + return totalAmount; + } + + public int getTotalProducts() { + int totalProducts = 0; + for (CartProduct product : getOrders()) { + totalProducts += product.getAmount(); + } + return totalProducts; + } + + public Cart clear() { + return new Cart(new ArrayList<>(), productIdToProduct, barcodeToProductId); + } + + public List getOrders() { + return Collections.unmodifiableList(orders); + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/CartActivity.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/CartActivity.java new file mode 100644 index 000000000..94996246a --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/CartActivity.java @@ -0,0 +1,325 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tap.cart; + +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.view.*; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import java.math.BigDecimal; +import java.text.NumberFormat; + +import be.ugent.zeus.hydra.R; +import be.ugent.zeus.hydra.common.arch.observers.EventObserver; +import be.ugent.zeus.hydra.common.arch.observers.PartialErrorObserver; +import be.ugent.zeus.hydra.common.arch.observers.SuccessObserver; +import be.ugent.zeus.hydra.common.barcode.Manager; +import be.ugent.zeus.hydra.common.network.NetworkState; +import be.ugent.zeus.hydra.common.request.RequestException; +import be.ugent.zeus.hydra.common.scanner.BarcodeScanner; +import be.ugent.zeus.hydra.common.ui.BaseActivity; +import be.ugent.zeus.hydra.common.ui.recyclerview.SpanItemSpacingDecoration; +import be.ugent.zeus.hydra.common.utils.ColourUtils; +import be.ugent.zeus.hydra.databinding.ActivityWpiTapCartBinding; +import be.ugent.zeus.hydra.wpi.tap.product.Product; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.snackbar.Snackbar; + +/** + * The Tap cart. + * + * @author Niko Strijbol + */ +public class CartActivity extends BaseActivity implements CartInteraction { + + private static final String TAG = "FormActivity"; + private final NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance(); + + /** + * The latest instance of the cart we've found. + * TODO: this can probably be nicer by moving this to the view holder. + */ + private CartViewModel viewModel; + // Ugly hack to disable menus while submitting carts. + private Boolean lastEnabledBoolean; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(ActivityWpiTapCartBinding::inflate); + + viewModel = new ViewModelProvider(this).get(CartViewModel.class); + updateCartSummary(null); + + binding.scanAdd.setOnClickListener(v -> { + BarcodeScanner scanner = Manager.getScanner(); + if (scanner.needsActivity()) { + Intent intent = scanner.getActivityIntent(CartActivity.this); + startActivityForResult(intent, scanner.getRequestCode()); + } else { + scanner.getBarcode(CartActivity.this, this::onBarcodeScan, this::onError); + } + }); + binding.manualAdd.setOnClickListener(v -> { + ProductPickerDialogFragment productPicker = new ProductPickerDialogFragment(); + productPicker.show(getSupportFragmentManager(), "productPick"); + }); + + // This is one ugly hack :( + binding.bottomSheet.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + int bottomSheetHeight = binding.bottomSheet.getHeight(); + RecyclerView rv = binding.recyclerView; + rv.setPadding(rv.getPaddingLeft(), rv.getPaddingTop(), rv.getPaddingRight(), (int) (bottomSheetHeight * 1.4)); + binding.bottomSheet.getViewTreeObserver().removeOnGlobalLayoutListener(this); + } + }); + + RecyclerView recyclerView = binding.recyclerView; + recyclerView.setHasFixedSize(true); + recyclerView.addItemDecoration(new SpanItemSpacingDecoration(this)); + CartProductAdapter adapter = new CartProductAdapter(this); + recyclerView.setAdapter(adapter); + + SwipeRefreshLayout swipeRefreshLayout = binding.swipeRefreshLayout; + swipeRefreshLayout.setEnabled(false); + swipeRefreshLayout.setColorSchemeColors(ColourUtils.resolveColour(this, R.attr.colorSecondary)); + + viewModel.getData().observe(this, PartialErrorObserver.with(this::onCartLoadError)); + // A custom adapter to map the data and save an instance of it. + viewModel.getData().observe(this, new SuccessObserver() { + @Override + protected void onSuccess(@NonNull Cart data) { + Log.i(TAG, "onSuccess: received cart, with X items: " + data.getOrders().size()); + adapter.submitData(data.getOrders()); + viewModel.registerLastCart(data); + updateCartSummary(data); + } + }); + viewModel.getRefreshing().observe(this, swipeRefreshLayout::setRefreshing); + swipeRefreshLayout.setOnRefreshListener(viewModel); + + viewModel.getRequestResult().observe(this, EventObserver.with(orderResult -> { + if (orderResult.isWithoutError()) { + BigDecimal total = orderResult.getData().getPrice(); + String formattedTotal = currencyFormatter.format(total); + String message = getString(R.string.wpi_tap_order_ok, formattedTotal); + Toast.makeText(CartActivity.this, message, Toast.LENGTH_SHORT).show(); + setResult(RESULT_OK); + this.clearCart(true); + finish(); + } else { + RequestException e = orderResult.getError(); + Log.e(TAG, "error during transaction request", e); + String message = e.getMessage(); + new MaterialAlertDialogBuilder(CartActivity.this) + .setTitle(android.R.string.dialog_alert_title) + .setIconAttribute(android.R.attr.alertDialogIcon) + .setMessage(getString(R.string.wpi_tap_form_error) + "\n" + message) + .setPositiveButton(android.R.string.ok, null) + .show(); + } + })); + + viewModel.getNetworkState().observe(this, networkState -> { + boolean enabled = networkState == null || networkState == NetworkState.IDLE; + lastEnabledBoolean = enabled; + binding.scanAdd.setEnabled(enabled); + binding.cartPay.setEnabled(enabled); + if (enabled) { + binding.cartProgress.setVisibility(View.GONE); + } else { + binding.cartProgress.setVisibility(View.VISIBLE); + } + invalidateOptionsMenu(); + }); + + binding.cartPay.setOnClickListener(v -> { + if (viewModel.getLastCart() == null) { + Toast.makeText(CartActivity.this, R.string.error_network, Toast.LENGTH_SHORT).show(); + return; + } + String formattedTotal = currencyFormatter.format(viewModel.getLastCart().getTotalPrice()); + String message = getString(R.string.wpi_tap_form_confirm, formattedTotal); + new MaterialAlertDialogBuilder(CartActivity.this) + .setMessage(message) + .setPositiveButton(android.R.string.ok, (dialog, which) -> viewModel.startRequest(viewModel.getLastCart())) + .setNegativeButton(android.R.string.cancel, null) + .show(); + }); + } + + private void onCartLoadError(Throwable throwable) { + Log.e(TAG, "Error while getting cart data.", throwable); + Toast.makeText(this, getString(R.string.error_network), Toast.LENGTH_SHORT) + .show(); + setResult(RESULT_CANCELED); + finish(); + } + + private void onError(Throwable throwable) { + Log.e(TAG, "Error while getting data.", throwable); + Snackbar.make(binding.getRoot(), getString(R.string.error_network), Snackbar.LENGTH_LONG) + .setAction(getString(R.string.action_again), v -> viewModel.onRefresh()) + .show(); + } + + private void onBarcodeScan(String barcode) { + if (barcode == null) { + return; + } + if (viewModel.getLastCart() == null) { + // There is no cart yet. + Log.w(TAG, "onCreate: cart not ready yet..."); + Snackbar.make(binding.getRoot(), "Product niet gevonden.", Snackbar.LENGTH_LONG) + .show(); + return; + } + Product foundProduct = viewModel.getLastCart().getProductFor(barcode); + if (foundProduct == null) { + Log.w(TAG, "onCreate: barcode niet gevonden in map " + barcode); + Snackbar.make(binding.getRoot(), "Product niet gevonden.", Snackbar.LENGTH_LONG) + .show(); + return; + } + Cart newCart = viewModel.getLastCart().addProduct(foundProduct); + saveCart(newCart, false); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + if (requestCode == Manager.getScanner().getRequestCode()) { + // Handle it. + String barcode = Manager.getScanner().interpretActivityResult(data, resultCode); + onBarcodeScan(barcode); + return; + } + super.onActivityResult(requestCode, resultCode, data); + } + + private void updateCartSummary(Cart cart) { + BigDecimal totalAmount = BigDecimal.ZERO; + int totalProducts = 0; + if (cart != null) { + totalAmount = cart.getTotalPrice(); + totalProducts = cart.getTotalProducts(); + } + + // Set texts + String totalString = getString(R.string.wpi_cart_total_amount, currencyFormatter.format(totalAmount)); + binding.cartSummaryAmount.setText(totalString); + String articleString = getResources().getQuantityString(R.plurals.wpi_cart_total_products, totalProducts, totalProducts); + binding.cartSummaryArticles.setText(articleString); + binding.cartPay.setEnabled(totalProducts != 0); + } + + @Override + protected void onStop() { + super.onStop(); + if (viewModel.getLastCart() != null) { + saveCart(viewModel.getLastCart(), true); + } + } + + private void saveCart(Cart toSave, boolean stopping) { + StorageCart storage = toSave.forStorage(); + ExistingCartRequest.saveCartStorage(this, storage); + viewModel.registerLastCart(toSave); + if (!stopping) { + viewModel.requestRefresh(); + } + } + + @Override + public void increment(CartProduct product) { + if (this.viewModel.getLastCart() != null) { + Cart newCart = this.viewModel.getLastCart().increment(product); + saveCart(newCart, false); + } + } + + @Override + public void decrement(CartProduct product) { + if (this.viewModel.getLastCart() != null) { + Cart newCart = this.viewModel.getLastCart().decrement(product); + saveCart(newCart, false); + } + } + + @Override + public void remove(CartProduct product) { + if (this.viewModel.getLastCart() != null) { + Cart newCart = this.viewModel.getLastCart().remove(product); + saveCart(newCart, false); + } + } + + @Override + public void add(Product product) { + if (this.viewModel.getLastCart() != null) { + Cart newCart = this.viewModel.getLastCart().addProduct(product); + saveCart(newCart, false); + } + } + + private void clearCart(boolean stopping) { + Cart newCart = this.viewModel.getLastCart().clear(); + saveCart(newCart, stopping); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + int itemId = item.getItemId(); + if (itemId == R.id.menu_cart_clear) { + this.clearCart(false); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.menu_cart, menu); + + // We need to manually set the color of this Drawable for some reason. + tintToolbarIcons(menu, R.id.menu_cart_clear); + + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + MenuItem item = menu.findItem(R.id.menu_cart_clear); + item.setEnabled(lastEnabledBoolean == null || lastEnabledBoolean); + return super.onPrepareOptionsMenu(menu); + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/CartInteraction.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/CartInteraction.java new file mode 100644 index 000000000..7cf0ab1b1 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/CartInteraction.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tap.cart; + +import java.util.function.Consumer; + +import be.ugent.zeus.hydra.wpi.tap.product.Product; + +/** + * Allows interaction with a cart. + * + * Mainly used to interact with the CartActivity from the Cart Item view holders. + * + * @author Niko Strijbol + */ +interface CartInteraction { + void increment(CartProduct product); + void decrement(CartProduct product); + void remove(CartProduct product); + void add(Product product); +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/CartProduct.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/CartProduct.java new file mode 100644 index 000000000..3c1b60104 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/CartProduct.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tap.cart; + +import java.math.BigDecimal; +import java.util.Locale; +import java.util.Objects; + +import be.ugent.zeus.hydra.common.network.Endpoints; +import be.ugent.zeus.hydra.wpi.tap.product.Product; + +/** + * Represents a product (or more than one) in the user's cart. + * + * Various details about the product are also saved, but this is to reduce + * the amount of network requests we need to make. + * + * @author Niko Strijbol + */ +class CartProduct { + private final int amount; + private final int productId; + private final String name; + private final int price; + private final String thumbnail; + + public CartProduct(Product product, int amount) { + this(amount, product.getId(), product.getName(), product.getPrice(), product.getImageUrl()); + } + + private CartProduct(int amount, int productId, String name, int price, String thumbnail) { + this.amount = amount; + this.productId = productId; + this.name = name; + this.price = price; + this.thumbnail = thumbnail; + } + + public String getName() { + return name; + } + + public int getAmount() { + return amount; + } + + public int getProductId() { + return productId; + } + + public String getThumbnail() { + return thumbnail; + } + + /** + * @return A new product cart with the amount incremented by 1. + */ + public CartProduct increment() { + return new CartProduct(this.amount + 1, this.productId, this.name, this.price, this.thumbnail); + } + + /** + * @return A new product cart with the amount decremented by 1. + */ + public CartProduct decrement() { + return new CartProduct(this.amount - 1, this.productId, this.name, this.price, this.thumbnail); + } + + public BigDecimal getPriceDecimal() { + return new BigDecimal(price).movePointLeft(2); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CartProduct that = (CartProduct) o; + return amount == that.amount && productId == that.productId; + } + + @Override + public int hashCode() { + return Objects.hash(amount, productId); + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/CartProductAdapter.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/CartProductAdapter.java new file mode 100644 index 000000000..c3a9baba8 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/CartProductAdapter.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tap.cart; + +import android.view.ViewGroup; +import androidx.annotation.NonNull; + +import be.ugent.zeus.hydra.R; +import be.ugent.zeus.hydra.common.ui.recyclerview.adapters.DiffAdapter; +import be.ugent.zeus.hydra.common.ui.recyclerview.adapters.EqualsItemCallback; +import be.ugent.zeus.hydra.common.utils.ViewUtils; + +/** + * @author Niko Strijbol + */ +class CartProductAdapter extends DiffAdapter { + + private final CartInteraction interaction; + + CartProductAdapter(CartInteraction interaction) { + super(); + this.interaction = interaction; + } + + @NonNull + @Override + public CartProductViewHolder onCreateViewHolder(@NonNull ViewGroup p, int viewType) { + return new CartProductViewHolder(ViewUtils.inflate(p, R.layout.item_product), interaction); + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/CartProductViewHolder.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/CartProductViewHolder.java new file mode 100644 index 000000000..f4e6faccf --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/CartProductViewHolder.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tap.cart; + +import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; + +import android.util.Log; +import android.view.*; +import android.widget.ImageView; +import android.widget.TextView; + +import java.math.BigDecimal; +import java.text.NumberFormat; +import java.util.Currency; + +import be.ugent.zeus.hydra.R; +import be.ugent.zeus.hydra.common.ui.recyclerview.viewholders.DataViewHolder; +import be.ugent.zeus.hydra.feed.cards.PriorityUtils; + +/** + * View holder for the products in the Tap cart. + * + * @author Niko Strijbol + */ +class CartProductViewHolder extends DataViewHolder implements View.OnCreateContextMenuListener, MenuItem.OnMenuItemClickListener { + + private final ImageView thumbnail; + private final TextView title; + private final TextView description; + private final TextView meta; + private final NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance(); + private final CartInteraction cartInteraction; + + CartProductViewHolder(View v, CartInteraction cartInteraction) { + super(v); + thumbnail = v.findViewById(R.id.thumbnail); + title = v.findViewById(R.id.title); + description = v.findViewById(R.id.description); + meta = v.findViewById(R.id.meta); + currencyFormatter.setCurrency(Currency.getInstance("EUR")); + itemView.setOnCreateContextMenuListener(this); + this.cartInteraction = cartInteraction; + } + + @Override + public void populate(final CartProduct product) { + title.setText(product.getName()); + PriorityUtils.loadThumbnail(itemView.getContext(), product.getThumbnail(), thumbnail); + BigDecimal totalPrice = product.getPriceDecimal().multiply(BigDecimal.valueOf(product.getAmount())); + meta.setText(currencyFormatter.format(totalPrice)); + String unitPrice = currencyFormatter.format(product.getPriceDecimal()); + description.setText(itemView.getContext().getString(R.string.wpi_cart_product_description, product.getAmount(), unitPrice)); + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + MenuInflater inflater = new MenuInflater(v.getContext()); + inflater.inflate(R.menu.menu_cart_item, menu); + for (int i = 0; i < menu.size(); i++) { + MenuItem item = menu.getItem(i); + item.setOnMenuItemClickListener(this); + } + + // Hide clear all based on count. + CartProductAdapter adapter = (CartProductAdapter) getBindingAdapter(); + if (adapter == null) { + return; + } + int position = getBindingAdapterPosition(); + if (position == NO_POSITION) { + return; + } + CartProduct product = adapter.getItem(position); + MenuItem item = menu.findItem(R.id.cart_minus); + item.setVisible(product.getAmount() != 1); + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + CartProductAdapter adapter = (CartProductAdapter) getBindingAdapter(); + if (adapter == null) { + return false; + } + int position = getBindingAdapterPosition(); + if (position == NO_POSITION) { + return false; + } + CartProduct product = adapter.getItem(position); + if (item.getItemId() == R.id.cart_plus) { + cartInteraction.increment(product); + return true; + } else if (item.getItemId() == R.id.cart_minus) { + cartInteraction.decrement(product); + return true; + } else if (item.getItemId() == R.id.cart_delete) { + cartInteraction.remove(product); + return true; + } + return false; + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/CartRequest.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/CartRequest.java new file mode 100644 index 000000000..539557bed --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/CartRequest.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tap.cart; + +import android.content.Context; +import android.os.Bundle; +import android.util.Pair; +import androidx.annotation.NonNull; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +import be.ugent.zeus.hydra.common.request.Request; +import be.ugent.zeus.hydra.common.request.Result; +import be.ugent.zeus.hydra.wpi.tap.barcode.Barcode; +import be.ugent.zeus.hydra.wpi.tap.barcode.BarcodeRequest; +import be.ugent.zeus.hydra.wpi.tap.product.Product; +import be.ugent.zeus.hydra.wpi.tap.product.ProductRequest; + +/** + * @author Niko Strijbol + */ +class CartRequest implements Request { + + private final Request> productRequest; + private final Request existingCartRequest; + private final Request> barcodeRequest; + + public CartRequest(@NonNull Context context) { + this.productRequest = new ProductRequest(context); + this.existingCartRequest = new ExistingCartRequest(context); + this.barcodeRequest = new BarcodeRequest(context); + } + + @NonNull + @Override + public Result execute(@NonNull Bundle args) { + return productRequest + .andThen(existingCartRequest) + .andThen(barcodeRequest) + .map(pair -> { + Map productMap = pair.first.first.stream().collect(Collectors.toMap(Product::getId, Function.identity())); + Map barcodeToProduct = pair.second.stream().collect(Collectors.toMap(Barcode::getCode, Barcode::getProductId)); + return new Cart(pair.first.second, productMap, barcodeToProduct); + }) + .execute(args); + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/CartViewModel.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/CartViewModel.java new file mode 100644 index 000000000..5d9942c28 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/CartViewModel.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tap.cart; + +import android.app.Application; +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import be.ugent.zeus.hydra.common.arch.data.Event; +import be.ugent.zeus.hydra.common.network.NetworkState; +import be.ugent.zeus.hydra.common.request.Request; +import be.ugent.zeus.hydra.common.request.Result; +import be.ugent.zeus.hydra.common.ui.RequestViewModel; +import be.ugent.zeus.hydra.common.utils.ThreadingUtils; + +/** + * @author Niko Strijbol + */ +public class CartViewModel extends RequestViewModel { + + private final MutableLiveData networkState = new MutableLiveData<>(NetworkState.IDLE); + private final MutableLiveData>> requestResult = new MutableLiveData<>(); + private final MutableLiveData lastSeenCart = new MutableLiveData<>(); + + public CartViewModel(Application application) { + super(application); + } + + public LiveData getNetworkState() { + return networkState; + } + + public LiveData>> getRequestResult() { + return requestResult; + } + + public LiveData getLastSeenCart() { + return this.lastSeenCart; + } + + @NonNull + @Override + protected Request getRequest() { + return new CartRequest(getApplication()); + } + + /** + * Send the cart to the server. + * + * @param cart The cart to save. + */ + public void startRequest(Cart cart) { + CreateOrderRequest request = new CreateOrderRequest(getApplication(), cart); + networkState.postValue(NetworkState.BUSY); + ThreadingUtils.execute(() -> { + Result result = request.execute(); + networkState.postValue(NetworkState.IDLE); + requestResult.postValue(new Event<>(result)); + }); + } + + public void registerLastCart(Cart lastSeenCart) { + this.lastSeenCart.setValue(lastSeenCart); + } + + public Cart getLastCart() { + return this.lastSeenCart.getValue(); + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/CreateOrderRequest.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/CreateOrderRequest.java new file mode 100644 index 000000000..86770787d --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/CreateOrderRequest.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tap.cart; + +import android.content.Context; +import android.os.Bundle; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.WorkerThread; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import be.ugent.zeus.hydra.common.network.Endpoints; +import be.ugent.zeus.hydra.common.network.InvalidFormatException; +import be.ugent.zeus.hydra.common.network.OkHttpRequest; +import be.ugent.zeus.hydra.common.request.RequestException; +import be.ugent.zeus.hydra.common.request.Result; +import be.ugent.zeus.hydra.wpi.account.AccountManager; +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.JsonDataException; +import com.squareup.moshi.Types; +import okhttp3.*; + +/** + * Creates a new transaction. + * + * @author Niko Strijbol + */ +class CreateOrderRequest extends OkHttpRequest { + + private final Cart cart; + private final Context context; + + public CreateOrderRequest(@NonNull Context context, @NonNull Cart cart) { + super(context); + this.context = context.getApplicationContext(); + this.cart = cart; + } + + @NonNull + @Override + @WorkerThread + public Result execute(@NonNull Bundle args) { + + MediaType json = MediaType.get("application/json; charset=utf-8"); + + Map>>> data = new HashMap<>(); + data.put("order", cart.forJson()); + Type type = Types.newParameterizedType(Map.class, String.class, Types.newParameterizedType(Map.class, String.class, + Types.newParameterizedType(List.class, Types.newParameterizedType(Map.class, String.class, Object.class))) + ); + JsonAdapter>>>> adapter = moshi.adapter(type); + + String rawData = adapter.toJson(data); + + // Create a request body. + RequestBody body = RequestBody.create(rawData, json); + + // Create the request itself. + Request request = new Request.Builder() + .addHeader("Accept", "application/json") + .addHeader("Content-Type", json.toString()) + .addHeader("Authorization", "Bearer " + AccountManager.getTapKey(context)) + .url(Endpoints.TAP + "users/" + AccountManager.getUsername(context) + "/orders") + .post(body) + .build(); + + try (Response response = client.newCall(request).execute()) { + if (response.isSuccessful()) { + // If the body is null, this is unexpected. + ResponseBody responseBody = response.body(); + if (responseBody == null) { + throw new IOException("Unexpected null body for response"); + } + JsonAdapter resultAdapter = moshi.adapter(OrderResult.class); + OrderResult result = resultAdapter.fromJson(responseBody.source()); + if (result == null || result.getId() == null) { + return Result.Builder.fromException(new RequestException("Unsuccessful transaction.")); + } + return Result.Builder.fromData(result); + } else { + // TODO: unsufficient money is also 403, so handle that better in the activity. + throw new IOException("Unexpected state in request; not successful: got " + response.code()); + } + } catch (JsonDataException | NullPointerException e) { + // Create, log and throw exception, since this is not normal. + String message = "The server did not respond with the expected format when creating a Tap order."; + InvalidFormatException exception = new InvalidFormatException(message, e); + tracker.logError(exception); + return Result.Builder.fromException(exception); + } catch (IOException e) { + // This is an unknown exception, e.g. the network is gone. + RequestException exception = new RequestException(e); + tracker.logError(exception); + return Result.Builder.fromException(exception); + } + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/ExistingCartRequest.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/ExistingCartRequest.java new file mode 100644 index 000000000..1a7cd88e5 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/ExistingCartRequest.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tap.cart; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.preference.PreferenceManager; + +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.Moshi; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Objects; + +import be.ugent.zeus.hydra.common.network.InstanceProvider; +import be.ugent.zeus.hydra.common.request.Request; +import be.ugent.zeus.hydra.common.request.Result; + +/** + * Get an existing cart if there is one available. + * + * @author Niko Strijbol + */ +class ExistingCartRequest implements Request { + + public static final String PREF_CART_STORAGE = "pref_cart_storage"; + + private final Context context; + + ExistingCartRequest(Context context) { + this.context = context.getApplicationContext(); + } + + public static void saveCartStorage(@NonNull Context context, @NonNull StorageCart storage) { + SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(context); + Moshi moshi = InstanceProvider.getMoshi(); + JsonAdapter adapter = moshi.adapter(StorageCart.class); + String raw = adapter.toJson(storage); + p.edit() + .putString(PREF_CART_STORAGE, raw) + .apply(); + } + + public static boolean hasExistingCart(@NonNull Context context) { + SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(context); + return p.contains(PREF_CART_STORAGE); + } + + @NonNull + @Override + public Result execute(@NonNull Bundle args) { + SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(context); + Moshi moshi = InstanceProvider.getMoshi(); + JsonAdapter adapter = moshi.adapter(StorageCart.class); + + StorageCart found; + try { + found = Objects.requireNonNull(adapter.fromJson(p.getString(PREF_CART_STORAGE, ""))); + + // Carts older than this are discarded. + OffsetDateTime oldestLimit = OffsetDateTime.now().minusDays(1); + if (found.getLastEdited().isBefore(oldestLimit)) { + found = null; + } + } catch (IOException | NullPointerException e) { + found = null; + } + + if (found == null) { + found = new StorageCart(new ArrayList<>(), OffsetDateTime.now()); + } + + return Result.Builder.fromData(found); + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/OrderResult.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/OrderResult.java new file mode 100644 index 000000000..cc8a3846d --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/OrderResult.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tap.cart; + +import java.math.BigDecimal; +import java.time.OffsetDateTime; +import java.util.Objects; + +import com.squareup.moshi.Json; + +/** + * The response object of creating a Tap order. + * + * @author Niko Strijbol + */ +class OrderResult { + private Integer id; + @Json(name = "user_id") + private int userId; + @Json(name = "price_cents") + private int price; + @Json(name = "created_at") + private OffsetDateTime createdAt; + @Json(name = "updated_at") + private OffsetDateTime updatedAt; + @Json(name = "transaction_id") + private Integer transactionId; + + public OrderResult() { + // Moshi + } + + public Integer getId() { + return id; + } + + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + public BigDecimal getPrice() { + return new BigDecimal(price).movePointLeft(2); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + OrderResult that = (OrderResult) o; + return userId == that.userId && price == that.price && Objects.equals(id, that.id) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt) && Objects.equals(transactionId, that.transactionId); + } + + @Override + public int hashCode() { + return Objects.hash(id, userId, price, createdAt, updatedAt, transactionId); + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/ProductPickerDialogFragment.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/ProductPickerDialogFragment.java new file mode 100644 index 000000000..8351e9398 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/ProductPickerDialogFragment.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tap.cart; + +import android.app.Dialog; +import android.content.Context; +import android.graphics.Rect; +import android.os.Bundle; +import android.view.*; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.SearchView; +import androidx.core.view.ViewCompat; +import androidx.fragment.app.DialogFragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.function.Consumer; + +import be.ugent.zeus.hydra.R; +import be.ugent.zeus.hydra.common.ui.recyclerview.SpanItemSpacingDecoration; +import be.ugent.zeus.hydra.wpi.tap.product.Product; +import be.ugent.zeus.hydra.wpi.tap.product.ProductAdapter; + +/** + * A dialog fragment allowing the user to search for and pick a product. + * + * @author Niko Strijbol + */ +public class ProductPickerDialogFragment extends DialogFragment implements Consumer { + + private CartInteraction interactor; + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + return super.onCreateDialog(savedInstanceState); + } + + @Override + public void onAttach(@NonNull Context context) { + interactor = (CartInteraction) context; + super.onAttach(context); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_wpi_cart_search, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + // Modify view :( + Rect displayRectangle = new Rect(); + Window window = requireActivity().getWindow(); + window.getDecorView().getWindowVisibleDisplayFrame(displayRectangle); + requireDialog().getWindow().setLayout((int) (displayRectangle.width() * 0.9f), (int) (displayRectangle.width() * 0.9f)); + + ProductAdapter adapter = new ProductAdapter(this); + RecyclerView recyclerView = ViewCompat.requireViewById(view, R.id.recycler_view); + recyclerView.setHasFixedSize(true); + recyclerView.addItemDecoration(new SpanItemSpacingDecoration(requireContext())); + recyclerView.setAdapter(adapter); + + SearchView searchView = ViewCompat.requireViewById(view, R.id.search_view); + searchView.setOnQueryTextListener(adapter); + searchView.setOnCloseListener(adapter); + searchView.setOnSearchClickListener(v -> adapter.onOpen()); + + // There must be a cart in the activity. + // TODO: this is actually not the case when the activity is recreated. + // Either fix it in the activity, or handle it here. + CartViewModel viewModel = new ViewModelProvider(requireActivity()).get(CartViewModel.class); + viewModel.getLastSeenCart().observe(this, cart -> { + adapter.submitData(new ArrayList<>(cart.getProductIdToProduct().values())); + ViewCompat.requireViewById(view, R.id.progress_bar).setVisibility(View.GONE); + }); + } + + @Override + public void accept(Product product) { + // When the item gets clicked. + interactor.add(product); + dismiss(); + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/StorageCart.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/StorageCart.java new file mode 100644 index 000000000..206cf9707 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/cart/StorageCart.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tap.cart; + +import android.util.Pair; + +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Objects; + +/** + * Class to save cart data. + * + * @author Niko Strijbol + */ +class StorageCart { + + private final List> productIdsAndAmounts; + private final OffsetDateTime lastEdited; + + public StorageCart(List> productIds, OffsetDateTime lastEdited) { + this.productIdsAndAmounts = productIds; + this.lastEdited = lastEdited; + } + + public OffsetDateTime getLastEdited() { + return lastEdited; + } + + public List> getProductIds() { + return productIdsAndAmounts; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + StorageCart that = (StorageCart) o; + return productIdsAndAmounts.equals(that.productIdsAndAmounts) && lastEdited.equals(that.lastEdited); + } + + @Override + public int hashCode() { + return Objects.hash(productIdsAndAmounts, lastEdited); + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/product/Product.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/product/Product.java new file mode 100644 index 000000000..19de0e807 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/product/Product.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tap.product; + +import com.squareup.moshi.Json; + +import java.math.BigDecimal; +import java.util.Locale; +import java.util.Objects; + +import be.ugent.zeus.hydra.common.network.Endpoints; + +/** + * Product from Tab + * + * @author Niko Strijbol + */ +public class Product { + + private static final String IMAGE_URL = "system/products/avatars/%s/%s/%s/medium/%s"; + + private int id; + private String name; + @Json(name = "price_cents") + private int price; + @Json(name = "avatar_file_name") + private String avatarFileName; + private String category; + private int stock; + private Integer calories; + + public Product() { + // Moshi + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } + + public int getPrice() { + return price; + } + + public String getAvatarFileName() { + return avatarFileName; + } + + public String getCategory() { + return category; + } + + public int getStock() { + return stock; + } + + public Integer getCalories() { + return calories; + } + + public BigDecimal getPriceDecimal() { + return new BigDecimal(getPrice()).movePointLeft(2); + } + + public String getImageUrl() { + if (this.avatarFileName == null) { + return null; + } + String paddedID = String.format(Locale.ROOT, "%09d", id); + String first = paddedID.substring(0, 3); + String second = paddedID.substring(3, 6); + String third = paddedID.substring(6, 9); + + return Endpoints.TAP + String.format(Locale.ROOT, IMAGE_URL, first, second, third, this.avatarFileName); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Product product = (Product) o; + return id == product.id; + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/product/ProductAdapter.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/product/ProductAdapter.java new file mode 100644 index 000000000..f5323ef83 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/product/ProductAdapter.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tap.product; + +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; + +import java.util.List; +import java.util.Locale; +import java.util.function.BiPredicate; +import java.util.function.Consumer; +import java.util.function.Function; + +import be.ugent.zeus.hydra.R; +import be.ugent.zeus.hydra.common.ui.customtabs.ActivityHelper; +import be.ugent.zeus.hydra.common.ui.recyclerview.adapters.DiffAdapter; +import be.ugent.zeus.hydra.common.ui.recyclerview.adapters.SearchableAdapter; +import be.ugent.zeus.hydra.common.utils.ViewUtils; +import be.ugent.zeus.hydra.news.NewsArticle; + +/** + * @author Niko Strijbol + */ +public class ProductAdapter extends SearchableAdapter { + + private final Consumer onClickListener; + + public ProductAdapter() { + this(null); + } + + public ProductAdapter(Consumer onClickListener) { + super(p -> p.getName().toLowerCase(Locale.getDefault())); + this.onClickListener = onClickListener; + } + + @NonNull + @Override + public ProductViewHolder onCreateViewHolder(@NonNull ViewGroup p, int viewType) { + return new ProductViewHolder(ViewUtils.inflate(p, R.layout.item_product), onClickListener); + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/product/ProductFragment.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/product/ProductFragment.java new file mode 100644 index 000000000..c4189c6c5 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/product/ProductFragment.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tap.product; + +import android.content.SharedPreferences; +import android.os.Bundle; +import android.util.Log; +import android.view.*; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.preference.PreferenceManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import be.ugent.zeus.hydra.R; +import be.ugent.zeus.hydra.common.arch.observers.AdapterObserver; +import be.ugent.zeus.hydra.common.arch.observers.PartialErrorObserver; +import be.ugent.zeus.hydra.common.arch.observers.ProgressObserver; +import be.ugent.zeus.hydra.common.ui.recyclerview.SpanItemSpacingDecoration; +import be.ugent.zeus.hydra.common.utils.ColourUtils; +import be.ugent.zeus.hydra.wpi.account.CombinedUserViewModel; +import com.google.android.material.snackbar.Snackbar; + +import static be.ugent.zeus.hydra.wpi.tap.product.ProductViewModel.PREF_SHOW_ONLY_IN_STOCK; + +/** + * Display TAP products. + * + * @author Niko Strijbol + */ +public class ProductFragment extends Fragment { + + private static final String TAG = "ProductFragment"; + private ProductViewModel viewModel; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_product, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + RecyclerView recyclerView = view.findViewById(R.id.recycler_view); + recyclerView.setHasFixedSize(true); + recyclerView.addItemDecoration(new SpanItemSpacingDecoration(requireContext())); + ProductAdapter adapter = new ProductAdapter(); + recyclerView.setAdapter(adapter); + + SwipeRefreshLayout swipeRefreshLayout = view.findViewById(R.id.swipeRefreshLayout); + swipeRefreshLayout.setColorSchemeColors(ColourUtils.resolveColour(requireContext(), R.attr.colorSecondary)); + + viewModel = new ViewModelProvider(this).get(ProductViewModel.class); + viewModel.getData().observe(getViewLifecycleOwner(), PartialErrorObserver.with(this::onError)); + viewModel.getData().observe(getViewLifecycleOwner(), new ProgressObserver<>(view.findViewById(R.id.progress_bar))); + viewModel.getData().observe(getViewLifecycleOwner(), new AdapterObserver<>(adapter)); + viewModel.getRefreshing().observe(getViewLifecycleOwner(), swipeRefreshLayout::setRefreshing); + + // For refreshing, we request a refresh from the parent activity. + // We then listen to the parent refresh state and refresh this fragment when the parent is refreshing. + CombinedUserViewModel activityViewModel = new ViewModelProvider(requireActivity()).get(CombinedUserViewModel.class); + swipeRefreshLayout.setOnRefreshListener(activityViewModel); + activityViewModel.getRefreshing().observe(getViewLifecycleOwner(), refreshing -> { + if (refreshing) { + viewModel.onRefresh(); + } + }); + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + inflater.inflate(R.menu.menu_wpi_products, menu); + MenuItem item = menu.findItem(R.id.action_filter_stock); + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()); + boolean show = preferences.getBoolean(PREF_SHOW_ONLY_IN_STOCK, true); + item.setChecked(show); + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.action_filter_stock) { + boolean checked = !item.isChecked(); + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()); + preferences.edit() + .putBoolean(PREF_SHOW_ONLY_IN_STOCK, checked) + .apply(); + item.setChecked(checked); + viewModel.requestRefresh(); + } + + return super.onOptionsItemSelected(item); + } + + private void onError(Throwable throwable) { + Log.e(TAG, "Error while getting data.", throwable); + Snackbar.make(requireView(), getString(R.string.error_network), Snackbar.LENGTH_LONG) + .setAction(getString(R.string.action_again), v -> viewModel.requestRefresh()) + .show(); + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/product/ProductRequest.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/product/ProductRequest.java new file mode 100644 index 000000000..4d8e1608a --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/product/ProductRequest.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2022 The Hydra authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tap.product; + +import android.content.Context; +import android.os.Bundle; +import androidx.annotation.NonNull; + +import java.time.Duration; + +import be.ugent.zeus.hydra.common.network.Endpoints; +import be.ugent.zeus.hydra.common.network.JsonArrayRequest; +import be.ugent.zeus.hydra.wpi.account.AccountManager; +import okhttp3.Request; + +/** + * @author Niko Strijbol + */ +public class ProductRequest extends JsonArrayRequest { + + private final Context context; + + public ProductRequest(Context context) { + super(context, Product.class); + this.context = context.getApplicationContext(); + } + + @Override + protected Request.Builder constructRequest(@NonNull Bundle arguments) { + Request.Builder builder = super.constructRequest(arguments); + builder.addHeader("Authorization", "Bearer " + AccountManager.getTapKey(context)); + return builder; + } + + @NonNull + @Override + protected String getAPIUrl() { + return Endpoints.TAP + "products"; + } + + @Override + public Duration getCacheDuration() { + return Duration.ofDays(1); + } +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/product/ProductViewHolder.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/product/ProductViewHolder.java new file mode 100644 index 000000000..1055cafe2 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/product/ProductViewHolder.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tap.product; + +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.Nullable; + +import org.w3c.dom.Text; + +import java.text.NumberFormat; +import java.util.Currency; +import java.util.function.Consumer; + +import be.ugent.zeus.hydra.R; +import be.ugent.zeus.hydra.common.ui.recyclerview.viewholders.DataViewHolder; +import be.ugent.zeus.hydra.feed.cards.PriorityUtils; + +/** + * View holder for the products in the TAP fragment. + * + * @author Niko Strijbol + */ +class ProductViewHolder extends DataViewHolder { + + private final ImageView thumbnail; + private final TextView title; + private final TextView description; + private final TextView meta; + private final NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance(); + private final NumberFormat decimalFormatter = NumberFormat.getNumberInstance(); + private final Consumer onClickListener; + + ProductViewHolder(View v, @Nullable Consumer onClickListener) { + super(v); + thumbnail = v.findViewById(R.id.thumbnail); + title = v.findViewById(R.id.title); + description = v.findViewById(R.id.description); + meta = v.findViewById(R.id.meta); + currencyFormatter.setCurrency(Currency.getInstance("EUR")); + this.onClickListener = onClickListener; + } + + @Override + public void populate(final Product product) { + title.setText(product.getName()); + PriorityUtils.loadThumbnail(itemView.getContext(), product.getImageUrl(), thumbnail); + meta.setText(currencyFormatter.format(product.getPriceDecimal())); + String calories; + if (product.getCalories() != null) { + calories = decimalFormatter.format(product.getCalories()) + " kcal"; + } else { + calories = itemView.getContext().getString(R.string.wpi_product_na); + } + description.setText(itemView.getContext().getString(R.string.wpi_product_description, calories, product.getStock())); + if (onClickListener != null) { + // TODO: should this move to the constructor? + itemView.setOnClickListener(v -> onClickListener.accept(product)); + } + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/product/ProductViewModel.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/product/ProductViewModel.java new file mode 100644 index 000000000..b783d68a2 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/product/ProductViewModel.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tap.product; + +import android.app.Application; +import android.content.SharedPreferences; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.preference.PreferenceManager; + +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +import be.ugent.zeus.hydra.common.request.Request; +import be.ugent.zeus.hydra.common.ui.PreferenceFragment; +import be.ugent.zeus.hydra.common.ui.RequestViewModel; + +/** + * @author Niko Strijbol + */ +public class ProductViewModel extends RequestViewModel> { + public static final String PREF_SHOW_ONLY_IN_STOCK = "pref_wpi_filter_stock"; + + public ProductViewModel(Application application) { + super(application); + } + + @NonNull + @Override + protected Request> getRequest() { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplication()); + return new ProductRequest(getApplication()).map(products -> { + boolean show = preferences.getBoolean(PREF_SHOW_ONLY_IN_STOCK, true); + if (show) { + return products.stream().filter(p -> p.getStock() > 0).collect(Collectors.toList()); + } else { + return products; + } + }); + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/user/TapUser.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/user/TapUser.java new file mode 100644 index 000000000..bcdb5c0bf --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/user/TapUser.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tap.user; + +import com.squareup.moshi.Json; + +import java.util.Locale; +import java.util.Objects; + +import be.ugent.zeus.hydra.common.network.Endpoints; + +/** + * Represents the Tap user. + * + * @author Niko Strijbol + */ +public class TapUser { + + private static final String IMAGE_URL = "system/users/avatars/%s/%s/%s/medium/%s"; + + private int id; + @Json(name = "avatar_file_name") + private String avatarFileName; + @Json(name = "orders_count") + private int orderCount; + private String name; + + public TapUser() { + // Needed for Moshi + } + + public int getId() { + return id; + } + + public String getAvatarFileName() { + return avatarFileName; + } + + public int getOrderCount() { + return orderCount; + } + + public String getName() { + return name; + } + + public String getProfileImageUrl() { + if (this.avatarFileName == null) { + return null; + } + String paddedID = String.format(Locale.ROOT, "%09d", id); + String first = paddedID.substring(0, 3); + String second = paddedID.substring(3, 6); + String third = paddedID.substring(6, 9); + + return Endpoints.TAP + String.format(Locale.ROOT, IMAGE_URL, first, second, third, this.avatarFileName); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TapUser tapUser = (TapUser) o; + return id == tapUser.id; + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/user/TapUserRequest.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/user/TapUserRequest.java new file mode 100644 index 000000000..55f5cda47 --- /dev/null +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tap/user/TapUserRequest.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.wpi.tap.user; + +import android.content.Context; +import android.os.Bundle; +import androidx.annotation.NonNull; + +import java.time.Duration; + +import be.ugent.zeus.hydra.common.network.Endpoints; +import be.ugent.zeus.hydra.common.network.JsonOkHttpRequest; +import be.ugent.zeus.hydra.wpi.account.AccountManager; +import okhttp3.Request; + +/** + * Get a Tap user. + * + * @author Niko Strijbol + */ +public class TapUserRequest extends JsonOkHttpRequest { + + private final Context context; + + public TapUserRequest(Context context) { + super(context, TapUser.class); + this.context = context.getApplicationContext(); + } + + @NonNull + @Override + protected String getAPIUrl() { + return Endpoints.TAP + "users/" + AccountManager.getUsername(context); + } + + @Override + protected Request.Builder constructRequest(@NonNull Bundle arguments) { + Request.Builder builder = super.constructRequest(arguments); + builder.addHeader("Authorization", "Bearer " + AccountManager.getTapKey(context)); + return builder; + } + + @Override + public Duration getCacheDuration() { + // Do not cache this at the moment. + return Duration.ZERO; + } +} diff --git a/app/src/main/res/drawable/ic_bank_transfer.xml b/app/src/main/res/drawable/ic_bank_transfer.xml new file mode 100644 index 000000000..c54f46da7 --- /dev/null +++ b/app/src/main/res/drawable/ic_bank_transfer.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_bank_transfer_in.xml b/app/src/main/res/drawable/ic_bank_transfer_in.xml new file mode 100644 index 000000000..19738dfe2 --- /dev/null +++ b/app/src/main/res/drawable/ic_bank_transfer_in.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_bank_transfer_out.xml b/app/src/main/res/drawable/ic_bank_transfer_out.xml new file mode 100644 index 000000000..cc3b93b86 --- /dev/null +++ b/app/src/main/res/drawable/ic_bank_transfer_out.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_barcode_scan.xml b/app/src/main/res/drawable/ic_barcode_scan.xml new file mode 100644 index 000000000..f820dfaaf --- /dev/null +++ b/app/src/main/res/drawable/ic_barcode_scan.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_bowl_outline.xml b/app/src/main/res/drawable/ic_bowl_outline.xml index 5996921f4..4195581c7 100644 --- a/app/src/main/res/drawable/ic_bowl_outline.xml +++ b/app/src/main/res/drawable/ic_bowl_outline.xml @@ -1,3 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/ic_login.xml b/app/src/main/res/drawable/ic_login.xml new file mode 100644 index 000000000..301222406 --- /dev/null +++ b/app/src/main/res/drawable/ic_login.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_receipt_long.xml b/app/src/main/res/drawable/ic_receipt_long.xml new file mode 100644 index 000000000..0dcf7617b --- /dev/null +++ b/app/src/main/res/drawable/ic_receipt_long.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_remove_shopping_cart.xml b/app/src/main/res/drawable/ic_remove_shopping_cart.xml new file mode 100644 index 000000000..a9a17fea8 --- /dev/null +++ b/app/src/main/res/drawable/ic_remove_shopping_cart.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_shopping_cart.xml b/app/src/main/res/drawable/ic_shopping_cart.xml new file mode 100644 index 000000000..44465977c --- /dev/null +++ b/app/src/main/res/drawable/ic_shopping_cart.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_shopping_cart_checkout.xml b/app/src/main/res/drawable/ic_shopping_cart_checkout.xml new file mode 100644 index 000000000..6e5d60929 --- /dev/null +++ b/app/src/main/res/drawable/ic_shopping_cart_checkout.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/app/src/main/res/drawable/logo_tap.xml b/app/src/main/res/drawable/logo_tap.xml new file mode 100644 index 000000000..80ca16f9e --- /dev/null +++ b/app/src/main/res/drawable/logo_tap.xml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/app/src/main/res/layout/activity_wpi.xml b/app/src/main/res/layout/activity_wpi.xml new file mode 100644 index 000000000..0d7c0f9bc --- /dev/null +++ b/app/src/main/res/layout/activity_wpi.xml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_wpi_api_key_management.xml b/app/src/main/res/layout/activity_wpi_api_key_management.xml new file mode 100644 index 000000000..a87b82001 --- /dev/null +++ b/app/src/main/res/layout/activity_wpi_api_key_management.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_wpi_tab_transaction_form.xml b/app/src/main/res/layout/activity_wpi_tab_transaction_form.xml new file mode 100644 index 000000000..c6fba90bb --- /dev/null +++ b/app/src/main/res/layout/activity_wpi_tab_transaction_form.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_wpi_tap_cart.xml b/app/src/main/res/layout/activity_wpi_tap_cart.xml new file mode 100644 index 000000000..b18d2a9da --- /dev/null +++ b/app/src/main/res/layout/activity_wpi_tap_cart.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_product.xml b/app/src/main/res/layout/fragment_product.xml new file mode 100644 index 000000000..1243d1c4f --- /dev/null +++ b/app/src/main/res/layout/fragment_product.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_wpi_cart_search.xml b/app/src/main/res/layout/fragment_wpi_cart_search.xml new file mode 100644 index 000000000..375472332 --- /dev/null +++ b/app/src/main/res/layout/fragment_wpi_cart_search.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_product.xml b/app/src/main/res/layout/item_product.xml new file mode 100644 index 000000000..5b43199f6 --- /dev/null +++ b/app/src/main/res/layout/item_product.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_transaction.xml b/app/src/main/res/layout/item_transaction.xml new file mode 100644 index 000000000..b3743f193 --- /dev/null +++ b/app/src/main/res/layout/item_transaction.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/drawer_items.xml b/app/src/main/res/menu/drawer_items.xml index 826d8a429..5429cd8cb 100644 --- a/app/src/main/res/menu/drawer_items.xml +++ b/app/src/main/res/menu/drawer_items.xml @@ -1,7 +1,7 @@ - - @color/md_theme_dark_primary - +

+ + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_cart_item.xml b/app/src/main/res/menu/menu_cart_item.xml new file mode 100644 index 000000000..c7b275a65 --- /dev/null +++ b/app/src/main/res/menu/menu_cart_item.xml @@ -0,0 +1,35 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_wpi.xml b/app/src/main/res/menu/menu_wpi.xml new file mode 100644 index 000000000..0cb645cff --- /dev/null +++ b/app/src/main/res/menu/menu_wpi.xml @@ -0,0 +1,32 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_wpi_products.xml b/app/src/main/res/menu/menu_wpi_products.xml new file mode 100644 index 000000000..65b8a1a11 --- /dev/null +++ b/app/src/main/res/menu/menu_wpi_products.xml @@ -0,0 +1,33 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index a78460a88..775f0393c 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -1,6 +1,6 @@ + API key management + Calories: %1$s • In stock: %2$d + n/a + Only in stock + Balance: %1$s • Orders: %2$s + Products + Transactions + Tab transfer + Zeus member + Who you want to give money to or request money from. The Zeus username must be spelled exactly correct. Due to limitations in the API, there is no autocomplete for this field yet. + Amount + If the amount is negative and you are not an admin, this will become a request, not a transfer. + Description + Confirm + Transaction saved + Request made + Sure? You will give %1$s to %2$s. + Sure? You will demand %1$s from %2$s (or you will take it if you are admin). + There was an error while creating the transaction. Check that the transaction has not been saved before trying it again. + This Zeus member was not found. + Zeus is not a bank; debt is forbidden. + %1$d ⨯ %2$s + Total: %1$s + + %d product + %d products + + Clear + Tap order + Add 1 + Remove 1 + Remove product + Pay + Scan + Search + Order + Transfer + There was an error while creating the order. Check that the order has not been saved before trying it again. + Ordered for %1$s + Sure? The total of your order is %1$s. + Zeus mode is enabled. + + Press %d more time… + Press %d more times… + + Username + API key for Tab + API key for Tap more diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 3d3eb2bf3..71feed21e 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -55,4 +55,39 @@ @android:color/transparent false + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 4729d039f..ead5643c9 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,5 +1,4 @@ - - @color/material_color_grey_100 @color/md_theme_light_primary + + + #984800 + #FFFFFF + #FFDBC4 + #321300 + #984800 + #FFFFFF + #FFDBC4 + #321300 + #984800 + #FFFFFF + #FFDBC4 + #321300 + #B3261E + #F9DEDC + #FFFFFF + #410E0B + #FCFCFC + #201A17 + #FCFCFC + #201A17 + #E7E0EC + #49454F + #79747E + #FBEEE8 + #362F2B + #FFB684 + #000000 + #FFB684 + #FFB684 + #512300 + #743500 + #FFDBC4 + #FFB684 + #512300 + #743500 + #FFDBC4 + #FFB684 + #512300 + #743500 + #FFDBC4 + #F2B8B5 + #8C1D18 + #601410 + #F9DEDC + #201A17 + #ECE0DA + #201A17 + #ECE0DA + #49454F + #CAC4D0 + #938F99 + #201A17 + #ECE0DA + #984800 + #000000 + #984800 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 961675573..7d58e4083 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,6 +1,6 @@ + Beheer van API-sleutels + Calorieën: %1$s • In voorraad: %2$d + n.v.t. + Enkel in voorraad + Saldo: %1$s • Bestellingen: %2$s + Producten + Transacties + Tab-overschrijving + Zeuslid + Aan wie u geld wilt geven of vragen. De Zeus-gebruikersnaam moet exact juist gespeld zijn. Door beperkingen in de API is nog geen automatische aanvulling in dit veld. + Bedrag + Als het bedrag negatief is, en u bent geen admin, dan zal dit een verzoek worden in plaats van een transactie. + Beschrijving + Bevestigen + Transactie gemaakt + Aanvraag gemaakt + Zeker? U geeft %1$s aan %2$s. + Zeker? U vraagt %1$s aan %2$s (of u neemt het als u admin bent). + Er trad een fout op bij het maken van de transactie. Controleer dat de transactie er niet toch doorgekomen is voor u het opnieuw probeert. + Dit Zeuslid werd niet gevonden. + Zeus is geen bank; schulden zijn verboden! + %1$d ⨯ %2$s + Totaal: %1$s + + %d product + %d producten + + Leegmaken + Tapbestelling + 1 toevoegen + 1 verwijderen + Product verwijderen + Afrekenen + Scannen + Zoeken + Bestellen + Overschrijven + Er trad een fout op bij de bestelling. Controleer dat de bestelling er niet toch doorgekomen is voor u het opnieuw probeert. + Besteld voor %1$s + Zeker? Het totaal van uw bestelling is %1$s. + Zeus-modus is ingeschakeld. + + Druk nog %d keer… + + Gebruikersnaam + API-sleutel voor Tab + API-sleutel voor Tap + meer Schamperfoto diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 99871f343..c727acc9e 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -32,4 +32,9 @@ 1dp ?android:attr/listDivider + + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index f273dde1b..ae4d5eef1 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -67,4 +67,39 @@ true true + + diff --git a/app/src/main/res/xml/pref_about.xml b/app/src/main/res/xml/pref_about.xml index b32a34473..7bab969a9 100644 --- a/app/src/main/res/xml/pref_about.xml +++ b/app/src/main/res/xml/pref_about.xml @@ -64,12 +64,7 @@ app:key="pref_about_creator_zeus" app:persistent="false" app:singleLineTitle="false" - app:title="@string/pref_about_creator_zeus" - tools:ignore="UnusedAttribute"> - - + app:title="@string/pref_about_creator_zeus" /> - - + + + + + diff --git a/app/src/open/java/be/ugent/zeus/hydra/common/barcode/Manager.java b/app/src/open/java/be/ugent/zeus/hydra/common/barcode/Manager.java new file mode 100644 index 000000000..554cc333b --- /dev/null +++ b/app/src/open/java/be/ugent/zeus/hydra/common/barcode/Manager.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.common.barcode; + +import be.ugent.zeus.hydra.common.scanner.BarcodeScanner; + +/** + * Get a barcode scanner. + * + * @author Niko Strijbol + */ +public class Manager { + + public static BarcodeScanner getScanner() { + return new OpenBarcodeScanner(); + } +} diff --git a/app/src/open/java/be/ugent/zeus/hydra/common/barcode/OpenBarcodeScanner.java b/app/src/open/java/be/ugent/zeus/hydra/common/barcode/OpenBarcodeScanner.java new file mode 100644 index 000000000..ce9132682 --- /dev/null +++ b/app/src/open/java/be/ugent/zeus/hydra/common/barcode/OpenBarcodeScanner.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.common.barcode; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import androidx.annotation.Nullable; + +import java.util.function.Consumer; + +import be.ugent.zeus.hydra.common.scanner.BarcodeScanner; +import com.google.zxing.integration.android.IntentIntegrator; +import com.google.zxing.integration.android.IntentResult; + +/** + * @author Niko Strijbol + */ +class OpenBarcodeScanner implements BarcodeScanner { + + @Override + public boolean needsActivity() { + return true; + } + + @Override + public Intent getActivityIntent(Activity activity) { + IntentIntegrator integrator = new IntentIntegrator(activity); + integrator.setDesiredBarcodeFormats(IntentIntegrator.PRODUCT_CODE_TYPES); + return integrator.createScanIntent(); + } + + public int getRequestCode() { + return IntentIntegrator.REQUEST_CODE; + } + + @Override + @Nullable + public String interpretActivityResult(Intent data, int resultCode) { + IntentResult result = IntentIntegrator.parseActivityResult(resultCode, data); + return result.getContents(); + } + + @Override + public void getBarcode(Context context, Consumer onSuccess, Consumer onError) { + throw new UnsupportedOperationException("This Barcode Scanner requires an activity."); + } +} diff --git a/app/src/store/java/be/ugent/zeus/hydra/common/barcode/GoogleBarcodeScanner.java b/app/src/store/java/be/ugent/zeus/hydra/common/barcode/GoogleBarcodeScanner.java new file mode 100644 index 000000000..94a9c12de --- /dev/null +++ b/app/src/store/java/be/ugent/zeus/hydra/common/barcode/GoogleBarcodeScanner.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.common.barcode; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; + +import be.ugent.zeus.hydra.common.request.RequestException; +import be.ugent.zeus.hydra.common.request.Result; +import be.ugent.zeus.hydra.common.scanner.BarcodeScanner; +import com.google.android.gms.tasks.OnFailureListener; +import com.google.android.gms.tasks.OnSuccessListener; +import com.google.android.gms.tasks.Tasks; +import com.google.mlkit.vision.barcode.common.Barcode; +import com.google.mlkit.vision.codescanner.GmsBarcodeScanner; +import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions; +import com.google.mlkit.vision.codescanner.GmsBarcodeScanning; + +/** + * @author Niko Strijbol + */ +class GoogleBarcodeScanner implements BarcodeScanner { + + @Override + public boolean needsActivity() { + return false; + } + + @Override + public Intent getActivityIntent(Activity activity) { + throw new UnsupportedOperationException("This Barcode Scanner does not use an activity."); + } + + @Override + public int getRequestCode() { + throw new UnsupportedOperationException("This Barcode Scanner does not use an activity."); + } + + @Nullable + @Override + public String interpretActivityResult(Intent data, int resultCode) { + throw new UnsupportedOperationException("This Barcode Scanner does not use an activity."); + } + + @Override + public void getBarcode(Context context, Consumer onSuccess, Consumer onError) { + GmsBarcodeScannerOptions options = new GmsBarcodeScannerOptions.Builder() + .setBarcodeFormats( + Barcode.FORMAT_EAN_13, + Barcode.FORMAT_EAN_8, + // Americans... + Barcode.FORMAT_UPC_E, + Barcode.FORMAT_UPC_A) + .build(); + GmsBarcodeScanner scanner = GmsBarcodeScanning.getClient(context, options); + scanner.startScan() + .addOnSuccessListener(barcode -> onSuccess.accept(barcode.getRawValue())) + .addOnFailureListener(onError::accept); + } +} diff --git a/app/src/store/java/be/ugent/zeus/hydra/common/barcode/Manager.java b/app/src/store/java/be/ugent/zeus/hydra/common/barcode/Manager.java new file mode 100644 index 000000000..23bec18cc --- /dev/null +++ b/app/src/store/java/be/ugent/zeus/hydra/common/barcode/Manager.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 Niko Strijbol + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package be.ugent.zeus.hydra.common.barcode; + +import be.ugent.zeus.hydra.common.scanner.BarcodeScanner; + +/** + * Get a barcode scanner. + * + * @author Niko Strijbol + */ +public class Manager { + + public static BarcodeScanner getScanner() { + return new GoogleBarcodeScanner(); + } +} diff --git a/material-intro/build.gradle b/material-intro/build.gradle index d1d1e2917..fa515ea61 100644 --- a/material-intro/build.gradle +++ b/material-intro/build.gradle @@ -1,12 +1,12 @@ apply plugin: 'com.android.library' android { - compileSdkVersion 31 - buildToolsVersion "31.0.0" + compileSdkVersion 32 + buildToolsVersion "32.0.0" defaultConfig { minSdkVersion 21 - targetSdkVersion 31 + targetSdkVersion 32 versionCode 1 versionName "1.0.0" } @@ -23,7 +23,10 @@ android { showAll true disable "Overdraw", // Allow overdraw, mainly used for selectable backgrounds // We decide when we upgrade - "OldTargetApi" + "OldTargetApi", + // We use dependabot for dependencies + "GradleDependency", + "ObsoleteLintCustomCheck" } } @@ -31,5 +34,5 @@ android { dependencies { implementation 'androidx.appcompat:appcompat:1.4.1' implementation 'androidx.constraintlayout:constraintlayout:2.1.3' - implementation 'com.google.android.material:material:1.5.0' + implementation 'com.google.android.material:material:1.6.0' }