SDPepe/AppArt

View on GitHub
app/src/main/java/ch/epfl/sdp/appart/ad/AdViewModel.java

Summary

Maintainability
A
0 mins
Test Coverage
A
94%
package ch.epfl.sdp.appart.ad;

import android.util.Pair;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModel;

import java.util.List;
import java.util.concurrent.CompletableFuture;

import javax.inject.Inject;

import ch.epfl.sdp.appart.database.DatabaseService;
import ch.epfl.sdp.appart.database.local.LocalDatabaseService;
import dagger.hilt.android.lifecycle.HiltViewModel;

/**
 * ViewModel for the AdActivity.
 * <p>
 * It contains LiveData for the information showed in an Ad page.
 */
@HiltViewModel
public class AdViewModel extends ViewModel {

    final DatabaseService db;
    final LocalDatabaseService localdb;

    private Ad ad;
    private final MutableLiveData<String> adTitle = new MutableLiveData<>();
    private final MutableLiveData<String> adAddress = new MutableLiveData<>();
    private final MutableLiveData<String> adPrice = new MutableLiveData<>();
    private final MutableLiveData<String> adDescription =
            new MutableLiveData<>();
    private final MutableLiveData<String> adAdvertiserName =
            new MutableLiveData<>();
    private final MutableLiveData<String> adAdvertiserId =
            new MutableLiveData<>();
    private final MutableLiveData<Pair<List<String>, Boolean>> adPhotosReferences =
            new MutableLiveData<>();
    private final MutableLiveData<Pair<List<String>, Boolean>> panoramasReferences =
            new MutableLiveData<>();
    private final MutableLiveData<Boolean> hasVTour = new MutableLiveData<>();
    private final MutableLiveData<Boolean> hasLoaded = new MutableLiveData<>();

    @Inject
    public AdViewModel(DatabaseService db, LocalDatabaseService localdb) {
        this.db = db;
        this.localdb = localdb;
    }

    /**
     * Loads and sets ad info from the local database. It then tries to fetch
     * the ad info from
     * the database. If successful, sets the ad info with the new data form
     * the server.
     *
     * @param id the unique ID of the ad in the database
     * @return a completable future that is normally completed if the server
     * fetch was successful
     */
    public CompletableFuture<Void> initAd(String id) {
        CompletableFuture<Void> result = new CompletableFuture<>();
        localLoad(id).whenComplete((res, e) -> fetchAndSet(result, id));
        return result;
    }

    public <T> void observePanoramasReferences(LifecycleOwner owner,
                                               @NonNull Observer<?
                                                       super Pair<List<String>, Boolean>> observer) {
        panoramasReferences.observe(owner, observer);
    }

    // Getters
    public LiveData<String> getTitle() {
        return adTitle;
    }

    public LiveData<Pair<List<String>, Boolean>> getPhotosRefs() {
        return adPhotosReferences;
    }

    public LiveData<String> getAddress() {
        return adAddress;
    }

    public LiveData<String> getPrice() {
        return adPrice;
    }

    public LiveData<String> getDescription() {
        return adDescription;
    }

    public LiveData<String> getAdAdvertiserName() {
        return adAdvertiserName;
    }

    public LiveData<String> getAdvertiserId() {
        return adAdvertiserId;
    }

    public LiveData<Boolean> getHasVTour() {
        return hasVTour;
    }

    public LiveData<Boolean> getHasLoaded() {
        return hasLoaded;
    }

    @Nullable
    public Ad getAd() {
        return ad;
    }

    /**
     * Loads the ad data from the local database and sets the values to
     * mutablelivedata fields.
     */
    private CompletableFuture<Void> localLoad(String adId) {
        CompletableFuture<Void> result = new CompletableFuture<>();
        setAdValues(result, localdb.getAd(adId), true);
        return result;
    }

    /**
     * Fetches ad data from the server
     */
    private void fetchAndSet(CompletableFuture<Void> result, String adId) {
        setAdValues(result, db.getAd(adId), false);
    }

    /**
     * Helper to set values from an ad
     */
    private void setAdValues(CompletableFuture<Void> result,
                             CompletableFuture<Ad> adRes, boolean isLocal) {
        adRes.exceptionally(e -> {
            result.completeExceptionally(e);
            return null;
        });
        adRes.thenAccept(ad -> {
            /*
            If the ad is null then the ad isn't stored locally --> it is
            not on disk
            Having a null ad here probably caused a NullPointerException, and
             since this is executed in another
            thread, it just disappeared... And therefore, the future was
            never completed,
            which resulted in fetchAndSet never being called.
            If an ad is null, we can just complete with null directly.
            We could also modify the behavior of the get in the local
            database to maybe
            complete exceptionally when an id is not present in the data
            structure.
            However, this is probably the simplest solution.
            */
            if (ad == null) {
                result.complete(null);
                return;
            }
            this.ad = ad;

            /*
                We need to use postValue because setValue only works from the
                 main thread. However, this lambda ban be called from a
                 background thread.
             */
            this.adAddress.postValue(addressFrom(ad.getStreet(), ad.getCity()));
            this.adTitle.postValue(ad.getTitle());
            this.adPrice.postValue(priceFrom(ad.getPrice(),
                    ad.getPricePeriod()));
            this.adDescription.postValue(ad.getDescription());
            this.adAdvertiserName.postValue(ad.getAdvertiserName());
            this.adAdvertiserId.postValue(ad.getAdvertiserId());
            this.adPhotosReferences.postValue(new Pair(ad.getPhotosRefs(),
                    isLocal));
            this.panoramasReferences.postValue(new Pair(ad.getPanoramaReferences(), isLocal));
            /*
                This is not exactly set as the equivalent of the hasVRTour
                attribute. However, this modification prevent some crashes in
                 the PanoramaActivity.
                 Some ads have the boolean hasVTour at false, while they have
                  panoramaReferences.
                 I think this is because while building an ad you have to
                 explicitly set the hasVTour and this can be forgotten.
                 Maybe here we can only rely on the panoramaReferences size.
             */
            this.hasVTour.postValue(ad.getPanoramaReferences().size() > 0);
            this.hasLoaded.postValue(true);
            result.complete(null);
        });
    }

    /**
     * Helper to concatenate an address string
     */
    private String addressFrom(String street, String city) {
        return street + ", " + city;
    }

    /**
     * Helper to concatenate a price
     */
    private String priceFrom(long price, PricePeriod period) {
        return price + " / " + period.toString();
    }
}