firebase / FirebaseUI-Android

Optimized UI components for Firebase
https://firebaseopensource.com/projects/firebase/firebaseui-android/
Apache License 2.0
4.63k stars 1.83k forks source link

Firebase-ui-storage direct copy into local cache #331

Open peterhav opened 8 years ago

peterhav commented 8 years ago

This is a feature request related to the new firebase-ui-storage library available since release 0.6.0:

I’m planning on using the provided integration with Glide to (efficiently) show images stored in Firebase Storage within my app. In my app I have the following workflow that is more or less the same as the sample:

  1. User selects image from the camera or gallery (using intents)
  2. The URI returned is used to initiate the Firebase Storage upload
  3. Wait period (progress dialog), once the onSuccess is triggered:
  4. Change Firebase database, set: hasImage = true
  5. The image is loaded into the relevant imageviews using the Glide call (with FirebaseImageLoader)

As you can see this is not optimal: step 3 could be omitted by directly inserting the selected image into the local cache. My question: is this currently possible or is this type of functionality foreseen for a future release?

asciimike commented 8 years ago

Can you just use the image you took as the placeholder image? Glide supports this out of the box (using the placeholder() method), so it's not anything we should have to do:

  Glide.with(this)
            .using(new FirebaseImageLoader())
            .placeholder(yourImageAsset)
            .load(storageReference)
            .into(imageView);

That said, this does incur the overhead of having to download the thing you just loaded (granted, this will be the behavior on every device other than the uploading user's). Glide talks some about it's cache invalidation here, and I'd take a look at that--our goal was to allow you to use the full power of Glide to do things like this without us having to implement everything.

peterhav commented 8 years ago

Hi @mcdonamp, this could be a work-around in some situations. Unfortunately is does not help in my case: the image selection (and crop) takes place in a dedicated activity. As soon as this activity is finished, the picture should be shown in several other activities/fragments.

kermitdefroghere commented 8 years ago

Not sure if this'll help but I handle the the scenario where an image is altered or changed in a dedicated activity by changing the filename which is also stored in the realtime database. (To ensure uniqueness I simply assign the filename to current time in millis.) Any db listener will notice the filename change and load the new image as Glide will not have a cache for the new file.

On Thu, Sep 29, 2016 at 10:01 AM, peterhav notifications@github.com wrote:

Hi @mcdonamp https://github.com/mcdonamp, this could be a work-around in some situations. Unfortunately is does not help in my case: the image selection (and crop) takes place in a dedicated activity. As soon as this activity is finished, the picture should be shown in several other activities/fragments.

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/firebase/FirebaseUI-Android/issues/331#issuecomment-250492569, or mute the thread https://github.com/notifications/unsubscribe-auth/AQPs6YoGwPylCujC1H5sURLJY308VhCAks5qu9LWgaJpZM4KIuLd .

samtstern commented 8 years ago

@peterhav @kermitdefroghere this is an interesting use case, I see how it's valuable. Looking around on Glide docs and forums it seems that they are resistant to allowing the cache to be modified directly, so there's no cache.put() operation we could easily call (if so, I'd do it).

However I think you could do this yourself by sub-classing FirebaseImageLoader to have a constructor that takes a @Nullable Uri.

Then you could do something like this:

public class MyFirebaseImageLoader extends FirebaseImageLoader {

    private Uri mUri;

    public MyFirebaseImageLoader(@Nullable Uri uri) {
        this.mUri = uri;
    }

    @Override
    public DataFetcher<InputStream> getResourceFetcher(StorageReference model, int width, int height) {
        return new MyFirebaseStorageFetcher(model);
    }

    private class MyFirebaseStorageFetcher extends FirebaseStorageFetcher {

        MyFirebaseStorageFetcher(StorageReference ref) {
            super(ref);
        }

        @Override
        public InputStream loadData(Priority priority) throws Exception {
            if (mUri != null) {
              // TODO(developer): Return inputStream from Uri
            } else {
              super.loadData(priority);
            }
        }
    }
}

Because the getId() value will still be based on the StorageReference, this will allow the Uri to populate the same cache entry.

samtstern commented 8 years ago

There's also this documentation on Glide cache invalidation, which you may need to do in cases where you do this trick but then the upload fails or something: https://github.com/bumptech/glide/wiki/Caching-and-Cache-Invalidation

peterhav commented 8 years ago

Hi @samtstern, I have tried this and it works well! The only thing is that FirebaseStorageFetcher is a private class that cannot be extended (I created a copy right now).

samtstern commented 8 years ago

@peterhav I figured that might be an issue. Glad it works with a copy, I will consider how we can solve this problem in a more elegant fashion for upcoming releases.

judeebene commented 8 years ago

hi @peterhav , Please would you mind sharing how you make it work with code in a gist, i am trying to do the same...

peterhav commented 8 years ago

Hi @judeebene the code I'm currently using is the following:

import android.net.Uri;
import android.support.annotation.Nullable;
import android.util.Log;

import com.bumptech.glide.Priority;
import com.bumptech.glide.load.data.DataFetcher;
import com.firebase.ui.storage.images.FirebaseImageLoader;
import com.google.android.gms.tasks.Tasks;
import com.google.firebase.storage.StorageReference;
import com.google.firebase.storage.StreamDownloadTask;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

class LocalCacheSupportingFirebaseImageLoader extends FirebaseImageLoader {
    private static final String TAG = LocalCacheSupportingFirebaseImageLoader.class.getName();

    private Uri mUri;

    LocalCacheSupportingFirebaseImageLoader(@Nullable Uri uri) {
        this.mUri = uri;
    }

    @Override
    public DataFetcher<InputStream> getResourceFetcher(StorageReference model, int width, int height) {
        return new MyFirebaseStorageFetcher(model);
    }

    private class MyFirebaseStorageFetcher extends FirebaseStorageFetcher {

        MyFirebaseStorageFetcher(StorageReference ref) {
            super(ref);
        }

        @Override
        public InputStream loadData(Priority priority) throws Exception {
            if (mUri != null) {
                File file = new File(mUri.getPath());
                return new FileInputStream(file);
            } else {
                return super.loadData(priority);
            }
        }
    }

    private class FirebaseStorageFetcher implements DataFetcher<InputStream> {

        private StorageReference mRef;
        private StreamDownloadTask mStreamTask;
        private InputStream mInputStream;

        FirebaseStorageFetcher(StorageReference ref) {
            mRef = ref;
        }

        @Override
        public InputStream loadData(Priority priority) throws Exception {
            mStreamTask = mRef.getStream();
            mInputStream = Tasks.await(mStreamTask).getStream();
            return mInputStream;
        }

        @Override
        public void cleanup() {
            // Close stream if possible
            if (mInputStream != null) {
                try {
                    mInputStream.close();
                    mInputStream = null;
                } catch (IOException e) {
                    Log.w(TAG, "Could not close stream", e);
                }
            }
        }

        @Override
        public String getId() {
            return mRef.getPath();
        }

        @Override
        public void cancel() {
            // Cancel task if possible
            if (mStreamTask != null && mStreamTask.isInProgress()) {
                mStreamTask.cancel();
            }
        }
    }
}

Note that this code is not yet being used operationally (nevertheless it seems to be working fine). I do not yet have a solution for the 'upload fail' scenario in place. If someone has a neat solution for this please let me know!

aterbo commented 7 years ago

@peterhav This issue becomes more critical when trying to deal with offline usage. My app follows almost exactly the same flow as the one described, but trying to account for offline usage. This app is often used by travelers to log data entries and photos, so it is expected to have long periods of offline usage.

When the app is online, the users gets an image using intents, the Database is updated with the path to the StorageReference of the photo, and the photo is uploaded to Storage and displayed using a FirebaseRecyclerAdapter.

However, when the app is offline, the photo is never uploaded to Storage and isn't accessible to the FirebaseRecyclerAdapter so only the placeholder is displayed.

Once the app reconnects to the internet and everything is finished uploading, the photos are visible.

@samtstern Is there a way the FirebaseImageLoader class modifications suggested above can be incorporated into FirebaseUI to more directly correct these issues?

guilmarc commented 7 years ago

I am on the same situation with my Android app.

Any updates on this issue ?

SUPERCILEX commented 6 years ago

I believe this issue can be closed in favor of https://github.com/firebase/FirebaseUI-Android/issues/993.