Assignment 13

Welcome to week eleven of Mobile Systems and Applications! (cs.upt.ro/~alext/msa/lab13)

Topics of discussion

  • Firebase Storage

  • Saving and reading images from Storage

  • Enabling data persistence with Picasso

Firebase Storage

Firebase Storage is built for app developers who need to store and serve user-generated content, such as photos or videos, and also adds Google security to file uploads and downloads for your Firebase apps, regardless of network quality. You can use it to store images, audio, video, or other user-generated content. Firebase Storage is backed by Google Cloud Storage, a powerful, simple, and cost-effective object storage service. To get started, make sure you read to online documentation.

Your files are stored in a Firebase Storage bucket. The files in this bucket are presented in a hierarchical structure, just like the file system on your local hard disk, or the data in the Firebase real-time database. By creating a reference to a file, your app gains access to it. These references can then be used to upload or download data, get or update metadata or delete the file. A reference can either point to a specific file or to a higher level in the hierarchy. Remember that the storage files have a different path than the database ones, and may also have different rules for writing and reading.

Creating a reference to a Storage instance enables you to upload, download, or delete a file, or to get or update its metadata. A reference can be thought of as a pointer to a file in the cloud. References are lightweight, so you can create as many as you need. They are also reusable for multiple operations. References are created using the FirebaseStorage singleton instance provided by the SDK by calling its getReferenceFromUrl method and passing in a URL of the form gs://<your-firebase-storage-bucket>. You can find this URL the Storage section of the Firebase console, e.g. gs://smart-wallet-4f214.appspot.com. An example of how to refer to an image stored in Storage, look at the code provided below.

// Points to the root reference

storageRef = storage.getReferenceFromUrl("gs://<your-bucket-name>");

// Points to "images"

imagesRef = storageRef.child("images");

// Points to "images/space.jpg"

// Note that you can use variables to create child values

String fileName = "space.jpg";

spaceRef = imagesRef.child(fileName);

// File path is "images/space.jpg"

String path = spaceRef.getPath();

If the application only requires reading images, we can add them manually at the expected path. In Figure 1, we add an icon file under the folder 'wallet' > current user uid > payment timestamp id. The uploaded icon has two URLs. One is an URL on which we can attach a storage listener to retrieve the file, the other is a global URL that can be used with third party libraries to retrieve the file (even a browser will show the image, if we are logged in with the UPT MSA user).

Fig.1.

Saving and reading images from Storage

First, to upload a file to Firebase Storage, we must create a reference to the full path of the file, including the file name. Once we have created an appropriate reference, we can call the putBytes(), putFile(), or putStream() method to upload the file to storage. Note that you cannot upload data with a reference to the root of the storage bucket. Your reference must point to a child URL.

// Create a storage reference from our app

FirebaseStorage storage = FirebaseStorage.getInstance();

StorageReference storageRef = storage.getReferenceFromUrl("gs://smart-wallet-4f214.appspot.com");

// Create a reference to "icon.jpg"

StorageReference iconRef = storageRef.child("icon.jpg");

// Create a reference to the full path of the icon

StorageReference iconPathRef = storageRef.child("wallet/<uid>/<timestamp>/icon.jpg");

// While the file names are the same, the references point to different files

iconRef.getName().equals(iconPathRef.getName()); // true

iconRef.getPath().equals(iconPathRef.getPath()); // false

The putBytes method is the simplest way to upload a file to storage. It takes a byte array and returns an UploadTask that you can use to manage and monitor the status of the upload.

// Get the data from an ImageView as bytes

iIcon.setDrawingCacheEnabled(true);

iIcon.buildDrawingCache();

Bitmap bitmap = iIcon.getDrawingCache();

ByteArrayOutputStream baos = new ByteArrayOutputStream();

bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);

byte[] data = baos.toByteArray();

UploadTask uploadTask = iconRef.putBytes(data);

uploadTask.addOnFailureListener(new OnFailureListener() {

@Override

public void onFailure(@NonNull Exception exception) {

// Handle unsuccessful uploads

}

}).addOnSuccessListener(new OnSuccessListener<UploadTask.TaskSnapshot>() {

@Override

public void onSuccess(UploadTask.TaskSnapshot taskSnapshot) {

// taskSnapshot.getMetadata() contains file metadata such as size, content-type, and download URL.

Uri downloadUrl = taskSnapshot.getDownloadUrl();

}

});

The putStream method is the most versatile way to upload a file to Firebase Storage. It takes an InputStream and returns an UploadTask that you can use to manage and monitor the status of the upload. Conversely, you can also upload local files on the device, such as photos and videos from the camera, with the putFile method. It takes a File and returns an UploadTask which you can use to manage and monitor the status of the upload. You can find all examples online here. Below is an example of using putFile.

Uri file = Uri.fromFile(new File("path/to/images/icon.jpg"));

StorageReference imagesRef = storageRef.child("wallet/<uid>/<timestamp>/" + file.getLastPathSegment());

uploadTask = imagesRef.putFile(file);

// Register observers to listen for when the download is done or if it fails

uploadTask.addOnFailureListener(new OnFailureListener() {

@Override

public void onFailure(@NonNull Exception exception) {

// Handle unsuccessful uploads

}

}).addOnSuccessListener(new OnSuccessListener<UploadTask.TaskSnapshot>() {

@Override

public void onSuccess(UploadTask.TaskSnapshot taskSnapshot) {

// taskSnapshot.getMetadata() contains file metadata such as size, content-type, and download URL.

Uri downloadUrl = taskSnapshot.getDownloadUrl();

}

});

Because upload tasks may be launched in offline mode, or they may simply take more time to finish than the user spends inside the application, there needs to be a way to save the UploadTasks and resume them when the user enters our app again. To handle these events please read the documentation, sections Handle Activity Lifecycle Changes and Continuing Uploads Across Process Restarts.

The standard procedure of downloading files is similar to the upload procedure, and is detailed in the online documentation. However, since most of the times we will be dealing with downloading image files for the purpose of loading them in specific image views, there are libraries designed especially for this, like Glide and Picasso. Both of them make code much lighter, by hiding the asynch callbacks behind their API.

An example of loading an icon into an image view using the standard way is given below.

StorageReference iconRef = storageRef.child("wallet/<uid>/<timestamp>/icon.jpg");

final long ONE_MEGABYTE = 1024 * 1024;

iconRef.getBytes(ONE_MEGABYTE).addOnSuccessListener(new OnSuccessListener<byte[]>() {

@Override

public void onSuccess(byte[] bytes) {

// Data for "icon.jpg" returns, use this as needed

iIcon.setImageDrawable(new BitmapDrawable(getResources(), BitmapFactory.decodeByteArray(bytes, 0, bytes.length)));

}

}).addOnFailureListener(new OnFailureListener() {

@Override

public void onFailure(@NonNull Exception exception) {

// Handle any errors

}

});

To load the same icon using Picasso, we only need the following code. However, it comes with a prerequisite. Picasso won't work with the internal URL of storage; it needs a publicly accessible URL. The URL of any uploaded file is retrieved once the UploadTask completes successfully, so we need to store that string in the real-time database, as a, additional member for each payment. See the example in Figure 2.

Picasso.with(this)

.load(payment.getIconUrl())

.into(iIcon);

Fig.2.

Enabling data persistence with Picasso

Coming back to the discussion we had on ensuring data persistence, we also need to enable the persistence of the images and other binary files from Firebase Storage. The SDK dows not offer any in-built support for this, so we will rely on the Picasso library for this important option. As such, we add a call to networkPolicy(NetworkPolicy.OFFLINE) to ensure data is written locally as well. This call also ensures that images are first sought after on the phone, rather than being re-downloaded from the server. In case a file is not found locally, we need to add an error callback to force Picasso to download the image from Storage. The complete code example is given below.

Picasso.with(this)

.load(payment.getIconUrl())

.networkPolicy(NetworkPolicy.OFFLINE)

.into(iIcon, new Callback() {

@Override

public void onSuccess() {

}

@Override

public void onError() {

//Try again online if cache failed

Picasso.with(getApplicationContext())

.load(payment.getIconUrl())

.error(R.drawable.ic_delete)

.into(iIcon, new Callback() {

@Override

public void onSuccess() {

}

@Override

public void onError() {

Log.v("Picasso", "Could not fetch image");

}

});

}

});

You can read an informative thread on working with Picasso.

Task #1

  • Based on the Smart Wallet application from last week, implement the functionality to upload and view icons (images) for each payment you add. This functionality will be available from the AddPaymentActivity only, as depicted in Figure 3. When the user taps on a payment, if there is an associated icon(URL), the image will be loaded from Storage. If there is no icon attached, then the image view will be swapped with a button allowing the user to add an image. An icon can be added from the phone gallery or by taking a new photo. This second use case is depicted in Figure 4.

    • Fig.3.

      • Fig.4.

  • First, we will implement loading an image file for a selected payment. To begin with, let's debug the app by staying logged in with the user a@a.com (he should have at least one payment for November and one for December). For this user, pick an existing payment from the real-time database and search for an icon online. For example, there is a payment called pizza in November, so search for a small pizza image on Google.

  • Once you have the image, we should use a standard naming procedure, that is, every payment which has an icon should store it under the path 'wallet/<uid>/<timestamp>/icon.jpg'. As such, rename your pizza file to icon.jpg. An exact path for the file could be 'wallet/qY4RoQ9EtWPYAJy2crks5g2YvK32/2016-11-24 17:33:05/icon.jpg'.

  • Go into the Firebase console in the Storage tab. There you will find a wallet folder already created. Inside it, create a folder for the user id you are using (it might be already created). Inside that folder, add a folder for the payment id (timestamp) you want to add an icon to. Inside the payment timestamp folder, upload the icon.jpg file. The result should look very similar to Figure 1.

  • Notice that if you click the icon you just uploaded, there will be a panel on the right side, like in Figure 1. From there you may copy the URL of the image file. The URL should start with "https://firebasestorage.googleapis.com...". To test that it works, just paste it into your browser and the image should be loaded.

  • With the correct URL in your clipboard, go to the Database tab in Firebase, navigate to the corresponding wallet/user/payment and add a new field called iconUrl, like depicted in Figure 2.

  • How you have successfully linked a payment object from the real-time database with an image file stored in Firebase Storage.

  • Next, we need to programatically load the image using Picasso. First, add the Storage and Picasso dependencies, and some code to handle offline data persistence. The code below need to be added in the onCreate method of the AppState which must extend Application, and be declared as the application in your manifest file (android:name=".model.AppState")

                • compile 'com.google.firebase:firebase-storage:9.4.0'

                • compile 'com.squareup.picasso:picasso:2.5.2'

                • compile 'com.squareup.okhttp:okhttp:2.4.0'

                  • // offline persitence of data

                  • FirebaseDatabase.getInstance().setPersistenceEnabled(true);

                  • // offline persistence of images from storage

                  • Picasso.Builder builder = new Picasso.Builder(this);

                  • builder.downloader(new OkHttpDownloader(this, Integer.MAX_VALUE));

                  • Picasso built = builder.build();

                  • built.setIndicatorsEnabled(true);

                  • built.setLoggingEnabled(true);

                  • Picasso.setSingletonInstance(built);

  • Inside the AddPaymentActivity xml file, add an image view and a button.

                • <ImageView

                • android:id="@+id/iIcon"

                • android:layout_width="128dp"

                • android:layout_height="128dp"

                • android:layout_gravity="center_horizontal"

                • android:background="@color/grey_100"

                • android:contentDescription="Icon"

                • android:scaleType="fitXY"/>

                • <Button

                • android:id="@+id/bAddPhoto"

                • android:layout_width="wrap_content"

                • android:layout_height="wrap_content"

                • android:layout_gravity="center_horizontal"

                • android:onClick="clicked"

                • android:padding="12dp"

                • android:text="Add Photo"/>

  • The image view iIcon will be used to display the image, if an iconUrl is found for the currently selected payment. If the URL is null, then we will hide the image view and show a button for adding a new icon. The main logic to be added is given below. Of course, don't forget to declare your new views and instantiate them in onCreate.

                • if (payment.getIconUrl() != null) {

                • bAddPhoto.setVisibility(View.GONE);

                • loadIcon();

                • } else {

                • iIcon.setVisibility(View.GONE);

                • }

  • Finally, the loadIcon method is the one responsible for loading our image from Storage. The necessary code, working both offline and online, is given below.

                • private void loadIcon() {

                • Picasso.with(this)

                • .load(payment.getIconUrl())

                • .networkPolicy(NetworkPolicy.OFFLINE)

                • .into(iIcon, new Callback() {

                • @Override

                • public void onSuccess() {

                • }

                • @Override

                • public void onError() {

                • //Try again online if cache failed

                • Picasso.with(getApplicationContext())

                • .load(payment.getIconUrl())

                • .error(R.drawable.ic_error)

                • .into(iIcon, new Callback() {

                • @Override

                • public void onSuccess() {

                • }

                • @Override

                • public void onError() {

                • Log.v("Picasso", "Could not fetch image");

                • }

                • });

                • }

                • });

                • }

  • With these steps implemented, the app should load the icon you added manually for the pizza payment for the user a@a.com.

Task #2

  • Next we need to enable the addition of payment icons through our app. For this, we need to load an image from gallery or take a photo. The code is explained in this tutorial (Note: strangely, Google Chrome seems to have problems loading this page. I had better results with Microsoft Edge).

  • Homework: Finish implementing both methods for adding icons to payments. Don't forget to save to storage URL in the real-time database as well, so you can pass it to Picasso. Plus two activity (+2) points and a golden star (+1*) for the homework. Homework may only be presented individually, on your smartphone or emulator, with the code running on your PC. Golden stars have very beneficial properties.

Have a fruitful week.

Lychee fruit

Lychees are brightly colored strawberry shaped fruits with thin large skin and sweet musky aroma. It is fairly easy to cover off the skin, with an engaging smelling white, revealing a clear flesh. It has a unique sweet, admirable taste with a half level acidic, pungent flavor. Tastes like unicorn. The seed in the middle can be of tiny shape, but it can be easily taken out. The sensation while eating is delightful but it can have some unpleasant effects on your digestive system.

http://www.fruitsinfo.com/Lychee-Exotic-fruits.php